progress on a shared pnl with pdf.

This commit is contained in:
2022-03-18 23:00:57 -07:00
parent 6e41ada061
commit f536d1ac5e
6 changed files with 1646 additions and 172 deletions

View File

@@ -0,0 +1,456 @@
(ns auto-ap.pdf.ledger
(:require
[amazonica.aws.s3 :as s3]
[auto-ap.datomic :refer [conn]]
[auto-ap.graphql.utils :refer [<-graphql]]
[auto-ap.time :as atime]
[auto-ap.utils :refer [dollars-0?]]
[clj-pdf.core :as pdf]
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.walk :refer [postwalk]]
[config.core :refer [env]]
[datomic.api :as d])
(:import
(java.io ByteArrayOutputStream)
(java.util UUID)))
(defn distribute [nums]
(let [sum (reduce + 0 nums)]
(map #(* 100 (/ % sum)) nums)))
(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]
(reduce
(fn [acc [start end]]
(if (<= start code end)
(reduced true)
acc))
false
(vals ranges)))
(defn locations [data]
(->> data
:periods
(mapcat :accounts)
(filter (comp in-range? :numeric-code))
(group-by (juxt :client-id :location))
(filter (fn [[k 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 expand [data]
(postwalk (fn [x]
(cond
(map-entry? x)
x
(sequential? x)
(vec (mapcat (fn [r]
(cond (and (sequential? r)
(= :<> (first r)))
(filter identity (rest r))
:else
[r]))
x))
:else
x
))
data))
(defn map-periods [for-every between periods include-deltas]
(into [:<>]
(for [[_ i] (map vector periods (range))]
[:<> (for-every i)
(if (and include-deltas (not= 0 i))
(between i))])))
(defn period-header [{:keys [include_deltas periods]}]
[
[[:cell "Period"]
[:<>
(map-periods
(fn [i]
[:cell {:colspan 2}
(str (date->str (get-in periods [i :start])) " - " (date->str (get-in periods [i :end])))])
(fn [i]
[:cell ""])
periods
include_deltas)]]
[[:cell ""]
[:<> (map-periods
(fn [i]
[:<>
[:cell
"Amount"]
[:cell
"% Sales"]])
(fn [i]
[:cell "𝝙"])
periods
include_deltas)]
]])
(defn all-accounts [data]
(transduce
(comp
(map #(->> (:accounts %)
(group-by (juxt :numeric-code :client-id :location))
(map (fn [[k v]]
[k
(reduce (fn [a n]
(-> a
(update :count (fn [z] (+ z (:count n))))
(update :amount (fn [z] (+ z (:amount n))))))
(first v)
(rest v))]))
(into {}))))
conj
[]
(:periods data)))
(defn filter-accounts [accounts period [from to] only-client only-location]
(->> (get accounts period)
vals
(filter (fn [{:keys [location client-id numeric-code]}]
(and (or (nil? only-location)
(= only-location location))
(or (nil? only-client)
(= only-client client-id))
(<= from numeric-code to))))
(sort-by :numeric-code)))
(defn aggregate-accounts [accounts]
(reduce (fnil + 0.0) 0.0 (map :amount accounts)))
(defn used-accounts [accounts [from to] client-id location]
(->> accounts
(mapcat vals)
(filter #(<= from (:numeric-code %) to))
(filter #(= client-id (:client-id %)))
(filter #(= location (:location %)))
(map #(select-keys % [:numeric-code :name]))
(set)
(sort-by :numeric-code)))
(defn subtotal-row [args data types negs title client-id location]
(let [all-accounts (all-accounts data)
raw (map-indexed
(fn [i p]
(aggregate-accounts (mapcat (fn [t]
(cond->> (filter-accounts all-accounts i (ranges t) client-id location)
(negs t) (map #(update % :amount -))))
types))
)
(:periods args))
sales (map-indexed
(fn [i _]
(aggregate-accounts (filter-accounts all-accounts i (ranges :sales) client-id location)))
(:periods args))
deltas (->> raw
(partition-all 2)
(map (fn [[a b]]
(- b
a))))]
(into [title]
(->> raw
(map (fn [s r]
[r (if (dollars-0? s)
0.0
(/ r s))])
sales)
(partition-all 2)
(mapcat (fn [d [[a a-sales] [b b-sales]]]
[a a-sales b b-sales d]
)
deltas)))))
(defn location-summary-table [args data client-id location]
[(subtotal-row args data [:sales] #{} "Sales" client-id location)
(subtotal-row args data [:cogs ] #{} "Cogs" client-id location)
(subtotal-row args data [:payroll ]#{} "Payroll" client-id location)
(subtotal-row args data [:sales :payroll :cogs] #{:payroll :cogs} "Gross Profits" client-id location)
(subtotal-row args data [:controllable :fixed-overhead :ownership-controllable] #{} "Overhead" client-id location)
(subtotal-row args data [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" client-id location)])
(defn detail-sub-rows [args data grouping client-id location]
(let [all-accounts (all-accounts data)]
(for [[grouping-name from to] grouping
:let [account-codes (used-accounts all-accounts [from to] client-id location)]
:when (seq account-codes)]
(->
[[(str "---" grouping-name "---")]]
(into (for [{:keys [numeric-code name]} account-codes]
(let [raw (map-indexed
(fn [i p]
(get-in all-accounts [i [numeric-code client-id location] :amount] 0.0))
(:periods args))
sales (map-indexed
(fn [i _]
(aggregate-accounts (filter-accounts all-accounts i (ranges :sales) client-id location)))
(:periods args))
deltas (->> raw
(partition-all 2)
(map (fn [[a b]]
(- b
a))))]
(into [name]
(->> raw
(map (fn [s r]
[r (if (dollars-0? s)
0.0
(/ r s))])
sales)
(partition-all 2)
(mapcat (fn [d [[a a-sales] [b b-sales]]]
[a a-sales b b-sales d]
)
deltas))))
#_[:tr
[:td name]
#_(map-periods
(fn [i]
(let [amount (get-in all-accounts [i [numeric-code client-id location] :amount] 0.0)]
[:<>
[:td.has-text-right (if multi-client?
[:span (->$ amount)]
[:a {:on-click (dispatch-event [::investigate-clicked location numeric-code numeric-code i :current])
:disabled (boolean multi-client?)}
(->$ amount)])]
[:td.has-text-right (->% (percent-of-sales amount all-accounts i client-id location))]]))
(fn [i]
[:td.has-text-right (->$ (- (get-in all-accounts [i [numeric-code client-id location] :amount] 0.0)
(get-in all-accounts [(dec i) [numeric-code client-id location] :amount] 0.0)))])
periods
include-deltas)])))
)))
(defn detail-rows [args data type title client-id location]
(-> [[title]]
(into (detail-sub-rows args data (type groupings) client-id location))
(conj (subtotal-row args data [type] #{} title client-id location))))
(defn location-detail-table [args data client-id location]
(-> []
(into (detail-rows args data :sales (str location " Sales") client-id location))))
(defn summarize-pnl [args data]
{:summaries (for [[client-id location] (locations data)]
(location-summary-table args data client-id location))
:details (for [[client-id location] (locations data)]
(location-detail-table args data client-id location))}
)
(defn make-pnl [args data]
(let [data (<-graphql data)
args (<-graphql args)
_ (clojure.pprint/pprint (summarize-pnl (assoc args :deltas true) data))output-stream (ByteArrayOutputStream.)
_ (println (:client_ids args))
clients (d/pull-many (d/db conn) '[:client/name] (:client-ids args))]
(pdf/pdf
(expand
[{:left-margin 25 :right-margin 0 :top-margin 0 :bottom-margin 0 :size :letter}
[:heading (str "Profit and Loss - " (str/join ", " (map :client/name clients)))]
#_(for [[client-id location] (locations data)]
^{:key (str client-id "-" location "-summary")}
(location-summary args data client-id location (:include-deltas data))
)
#_(let [{:keys [bank-account paid-to client check date amount memo] {print-as :vendor/print-as vendor-name :vendor/name :as vendor} :vendor} check
df (DecimalFormat. "#,###.00")]
[:table {:num-cols 6 :border false :leading 11 :widths (distribute [2 2 2 2 2 2])}
[(let [{:keys [:client/name] {:keys [:address/street1 :address/city :address/state :address/zip]} :client/address} client]
[:cell {:colspan 4 } [:paragraph {:leading 14} name "\n" street1 "\n" (str city ", " state " " zip)] ])
(let [{:keys [:bank-account/bank-name :bank-account/bank-code] } bank-account]
[:cell {:colspan 6 :align :center} [:paragraph {:style :bold} bank-name] [:paragraph {:size 8 :leading 8} bank-code]])
[:cell {:colspan 2 :size 13}
check]]
[[:cell {:colspan 9}]
[:cell {:colspan 3 :leading -10} date]]
[[:cell {:colspan 12 :size 14}]
]
[[:cell {:size 13 :leading 13} "PAY"]
[:cell {:size 8 :leading 8 } "TO THE ORDER OF"]
[:cell {:colspan 7} (if (seq print-as)
print-as
vendor-name)]
[:cell {:colspan 3} amount]]
[[:cell {}]
[:cell {:colspan 8} (str " -- " word-amount " " (str/join "" (take (max
2
(- 95
(count word-amount)))
(repeat "-"))))
[:line {:line-width 0.15 :color [50 50 50]}]]
[:cell {:colspan 3}]]
[[:cell {:size 9 :leading 11.5} "\n\n\n\n\nMEMO"]
[:cell {:colspan 5 :leading 11.5} (split-memo memo)
[:line {:line-width 0.15 :color [50 50 50]}]]
[:cell {:colspan 6 } (if (:client/signature-file client)
[:image { :top-margin 90 :xscale 0.30 :yscale 0.30 :align :center}
(:client/signature-file client)]
[:spacer])]]
#_[
#_[:cell {:colspan 5} #_memo ]
#_[:cell {:colspan 6}]]
[[:cell {:colspan 2}]
[:cell {:colspan 10 :leading 30}
[:phrase {:size 18 :ttf-name "public/micrenc.ttf"} (str "c" check "c a" (:bank-account/routing bank-account) "a " (:bank-account/number bank-account) "c")]]]
[[:cell {:colspan 12 :leading 18} [:spacer]]]
[[:cell]
(into
[:cell {:colspan 9}]
(let [{:keys [:client/name]
{:keys [:address/street1 :address/street2 :address/city :address/state :address/zip ]} :client/address} client]
(filter identity
(list
[:paragraph " " name]
[:paragraph " " street1]
(when (not (str/blank? street2))
[:paragraph " " street2])
[:paragraph " " city ", " state " " zip]))))
[:cell {:colspan 2 :size 13}
check]]
[[:cell {:colspan 12 :leading 74} [:spacer]]]
[[:cell]
[:cell {:colspan 5} [:paragraph
" " vendor-name "\n"
" " (:address/street1 (:vendor/address vendor)) "\n"
(when (not (str/blank? (:address/street2 (:vendor/address vendor))))
(str " " (:address/street2 (:vendor/address vendor)) "\n")
)
" " (:address/city (:vendor/address vendor)) ", " (:address/state (:vendor/address vendor)) " " (:address/zip (:vendor/address vendor))]]
[:cell {:align :right}
"Paid to:\n"
"Amount:\n"
"Date:\n"]
[:cell {:colspan 5}
[:paragraph paid-to]
[:paragraph amount]
[:paragraph date]]]
[[:cell {:colspan 3} "Memo:"]
[:cell {:colspan 9} memo]]
[[:cell {:colspan 12} [:spacer]]]
[[:cell {:colspan 12} [:spacer]]]
[[:cell {:colspan 12} [:spacer]]]
[[:cell {:colspan 12} [:spacer]]]
[[:cell {:colspan 5}]
[:cell {:align :right :colspan 2}
"Check:\n"
"Vendor:\n"
"Company:\n"
"Bank Account:\n"
"Paid To:\n"
"Amount:\n"
"Date:\n"]
[:cell {:colspan 5}
[:paragraph check]
[:paragraph vendor-name]
[:paragraph (:client/name client)]
[:paragraph (:bank-account/bank-name bank-account)]
[:paragraph paid-to]
[:paragraph amount]
[:paragraph date]]]
[[:cell {:colspan 3} "Memo:"]
[:cell {:colspan 9} memo]]
])])
output-stream)
(.toByteArray output-stream)))
(defn print-pnl [args data]
(let [uuid (str (UUID/randomUUID))
pdf-data (make-pnl args data)]
(s3/put-object :bucket-name (:data-bucket env)
:key (str "reports/pnl/" uuid ".pdf")
:input-stream (io/make-input-stream pdf-data {})
:metadata {:content-length (count pdf-data)
:content-type "application/pdf"})
(str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/reports/pnl/" uuid ".pdf")))