103 lines
5 KiB
Clojure
103 lines
5 KiB
Clojure
;; Copyright 2024 Ben Sturmfels
|
|
;; License: GPLv3-or-later
|
|
|
|
(ns core
|
|
"Beancount importer for Paychex Pay Item Details report."
|
|
(:require [clojure.set :as set]
|
|
[clojure.string :as str]
|
|
[clojure.tools.cli :refer [parse-opts]]
|
|
[import :as import])
|
|
(:gen-class))
|
|
|
|
(def cli-options
|
|
[[nil "--csv FILE" "Pay Item Details CSV report"]
|
|
[nil "--date DATE" "Date used for the transactions (YYYY-MM-DD)"
|
|
:validate [#(re-matches #"\d{4}-\d{2}-\d{2}" %) "Must be of format YYYY-MM-DD"]
|
|
:default "TODO-DATE"]
|
|
[nil "--period PERIOD" "Month/year covered by the pay run eg. \"December 2023\""
|
|
:default "TODO-PERIOD"]
|
|
[nil "--total-fees NUM" "Total fee charged by Paychex, eg. \"206.50\""
|
|
:parse-fn bigdec
|
|
:default 0M]
|
|
[nil "--pay-receipt-no REFERENCE" "Payroll receipt number, eg. \"rt:111/222\""
|
|
:default "TODO-PAY-RECEIPT"]
|
|
[nil "--pay-invoice-no REFERENCE" "Payroll invoice number, eg. \"rt:111/222\""
|
|
:default "TODO-PAY-INVOICE"]
|
|
[nil "--fees-receipt-no REFERENCE" "Paychex fees receipt number, eg. \"rt:111/222\""
|
|
:default "TODO-FEES-RECEIPT"]
|
|
[nil "--fees-invoice-no REFERENCE" "Paychex fees invoice number, eg. \"rt:111/222\""
|
|
:default "TODO-FEES-INVOICE"]
|
|
[nil "--retirement-receipt-no REFERENCE" "Retirement receipt number, eg. \"rt:111/222\""
|
|
:default "TODO-RETIREMENT-RECEIPT"]
|
|
[nil "--retirement-invoice-no REFERENCE" "Retirement receipt number, eg. \"rt:111/222\""
|
|
:default "TODO-RETIREMENT-INVOICE"]
|
|
[nil "--project EMPLOYEE:PROJECT" "Allocate an employee to a specific project, eg. \"Doe-Jane:Outreachy\". Use once for each employee."
|
|
:multi true
|
|
:validate [#(= 2 (count %)) "Must be of the form \"name:project\""]
|
|
:parse-fn #(str/split % #":")
|
|
:default {}
|
|
:assoc-fn (fn [m k [name proj]] (assoc-in m [k name] proj))]
|
|
["-h" "--help"]])
|
|
|
|
(defn unmatched-employees
|
|
"Identify any mismatches between employees in the pay run and --project employee allocations."
|
|
[records projects]
|
|
(set/difference (set (keys projects)) (->> records (map :name) set)))
|
|
|
|
(defn -main [& args]
|
|
(let [{:keys [options errors summary]} (parse-opts args cli-options)
|
|
{:keys [date period pay-receipt-no pay-invoice-no total-fees fees-receipt-no fees-invoice-no retirement-receipt-no retirement-invoice-no project]} options]
|
|
(when (:help options)
|
|
(println summary)
|
|
(System/exit 0))
|
|
(when errors
|
|
(println
|
|
(str "The following errors occurred while parsing your command:\n\n"
|
|
(str/join \newline errors)))
|
|
(System/exit 1))
|
|
(let [records (import/read-csv (:csv options))
|
|
imported (concat (import/net-pay date period pay-invoice-no project records)
|
|
(import/individual-taxes date period pay-invoice-no retirement-invoice-no project records)
|
|
(import/employer-taxes date period pay-invoice-no project records)
|
|
(import/net-pay-ach-debit date period pay-receipt-no pay-invoice-no {} records)
|
|
(import/taxes-ach-debit date period pay-receipt-no pay-invoice-no {} records)
|
|
(import/fees date period fees-receipt-no fees-invoice-no total-fees project records)
|
|
(import/retirement date period retirement-receipt-no retirement-invoice-no records))
|
|
unmatched (unmatched-employees records project)]
|
|
(when-not (empty? unmatched)
|
|
(println
|
|
(str "Could not find these employees in the payroll:\n\n"
|
|
(str/join ", " unmatched)))
|
|
(System/exit 1))
|
|
(doseq [i imported]
|
|
(println (import/render-transaction i))))))
|
|
|
|
(comment
|
|
;; Examples to exercise the importer during development.
|
|
(require
|
|
'[parse :refer [parse sort-postings]]
|
|
'[lambdaisland.deep-diff2 :as dd])
|
|
|
|
;; These examples are not included with the code for privacy reasons.
|
|
(require '[examples])
|
|
|
|
(def records (import/read-csv "/home/ben/Downloads/2024-01-29_Pay-Item-Details_2024-01.csv"))
|
|
(def imported
|
|
(concat (import/net-pay "2024-01-31" "January 2024" "rt:19462/685751" {} records)
|
|
(import/individual-taxes "2024-01-31" "January 2024" "rt:19462/685751" "rt:19403/685602" {} records)
|
|
(import/employer-taxes "2024-01-31" "January 2024" "rt:19462/685751" {} records)
|
|
(import/net-pay-ach-debit "2024-01-31" "January 2024" "TODO-PAY-RECEIPT" "TODO-PAY-INVOICE" {} records)
|
|
(import/taxes-ach-debit "2024-01-31" "January 2024" "TODO-PAY-RECEIPT" "TODO-PAY-INVOICE" {} records)
|
|
(import/fees "2024-01-31" "January 2024" "rt:19459/675387" "rt:19459/674887" 206.50M {} records)
|
|
(import/retirement "2024-01-31" "January 2024" "rt:19403/685929" "rt:19403/685602" records)))
|
|
;; Compare hand-written transactions with imported (ignoring ordering).
|
|
(dd/pretty-print
|
|
(dd/diff
|
|
(sort-postings (parse examples/jan-2024-with-liabilities))
|
|
(sort-postings imported)))
|
|
|
|
;; Print out text transactions
|
|
(doseq [i imported]
|
|
(println (import/render-transaction i)))
|
|
|
|
)
|