Fix error handling, add CLI option to assign employee to project
Project assignment was previously hard-coded.
This commit is contained in:
parent
f9d8ea9e45
commit
8029f8b60c
3 changed files with 47 additions and 36 deletions
47
src/core.clj
47
src/core.clj
|
@ -3,7 +3,8 @@
|
||||||
|
|
||||||
(ns core
|
(ns core
|
||||||
"Beancount importer for Paychex Pay Item Details report."
|
"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])
|
[import :as import])
|
||||||
(:gen-class))
|
(:gen-class))
|
||||||
|
|
||||||
|
@ -27,22 +28,33 @@
|
||||||
:default "TODO-RETIREMENT-RECEIPT"]
|
:default "TODO-RETIREMENT-RECEIPT"]
|
||||||
[nil "--retirement-invoice-no REFERENCE" "Retirement receipt number, eg. \"rt:111/222\""
|
[nil "--retirement-invoice-no REFERENCE" "Retirement receipt number, eg. \"rt:111/222\""
|
||||||
:default "TODO-RETIREMENT-INVOICE"]
|
: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"]])
|
["-h" "--help"]])
|
||||||
|
|
||||||
(defn -main [& args]
|
(defn -main [& args]
|
||||||
(let [{:keys [options _arguments _errors summary]} (parse-opts args cli-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]} 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)
|
(when (:help options)
|
||||||
(println summary)
|
(println summary)
|
||||||
(System/exit 0))
|
(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))
|
(let [data (import/read-csv (:csv options))
|
||||||
imported (concat (import/payroll date period pay-receipt-no data)
|
imported (concat (import/payroll date period pay-receipt-no project data)
|
||||||
(import/individual-taxes date period pay-receipt-no retirement-invoice-no data)
|
(import/individual-taxes date period pay-receipt-no retirement-invoice-no project data)
|
||||||
(import/employer-taxes date period pay-receipt-no data)
|
(import/employer-taxes date period pay-receipt-no project data)
|
||||||
(import/payroll-fees date period fees-receipt-no fees-invoice-no total-fees 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))]
|
(import/retirement date period retirement-receipt-no retirement-invoice-no data))]
|
||||||
(doseq [i imported]
|
(doseq [i imported]
|
||||||
(println (import/render-transaction i))))))
|
(println (import/render-transaction i))))))
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
;; Examples to exercise the importer during development.
|
;; 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 data (import/read-csv "/home/ben/Downloads/2023-12-27_Pay-Item-Details_2023-12-2.csv"))
|
||||||
(def imported
|
(def imported
|
||||||
(concat (import/payroll "2023-12-29" "December 2023" "rt:19462/674660" 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" 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" 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 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)))
|
(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/pretty-print
|
||||||
(dd/diff
|
(dd/diff
|
||||||
(sort-postings (parse examples/dec-2023))
|
(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 data (import/read-csv "/home/ben/Downloads/2024-01-29_Pay-Item-Details_2024-01.csv"))
|
||||||
(def imported
|
(def imported
|
||||||
(concat (import/payroll "2024-01-31" "January 2024" "rt:19462/685751" 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" 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" 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 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)))
|
(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/pretty-print
|
||||||
(dd/diff
|
(dd/diff
|
||||||
(sort-postings (parse examples/jan-2024))
|
(sort-postings (parse examples/jan-2024))
|
||||||
|
|
|
@ -60,9 +60,9 @@
|
||||||
"Conditionally adds a specific project tag to metadata.
|
"Conditionally adds a specific project tag to metadata.
|
||||||
Some employees need to be tagged with a particular project to override the
|
Some employees need to be tagged with a particular project to override the
|
||||||
default \"Conservancy\" project."
|
default \"Conservancy\" project."
|
||||||
[name m]
|
[projects name m]
|
||||||
(if (= name "Sharp-Sage-A")
|
(if (contains? projects name)
|
||||||
(assoc m :project "Outreachy")
|
(assoc m :project (get projects name))
|
||||||
m))
|
m))
|
||||||
|
|
||||||
(defn- split-fee
|
(defn- split-fee
|
||||||
|
@ -121,7 +121,7 @@
|
||||||
|
|
||||||
(defn payroll
|
(defn payroll
|
||||||
"Return a net pay transaction."
|
"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 [postings (for [[name records] (group-by :name data)]
|
||||||
(let [total-net-pay (->> records
|
(let [total-net-pay (->> records
|
||||||
(filter #(= (:type %) "Net Pay"))
|
(filter #(= (:type %) "Net Pay"))
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
[{:account "Expenses:Payroll:Salary"
|
[{:account "Expenses:Payroll:Salary"
|
||||||
:amount actual-total-net-pay
|
:amount actual-total-net-pay
|
||||||
:currency "USD"
|
:currency "USD"
|
||||||
:meta (assoc-project name {:entity name})}
|
:meta (assoc-project projects name {:entity name})}
|
||||||
{:account "Assets:FR:Check2721"
|
{:account "Assets:FR:Check2721"
|
||||||
:amount (- actual-total-net-pay)
|
:amount (- actual-total-net-pay)
|
||||||
:currency "USD"
|
:currency "USD"
|
||||||
|
@ -143,11 +143,11 @@
|
||||||
{:account "Expenses:Hosting"
|
{:account "Expenses:Hosting"
|
||||||
:amount total-reimbursement
|
:amount total-reimbursement
|
||||||
:currency "USD"
|
: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"
|
{:account "Assets:FR:Check2721"
|
||||||
:amount (- total-reimbursement)
|
:amount (- total-reimbursement)
|
||||||
:currency "USD"
|
: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)
|
[{:date date :desc (format "Monthly Payroll - %s - Net Pay" period)
|
||||||
:meta {:program "Conservancy:Payroll"
|
:meta {:program "Conservancy:Payroll"
|
||||||
:project "Conservancy"
|
:project "Conservancy"
|
||||||
|
@ -159,7 +159,7 @@
|
||||||
|
|
||||||
(defn individual-taxes
|
(defn individual-taxes
|
||||||
"Return a transaction of expenses/witholding for each employee."
|
"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)]
|
(for [[name records] (group-by :name data)]
|
||||||
(let [retirement-lines (filter #(= (:type %) "Retirement") records)
|
(let [retirement-lines (filter #(= (:type %) "Retirement") records)
|
||||||
witholding-lines (filter #(= (:type %) "Withholding") records)
|
witholding-lines (filter #(= (:type %) "Withholding") records)
|
||||||
|
@ -205,7 +205,7 @@
|
||||||
:amount (reduce + 0M (map :amount insurance-lines))
|
:amount (reduce + 0M (map :amount insurance-lines))
|
||||||
:currency "USD"}]]
|
:currency "USD"}]]
|
||||||
{:date date :desc (format "Monthly Payroll - %s - TAXES - %s" period name)
|
{: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"
|
:program "Conservancy:Payroll"
|
||||||
:entity name
|
:entity name
|
||||||
:receipt receipt-no
|
:receipt receipt-no
|
||||||
|
@ -220,7 +220,7 @@
|
||||||
|
|
||||||
(defn employer-taxes
|
(defn employer-taxes
|
||||||
"Return an employer taxes transaction."
|
"Return an employer taxes transaction."
|
||||||
[date period receipt-no data]
|
[date period receipt-no projects data]
|
||||||
(let [liability-lines (filter (fn [{:keys [category type]}]
|
(let [liability-lines (filter (fn [{:keys [category type]}]
|
||||||
(and (= type "Liability")
|
(and (= type "Liability")
|
||||||
(not (str/includes? category "Unemploy")))) data)
|
(not (str/includes? category "Unemploy")))) data)
|
||||||
|
@ -230,7 +230,7 @@
|
||||||
:currency "USD"
|
:currency "USD"
|
||||||
;; Use eg. "US:Medicare", not "US:Tax:Medicare" as
|
;; Use eg. "US:Medicare", not "US:Tax:Medicare" as
|
||||||
;; in individual-taxes as confirmed by Rosanne.
|
;; in individual-taxes as confirmed by Rosanne.
|
||||||
:meta (assoc-project
|
:meta (assoc-project projects
|
||||||
name
|
name
|
||||||
{:entity name
|
{:entity name
|
||||||
:payroll-type (str/replace (cat->payroll-type category) "Tax:" "")})})
|
:payroll-type (str/replace (cat->payroll-type category) "Tax:" "")})})
|
||||||
|
@ -242,7 +242,7 @@
|
||||||
{:account "Expenses:Payroll:Tax"
|
{:account "Expenses:Payroll:Tax"
|
||||||
:amount amount
|
:amount amount
|
||||||
:currency "USD"
|
:currency "USD"
|
||||||
:meta (assoc-project
|
:meta (assoc-project projects
|
||||||
name
|
name
|
||||||
{:entity (first (str/split category #" "))
|
{:entity (first (str/split category #" "))
|
||||||
:memo name ; distinguishes multiple employees in one state
|
:memo name ; distinguishes multiple employees in one state
|
||||||
|
@ -262,7 +262,7 @@
|
||||||
|
|
||||||
(defn payroll-fees
|
(defn payroll-fees
|
||||||
"Return a payroll fees transaction."
|
"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))
|
(let [employees (distinct (map :name data))
|
||||||
exact-fee-allocation (split-fee total-fees (count employees))
|
exact-fee-allocation (split-fee total-fees (count employees))
|
||||||
employee-fees (map vector employees exact-fee-allocation)
|
employee-fees (map vector employees exact-fee-allocation)
|
||||||
|
@ -270,7 +270,7 @@
|
||||||
{:account "Expenses:Payroll:Fees"
|
{:account "Expenses:Payroll:Fees"
|
||||||
:amount fee
|
:amount fee
|
||||||
:currency "USD"
|
:currency "USD"
|
||||||
:meta (assoc-project name {:entity name})})
|
:meta (assoc-project projects name {:entity name})})
|
||||||
asset-postings [{:account "Assets:FR:Check2721"
|
asset-postings [{:account "Assets:FR:Check2721"
|
||||||
:amount (- total-fees)
|
:amount (- total-fees)
|
||||||
:currency "USD"}]]
|
:currency "USD"}]]
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
(deftest payroll
|
(deftest payroll
|
||||||
(let [data (import/read-csv (clojure.java.io/resource "example-paychex-pay-item-details.csv"))
|
(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"
|
expected '[{:date "DATE"
|
||||||
:desc "Monthly Payroll - PERIOD - Net Pay"
|
:desc "Monthly Payroll - PERIOD - Net Pay"
|
||||||
:meta
|
:meta
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
|
|
||||||
(deftest individual-taxes
|
(deftest individual-taxes
|
||||||
(let [data (import/read-csv (clojure.java.io/resource "example-paychex-pay-item-details.csv"))
|
(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"
|
expected '({:date "DATE"
|
||||||
:desc "Monthly Payroll - PERIOD - TAXES - Citizen-Jack"
|
:desc "Monthly Payroll - PERIOD - TAXES - Citizen-Jack"
|
||||||
:meta
|
:meta
|
||||||
|
@ -152,7 +152,7 @@
|
||||||
|
|
||||||
(deftest employer-taxes
|
(deftest employer-taxes
|
||||||
(let [data (import/read-csv (clojure.java.io/resource "example-paychex-pay-item-details.csv"))
|
(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"
|
expected '[{:date "DATE"
|
||||||
:desc "Monthly Payroll - PERIOD - TAXES - Employer"
|
:desc "Monthly Payroll - PERIOD - TAXES - Employer"
|
||||||
:meta
|
:meta
|
||||||
|
@ -223,7 +223,7 @@
|
||||||
|
|
||||||
(deftest payroll-fees
|
(deftest payroll-fees
|
||||||
(let [data (import/read-csv (clojure.java.io/resource "example-paychex-pay-item-details.csv"))
|
(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"
|
expected '[{:date "DATE"
|
||||||
:payee "Paychex"
|
:payee "Paychex"
|
||||||
:desc "Monthly Payroll - PERIOD - Fee"
|
:desc "Monthly Payroll - PERIOD - Fee"
|
||||||
|
|
Loading…
Reference in a new issue