(ns auto-ap.ledger.reports #?@ (:clj [(:require [auto-ap.time :as atime] [auto-ap.time-utils :refer [user-friendly-date]] [auto-ap.utils :refer [dollars-0? dollars=]] [clojure.string :as str] [clj-time.coerce :as coerce] [auto-ap.time-utils :refer [user-friendly-date]] )] :cljs [(:require [auto-ap.utils :refer [dollars-0? dollars=]] [auto-ap.views.utils :as au] [clojure.string :as str] [auto-ap.time-utils :refer [user-friendly-date]])])) (defn date->str [d] #?(:clj (if (inst? d) (atime/unparse-local (coerce/to-date-time d) atime/normal-date) (atime/unparse-local d atime/normal-date)) :cljs (au/date->str d au/pretty))) (def groupings {:assets [["1100 Cash and Bank Accounts" 11000 11999] ["1200 Accounts Receivable" 12000 12999] ["1300 Inventory" 13000 13999] ["1400 Prepaid Expenses" 14000 14999] ["1500 Property and Equipment" 15000 15999] ["1600 Intangible Assets" 16000 16999] ["1700 Other Assets" 17000 19999]] :liabilities [["2000 Accounts Payable" 20100 23999] ["2400 Accrued Expenses" 24000 24999] ["2500 Other Liabilities" 25000 25999] ["2600 Split Accounts" 26000 26999] ["2700 Current Portion of Long-Term Debt" 27000 27999] ["2800 Notes Payable" 28000 28999]] :equities [["3000 Owner's Equity" 30000 39999]] :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]] :operating-activities [ ;; BEN EDIT STARTING HERE ["20100-20199 Credit Card Balances" 20100 20199 :add] ["21000-24000 Accounts Payable" 21000 23999 :add] ["25000-28000 Accounts Payable" 25000 27999 :add] ["24000-25000 Accrual Liabilities" 24000 24999 :add] ["12000-13000 Accounts Receivable" 12000 13000 :subtract] ["96000-97000 Depreciation Expense" 96000 96999 :add] ["13000-15000 Inventory" 13000 14999 :subtract] ;; BEN ENDING HERE ] :investment-activities [ ;; BEN EDIT STARTING HERE ["15000-18000 Investments" 15000 17999 :subtract] ;; BEN ENDING HERE ] :financing-activities [ ;; BEN EDIT STARTING HERE ["30000-33000 Other Equity Accounts" 30000 32999 :add] ["33000-34000 Owner's Contributions" 33000 33999 :add] ["34000-35000 Owner's Distributions" 34000 34999 :add] ["35000-36000 Retained Earnings" 35000 35999 :add] ["28000-29000 Loans (payable)" 28000 28999 :add] ;; BEN ENDING HERE ] :cash [ ;; BEN EDIT STARTING HERE ["11000-11400 Bank Accounts / Cash" 11000 11399 :add] ;; BEN ENDING HERE ]}) (def cashflow-aggregation (->> (select-keys groupings [:operating-activities :investment-activities :financing-activities]) vals (mapcat identity) (map (fn [[_ start end rule]] [start end rule])) (into []))) (defn cashflow-account->amount [account-code amount] (let [operation (->> cashflow-aggregation (filter (fn [[start end]] (<= start account-code end))) first last)] (cond (= operation :add) amount (= operation :subtract) (- amount) :else amount))) (defn min-numeric-code [category] (->> (groupings category) (map second) (apply min))) (defn max-numeric-code [category] (->> (groupings category) (map second) (apply max))) (def flat-categories (for [[category groups] groupings [_ start end] groups] [category start end])) (defn in-range? [code] (if code (reduce (fn [acc [_ start end]] (if (<= start code end) (reduced true) acc)) false flat-categories) false)) (defn client-locations [pnl-data] (->> pnl-data :data (filter (comp in-range? :numeric-code)) (filter #(not= "A" (:location %))) (group-by (juxt :client-id :location)) (filter (fn [[_ 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))])))) (defn aggregate-accounts [pnl-data] (reduce (fnil + 0.0) 0.0 (map :amount (:data pnl-data)))) (defn aggregate-cashflow-accounts [pnl-data] (reduce (fnil + 0.0) 0.0 (map (fn [d] (cashflow-account->amount (:numeric-code d) (:amount d))) (:data pnl-data)))) (defn aggregate-credits [pnl-data] (reduce (fnil + 0.0) 0.0 (map :credits (:data pnl-data)))) (defn aggregate-debits [pnl-data] (reduce (fnil + 0.0) 0.0 (map :debits (:data pnl-data)))) (defn best-category [a] (->> flat-categories (filter (fn [[category start end]] (and (:numeric-code a) (<= start (:numeric-code a) end)))) first first)) (defn filter-client [pnl-data client] (-> 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 (update :data (fn [data] ((group-by :location data) location))) (update :filters (fn [f] (assoc f :location location))))) (defn filter-locations [pnl-data locations] (-> pnl-data (update :filters (fn [f] (assoc f :locations locations))))) (defn filter-numeric-code [pnl-data from to] (-> pnl-data (update :data (fn [data] (filter #(<= from (or (:numeric-code %) 0) to) data))) (update :filters (fn [f] (assoc f :numeric-code [{:from from :to to}]))))) (defn account-belongs-in-category? [numeric-code category] (->> (groupings category) (some (fn [[_ from to]] (<= from (or numeric-code 0) to))))) ;; TODO make click-through work for multiple ranges :fliters (defn filter-categories [pnl-data categories] (-> pnl-data (update :data (fn [data] (for [account data :when (some #(account-belongs-in-category? (:numeric-code account) %) categories)] account))) (update :filters (fn [f] (assoc f :numeric-code (for [category categories [_ from to] (groupings category)] {:from from :to to})))))) (defn filter-period [pnl-data period] (-> pnl-data (update :data (fn [data] ((group-by :period data) period))) (update :filters (fn [f] (assoc f :date-range period))))) (defn zebra [pnl-data i] (if (odd? i) (assoc-in pnl-data [:cell-args :bg-color] [240 240 240]) pnl-data)) (defn negate [pnl-data types] (update pnl-data :data (fn [accounts] (map (fn [account] (if (types (best-category account)) (update account :amount #(- (or % 0.0))) account)) accounts)))) (defn used-accounts [pnl-datas] (->> pnl-datas (mapcat :data) (map #(select-keys % [:numeric-code :name])) (set) (sort-by :numeric-code))) (defn subtotal-by-column-row [pnl-datas title & [cell-args]] (into [{:value title :bold true}] (map (fn [p] (merge {:format :dollar :value (aggregate-accounts p) :filters (when (:numeric-code (:filters p)) ;; don't allow filtering when you don't at least filter numeric codes (:filters p))} (:cell-args p) cell-args)) pnl-datas))) (defn cashflow-subtotal-by-column-row [pnl-datas title & [cell-args]] (into [{:value title :bold true}] (mapcat (fn [p] [ (merge {:format :text :value nil} (:cell-args p) cell-args) (merge {:format :text :value nil} (:cell-args p) cell-args) (merge {:format :dollar :value (aggregate-cashflow-accounts p) :filters (when (:numeric-code (:filters p)) ;; don't allow filtering when you don't at least filter numeric codes (:filters p))} (:cell-args p) cell-args)]) pnl-datas))) (defn calc-percent-of-sales [table pnl-datas] (let [sales (map (fn [p] (aggregate-accounts (filter-categories p [:sales]))) pnl-datas)] (->> table (map (fn [[_ & values]] (map (fn [v s] {:border (:border v) :bg-color (:bg-color v) :format (if (string? (:value v)) :text :percent) :color [128 128 128] :value (cond (string? (:value v)) "" (dollars-0? s) 0.0 :else (/ (:value v) s))}) values sales)))))) (defn calc-deltas [table] (->> table (map (fn [[_ & values]] (->> values (partition 2 1) (map (fn [[a b]] (if (or (string? (:value b)) (string? (:value a))) {:value "" :format :text :bg-color (:bg-color b)} {:border (:border b) :format :dollar :value (- (:value b) (:value a)) :bg-color (:bg-color b)})))))))) (defn combine-tables ([[pnl-data] table percent-of-sales deltas] (map (fn [[title & row] percent-of-sales deltas ] (let [deltas (cons {:value 0.0 :format :dollar :border (:border (first row))} deltas)] (into [title] (mapcat (fn [v p d] (if (:include-deltas (:args pnl-data)) [v p d] [v p])) row percent-of-sales deltas)))) table percent-of-sales deltas))) (defn headers [[pnl-data :as pnl-datas] header-title] (let [big-header (into [{:value header-title :bold true}] (map-indexed (fn [i p] (cond-> {:value (str (date->str (:start p)) " - " (date->str (:end p))) :colspan (cond (-> pnl-data :args :include-deltas) 3 (-> pnl-data :args :column-per-location) (* 2 (/ (count pnl-datas) (count (-> pnl-data :args :periods)))) :else 2) :align :center :bold true} (odd? i) (assoc :bg-color [240 240 240]))) (:periods (:args pnl-data)))) sub-header (into [{:value ""}] (if (-> pnl-data :args :column-per-location) (mapcat (fn [p] (cond-> [(merge {:value (or (when (-> p :filters :location) (str ((-> p :client-codes) (-> p :filters :client-id)) "-" (-> p :filters :location))) "Total") :align :right} (:cell-args p)) (merge {:value "%" :align :right} (:cell-args p))] (-> pnl-data :args :include-deltas) (conj (merge {:value "+/-" :align :right} (:cell-args p))))) pnl-datas) (mapcat (fn [p] (cond-> [(merge {:value "Amt" :align :right} (:cell-args p)) (merge {:value "%" :align :right} (:cell-args p))] (-> pnl-data :args :include-deltas) (conj (merge {:value "+/-" :align :right} (:cell-args p))))) pnl-datas)))] [big-header sub-header])) (defn location-summary-table [pnl-datas title] (let [table [(subtotal-by-column-row (map #(filter-categories % [:sales]) pnl-datas) "Sales") (subtotal-by-column-row (map #(filter-categories % [:cogs ]) pnl-datas) "Cogs") (subtotal-by-column-row (map #(filter-categories % [:payroll ]) pnl-datas) "Payroll") (subtotal-by-column-row (map #(-> % (filter-categories [:sales :payroll :cogs]) (negate #{:payroll :cogs})) pnl-datas) "Gross Profits") (subtotal-by-column-row (map #(filter-categories % [:controllable :fixed-overhead :ownership-controllable]) pnl-datas) "Overhead") (subtotal-by-column-row (map #(-> % (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) pnl-datas) "Net Income")] percent-of-sales (calc-percent-of-sales table pnl-datas) deltas (calc-deltas table)] {:header (headers pnl-datas title) :rows (combine-tables pnl-datas table percent-of-sales deltas)})) (defn detail-rows [pnl-datas grouping title] (let [pnl-datas (map #(filter-categories % [grouping]) pnl-datas) individual-accounts (for [[grouping-name from to] (groupings grouping) :let [pnl-datas (map #(filter-numeric-code % from to) pnl-datas) account-codes (used-accounts pnl-datas)] :when (seq account-codes) row (-> [(into [{:value (str "---" grouping-name "---")}] (map (fn [p] (assoc (:cell-args p) :value "" :format "")) pnl-datas) )] (into (for [{:keys [numeric-code name]} account-codes] (into [{:value (str name ":" numeric-code)}] (map (fn [p] (let [pnl-data (-> p (filter-numeric-code numeric-code numeric-code)) this-name-exists? (->> (:data p) (filter (comp #{name} :name)) seq)] (merge (if this-name-exists? {:format :dollar :filters (:filters pnl-data) :value (aggregate-accounts pnl-data)} {:filters (:filters pnl-data) :value ""}) (:cell-args p)))) pnl-datas)))) (conj (subtotal-by-column-row pnl-datas "" {:border [:top]})))] row)] (-> [(into [{:value title :bold true}] (map (fn [p] (assoc (:cell-args p) :value "" :format "")) pnl-datas))] (into individual-accounts) (conj (subtotal-by-column-row pnl-datas title))))) (defn location-detail-table [pnl-datas client-datas title prefix] (let [table (-> [] (into (detail-rows pnl-datas :sales (str prefix " Sales"))) (into (detail-rows pnl-datas :cogs (str prefix " COGS"))) (into (detail-rows pnl-datas :payroll (str prefix " Payroll"))) (conj (subtotal-by-column-row (map #(filter-categories % [:payroll :cogs]) pnl-datas) (str prefix " Prime Costs"))) (conj (subtotal-by-column-row (map #(-> % (filter-categories [:sales :payroll :cogs]) (negate #{:payroll :cogs})) pnl-datas) (str prefix " Gross Profits"))) (into (detail-rows pnl-datas :controllable (str prefix " Controllable Expenses"))) (into (detail-rows pnl-datas :fixed-overhead (str prefix " Fixed Overhead"))) (into (detail-rows pnl-datas :ownership-controllable (str prefix " Ownership Controllable"))) (conj (subtotal-by-column-row (map #(filter-categories % [:controllable :fixed-overhead :ownership-controllable]) pnl-datas) (str prefix " Overhead"))) (conj (subtotal-by-column-row (map #(-> % (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) pnl-datas) (str prefix " Net Income")))) table (if (seq client-datas) (conj table (subtotal-by-column-row (map #(-> % (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) client-datas) "All Location Net Income")) table) percent-of-sales (calc-percent-of-sales table pnl-datas) deltas (into [] (calc-deltas table))] {:header (headers pnl-datas title) :rows (combine-tables pnl-datas table percent-of-sales deltas)})) (defn cash-flow-headers [[pnl-data :as pnl-datas] header-title] (let [big-header (into [{:value header-title :bold true}] (map-indexed (fn [i p] (cond-> {:value (str (date->str (:start p)) " - " (date->str (:end p))) :colspan 3 :align :center :bold true} (odd? i) (assoc :bg-color [240 240 240]))) (:periods (:args pnl-data)))) sub-header (into [{:value "Account"}] (mapcat (fn [p] [(merge {:value "Increases" :align :right} (:cell-args p)) (merge {:value "Decreases" :align :right} (:cell-args p)) (merge {:value "+/- in Cash" :align :right} (:cell-args p))]) pnl-datas))] [big-header sub-header])) (defn cash-flow-detail-rows [pnl-datas grouping title] (let [pnl-datas (map #(filter-categories % [grouping]) pnl-datas) individual-accounts (for [[grouping-name from to add-or-subtract] (groupings grouping) :let [pnl-datas (map #(filter-numeric-code % from to) pnl-datas) account-codes (used-accounts pnl-datas)] :when (seq account-codes) row (-> [(into [{:value (str "---" grouping-name "---")}] (mapcat (fn [p] [(assoc (:cell-args p) :value "" :format "") (assoc (:cell-args p) :value "" :format "") (assoc (:cell-args p) :value "" :format "")]) pnl-datas) )] (into (for [{:keys [numeric-code name]} account-codes] (into [{:value name}] (mapcat (fn [p] (let [pnl-data (-> p (filter-numeric-code numeric-code numeric-code)) aggregated (aggregate-accounts pnl-data) credits (aggregate-credits pnl-data) debits (aggregate-debits pnl-data)] (if (dollars= (- debits credits) aggregated) [(merge {:format :dollar :filters (:filters pnl-data) :value debits} (:cell-args p)) (merge {:format :dollar :filters (:filters pnl-data) :value credits} (:cell-args p)) (merge {:format :dollar :filters (:filters pnl-data) :value (cashflow-account->amount numeric-code aggregated)} (:cell-args p))] [(merge {:format :dollar :filters (:filters pnl-data) :value credits} (:cell-args p)) (merge {:format :dollar :filters (:filters pnl-data) :value debits} (:cell-args p)) (merge {:format :dollar :filters (:filters pnl-data) :value (cashflow-account->amount numeric-code aggregated)} (:cell-args p))]))) pnl-datas)))) (conj (cashflow-subtotal-by-column-row pnl-datas "" {:border [:top]})))] row)] (-> [(into [{:value title :bold true}] (mapcat (fn [p] [(assoc (:cell-args p) :value "" :format "") (assoc (:cell-args p) :value "" :format "") (assoc (:cell-args p) :value "" :format "")]) pnl-datas))] (into individual-accounts) (conj (cashflow-subtotal-by-column-row pnl-datas title))))) (defn cash-flows-table [pnl-datas #_client-datas title prefix] (let [table (-> [] (conj (cashflow-subtotal-by-column-row (map #(-> % (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) pnl-datas) "Net Income")) (into (cash-flow-detail-rows pnl-datas :operating-activities (str prefix " Operating Activities"))) (into (cash-flow-detail-rows pnl-datas :investment-activities (str prefix " Investment Activities"))) (into (cash-flow-detail-rows pnl-datas :financing-activities (str prefix " Financing Activities"))) (conj (cashflow-subtotal-by-column-row (map #(-> % (filter-categories [:operating-activities :investment-activities :financing-activities :sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) pnl-datas) "Change in Cash and Cash Equivalents")) (into (cash-flow-detail-rows pnl-datas :cash (str prefix " Bank Accounts / Cash"))) )] {:header (cash-flow-headers pnl-datas "Cash Flow") :rows table})) (defn warning-message [pnl-data] (let [errors (->> pnl-data :data (filter (fn [{:keys [numeric-code]}] (nil? numeric-code)))) error-count (count errors)] (when (> error-count 0) (str "This report does not include " (str/join ", " (map #(str (:count %) " unresolved ledger entries for " (if (str/blank? (:location %)) " all locations" (:location %))) errors)))))) (defn summarize-pnl [pnl-data] {:warning (warning-message pnl-data) :summaries (if (-> pnl-data :args :column-per-location) [(location-summary-table (mapcat identity (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] (concat (for [[client-id location] (client-locations pnl-data)] (-> pnl-data (filter-client client-id) (filter-location location) (filter-period period) (zebra i))) [(zebra (filter-locations (filter-period pnl-data period) (map second (client-locations pnl-data))) i)]))) "All location Summary")] (for [[client-id location] (client-locations pnl-data)] (location-summary-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] (-> pnl-data (filter-client client-id) (filter-location location) (filter-period period) (zebra i))) (str (-> pnl-data :clients-by-id (get client-id)) " (" location ") Summary")))) :details (doall (if (-> pnl-data :args :column-per-location) [(location-detail-table (mapcat identity (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] (concat (for [[client-id location] (client-locations pnl-data)] (-> pnl-data (filter-client client-id) (filter-location location) (filter-period period) (zebra i))) [(-> pnl-data (filter-period period) (filter-locations (map second (client-locations pnl-data))) (zebra i))]))) nil "All location Detail" "")] (for [[client-id location] (client-locations pnl-data)] (location-detail-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] (-> pnl-data (filter-client client-id) (filter-location location) (filter-period period) (zebra i))) (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] (-> pnl-data (filter-client client-id) (filter-period period) (filter-locations (map second (client-locations pnl-data))) (zebra i))) (str (-> pnl-data :clients-by-id (get client-id)) " (" location ") Detail") location))))}) (defn summarize-cash-flows [pnl-data] (let [client-ids (->> (client-locations pnl-data) (map first) set)] {:warning (warning-message pnl-data) :details (doall (for [client-id client-ids] (cash-flows-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] (-> pnl-data (filter-client client-id) (filter-period period) (zebra i))) (str (-> pnl-data :clients-by-id (get client-id)) " Detail") "")))})) (defn balance-sheet-headers [pnl-data] (let [period-count (count (:periods (:args pnl-data)))] (cond-> [] (> (count (set (map :client-id (:data pnl-data)))) 1) (conj (into [{:value "Client"}] (mapcat identity (for [client (set (map :client-id (:data pnl-data))) ] (cond-> [{:value (str (-> pnl-data :client-codes (get client)))}] (> period-count 1) (into (apply concat (repeat (dec period-count) ["" ""])))))))) true (conj (into [{:value "Period Ending"}] (for [client (set (map :client-id (:data pnl-data))) [index p] (map vector (range) (:periods (:args pnl-data))) :let [is-first? (= 0 index) period-date (date->str p) period-headers (if is-first? [{:value period-date}] [{:value period-date} {:value "+/-"}])] header period-headers] header)))))) (defn append-deltas [table] (->> table (map (fn [[title & values]] (loop [result [title] previous nil [current :as values] values] (if current (recur (cond-> result true (conj current) (and (:value current) (:value previous) (number? (:value current)) (number? (:value previous)) (= (:client-id (:filters previous)) (:client-id (:filters current)))) (conj {:border (:border previous) :format :dollar :value (- (or (:value current) 0.0) (or (:value previous) 0.0))})) current (rest values)) result)))))) #_(defn summarize-balance-sheet [pnl-data] (reduce (fn [result table] (-> result (update :header into (:header table)) (update :rows (fn [current-rows] (if (seq current-rows) (map concat current-rows (:rows table)) (:rows table)))))) {:header [] :rows []} (for [client-id (set (map :client-id (:data pnl-data)))] (let [pnl-datas (map (fn [p] (-> pnl-data (filter-client client-id) (filter-period p))) (:periods (:args pnl-data))) table (-> [] (into (detail-rows pnl-datas :assets "Assets")) (into (detail-rows pnl-datas :liabilities "Liabilities")) (into (detail-rows pnl-datas :equities "Owner's Equity")) (conj (subtotal-by-column-row (map #(-> % (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) pnl-datas) "Retained Earnings"))) table (if (:include-comparison (:args pnl-data)) (append-deltas table) table)] {:header (balance-sheet-headers pnl-data) :rows table}))) ) (defn summarize-balance-sheet [pnl-data] (let [pnl-datas (for [client-id (set (map :client-id (:data pnl-data))) p (:periods (:args pnl-data))] (-> pnl-data (filter-client client-id) (filter-period p)))] (let [table (-> [] (into (detail-rows pnl-datas :assets "Assets")) (into (detail-rows pnl-datas :liabilities "Liabilities")) (into (detail-rows pnl-datas :equities "Owner's Equity")) (conj (subtotal-by-column-row (map #(-> % (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) pnl-datas) "Retained Earnings"))) table (if (> (count (:periods (:args pnl-data))) 1) (append-deltas table) table)] {:warning (warning-message pnl-data) :header (balance-sheet-headers pnl-data) :rows table})) ) (defn journal-detail-report [args data client-codes] {:header [[{:value "Category"} {:value "Date"} {:value "Description"} {:value "Debit"} {:value "Credit"} {:value "Running Balance"}]] :rows (reduce (fn [rows category] (into rows ;; TODO colspan ? (concat (when (seq (:journal-entries category)) [[ {:value (str (client-codes (:client-id category)) " - " (:location category) " - " (:name (:account category)))} {:value ""} {:value ""} {:value ""} {:value ""} {:value ""}]]) (map (fn [je] [{:value ""} {:value (user-friendly-date (:date je))} {:value (:description je "")} {:value (get je :debit) :format :dollar} {:value (get je :credit) :format :dollar} {:value (get je :running-balance) :format :dollar}]) (:journal-entries category)) [[ {:value (str (client-codes (:client-id category)) " - " (:location category) " - " (:name (:account category))) :bold true :border [:top]} {:value "" :border [:top]} {:value (str "Total" ) :bold true :border [:top]} {:value "" :border [:top]} {:value "" :border [:top]} {:value (:total category) :format :dollar :bold true :border [:top]}]])) ) [] (:categories data))} ) (defrecord PNLData [args data client-codes])