Add CLI, build script, fees importer and retirement importer
This commit is contained in:
parent
c57b842c49
commit
18eb076df0
4 changed files with 133 additions and 42 deletions
7
deps.edn
7
deps.edn
|
@ -1,4 +1,9 @@
|
||||||
{:deps {
|
{:deps {
|
||||||
org.clojure/clojure {:mvn/version "1.11.1"}
|
org.clojure/clojure {:mvn/version "1.11.1"}
|
||||||
org.clojure/data.csv {:mvn/version "1.0.1"}
|
org.clojure/data.csv {:mvn/version "1.0.1"}
|
||||||
lambdaisland/deep-diff2 {:mvn/version "2.10.211"}}}
|
org.clojure/tools.cli {:mvn/version "1.1.230"}
|
||||||
|
lambdaisland/deep-diff2 {:mvn/version "2.10.211"}}
|
||||||
|
:aliases
|
||||||
|
{;; Run with clj -T:build function-in-build
|
||||||
|
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.6"}}
|
||||||
|
:ns-default build}}}
|
||||||
|
|
67
src/core.clj
67
src/core.clj
|
@ -1,26 +1,68 @@
|
||||||
(ns core
|
(ns core
|
||||||
(:require [import :as import]
|
(:require [clojure.tools.cli :refer [parse-opts]]
|
||||||
|
[import :as import]
|
||||||
[parse :refer [parse]]
|
[parse :refer [parse]]
|
||||||
[lambdaisland.deep-diff2 :as dd]))
|
[lambdaisland.deep-diff2 :as dd])
|
||||||
|
(:gen-class))
|
||||||
|
|
||||||
;; TODO: Need some tests now it's working.
|
;; TODO: Need some tests now it's working.
|
||||||
|
|
||||||
;; TODO: Where do the $25.81 fees come from?
|
|
||||||
|
|
||||||
;; TODO: Need a CLI to run it on a CSV file.
|
|
||||||
|
|
||||||
(defn sort-postings [transactions]
|
(defn sort-postings [transactions]
|
||||||
(for [t transactions]
|
(for [t transactions]
|
||||||
(update t :postings
|
(update t :postings
|
||||||
(fn [ps] (sort-by (juxt #(get-in % [:meta :entity]) :account :amount) (filter #(not (zero? (:amount %))) ps))))))
|
(fn [ps] (sort-by (juxt #(get-in % [:meta :entity]) :account :amount) (filter #(not (zero? (:amount %))) ps))))))
|
||||||
|
|
||||||
|
|
||||||
|
(def cli-options
|
||||||
|
[[nil "--csv FILE" "Pay Item Details CSV report"]
|
||||||
|
[nil "--pay-date DATE" "Date for the payroll transactions (YYYY-MM-DD)"
|
||||||
|
:validate [#(re-matches #"\d{4}-\d{2}-\d{2}" %) "Must be of format YYYY-MM-DD"]
|
||||||
|
:default "FIXME"]
|
||||||
|
[nil "--period PERIOD" "Month/year covered by the pay run eg. \"December 2023\""
|
||||||
|
:default "FIXME"]
|
||||||
|
[nil "--pay-receipt-no REFERENCE" "Payroll receipt number, eg. \"rt:111/222\""
|
||||||
|
:default "FIXME"]
|
||||||
|
[nil "--pay-invoice-no REFERENCE" "Payroll invoice number, eg. \"rt:111/222\""
|
||||||
|
:default "FIXME"]
|
||||||
|
[nil "--total-fees NUM" "Total fee charged by Paychex, eg. \"206.50\""
|
||||||
|
:default (bigdec 0)]
|
||||||
|
[nil "--fees-receipt-no REFERENCE" "Paychex fees receipt number, eg. \"rt:111/222\""
|
||||||
|
:default "FIXME"]
|
||||||
|
[nil "--fees-invoice-no REFERENCE" "Paychex fees invoice number, eg. \"rt:111/222\""
|
||||||
|
:default "FIXME"]
|
||||||
|
[nil "--retirement-date DATE" "Date for the retirement transactions (YYYY-MM-DD)"
|
||||||
|
:validate [#(re-matches #"\d{4}-\d{2}-\d{2}" %) "Must be of format YYYY-MM-DD"]
|
||||||
|
:default "FIXME"]
|
||||||
|
[nil "--retirement-receipt-no REFERENCE" "Retirement receipt number, eg. \"rt:111/222\""
|
||||||
|
:default "FIXME"]
|
||||||
|
[nil "--retirement-invoice-no REFERENCE" "Retirement receipt number, eg. \"rt:111/222\""
|
||||||
|
:default "FIXME"]
|
||||||
|
["-h" "--help"]])
|
||||||
|
|
||||||
|
(defn -main [& args]
|
||||||
|
(let [{:keys [options _arguments _errors summary]} (parse-opts args cli-options)
|
||||||
|
{:keys [pay-date period pay-receipt-no pay-invoice-no total-fees fees-receipt-no fees-invoice-no retirement-date retirement-receipt-no retirement-invoice-no]} options]
|
||||||
|
(when (:help options)
|
||||||
|
(println summary)
|
||||||
|
(System/exit 0))
|
||||||
|
(let [grouped-data (import/read-grouped-csv (:csv options))
|
||||||
|
imported (concat [(import/import-monthly-payroll pay-date period pay-receipt-no grouped-data)]
|
||||||
|
(import/import-individual-taxes pay-date period pay-receipt-no pay-invoice-no grouped-data)
|
||||||
|
[(import/import-employer-taxes pay-date period pay-receipt-no grouped-data)]
|
||||||
|
[(import/payroll-fees pay-date period fees-receipt-no fees-invoice-no total-fees grouped-data)]
|
||||||
|
[(import/import-retirement retirement-date period retirement-receipt-no retirement-invoice-no grouped-data)])]
|
||||||
|
(doseq [i imported]
|
||||||
|
(println (import/render-transaction i))))))
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
(require '[examples :as examples])
|
(require '[examples :as examples])
|
||||||
(def data (import/read-grouped-csv "/home/ben/Downloads/2023-12-27_Pay-Item-Details_2023-12-2.csv"))
|
(def grouped-data (import/read-grouped-csv "/home/ben/Downloads/2023-12-27_Pay-Item-Details_2023-12-2.csv"))
|
||||||
(def imported
|
(def imported
|
||||||
(concat [(import/import-monthly-payroll data)]
|
(concat [(import/import-monthly-payroll "2023-12-29" "December 2023" "rt:19462/674660" grouped-data)]
|
||||||
(import/import-individual-taxes data)
|
(import/import-individual-taxes "2023-12-29" "December 2023" "rt:19462/674660" "rt:19403/675431" grouped-data)
|
||||||
[(import/import-employer-taxes data)]))
|
[(import/import-employer-taxes "2023-12-29" "December 2023" "rt:19462/674660" grouped-data)]
|
||||||
|
[(import/payroll-fees "2023-12-29" "December 2023" "rt:19459/675387" "rt:19459/674887" (bigdec 206.50) grouped-data)]
|
||||||
|
[(import/import-retirement "2024-01-02" "December 2023" "rt:19403/676724" "rt:19403/675431" grouped-data)]))
|
||||||
|
|
||||||
(dd/pretty-print
|
(dd/pretty-print
|
||||||
(dd/diff
|
(dd/diff
|
||||||
|
@ -30,9 +72,4 @@
|
||||||
(doseq [i imported]
|
(doseq [i imported]
|
||||||
(println (import/render-transaction i)))
|
(println (import/render-transaction i)))
|
||||||
|
|
||||||
(dd/pretty-print
|
|
||||||
(dd/diff
|
|
||||||
(sort-postings (parse examples/employer-taxes))
|
|
||||||
(sort-postings [(import/import-employer-taxes data)])))
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,13 +13,11 @@
|
||||||
(reduce #(update %1 %2 f) m ks))
|
(reduce #(update %1 %2 f) m ks))
|
||||||
|
|
||||||
(defn bigdec-or-zero [s]
|
(defn bigdec-or-zero [s]
|
||||||
(if (str/blank? s) (bigdec 0) (bigdec s)))
|
(if (str/blank? s)
|
||||||
|
(bigdec 0)
|
||||||
(defn read-grouped-csv [filename]
|
(bigdec s)))
|
||||||
(with-open [reader (io/reader filename)]
|
|
||||||
(doall
|
|
||||||
(group-by :name (import/prep-csv-data (csv/read-csv reader))))))
|
|
||||||
|
|
||||||
|
;; TODO: Consider merging into a single amount column.
|
||||||
(defn prep-csv-data [records]
|
(defn prep-csv-data [records]
|
||||||
(let [headers [:org :name :id :category :type :t-earnings :_ :t-earnings-match :_ :t-reimbursement :t-retirement :_ :t-withholding :t-liability :_ :t-net-pay]
|
(let [headers [:org :name :id :category :type :t-earnings :_ :t-earnings-match :_ :t-reimbursement :t-retirement :_ :t-withholding :t-liability :_ :t-net-pay]
|
||||||
map-records (map zipmap (repeat headers) records)
|
map-records (map zipmap (repeat headers) records)
|
||||||
|
@ -36,6 +34,11 @@
|
||||||
bigdec-or-zero))))]
|
bigdec-or-zero))))]
|
||||||
cleaned-records))
|
cleaned-records))
|
||||||
|
|
||||||
|
(defn read-grouped-csv [filename]
|
||||||
|
(with-open [reader (io/reader filename)]
|
||||||
|
(doall
|
||||||
|
(group-by :name (import/prep-csv-data (csv/read-csv reader))))))
|
||||||
|
|
||||||
(defn format-name [name]
|
(defn format-name [name]
|
||||||
(case name
|
(case name
|
||||||
"Sharp, Sage A" "Sharp-Sage-A"
|
"Sharp, Sage A" "Sharp-Sage-A"
|
||||||
|
@ -80,13 +83,11 @@
|
||||||
;; TODO: How do we know we used all the relevant data from the report? Didn't
|
;; TODO: How do we know we used all the relevant data from the report? Didn't
|
||||||
;; miss anything?
|
;; miss anything?
|
||||||
|
|
||||||
;; TODO: Split out the match amount into a separate transaction entry?
|
(defn import-monthly-payroll [date period receipt-no groups]
|
||||||
|
{:date date :desc (format "Monthly Payroll - %s - Net Pay" period)
|
||||||
(defn import-monthly-payroll [groups]
|
|
||||||
{:date "2023-12-29" :desc "Monthly Payroll - December 2023 - Net Pay"
|
|
||||||
:meta {:program "Conservancy:Payroll"
|
:meta {:program "Conservancy:Payroll"
|
||||||
:project "Conservancy"
|
:project "Conservancy"
|
||||||
:receipt "rt:19462/674660"
|
:receipt receipt-no
|
||||||
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"
|
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"
|
||||||
:tax-implication "W2"
|
:tax-implication "W2"
|
||||||
:payroll-type "US:General"}
|
:payroll-type "US:General"}
|
||||||
|
@ -135,14 +136,14 @@
|
||||||
:tax-implication "Reimbursement"})}]]
|
:tax-implication "Reimbursement"})}]]
|
||||||
(concat pay-exp-trans reimbursement-exp-trans))))})
|
(concat pay-exp-trans reimbursement-exp-trans))))})
|
||||||
|
|
||||||
(defn import-individual-taxes [groups]
|
(defn import-individual-taxes [date period receipt-no invoice-no groups]
|
||||||
;; Print the individual taxes blocks
|
;; Print the individual taxes blocks
|
||||||
(for [[name records] groups]
|
(for [[name records] groups]
|
||||||
{:date "2023-12-29" :desc (format "Monthly Payroll - December 2023 - TAXES - %s" (format-name name))
|
{:date date :desc (format "Monthly Payroll - %s - TAXES - %s" period (format-name name))
|
||||||
:meta {:project (if (= (format-name name) "Sharp-Sage-A") "Outreachy" "Conservancy")
|
:meta {:project (if (= (format-name name) "Sharp-Sage-A") "Outreachy" "Conservancy")
|
||||||
:program "Conservancy:Payroll"
|
:program "Conservancy:Payroll"
|
||||||
:entity (format-name name)
|
:entity (format-name name)
|
||||||
:receipt "rt:19462/674660"
|
:receipt receipt-no
|
||||||
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"}
|
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"}
|
||||||
:postings
|
:postings
|
||||||
(let [super-lines (filter #(str/starts-with? (:category %) "403b") records)
|
(let [super-lines (filter #(str/starts-with? (:category %) "403b") records)
|
||||||
|
@ -161,16 +162,16 @@
|
||||||
:amount (:t-retirement x)
|
:amount (:t-retirement x)
|
||||||
:currency "USD"
|
:currency "USD"
|
||||||
:meta {:payroll-type "US:403b:Match"
|
:meta {:payroll-type "US:403b:Match"
|
||||||
:invoice "rt:19403/675431"}}
|
:invoice invoice-no}}
|
||||||
{:account "Expenses:Payroll:Salary"
|
{:account "Expenses:Payroll:Salary"
|
||||||
:amount (:t-retirement x)
|
:amount (:t-retirement x)
|
||||||
:currency "USD"
|
:currency "USD"
|
||||||
:meta {:payroll-type "US:403b:Employee"
|
:meta {:payroll-type "US:403b:Employee"
|
||||||
:invoice "rt:19403/675431"}}))
|
:invoice invoice-no}}))
|
||||||
[{:account "Liabilities:Payable:Accounts"
|
[{:account "Liabilities:Payable:Accounts"
|
||||||
:amount (- total-super)
|
:amount (- total-super)
|
||||||
:currency "USD"
|
:currency "USD"
|
||||||
:meta {:invoice "rt:19403/675431"}}]
|
:meta {:invoice invoice-no}}]
|
||||||
(conj
|
(conj
|
||||||
(vec (for [x witholding-lines]
|
(vec (for [x witholding-lines]
|
||||||
{:account "Expenses:Payroll:Salary"
|
{:account "Expenses:Payroll:Salary"
|
||||||
|
@ -181,7 +182,7 @@
|
||||||
:amount (- (reduce + (map :t-withholding witholding-lines)))
|
:amount (- (reduce + (map :t-withholding witholding-lines)))
|
||||||
:currency "USD"
|
:currency "USD"
|
||||||
:meta {:tax-implication "W2"}})
|
:meta {:tax-implication "W2"}})
|
||||||
;; We seem to add these extra insurance lines for Karen (only). Confirm with Rosanne.
|
;; TODO: We seem to add these extra insurance lines for Karen (only). Confirm with Rosanne.
|
||||||
(conj
|
(conj
|
||||||
(vec (for [x insurance-lines]
|
(vec (for [x insurance-lines]
|
||||||
{:account "Expenses:Insurance"
|
{:account "Expenses:Insurance"
|
||||||
|
@ -192,11 +193,11 @@
|
||||||
:amount (bigdec (reduce + (map :t-withholding insurance-lines)))
|
:amount (bigdec (reduce + (map :t-withholding insurance-lines)))
|
||||||
:currency "USD"})))}))
|
:currency "USD"})))}))
|
||||||
|
|
||||||
(defn import-employer-taxes [groups]
|
(defn import-employer-taxes [date period receipt-no groups]
|
||||||
{:date "2023-12-29" :desc "Monthly Payroll - December 2023 - TAXES - Employer"
|
{:date date :desc (format "Monthly Payroll - %s - TAXES - Employer" period)
|
||||||
:meta {:program "Conservancy:Payroll"
|
:meta {:program "Conservancy:Payroll"
|
||||||
:project "Conservancy"
|
:project "Conservancy"
|
||||||
:receipt "rt:19462/674660"
|
:receipt receipt-no
|
||||||
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"}
|
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"}
|
||||||
:postings
|
:postings
|
||||||
(let [total-emp-tax (reduce + (map :t-liability (apply concat (vals groups))))]
|
(let [total-emp-tax (reduce + (map :t-liability (apply concat (vals groups))))]
|
||||||
|
@ -218,9 +219,55 @@
|
||||||
:amount (- total-emp-tax)
|
:amount (- total-emp-tax)
|
||||||
:currency "USD"
|
:currency "USD"
|
||||||
:meta {:entity "Paychex"
|
:meta {:entity "Paychex"
|
||||||
:tax-implication "Tax-Payment"}
|
:tax-implication "Tax-Payment"}}))})
|
||||||
}))})
|
|
||||||
|
|
||||||
;; TODO: Fee
|
(defn payroll-fees [date period receipt-no invoice-no total-fees groups]
|
||||||
|
(let [employees (map import/format-name (keys groups))
|
||||||
|
num-employees (count employees)
|
||||||
|
fee-share (.setScale (/ total-fees num-employees) 2 java.math.RoundingMode/FLOOR)
|
||||||
|
extra-cents (* 100 (- total-fees (* fee-share num-employees)))
|
||||||
|
base-fee-allocation (repeat fee-share)
|
||||||
|
cents-allocation (concat (repeat extra-cents (bigdec 0.01)) (repeat 0))
|
||||||
|
exact-fee-allocation (reverse (take num-employees (map + base-fee-allocation cents-allocation)))
|
||||||
|
expense-postings (for [[name fee] (map vector employees exact-fee-allocation)]
|
||||||
|
{:account "Expenses:Payroll:Fees"
|
||||||
|
:amount fee
|
||||||
|
:currency "USD"
|
||||||
|
:meta {:entity name}})
|
||||||
|
asset-posting {:account "Assets:FR:Check2721"
|
||||||
|
:amount (- total-fees)
|
||||||
|
:currency "USD"}
|
||||||
|
all-postings (conj (vec expense-postings) asset-posting)]
|
||||||
|
{:date date :payee "Paychex" :desc (format "Monthly Payroll - %s - Fee" period)
|
||||||
|
:meta {:program "Conservancy:Payroll"
|
||||||
|
:project "Conservancy"
|
||||||
|
:receipt receipt-no
|
||||||
|
:invoice invoice-no
|
||||||
|
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"
|
||||||
|
:tax-implication "USA-Corporation"}
|
||||||
|
:postings all-postings}))
|
||||||
|
|
||||||
;; TODO: Vanguard 403(b)
|
(defn import-retirement [date period receipt-no invoice-no groups]
|
||||||
|
{:date date :desc (format "ASCENSUS TRUST RET PLAN - ACH DEBIT - Vanguard 403(b) - %s" period)
|
||||||
|
:meta {:program "Conservancy:Payroll"
|
||||||
|
:project "Conservancy"
|
||||||
|
:receipt receipt-no
|
||||||
|
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"
|
||||||
|
:tax-implication "Retirement-Pretax"
|
||||||
|
:invoice invoice-no}
|
||||||
|
:postings
|
||||||
|
(let [total-retirement (reduce + (map :t-retirement (apply concat (vals groups))))]
|
||||||
|
(conj (vec (for [[name records] groups]
|
||||||
|
(let [name (format-name name)
|
||||||
|
total-retirement (->> records
|
||||||
|
(filter #(= (:type %) "Retirement"))
|
||||||
|
(map :t-retirement)
|
||||||
|
(apply +)
|
||||||
|
bigdec)]
|
||||||
|
{:account "Liabilities:Payable:Accounts",
|
||||||
|
:amount total-retirement,
|
||||||
|
:currency "USD",
|
||||||
|
:meta {:entity name}})))
|
||||||
|
{:account "Assets:FR:Check1345"
|
||||||
|
:amount (- total-retirement)
|
||||||
|
:currency "USD"}))})
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
(s/def ::quoted-token (s/cat
|
(s/def ::quoted-token (s/cat
|
||||||
:_ #{\"}
|
:_ #{\"}
|
||||||
:token (s/+ (set "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890-:./% "))
|
:token (s/+ (set "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890-:./% ()"))
|
||||||
:_ #{\"}))
|
:_ #{\"}))
|
||||||
|
|
||||||
(s/def ::whitespace (s/+ #{\space}))
|
(s/def ::whitespace (s/+ #{\space}))
|
||||||
|
@ -37,6 +37,8 @@
|
||||||
:_ ::whitespace
|
:_ ::whitespace
|
||||||
:_ ::token
|
:_ ::token
|
||||||
:_ ::whitespace
|
:_ ::whitespace
|
||||||
|
:payee (s/? ::quoted-token)
|
||||||
|
:_ (s/? ::whitespace)
|
||||||
:desc ::quoted-token
|
:desc ::quoted-token
|
||||||
:_ #{\newline}
|
:_ #{\newline}
|
||||||
:meta (s/* ::meta)
|
:meta (s/* ::meta)
|
||||||
|
|
Loading…
Reference in a new issue