(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 format-name [name] (case name "Sharp, Sage A" "Sharp-Sage-A" (-> name (str/replace " Jr." "") (str/replace ", " "-") (str/replace #" \w$" "")))) (defn read-grouped-csv [filename] (with-open [reader (io/reader filename)] (doall (group-by :name (for [[_ name _ category type & totals] (csv/read-csv reader)] {:name (import/format-name name) :category category :type type :amount (apply max (map bigdec (remove str/blank? totals)))}))))) (defn render-transaction [{:keys [date payee desc meta postings]}] (str/join (concat [(if payee (format "%s txn \"%s\" \"%s\"\n" date payee desc) (format "%s txn \"%s\"\n" date desc))] (for [[k v] meta] (format " %s: \"%s\"\n" (name k) v)) (for [{:keys [amount account currency meta]} postings] (when (not (zero? amount)) (format " %-40s %10.2f %s\n%s" account amount currency (str/join (for [[k v] meta] (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" "NY Re-empl Svc" "US:NY:Reempt" cat)) (defn assoc-project [name m] (if (= name "Sharp-Sage-A") (assoc m :project "Outreachy") m)) (defn split-fee "Share a total fee into n groups allocating the remainder as evenly as possible." [total n] (let [total (bigdec total) fee-share (.setScale (bigdec (/ (double total) n)) 2 java.math.RoundingMode/FLOOR) extra-cents (* 100M (- total (* fee-share n))) base-fee-allocation (repeat fee-share) cents-allocation (take n (concat (repeat extra-cents 0.01M) (repeat 0)))] (map + base-fee-allocation cents-allocation))) (defn payroll "Return a net pay transaction." [date period receipt-no groups] (let [postings (for [[name records] groups] (let [total-net-pay (->> records (filter #(= (:type %) "Net Pay")) (map :amount) (apply +)) total-reimbursement (->> records (filter #(= (:type %) "Reimbursement")) (map :amount) (apply +)) actual-total-net-pay (- total-net-pay total-reimbursement)] [{:account "Expenses:Payroll:Salary" :amount actual-total-net-pay :currency "USD" :meta (assoc-project name {:entity name})} {:account "Assets:FR:Check2721" :amount (- actual-total-net-pay) :currency "USD" :meta {:entity name}} {:account "Expenses:Hosting" :amount total-reimbursement :currency "USD" :meta (assoc-project 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"})}]))] [{: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 postings)}])) (defn individual-taxes "Return a transaction of expenses/witholding for each employee." [date period receipt-no invoice-no groups] (for [[name records] groups] (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) ;; TODO: We seem to add these extra insurance lines for Karen (NY) only. Confirm with Rosanne. insurance-lines (filter #(and (= (:type %) "Withholding") (str/includes? (:category %) "NY Disability")) records) total-super (->> super-lines (map :amount) (apply +)) super-postings (for [{:keys [category amount]} super-lines] (if (= category "403b ER match") {:account "Expenses:Payroll:Salary" :amount amount :currency "USD" :meta {:payroll-type "US:403b:Match" :invoice invoice-no}} {:account "Expenses:Payroll:Salary" :amount amount :currency "USD" :meta {:payroll-type "US:403b:Employee" :invoice invoice-no}})) liability-postings [{:account "Liabilities:Payable:Accounts" :amount (- total-super) :currency "USD" :meta {:invoice invoice-no}}] withholding-postings (for [{:keys [category amount]} witholding-lines] {:account "Expenses:Payroll:Salary" :amount amount :currency "USD" :meta {:payroll-type (cat->acct category)}}) withholding-asset-postings [{:account "Assets:FR:Check2721" :amount (- (reduce + (map :amount witholding-lines))) :currency "USD" :meta {:tax-implication "W2"}}] insurance-postings (for [{:keys [category amount]} insurance-lines] {:account "Expenses:Insurance" :amount (- amount) :currency "USD" :meta {:payroll-type (cat->acct category)}}) insurance-asset-postings [{:account "Assets:FR:Check2721" :amount (reduce + (map :amount insurance-lines)) :currency "USD"}]] {:date date :desc (format "Monthly Payroll - %s - TAXES - %s" period name) :meta (assoc-project name {:project "Conservancy" :program "Conservancy:Payroll" :entity name :receipt receipt-no :approval "Financial/Employment-Records/memo-re-board-approval-of-payroll.txt"}) :postings (concat super-postings liability-postings withholding-postings withholding-asset-postings insurance-postings insurance-asset-postings)}))) (defn employer-taxes "Return an employer taxes transaction." [date period receipt-no groups] (let [liability-postings (apply concat (for [[name records] groups] (let [liability-lines (filter #(and (= (:type %) "Liability") (not (str/includes? (:category %) "Unemploy"))) records)] (for [{:keys [category amount]} liability-lines] {:account "Expenses:Payroll:Tax" :amount amount :currency "USD" ;; TODO: Check lack of ":Tax:" with Rosanne. :meta (assoc-project name {:entity name :payroll-type (str/replace (cat->acct category) "Tax:" "")})})))) total-liabilities (->> liability-postings (map :amount) (reduce +)) unemploy-postings (apply concat (for [[name records] groups] (let [unemploy-lines (filter #(and (= (:type %) "Liability") (str/includes? (:category %) "Unemploy")) records)] (for [{:keys [amount category]} unemploy-lines] {:account "Expenses:Payroll:Tax" :amount amount :currency "USD" :meta (assoc-project name {:entity (first (str/split category #" ")) ;; TODO: Karen doesn't have a memo in January 2024 :memo name :payroll-type (str "US:" (cat->acct category))})})))) total-unemploy (->> unemploy-postings (map :amount) (reduce +)) asset-postings [{:account "Assets:FR:Check2721" :amount (- (+ total-liabilities total-unemploy)) :currency "USD" :meta {:entity "Paychex" :tax-implication "Tax-Payment"}}]] [{: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 (concat liability-postings unemploy-postings asset-postings)}])) (defn payroll-fees "Return a payroll fees transaction." [date period receipt-no invoice-no total-fees groups] (let [employees (keys groups) exact-fee-allocation (split-fee total-fees (count employees)) employee-fees (map vector employees exact-fee-allocation) expense-postings (for [[name fee] employee-fees] {:account "Expenses:Payroll:Fees" :amount fee :currency "USD" :meta (assoc-project name {:entity name})}) asset-postings [{:account "Assets:FR:Check2721" :amount (- total-fees) :currency "USD"}]] [{: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 (concat expense-postings asset-postings)}])) (defn retirement "Return a retirement transaction." [date period receipt-no invoice-no groups] (let [liability-postings (for [[name records] groups] (let [total-retirement (->> records (filter #(= (:type %) "Retirement")) (map :amount) (reduce +))] {:account "Liabilities:Payable:Accounts", :amount total-retirement, :currency "USD", :meta {:entity name}})) total-liabilities (->> liability-postings (map :amount) (reduce +)) asset-postings [{:account "Assets:FR:Check1345" :amount (- total-liabilities) :currency "USD"}]] [{: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 (concat liability-postings asset-postings)}]))