feat(ssr): add sales summary csv download

Exports one row per sales summary item so each category is individually
auditable, rather than one row per daily summary. Uses the grid helper's
page->csv-entities hook, the same seam the ledger export uses to fan a
journal entry out into its line items.

Includes the GL account code alongside the name so rows tie back to the
chart of accounts, and formats amounts to cents to keep float noise
(36.900000000000006) out of the export. Accounts are resolved in a single
batched pull and clientized per summary so name overrides are respected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 23:51:46 -07:00
parent 9cd3d9c962
commit 80e45c026e
2 changed files with 78 additions and 2 deletions

View File

@@ -2,7 +2,7 @@
(:require
[auto-ap.datomic
:refer [apply-pagination apply-sort-3 conn merge-query pull-many
query2]]
pull-many-by-id query2]]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
@@ -128,6 +128,38 @@
(map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0)))
(def csv-account-read
[:db/id
:account/name
:account/code
{:account/client-overrides [:account-client-override/name
{:account-client-override/client [:db/id]}]}])
(defn- csv-amount
"Renders an item's amount when it falls on `side`, fixed to cents so the CSV
never carries floating point noise."
[side item]
(when (= side (:ledger-mapped/ledger-side item))
(format "%.2f" (double (:ledger-mapped/amount item 0.0)))))
(defn summaries->csv-rows
"Flattens each summary into one row per item, so every category is its own
auditable CSV line. Accounts are resolved in a single batch pull and
clientized against the summary's own client."
[summaries]
(let [account-ids (into #{}
(comp (mapcat :sales-summary/items)
(keep (comp :db/id :ledger-mapped/account)))
summaries)
id->account (if (seq account-ids)
(pull-many-by-id (dc/db conn) csv-account-read account-ids)
{})]
(for [ss summaries
item (sort-items (:sales-summary/items ss))]
(assoc (merge item ss)
::account (some-> item :ledger-mapped/account :db/id id->account
(d-accounts/clientize (-> ss :sales-summary/client :db/id)))))))
(defn truncate [s max-len]
(if (> (count s) max-len)
(str (subs s 0 (- max-len 3)) "...")
@@ -215,7 +247,15 @@
:title "Sales Summaries"
:entity-name "Daily Summary"
:route ::route/table
:headers [{:key "client"
:csv-route ::route/csv
:page->csv-entities (fn [[summaries]]
(summaries->csv-rows summaries))
:headers [{:key "id"
:name "Id"
:render-csv :db/id
:render-for #{:csv}}
{:key "client"
:name "Client"
:sort-key "client"
:hide? (fn [args]
@@ -231,6 +271,7 @@
:name "Debits"
:sort-key "debits"
:class "w-72 align-top"
:render-for #{:html}
:render (fn [ss]
(let [items (:sales-summary/items ss)
debit-items (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)) (sort-items items))
@@ -257,6 +298,7 @@
:name "Credits"
:sort-key "credits"
:class "w-72 align-top"
:render-for #{:html}
:render (fn [ss]
(let [items (:sales-summary/items ss)
credit-items (filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)) (sort-items items))
@@ -283,6 +325,7 @@
:name "Status"
:sort-key "balance"
:class "w-28 align-top"
:render-for #{:html}
:render (fn [ss]
(let [items (:sales-summary/items ss)
total-debits (total-debits items)
@@ -304,10 +347,41 @@
[:span.text-xs.uppercase.tracking-wider.text-red-600.font-medium.mt-0.5
(if (> total-debits total-credits) "Debit over" "Credit over")]])]))}
{:key "category"
:name "Category"
:render-csv :sales-summary-item/category
:render-for #{:csv}}
{:key "account-code"
:name "Account Code"
:render-csv #(-> % ::account :account/code)
:render-for #{:csv}}
{:key "account"
:name "Account"
:render-csv #(-> % ::account :account/name)
:render-for #{:csv}}
{:key "debit"
:name "Debit"
:render-csv (partial csv-amount :ledger-side/debit)
:render-for #{:csv}}
{:key "credit"
:name "Credit"
:render-csv (partial csv-amount :ledger-side/credit)
:render-for #{:csv}}
{:key "manual"
:name "Manual"
:render-csv #(boolean (:sales-summary-item/manual? %))
:render-for #{:csv}}
{:key "links"
:name "Links"
:show-starting "lg"
:class "w-8"
:render-for #{:html}
:render (fn [ss]
(let [ledger-entry (:journal-entry/original-entity ss)]
(when (seq ledger-entry)
@@ -754,6 +828,7 @@
(->>
{::route/page (helper/page-route grid-page)
::route/table (helper/table-route grid-page)
::route/csv (helper/csv-route grid-page)
::route/edit-wizard (-> mm/open-wizard-handler
(mm/wrap-wizard edit-wizard)
(mm/wrap-init-multi-form-state initial-edit-wizard-state)

View File

@@ -2,6 +2,7 @@
(def routes {"" {:get ::page
:put ::edit-wizard-submit}
"/table" ::table
"/csv" ::csv
["/" [#"\d+" :db/id]] {:get ::edit-wizard}
"/edit/navigate" ::edit-wizard-navigate
"/edit/sales-summary-item" ::new-summary-item