From 3aea1d5e4598bff4f040c71f2bf6971d8306aab8 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Tue, 29 Mar 2022 12:53:16 -0700 Subject: [PATCH] trimming down profit and loss considerably. --- src/clj/auto_ap/ledger.clj | 4 +- src/cljc/auto_ap/ledger/reports.cljc | 32 +- .../views/pages/ledger/profit_and_loss.cljs | 668 +++++------------- 3 files changed, 207 insertions(+), 497 deletions(-) diff --git a/src/clj/auto_ap/ledger.clj b/src/clj/auto_ap/ledger.clj index bb4f6fd4..c4479775 100644 --- a/src/clj/auto_ap/ledger.clj +++ b/src/clj/auto_ap/ledger.clj @@ -382,7 +382,7 @@ (let [mismatched-ts (mismatched-transactions)] (if (seq mismatched-ts) (do - (log/warn (count mismatched-ts) " transactions exist but don't match ledger ") + (log/warn (count mismatched-ts) " transactions exist but don't match ledger " (pr-str (take 10 mismatched-ts) )) (doseq [[m] mismatched-ts] (touch-transaction m)) (statsd/gauge "data.mismatched_transactions" (count (mismatched-transactions)))) @@ -391,7 +391,7 @@ (let [unbalanced-ts (unbalanced-transactions)] (if (seq unbalanced-ts) (do - (log/warn (count unbalanced-ts) " transactions exist but don't have matching debits/credits (" (pr-str (take 3 unbalanced-ts) ")")) + (log/warn (count unbalanced-ts) " transactions exist but don't have matching debits/credits (" (pr-str (take 10 unbalanced-ts) ) ")") (doseq [m unbalanced-ts] (touch-transaction m)) (statsd/gauge "data.unbalanced_transactions" (count (unbalanced-transactions)))) diff --git a/src/cljc/auto_ap/ledger/reports.cljc b/src/cljc/auto_ap/ledger/reports.cljc index a128ec6a..6c09629c 100644 --- a/src/cljc/auto_ap/ledger/reports.cljc +++ b/src/cljc/auto_ap/ledger/reports.cljc @@ -100,14 +100,14 @@ first)) (defn filter-client [pnl-data client] - (-> pnl-data + (-> 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 + (-> pnl-data (update :data (fn [data] ((group-by :location data) location))) (update :filters (fn [f] @@ -115,8 +115,8 @@ (defn filter-categories [pnl-data categories] (update pnl-data :data (fn [data] - (mapcat identity - ((apply juxt categories) + (mapcat identity + ((apply juxt categories) (group-by best-category data)))))) (defn filter-period [pnl-data period] @@ -157,14 +157,16 @@ (defn subtotal-row [pnl-data title & [cell-args]] (into [{:value title :bold true - :filters (:filters pnl-data)}] + :filters (when (:from-numeric-code (:filters pnl-data)) ;; don't allow filtering when you don't at least filter numeric codes + (:filters pnl-data))}] (map (fn [p] (let [data (filter-period pnl-data p)] (merge {:format :dollar :value (aggregate-accounts data) - :filters (:filters data)} + :filters (when (:from-numeric-code (:filters pnl-data)) ;; don't allow filtering when you don't at least filter numeric codes + (:filters pnl-data))} cell-args))) (-> pnl-data :args :periods)))) @@ -278,11 +280,13 @@ (into [{:value name}] (map (fn [p] - {:format :dollar - :value (-> pnl-data - (filter-numeric-code numeric-code numeric-code) - (filter-period p) - (aggregate-accounts))}) + (let [pnl-data (-> pnl-data + (filter-numeric-code numeric-code numeric-code) + (filter-period p) + )] + {:format :dollar + :filters (:filters pnl-data) + :value (aggregate-accounts pnl-data)})) (-> pnl-data :args :periods))))) (conj (subtotal-row pnl-data "" {:border [:top]})))] @@ -339,7 +343,7 @@ (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"))) + (str (-> pnl-data :clients-by-id (get client-id)) " (" location ") Summary"))) :details (for [[client-id location] (locations (:data pnl-data))] (location-detail-table (-> pnl-data (filter-client client-id) @@ -347,9 +351,9 @@ (assoc :prefix location)) (-> pnl-data (filter-client client-id)) - (str (-> pnl-data :clients-by-id (get client-id) :client/name) " (" location ") Detail")))}) + (str (-> pnl-data :clients-by-id (get client-id)) " (" location ") Detail")))}) -(defrecord PNLData [args data clients-by-id]) +(defrecord PNLData [args data client-names]) 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 ed14523b..7a2134a9 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 @@ -1,27 +1,40 @@ (ns auto-ap.views.pages.ledger.profit-and-loss - (:require [auto-ap.subs :as subs] - [auto-ap.views.components.layouts :refer [side-bar-layout appearing-side-bar]] - [auto-ap.views.pages.ledger.table :as ledger-table ] - [vimsical.re-frame.cofx.inject :as inject] - [goog.string :as gstring] - [auto-ap.utils :refer [dollars-0? by ]] - [auto-ap.forms :as forms] - [auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]] - [auto-ap.views.components.typeahead :refer [typeahead-v3]] - [auto-ap.views.components.buttons :as buttons] - [auto-ap.views.components.switch-field :refer [switch-field]] - [auto-ap.views.components.modal :as modal] - [auto-ap.views.utils :refer [date->str date-picker date-picker-friendly bind-field standard pretty dispatch-event local-today ->% ->$ str->date with-user dispatch-value-change query-params multi-field]] - [cljs-time.core :as t] - [re-frame.core :as re-frame] - [react-dom :as react-dom] - [auto-ap.status :as status] - [auto-ap.views.pages.data-page :as data-page] - [vimsical.re-frame.fx.track :as track] - [reagent.core :as reagent] - [clojure.set :as set] - [clojure.string :as str] - [cljs-time.core :as time])) + (:require + [auto-ap.forms :as forms] + [auto-ap.ledger.reports :as l-reports] + [auto-ap.status :as status] + [auto-ap.subs :as subs] + [auto-ap.utils :refer [dollars-0?]] + [auto-ap.views.components.buttons :as buttons] + [auto-ap.views.components.layouts + :refer [appearing-side-bar side-bar-layout]] + [auto-ap.views.components.modal :as modal] + [auto-ap.views.components.switch-field :refer [switch-field]] + [auto-ap.views.components.typeahead :refer [typeahead-v3]] + [auto-ap.views.pages.data-page :as data-page] + [auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]] + [auto-ap.views.pages.ledger.table :as ledger-table] + [auto-ap.views.utils + :refer [date->str + date-picker-friendly + ->$ + ->% + dispatch-event + local-today + multi-field + query-params + standard + str->date + with-user]] + [cljs-time.core :as t] + [clojure.set :as set] + [clojure.string :as str] + [re-frame.core :as re-frame] + [react-dom :as react-dom] + [reagent.core :as reagent] + [vimsical.re-frame.cofx.inject :as inject] + [vimsical.re-frame.fx.track :as track])) + (def ranges {:sales [40000 49999] :cogs [50000 59999] @@ -46,44 +59,6 @@ ;; SUBS -(re-frame/reg-sub - ::locations - - :<- [::forms/form ::form] - - (fn [db] - (->> db - :report - :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))]))))) - -(re-frame/reg-sub - ::multi-client? - :<- [::forms/form ::form] - (fn [db] - (> (->> db - :data - :clients - count) 1 ))) (re-frame/reg-sub ::error @@ -124,20 +99,6 @@ (map #(update % :amount js/parseFloat) a)))) -(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))) (re-frame/reg-sub ::all-accounts @@ -266,12 +227,7 @@ Please download it by clicking this link: " report-url))) (fn [data field value] (cond (= [:periods] field) - [field (mapv (fn [period] - (mapv - (fn [dt] - (str->date dt standard)) - period)) - value)] + [field value] (and (= :periods (first field)) (= 2 (count field))) @@ -290,13 +246,13 @@ Please download it by clicking this link: " report-url))) :dispatch (into [::change-internal] event)})) -(defn data-params->query-params [params client-id] +(defn data-params->query-params [params] (when params {:start (:start params 0) :sort (:sort params) :per-page (:per-page params) :vendor-id (:id (:vendor params)) - :client-id client-id + :client-id (:client-id params) :from-numeric-code (:from-numeric-code params) :to-numeric-code (:to-numeric-code params) :location (:location params) @@ -306,16 +262,11 @@ Please download it by clicking this link: " report-url))) ::ledger-params-change [with-user] (fn [{:keys [user db]} [_ ledger-params]] - (if (seq ledger-params) {:graphql {:token user :owns-state {:single [::data-page/page ::ledger]} :query-obj {:venia/queries [[:ledger-page - {:filters (data-params->query-params ledger-params - (->> (get-in db [::forms/forms ::form :data]) - :clients - first - :id ))} + {:filters (data-params->query-params ledger-params)} [[:journal-entries [:id :source :original-entity @@ -334,362 +285,101 @@ Please download it by clicking this link: " report-url))) :start :end]]]} :on-success (fn [result] - [::data-page/received ::ledger (set/rename-keys (:ledger-page result) {:journal-entries :data})])}}))) (re-frame/reg-event-fx ::investigate-clicked - (fn [{:keys [db]} [_ location from-numeric-code to-numeric-code which]] - (let [[from to] (get @(re-frame/subscribe [::periods]) which)] - {:db (-> db (assoc ::ledger-list-active? true)) - :dispatch [::data-page/additional-params-changed ::ledger {:client-id (:id @(re-frame/subscribe [::subs/client])) - :from-numeric-code from-numeric-code - :to-numeric-code to-numeric-code - :location location - :date-range {:start (date->str from standard) - :end (date->str to standard)}}]}))) - -(re-frame/reg-sub - ::can-submit-period-form - (fn [] - true)) - -(def change-period-form (forms/vertical-form {:submit-event [::change-period] - :change-event [::forms/change ::period-form] - :can-submit [::can-submit-period-form] - :id ::period-form})) - -(defn change-period-body [idx which] - (let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::period-form]) - {:keys [form-inline horizontal-field field raw-field error-notification submit-button]} change-period-form] - (form-inline {} - [:<> - (field "From" - [date-picker {:class-name "input" - :class "input" - :format-week-number (fn [] "") - :previous-month-button-label "" - :placeholder "mm/dd/yyyy" - :next-month-button-label "" - :next-month-label "" - :type "date" - :field [:period 0] - :popper-props (clj->js {:placement "right"}) - }] ) - (field "To" - [date-picker {:class-name "input" - :class "input" - :format-week-number (fn [] "") - :previous-month-button-label "" - :placeholder "mm/dd/yyyy" - :next-month-button-label "" - :next-month-label "" - :type "date" - :field [:period 1] - :popper-props (clj->js {:placement "right"}) - }] )]))) -(re-frame/reg-event-fx - ::period-change-submitted - (fn [{:keys [db]} [_ idx [start end] :as z]] - { :db (-> db (forms/stop-form ::period-form)) - :dispatch-n [[::modal/modal-closed ] - [::change [:periods idx ] [start end]]]})) - -(re-frame/reg-event-fx - ::period-removed - (fn [{:keys [db]} [_ idx which]] - { :db (-> db (forms/stop-form ::period-form) - (update-in [::forms/forms ::form :data :periods] - (fn [ps] - (->> ps - (map vector (range) ) - (filter (fn [[i _]] - (not= i idx))) - (map second) - (into []))))) - :dispatch [::modal/modal-closed ]})) - -(defn change-period-foot [idx which] - [:div.buttons - [:button.button {:class "is-primary" - :on-click (dispatch-event [::period-change-submitted idx - (-> @(re-frame/subscribe [::forms/form ::period-form]) - :data - :period - (update 0 #(if (instance? goog.date.Date %) - % - (str->date % standard))) - (update 1 #(if (instance? goog.date.Date %) - % - (str->date % standard))) - )])} - "Change period"] - [:button.button {:class "is-warning" - :on-click (dispatch-event [::period-removed idx ])} - "Delete period"]] - - ) -(re-frame/reg-event-fx - ::period-change-requested - (fn [{:keys [db]} [_ idx which]] - { :db (-> db - (forms/start-form ::period-form - {:idx idx - :period which})) - :dispatch [::modal/modal-requested {:title (str "Change period " (date->str (nth which 0)) " - " (date->str (nth which 1))) - :class "" - :body [change-period-body idx which] - :foot [change-period-foot idx which]}]})) - -(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 percent-of-sales [amount accounts which client-id location] - (let [sales (aggregate-accounts (filter-accounts accounts which (get ranges :sales) client-id location))] - (if (not (dollars-0? sales)) - (/ amount - sales) - 0.0))) - -(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 map-periods [for-every between periods include-deltas] - (for [[_ i] (map vector periods (range))] - ^{:key (str "period-" i)} - [:<> - (with-meta - (for-every i) - {:key i}) - (if (and include-deltas (not= 0 i)) - (with-meta (between i) - {:key (str "between-" i)}))])) + (fn [{:keys [db]} [_ {:keys [location from-numeric-code to-numeric-code client-id] + {:keys [start end]} :date-range + :as filters}]] + {:db (-> db (assoc ::ledger-list-active? true)) + :dispatch [::data-page/additional-params-changed ::ledger {:client-id client-id + :from-numeric-code from-numeric-code + :to-numeric-code to-numeric-code + :location location + :date-range {:start (date->str start standard) + :end (date->str end standard)}}]})) -;; TODO allow "sandwhiching" clients data -(defn grouping [{:keys [header type groupings client-id location periods all-accounts]}] - (let [include-deltas @(re-frame/subscribe [::include-deltas]) - multi-client? @(re-frame/subscribe [::multi-client?])] - [:<> - (doall - (for [[grouping-name from to] groupings - :let [account-codes (used-accounts all-accounts [from to] client-id location)] - :when (seq account-codes)] - ^{:key grouping-name} - [:<> - [:tr [:td "---" grouping-name "---"] - (map-periods - (fn [i] - [:<> - [:td] - [:td]]) - (fn [i] - [:td]) - periods - include-deltas)] - [:<> - (for [{:keys [numeric-code name]} account-codes] - ^{:key numeric-code} +(defn cell [{:keys [width]} c] + (let [cell-contents (cond + (and (= :dollar (:format c)) + (dollars-0? (:value c))) + "-" + + (= :dollar (:format c)) + (->$ (:value c)) + #_(.format (DecimalFormat. "$###,##0.00") (:value cell)) + + (= :percent (:format c)) + (->% (:value c)) + #_(.format (DecimalFormat. "0%") (:value cell)) + + :else + (str (:value c))) + cell-contents (if (:filters c) + [:a {:on-click (dispatch-event [::investigate-clicked (:filters c)])} + cell-contents] + cell-contents)] + [:td + (cond-> {:style {:width (str width "em")}} + + (:border c) (update :style + (fn [s] + (->> (:border c) + (map + (fn [b] + [(keyword (str "border-" (name b))) "1px solid black"]) + ) + (into s)))) + (:colspan c) (assoc :colspan (:colspan c)) + (:align c) (assoc :align (:align c)) + (= :dollar (:format c)) (assoc :align :right) + (= :percent (:format c)) (assoc :align :right) + (:bold c) (assoc-in [:style :font-weight] "bold") + (:color c) (assoc-in [:style :color] (str "rgb(" + (str/join "," + (:color c)) + ")"))) + + cell-contents + ])) + +(defn cell-count [table] + (let [counts (map count (:rows table))] + (if (seq counts) + (apply max counts) + 0))) + +(defn table->pdf [{:keys [table widths]}] + (let [cell-count (cell-count table)] + (-> [:table.table.compact.balance-sheet {:style nil} + (map + (fn [i header] + (into ^{:key i} + [:tr] + (map + (fn [w header i] + ^{:key i} [cell {:width w} header]) + widths + header + (range)))) + (range) + (:header table))] + + (into + (for [[i row] (map vector (range) (:rows table))] + ^{:key i} [: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)])] - - [:tr - [:th] - (map-periods - (fn [i] - (let [amount (aggregate-accounts (filter-accounts all-accounts i [from to] client-id location))] - [:<> - [:th.has-text-right.total (if multi-client? - [:span (->$ amount)] - [:a {:on-click (dispatch-event [::investigate-clicked location from to i])} - (->$ amount)])] - [:th.has-text-right.total (->% (percent-of-sales amount all-accounts i client-id location))]])) - (fn [i] - [:th.has-text-right.total (->$ (- (aggregate-accounts (filter-accounts all-accounts i [from to] client-id location)) - (aggregate-accounts (filter-accounts all-accounts (dec i) [from to] client-id location))))]) - periods - include-deltas)]]))])) - - - -(defn overall-grouping [type title client-id location] - (let [all-accounts @(re-frame/subscribe [::all-accounts]) - periods @(re-frame/subscribe [::periods]) - [min-numeric-code max-numeric-code] (ranges type) - include-deltas @(re-frame/subscribe [::include-deltas]) - multi-client? @(re-frame/subscribe [::multi-client?])] - [:<> - [:tr [:th.is-size-5 title]] - - [grouping {:location location - :client-id client-id - :groupings (type groupings) - :periods periods - :all-accounts all-accounts}] - - [:tr [:th.is-size-5 title] - (map-periods - (fn [i] - (let [amount (aggregate-accounts (filter-accounts all-accounts i [min-numeric-code max-numeric-code] client-id location))] - [:<> - [:th.has-text-right (if multi-client? - [:span (->$ amount)] - [:a - {:on-click (dispatch-event [::investigate-clicked location min-numeric-code max-numeric-code i])} - (->$ amount)])] - [:th.has-text-right (->% (percent-of-sales amount all-accounts i client-id location))] - ])) - (fn [i] - [:th.has-text-right (->$ (- (aggregate-accounts (filter-accounts all-accounts i [min-numeric-code max-numeric-code] client-id location)) - (aggregate-accounts (filter-accounts all-accounts (dec i) [min-numeric-code max-numeric-code] client-id location))))]) - periods - include-deltas)]])) - -(defn subtotal [types negs title client-id location] - (let [all-accounts @(re-frame/subscribe [::all-accounts]) - periods @(re-frame/subscribe [::periods]) - include-deltas @(re-frame/subscribe [::include-deltas])] - [:tr [:th.is-size-5 title] - - (map-periods - (fn [i] - (let [amount (aggregate-accounts (mapcat (fn [t] - (cond->> (filter-accounts all-accounts i (ranges t) client-id location) - (negs t) (map #(update % :amount -)))) - types))] - [:<> - [:td.has-text-right [:span (->$ amount)]] - [:td.has-text-right (->% (percent-of-sales amount all-accounts i client-id location))]])) - (fn [i] - [:td.has-text-right (->$ (- (aggregate-accounts (mapcat (fn [t] - (cond->> (filter-accounts all-accounts i (ranges t) client-id location) - (negs t) (map #(update % :amount -)))) - types)) - (aggregate-accounts (mapcat (fn [t] - (cond->> (filter-accounts all-accounts (dec i) (ranges t) client-id location) - (negs t) (map #(update % :amount -)))) - types))))]) - periods - include-deltas)])) - -(defn period-header [{:keys [include-deltas periods]}] - [:<> - [:tr - [:td.has-text-right "Period"] - (map-periods - (fn [i] - [:<> - [:td.has-text-centered {:colspan 2} - [:a {:on-click (dispatch-event [::period-change-requested i (get periods i)])} - (or (get-in periods [i 2]) - (str (date->str (get-in periods [i 0])) " - " (date->str (get-in periods [i 1]))))]]]) - (fn [i] - [:td.has-text-right ""]) - periods - include-deltas)] - [:tr - [:td.has-text-right ""] - (map-periods - (fn [i] - [:<> - [:td.has-text-right - "Amount"] - [:td.has-text-right - "% Sales"]]) - (fn [i] - [:td.has-text-right "𝝙"]) - periods - include-deltas)]]) - -(defn location-rows [client-id location] - - [:<> - [overall-grouping :sales (str location " Sales") client-id location] - [overall-grouping :cogs (str location " COGS") client-id location] - [overall-grouping :payroll (str location " Payroll") client-id location] - [subtotal [:payroll :cogs] #{} (str location " Prime Costs") client-id location] - [subtotal [:sales :payroll :cogs] #{:payroll :cogs} (str location " Gross Profits") client-id location] - [overall-grouping :controllable (str location " Controllable Expenses") client-id location] - [overall-grouping :fixed-overhead (str location " Fixed Overhead") client-id location] - [overall-grouping :ownership-controllable (str location " Ownership Controllable") client-id location] - [subtotal [:controllable :fixed-overhead :ownership-controllable] #{} (str location " Overhead") client-id location] - [subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} (str location " Net Income") client-id location] - [subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" client-id nil]]) - -(defn location-summary [client-id location include-deltas] - (let [periods @(re-frame/subscribe [::periods])] - [:div - [:h2.title.is-4.mb-4 location " Summary"] - [:table.table.compact.balance-sheet.mb-6 - [:tbody - [period-header {:include-deltas include-deltas - :periods periods}] - - - [subtotal [:sales ] #{} "Sales" client-id location] - [subtotal [:cogs ] #{} "Cogs" client-id location] - [subtotal [:payroll ]#{} "Payroll" client-id location] - [subtotal [:sales :payroll :cogs] #{:payroll :cogs} "Gross Profits" client-id location] - [subtotal [:controllable :fixed-overhead :ownership-controllable] #{} "Overhead" client-id location] - [subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" client-id location]]]])) + (for [[i c] (map vector (range) (take cell-count (concat row (repeat nil))))] + ^{:key i} + [cell {} c])])) + (conj ^{:key "last"} + [:tr (for [i (range cell-count)] + ^{:key i} + [cell {} {:value " "}])])))) (re-frame/reg-sub ::can-submit @@ -756,7 +446,7 @@ Please download it by clicking this link: " report-url))) [:a.button {:class (when (= selected-period "13 periods") "is-active") :on-click (dispatch-event - [::forms/change ::form + [::change [:periods] (let [today (or (some-> (:thirteen-periods-end data) (str->date standard)) (local-today))] @@ -784,7 +474,7 @@ Please download it by clicking this link: " report-url))) [:a.button {:class (when (= selected-period "12 months") "is-active") :on-click (dispatch-event - [::forms/change ::form + [::change [:periods] (let [end-date (or (some-> (:twelve-periods-end data) (str->date standard)) (local-today)) @@ -808,7 +498,7 @@ Please download it by clicking this link: " report-url))) [:a.button {:class (when (= selected-period "Last week") "is-active") :on-click (dispatch-event - [::forms/change ::form + [::change [:periods] (let [last-sunday (loop [current (local-today)] (if (= 7 (t/day-of-week current)) @@ -822,7 +512,7 @@ Please download it by clicking this link: " report-url))) [:a.button {:class (when (= selected-period "Week to date") "is-active") :on-click (dispatch-event - [::forms/change ::form + [::change [:periods] (and-last-year [(loop [current (local-today)] (if (= 1 (t/day-of-week current)) @@ -836,7 +526,7 @@ Please download it by clicking this link: " report-url))) {:class (when (= selected-period "Last Month") "is-active") :on-click (dispatch-event - [::forms/change ::form + [::change [:periods] (and-last-year [(t/minus (t/local-date (t/year (local-today)) (t/month (local-today)) @@ -852,7 +542,7 @@ Please download it by clicking this link: " report-url))) [:a.button {:class (when (= selected-period "Month to date") "is-active") :on-click (dispatch-event - [::forms/change ::form + [::change [:periods] (and-last-year [(t/local-date (t/year (local-today)) (t/month (local-today)) @@ -865,7 +555,7 @@ Please download it by clicking this link: " report-url))) [:a.button {:class (when (= selected-period "Year to date") "is-active") :on-click (dispatch-event - [::forms/change ::form + [::change [:periods] (and-last-year [(t/local-date (t/year (local-today)) 1 1) (local-today)]) @@ -875,7 +565,7 @@ Please download it by clicking this link: " report-url))) [:a.button {:class (when (= selected-period "Full year") "is-active") :on-click (dispatch-event - [::forms/change ::form + [::change [:periods] [[(t/local-date (dec (t/year (local-today))) 1 1) (t/local-date (dec (t/year (local-today))) 12 31)]] @@ -885,7 +575,7 @@ Please download it by clicking this link: " report-url))) [:a.button {:class (when (= selected-period "Full year") "is-active") :on-click (dispatch-event - [::forms/change ::form + [::change [:periods] (and-last-year [(t/plus (t/minus (local-today) (t/period :years 1)) (t/period :days 1)) @@ -921,7 +611,7 @@ Please download it by clicking this link: " report-url))) [switch-field {:id "include-deltas" :checked (boolean include-deltas) :on-change (fn [e] - (re-frame/dispatch [::forms/change ::form + (re-frame/dispatch [::change [:include-deltas] (.-checked (.-target e))])) :label "Include deltas" :type "checkbox"}]]]] @@ -937,50 +627,66 @@ Please download it by clicking this link: " report-url))) (when-not @!box (reset! !box el)))}]])))) +(defn pnl-report [{:keys [args report-data]}] + (let [args (update args :periods + (fn [p] + (mapv (fn [[start end] ] + {:start start + :end end} ) + p))) + + pnl-data (->> report-data + :periods + (mapcat (fn [p1 p2] + (map + (fn [a] + (assoc a :period p1 + :amount (js/parseFloat (:amount a))) + ) + (:accounts p2))) + (:periods args))) + unresolved-accounts @(re-frame/subscribe [::uncategorized-accounts]) + client-names (->> @(re-frame/subscribe [::subs/clients-by-id]) + (map (fn [[k v]] + [k (:name v)])) + (into {})) + pnl-data (l-reports/->PNLData args pnl-data client-names) + report (l-reports/summarize-pnl pnl-data)] + [:div + [:h1.title "Profit and Loss - " (str/join ", " (map :name (:clients args)))] + (when (seq unresolved-accounts) + [:div.notification.is-warning.is-light + "This report does not include " (str/join ", " + (map #(str (:count %) " unresolved ledger entries for " (if (str/blank? (:location %)) + " all locations" + (:location %))) + unresolved-accounts))]) + (for [[index table] (map vector (range) (concat (:summaries report) + (:details report)))] + ^{:key index} + [table->pdf {:widths (into [20] (take (dec (cell-count table)) + (mapcat identity + (repeat + (if (-> pnl-data :args :include-deltas) + [13 6 13] + [13 6]))))) + :table table}])])) + (defn profit-and-loss-content [] - (let [status @(re-frame/subscribe [::status/single ::page]) - unresolved-accounts @(re-frame/subscribe [::uncategorized-accounts]) - clients-by-id @(re-frame/subscribe [::subs/clients-by-id]) - {:keys [data report active? error id]} @(re-frame/subscribe [::forms/form ::form]) + (let [status @(re-frame/subscribe [::status/single ::page]) + {:keys [data report active? error id]} @(re-frame/subscribe [::forms/form ::form]) {:keys [form-inline field raw-field error-notification submit-button ]} pnl-form - periods (:periods data)] + ] (form-inline {} - [:div + [:div [status/status-notification {:statuses [[::status/single ::page]]}] [report-controls pnl-form] - [status/big-loader status] (when (and (not= :loading (:state status)) report) - - [:div - - [:h1.title "Profit and Loss - " (str/join ", " (map :name (:clients data)))] - (when (seq unresolved-accounts) - [:div.notification.is-warning.is-light - "This report does not include " (str/join ", " - (map #(str (:count %) " unresolved ledger entries for " (if (str/blank? (:location %)) - " all locations" - (:location %))) - unresolved-accounts))]) - [:<> - (for [[client-id location] @(re-frame/subscribe [::locations])] - ^{:key (str client-id "-" location "-summary")} - [location-summary client-id location (:include-deltas data)] - )] - [:h2.title.is-4 {:style {:margin-bottom "1rem"}} "Detail"] - [:table.table.compact.balance-sheet - [:tbody - [period-header {:include-deltas (:include-deltas data) - :periods periods}] - - [:<> - (for [[client-id location] @(re-frame/subscribe [::locations])] - ^{:key (str client-id "-" location)} - [:<> - [:tr [:th.is-size-3 (:name (clients-by-id client-id))]] - [location-rows client-id location]])]]]])]))) + [pnl-report {:report-data report + :args data}])]))) (re-frame/reg-event-fx ::unmounted-pnl