trimming down profit and loss considerably.

This commit is contained in:
2022-03-29 12:53:16 -07:00
parent ee6048329b
commit 3aea1d5e45
3 changed files with 207 additions and 497 deletions

View File

@@ -382,7 +382,7 @@
(let [mismatched-ts (mismatched-transactions)] (let [mismatched-ts (mismatched-transactions)]
(if (seq mismatched-ts) (if (seq mismatched-ts)
(do (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] (doseq [[m] mismatched-ts]
(touch-transaction m)) (touch-transaction m))
(statsd/gauge "data.mismatched_transactions" (count (mismatched-transactions)))) (statsd/gauge "data.mismatched_transactions" (count (mismatched-transactions))))
@@ -391,7 +391,7 @@
(let [unbalanced-ts (unbalanced-transactions)] (let [unbalanced-ts (unbalanced-transactions)]
(if (seq unbalanced-ts) (if (seq unbalanced-ts)
(do (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] (doseq [m unbalanced-ts]
(touch-transaction m)) (touch-transaction m))
(statsd/gauge "data.unbalanced_transactions" (count (unbalanced-transactions)))) (statsd/gauge "data.unbalanced_transactions" (count (unbalanced-transactions))))

View File

@@ -157,14 +157,16 @@
(defn subtotal-row [pnl-data title & [cell-args]] (defn subtotal-row [pnl-data title & [cell-args]]
(into [{:value title (into [{:value title
:bold true :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 (map
(fn [p] (fn [p]
(let [data (filter-period pnl-data p)] (let [data (filter-period pnl-data p)]
(merge (merge
{:format :dollar {:format :dollar
:value (aggregate-accounts data) :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))) cell-args)))
(-> pnl-data :args :periods)))) (-> pnl-data :args :periods))))
@@ -278,11 +280,13 @@
(into [{:value name}] (into [{:value name}]
(map (map
(fn [p] (fn [p]
{:format :dollar (let [pnl-data (-> pnl-data
:value (-> pnl-data
(filter-numeric-code numeric-code numeric-code) (filter-numeric-code numeric-code numeric-code)
(filter-period p) (filter-period p)
(aggregate-accounts))}) )]
{:format :dollar
:filters (:filters pnl-data)
:value (aggregate-accounts pnl-data)}))
(-> pnl-data :args :periods))))) (-> pnl-data :args :periods)))))
(conj (subtotal-row pnl-data "" {:border [:top]})))] (conj (subtotal-row pnl-data "" {:border [:top]})))]
@@ -339,7 +343,7 @@
(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"))) (str (-> pnl-data :clients-by-id (get client-id)) " (" 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)
@@ -347,9 +351,9 @@
(assoc :prefix location)) (assoc :prefix location))
(-> pnl-data (-> pnl-data
(filter-client client-id)) (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])

View File

@@ -1,27 +1,40 @@
(ns auto-ap.views.pages.ledger.profit-and-loss (ns auto-ap.views.pages.ledger.profit-and-loss
(:require [auto-ap.subs :as subs] (:require
[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.forms :as forms]
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]] [auto-ap.ledger.reports :as l-reports]
[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.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.data-page :as data-page]
[vimsical.re-frame.fx.track :as track] [auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
[reagent.core :as reagent] [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.set :as set]
[clojure.string :as str] [clojure.string :as str]
[cljs-time.core :as time])) [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 (def ranges
{:sales [40000 49999] {:sales [40000 49999]
:cogs [50000 59999] :cogs [50000 59999]
@@ -46,44 +59,6 @@
;; SUBS ;; 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 (re-frame/reg-sub
::error ::error
@@ -124,20 +99,6 @@
(map #(update % :amount js/parseFloat) a)))) (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 (re-frame/reg-sub
::all-accounts ::all-accounts
@@ -266,12 +227,7 @@ Please download it by clicking this link: " report-url)))
(fn [data field value] (fn [data field value]
(cond (cond
(= [:periods] field) (= [:periods] field)
[field (mapv (fn [period] [field value]
(mapv
(fn [dt]
(str->date dt standard))
period))
value)]
(and (= :periods (first field)) (and (= :periods (first field))
(= 2 (count field))) (= 2 (count field)))
@@ -290,13 +246,13 @@ Please download it by clicking this link: " report-url)))
:dispatch (into [::change-internal] event)})) :dispatch (into [::change-internal] event)}))
(defn data-params->query-params [params client-id] (defn data-params->query-params [params]
(when params (when params
{:start (:start params 0) {:start (:start params 0)
:sort (:sort params) :sort (:sort params)
:per-page (:per-page params) :per-page (:per-page params)
:vendor-id (:id (:vendor params)) :vendor-id (:id (:vendor params))
:client-id client-id :client-id (:client-id params)
:from-numeric-code (:from-numeric-code params) :from-numeric-code (:from-numeric-code params)
:to-numeric-code (:to-numeric-code params) :to-numeric-code (:to-numeric-code params)
:location (:location params) :location (:location params)
@@ -306,16 +262,11 @@ Please download it by clicking this link: " report-url)))
::ledger-params-change ::ledger-params-change
[with-user] [with-user]
(fn [{:keys [user db]} [_ ledger-params]] (fn [{:keys [user db]} [_ ledger-params]]
(if (seq ledger-params) (if (seq ledger-params)
{:graphql {:token user {:graphql {:token user
:owns-state {:single [::data-page/page ::ledger]} :owns-state {:single [::data-page/page ::ledger]}
:query-obj {:venia/queries [[:ledger-page :query-obj {:venia/queries [[:ledger-page
{:filters (data-params->query-params ledger-params {:filters (data-params->query-params ledger-params)}
(->> (get-in db [::forms/forms ::form :data])
:clients
first
:id ))}
[[:journal-entries [:id [[:journal-entries [:id
:source :source
:original-entity :original-entity
@@ -334,362 +285,101 @@ Please download it by clicking this link: " report-url)))
:start :start
:end]]]} :end]]]}
:on-success (fn [result] :on-success (fn [result]
[::data-page/received ::ledger (set/rename-keys (:ledger-page result) [::data-page/received ::ledger (set/rename-keys (:ledger-page result)
{:journal-entries :data})])}}))) {:journal-entries :data})])}})))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::investigate-clicked ::investigate-clicked
(fn [{:keys [db]} [_ location from-numeric-code to-numeric-code which]] (fn [{:keys [db]} [_ {:keys [location from-numeric-code to-numeric-code client-id]
(let [[from to] (get @(re-frame/subscribe [::periods]) which)] {:keys [start end]} :date-range
:as filters}]]
{:db (-> db (assoc ::ledger-list-active? true)) {:db (-> db (assoc ::ledger-list-active? true))
:dispatch [::data-page/additional-params-changed ::ledger {:client-id (:id @(re-frame/subscribe [::subs/client])) :dispatch [::data-page/additional-params-changed ::ledger {:client-id client-id
:from-numeric-code from-numeric-code :from-numeric-code from-numeric-code
:to-numeric-code to-numeric-code :to-numeric-code to-numeric-code
:location location :location location
:date-range {:start (date->str from standard) :date-range {:start (date->str start standard)
:end (date->str to standard)}}]}))) :end (date->str end 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] (defn cell [{:keys [width]} c]
(let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::period-form]) (let [cell-contents (cond
{:keys [form-inline horizontal-field field raw-field error-notification submit-button]} change-period-form] (and (= :dollar (:format c))
(form-inline {} (dollars-0? (:value c)))
[:<> "-"
(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 (= :dollar (:format c))
::period-removed (->$ (:value c))
(fn [{:keys [db]} [_ idx which]] #_(.format (DecimalFormat. "$###,##0.00") (:value cell))
{ :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] (= :percent (:format c))
[:div.buttons (->% (:value c))
[:button.button {:class "is-primary" #_(.format (DecimalFormat. "0%") (:value cell))
: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"]]
: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"])
) )
(re-frame/reg-event-fx (into s))))
::period-change-requested (:colspan c) (assoc :colspan (:colspan c))
(fn [{:keys [db]} [_ idx which]] (:align c) (assoc :align (:align c))
{ :db (-> db (= :dollar (:format c)) (assoc :align :right)
(forms/start-form ::period-form (= :percent (:format c)) (assoc :align :right)
{:idx idx (:bold c) (assoc-in [:style :font-weight] "bold")
:period which})) (:color c) (assoc-in [:style :color] (str "rgb("
:dispatch [::modal/modal-requested {:title (str "Change period " (date->str (nth which 0)) " - " (date->str (nth which 1))) (str/join ","
:class "" (:color c))
:body [change-period-body idx which] ")")))
:foot [change-period-foot idx which]}]}))
(def groupings cell-contents
{: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)}))]))
;; 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}
[: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] (defn cell-count [table]
(let [all-accounts @(re-frame/subscribe [::all-accounts]) (let [counts (map count (:rows table))]
periods @(re-frame/subscribe [::periods]) (if (seq counts)
include-deltas @(re-frame/subscribe [::include-deltas])] (apply max counts)
[:tr [:th.is-size-5 title] 0)))
(map-periods (defn table->pdf [{:keys [table widths]}]
(fn [i] (let [cell-count (cell-count table)]
(let [amount (aggregate-accounts (mapcat (fn [t] (-> [:table.table.compact.balance-sheet {:style nil}
(cond->> (filter-accounts all-accounts i (ranges t) client-id location) (map
(negs t) (map #(update % :amount -)))) (fn [i header]
types))] (into ^{:key i}
[:<> [:tr]
[:td.has-text-right [:span (->$ amount)]] (map
[:td.has-text-right (->% (percent-of-sales amount all-accounts i client-id location))]])) (fn [w header i]
(fn [i] ^{:key i} [cell {:width w} header])
[:td.has-text-right (->$ (- (aggregate-accounts (mapcat (fn [t] widths
(cond->> (filter-accounts all-accounts i (ranges t) client-id location) header
(negs t) (map #(update % :amount -)))) (range))))
types)) (range)
(aggregate-accounts (mapcat (fn [t] (:header table))]
(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]}] (into
[:<> (for [[i row] (map vector (range) (:rows table))]
^{:key i}
[:tr [:tr
[:td.has-text-right "Period"] (for [[i c] (map vector (range) (take cell-count (concat row (repeat nil))))]
(map-periods ^{:key i}
(fn [i] [cell {} c])]))
[:<> (conj ^{:key "last"}
[:td.has-text-centered {:colspan 2} [:tr (for [i (range cell-count)]
[:a {:on-click (dispatch-event [::period-change-requested i (get periods i)])} ^{:key i}
(or (get-in periods [i 2]) [cell {} {:value " "}])]))))
(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]]]]))
(re-frame/reg-sub (re-frame/reg-sub
::can-submit ::can-submit
@@ -756,7 +446,7 @@ Please download it by clicking this link: " report-url)))
[:a.button [:a.button
{:class (when (= selected-period "13 periods") "is-active") {:class (when (= selected-period "13 periods") "is-active")
:on-click (dispatch-event :on-click (dispatch-event
[::forms/change ::form [::change
[:periods] [:periods]
(let [today (or (some-> (:thirteen-periods-end data) (str->date standard)) (let [today (or (some-> (:thirteen-periods-end data) (str->date standard))
(local-today))] (local-today))]
@@ -784,7 +474,7 @@ Please download it by clicking this link: " report-url)))
[:a.button [:a.button
{:class (when (= selected-period "12 months") "is-active") {:class (when (= selected-period "12 months") "is-active")
:on-click (dispatch-event :on-click (dispatch-event
[::forms/change ::form [::change
[:periods] [:periods]
(let [end-date (or (some-> (:twelve-periods-end data) (str->date standard)) (let [end-date (or (some-> (:twelve-periods-end data) (str->date standard))
(local-today)) (local-today))
@@ -808,7 +498,7 @@ Please download it by clicking this link: " report-url)))
[:a.button [:a.button
{:class (when (= selected-period "Last week") "is-active") {:class (when (= selected-period "Last week") "is-active")
:on-click (dispatch-event :on-click (dispatch-event
[::forms/change ::form [::change
[:periods] [:periods]
(let [last-sunday (loop [current (local-today)] (let [last-sunday (loop [current (local-today)]
(if (= 7 (t/day-of-week current)) (if (= 7 (t/day-of-week current))
@@ -822,7 +512,7 @@ Please download it by clicking this link: " report-url)))
[:a.button [:a.button
{:class (when (= selected-period "Week to date") "is-active") {:class (when (= selected-period "Week to date") "is-active")
:on-click (dispatch-event :on-click (dispatch-event
[::forms/change ::form [::change
[:periods] [:periods]
(and-last-year [(loop [current (local-today)] (and-last-year [(loop [current (local-today)]
(if (= 1 (t/day-of-week current)) (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") {:class (when (= selected-period "Last Month") "is-active")
:on-click (dispatch-event :on-click (dispatch-event
[::forms/change ::form [::change
[:periods] [:periods]
(and-last-year [(t/minus (t/local-date (t/year (local-today)) (and-last-year [(t/minus (t/local-date (t/year (local-today))
(t/month (local-today)) (t/month (local-today))
@@ -852,7 +542,7 @@ Please download it by clicking this link: " report-url)))
[:a.button [:a.button
{:class (when (= selected-period "Month to date") "is-active") {:class (when (= selected-period "Month to date") "is-active")
:on-click (dispatch-event :on-click (dispatch-event
[::forms/change ::form [::change
[:periods] [:periods]
(and-last-year [(t/local-date (t/year (local-today)) (and-last-year [(t/local-date (t/year (local-today))
(t/month (local-today)) (t/month (local-today))
@@ -865,7 +555,7 @@ Please download it by clicking this link: " report-url)))
[:a.button [:a.button
{:class (when (= selected-period "Year to date") "is-active") {:class (when (= selected-period "Year to date") "is-active")
:on-click (dispatch-event :on-click (dispatch-event
[::forms/change ::form [::change
[:periods] [:periods]
(and-last-year [(t/local-date (t/year (local-today)) 1 1) (and-last-year [(t/local-date (t/year (local-today)) 1 1)
(local-today)]) (local-today)])
@@ -875,7 +565,7 @@ Please download it by clicking this link: " report-url)))
[:a.button [:a.button
{:class (when (= selected-period "Full year") "is-active") {:class (when (= selected-period "Full year") "is-active")
:on-click (dispatch-event :on-click (dispatch-event
[::forms/change ::form [::change
[:periods] [:periods]
[[(t/local-date (dec (t/year (local-today))) 1 1) [[(t/local-date (dec (t/year (local-today))) 1 1)
(t/local-date (dec (t/year (local-today))) 12 31)]] (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 [:a.button
{:class (when (= selected-period "Full year") "is-active") {:class (when (= selected-period "Full year") "is-active")
:on-click (dispatch-event :on-click (dispatch-event
[::forms/change ::form [::change
[:periods] [:periods]
(and-last-year [(t/plus (t/minus (local-today) (t/period :years 1)) (and-last-year [(t/plus (t/minus (local-today) (t/period :years 1))
(t/period :days 1)) (t/period :days 1))
@@ -921,7 +611,7 @@ Please download it by clicking this link: " report-url)))
[switch-field {:id "include-deltas" [switch-field {:id "include-deltas"
:checked (boolean include-deltas) :checked (boolean include-deltas)
:on-change (fn [e] :on-change (fn [e]
(re-frame/dispatch [::forms/change ::form (re-frame/dispatch [::change
[:include-deltas] (.-checked (.-target e))])) [:include-deltas] (.-checked (.-target e))]))
:label "Include deltas" :label "Include deltas"
:type "checkbox"}]]]] :type "checkbox"}]]]]
@@ -937,26 +627,33 @@ Please download it by clicking this link: " report-url)))
(when-not @!box (when-not @!box
(reset! !box el)))}]])))) (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)))
(defn profit-and-loss-content [] pnl-data (->> report-data
(let [status @(re-frame/subscribe [::status/single ::page]) :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]) unresolved-accounts @(re-frame/subscribe [::uncategorized-accounts])
clients-by-id @(re-frame/subscribe [::subs/clients-by-id]) client-names (->> @(re-frame/subscribe [::subs/clients-by-id])
{:keys [data report active? error id]} @(re-frame/subscribe [::forms/form ::form]) (map (fn [[k v]]
{:keys [form-inline field raw-field error-notification submit-button ]} pnl-form [k (:name v)]))
periods (:periods data)] (into {}))
(form-inline {} pnl-data (l-reports/->PNLData args pnl-data client-names)
report (l-reports/summarize-pnl pnl-data)]
[:div [:div
[status/status-notification {:statuses [[::status/single ::page]]}] [:h1.title "Profit and Loss - " (str/join ", " (map :name (:clients args)))]
[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) (when (seq unresolved-accounts)
[:div.notification.is-warning.is-light [:div.notification.is-warning.is-light
"This report does not include " (str/join ", " "This report does not include " (str/join ", "
@@ -964,23 +661,32 @@ Please download it by clicking this link: " report-url)))
" all locations" " all locations"
(:location %))) (:location %)))
unresolved-accounts))]) unresolved-accounts))])
[:<> (for [[index table] (map vector (range) (concat (:summaries report)
(for [[client-id location] @(re-frame/subscribe [::locations])] (:details report)))]
^{:key (str client-id "-" location "-summary")} ^{:key index}
[location-summary client-id location (:include-deltas data)] [table->pdf {:widths (into [20] (take (dec (cell-count table))
)] (mapcat identity
[:h2.title.is-4 {:style {:margin-bottom "1rem"}} "Detail"] (repeat
[:table.table.compact.balance-sheet (if (-> pnl-data :args :include-deltas)
[:tbody [13 6 13]
[period-header {:include-deltas (:include-deltas data) [13 6])))))
:periods periods}] :table table}])]))
[:<>
(for [[client-id location] @(re-frame/subscribe [::locations])] (defn profit-and-loss-content []
^{:key (str client-id "-" location)} (let [status @(re-frame/subscribe [::status/single ::page])
[:<> {:keys [data report active? error id]} @(re-frame/subscribe [::forms/form ::form])
[:tr [:th.is-size-3 (:name (clients-by-id client-id))]] {:keys [form-inline field raw-field error-notification submit-button ]} pnl-form
[location-rows client-id location]])]]]])]))) ]
(form-inline {}
[:div
[status/status-notification {:statuses [[::status/single ::page]]}]
[report-controls pnl-form]
[status/big-loader status]
(when (and (not= :loading (:state status))
report)
[pnl-report {:report-data report
:args data}])])))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::unmounted-pnl ::unmounted-pnl