feat: complete automatic sales summary calculations and ledger posting

This commit is contained in:
2026-04-25 08:43:16 -07:00
committed by Bryce
parent a78c818270
commit baf8cfff97
8 changed files with 529 additions and 105 deletions

View File

@@ -5,10 +5,11 @@
[iol-ion.tx.propose-invoice]
[iol-ion.tx.reset-rels]
[iol-ion.tx.reset-scalars]
[iol-ion.tx.upsert-entity]
[iol-ion.tx.upsert-invoice]
[iol-ion.tx.upsert-ledger]
[iol-ion.tx.upsert-transaction]
[iol-ion.tx.upsert-entity]
[iol-ion.tx.upsert-invoice]
[iol-ion.tx.upsert-ledger]
[iol-ion.tx.upsert-transaction]
[iol-ion.tx.upsert-sales-summary-ledger]
[com.github.ivarref.gen-fn :refer [gen-fn! datomic-fn]]
[auto-ap.utils :refer [default-pagination-size by]]
[clojure.edn :as edn]

View File

@@ -276,48 +276,108 @@
:ledger-mapped/amount amount
:ledger-mapped/ledger-side :ledger-side/debit}))
(defn- summary->journal-entry [db summary-id]
(let [summary (dc/pull db '[:sales-summary/client
:sales-summary/date
{:sales-summary/items [:sales-summary-item/category
:ledger-mapped/account
:ledger-mapped/amount
:ledger-mapped/ledger-side]}]
summary-id)
items (:sales-summary/items summary)
;; Aggregate items by account and ledger side
aggregated (->> items
(filter :ledger-mapped/account)
(group-by :ledger-mapped/account)
(map (fn [[account acc-items]]
(reduce
(fn [m item]
(update m (:ledger-mapped/ledger-side item) (fnil + 0.0) (:ledger-mapped/amount item 0.0)))
{:account account}
acc-items))))
line-items (mapv (fn [{:keys [account] :as m}]
(cond-> {:db/id (str (java.util.UUID/randomUUID))
:journal-entry-line/account account}
(get m :ledger-side/debit) (assoc :journal-entry-line/debit (get m :ledger-side/debit))
(get m :ledger-side/credit) (assoc :journal-entry-line/credit (get m :ledger-side/credit))))
aggregated)
total-amount (reduce + 0.0 (map #(get % :ledger-side/debit 0.0) aggregated))]
(when (seq line-items)
{:journal-entry/source "sales-summary"
:journal-entry/client (:db/id (:sales-summary/client summary))
:journal-entry/date (:sales-summary/date summary)
:journal-entry/original-entity summary-id
:journal-entry/amount total-amount
:journal-entry/line-items line-items})))
(defn- calc-aggregate-totals [items]
(reduce
(fn [acc item]
(let [cat (:sales-summary-item/category item)
amt (:ledger-mapped/amount item 0.0)
side (:ledger-mapped/ledger-side item)]
(cond-> acc
;; Payments (debits)
(= cat "Card Payments") (update :sales-summary/total-card-payments (fnil + 0.0) amt)
(= cat "Cash Payments") (update :sales-summary/total-cash-payments (fnil + 0.0) amt)
(= cat "Food App Payments") (update :sales-summary/total-food-app-payments (fnil + 0.0) amt)
(= cat "Gift Card Payments") (update :sales-summary/total-gift-card-payments (fnil + 0.0) amt)
(= cat "Unknown") (update :sales-summary/total-unknown-payments (fnil + 0.0) amt)
;; Refunds (credits)
(= cat "Card Refunds") (update :sales-summary/total-card-refunds (fnil + 0.0) amt)
(= cat "Cash Refunds") (update :sales-summary/total-cash-refunds (fnil + 0.0) amt)
(= cat "Food App Refunds") (update :sales-summary/total-food-app-refunds (fnil + 0.0) amt)
;; Other
(= cat "Fees") (update :sales-summary/total-fees (fnil + 0.0) amt)
(= cat "Discounts") (update :sales-summary/total-discounts (fnil + 0.0) amt)
(= cat "Tax") (update :sales-summary/total-tax (fnil + 0.0) amt)
(= cat "Tip") (update :sales-summary/total-tip (fnil + 0.0) amt)
(= cat "Returns") (update :sales-summary/total-returns (fnil + 0.0) amt)
;; Net from sales line items
:else (update :sales-summary/total-net (fnil + 0.0) amt))))
{}
items))
(defn sales-summaries-v2 []
(doseq [[c client-code] (dc/q '[:find ?c ?client-code
:in $
:where [?c :client/code ?client-code]]
(dc/db conn))
{:sales-summary/keys [date] :db/keys [id]} (dirty-sales-summaries c)]
:in $
:where [?c :client/code ?client-code]]
(dc/db conn))
{:sales-summary/keys [date] :db/keys [id] :as existing-summary} (dirty-sales-summaries c)]
(mu/with-context {:client-code client-code
:date date}
(alog/info ::updating)
(let [result {:db/id id
:sales-summary/client c
:sales-summary/date date
:sales-summary/dirty false
:sales-summary/client+date [c date]
:sales-summary/items
(->>
(get-sales c date)
(concat (get-payment-items c date))
(concat (get-refund-items c date))
(cons (get-discounts c date))
(cons (get-fees c date))
(cons (get-tax c date))
(cons (get-tip c date))
(cons (get-returns c date))
(filter identity)
(map (fn [z]
(assoc z :ledger-mapped/account (some-> z :sales-summary-item/category str/lower-case name->number lookup-account)
:sales-summary-item/manual? false))
)) }]
(if (seq (:sales-summary/items result))
(do
(alog/info ::upserting-summaries
:category-count (count (:sales-summary/items result)))
@(dc/transact conn [[:upsert-entity result]]))
@(dc/transact conn [{:db/id id :sales-summary/dirty false}]))))))
(let [c (auto-ap.datomic/pull-attr (dc/db conn) :db/id [:client/code "NGCL" ])
date #inst "2024-04-14T00:00:00-07:00"]
(get-payment-items c date)
)
:date date}
(alog/info ::updating)
(let [manual-items (->> existing-summary
:sales-summary/items
(filter :sales-summary-item/manual?))
calculated-items (->>
(get-sales c date)
(concat (get-payment-items c date))
(concat (get-refund-items c date))
(cons (get-discounts c date))
(cons (get-fees c date))
(cons (get-tax c date))
(cons (get-tip c date))
(cons (get-returns c date))
(filter identity)
(map (fn [z]
(assoc z :ledger-mapped/account (some-> z :sales-summary-item/category str/lower-case name->number lookup-account)
:sales-summary-item/manual? false))))
all-items (concat calculated-items manual-items)
aggregates (calc-aggregate-totals all-items)
result (into {:db/id id
:sales-summary/client c
:sales-summary/date date
:sales-summary/dirty false
:sales-summary/client+date [c date]
:sales-summary/items all-items}
aggregates)]
(if (seq (:sales-summary/items result))
(do
(alog/info ::upserting-summaries
:category-count (count (:sales-summary/items result)))
@(dc/transact conn [[:upsert-entity result]]))
@(dc/transact conn [{:db/id id :sales-summary/dirty false}]))))))
(defn reset-summaries []

View File

@@ -35,27 +35,42 @@
invoices-missing-ledger-entries (->> (dc/q {:find ['?t ]
:in ['$ '?sd]
:where ['[?t :invoice/date ?d]
'[(>= ?d ?sd)]
'(not [_ :journal-entry/original-entity ?t])
'[?t :invoice/total ?amt]
'[(not= 0.0 ?amt)]
'(not [?t :invoice/status :invoice-status/voided])
'(not [?t :invoice/import-status :import-status/pending])
'(not [?t :invoice/exclude-from-ledger true])
]}
(dc/db conn) start-date)
:in ['$ '?sd]
:where ['[?t :invoice/date ?d]
'[(>= ?d ?sd)]
'(not [_ :journal-entry/original-entity ?t])
'[?t :invoice/total ?amt]
'[(not= 0.0 ?amt)]
'(not [?t :invoice/status :invoice-status/voided])
'(not [?t :invoice/import-status :import-status/pending])
'(not [?t :invoice/exclude-from-ledger true])
]}
(dc/db conn) start-date)
(map first)
(mapv (fn [i]
[:upsert-invoice {:db/id i}])))
repairs (vec (concat txes-missing-ledger-entries invoices-missing-ledger-entries))]
sales-summaries-missing-ledger-entries (->> (dc/q {:find ['?ss ]
:in ['$ '?sd]
:where ['[?ss :sales-summary/date ?d]
'[(>= ?d ?sd)]
'(not [_ :journal-entry/original-entity ?ss])
'[?ss :sales-summary/items ?item]
'[?item :ledger-mapped/account]
]}
(dc/db conn) start-date)
(map first)
(mapv (fn [ss]
[:upsert-sales-summary-ledger {:db/id ss}])))
repairs (vec (concat txes-missing-ledger-entries invoices-missing-ledger-entries sales-summaries-missing-ledger-entries))]
(when (seq repairs)
(mu/log ::ledger-repairs-needed
:sample (take 3 repairs)
:transaction-count (count txes-missing-ledger-entries)
:invoice-count (count invoices-missing-ledger-entries))
@(dc/transact conn repairs)))))
(mu/log ::ledger-repairs-needed
:sample (take 3 repairs)
:transaction-count (count txes-missing-ledger-entries)
:invoice-count (count invoices-missing-ledger-entries)
:sales-summary-count (count sales-summaries-missing-ledger-entries))
@(dc/transact conn repairs)))))
(defn touch-transaction [e]

View File

@@ -72,14 +72,14 @@
[:sales-summary/date :xform clj-time.coerce/from-date]
{:sales-summary/client [:client/code :client/name :db/id]}
{:sales-summary/items [{[:ledger-mapped/ledger-side :xform iol-ion.query/ident] [:db/ident]
} ;; TODO clientize
:ledger-mapped/account
:ledger-mapped/amount
:sales-summary-item/category
:sales-summary-item/sort-order
:db/id
:sales-summary-item/manual?]
} ]) ;; TODO
}
:ledger-mapped/account
:ledger-mapped/amount
:sales-summary-item/category
:sales-summary-item/sort-order
:db/id
:sales-summary-item/manual?]
} ])
(defn fetch-ids [db request]
(let [query-params (:query-params request)
@@ -179,10 +179,8 @@
:db/id (:db/id entity))}
svg/pencil)])
:oob-render
(fn [request]
[#_(assoc-in (date-range-field {:value {:start (:start-date (:query-params request))
:end (:end-date (:query-params request))}
:id "date-range"}) [1 :hx-swap-oob] true)]) ;; TODO
(fn [_request]
[])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
"Admin"]
@@ -241,13 +239,9 @@
:primary
:red)} "Total: " (format "$%,.2f" total-credits))]]))}]}))
;; TODO schema cleanup
;; Decide on what should be calculated as generating ledger entries, and what should be calculated
;; as part of the summary
;; default thought here is that the summary has more detail (e.g., line items), fees broken out by type
;; and aggregated into the final ledger entry
;; that allows customization at any level.
;; TODO rename refunds/returns
;; Architecture: Sales summary maintains granular detail (line items, fee types)
;; and is aggregated into ledger entries by account/location. Manual adjustments
;; are preserved during automatic recalculation.
(def row* (partial helper/row* grid-page))
(def table* (partial helper/table* grid-page))
@@ -436,15 +430,14 @@
(com/data-grid-header {} "")]}
(fc/with-field :sales-summary/items
(list
(fc/cursor-map #(sales-summary-item-row* {:value %
:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state))) }))
;; TODO
(com/data-grid-new-row {:colspan 5
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
:row-offset 0
:index (count (fc/field-value))
:tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}} ;; TODO
"New Summary Item")))
(fc/cursor-map #(sales-summary-item-row* {:value %
:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state))) }))
(com/data-grid-new-row {:colspan 5
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
:row-offset 0
:index (count (fc/field-value))
:tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}}
"New Summary Item")))
(summary-total-row* request)
(unbalanced-row* request)) ])