adds export button.

This commit is contained in:
2022-03-21 08:14:42 -07:00
parent dcc7b8f304
commit 666fd40bce
2 changed files with 236 additions and 319 deletions

View File

@@ -4,16 +4,15 @@
[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]
[auto-ap.utils :refer [dollars-0?]] [auto-ap.utils :refer [by dollars-0?]]
[clj-pdf.core :as pdf] [clj-pdf.core :as pdf]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.string :as str] [clojure.string :as str]
[clojure.walk :refer [postwalk]]
[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.util UUID))) (java.util UUID)))
(defn distribute [nums] (defn distribute [nums]
@@ -99,28 +98,6 @@
"ZZZZZZ" "ZZZZZZ"
(:location x))])))) (: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 aggregate-accounts [pnl-data] (defn aggregate-accounts [pnl-data]
(reduce (fnil + 0.0) 0.0 (map :amount (:data pnl-data)))) (reduce (fnil + 0.0) 0.0 (map :amount (:data pnl-data))))
@@ -172,323 +149,261 @@
(set) (set)
(sort-by :numeric-code))) (sort-by :numeric-code)))
(defn subtotal-row [pnl-data sales-pnl-data title] (defn subtotal-row [pnl-data title]
(let [raw (map (into [{:value title
(fn [p] :bold true}]
(aggregate-accounts (filter-period pnl-data p))) (map
(-> pnl-data :args :periods)) (fn [p]
sales (map {:format :dollar
(fn [p] :value (aggregate-accounts (filter-period pnl-data p))})
(aggregate-accounts (filter-period sales-pnl-data p))) (-> pnl-data :args :periods))))
(-> pnl-data :args :periods))
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 [pnl-data] (defn calc-percent-of-sales [table pnl-data]
(let [sales-pnl-data (filter-categories pnl-data [:sales])
(let [sales-data (filter-categories pnl-data [:sales])] sales (map
[(subtotal-row (filter-categories pnl-data [:sales]) (fn [p]
sales-data (aggregate-accounts (filter-period sales-pnl-data p)))
"Sales") (-> pnl-data :args :periods))]
(subtotal-row (filter-categories pnl-data [:cogs ]) (->> table
sales-data (map (fn [[_ & values]]
"Cogs") (map
(fn [v s]
{:format :percent
:value (if (dollars-0? s)
0.0
(/ (:value v) s))})
values sales))))))
(subtotal-row (filter-categories pnl-data [:payroll ]) (defn calc-deltas [table]
sales-data (->> table
"Payroll") (map (fn [[_ & values]]
(->> values
(partition 2 1)
(map (fn [[a b]]
{:format :dollar
:value (- (:value b)
(:value a))})))))))
(subtotal-row (-> pnl-data (defn combine-tables [pnl-data table percent-of-sales deltas]
(filter-categories [:sales :payroll :cogs]) (map (fn [[title & row] percent-of-sales deltas ]
(negate #{:payroll :cogs})) (let [deltas (cons nil deltas)]
sales-data (into [title]
"Gross Profits") (filter identity
(mapcat
(fn [v p d]
[v p d])
row
percent-of-sales
deltas)))
))
table
percent-of-sales
deltas))
(subtotal-row (filter-categories pnl-data [:controllable :fixed-overhead :ownership-controllable]) (defn headers [pnl-data header-title]
sales-data (let [deltas (cons nil (repeat "Change"))
"Overhead") big-header (into [{:value header-title
:bold true}]
(map-indexed (fn [i p]
{:value
(str (date->str (:start p))
" - "
(date->str (:end p)))
:colspan (if (= 0 i)
2
3)
:align :center
:bold true})
(:periods (:args pnl-data))))
sub-header (into [{:value ""}]
(filter identity
(mapcat
(fn [v p d]
[{:value "Amount"
:align :right
:bold true}
{:value "% Sales"
:align :right
:bold true}
(when d
{:value d
:align :right
:bold true})])
(:periods (:args pnl-data))
(:periods (:args pnl-data))
deltas)))]
[big-header
sub-header]))
(subtotal-row (-> pnl-data (defn location-summary-table [pnl-data title]
(filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) (let [table [(subtotal-row (filter-categories pnl-data [:sales]) "Sales")
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) (subtotal-row (filter-categories pnl-data [:cogs ]) "Cogs")
sales-data
"Net Income") (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-sub-rows [pnl-data sales-data grouping] (defn detail-rows [pnl-data grouping title]
(for [[grouping-name from to] grouping (let [pnl-data (filter-categories pnl-data [grouping])
:let [pnl-data (filter-numeric-code pnl-data from to) individual-accounts
account-codes (used-accounts pnl-data)] (for [[grouping-name from to] (groupings grouping)
:when (seq account-codes)] :let [pnl-data (filter-numeric-code pnl-data from to)
(-> account-codes (used-accounts pnl-data)]
[[(str "---" grouping-name "---")]] :when (seq account-codes)
(into (for [{:keys [numeric-code name]} account-codes] row (-> [[{:value (str "---" grouping-name "---")
(let [raw (map :bold true}]]
(fn [p] (into (for [{:keys [numeric-code name]} account-codes]
(-> pnl-data (into [{:value name
(filter-numeric-code numeric-code numeric-code) :bold true}]
(filter-period p) (map
(aggregate-accounts))) (fn [p]
{:format :dollar
:value (-> pnl-data
(filter-numeric-code numeric-code numeric-code)
(filter-period p)
(aggregate-accounts))})
(-> pnl-data :args :periods)) (-> pnl-data :args :periods))))))]
sales (map row)]
(fn [p] (-> [[{:value title
(-> sales-data :bold true}]]
(filter-period p) (into individual-accounts)
(aggregate-accounts))) (conj (subtotal-row pnl-data title)))))
(-> pnl-data :args :periods)) (defn location-detail-table [pnl-data title]
deltas (->> raw (let [table (-> []
(partition-all 2) (into (detail-rows pnl-data
(map (fn [[a b]] :sales
(- b (str (:prefix pnl-data) " Sales")))
a))))] (into (detail-rows pnl-data
(into [name] :cogs
(->> raw (str (:prefix pnl-data) " COGS")))
(map (fn [s r] (into (detail-rows
[r (if (dollars-0? s) pnl-data
0.0 :payroll
(/ r s))]) (str (:prefix pnl-data) " Payroll")))
sales) (conj (subtotal-row (filter-categories pnl-data [:payroll :cogs])
(partition-all 2) (str (:prefix pnl-data) " Prime Costs")))
(mapcat (fn [d [[a a-sales] [b b-sales]]] (conj (subtotal-row (-> pnl-data
[a a-sales b b-sales d] (filter-categories [:sales :payroll :cogs])
) (negate #{:payroll :cogs}))
deltas))))))))) (str (:prefix pnl-data) " Gross Profits")))
(into (detail-rows
(defn detail-rows [pnl-data grouping sales-data title] pnl-data
(let [pnl-data (filter-categories pnl-data [grouping])] :fixed-overhead
(-> [[title]] (str (:prefix pnl-data) " Fixed Overhead")))
(into (detail-sub-rows pnl-data (into (detail-rows
sales-data pnl-data
(grouping groupings))) :ownership-controllable
(conj (subtotal-row pnl-data sales-data title))))) (str (:prefix pnl-data) " Ownership Controllable")))
(conj (subtotal-row (-> pnl-data
(defn location-detail-table [pnl-data] (filter-categories [:controllable :fixed-overhead :ownership-controllable]))
(let [sales-data (filter-categories pnl-data [:sales])] (str (:prefix pnl-data) " Overhead")))
(-> [] (conj (subtotal-row (-> pnl-data
(into (detail-rows pnl-data (filter-categories [:controllable :fixed-overhead :ownership-controllable]))
:sales (str (:prefix pnl-data) " Overhead")))
sales-data (conj (subtotal-row (-> pnl-data
(str (:prefix pnl-data) " Sales"))) (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
(into (detail-rows pnl-data (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
:cogs (str (:prefix pnl-data) " Net Income"))))
sales-data percent-of-sales (calc-percent-of-sales table pnl-data)
(str (:prefix pnl-data) " COGS"))) deltas (into [] (calc-deltas table))]
(into (detail-rows {:header (headers pnl-data title)
pnl-data :rows (combine-tables pnl-data table percent-of-sales deltas)}))
:payroll
sales-data
(str (:prefix pnl-data) " Payroll")))
(conj (subtotal-row (filter-categories pnl-data [:payroll :cogs])
sales-data
(str (:prefix pnl-data) " Prime Costs")))
(conj (subtotal-row (-> pnl-data
(filter-categories [:payroll :cogs])
(negate #{:payroll :cogs}))
sales-data
(str (:prefix pnl-data) " Gross Profits")))
(into (detail-rows
pnl-data
:fixed-overhead
sales-data
(str (:prefix pnl-data) " Fixed Overhead")))
(into (detail-rows
pnl-data
:ownership-controllable
sales-data
(str (:prefix pnl-data) " Ownership Controllable")))
(conj (subtotal-row (-> pnl-data
(filter-categories [:controllable :fixed-overhead :ownership-controllable]))
sales-data
(str (:prefix pnl-data) " Overhead")))
(conj (subtotal-row (-> pnl-data
(filter-categories [:controllable :fixed-overhead :ownership-controllable]))
sales-data
(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}))
sales-data
(str (:prefix pnl-data) " Net Income"))))))
(defn summarize-pnl [pnl-data] (defn summarize-pnl [pnl-data]
(try {:summaries (for [[client-id location] (locations (:data pnl-data))]
{:summaries (for [[client-id location] (locations (:data pnl-data))] (location-summary-table (-> pnl-data
(location-summary-table (-> pnl-data (filter-client client-id)
(filter-client client-id) (filter-location location))
(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))] :details (for [[client-id location] (locations (:data pnl-data))]
(location-detail-table (-> pnl-data (location-detail-table (-> pnl-data
(filter-client client-id) (filter-client client-id)
(filter-location location) (filter-location location)
(assoc :prefix location))))} (assoc :prefix location))
(catch Throwable e (str (-> pnl-data :clients-by-id (get client-id) :client/name) " (" location ") Detail")))})
(println e))))
(defrecord PNLData [args data] (defrecord PNLData [args data clients-by-id])
)
(defn cell->pdf [cell]
[:pdf-cell
(cond-> {}
(= :dollar (:format cell)) (assoc :align :right)
(= :percent (:format cell)) (assoc :align :right)
(:bold cell) (assoc :style :bold)
(:align cell) (assoc :align (:align cell))
(:colspan cell) (assoc :colspan (:colspan cell)))
(cond (= :dollar (:format cell))
(.format (DecimalFormat. "$###,###.00") (:value cell))
(= :percent (:format cell))
(.format (DecimalFormat. "0%") (:value cell))
:else
(str (:value cell)))])
(defn table->pdf [table]
(let [cell-count (apply max (map count (:rows table)))]
(-> [:pdf-table {:header (mapv
(fn [header]
(map cell->pdf header))
(:header table))
:cell-border false}
(into [70] (take (dec cell-count) (repeat 20)))]
(into
(for [row (:rows table)]
(into []
(for [cell (take cell-count (concat row (repeat nil)))]
(cell->pdf cell)
))))
(conj (take cell-count (repeat (cell->pdf {:value " "})))))))
(defn make-pnl [args data] (defn make-pnl [args data]
(let [data (<-graphql data) (let [data (<-graphql data)
args (<-graphql args) args (<-graphql args)
clients (d/pull-many (d/db conn) '[:client/name :db/id] (:client-ids args))
data (->> data data (->> data
:periods :periods
(mapcat (fn [p1 p2] (mapcat (fn [p1 p2]
(map (map
(fn [a] (fn [a]
(assoc a :period p1) (assoc a :period p1)
) )
(:accounts p2)) (:accounts p2))
) )
(:periods args))) (:periods args)))
report (PNLData. (assoc args :deltas true) data) report (summarize-pnl (PNLData. (assoc args :deltas true) data (by :db/id clients)))
_ (clojure.pprint/pprint (summarize-pnl report)) output-stream (ByteArrayOutputStream.)]
output-stream (ByteArrayOutputStream.)
_ (println (:client_ids args))
clients (d/pull-many (d/db conn) '[:client/name] (:client-ids args))]
(pdf/pdf (pdf/pdf
(expand [{:left-margin 15 :right-margin 15 :top-margin 15 :bottom-margin 15 :size :letter
[{:left-margin 25 :right-margin 0 :top-margin 0 :bottom-margin 0 :size :letter} :font {:size 8}}
[:heading (str "Profit and Loss - " (str/join ", " (map :client/name clients)))] [:heading (str "Profit and Loss - " (str/join ", " (map :client/name clients)))]
#_(for [[client-id location] (locations data)] (into [:paragraph]
^{:key (str client-id "-" location "-summary")} (map table->pdf (:summaries report)))
(location-summary args data client-id location (:include-deltas data)) (into [:paragraph]
) (map table->pdf (:details report)))]
#_(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) output-stream)
(.toByteArray output-stream))) (.toByteArray output-stream)))

View File

@@ -898,7 +898,9 @@
:type "checkbox"}]]]] :type "checkbox"}]]]]
[:div.level-right [:div.level-right
[:div.buttons [:div.buttons
[:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"]
(when @(re-frame/subscribe [::subs/is-admin?])
[:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"])
[:button.button.is-primary "Run"]] [:button.button.is-primary "Run"]]
]] ]]