(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))) ;; 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" "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) :meta {:program "Conservancy:Payroll" :project "Conservancy" :receipt receipt-no :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)) :meta {:project (if (= (format-name name) "Sharp-Sage-A") "Outreachy" "Conservancy") :program "Conservancy:Payroll" :entity (format-name name) :receipt receipt-no :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}} {:account "Expenses:Payroll:Salary" :amount (:t-retirement x) :currency "USD" :meta {:payroll-type "US:403b:Employee" :invoice invoice-no}})) [{:account "Liabilities:Payable:Accounts" :amount (- total-super) :currency "USD" :meta {:invoice invoice-no}}] (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. (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) :meta {:program "Conservancy:Payroll" :project "Conservancy" :receipt receipt-no :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"}))})