970 lines
47 KiB
Clojure
970 lines
47 KiB
Clojure
(ns auto-ap.ledger.reports
|
|
#?@
|
|
(:clj
|
|
[(:require
|
|
[auto-ap.time :as atime]
|
|
[auto-ap.time-utils :refer [user-friendly-date]]
|
|
[auto-ap.utils :refer [dollars-0? dollars=]]
|
|
[clojure.string :as str]
|
|
[clj-time.coerce :as coerce]
|
|
[auto-ap.time-utils :refer [user-friendly-date]]
|
|
)]
|
|
:cljs
|
|
[(:require
|
|
[auto-ap.utils :refer [dollars-0? dollars=]]
|
|
[auto-ap.views.utils :as au]
|
|
[clojure.string :as str]
|
|
[auto-ap.time-utils :refer [user-friendly-date]])]))
|
|
|
|
(defn date->str [d]
|
|
#?(:clj
|
|
(if (inst? d)
|
|
(atime/unparse-local (coerce/to-date-time d) atime/normal-date)
|
|
(atime/unparse-local d atime/normal-date))
|
|
:cljs (au/date->str d au/pretty)))
|
|
|
|
|
|
|
|
(def groupings
|
|
{:assets [["1100 Cash and Bank Accounts" 11000 11999]
|
|
["1200 Accounts Receivable" 12000 12999]
|
|
["1300 Inventory" 13000 13999]
|
|
["1400 Prepaid Expenses" 14000 14999]
|
|
["1500 Property and Equipment" 15000 15999]
|
|
["1600 Intangible Assets" 16000 16999]
|
|
["1700 Other Assets" 17000 19999]]
|
|
:liabilities [["2000 Accounts Payable" 20100 23999]
|
|
["2400 Accrued Expenses" 24000 24999]
|
|
["2500 Other Liabilities" 25000 25999]
|
|
["2600 Split Accounts" 26000 26999]
|
|
["2700 Current Portion of Long-Term Debt" 27000 27999]
|
|
["2800 Notes Payable" 28000 28999]]
|
|
:equities [["3000 Owner's Equity" 30000 39999]]
|
|
:sales [["40000-43999 Food Sales " 40000 43999]
|
|
["44000-46999 Alcohol Sales" 44000 46999]
|
|
["47000 Merchandise Sales" 47000 47999]
|
|
["48000 Other Operating Income" 48000 48999]
|
|
["49000 Non-Business Income" 49000 49999]]
|
|
:cogs [
|
|
["50000-54000 Food Costs" 50000 53999]
|
|
["54000-56000 Alcohol Costs" 54000 55999]
|
|
["56000 Merchandise Costs" 56000 56999]
|
|
["57000-60000 Other Costs of Sales" 57000 59999]]
|
|
:payroll [["60000 Payroll - General" 60000 60999]
|
|
["61000 Payroll - Management" 61000 61999]
|
|
["62000 Payroll - BOH" 62000 62999]
|
|
["63000-66000 Payroll - FOH" 63000 65999]
|
|
["66000-70000 Payroll - Other" 66000 69999]]
|
|
|
|
:controllable [["70000 72000 GM Controllable Costs - Ops Related" 70000 71999]
|
|
["72000 GM Controllable Costs - Customer Related" 72000 72999]
|
|
["73000 GM Controllable Costs - Employee Related" 73000 73999]
|
|
["74000 GM Controllable Costs - Building & Equipment Related" 74000 74999]
|
|
["75000 GM Controllable Costs - Office & Management Related" 75000 75999]
|
|
["76000-80000 GM Controllable Costs - Other" 76000 79999]]
|
|
|
|
:fixed-overhead [["80000-82000 Operational Costs" 80000 81999]
|
|
["82000 Occupancy Costs" 82000 82999]
|
|
["83000 Utility Costs" 83000 83999]
|
|
["84000 Equipment Rental" 84000 84999]
|
|
["85000-87000 Taxes & Insurance" 85000 86999]
|
|
["87000-90000 Other Non-Controllable Costs" 87000 89999]]
|
|
:ownership-controllable [["90000-93000 Research & Entertainment" 90000 92999]
|
|
["93000 Bank Charges & Interest" 93000 93999]
|
|
["94000-96000 Other Owner Controllable Costs" 94000 95999]
|
|
["96000 Depreciation" 96000 96999]
|
|
["97000 Taxes" 97000 97999]
|
|
["98000 Other Expenses" 98000 98999]]
|
|
|
|
:operating-activities [
|
|
;; BEN EDIT STARTING HERE
|
|
["20100-20199 Credit Card Balances" 20100 20199 :add]
|
|
["21000-24000 Accounts Payable" 21000 23999 :add]
|
|
["25000-28000 Accounts Payable" 25000 27999 :add]
|
|
["24000-25000 Accrual Liabilities" 24000 24999 :add]
|
|
["12000-13000 Accounts Receivable" 12000 13000 :subtract]
|
|
["96000-97000 Depreciation Expense" 96000 96999 :add]
|
|
["13000-15000 Inventory" 13000 14999 :subtract]
|
|
;; BEN ENDING HERE
|
|
]
|
|
|
|
:investment-activities [
|
|
|
|
;; BEN EDIT STARTING HERE
|
|
["15000-18000 Investments" 15000 17999 :subtract]
|
|
;; BEN ENDING HERE
|
|
|
|
]
|
|
:financing-activities [
|
|
;; BEN EDIT STARTING HERE
|
|
["30000-33000 Other Equity Accounts" 30000 32999 :add]
|
|
["33000-34000 Owner's Contributions" 33000 33999 :add]
|
|
["34000-35000 Owner's Distributions" 34000 34999 :add]
|
|
["35000-36000 Retained Earnings" 35000 35999 :add]
|
|
["28000-29000 Loans (payable)" 28000 28999 :add]
|
|
;; BEN ENDING HERE
|
|
]
|
|
:cash [
|
|
;; BEN EDIT STARTING HERE
|
|
["11000-11400 Bank Accounts / Cash" 11000 11399 :add]
|
|
;; BEN ENDING HERE
|
|
|
|
]})
|
|
|
|
(def cashflow-aggregation
|
|
(->> (select-keys groupings [:operating-activities :investment-activities :financing-activities])
|
|
vals
|
|
(mapcat identity)
|
|
(map (fn [[_ start end rule]]
|
|
[start end rule]))
|
|
(into [])))
|
|
|
|
(defn cashflow-account->amount [account-code amount]
|
|
(let [operation (->> cashflow-aggregation
|
|
(filter (fn [[start end]]
|
|
(<= start account-code end)))
|
|
first
|
|
last)]
|
|
(cond (= operation :add)
|
|
amount
|
|
|
|
(= operation :subtract)
|
|
(- amount)
|
|
|
|
:else
|
|
amount)))
|
|
|
|
(defn min-numeric-code [category]
|
|
(->> (groupings category)
|
|
(map second)
|
|
(apply min)))
|
|
|
|
(defn max-numeric-code [category]
|
|
(->> (groupings category)
|
|
(map second)
|
|
(apply max)))
|
|
|
|
|
|
(def flat-categories
|
|
(for [[category groups] groupings
|
|
[_ start end] groups]
|
|
[category start end]))
|
|
|
|
(defn in-range? [code]
|
|
(if code
|
|
(reduce
|
|
(fn [acc [_ start end]]
|
|
(if (<= start code end)
|
|
(reduced true)
|
|
acc))
|
|
false
|
|
flat-categories)
|
|
false))
|
|
|
|
(defn client-locations [pnl-data]
|
|
(->> pnl-data
|
|
:data
|
|
(filter (comp in-range? :numeric-code))
|
|
(filter #(not= "A" (:location %)))
|
|
(group-by (juxt :client-id :location))
|
|
(filter (fn [[_ as]]
|
|
(not (dollars-0? (reduce + 0 (map :amount as))))))
|
|
(mapcat second)
|
|
(map (fn [a]
|
|
(if (or (not (:client-id a))
|
|
(empty? (:location a)))
|
|
nil
|
|
[(:client-id a)
|
|
(:location a)])))
|
|
(filter identity)
|
|
(set)
|
|
|
|
(sort-by (fn [x]
|
|
[(:client-id x)
|
|
(if (= (:location x) "HQ" )
|
|
"ZZZZZZ"
|
|
(:location x))]))))
|
|
|
|
(defn aggregate-accounts [pnl-data]
|
|
(reduce (fnil + 0.0) 0.0 (map :amount (:data pnl-data))))
|
|
|
|
(defn aggregate-cashflow-accounts [pnl-data]
|
|
(reduce (fnil + 0.0) 0.0 (map (fn [d]
|
|
(cashflow-account->amount (:numeric-code d) (:amount d)))
|
|
(:data pnl-data))))
|
|
|
|
(defn aggregate-credits [pnl-data]
|
|
(reduce (fnil + 0.0) 0.0 (map :credits (:data pnl-data))))
|
|
|
|
(defn aggregate-debits [pnl-data]
|
|
(reduce (fnil + 0.0) 0.0 (map :debits (:data pnl-data))))
|
|
|
|
(defn best-category [a]
|
|
(->> flat-categories
|
|
(filter (fn [[category start end]]
|
|
(and (:numeric-code a)
|
|
(<= start (:numeric-code a) end))))
|
|
first
|
|
first))
|
|
|
|
(defn filter-client [pnl-data client]
|
|
(-> pnl-data
|
|
(update :data (fn [data]
|
|
((group-by :client-id data) client)))
|
|
(update :filters (fn [f]
|
|
(assoc f :client-id client)))))
|
|
|
|
(defn filter-location [pnl-data location]
|
|
(-> pnl-data
|
|
(update :data (fn [data]
|
|
((group-by :location data) location)))
|
|
(update :filters (fn [f]
|
|
(assoc f :location location)))))
|
|
|
|
(defn filter-locations [pnl-data locations]
|
|
(-> pnl-data
|
|
(update :filters (fn [f]
|
|
(assoc f :locations locations)))))
|
|
|
|
(defn filter-numeric-code [pnl-data from to]
|
|
(-> pnl-data
|
|
(update :data (fn [data]
|
|
(filter
|
|
#(<= from (or (:numeric-code %) 0) to)
|
|
data)))
|
|
(update :filters (fn [f]
|
|
(assoc f
|
|
:numeric-code [{:from from
|
|
:to to}])))))
|
|
|
|
(defn account-belongs-in-category? [numeric-code category]
|
|
(->> (groupings category)
|
|
(some (fn [[_ from to]]
|
|
(<= from (or numeric-code 0) to)))))
|
|
|
|
;; TODO make click-through work for multiple ranges :fliters
|
|
(defn filter-categories [pnl-data categories]
|
|
(-> pnl-data
|
|
(update :data
|
|
(fn [data]
|
|
(for [account data
|
|
:when (some #(account-belongs-in-category? (:numeric-code account) %) categories)]
|
|
account)))
|
|
(update :filters
|
|
(fn [f]
|
|
(assoc f :numeric-code
|
|
(for [category categories
|
|
[_ from to] (groupings category)]
|
|
{:from from
|
|
:to to}))))))
|
|
|
|
(defn filter-period [pnl-data period]
|
|
(-> pnl-data
|
|
(update :data (fn [data]
|
|
((group-by :period data) period)))
|
|
(update :filters (fn [f]
|
|
(assoc f :date-range period)))))
|
|
|
|
(defn zebra [pnl-data i]
|
|
(if (odd? i)
|
|
(assoc-in pnl-data [:cell-args :bg-color] [240 240 240])
|
|
pnl-data))
|
|
|
|
|
|
|
|
(defn negate [pnl-data types]
|
|
(update pnl-data :data
|
|
(fn [accounts]
|
|
(map
|
|
(fn [account]
|
|
(if (types (best-category account))
|
|
(update account :amount #(- (or % 0.0)))
|
|
account))
|
|
accounts))))
|
|
|
|
|
|
(defn used-accounts [pnl-datas]
|
|
(->>
|
|
pnl-datas
|
|
(mapcat :data)
|
|
(map #(select-keys % [:numeric-code :name]))
|
|
(set)
|
|
(sort-by :numeric-code)))
|
|
|
|
(defn subtotal-by-column-row [pnl-datas title & [cell-args]]
|
|
(into [{:value title
|
|
:bold true}]
|
|
(map
|
|
(fn [p]
|
|
(merge
|
|
{:format :dollar
|
|
:value (aggregate-accounts p)
|
|
:filters (when (:numeric-code (:filters p)) ;; don't allow filtering when you don't at least filter numeric codes
|
|
(:filters p))}
|
|
(:cell-args p)
|
|
cell-args))
|
|
pnl-datas)))
|
|
|
|
(defn cashflow-subtotal-by-column-row [pnl-datas title & [cell-args]]
|
|
(into [{:value title
|
|
:bold true}]
|
|
(mapcat
|
|
(fn [p]
|
|
[
|
|
(merge
|
|
{:format :text
|
|
:value nil}
|
|
(:cell-args p)
|
|
cell-args)
|
|
(merge
|
|
{:format :text
|
|
:value nil}
|
|
(:cell-args p)
|
|
cell-args)
|
|
(merge
|
|
{:format :dollar
|
|
:value (aggregate-cashflow-accounts p)
|
|
:filters (when (:numeric-code (:filters p)) ;; don't allow filtering when you don't at least filter numeric codes
|
|
(:filters p))}
|
|
(:cell-args p)
|
|
cell-args)])
|
|
pnl-datas)))
|
|
|
|
(defn calc-percent-of-sales [table pnl-datas]
|
|
(let [sales (map
|
|
(fn [p]
|
|
(aggregate-accounts (filter-categories p [:sales])))
|
|
pnl-datas)]
|
|
(->> table
|
|
(map (fn [[_ & values]]
|
|
(map
|
|
(fn [v s]
|
|
{:border (:border v)
|
|
:bg-color (:bg-color v)
|
|
:format (if (string? (:value v))
|
|
:text
|
|
:percent)
|
|
:color [128 128 128]
|
|
:value (cond
|
|
(string? (:value v))
|
|
""
|
|
|
|
(dollars-0? s)
|
|
0.0
|
|
:else
|
|
(/ (:value v) s))})
|
|
values sales))))))
|
|
|
|
(defn calc-deltas [table]
|
|
(->> table
|
|
(map (fn [[_ & values]]
|
|
(->> values
|
|
(partition 2 1)
|
|
(map (fn [[a b]]
|
|
(if (or (string? (:value b))
|
|
(string? (:value a)))
|
|
{:value ""
|
|
:format :text
|
|
:bg-color (:bg-color b)}
|
|
{:border (:border b)
|
|
:format :dollar
|
|
:value (- (:value b)
|
|
(:value a))
|
|
:bg-color (:bg-color b)}))))))))
|
|
|
|
(defn combine-tables
|
|
([[pnl-data] table percent-of-sales deltas]
|
|
(map (fn [[title & row] percent-of-sales deltas ]
|
|
(let [deltas (cons {:value 0.0
|
|
:format :dollar
|
|
:border (:border (first row))} deltas)]
|
|
(into [title]
|
|
(mapcat
|
|
(fn [v p d]
|
|
(if (:include-deltas (:args pnl-data))
|
|
[v p d]
|
|
[v p]))
|
|
row
|
|
percent-of-sales
|
|
deltas))))
|
|
table
|
|
percent-of-sales
|
|
deltas)))
|
|
|
|
(defn headers [[pnl-data :as pnl-datas] header-title]
|
|
(let [big-header (into [{:value header-title
|
|
:bold true}]
|
|
(map-indexed (fn [i p]
|
|
(cond-> {:value
|
|
(str (date->str (:start p))
|
|
" - "
|
|
(date->str (:end p)))
|
|
:colspan (cond
|
|
(-> pnl-data :args :include-deltas)
|
|
3
|
|
|
|
(-> pnl-data :args :column-per-location)
|
|
(* 2 (/ (count pnl-datas)
|
|
(count (-> pnl-data :args :periods))))
|
|
|
|
:else
|
|
2)
|
|
:align :center
|
|
:bold true}
|
|
(odd? i) (assoc :bg-color [240 240 240])))
|
|
(:periods (:args pnl-data))))
|
|
sub-header (into [{:value ""}]
|
|
(if (-> pnl-data :args :column-per-location)
|
|
(mapcat
|
|
(fn [p]
|
|
(cond-> [(merge {:value (or (when (-> p :filters :location)
|
|
(str ((-> p :client-codes) (-> p :filters :client-id)) "-" (-> p :filters :location)))
|
|
"Total")
|
|
:align :right}
|
|
(:cell-args p))
|
|
(merge {:value "%"
|
|
:align :right}
|
|
(:cell-args p))]
|
|
(-> pnl-data :args :include-deltas) (conj (merge {:value "+/-"
|
|
:align :right}
|
|
(:cell-args p)))))
|
|
pnl-datas)
|
|
(mapcat
|
|
(fn [p]
|
|
(cond-> [(merge {:value "Amt"
|
|
:align :right}
|
|
(:cell-args p))
|
|
(merge {:value "%"
|
|
:align :right}
|
|
(:cell-args p))]
|
|
(-> pnl-data :args :include-deltas) (conj (merge {:value "+/-"
|
|
:align :right}
|
|
(:cell-args p)))))
|
|
pnl-datas)))]
|
|
[big-header
|
|
sub-header]))
|
|
|
|
(defn location-summary-table [pnl-datas title]
|
|
(let [table [(subtotal-by-column-row (map #(filter-categories % [:sales])
|
|
pnl-datas)
|
|
"Sales")
|
|
(subtotal-by-column-row (map #(filter-categories % [:cogs ]) pnl-datas)
|
|
"Cogs")
|
|
|
|
(subtotal-by-column-row (map #(filter-categories % [:payroll ]) pnl-datas)
|
|
"Payroll")
|
|
|
|
(subtotal-by-column-row (map #(-> %
|
|
(filter-categories [:sales :payroll :cogs])
|
|
(negate #{:payroll :cogs}))
|
|
pnl-datas)
|
|
"Gross Profits")
|
|
|
|
(subtotal-by-column-row (map #(filter-categories % [:controllable :fixed-overhead :ownership-controllable])
|
|
pnl-datas)
|
|
"Overhead")
|
|
|
|
(subtotal-by-column-row (map #(-> %
|
|
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
|
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
|
pnl-datas)
|
|
"Net Income")]
|
|
percent-of-sales (calc-percent-of-sales table pnl-datas)
|
|
deltas (calc-deltas table)]
|
|
{:header (headers pnl-datas title)
|
|
:rows (combine-tables pnl-datas table percent-of-sales deltas)}))
|
|
|
|
|
|
(defn detail-rows
|
|
[pnl-datas grouping title]
|
|
(let [pnl-datas (map #(filter-categories % [grouping])
|
|
pnl-datas)
|
|
individual-accounts
|
|
(for [[grouping-name from to] (groupings grouping)
|
|
:let [pnl-datas (map #(filter-numeric-code % from to)
|
|
pnl-datas)
|
|
account-codes (used-accounts pnl-datas)]
|
|
:when (seq account-codes)
|
|
row (-> [(into [{:value (str "---" grouping-name "---")}]
|
|
(map
|
|
(fn [p]
|
|
(assoc (:cell-args p) :value "" :format ""))
|
|
pnl-datas)
|
|
)]
|
|
(into (for [{:keys [numeric-code name]} account-codes]
|
|
(into [{:value (str name ":" numeric-code)}]
|
|
(map
|
|
(fn [p]
|
|
(let [pnl-data (-> p (filter-numeric-code numeric-code numeric-code))
|
|
this-name-exists? (->> (:data p)
|
|
(filter (comp #{name} :name))
|
|
seq)]
|
|
(merge
|
|
(if this-name-exists?
|
|
{:format :dollar
|
|
:filters (:filters pnl-data)
|
|
:value (aggregate-accounts pnl-data)}
|
|
{:filters (:filters pnl-data)
|
|
:value ""})
|
|
(:cell-args p))))
|
|
|
|
pnl-datas))))
|
|
(conj (subtotal-by-column-row pnl-datas "" {:border [:top]})))]
|
|
row)]
|
|
(-> [(into [{:value title
|
|
:bold true}]
|
|
(map
|
|
(fn [p]
|
|
(assoc (:cell-args p) :value "" :format ""))
|
|
pnl-datas))]
|
|
(into individual-accounts)
|
|
(conj (subtotal-by-column-row pnl-datas title)))))
|
|
|
|
(defn location-detail-table [pnl-datas client-datas title prefix]
|
|
(let [table (-> []
|
|
(into (detail-rows pnl-datas
|
|
:sales
|
|
(str prefix " Sales")))
|
|
(into (detail-rows pnl-datas
|
|
:cogs
|
|
(str prefix " COGS")))
|
|
(into (detail-rows
|
|
pnl-datas
|
|
:payroll
|
|
(str prefix " Payroll")))
|
|
(conj (subtotal-by-column-row (map #(filter-categories % [:payroll :cogs])
|
|
pnl-datas)
|
|
(str prefix " Prime Costs")))
|
|
(conj (subtotal-by-column-row (map #(-> %
|
|
(filter-categories [:sales :payroll :cogs])
|
|
(negate #{:payroll :cogs}))
|
|
pnl-datas)
|
|
(str prefix " Gross Profits")))
|
|
(into (detail-rows
|
|
pnl-datas
|
|
:controllable
|
|
(str prefix " Controllable Expenses")))
|
|
(into (detail-rows
|
|
pnl-datas
|
|
:fixed-overhead
|
|
(str prefix " Fixed Overhead")))
|
|
(into (detail-rows
|
|
pnl-datas
|
|
:ownership-controllable
|
|
(str prefix " Ownership Controllable")))
|
|
(conj (subtotal-by-column-row (map #(filter-categories % [:controllable :fixed-overhead :ownership-controllable]) pnl-datas)
|
|
(str prefix " Overhead")))
|
|
(conj (subtotal-by-column-row (map #(-> %
|
|
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
|
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
|
pnl-datas)
|
|
(str prefix " Net Income"))))
|
|
table (if (seq client-datas)
|
|
(conj table (subtotal-by-column-row (map #(-> %
|
|
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
|
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
|
client-datas)
|
|
"All Location Net Income"))
|
|
table)
|
|
percent-of-sales (calc-percent-of-sales table pnl-datas)
|
|
deltas (into [] (calc-deltas table))]
|
|
{:header (headers pnl-datas title)
|
|
:rows (combine-tables pnl-datas table percent-of-sales deltas)}))
|
|
|
|
(defn cash-flow-headers [[pnl-data :as pnl-datas] header-title]
|
|
(let [big-header (into [{:value header-title
|
|
:bold true}]
|
|
(map-indexed (fn [i p]
|
|
(cond-> {:value
|
|
(str (date->str (:start p))
|
|
" - "
|
|
(date->str (:end p)))
|
|
:colspan 3
|
|
:align :center
|
|
:bold true}
|
|
(odd? i) (assoc :bg-color [240 240 240])))
|
|
(:periods (:args pnl-data))))
|
|
sub-header (into [{:value "Account"}]
|
|
(mapcat
|
|
(fn [p]
|
|
[(merge {:value "Increases"
|
|
:align :right}
|
|
(:cell-args p))
|
|
(merge {:value "Decreases"
|
|
:align :right}
|
|
(:cell-args p))
|
|
(merge {:value "+/- in Cash"
|
|
:align :right}
|
|
(:cell-args p))])
|
|
pnl-datas))]
|
|
[big-header sub-header]))
|
|
|
|
(defn cash-flow-detail-rows
|
|
[pnl-datas grouping title]
|
|
(let [pnl-datas (map #(filter-categories % [grouping])
|
|
pnl-datas)
|
|
individual-accounts
|
|
(for [[grouping-name from to add-or-subtract] (groupings grouping)
|
|
:let [pnl-datas (map #(filter-numeric-code % from to)
|
|
pnl-datas)
|
|
account-codes (used-accounts pnl-datas)]
|
|
:when (seq account-codes)
|
|
row (-> [(into [{:value (str "---" grouping-name "---")}]
|
|
(mapcat
|
|
(fn [p]
|
|
[(assoc (:cell-args p) :value "" :format "")
|
|
(assoc (:cell-args p) :value "" :format "")
|
|
(assoc (:cell-args p) :value "" :format "")])
|
|
pnl-datas)
|
|
)]
|
|
(into (for [{:keys [numeric-code name]} account-codes]
|
|
(into [{:value name}]
|
|
(mapcat
|
|
(fn [p]
|
|
(let [pnl-data (-> p (filter-numeric-code numeric-code numeric-code))
|
|
aggregated (aggregate-accounts pnl-data)
|
|
credits (aggregate-credits pnl-data)
|
|
debits (aggregate-debits pnl-data)]
|
|
(if (dollars= (- debits credits) aggregated)
|
|
[(merge
|
|
{:format :dollar
|
|
:filters (:filters pnl-data)
|
|
:value debits}
|
|
(:cell-args p))
|
|
(merge
|
|
{:format :dollar
|
|
:filters (:filters pnl-data)
|
|
:value credits}
|
|
(:cell-args p))
|
|
(merge
|
|
{:format :dollar
|
|
:filters (:filters pnl-data)
|
|
:value (cashflow-account->amount numeric-code aggregated)}
|
|
(:cell-args p))]
|
|
[(merge
|
|
{:format :dollar
|
|
:filters (:filters pnl-data)
|
|
:value credits}
|
|
(:cell-args p))
|
|
(merge
|
|
{:format :dollar
|
|
:filters (:filters pnl-data)
|
|
:value debits}
|
|
(:cell-args p))
|
|
(merge
|
|
{:format :dollar
|
|
:filters (:filters pnl-data)
|
|
:value (cashflow-account->amount numeric-code aggregated)}
|
|
(:cell-args p))])))
|
|
|
|
pnl-datas))))
|
|
(conj (cashflow-subtotal-by-column-row pnl-datas "" {:border [:top]})))]
|
|
row)]
|
|
(-> [(into [{:value title
|
|
:bold true}]
|
|
(mapcat
|
|
(fn [p]
|
|
[(assoc (:cell-args p) :value "" :format "")
|
|
(assoc (:cell-args p) :value "" :format "")
|
|
(assoc (:cell-args p) :value "" :format "")])
|
|
pnl-datas))]
|
|
(into individual-accounts)
|
|
(conj (cashflow-subtotal-by-column-row pnl-datas title)))))
|
|
|
|
(defn cash-flows-table [pnl-datas #_client-datas title prefix]
|
|
(let [table (-> []
|
|
(conj (cashflow-subtotal-by-column-row (map #(-> %
|
|
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
|
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
|
pnl-datas)
|
|
"Net Income"))
|
|
(into (cash-flow-detail-rows pnl-datas
|
|
:operating-activities
|
|
(str prefix " Operating Activities")))
|
|
(into (cash-flow-detail-rows pnl-datas
|
|
:investment-activities
|
|
(str prefix " Investment Activities")))
|
|
(into (cash-flow-detail-rows pnl-datas
|
|
:financing-activities
|
|
(str prefix " Financing Activities")))
|
|
|
|
(conj (cashflow-subtotal-by-column-row (map #(-> %
|
|
(filter-categories [:operating-activities :investment-activities :financing-activities :sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
|
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
|
pnl-datas)
|
|
"Change in Cash and Cash Equivalents"))
|
|
|
|
(into (cash-flow-detail-rows pnl-datas
|
|
:cash
|
|
(str prefix " Bank Accounts / Cash")))
|
|
|
|
)]
|
|
{:header (cash-flow-headers pnl-datas "Cash Flow")
|
|
:rows table}))
|
|
|
|
(defn warning-message [pnl-data]
|
|
(let [errors (->> pnl-data
|
|
:data
|
|
(filter (fn [{:keys [numeric-code]}]
|
|
(nil? numeric-code))))
|
|
error-count (count errors)]
|
|
(when (> error-count 0)
|
|
(str "This report does not include "
|
|
(str/join ", "
|
|
(map #(str (:count %) " unresolved ledger entries for " (if (str/blank? (:location %))
|
|
" all locations"
|
|
(:location %)))
|
|
errors))))))
|
|
|
|
(defn summarize-pnl [pnl-data]
|
|
{:warning (warning-message pnl-data)
|
|
:summaries
|
|
(if (-> pnl-data :args :column-per-location)
|
|
[(location-summary-table (mapcat identity (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
|
|
(concat
|
|
(for [[client-id location] (client-locations pnl-data)]
|
|
(-> pnl-data
|
|
(filter-client client-id)
|
|
(filter-location location)
|
|
(filter-period period)
|
|
(zebra i)))
|
|
[(zebra (filter-locations (filter-period pnl-data period)
|
|
(map second (client-locations pnl-data))) i)])))
|
|
"All location Summary")]
|
|
(for [[client-id location] (client-locations pnl-data)]
|
|
(location-summary-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
|
|
(-> pnl-data
|
|
(filter-client client-id)
|
|
(filter-location location)
|
|
(filter-period period)
|
|
(zebra i)))
|
|
(str (-> pnl-data :clients-by-id (get client-id)) " (" location ") Summary"))))
|
|
:details
|
|
(doall (if (-> pnl-data :args :column-per-location)
|
|
[(location-detail-table (mapcat identity (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
|
|
(concat
|
|
(for [[client-id location] (client-locations pnl-data)]
|
|
(-> pnl-data
|
|
(filter-client client-id)
|
|
(filter-location location)
|
|
(filter-period period)
|
|
(zebra i)))
|
|
[(-> pnl-data
|
|
(filter-period period)
|
|
(filter-locations (map second (client-locations pnl-data)))
|
|
(zebra i))])))
|
|
nil
|
|
"All location Detail"
|
|
"")]
|
|
(for [[client-id location] (client-locations pnl-data)]
|
|
(location-detail-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
|
|
(-> pnl-data
|
|
(filter-client client-id)
|
|
(filter-location location)
|
|
(filter-period period)
|
|
(zebra i)))
|
|
(for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
|
|
(-> pnl-data
|
|
(filter-client client-id)
|
|
(filter-period period)
|
|
(filter-locations (map second (client-locations pnl-data)))
|
|
(zebra i)))
|
|
(str (-> pnl-data :clients-by-id (get client-id)) " (" location ") Detail")
|
|
location))))})
|
|
|
|
(defn summarize-cash-flows [pnl-data]
|
|
(let [client-ids (->> (client-locations pnl-data)
|
|
(map first)
|
|
set)]
|
|
{:warning (warning-message pnl-data)
|
|
:details
|
|
(doall (for [client-id client-ids]
|
|
(cash-flows-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
|
|
(-> pnl-data
|
|
(filter-client client-id)
|
|
(filter-period period)
|
|
(zebra i)))
|
|
(str (-> pnl-data :clients-by-id (get client-id)) " Detail")
|
|
"")))}))
|
|
|
|
|
|
(defn balance-sheet-headers [pnl-data]
|
|
(let [period-count (count (:periods (:args pnl-data)))]
|
|
(cond-> []
|
|
(> (count (set (map :client-id (:data pnl-data)))) 1)
|
|
(conj (into [{:value "Client"}]
|
|
|
|
(mapcat identity
|
|
(for [client (set (map :client-id (:data pnl-data))) ]
|
|
(cond-> [{:value (str (-> pnl-data :client-codes (get client)))}]
|
|
|
|
(> period-count 1)
|
|
(into (apply concat (repeat (dec period-count) ["" ""]))))))))
|
|
true
|
|
(conj (into [{:value "Period Ending"}]
|
|
(for [client (set (map :client-id (:data pnl-data)))
|
|
[index p] (map vector (range) (:periods (:args pnl-data)))
|
|
:let [is-first? (= 0 index)
|
|
period-date (date->str p)
|
|
period-headers (if is-first?
|
|
[{:value period-date}]
|
|
[{:value period-date}
|
|
{:value "+/-"}])]
|
|
header period-headers]
|
|
header))))))
|
|
|
|
(defn append-deltas [table]
|
|
(->> table
|
|
(map (fn [[title & values]]
|
|
(loop [result [title]
|
|
previous nil
|
|
[current :as values] values]
|
|
(if current
|
|
(recur
|
|
(cond-> result
|
|
true (conj current)
|
|
|
|
(and (:value current) (:value previous)
|
|
(number? (:value current)) (number? (:value previous))
|
|
(= (:client-id (:filters previous))
|
|
(:client-id (:filters current))))
|
|
(conj {:border (:border previous)
|
|
:format :dollar
|
|
:value (- (or (:value current) 0.0)
|
|
(or (:value previous) 0.0))}))
|
|
current
|
|
(rest values))
|
|
result))))))
|
|
|
|
#_(defn summarize-balance-sheet [pnl-data]
|
|
(reduce
|
|
(fn [result table]
|
|
(-> result
|
|
(update :header into (:header table))
|
|
(update :rows
|
|
(fn [current-rows]
|
|
(if (seq current-rows)
|
|
(map
|
|
concat
|
|
current-rows
|
|
(:rows table))
|
|
(:rows table))))))
|
|
{:header []
|
|
:rows []}
|
|
(for [client-id (set (map :client-id (:data pnl-data)))]
|
|
(let [pnl-datas (map (fn [p]
|
|
(-> pnl-data
|
|
(filter-client client-id)
|
|
(filter-period p)))
|
|
(:periods (:args pnl-data)))
|
|
table (-> []
|
|
(into (detail-rows pnl-datas
|
|
:assets
|
|
"Assets"))
|
|
(into (detail-rows pnl-datas
|
|
:liabilities
|
|
"Liabilities"))
|
|
(into (detail-rows pnl-datas
|
|
:equities
|
|
"Owner's Equity"))
|
|
(conj (subtotal-by-column-row
|
|
(map #(-> %
|
|
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
|
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
|
pnl-datas)
|
|
"Retained Earnings")))
|
|
table (if (:include-comparison (:args pnl-data))
|
|
(append-deltas table)
|
|
table)]
|
|
{:header (balance-sheet-headers pnl-data)
|
|
:rows table})))
|
|
)
|
|
|
|
(defn summarize-balance-sheet [pnl-data]
|
|
(let [pnl-datas (for [client-id (set (map :client-id (:data pnl-data)))
|
|
p (:periods (:args pnl-data))]
|
|
(-> pnl-data
|
|
(filter-client client-id)
|
|
(filter-period p)))]
|
|
(let [table (-> []
|
|
(into (detail-rows pnl-datas
|
|
:assets
|
|
"Assets"))
|
|
(into (detail-rows pnl-datas
|
|
:liabilities
|
|
"Liabilities"))
|
|
(into (detail-rows pnl-datas
|
|
:equities
|
|
"Owner's Equity"))
|
|
(conj (subtotal-by-column-row
|
|
(map #(-> %
|
|
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
|
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
|
pnl-datas)
|
|
"Retained Earnings")))
|
|
table (if (> (count (:periods (:args pnl-data))) 1)
|
|
(append-deltas table)
|
|
table)]
|
|
{:warning (warning-message pnl-data)
|
|
:header (balance-sheet-headers pnl-data)
|
|
:rows table}))
|
|
)
|
|
|
|
|
|
(defn journal-detail-report [args data client-codes]
|
|
{:header [[{:value "Category"}
|
|
{:value "Date"}
|
|
{:value "Description"}
|
|
{:value "Debit"}
|
|
{:value "Credit"}
|
|
{:value "Running Balance"}]]
|
|
:rows (reduce
|
|
(fn [rows category]
|
|
(into rows
|
|
;; TODO colspan ?
|
|
(concat (when (seq (:journal-entries category))
|
|
[[
|
|
{:value (str (client-codes (:client-id category)) " - " (:location category) " - " (:name (:account category)))}
|
|
{:value ""}
|
|
{:value ""}
|
|
{:value ""}
|
|
{:value ""}
|
|
{:value ""}]])
|
|
(map
|
|
(fn [je]
|
|
[{:value ""}
|
|
{:value (user-friendly-date (:date je))}
|
|
{:value (:description je "")}
|
|
{:value (get je :debit)
|
|
:format :dollar}
|
|
{:value (get je :credit)
|
|
:format :dollar}
|
|
{:value (get je :running-balance)
|
|
:format :dollar}])
|
|
(:journal-entries category))
|
|
[[
|
|
{:value (str (client-codes (:client-id category)) " - " (:location category) " - " (:name (:account category)))
|
|
:bold true
|
|
:border [:top]}
|
|
{:value ""
|
|
:border [:top]}
|
|
{:value (str "Total" )
|
|
:bold true
|
|
:border [:top]}
|
|
{:value ""
|
|
:border [:top]}
|
|
{:value ""
|
|
:border [:top]}
|
|
{:value (:total category)
|
|
:format :dollar
|
|
:bold true
|
|
:border [:top]}]]))
|
|
|
|
)
|
|
[]
|
|
(:categories data))}
|
|
)
|
|
|
|
|
|
(defrecord PNLData [args data client-codes])
|