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)]
(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))))

View File

@@ -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])

View File

@@ -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