;; 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 "--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." [data projects] (set/difference (set (keys projects)) (->> data (map :name) set))) (defn -main [& args] (let [{:keys [options errors summary]} (parse-opts args cli-options) {:keys [date period pay-receipt-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 [data (import/read-csv (:csv options)) imported (concat (import/payroll date period pay-receipt-no project data) (import/individual-taxes date period pay-receipt-no retirement-invoice-no project data) (import/employer-taxes date period pay-receipt-no project data) (import/payroll-fees date period fees-receipt-no fees-invoice-no total-fees project data) (import/retirement date period retirement-receipt-no retirement-invoice-no data)) unmatched (unmatched-employees data 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 data (import/read-csv "/home/ben/Downloads/2023-12-27_Pay-Item-Details_2023-12-2.csv")) (def imported (concat (import/payroll "2023-12-29" "December 2023" "rt:19462/674660" {"Sharp-Sage-A" "Outreachy"} data) (import/individual-taxes "2023-12-29" "December 2023" "rt:19462/674660" "rt:19403/675431" {"Sharp-Sage-A" "Outreachy"} data) (import/employer-taxes "2023-12-29" "December 2023" "rt:19462/674660" {"Sharp-Sage-A" "Outreachy"} data) (import/payroll-fees "2023-12-29" "December 2023" "rt:19459/675387" "rt:19459/674887" 206.50M {"Sharp-Sage-A" "Outreachy"} data) (import/retirement "2024-01-02" "December 2023" "rt:19403/676724" "rt:19403/675431" data))) ;; Compare hand-written transactions with imported (ignoring ordering). (dd/pretty-print (dd/diff (sort-postings (parse examples/dec-2023)) (sort-postings imported))) (def data (import/read-csv "/home/ben/Downloads/2024-01-29_Pay-Item-Details_2024-01.csv")) (def imported (concat (import/payroll "2024-01-31" "January 2024" "rt:19462/685751" {"Sharp-Sage-A" "Outreachy"} data) (import/individual-taxes "2024-01-31" "January 2024" "rt:19462/685751" "rt:19403/685602" {"Sharp-Sage-A" "Outreachy"} data) (import/employer-taxes "2024-01-31" "January 2024" "rt:19462/685751" {"Sharp-Sage-A" "Outreachy"} data) (import/payroll-fees "2024-01-31" "January 2024" "rt:19459/675387" "rt:19459/674887" 206.50M {"Sharp-Sage-A" "Outreachy"} data) (import/retirement "2024-01-31" "January 2024" "rt:19403/685929" "rt:19403/685602" data))) (dd/pretty-print (dd/diff (sort-postings (parse examples/jan-2024)) (sort-postings imported))) ;; Print out text transactions (doseq [i imported] (println (import/render-transaction i))) )