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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user