diff --git a/docs/plans/2026-04-24-001-feat-complete-sales-summary-calculations-plan.md b/docs/plans/2026-04-24-001-feat-complete-sales-summary-calculations-plan.md new file mode 100644 index 00000000..433c77bc --- /dev/null +++ b/docs/plans/2026-04-24-001-feat-complete-sales-summary-calculations-plan.md @@ -0,0 +1,219 @@ +--- +title: Complete Automatic Sales Summary Calculations and Ledger Posting +type: feat +status: completed +date: 2026-04-24 +--- + +# Complete Automatic Sales Summary Calculations and Ledger Posting + +## What's Incomplete +- **Automatic Totals**: Aggregate attributes (e.g., `:sales-summary/total-card-payments`) are not calculated/stored by the job. +- **Data Persistence**: Automatic recalculations risk overwriting manual user adjustments. +- **Automation Gap**: Ledger entries are currently imported from external Excel files rather than generated automatically from the summaries. +- **UI Polish**: "Clientization" and HTMX context (`client-id`) TODOs remain in the admin interface. + +--- + +## Overview +... + + +This plan completes the implementation of automatic sales summary calculations and ensures they are correctly posted to the ledger. Currently, the `sales-summaries-v2` job calculates detailed daily summaries, but it doesn't store aggregate totals, preserve manual adjustments, or trigger the creation of actual ledger entries. Additionally, the admin UI has several unresolved TODOs. + +--- + +## Problem Frame + +The system currently aggregates raw sales data into a `sales-summary` entity, but the final step—creating balanced journal entries for the general ledger—is a manual process involving external Excel calculations and subsequent imports. This creates a dependency on external tools and increases the risk of data entry errors. The goal is to automate this pipeline entirely within the product. + +--- + +## Requirements Trace + +- R1. Calculate and store aggregate totals (e.g., `:sales-summary/total-card-payments`) on the `sales-summary` entity. +- R2. Preserve user-made manual adjustments (`:sales-summary-item/manual? true`) during automatic recalculations. +- R3. Aggregate detailed `sales-summary-item`s into balanced `journal-entry` lines by account and location. +- R4. Automate the posting of these aggregated totals to the ledger. +- R5. Resolve UI TODOs in the Sales Summaries admin page, specifically regarding client-scoping ("clientize") and HTMX context (`client-id`). + +--- + +## Scope Boundaries + +- **In-Scope**: + - Enhancements to the `sales-summaries-v2` job. + - Implementation of the summary-to-ledger aggregation and posting logic. + - Cleanup of the Sales Summaries admin UI. +- **Out-of-Scope**: + - Changing the fundamental calculation logic for sales orders/refunds. + - Creating new ledger accounts (assume existing account mapping is sufficient). + - Changing the naming of refunds/returns (user requested to keep as is). + +--- + +## Context & Research + +### Relevant Code and Patterns + +- **Jobs**: `src/clj/auto_ap/jobs/sales_summaries.clj` contains the main calculation logic. +- **UI**: `src/clj/auto_ap/ssr/admin/sales_summaries.clj` implements the admin interface. +- **Ledger Posting**: `src/clj/auto_ap/ledger.clj` and `iol_ion/src/iol_ion/tx/upsert_ledger.clj` handle journal entry creation. +- **Reconciliation Pattern**: `reconcile-ledger` in `src/clj/auto_ap/ledger.clj` shows how to find missing ledger entries and trigger their creation. + +### Institutional Learnings + +- No existing documented patterns for sales summary posting were found in `docs/solutions/`. This implementation will establish the pattern. + +--- + +## Key Technical Decisions + +- **Detailed Summary $\to$ Aggregated Ledger**: The `sales-summary` will maintain granular detail (line items, specific fee types), but the ledger posting will aggregate these items by account and location to create balanced `journal-entry` lines. +- **Automatic Posting**: Posting to the ledger will be integrated into the reconciliation process, similar to how invoices and transactions are handled in `reconcile-ledger`. +- **Location Handling**: Since `sales-summary-item`s don't have a location, a default location for the client will be used for ledger posting. + +--- + +## Open Questions + +### Resolved During Planning + +- **Architectural Decision**: Use a detailed summary that aggregates into the ledger. +- **Renaming**: Keep "Refunds/Returns" as is. + +### Deferred to Implementation + +- **Default Location Logic**: Exactly how the "default location" for a client is retrieved or defined. + +--- + +## Implementation Units + +- U1. **Enhance `sales-summaries-v2` Job** + +**Goal:** Ensure the job stores aggregate totals and preserves manual adjustments. + +**Requirements:** R1, R2 + +**Dependencies:** None + +**Files:** +- Modify: `src/clj/auto_ap/jobs/sales_summaries.clj` + +**Approach:** +- Update `sales-summaries-v2` to calculate totals for attributes like `:sales-summary/total-card-payments`, `:sales-summary/total-cash-payments`, etc., based on the generated items. +- Implement a merge strategy: when updating a summary, keep any items where `:sales-summary-item/manual?` is true, and only replace the automatically calculated items. + +**Test scenarios:** +- Happy path: Running the job for a client with sales and refunds results in a `sales-summary` with correct `:sales-summary/total-*` attributes. +- Edge case: Running the job on a summary that already has a manual item ensures the manual item is not overwritten. + +**Verification:** +- Datomic query shows `sales-summary` entities have populated total attributes and preserved manual items. + +--- + +- U2. **Implement Summary-to-Ledger Aggregation** + +**Goal:** Create a function to transform detailed summary items into balanced ledger lines. + +**Requirements:** R3 + +**Dependencies:** U1 + +**Files:** +- Create: `src/clj/auto_ap/ledger/sales_summaries.clj` (or add to `src/clj/auto_ap/ledger.clj`) +- Test: `test/clj/auto_ap/ledger_test.clj` + +**Approach:** +- Create a function `aggregate-summary-items` that: + 1. Groups `sales-summary-item`s by `:ledger-mapped/account`. + 2. Sums the `:ledger-mapped/amount` based on `:ledger-mapped/ledger-side` (debit vs credit). + 3. Assigns a location (default client location). + 4. Returns a list of `journal-entry-line` maps. + +**Test scenarios:** +- Happy path: A set of items with mixed accounts and sides aggregates into the correct number of ledger lines with summed amounts. +- Edge case: Items with `nil` accounts are handled gracefully (e.g., mapped to an "Unknown" account or logged as error). + +**Verification:** +- Unit tests verify that a list of `sales-summary-item`s is correctly transformed into `journal-entry-line`s. + +--- + +- U3. **Implement Automatic Ledger Posting for Summaries** + +**Goal:** Ensure sales summaries trigger the creation of ledger entries. + +**Requirements:** R4 + +**Dependencies:** U2 + +**Files:** +- Modify: `src/clj/auto_ap/ledger.clj` +- Modify: `src/clj/auto_ap/jobs/sales_summaries.clj` + +**Approach:** +- Implement a `:upsert-sales-summary-ledger` transaction or function that takes a `sales-summary` and uses the aggregation logic from U2 to post to the ledger. +- Integrate this into the `reconcile-ledger` function in `src/clj/auto_ap/ledger.clj` to find summaries missing ledger entries and post them. + +**Test scenarios:** +- Integration: Running `reconcile-ledger` identifies a `sales-summary` missing a `journal-entry` and creates a balanced `journal-entry` for it. +- Happy path: The created `journal-entry` has the correct total amount and matches the summary totals. + +**Verification:** +- A `sales-summary` entity is linked to a `journal-entry` via `:journal-entry/original-entity`. + +--- + +- U4. **Resolve UI TODOs in Sales Summaries Admin** + +**Goal:** Fix client-scoping and HTMX context in the admin UI. + +**Requirements:** R5 + +**Dependencies:** None + +**Files:** +- Modify: `src/clj/auto_ap/ssr/admin/sales_summaries.clj` + +**Approach:** +- Resolve "clientize" TODOs: Ensure the data pulled for the table and edit wizard is correctly scoped and transformed using client-specific context. +- Fix HTMX `client-id` passing: Update the `new-summary-item` trigger to correctly pass the `client-id` via `hx-vals` from the form state. +- Clean up any remaining schema TODOs in the SSR file. + +**Test scenarios:** +- Integration: Adding a new summary item in the UI correctly sends the `client-id` and the item is created for the correct client. +- Happy path: The summary table displays correctly and "missing account" warnings appear only for items without a mapped account. + +**Verification:** +- Manual verification in the browser: New items are added correctly, and the UI is free of "missing account" red pills for mapped items. + +--- + +## System-Wide Impact + +- **Interaction graph**: The `sales-summaries-v2` job now feeds into the ledger system via `reconcile-ledger`. +- **Error propagation**: Failures in the aggregation logic will prevent the `journal-entry` from being created, which will be surfaced by `reconcile-ledger` as a missing entry. +- **State lifecycle risks**: Ensuring that `manual?` items are not overwritten during automatic recalculation is critical to avoid losing user adjustments. +- **Integration coverage**: Integration tests must cover the full flow: `sales-orders` $\to$ `sales-summary` $\to$ `journal-entry`. + +--- + +## Risks & Dependencies + +| Risk | Mitigation | +|------|------------| +| Overwriting manual adjustments | Implement explicit merge logic based on the `:sales-summary-item/manual?` flag. | +| Unbalanced ledger entries | Use a strict aggregation function that ensures debits = credits for every posted summary. | +| Missing location data | Implement a robust fallback to a default client location. | + +--- + +## Sources & References + +- Related code: `src/clj/auto_ap/jobs/sales_summaries.clj` +- Related code: `src/clj/auto_ap/ssr/admin/sales_summaries.clj` +- Related code: `src/clj/auto_ap/ledger.clj` +- Related code: `iol_ion/src/iol_ion/tx/upsert_ledger.clj` diff --git a/iol_ion/src/iol_ion/tx.clj b/iol_ion/src/iol_ion/tx.clj index cf81c480..79f0bf7c 100644 --- a/iol_ion/src/iol_ion/tx.clj +++ b/iol_ion/src/iol_ion/tx.clj @@ -38,18 +38,19 @@ (defn regenerate-literals [] (require 'com.github.ivarref.gen-fn) - (spit - "resources/functions.edn" - (let [datomic-fn @(resolve 'com.github.ivarref.gen-fn/datomic-fn)] - [(datomic-fn :pay #'iol-ion.tx.pay/pay) - (datomic-fn :plus #'iol-ion.tx.plus/plus) - (datomic-fn :propose-invoice #'iol-ion.tx.propose-invoice/propose-invoice) - (datomic-fn :reset-rels #'iol-ion.tx.reset-rels/reset-rels) - (datomic-fn :reset-scalars #'iol-ion.tx.reset-scalars/reset-scalars) - (datomic-fn :upsert-entity #'iol-ion.tx.upsert-entity/upsert-entity) - (datomic-fn :upsert-invoice #'iol-ion.tx.upsert-invoice/upsert-invoice) - (datomic-fn :upsert-ledger #'iol-ion.tx.upsert-ledger/upsert-ledger) - (datomic-fn :upsert-transaction #'iol-ion.tx.upsert-transaction/upsert-transaction)]))) + (spit + "resources/functions.edn" + (let [datomic-fn @(resolve 'com.github.ivarref.gen-fn/datomic-fn)] + [(datomic-fn :pay #'iol-ion.tx.pay/pay) + (datomic-fn :plus #'iol-ion.tx.plus/plus) + (datomic-fn :propose-invoice #'iol-ion.tx.propose-invoice/propose-invoice) + (datomic-fn :reset-rels #'iol-ion.tx.reset-rels/reset-rels) + (datomic-fn :reset-scalars #'iol-ion.tx.reset-scalars/reset-scalars) + (datomic-fn :upsert-entity #'iol-ion.tx.upsert-entity/upsert-entity) + (datomic-fn :upsert-invoice #'iol-ion.tx.upsert-invoice/upsert-invoice) + (datomic-fn :upsert-ledger #'iol-ion.tx.upsert-ledger/upsert-ledger) + (datomic-fn :upsert-transaction #'iol-ion.tx.upsert-transaction/upsert-transaction) + (datomic-fn :upsert-sales-summary-ledger #'iol-ion.tx.upsert-sales-summary-ledger/upsert-sales-summary-ledger)]))) (comment (regenerate-literals) diff --git a/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj b/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj new file mode 100644 index 00000000..8fb30e2c --- /dev/null +++ b/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj @@ -0,0 +1,78 @@ +(ns iol-ion.tx.upsert-sales-summary-ledger + (:import [java.util UUID Date]) + (:require [datomic.api :as dc])) + +(defn -random-tempid [] + (dc/tempid :db.part/user)) + +(def extant-read '[:db/id :journal-entry/date :journal-entry/client {:journal-entry/line-items [:journal-entry-line/account :journal-entry-line/location :db/id :journal-entry-line/client+account+location+date]}]) + +(defn current-date [db] + (let [last-tx (dc/t->tx (dc/basis-t db)) + [[date]] (seq (dc/q '[:find ?ti :in $ ?tx + :where [?tx :db/txInstant ?ti]] + db + last-tx))] + date)) + +(defn calc-client+account+location+date [je jel] + [(or + (:db/id (:journal-entry/client je)) + (:journal-entry/client je)) + (or (:db/id (:journal-entry-line/account jel)) + (:journal-entry-line/account jel)) + (-> jel :journal-entry-line/location) + (-> je :journal-entry/date)]) + +(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) + 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 upsert-sales-summary-ledger [db summary] + (assert (:db/id summary) "Must provide summary id") + (let [upserted-entity [[:upsert-entity {:db/id (:db/id summary)}]] + with-summary (dc/with db upserted-entity) + summary-id (:db/id summary) + journal-entry (summary->journal-entry (:db-after with-summary) summary-id) + client-id (-> (dc/pull (:db-after with-summary) + [{:sales-summary/client [:db/id]}] + summary-id) + :sales-summary/client + :db/id)] + (into upserted-entity + (if journal-entry + [[:upsert-ledger journal-entry] + {:db/id client-id + :client/ledger-last-change (current-date db)}] + [[:db/retractEntity [:journal-entry/original-entity summary-id]] + {:db/id client-id + :client/ledger-last-change (current-date db)}])))) diff --git a/resources/schema.edn b/resources/schema.edn index bae45322..1291663b 100644 --- a/resources/schema.edn +++ b/resources/schema.edn @@ -1949,12 +1949,69 @@ :db/unique :db.unique/identity :db/index true} {:db/ident :sales-summary/client+dirty - :db/valueType :db.type/tuple - :db/tupleAttrs [:sales-summary/client :sales-summary/dirty] - :db/cardinality :db.cardinality/one - :db/index true} + :db/valueType :db.type/tuple + :db/tupleAttrs [:sales-summary/client :sales-summary/dirty] + :db/cardinality :db.cardinality/one + :db/index true} - {:db/ident :sales-summary-item/category + {:db/ident :sales-summary/total-card-payments + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-cash-payments + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-food-app-payments + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-gift-card-payments + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-card-refunds + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-cash-refunds + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-food-app-refunds + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-fees + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-discounts + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-tax + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-tip + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-returns + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-unknown-payments + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-net + :db/noHistory true + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + + {:db/ident :sales-summary-item/category :db/valueType :db.type/string :db/cardinality :db.cardinality/one} {:db/ident :sales-summary-item/sort-order diff --git a/src/clj/auto_ap/datomic.clj b/src/clj/auto_ap/datomic.clj index e70bc375..80b9271e 100644 --- a/src/clj/auto_ap/datomic.clj +++ b/src/clj/auto_ap/datomic.clj @@ -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] diff --git a/src/clj/auto_ap/jobs/sales_summaries.clj b/src/clj/auto_ap/jobs/sales_summaries.clj index 9ee556be..1a81a222 100644 --- a/src/clj/auto_ap/jobs/sales_summaries.clj +++ b/src/clj/auto_ap/jobs/sales_summaries.clj @@ -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 [] diff --git a/src/clj/auto_ap/ledger.clj b/src/clj/auto_ap/ledger.clj index 7c714426..19a71609 100644 --- a/src/clj/auto_ap/ledger.clj +++ b/src/clj/auto_ap/ledger.clj @@ -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] diff --git a/src/clj/auto_ap/ssr/admin/sales_summaries.clj b/src/clj/auto_ap/ssr/admin/sales_summaries.clj index 55c67617..80e0e975 100644 --- a/src/clj/auto_ap/ssr/admin/sales_summaries.clj +++ b/src/clj/auto_ap/ssr/admin/sales_summaries.clj @@ -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)) ])