;; 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))) )