diff --git a/src/clj/auto_ap/pdf/ledger.clj b/src/clj/auto_ap/pdf/ledger.clj index db3abbe8..389bacf4 100644 --- a/src/clj/auto_ap/pdf/ledger.clj +++ b/src/clj/auto_ap/pdf/ledger.clj @@ -4,16 +4,15 @@ [auto-ap.datomic :refer [conn]] [auto-ap.graphql.utils :refer [<-graphql]] [auto-ap.time :as atime] - [auto-ap.utils :refer [dollars-0?]] + [auto-ap.utils :refer [by 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] - [clojure.tools.logging :as log]) + [datomic.api :as d]) (:import (java.io ByteArrayOutputStream) + (java.text DecimalFormat) (java.util UUID))) (defn distribute [nums] @@ -99,28 +98,6 @@ "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 aggregate-accounts [pnl-data] (reduce (fnil + 0.0) 0.0 (map :amount (:data pnl-data)))) @@ -172,323 +149,261 @@ (set) (sort-by :numeric-code))) -(defn subtotal-row [pnl-data sales-pnl-data title] - (let [raw (map - (fn [p] - (aggregate-accounts (filter-period pnl-data p))) - (-> pnl-data :args :periods)) - sales (map - (fn [p] - (aggregate-accounts (filter-period sales-pnl-data p))) - (-> 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 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 location-summary-table [pnl-data] - - (let [sales-data (filter-categories pnl-data [:sales])] - [(subtotal-row (filter-categories pnl-data [:sales]) - sales-data - "Sales") - (subtotal-row (filter-categories pnl-data [:cogs ]) - sales-data - "Cogs") +(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 + :value (if (dollars-0? s) + 0.0 + (/ (:value v) s))}) + values sales)))))) - (subtotal-row (filter-categories pnl-data [:payroll ]) - sales-data - "Payroll") +(defn calc-deltas [table] + (->> table + (map (fn [[_ & values]] + (->> values + (partition 2 1) + (map (fn [[a b]] + {:format :dollar + :value (- (:value b) + (:value a))}))))))) - (subtotal-row (-> pnl-data - (filter-categories [:sales :payroll :cogs]) - (negate #{:payroll :cogs})) - sales-data - "Gross Profits") +(defn combine-tables [pnl-data table percent-of-sales deltas] + (map (fn [[title & row] percent-of-sales deltas ] + (let [deltas (cons nil deltas)] + (into [title] + (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]) - sales-data - "Overhead") +(defn headers [pnl-data header-title] + (let [deltas (cons nil (repeat "Change")) + 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 - (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) - (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) - sales-data - "Net Income") - ])) +(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-sub-rows [pnl-data sales-data grouping] - (for [[grouping-name from to] grouping - :let [pnl-data (filter-numeric-code pnl-data from to) - account-codes (used-accounts pnl-data)] - :when (seq account-codes)] - (-> - [[(str "---" grouping-name "---")]] - (into (for [{:keys [numeric-code name]} account-codes] - (let [raw (map - (fn [p] - (-> pnl-data - (filter-numeric-code numeric-code numeric-code) - (filter-period p) - (aggregate-accounts))) +(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 "---") + :bold true}]] + (into (for [{:keys [numeric-code name]} account-codes] + (into [{:value name + :bold true}] + (map + (fn [p] + {:format :dollar + :value (-> pnl-data + (filter-numeric-code numeric-code numeric-code) + (filter-period p) + (aggregate-accounts))}) - (-> pnl-data :args :periods)) - sales (map - (fn [p] - (-> sales-data - (filter-period p) - (aggregate-accounts))) + (-> pnl-data :args :periods))))))] + row)] + (-> [[{:value title + :bold true}]] + (into individual-accounts) + (conj (subtotal-row pnl-data title))))) - (-> pnl-data :args :periods)) - 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))))))))) - -(defn detail-rows [pnl-data grouping sales-data title] - (let [pnl-data (filter-categories pnl-data [grouping])] - (-> [[title]] - (into (detail-sub-rows pnl-data - sales-data - (grouping groupings))) - (conj (subtotal-row pnl-data sales-data title))))) - -(defn location-detail-table [pnl-data] - (let [sales-data (filter-categories pnl-data [:sales])] - (-> [] - (into (detail-rows pnl-data - :sales - sales-data - (str (:prefix pnl-data) " Sales"))) - (into (detail-rows pnl-data - :cogs - sales-data - (str (:prefix pnl-data) " COGS"))) - (into (detail-rows - pnl-data - :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 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 [: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] - (try - {:summaries (for [[client-id location] (locations (:data pnl-data))] - (location-summary-table (-> pnl-data - (filter-client client-id) - (filter-location location)))) - :details (for [[client-id location] (locations (:data 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))))} - (catch Throwable e - - (println e)))) + (assoc :prefix location)) + (str (-> pnl-data :clients-by-id (get client-id) :client/name) " (" location ") Detail")))}) -(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] (let [data (<-graphql data) args (<-graphql args) + clients (d/pull-many (d/db conn) '[:client/name :db/id] (:client-ids args)) data (->> data - :periods - (mapcat (fn [p1 p2] - (map - (fn [a] - (assoc a :period p1) - ) - (:accounts p2)) - ) - (:periods args))) - report (PNLData. (assoc args :deltas true) data) - _ (clojure.pprint/pprint (summarize-pnl report)) - output-stream (ByteArrayOutputStream.) - _ (println (:client_ids args)) - clients (d/pull-many (d/db conn) '[:client/name] (:client-ids args))] + :periods + (mapcat (fn [p1 p2] + (map + (fn [a] + (assoc a :period p1) + ) + (:accounts p2)) + ) + (:periods args))) + report (summarize-pnl (PNLData. (assoc args :deltas true) data (by :db/id clients))) + output-stream (ByteArrayOutputStream.)] (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]] - ])]) + [{:left-margin 15 :right-margin 15 :top-margin 15 :bottom-margin 15 :size :letter + :font {:size 8}} + [:heading (str "Profit and Loss - " (str/join ", " (map :client/name clients)))] + (into [:paragraph] + (map table->pdf (:summaries report))) + (into [:paragraph] + (map table->pdf (:details report)))] output-stream) (.toByteArray output-stream))) diff --git a/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs b/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs index 6fbc8fb0..4de9f5d0 100644 --- a/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs +++ b/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs @@ -898,7 +898,9 @@ :type "checkbox"}]]]] [:div.level-right [: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"]] ]]