payroll-import/src/import.clj

274 lines
14 KiB
Clojure
Raw Normal View History

(ns import
(:require [clojure.data.csv :as csv]
[clojure.java.io :as io]
[clojure.string :as str]))
(defn kebab-kw [x]
(-> x
str/lower-case
(str/replace " " "-")
keyword))
(defn update-vals-subset [m ks f]
(reduce #(update %1 %2 f) m ks))
(defn bigdec-or-zero [s]
(if (str/blank? s)
(bigdec 0)
(bigdec s)))
2024-02-20 03:55:14 +00:00
;; TODO: Consider merging into a single amount column.
(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]
map-records (map zipmap (repeat headers) records)
cleaned-records (->> map-records
(map #(-> %
(dissoc :org :_)
(update-vals-subset [:t-earnings
:t-earnings-match
:t-reimbursement
:t-retirement
:t-withholding
:t-liability
:t-net-pay]
bigdec-or-zero))))]
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]
(case name
"Sharp, Sage A" "Sharp-Sage-A"
(-> name
(str/replace " Jr." "")
(str/replace ", " "-")
(str/replace #" \w$" ""))))
(defn render-transaction [trans]
(str/join (concat [(format "%s txn \"%s\"\n" (:date trans) (:desc trans))]
(for [[k v] (seq (:meta trans))]
(format " %s: \"%s\"\n" (name k) v))
(for [posting (:postings trans)]
(when (not (zero? (:amount posting)))
(format " %-40s %10.2f %s\n%s" (:account posting) (:amount posting) (:currency posting)
(str/join (for [[k v] (seq (:meta posting))]
(format " %s: \"%s\"\n" (name k) v)))))))))
(defn cat->acct [cat]
;; TODO: Can we make this more general for other states?
(case cat
"Fed Income Tax" "US:Tax:Income"
"Medicare" "US:Tax:Medicare"
"IL Income Tax" "US:IL:Tax:Income"
"NY Income Tax" "US:NY:Tax:Income"
"NYC Income Tax" "US:NY:Tax:NYC"
"OR Income Tax" "US:OR:Tax:Income"
"OH Income Tax" "US:OH:Tax:Income"
"PNTSD Income Tax" "US:OH:Tax:PNTSD"
"COLMB Income Tax" "US:OH:Tax:COLUMB"
"Social Security" "US:Tax:SocialSecurity"
"NY Disability" "US:NY:Disability"
"OR Disability PFL" "US:OR:Disability:PFL"
"NY Disability PFL" "US:NY:Disability:PFL"
"OR TRANS STT" "US:OR:Tax:STT"
2024-02-20 07:26:45 +00:00
"Fed Unemploy" "US:Unemployment"
"IL Unemploy" "IL:Unemployment"
"NY Unemploy" "NY:Unemployment"
"OR Unemploy" "OR:Unemployment"
cat))
;; TODO: How do we know we used all the relevant data from the report? Didn't
;; miss anything?
(defn import-monthly-payroll [date period receipt-no groups]
{:date date :desc (format "Monthly Payroll - %s - Net Pay" period)
2024-02-20 07:26:45 +00:00
:meta {:program "Conservancy:Payroll"
:project "Conservancy"
:receipt receipt-no
2024-02-20 07:26:45 +00:00
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"
:tax-implication "W2"
:payroll-type "US:General"}
:postings
(apply concat (for [[name records] groups]
(let [name (format-name name)
total-net-pay (->> records
(filter #(= (:type %) "Net Pay"))
(map :t-net-pay)
(apply +)
bigdec)
total-reimbursement (->> records
(filter #(= (:type %) "Reimbursement"))
(map :t-reimbursement)
(apply +)
bigdec)
total-net-pay-less-reimb (- total-net-pay total-reimbursement)
pay-exp-trans [{:account "Expenses:Payroll:Salary"
:amount total-net-pay-less-reimb
:currency "USD"
:meta (if (= name "Sharp-Sage-A")
{:entity name
:project "Outreachy"}
{:entity name})}
{:account "Assets:FR:Check2721"
:amount (- total-net-pay-less-reimb)
:currency "USD"
:meta {:entity name}}]
reimbursement-exp-trans [{:account "Expenses:Hosting"
:amount total-reimbursement
:currency "USD"
:meta (if (= name "Sharp-Sage-A")
{:entity name
:project "Outreachy"
:payroll-type "US:Reimbursement"}
{:entity name
:payroll-type "US:Reimbursement"})}
{:account "Assets:FR:Check2721"
:amount (- total-reimbursement)
:currency "USD"
:meta (if (= name "Sharp-Sage-A")
{:entity name
:project "Outreachy"
:tax-implication "Reimbursement"}
{:entity name
:tax-implication "Reimbursement"})}]]
(concat pay-exp-trans reimbursement-exp-trans))))})
(defn import-individual-taxes [date period receipt-no invoice-no groups]
;; Print the individual taxes blocks
(for [[name records] groups]
{:date date :desc (format "Monthly Payroll - %s - TAXES - %s" period (format-name name))
2024-02-20 07:26:45 +00:00
:meta {:project (if (= (format-name name) "Sharp-Sage-A") "Outreachy" "Conservancy")
:program "Conservancy:Payroll"
:entity (format-name name)
:receipt receipt-no
2024-02-20 07:26:45 +00:00
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"}
:postings
(let [super-lines (filter #(str/starts-with? (:category %) "403b") records)
;; TODO: Have I got the liability/witholding right? Which is used in which report.
witholding-lines (filter #(= (:type %) "Withholding") records)
insurance-lines (filter #(and (= (:type %) "Withholding")
(str/includes? (:category %) "NY Disability")) records)
total-super (->> super-lines
(map :t-retirement)
(apply +)
bigdec)]
(concat
(for [x super-lines]
(if (= (:category x) "403b ER match")
{:account "Expenses:Payroll:Salary"
:amount (:t-retirement x)
:currency "USD"
:meta {:payroll-type "US:403b:Match"
:invoice invoice-no}}
2024-02-20 07:26:45 +00:00
{:account "Expenses:Payroll:Salary"
:amount (:t-retirement x)
:currency "USD"
:meta {:payroll-type "US:403b:Employee"
:invoice invoice-no}}))
2024-02-20 07:26:45 +00:00
[{:account "Liabilities:Payable:Accounts"
:amount (- total-super)
:currency "USD"
:meta {:invoice invoice-no}}]
2024-02-20 07:26:45 +00:00
(conj
(vec (for [x witholding-lines]
{:account "Expenses:Payroll:Salary"
:amount (:t-withholding x)
:currency "USD"
:meta {:payroll-type (cat->acct (:category x))}}))
{:account "Assets:FR:Check2721"
:amount (- (reduce + (map :t-withholding witholding-lines)))
:currency "USD"
:meta {:tax-implication "W2"}})
;; TODO: We seem to add these extra insurance lines for Karen (only). Confirm with Rosanne.
2024-02-20 07:26:45 +00:00
(conj
(vec (for [x insurance-lines]
{:account "Expenses:Insurance"
:amount (- (:t-withholding x))
:currency "USD"
:meta {:payroll-type (cat->acct (:category x))}}))
{:account "Assets:FR:Check2721"
:amount (bigdec (reduce + (map :t-withholding insurance-lines)))
:currency "USD"})))}))
(defn import-employer-taxes [date period receipt-no groups]
{:date date :desc (format "Monthly Payroll - %s - TAXES - Employer" period)
2024-02-20 07:26:45 +00:00
:meta {:program "Conservancy:Payroll"
:project "Conservancy"
:receipt receipt-no
2024-02-20 07:26:45 +00:00
:approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"}
:postings
(let [total-emp-tax (reduce + (map :t-liability (apply concat (vals groups))))]
(conj (vec (apply concat (for [[name records] groups]
(let [name (format-name name)
liability-lines (filter #(= (:type %) "Liability") records)]
(for [x liability-lines]
{:account "Expenses:Payroll:Tax"
:amount (:t-liability x)
:currency "USD"
:meta (if (= name "Sharp-Sage-A")
{:entity name
:project "Outreachy"
;; TODO: Check lack of ":Tax:" with Rosanne.
:payroll-type (str/replace (cat->acct (:category x)) "Tax:" "")}
{:entity name
:payroll-type (str/replace (cat->acct (:category x)) "Tax:" "")})})))))
{:account "Assets:FR:Check2721"
:amount (- total-emp-tax)
:currency "USD"
:meta {:entity "Paychex"
:tax-implication "Tax-Payment"}}))})
(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}))
(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"}))})