trimming down profit and loss considerably.
This commit is contained in:
@@ -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))))
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user