From 8029f8b60c8cdedf14df4908609024f50ae65bac Mon Sep 17 00:00:00 2001 From: Ben Sturmfels Date: Wed, 6 Mar 2024 15:23:27 +1100 Subject: [PATCH] Fix error handling, add CLI option to assign employee to project Project assignment was previously hard-coded. --- src/core.clj | 47 +++++++++++++++++++++++++++----------------- src/import.clj | 28 +++++++++++++------------- test/import_test.clj | 8 ++++---- 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/core.clj b/src/core.clj index 784c957..147639e 100644 --- a/src/core.clj +++ b/src/core.clj @@ -3,7 +3,8 @@ (ns core "Beancount importer for Paychex Pay Item Details report." - (:require [clojure.tools.cli :refer [parse-opts]] + (:require [clojure.string :as str] + [clojure.tools.cli :refer [parse-opts]] [import :as import]) (:gen-class)) @@ -27,22 +28,33 @@ :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 -main [& args] - (let [{:keys [options _arguments _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]} options] + (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 data) - (import/individual-taxes date period pay-receipt-no retirement-invoice-no data) - (import/employer-taxes date period pay-receipt-no data) - (import/payroll-fees date period fees-receipt-no fees-invoice-no total-fees data) + 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))] (doseq [i imported] - (println (import/render-transaction i)))))) + (println (import/render-transaction i)))))) (comment ;; Examples to exercise the importer during development. @@ -55,11 +67,12 @@ (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" data) - (import/individual-taxes "2023-12-29" "December 2023" "rt:19462/674660" "rt:19403/675431" data) - (import/employer-taxes "2023-12-29" "December 2023" "rt:19462/674660" data) - (import/payroll-fees "2023-12-29" "December 2023" "rt:19459/675387" "rt:19459/674887" 206.50M data) + (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)) @@ -67,13 +80,11 @@ (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" data) - (import/individual-taxes "2024-01-31" "January 2024" "rt:19462/685751" "rt:19403/685602" data) - (import/employer-taxes "2024-01-31" "January 2024" "rt:19462/685751" data) - (import/payroll-fees "2024-01-31" "January 2024" "rt:19459/675387" "rt:19459/674887" 206.50M data) + (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))) - - ;; Compare hand-written transactions with imported (ignoring ordering). (dd/pretty-print (dd/diff (sort-postings (parse examples/jan-2024)) diff --git a/src/import.clj b/src/import.clj index a203fcd..427637a 100644 --- a/src/import.clj +++ b/src/import.clj @@ -60,9 +60,9 @@ "Conditionally adds a specific project tag to metadata. Some employees need to be tagged with a particular project to override the default \"Conservancy\" project." - [name m] - (if (= name "Sharp-Sage-A") - (assoc m :project "Outreachy") + [projects name m] + (if (contains? projects name) + (assoc m :project (get projects name)) m)) (defn- split-fee @@ -121,7 +121,7 @@ (defn payroll "Return a net pay transaction." - [date period receipt-no data] + [date period receipt-no projects data] (let [postings (for [[name records] (group-by :name data)] (let [total-net-pay (->> records (filter #(= (:type %) "Net Pay")) @@ -135,7 +135,7 @@ [{:account "Expenses:Payroll:Salary" :amount actual-total-net-pay :currency "USD" - :meta (assoc-project name {:entity name})} + :meta (assoc-project projects name {:entity name})} {:account "Assets:FR:Check2721" :amount (- actual-total-net-pay) :currency "USD" @@ -143,11 +143,11 @@ {:account "Expenses:Hosting" :amount total-reimbursement :currency "USD" - :meta (assoc-project name {:entity name :payroll-type "US:Reimbursement"})} + :meta (assoc-project projects name {:entity name :payroll-type "US:Reimbursement"})} {:account "Assets:FR:Check2721" :amount (- total-reimbursement) :currency "USD" - :meta (assoc-project name {:entity name :tax-implication "Reimbursement"})}]))] + :meta (assoc-project projects name {:entity name :tax-implication "Reimbursement"})}]))] [{:date date :desc (format "Monthly Payroll - %s - Net Pay" period) :meta {:program "Conservancy:Payroll" :project "Conservancy" @@ -159,7 +159,7 @@ (defn individual-taxes "Return a transaction of expenses/witholding for each employee." - [date period receipt-no invoice-no data] + [date period receipt-no invoice-no projects data] (for [[name records] (group-by :name data)] (let [retirement-lines (filter #(= (:type %) "Retirement") records) witholding-lines (filter #(= (:type %) "Withholding") records) @@ -205,7 +205,7 @@ :amount (reduce + 0M (map :amount insurance-lines)) :currency "USD"}]] {:date date :desc (format "Monthly Payroll - %s - TAXES - %s" period name) - :meta (assoc-project name {:project "Conservancy" + :meta (assoc-project projects name {:project "Conservancy" :program "Conservancy:Payroll" :entity name :receipt receipt-no @@ -220,7 +220,7 @@ (defn employer-taxes "Return an employer taxes transaction." - [date period receipt-no data] + [date period receipt-no projects data] (let [liability-lines (filter (fn [{:keys [category type]}] (and (= type "Liability") (not (str/includes? category "Unemploy")))) data) @@ -230,7 +230,7 @@ :currency "USD" ;; Use eg. "US:Medicare", not "US:Tax:Medicare" as ;; in individual-taxes as confirmed by Rosanne. - :meta (assoc-project + :meta (assoc-project projects name {:entity name :payroll-type (str/replace (cat->payroll-type category) "Tax:" "")})}) @@ -242,7 +242,7 @@ {:account "Expenses:Payroll:Tax" :amount amount :currency "USD" - :meta (assoc-project + :meta (assoc-project projects name {:entity (first (str/split category #" ")) :memo name ; distinguishes multiple employees in one state @@ -262,7 +262,7 @@ (defn payroll-fees "Return a payroll fees transaction." - [date period receipt-no invoice-no total-fees data] + [date period receipt-no invoice-no total-fees projects data] (let [employees (distinct (map :name data)) exact-fee-allocation (split-fee total-fees (count employees)) employee-fees (map vector employees exact-fee-allocation) @@ -270,7 +270,7 @@ {:account "Expenses:Payroll:Fees" :amount fee :currency "USD" - :meta (assoc-project name {:entity name})}) + :meta (assoc-project projects name {:entity name})}) asset-postings [{:account "Assets:FR:Check2721" :amount (- total-fees) :currency "USD"}]] diff --git a/test/import_test.clj b/test/import_test.clj index a839c5f..387d292 100644 --- a/test/import_test.clj +++ b/test/import_test.clj @@ -9,7 +9,7 @@ (deftest payroll (let [data (import/read-csv (clojure.java.io/resource "example-paychex-pay-item-details.csv")) - actual (import/payroll "DATE" "PERIOD" "TODO-PAY-RECEIPT" data) + actual (import/payroll "DATE" "PERIOD" "TODO-PAY-RECEIPT" {} data) expected '[{:date "DATE" :desc "Monthly Payroll - PERIOD - Net Pay" :meta @@ -57,7 +57,7 @@ (deftest individual-taxes (let [data (import/read-csv (clojure.java.io/resource "example-paychex-pay-item-details.csv")) - actual (import/individual-taxes "DATE" "PERIOD" "TODO-PAY-RECEIPT" "TODO-RETIREMENT-INVOICE" data) + actual (import/individual-taxes "DATE" "PERIOD" "TODO-PAY-RECEIPT" "TODO-RETIREMENT-INVOICE" {} data) expected '({:date "DATE" :desc "Monthly Payroll - PERIOD - TAXES - Citizen-Jack" :meta @@ -152,7 +152,7 @@ (deftest employer-taxes (let [data (import/read-csv (clojure.java.io/resource "example-paychex-pay-item-details.csv")) - actual (import/employer-taxes "DATE" "PERIOD" "TODO-PAY-RECEIPT" data) + actual (import/employer-taxes "DATE" "PERIOD" "TODO-PAY-RECEIPT" {} data) expected '[{:date "DATE" :desc "Monthly Payroll - PERIOD - TAXES - Employer" :meta @@ -223,7 +223,7 @@ (deftest payroll-fees (let [data (import/read-csv (clojure.java.io/resource "example-paychex-pay-item-details.csv")) - actual (import/payroll-fees "DATE" "PERIOD" "TODO-FEES-RECEIPT" "TODO-FEES-INVOICE" 206.51 data) + actual (import/payroll-fees "DATE" "PERIOD" "TODO-FEES-RECEIPT" "TODO-FEES-INVOICE" 206.51 {} data) expected '[{:date "DATE" :payee "Paychex" :desc "Monthly Payroll - PERIOD - Fee"