Tweaks on PNL
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
(ns auto-ap.pdf.ledger
|
(ns auto-ap.pdf.ledger
|
||||||
(:require
|
(:require
|
||||||
[amazonica.aws.s3 :as s3]
|
[amazonica.aws.s3 :as s3]
|
||||||
|
[auto-ap.ledger.reports :as l-reports]
|
||||||
[auto-ap.datomic :refer [conn]]
|
[auto-ap.datomic :refer [conn]]
|
||||||
[auto-ap.graphql.utils :refer [<-graphql]]
|
[auto-ap.graphql.utils :refer [<-graphql]]
|
||||||
[auto-ap.time :as atime]
|
[auto-ap.time :as atime]
|
||||||
@@ -9,333 +10,18 @@
|
|||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[config.core :refer [env]]
|
[config.core :refer [env]]
|
||||||
[datomic.api :as d]
|
[datomic.api :as d])
|
||||||
[clojure.tools.logging :as log])
|
|
||||||
(:import
|
(:import
|
||||||
(java.io ByteArrayOutputStream)
|
(java.io ByteArrayOutputStream)
|
||||||
(java.text DecimalFormat)
|
(java.text DecimalFormat)
|
||||||
(java.util UUID)))
|
(java.util UUID)))
|
||||||
|
|
||||||
(defn date->str [d]
|
|
||||||
(atime/unparse-local d atime/normal-date))
|
|
||||||
|
|
||||||
(def ranges
|
|
||||||
{:sales [40000 49999]
|
|
||||||
:cogs [50000 59999]
|
|
||||||
:payroll [60000 69999]
|
|
||||||
:controllable [70000 79999]
|
|
||||||
:fixed-overhead [80000 89999]
|
|
||||||
:ownership-controllable [90000 99999]})
|
|
||||||
|
|
||||||
(def groupings
|
|
||||||
{: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]]})
|
|
||||||
|
|
||||||
(defn in-range? [code]
|
|
||||||
(if code
|
|
||||||
(reduce
|
|
||||||
(fn [acc [start end]]
|
|
||||||
(if (<= start code end)
|
|
||||||
(reduced true)
|
|
||||||
acc))
|
|
||||||
false
|
|
||||||
(vals ranges))
|
|
||||||
false))
|
|
||||||
|
|
||||||
(defn locations [data]
|
|
||||||
(->> data
|
|
||||||
(filter (comp in-range? :numeric-code))
|
|
||||||
(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 best-category [a]
|
|
||||||
(->> ranges
|
|
||||||
(filter (fn [[_ [start end]]]
|
|
||||||
(<= start (:numeric-code a) end)))
|
|
||||||
first
|
|
||||||
first))
|
|
||||||
|
|
||||||
(defn filter-client [pnl-data client]
|
|
||||||
(update pnl-data :data (fn [data]
|
|
||||||
((group-by :client-id data) client))))
|
|
||||||
|
|
||||||
(defn filter-location [pnl-data location]
|
|
||||||
(update pnl-data :data (fn [data]
|
|
||||||
((group-by :location data) location))))
|
|
||||||
|
|
||||||
(defn filter-categories [pnl-data categories]
|
|
||||||
(update pnl-data :data (fn [data]
|
|
||||||
(mapcat identity
|
|
||||||
((apply juxt categories)
|
|
||||||
(group-by best-category data))))))
|
|
||||||
|
|
||||||
(defn filter-period [pnl-data period]
|
|
||||||
(update pnl-data :data (fn [data]
|
|
||||||
((group-by :period data) period))))
|
|
||||||
|
|
||||||
(defn filter-numeric-code [pnl-data from to]
|
|
||||||
(update pnl-data :data (fn [data]
|
|
||||||
(filter
|
|
||||||
#(<= from (:numeric-code %) to)
|
|
||||||
data))))
|
|
||||||
|
|
||||||
(defn negate [pnl-data types]
|
|
||||||
(update pnl-data :data
|
|
||||||
(fn [accounts]
|
|
||||||
(map
|
|
||||||
(fn [account]
|
|
||||||
(if (types (best-category account))
|
|
||||||
(update account :amount -)
|
|
||||||
account))
|
|
||||||
accounts))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn used-accounts [pnl-data]
|
|
||||||
(->> (:data pnl-data)
|
|
||||||
(map #(select-keys % [:numeric-code :name]))
|
|
||||||
(set)
|
|
||||||
(sort-by :numeric-code)))
|
|
||||||
|
|
||||||
(defn subtotal-row [pnl-data title]
|
|
||||||
(into [{:value title
|
|
||||||
:bold true}]
|
|
||||||
(map
|
|
||||||
(fn [p]
|
|
||||||
{:format :dollar
|
|
||||||
:value (aggregate-accounts (filter-period pnl-data p))})
|
|
||||||
(-> pnl-data :args :periods))))
|
|
||||||
|
|
||||||
(defn calc-percent-of-sales [table pnl-data]
|
|
||||||
(let [sales-pnl-data (filter-categories pnl-data [:sales])
|
|
||||||
sales (map
|
|
||||||
(fn [p]
|
|
||||||
(aggregate-accounts (filter-period sales-pnl-data p)))
|
|
||||||
(-> pnl-data :args :periods))]
|
|
||||||
(->> table
|
|
||||||
(map (fn [[_ & values]]
|
|
||||||
(map
|
|
||||||
(fn [v s]
|
|
||||||
{:format :percent
|
|
||||||
:color [128 128 128]
|
|
||||||
:value (if (dollars-0? s)
|
|
||||||
0.0
|
|
||||||
(/ (:value v) s))})
|
|
||||||
values sales))))))
|
|
||||||
|
|
||||||
(defn calc-deltas [table]
|
|
||||||
(->> table
|
|
||||||
(map (fn [[_ & values]]
|
|
||||||
(->> values
|
|
||||||
(partition 2 1)
|
|
||||||
(map (fn [[a b]]
|
|
||||||
{:format :dollar
|
|
||||||
:value (- (:value b)
|
|
||||||
(:value a))})))))))
|
|
||||||
|
|
||||||
(defn combine-tables [pnl-data table percent-of-sales deltas]
|
|
||||||
(map (fn [[title & row] percent-of-sales deltas ]
|
|
||||||
(let [deltas (cons 0.0 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 header-title]
|
|
||||||
(let [big-header (into [{:value header-title
|
|
||||||
:bold true}]
|
|
||||||
(map (fn [p]
|
|
||||||
{:value
|
|
||||||
(str (date->str (:start p))
|
|
||||||
" - "
|
|
||||||
(date->str (:end p)))
|
|
||||||
:colspan (if (-> pnl-data :args :include-deltas)
|
|
||||||
3
|
|
||||||
2)
|
|
||||||
:align :center
|
|
||||||
:bold true})
|
|
||||||
(:periods (:args pnl-data))))
|
|
||||||
sub-header (into [{:value ""}]
|
|
||||||
(mapcat
|
|
||||||
(fn [_]
|
|
||||||
(cond-> [{:value "Amt"
|
|
||||||
:align :right}
|
|
||||||
{:value "%"
|
|
||||||
:align :right}]
|
|
||||||
(-> pnl-data :args :include-deltas) (conj {:value "+/-"
|
|
||||||
:align :right})))
|
|
||||||
(:periods (:args pnl-data))))]
|
|
||||||
[big-header
|
|
||||||
sub-header]))
|
|
||||||
|
|
||||||
(defn location-summary-table [pnl-data title]
|
|
||||||
(let [table [(subtotal-row (filter-categories pnl-data [:sales]) "Sales")
|
|
||||||
(subtotal-row (filter-categories pnl-data [:cogs ]) "Cogs")
|
|
||||||
|
|
||||||
(subtotal-row (filter-categories pnl-data [:payroll ]) "Payroll")
|
|
||||||
|
|
||||||
(subtotal-row (-> pnl-data
|
|
||||||
(filter-categories [:sales :payroll :cogs])
|
|
||||||
(negate #{:payroll :cogs}))
|
|
||||||
"Gross Profits")
|
|
||||||
|
|
||||||
(subtotal-row (filter-categories pnl-data [:controllable :fixed-overhead :ownership-controllable])
|
|
||||||
"Overhead")
|
|
||||||
|
|
||||||
(subtotal-row (-> pnl-data
|
|
||||||
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
|
||||||
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
|
||||||
"Net Income")]
|
|
||||||
percent-of-sales (calc-percent-of-sales table pnl-data)
|
|
||||||
deltas (calc-deltas table)]
|
|
||||||
{:header (headers pnl-data title)
|
|
||||||
:rows (combine-tables pnl-data table percent-of-sales deltas)}))
|
|
||||||
|
|
||||||
|
|
||||||
(defn detail-rows [pnl-data grouping title]
|
|
||||||
(let [pnl-data (filter-categories pnl-data [grouping])
|
|
||||||
individual-accounts
|
|
||||||
(for [[grouping-name from to] (groupings grouping)
|
|
||||||
:let [pnl-data (filter-numeric-code pnl-data from to)
|
|
||||||
account-codes (used-accounts pnl-data)]
|
|
||||||
:when (seq account-codes)
|
|
||||||
row (-> [[{:value (str "---" grouping-name "---")}]]
|
|
||||||
(into (for [{:keys [numeric-code name]} account-codes]
|
|
||||||
(into [{:value name}]
|
|
||||||
(map
|
|
||||||
(fn [p]
|
|
||||||
{:format :dollar
|
|
||||||
:value (-> pnl-data
|
|
||||||
(filter-numeric-code numeric-code numeric-code)
|
|
||||||
(filter-period p)
|
|
||||||
(aggregate-accounts))})
|
|
||||||
|
|
||||||
(-> pnl-data :args :periods))))))]
|
|
||||||
row)]
|
|
||||||
(-> [[{:value title
|
|
||||||
:bold true}]]
|
|
||||||
(into individual-accounts)
|
|
||||||
(conj (subtotal-row pnl-data title)))))
|
|
||||||
|
|
||||||
(defn location-detail-table [pnl-data title]
|
|
||||||
(let [table (-> []
|
|
||||||
(into (detail-rows pnl-data
|
|
||||||
:sales
|
|
||||||
(str (:prefix pnl-data) " Sales")))
|
|
||||||
(into (detail-rows pnl-data
|
|
||||||
:cogs
|
|
||||||
(str (:prefix pnl-data) " COGS")))
|
|
||||||
(into (detail-rows
|
|
||||||
pnl-data
|
|
||||||
:payroll
|
|
||||||
(str (:prefix pnl-data) " Payroll")))
|
|
||||||
(conj (subtotal-row (filter-categories pnl-data [:payroll :cogs])
|
|
||||||
(str (:prefix pnl-data) " Prime Costs")))
|
|
||||||
(conj (subtotal-row (-> pnl-data
|
|
||||||
(filter-categories [:sales :payroll :cogs])
|
|
||||||
(negate #{:payroll :cogs}))
|
|
||||||
(str (:prefix pnl-data) " Gross Profits")))
|
|
||||||
(into (detail-rows
|
|
||||||
pnl-data
|
|
||||||
:fixed-overhead
|
|
||||||
(str (:prefix pnl-data) " Fixed Overhead")))
|
|
||||||
(into (detail-rows
|
|
||||||
pnl-data
|
|
||||||
:ownership-controllable
|
|
||||||
(str (:prefix pnl-data) " Ownership Controllable")))
|
|
||||||
(conj (subtotal-row (-> pnl-data
|
|
||||||
(filter-categories [:controllable :fixed-overhead :ownership-controllable]))
|
|
||||||
(str (:prefix pnl-data) " Overhead")))
|
|
||||||
(conj (subtotal-row (-> pnl-data
|
|
||||||
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
|
||||||
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
|
||||||
(str (:prefix pnl-data) " Net Income"))))
|
|
||||||
percent-of-sales (calc-percent-of-sales table pnl-data)
|
|
||||||
deltas (into [] (calc-deltas table))]
|
|
||||||
{:header (headers pnl-data title)
|
|
||||||
:rows (combine-tables pnl-data table percent-of-sales deltas)}))
|
|
||||||
|
|
||||||
(defn summarize-pnl [pnl-data]
|
|
||||||
{:summaries (for [[client-id location] (locations (:data pnl-data))]
|
|
||||||
(location-summary-table (-> pnl-data
|
|
||||||
(filter-client client-id)
|
|
||||||
(filter-location location))
|
|
||||||
(str (-> pnl-data :clients-by-id (get client-id) :client/name) " (" location ") Summary")))
|
|
||||||
:details (for [[client-id location] (locations (:data pnl-data))]
|
|
||||||
(location-detail-table (-> pnl-data
|
|
||||||
(filter-client client-id)
|
|
||||||
(filter-location location)
|
|
||||||
(assoc :prefix location))
|
|
||||||
(str (-> pnl-data :clients-by-id (get client-id) :client/name) " (" location ") Detail")))})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(defrecord PNLData [args data clients-by-id])
|
|
||||||
|
|
||||||
(defn cell->pdf [cell]
|
(defn cell->pdf [cell]
|
||||||
[:pdf-cell
|
[:pdf-cell
|
||||||
(cond-> {}
|
(cond-> {}
|
||||||
|
|
||||||
|
(:border cell) (assoc :border true
|
||||||
|
:set-border (:border cell))
|
||||||
(:colspan cell) (assoc :colspan (:colspan cell))
|
(:colspan cell) (assoc :colspan (:colspan cell))
|
||||||
(:align cell) (assoc :align (:align cell))
|
(:align cell) (assoc :align (:align cell))
|
||||||
(= :dollar (:format cell)) (assoc :align :right)
|
(= :dollar (:format cell)) (assoc :align :right)
|
||||||
@@ -467,8 +153,8 @@
|
|||||||
(:accounts p2))
|
(:accounts p2))
|
||||||
)
|
)
|
||||||
(:periods args)))
|
(:periods args)))
|
||||||
pnl-data (PNLData. args data (by :db/id clients))
|
pnl-data (l-reports/PNLData. args data (by :db/id clients))
|
||||||
report (summarize-pnl pnl-data)
|
report (l-reports/summarize-pnl pnl-data)
|
||||||
output-stream (ByteArrayOutputStream.)]
|
output-stream (ByteArrayOutputStream.)]
|
||||||
(pdf/pdf
|
(pdf/pdf
|
||||||
(into
|
(into
|
||||||
@@ -513,7 +199,7 @@
|
|||||||
(let [uuid (str (UUID/randomUUID))
|
(let [uuid (str (UUID/randomUUID))
|
||||||
pdf-data (make-pnl args data)
|
pdf-data (make-pnl args data)
|
||||||
name (args->name args)
|
name (args->name args)
|
||||||
key (str "reports/pnl/" uuid "/ " name ".pdf")
|
key (str "reports/pnl/" uuid "/" name ".pdf")
|
||||||
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" key)]
|
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" key)]
|
||||||
(s3/put-object :bucket-name (:data-bucket env)
|
(s3/put-object :bucket-name (:data-bucket env)
|
||||||
:key key
|
:key key
|
||||||
|
|||||||
338
src/cljc/auto_ap/ledger/reports.cljc
Normal file
338
src/cljc/auto_ap/ledger/reports.cljc
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
(ns auto-ap.ledger.reports
|
||||||
|
(:require
|
||||||
|
[auto-ap.utils :refer [dollars-0?]]
|
||||||
|
#?(:cljs [auto-ap.views.utils :as au ]
|
||||||
|
:clj [auto-ap.time :as atime])))
|
||||||
|
|
||||||
|
|
||||||
|
(defn date->str [d]
|
||||||
|
#?(:clj
|
||||||
|
(atime/unparse-local d atime/normal-date)
|
||||||
|
:cljs (au/date->str d au/standard)))
|
||||||
|
|
||||||
|
|
||||||
|
(def ranges
|
||||||
|
{:sales [40000 49999]
|
||||||
|
:cogs [50000 59999]
|
||||||
|
:payroll [60000 69999]
|
||||||
|
:controllable [70000 79999]
|
||||||
|
:fixed-overhead [80000 89999]
|
||||||
|
:ownership-controllable [90000 99999]})
|
||||||
|
|
||||||
|
(def groupings
|
||||||
|
{: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]]})
|
||||||
|
|
||||||
|
(defn in-range? [code]
|
||||||
|
(if code
|
||||||
|
(reduce
|
||||||
|
(fn [acc [start end]]
|
||||||
|
(if (<= start code end)
|
||||||
|
(reduced true)
|
||||||
|
acc))
|
||||||
|
false
|
||||||
|
(vals ranges))
|
||||||
|
false))
|
||||||
|
|
||||||
|
(defn locations [data]
|
||||||
|
(->> data
|
||||||
|
(filter (comp in-range? :numeric-code))
|
||||||
|
(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 best-category [a]
|
||||||
|
(->> ranges
|
||||||
|
(filter (fn [[_ [start end]]]
|
||||||
|
(<= start (:numeric-code a) end)))
|
||||||
|
first
|
||||||
|
first))
|
||||||
|
|
||||||
|
(defn filter-client [pnl-data client]
|
||||||
|
(update pnl-data :data (fn [data]
|
||||||
|
((group-by :client-id data) client))))
|
||||||
|
|
||||||
|
(defn filter-location [pnl-data location]
|
||||||
|
(update pnl-data :data (fn [data]
|
||||||
|
((group-by :location data) location))))
|
||||||
|
|
||||||
|
(defn filter-categories [pnl-data categories]
|
||||||
|
(update pnl-data :data (fn [data]
|
||||||
|
(mapcat identity
|
||||||
|
((apply juxt categories)
|
||||||
|
(group-by best-category data))))))
|
||||||
|
|
||||||
|
(defn filter-period [pnl-data period]
|
||||||
|
(update pnl-data :data (fn [data]
|
||||||
|
((group-by :period data) period))))
|
||||||
|
|
||||||
|
(defn filter-numeric-code [pnl-data from to]
|
||||||
|
(update pnl-data :data (fn [data]
|
||||||
|
(filter
|
||||||
|
#(<= from (:numeric-code %) to)
|
||||||
|
data))))
|
||||||
|
|
||||||
|
(defn negate [pnl-data types]
|
||||||
|
(update pnl-data :data
|
||||||
|
(fn [accounts]
|
||||||
|
(map
|
||||||
|
(fn [account]
|
||||||
|
(if (types (best-category account))
|
||||||
|
(update account :amount -)
|
||||||
|
account))
|
||||||
|
accounts))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn used-accounts [pnl-data]
|
||||||
|
(->> (:data pnl-data)
|
||||||
|
(map #(select-keys % [:numeric-code :name]))
|
||||||
|
(set)
|
||||||
|
(sort-by :numeric-code)))
|
||||||
|
|
||||||
|
(defn subtotal-row [pnl-data title & [cell-args]]
|
||||||
|
(into [{:value title
|
||||||
|
:bold true}]
|
||||||
|
(map
|
||||||
|
(fn [p]
|
||||||
|
(merge
|
||||||
|
{:format :dollar
|
||||||
|
:value (aggregate-accounts (filter-period pnl-data p))}
|
||||||
|
cell-args))
|
||||||
|
(-> pnl-data :args :periods))))
|
||||||
|
|
||||||
|
(defn calc-percent-of-sales [table pnl-data]
|
||||||
|
(let [sales-pnl-data (filter-categories pnl-data [:sales])
|
||||||
|
sales (map
|
||||||
|
(fn [p]
|
||||||
|
(aggregate-accounts (filter-period sales-pnl-data p)))
|
||||||
|
(-> pnl-data :args :periods))]
|
||||||
|
(->> table
|
||||||
|
(map (fn [[_ & values]]
|
||||||
|
(map
|
||||||
|
(fn [v s]
|
||||||
|
{:border (:border v)
|
||||||
|
:format :percent
|
||||||
|
:color [128 128 128]
|
||||||
|
:value (if (dollars-0? s)
|
||||||
|
0.0
|
||||||
|
(/ (:value v) s))})
|
||||||
|
values sales))))))
|
||||||
|
|
||||||
|
(defn calc-deltas [table]
|
||||||
|
(->> table
|
||||||
|
(map (fn [[_ & values]]
|
||||||
|
(->> values
|
||||||
|
(partition 2 1)
|
||||||
|
(map (fn [[a b]]
|
||||||
|
{:border (:border b)
|
||||||
|
:format :dollar
|
||||||
|
:value (- (:value b)
|
||||||
|
(:value a))})))))))
|
||||||
|
|
||||||
|
(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 header-title]
|
||||||
|
(let [big-header (into [{:value header-title
|
||||||
|
:bold true}]
|
||||||
|
(map (fn [p]
|
||||||
|
{:value
|
||||||
|
(str (date->str (:start p))
|
||||||
|
" - "
|
||||||
|
(date->str (:end p)))
|
||||||
|
:colspan (if (-> pnl-data :args :include-deltas)
|
||||||
|
3
|
||||||
|
2)
|
||||||
|
:align :center
|
||||||
|
:bold true})
|
||||||
|
(:periods (:args pnl-data))))
|
||||||
|
sub-header (into [{:value ""}]
|
||||||
|
(mapcat
|
||||||
|
(fn [_]
|
||||||
|
(cond-> [{:value "Amt"
|
||||||
|
:align :right}
|
||||||
|
{:value "%"
|
||||||
|
:align :right}]
|
||||||
|
(-> pnl-data :args :include-deltas) (conj {:value "+/-"
|
||||||
|
:align :right})))
|
||||||
|
(:periods (:args pnl-data))))]
|
||||||
|
[big-header
|
||||||
|
sub-header]))
|
||||||
|
|
||||||
|
(defn location-summary-table [pnl-data title]
|
||||||
|
(let [table [(subtotal-row (filter-categories pnl-data [:sales]) "Sales")
|
||||||
|
(subtotal-row (filter-categories pnl-data [:cogs ]) "Cogs")
|
||||||
|
|
||||||
|
(subtotal-row (filter-categories pnl-data [:payroll ]) "Payroll")
|
||||||
|
|
||||||
|
(subtotal-row (-> pnl-data
|
||||||
|
(filter-categories [:sales :payroll :cogs])
|
||||||
|
(negate #{:payroll :cogs}))
|
||||||
|
"Gross Profits")
|
||||||
|
|
||||||
|
(subtotal-row (filter-categories pnl-data [:controllable :fixed-overhead :ownership-controllable])
|
||||||
|
"Overhead")
|
||||||
|
|
||||||
|
(subtotal-row (-> pnl-data
|
||||||
|
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
||||||
|
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
||||||
|
"Net Income")]
|
||||||
|
percent-of-sales (calc-percent-of-sales table pnl-data)
|
||||||
|
deltas (calc-deltas table)]
|
||||||
|
{:header (headers pnl-data title)
|
||||||
|
:rows (combine-tables pnl-data table percent-of-sales deltas)}))
|
||||||
|
|
||||||
|
|
||||||
|
(defn detail-rows [pnl-data grouping title]
|
||||||
|
(let [pnl-data (filter-categories pnl-data [grouping])
|
||||||
|
individual-accounts
|
||||||
|
(for [[grouping-name from to] (groupings grouping)
|
||||||
|
:let [pnl-data (filter-numeric-code pnl-data from to)
|
||||||
|
account-codes (used-accounts pnl-data)]
|
||||||
|
:when (seq account-codes)
|
||||||
|
row (-> [[{:value (str "---" grouping-name "---")}]]
|
||||||
|
(into (for [{:keys [numeric-code name]} account-codes]
|
||||||
|
(into [{:value name}]
|
||||||
|
(map
|
||||||
|
(fn [p]
|
||||||
|
{:format :dollar
|
||||||
|
:value (-> pnl-data
|
||||||
|
(filter-numeric-code numeric-code numeric-code)
|
||||||
|
(filter-period p)
|
||||||
|
(aggregate-accounts))})
|
||||||
|
|
||||||
|
(-> pnl-data :args :periods)))))
|
||||||
|
(conj (subtotal-row pnl-data "" {:border [:top]})))]
|
||||||
|
row)]
|
||||||
|
(-> [[{:value title
|
||||||
|
:bold true}]]
|
||||||
|
(into individual-accounts)
|
||||||
|
(conj (subtotal-row pnl-data title)))))
|
||||||
|
|
||||||
|
(defn location-detail-table [pnl-data client-data title]
|
||||||
|
(let [table (-> []
|
||||||
|
(into (detail-rows pnl-data
|
||||||
|
:sales
|
||||||
|
(str (:prefix pnl-data) " Sales")))
|
||||||
|
(into (detail-rows pnl-data
|
||||||
|
:cogs
|
||||||
|
(str (:prefix pnl-data) " COGS")))
|
||||||
|
(into (detail-rows
|
||||||
|
pnl-data
|
||||||
|
:payroll
|
||||||
|
(str (:prefix pnl-data) " Payroll")))
|
||||||
|
(conj (subtotal-row (filter-categories pnl-data [:payroll :cogs])
|
||||||
|
(str (:prefix pnl-data) " Prime Costs")))
|
||||||
|
(conj (subtotal-row (-> pnl-data
|
||||||
|
(filter-categories [:sales :payroll :cogs])
|
||||||
|
(negate #{:payroll :cogs}))
|
||||||
|
(str (:prefix pnl-data) " Gross Profits")))
|
||||||
|
(into (detail-rows
|
||||||
|
pnl-data
|
||||||
|
:fixed-overhead
|
||||||
|
(str (:prefix pnl-data) " Fixed Overhead")))
|
||||||
|
(into (detail-rows
|
||||||
|
pnl-data
|
||||||
|
:ownership-controllable
|
||||||
|
(str (:prefix pnl-data) " Ownership Controllable")))
|
||||||
|
(conj (subtotal-row (-> pnl-data
|
||||||
|
(filter-categories [:controllable :fixed-overhead :ownership-controllable]))
|
||||||
|
(str (:prefix pnl-data) " Overhead")))
|
||||||
|
(conj (subtotal-row (-> pnl-data
|
||||||
|
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
||||||
|
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
||||||
|
(str (:prefix pnl-data) " Net Income")))
|
||||||
|
(conj (subtotal-row (-> client-data
|
||||||
|
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
|
||||||
|
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
|
||||||
|
"All Location Net Income")))
|
||||||
|
percent-of-sales (calc-percent-of-sales table pnl-data)
|
||||||
|
deltas (into [] (calc-deltas table))]
|
||||||
|
{:header (headers pnl-data title)
|
||||||
|
:rows (combine-tables pnl-data table percent-of-sales deltas)}))
|
||||||
|
|
||||||
|
(defn summarize-pnl [pnl-data]
|
||||||
|
{:summaries (for [[client-id location] (locations (:data pnl-data))]
|
||||||
|
(location-summary-table (-> pnl-data
|
||||||
|
(filter-client client-id)
|
||||||
|
(filter-location location))
|
||||||
|
(str (-> pnl-data :clients-by-id (get client-id) :client/name) " (" location ") Summary")))
|
||||||
|
:details (for [[client-id location] (locations (:data pnl-data))]
|
||||||
|
(location-detail-table (-> pnl-data
|
||||||
|
(filter-client client-id)
|
||||||
|
(filter-location location)
|
||||||
|
(assoc :prefix location))
|
||||||
|
(-> pnl-data
|
||||||
|
(filter-client client-id))
|
||||||
|
(str (-> pnl-data :clients-by-id (get client-id) :client/name) " (" location ") Detail")))})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defrecord PNLData [args data clients-by-id])
|
||||||
Reference in New Issue
Block a user