From baf8cfff97de41dadcd092087c062dfd40dff6c2 Mon Sep 17 00:00:00 2001 From: Bryce Date: Sat, 25 Apr 2026 08:43:16 -0700 Subject: [PATCH 1/5] feat: complete automatic sales summary calculations and ledger posting --- ...omplete-sales-summary-calculations-plan.md | 219 ++++++++++++++++++ iol_ion/src/iol_ion/tx.clj | 25 +- .../tx/upsert_sales_summary_ledger.clj | 78 +++++++ resources/schema.edn | 67 +++++- src/clj/auto_ap/datomic.clj | 9 +- src/clj/auto_ap/jobs/sales_summaries.clj | 138 +++++++---- src/clj/auto_ap/ledger.clj | 49 ++-- src/clj/auto_ap/ssr/admin/sales_summaries.clj | 49 ++-- 8 files changed, 529 insertions(+), 105 deletions(-) create mode 100644 docs/plans/2026-04-24-001-feat-complete-sales-summary-calculations-plan.md create mode 100644 iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj 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)) ]) -- 2.49.1 From 0e76506c22b7aa7b49cf9c00f93b1856722f88da Mon Sep 17 00:00:00 2001 From: Bryce Date: Fri, 1 May 2026 14:05:23 -0700 Subject: [PATCH 2/5] consolidate sales summary ledger entry creation into upsert-sales-summary tx Move journal entry calculation and creation from the reconcile-ledger background job into the upsert-sales-summary tx function. Now any save of a sales summary (job recalculation, admin edit wizard, or manual touch) automatically creates the journal entry if balanced with all accounts mapped, or retracts it if conditions no longer hold. Eliminates the need for a separate upsert-sales-summary-ledger call and the reconcile ledger pass for sales summaries. --- iol_ion/src/iol_ion/tx.clj | 2 +- .../tx/upsert_sales_summary_ledger.clj | 78 +++++++------------ src/clj/auto_ap/jobs/sales_summaries.clj | 36 +-------- src/clj/auto_ap/ledger.clj | 2 +- src/clj/auto_ap/ssr/admin/sales_summaries.clj | 2 +- 5 files changed, 34 insertions(+), 86 deletions(-) diff --git a/iol_ion/src/iol_ion/tx.clj b/iol_ion/src/iol_ion/tx.clj index 79f0bf7c..69f7fbb3 100644 --- a/iol_ion/src/iol_ion/tx.clj +++ b/iol_ion/src/iol_ion/tx.clj @@ -50,7 +50,7 @@ (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)]))) + (datomic-fn :upsert-sales-summary #'iol-ion.tx.upsert-sales-summary-ledger/upsert-sales-summary)]))) (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 index 8fb30e2c..26db4a9f 100644 --- a/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj +++ b/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj @@ -1,28 +1,7 @@ (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)]) + (:require [datomic.api :as dc] + [iol-ion.tx.upsert-entity :as upsert-entity] + [iol-ion.tx.upsert-ledger :as upsert-ledger])) (defn summary->journal-entry [db summary-id] (let [summary (dc/pull db '[:sales-summary/client @@ -42,37 +21,40 @@ (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} + line-items (mapv (fn [{:keys [account] :as m}] + (cond-> {:db/id (str (java.util.UUID/randomUUID)) + :journal-entry-line/account account + :journal-entry-line/location "A"} (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) + aggregated) + total-debits (reduce + 0.0 (map #(get % :ledger-side/debit 0.0) aggregated)) + total-credits (reduce + 0.0 (map #(get % :ledger-side/credit 0.0) aggregated))] + (when (and (seq line-items) + (= (Math/round (* 1000 total-debits)) + (Math/round (* 1000 total-credits)))) {: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/amount total-debits :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) +(defn upsert-sales-summary [db summary] + (let [upserted-summary [[:upsert-entity summary]] + db-after (-> (dc/with db upserted-summary) :db-after) + summary-id (:db/id summary) + client-id (-> (dc/pull db-after [{:sales-summary/client [:db/id]}] summary-id) :sales-summary/client - :db/id)] - (into upserted-entity + :db/id) + journal-entry (summary->journal-entry db-after summary-id)] + (into upserted-summary (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)}])))) + [[:upsert-ledger journal-entry]] + (let [existing-je-id (ffirst (dc/q '[:find ?je . :in $ ?ss + :where [?je :journal-entry/original-entity ?ss]] + db-after summary-id))] + (concat + (when existing-je-id [[:db/retractEntity existing-je-id]]) + (when client-id [{:db/id client-id + :client/ledger-last-change (upsert-ledger/current-date db)}]))))))) diff --git a/src/clj/auto_ap/jobs/sales_summaries.clj b/src/clj/auto_ap/jobs/sales_summaries.clj index 1a81a222..9f1b02b7 100644 --- a/src/clj/auto_ap/jobs/sales_summaries.clj +++ b/src/clj/auto_ap/jobs/sales_summaries.clj @@ -276,40 +276,6 @@ :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] @@ -376,7 +342,7 @@ (do (alog/info ::upserting-summaries :category-count (count (:sales-summary/items result))) - @(dc/transact conn [[:upsert-entity result]])) + @(dc/transact conn [[:upsert-sales-summary result]])) @(dc/transact conn [{:db/id id :sales-summary/dirty false}])))))) diff --git a/src/clj/auto_ap/ledger.clj b/src/clj/auto_ap/ledger.clj index 19a71609..2f6a1f4a 100644 --- a/src/clj/auto_ap/ledger.clj +++ b/src/clj/auto_ap/ledger.clj @@ -61,7 +61,7 @@ (dc/db conn) start-date) (map first) (mapv (fn [ss] - [:upsert-sales-summary-ledger {:db/id ss}]))) + [:upsert-sales-summary {:db/id ss}]))) repairs (vec (concat txes-missing-ledger-entries invoices-missing-ledger-entries sales-summaries-missing-ledger-entries))] (when (seq repairs) diff --git a/src/clj/auto_ap/ssr/admin/sales_summaries.clj b/src/clj/auto_ap/ssr/admin/sales_summaries.clj index 80e0e975..144ab20e 100644 --- a/src/clj/auto_ap/ssr/admin/sales_summaries.clj +++ b/src/clj/auto_ap/ssr/admin/sales_summaries.clj @@ -483,7 +483,7 @@ edit-schema) (submit [this {:keys [multi-form-state request-method identity] :as request}] (let [result (:snapshot multi-form-state ) - transaction [:upsert-entity {:db/id (:db/id result) + transaction [:upsert-sales-summary {:db/id (:db/id result) :sales-summary/items (map (fn [i] (if (:sales-summary-item/manual? i) -- 2.49.1 From 95f12a6072430234545470a3484f2e5803880737 Mon Sep 17 00:00:00 2001 From: Bryce Date: Fri, 1 May 2026 15:40:42 -0700 Subject: [PATCH 3/5] refactor: remove dead calc-aggregate-totals and unused schema attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 13 sales-summary/total-* attributes were computed and stored but never read — the only consumer (get-debits) was commented out. Active display code computes totals on-the-fly from the items list instead. --- resources/schema.edn | 57 ------------------- src/clj/auto_ap/jobs/sales_summaries.clj | 49 +++------------- src/clj/auto_ap/ssr/admin/sales_summaries.clj | 20 ------- 3 files changed, 7 insertions(+), 119 deletions(-) diff --git a/resources/schema.edn b/resources/schema.edn index 1291663b..4570036d 100644 --- a/resources/schema.edn +++ b/resources/schema.edn @@ -1954,63 +1954,6 @@ :db/cardinality :db.cardinality/one :db/index true} - {: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} diff --git a/src/clj/auto_ap/jobs/sales_summaries.clj b/src/clj/auto_ap/jobs/sales_summaries.clj index 9f1b02b7..0fcb23ed 100644 --- a/src/clj/auto_ap/jobs/sales_summaries.clj +++ b/src/clj/auto_ap/jobs/sales_summaries.clj @@ -276,34 +276,6 @@ :ledger-mapped/amount amount :ledger-mapped/ledger-side :ledger-side/debit})) -(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 $ @@ -329,15 +301,13 @@ (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)] + all-items (concat calculated-items manual-items) + 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 all-items}] (if (seq (:sales-summary/items result)) (do (alog/info ::upserting-summaries @@ -360,11 +330,6 @@ (comment (auto-ap.datomic/transact-schema conn) - @(dc/transact conn [{:db/ident :sales-summary/total-unknown-processor-payments - :db/noHistory true, - :db/valueType :db.type/double - :db/cardinality :db.cardinality/one}]) - (apply mark-dirty [:client/code "NGCL"] (last-n-days 30)) (apply mark-dirty [:client/code "NGDG"] (last-n-days 30)) diff --git a/src/clj/auto_ap/ssr/admin/sales_summaries.clj b/src/clj/auto_ap/ssr/admin/sales_summaries.clj index 144ab20e..186780c8 100644 --- a/src/clj/auto_ap/ssr/admin/sales_summaries.clj +++ b/src/clj/auto_ap/ssr/admin/sales_summaries.clj @@ -129,26 +129,6 @@ [(->> (hydrate-results ids-to-retrieve db request)) matching-count])) -#_(defn get-debits [ss] - {:card-payments (+ (:sales-summary/total-card-payments ss 0.0) - (:sales-summary/total-card-fees ss 0.0) - (- (:sales-summary/total-card-refunds ss 0.0))) - :food-app-payments (+ (:sales-summary/total-food-app-payments ss 0.0) - (:sales-summary/total-food-app-fees ss 0.0) - (- (:sales-summary/total-food-app-refunds ss 0.0))) - :gift-card-payments (+ (:sales-summary/total-gift-card-payments ss 0.0) - (:sales-summary/total-gift-card-fees ss 0.0) - (- (:sales-summary/total-gift-card-refunds ss 0.0))) - #_#_:refunds (+ (:sales-summary/total-food-app-refunds ss 0.0) - (:sales-summary/total-card-refunds ss 0.0) - (:sales-summary/total-cash-refunds ss 0.0)) - - :fees (- (:sales-summary/total-card-fees ss 0.0)) - :cash-payments (+ (:sales-summary/total-cash-payments ss 0.0) - (- (:sales-summary/total-cash-refunds ss 0.0))) - :total-unknown-processor-payments (:sales-summary/total-unknown-processor-payments ss 0.0) - :discounts (+ (:sales-summary/discount ss 0.0)) - :returns (+ (:sales-summary/total-returns ss 0.0))}) (defn sort-items [ss] (sort-by (juxt :ledger-mapped/ledger-side :sales-summary-item/sort-order :sales-summary-item/category) ss)) -- 2.49.1 From 5a39a0c7627484ec06e0b9e219f198e494085656 Mon Sep 17 00:00:00 2001 From: Bryce Date: Fri, 15 May 2026 23:20:48 -0700 Subject: [PATCH 4/5] fixes for sales summaries being automatic. --- iol_ion/src/iol_ion/tx.clj | 29 ++++++----- .../tx/upsert_sales_summary_ledger.clj | 48 +++++++++++-------- resources/functions.edn | 2 +- src/clj/auto_ap/jobs/sales_summaries.clj | 36 ++++++++++---- 4 files changed, 71 insertions(+), 44 deletions(-) diff --git a/iol_ion/src/iol_ion/tx.clj b/iol_ion/src/iol_ion/tx.clj index 69f7fbb3..1d90c284 100644 --- a/iol_ion/src/iol_ion/tx.clj +++ b/iol_ion/src/iol_ion/tx.clj @@ -38,21 +38,20 @@ (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) - (datomic-fn :upsert-sales-summary #'iol-ion.tx.upsert-sales-summary-ledger/upsert-sales-summary)]))) + (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 #'iol-ion.tx.upsert-sales-summary-ledger/upsert-sales-summary)]))) (comment (regenerate-literals) - - (auto-ap.datomic/install-functions)) \ No newline at end of file + (auto-ap.datomic/install-functions)) 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 index 26db4a9f..3db00f80 100644 --- a/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj +++ b/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj @@ -1,7 +1,5 @@ (ns iol-ion.tx.upsert-sales-summary-ledger - (:require [datomic.api :as dc] - [iol-ion.tx.upsert-entity :as upsert-entity] - [iol-ion.tx.upsert-ledger :as upsert-ledger])) + (:require [datomic.api :as dc])) (defn summary->journal-entry [db summary-id] (let [summary (dc/pull db '[:sales-summary/client @@ -9,7 +7,7 @@ {:sales-summary/items [:sales-summary-item/category :ledger-mapped/account :ledger-mapped/amount - :ledger-mapped/ledger-side]}] + {:ledger-mapped/ledger-side [:db/ident]}]}] summary-id) items (:sales-summary/items summary) aggregated (->> items @@ -18,18 +16,22 @@ (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))) + (update m (:db/ident (:ledger-mapped/ledger-side item)) (fnil + 0.0) (:ledger-mapped/amount item 0.0))) {:account account} acc-items)))) + _ (clojure.pprint/pprint aggregated) line-items (mapv (fn [{:keys [account] :as m}] - (cond-> {:db/id (str (java.util.UUID/randomUUID)) - :journal-entry-line/account account - :journal-entry-line/location "A"} - (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)))) + (cond-> {:db/id (str (java.util.UUID/randomUUID)) + :journal-entry-line/account account + :journal-entry-line/location "A"} + (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-debits (reduce + 0.0 (map #(get % :ledger-side/debit 0.0) aggregated)) - total-credits (reduce + 0.0 (map #(get % :ledger-side/credit 0.0) aggregated))] + total-credits (reduce + 0.0 (map #(get % :ledger-side/credit 0.0) aggregated)) +_ (clojure.pprint/pprint [total-debits total-credits]) + ] (when (and (seq line-items) (= (Math/round (* 1000 total-debits)) (Math/round (* 1000 total-credits)))) @@ -40,6 +42,14 @@ :journal-entry/amount total-debits :journal-entry/line-items line-items}))) +(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 upsert-sales-summary [db summary] (let [upserted-summary [[:upsert-entity summary]] db-after (-> (dc/with db upserted-summary) :db-after) @@ -48,13 +58,13 @@ :sales-summary/client :db/id) journal-entry (summary->journal-entry db-after summary-id)] - (into upserted-summary + upserted-summary + #_(into upserted-summary (if journal-entry [[:upsert-ledger journal-entry]] - (let [existing-je-id (ffirst (dc/q '[:find ?je . :in $ ?ss - :where [?je :journal-entry/original-entity ?ss]] - db-after summary-id))] - (concat - (when existing-je-id [[:db/retractEntity existing-je-id]]) - (when client-id [{:db/id client-id - :client/ledger-last-change (upsert-ledger/current-date db)}]))))))) + (concat + [[:db/retractEntity [:journal-entry/original-entity (:db/id summary)]]] + + + (when client-id [{:db/id client-id + :client/ledger-last-change (current-date db)}])))))) diff --git a/resources/functions.edn b/resources/functions.edn index efe3cac1..776cf345 100644 --- a/resources/functions.edn +++ b/resources/functions.edn @@ -1 +1 @@ -[#:db{:ident :pay, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) amount (genfn-coerce-arg amount)] (do (let [current-outstanding-balance (-> (dc/pull db [:invoice/outstanding-balance] e) :invoice/outstanding-balance) new-outstanding-balance (- current-outstanding-balance amount)] [[:upsert-invoice {:invoice/status (if (< -1.0E-4 new-outstanding-balance 1.0E-4) :invoice-status/paid :invoice-status/unpaid), :db/id e, :invoice/outstanding-balance new-outstanding-balance}]]))))))"}} #:db{:ident :plus, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) amount (genfn-coerce-arg amount)] (do [[:db/add e a (-> (dc/pull db [a] e) a (+ amount))]])))))"}} #:db{:ident :propose-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [existing? (boolean (seq (dc/q (quote [:find ?i :in $ ?invoice-number ?client ?vendor :where [?i :invoice/invoice-number ?invoice-number] [?i :invoice/client ?client] [?i :invoice/vendor ?vendor] (not [?i :invoice/status :invoice-status/voided])]) db (:invoice/invoice-number invoice) (:invoice/client invoice) (:invoice/vendor invoice)))) [locked-until] (first (dc/q (quote [:find ?locked-until :in $ ?c :where [?c :client/locked-until ?locked-until]]) db (:invoice/client invoice))) is-locked? (cond (not locked-until) false (not (:invoice/date invoice)) true (< (compare (:invoice/date invoice) locked-until) 0) true :else false)] (if (or existing? is-locked?) [] [[:upsert-invoice invoice]])))))))"}} #:db{:ident :reset-rels, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (assert (every? :db/id vs) (format \"In order to reset attribute %s, every value must have :db/id\" a)) (let [ids (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) new-id-set (set (map :db/id vs)) retract-ids (filter (complement new-id-set) ids) {is-component? :db/isComponent} (dc/pull db [:db/isComponent] a) new-rels (filter (complement (set ids)) (map :db/id vs))] (-> [] (into (map (fn [i] (if is-component? [:db/retractEntity i] [:db/retract e a i])) retract-ids)) (into (map (fn [i] [:db/add e a i]) new-rels)) (into (map (fn [i] [:upsert-entity i]) vs)))))))))"}} #:db{:ident :reset-scalars, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (let [extant (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) retracts (filter (complement (set vs)) extant) new (filter (complement (set extant)) vs)] (-> [] (into (map (fn [i] [:db/retract e a i]) retracts)) (into (map (fn [i] [:db/add e a i]) new)))))))))"}} #:db{:ident :upsert-entity, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db entity], :code "(let [] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (-by [f fv xs] (do (reduce (fn* [p1__143570# p2__143571#] (assoc p1__143570# (f p2__143571#) (fv p2__143571#))) {} xs))) (-pull-many [db read ids] (do (->> (dc/q (quote [:find (pull ?e r) :in $ [?e ...] r]) db ids read) (map first))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [entity (genfn-coerce-arg entity)] (do (when-not (or (:db/id entity) (:db/ident entity)) (datomic.api/cancel #:cognitect.anomalies{:message (str \"Cannot upsert without :db/id or :db/ident, \" entity), :category :cognitect.anomalies/incorrect})) (let [e (or (:db/id entity) (:db/ident entity)) is-new? (string? e) extant-entity (when-not is-new? (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity)))) ident->value-type (-by :db/ident (comp :db/ident :db/valueType) (-pull-many db [#:db{:valueType [:db/ident]} :db/ident] (keys entity))) ident->cardinality (-by :db/ident (comp :db/ident :db/cardinality) (-pull-many db [#:db{:cardinality [:db/ident]} :db/ident] (keys entity))) ops (->> entity (reduce (fn [ops [a v]] (cond (= :db/id a) ops (= :db/ident a) ops (or (= v (a extant-entity)) (= v (:db/ident (a extant-entity) :nope)) (= v (:db/id (a extant-entity)) :nope)) ops (and (nil? v) (not (nil? (a extant-entity)))) (if (= :db.cardinality/many (ident->cardinality a)) (into ops (map (fn [v] [:db/retract e a (cond-> v (:db/id v) :db/id)]) (a extant-entity))) (conj ops [:db/retract e a (cond-> (a extant-entity) (:db/id (a extant-entity)) :db/id)])) (nil? v) ops (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) (conj ops [:db/add e a v]) (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) (into ops [[:reset-rels e a v]]) (= :db.cardinality/many (ident->cardinality a)) (into ops [[:reset-scalars e a v]]) (and (sequential? v) (not= :db.type/ref (ident->value-type a))) (into ops [[:reset-scalars e a v]]) (and (map? v) (= :db.type/ref (ident->value-type a))) (let [id (or (:db/id v) (-random-tempid))] (-> ops (conj [:db/add e a id]) (into [[:upsert-entity (assoc v :db/id id)]]))) :else (conj ops [:db/add e a v]))) []))] ops))))))"}} #:db{:ident :upsert-invoice, :fn #db/fn{:lang :clojure, :imports [[java.util Date]], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (invoice->journal-entry [db invoice-id raw-invoice-id] (do (let [entity (dc/pull db (quote [:invoice/total :invoice/exclude-from-ledger :invoice/outstanding-balance :invoice/date #:invoice{:client [:db/id :client/code], :status [:db/ident], :import-status [:db/ident], :vendor [:db/id :vendor/name], :payment [:db/id #:payment{:status [:db/ident]}], :expense-accounts [:invoice-expense-account/account :invoice-expense-account/amount :invoice-expense-account/location]}]) invoice-id) credit-invoice? (< (:invoice/total entity 0.0) 0.0)] (when-not (or (not (:invoice/total entity)) (= true (:invoice/exclude-from-ledger entity)) (= :import-status/pending (:db/ident (:invoice/import-status entity))) (= :invoice-status/voided (:db/ident (:invoice/status entity))) (< -0.001 (:invoice/total entity) 0.001)) (-remove-nils #:journal-entry{:date (:invoice/date entity), :original-entity raw-invoice-id, :client (:db/id (:invoice/client entity)), :line-items (into [(cond-> {:journal-entry-line/account :account/accounts-payable, :db/id (str raw-invoice-id \"-\" 0), :journal-entry-line/location \"A\"} credit-invoice? (assoc :journal-entry-line/debit (Math/abs (:invoice/total entity))) (not credit-invoice?) (assoc :journal-entry-line/credit (Math/abs (:invoice/total entity))))] (map-indexed (fn [i ea] (cond-> {:journal-entry-line/account (:db/id (:invoice-expense-account/account ea)), :db/id (str raw-invoice-id \"-\" (inc i)), :journal-entry-line/location (or (:invoice-expense-account/location ea) \"HQ\")} credit-invoice? (assoc :journal-entry-line/credit (Math/abs (:invoice-expense-account/amount ea))) (not credit-invoice?) (assoc :journal-entry-line/debit (Math/abs (:invoice-expense-account/amount ea))))) (:invoice/expense-accounts entity))), :source \"invoice\", :cleared (and (< (:invoice/outstanding-balance entity) 0.01) (every? (fn* [p1__143573#] (= :payment-status/cleared (:payment/status p1__143573#))) (:invoice/payments entity))), :amount (Math/abs (:invoice/total entity)), :vendor (:db/id (:invoice/vendor entity))}))))) (current-date [db] (do (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q (quote [:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]]) db last-tx))] date)))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [upserted-entity [[:upsert-entity invoice]] with-invoice (dc/with db upserted-entity) invoice-id (or (-> with-invoice :tempids (get (:db/id invoice))) (:db/id invoice)) journal-entry (invoice->journal-entry (:db-after with-invoice) invoice-id (:db/id invoice)) client-id (-> (dc/pull (:db-after with-invoice) [#:invoice{:client [:db/id]}] invoice-id) :invoice/client :db/id)] (into upserted-entity (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id invoice)]] {:client/ledger-last-change (current-date db), :db/id client-id}]))))))))"}} #:db{:ident :upsert-ledger, :fn #db/fn{:lang :clojure, :imports [[java.util UUID Date]], :requires [[datomic.api :as dc]], :params [db ledger-entry], :code "(let [extant-read (quote [: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]}])] (letfn [(-random-tempid [] (do (dc/tempid :db.part/user))) (get-line-items-after [db journal-entry] (do (for [jel (:journal-entry/line-items journal-entry) next-jel (->> (dc/index-pull db {:selector [:db/id :journal-entry-line/client+account+location+date], :index :avet, :start [:journal-entry-line/client+account+location+date (:journal-entry-line/client+account+location+date jel) (:db/id jel)]}) (take-while (fn line-must-match-client-account-location [result] (and (= (take 3 (:journal-entry-line/client+account+location+date result)) (take 3 (:journal-entry-line/client+account+location+date jel))) (not= (:db/id jel) (:db/id result))))) (take 2)) :when next-jel] (:db/id next-jel)))) (current-date [db] (do (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q (quote [:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]]) db last-tx))] date))) (calc-client+account+location+date [je jel] (do [(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)]))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [ledger-entry (genfn-coerce-arg ledger-entry)] (do (assert (:journal-entry/date ledger-entry) (format \"Must at least provide date when updating ledger: %s\" (pr-str ledger-entry))) (assert (:journal-entry/client ledger-entry) \"Must at least provide client when updating ledger\") (let [extant-entry (or (when-let [original-entity (:journal-entry/original-entity ledger-entry)] (dc/pull db extant-read [:journal-entry/original-entity original-entity])) (when-let [external-id (:journal-entry/external-id ledger-entry)] (dc/pull db extant-read [:journal-entry/external-id external-id])))] (cond-> [[:upsert-entity (into (-> ledger-entry (assoc :db/id (or (:db/id ledger-entry) (:db/id extant-entry) (-random-tempid))) (update :journal-entry/line-items (fn [lis] (mapv (fn* [p1__143577#] (-> p1__143577# (assoc :journal-entry-line/date (:journal-entry/date ledger-entry)) (assoc :journal-entry-line/client (:journal-entry/client ledger-entry)))) lis)))))] {:client/ledger-last-change (current-date db), :db/id (:journal-entry/client ledger-entry)}])))))))"}} #:db{:ident :upsert-transaction, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db transaction], :code "(let [my-transaction {:transaction/bank-account 17592232681223, :transaction/date (read-string \"#inst \\\"2024-02-24T08:00:00.000-00:00\\\"\"), :transaction/matched-rule 17592233159891, :transaction/client 17592232577980, :transaction/status \"POSTED\", :transaction/plaid-merchant {:plaid-merchant/name \"Rotten Robbie\", :db/id \"b2776792-9e2b-46e8-a9c8-bf80abea359e\"}, :db/id \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc\", :transaction/id \"11a4a13e713d63f476009027e9a53e217e13d0192a37df8ab96c0eed4bdbe996\", :transaction/description-original \"Rotten Robbie #03\", :transaction/approval-status #:db{:id 17592231963877, :ident :transaction-approval-status/approved}, :transaction/amount -84.43, :transaction/accounts [{:transaction-account/account 17592231963549, :db/id \"c402c7b3-c11b-484b-b670-bd48f79a3e5f\", :transaction-account/location \"CB\", :transaction-account/amount 84.43}], :transaction/raw-id \"gQypbv5946F08op74wZmidDg8qD8Q1fM6gEBP\", :transaction/vendor 17592232627053} my-journal #:journal-entry{:vendor 17592232627053, :amount 84.43, :date (read-string \"#inst \\\"2024-02-24T08:00:00.000-00:00\\\"\"), :alternate-description \"Rotten Robbie #03\", :original-entity \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc\", :client 17592232577980, :source \"transaction\", :line-items [{:journal-entry-line/credit 84.43, :journal-entry-line/account 17592232681223, :db/id \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-0\", :journal-entry-line/location \"A\"} {:journal-entry-line/account 17592231963549, :db/id \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-1\", :journal-entry-line/debit 84.43, :journal-entry-line/location \"CB\"} {:journal-entry-line/account 17592231963549, :db/id \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-2\", :journal-entry-line/debit 84.43, :journal-entry-line/location \"CB\"}], :cleared true}] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (transaction->journal-entry [db transaction-id raw-transaction-id] (do (let [entity (dc/pull db [:transaction/client :transaction/date :transaction/description-original :db/id :transaction/vendor :transaction/amount :transaction/cleared-against #:transaction{:bank-account [:db/id #:bank-account{:type [:db/ident]}], :approval-status [:db/ident], :accounts [:transaction-account/account :transaction-account/location :transaction-account/amount]}] transaction-id) decreasing? (< (or (:transaction/amount entity) 0.0) 0.0) credit-from-bank? decreasing? debit-from-bank? (not decreasing?)] (when (and (not (= :transaction-approval-status/excluded (:db/ident (:transaction/approval-status entity)))) (not (= :transaction-approval-status/suppressed (:db/ident (:transaction/approval-status entity)))) (:transaction/amount entity) (not (< -0.001 (:transaction/amount entity) 0.001))) (-remove-nils #:journal-entry{:vendor (:db/id (:transaction/vendor entity)), :amount (Math/abs (:transaction/amount entity)), :date (:transaction/date entity), :alternate-description (:transaction/description-original entity), :original-entity raw-transaction-id, :client (:db/id (:transaction/client entity)), :cleared-against (:transaction/cleared-against entity), :source \"transaction\", :line-items (into [(-remove-nils {:journal-entry-line/credit (when credit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/account (:db/id (:transaction/bank-account entity)), :db/id (str raw-transaction-id \"-\" 0), :journal-entry-line/debit (when debit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/location \"A\"})] (map-indexed (fn [i a] (-remove-nils {:journal-entry-line/credit (when debit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/account (:db/id (:transaction-account/account a)), :db/id (str raw-transaction-id \"-\" (inc i)), :journal-entry-line/debit (when credit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/location (:transaction-account/location a)})) (if (seq (:transaction/accounts entity)) (:transaction/accounts entity) [#:transaction-account{:amount (:transaction/amount entity)}]))), :cleared true})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [transaction (genfn-coerce-arg transaction)] (do (let [upserted-entity [[:upsert-entity (dissoc transaction :transaction/payment :import-batch/_entry)]] with-transaction (dc/with db upserted-entity) transaction-id (or (-> with-transaction :tempids (get (:db/id transaction))) (:db/id transaction)) journal-entry (transaction->journal-entry (:db-after with-transaction) transaction-id (:db/id transaction))] (into [[:upsert-entity transaction]] (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id transaction)]]]))))))))"}}] \ No newline at end of file +[#:db{:ident :pay, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) amount (genfn-coerce-arg amount)] (do (let [current-outstanding-balance (-> (dc/pull db [:invoice/outstanding-balance] e) :invoice/outstanding-balance) new-outstanding-balance (- current-outstanding-balance amount)] [[:upsert-invoice {:invoice/status (if (< -1.0E-4 new-outstanding-balance 1.0E-4) :invoice-status/paid :invoice-status/unpaid), :db/id e, :invoice/outstanding-balance new-outstanding-balance}]]))))))"}} #:db{:ident :plus, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) amount (genfn-coerce-arg amount)] (do [[:db/add e a (-> (dc/pull db [a] e) a (+ amount))]])))))"}} #:db{:ident :propose-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [existing? (boolean (seq (dc/q (quote [:find ?i :in $ ?invoice-number ?client ?vendor :where [?i :invoice/invoice-number ?invoice-number] [?i :invoice/client ?client] [?i :invoice/vendor ?vendor] (not [?i :invoice/status :invoice-status/voided])]) db (:invoice/invoice-number invoice) (:invoice/client invoice) (:invoice/vendor invoice)))) [locked-until] (first (dc/q (quote [:find ?locked-until :in $ ?c :where [?c :client/locked-until ?locked-until]]) db (:invoice/client invoice))) is-locked? (cond (not locked-until) false (not (:invoice/date invoice)) true (< (compare (:invoice/date invoice) locked-until) 0) true :else false)] (if (or existing? is-locked?) [] [[:upsert-invoice invoice]])))))))"}} #:db{:ident :reset-rels, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (assert (every? :db/id vs) (format \"In order to reset attribute %s, every value must have :db/id\" a)) (let [ids (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) new-id-set (set (map :db/id vs)) retract-ids (filter (complement new-id-set) ids) {is-component? :db/isComponent} (dc/pull db [:db/isComponent] a) new-rels (filter (complement (set ids)) (map :db/id vs))] (-> [] (into (map (fn [i] (if is-component? [:db/retractEntity i] [:db/retract e a i])) retract-ids)) (into (map (fn [i] [:db/add e a i]) new-rels)) (into (map (fn [i] [:upsert-entity i]) vs)))))))))"}} #:db{:ident :reset-scalars, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (let [extant (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) retracts (filter (complement (set vs)) extant) new (filter (complement (set extant)) vs)] (-> [] (into (map (fn [i] [:db/retract e a i]) retracts)) (into (map (fn [i] [:db/add e a i]) new)))))))))"}} #:db{:ident :upsert-entity, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db entity], :code "(let [] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (-by [f fv xs] (do (reduce (fn* [p1__112282# p2__112283#] (assoc p1__112282# (f p2__112283#) (fv p2__112283#))) {} xs))) (-pull-many [db read ids] (do (->> (dc/q (quote [:find (pull ?e r) :in $ [?e ...] r]) db ids read) (map first))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [entity (genfn-coerce-arg entity)] (do (when-not (or (:db/id entity) (:db/ident entity)) (datomic.api/cancel #:cognitect.anomalies{:message (str \"Cannot upsert without :db/id or :db/ident, \" entity), :category :cognitect.anomalies/incorrect})) (let [e (or (:db/id entity) (:db/ident entity)) is-new? (string? e) extant-entity (when-not is-new? (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity)))) ident->value-type (-by :db/ident (comp :db/ident :db/valueType) (-pull-many db [#:db{:valueType [:db/ident]} :db/ident] (keys entity))) ident->cardinality (-by :db/ident (comp :db/ident :db/cardinality) (-pull-many db [#:db{:cardinality [:db/ident]} :db/ident] (keys entity))) ops (->> entity (reduce (fn [ops [a v]] (cond (= :db/id a) ops (= :db/ident a) ops (or (= v (a extant-entity)) (= v (:db/ident (a extant-entity) :nope)) (= v (:db/id (a extant-entity)) :nope)) ops (and (nil? v) (not (nil? (a extant-entity)))) (if (= :db.cardinality/many (ident->cardinality a)) (into ops (map (fn [v] [:db/retract e a (cond-> v (:db/id v) :db/id)]) (a extant-entity))) (conj ops [:db/retract e a (cond-> (a extant-entity) (:db/id (a extant-entity)) :db/id)])) (nil? v) ops (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) (conj ops [:db/add e a v]) (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) (into ops [[:reset-rels e a v]]) (= :db.cardinality/many (ident->cardinality a)) (into ops [[:reset-scalars e a v]]) (and (sequential? v) (not= :db.type/ref (ident->value-type a))) (into ops [[:reset-scalars e a v]]) (and (map? v) (= :db.type/ref (ident->value-type a))) (let [id (or (:db/id v) (-random-tempid))] (-> ops (conj [:db/add e a id]) (into [[:upsert-entity (assoc v :db/id id)]]))) :else (conj ops [:db/add e a v]))) []))] ops))))))"}} #:db{:ident :upsert-invoice, :fn #db/fn{:lang :clojure, :imports [[java.util Date]], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (invoice->journal-entry [db invoice-id raw-invoice-id] (do (let [entity (dc/pull db (quote [:invoice/total :invoice/exclude-from-ledger :invoice/outstanding-balance :invoice/date #:invoice{:client [:db/id :client/code], :status [:db/ident], :import-status [:db/ident], :vendor [:db/id :vendor/name], :payment [:db/id #:payment{:status [:db/ident]}], :expense-accounts [:invoice-expense-account/account :invoice-expense-account/amount :invoice-expense-account/location]}]) invoice-id) credit-invoice? (< (:invoice/total entity 0.0) 0.0)] (when-not (or (not (:invoice/total entity)) (= true (:invoice/exclude-from-ledger entity)) (= :import-status/pending (:db/ident (:invoice/import-status entity))) (= :invoice-status/voided (:db/ident (:invoice/status entity))) (< -0.001 (:invoice/total entity) 0.001)) (-remove-nils #:journal-entry{:date (:invoice/date entity), :original-entity raw-invoice-id, :client (:db/id (:invoice/client entity)), :line-items (into [(cond-> {:journal-entry-line/account :account/accounts-payable, :db/id (str raw-invoice-id \"-\" 0), :journal-entry-line/location \"A\"} credit-invoice? (assoc :journal-entry-line/debit (Math/abs (:invoice/total entity))) (not credit-invoice?) (assoc :journal-entry-line/credit (Math/abs (:invoice/total entity))))] (map-indexed (fn [i ea] (cond-> {:journal-entry-line/account (:db/id (:invoice-expense-account/account ea)), :db/id (str raw-invoice-id \"-\" (inc i)), :journal-entry-line/location (or (:invoice-expense-account/location ea) \"HQ\")} credit-invoice? (assoc :journal-entry-line/credit (Math/abs (:invoice-expense-account/amount ea))) (not credit-invoice?) (assoc :journal-entry-line/debit (Math/abs (:invoice-expense-account/amount ea))))) (:invoice/expense-accounts entity))), :source \"invoice\", :cleared (and (< (:invoice/outstanding-balance entity) 0.01) (every? (fn* [p1__112285#] (= :payment-status/cleared (:payment/status p1__112285#))) (:invoice/payments entity))), :amount (Math/abs (:invoice/total entity)), :vendor (:db/id (:invoice/vendor entity))}))))) (current-date [db] (do (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q (quote [:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]]) db last-tx))] date)))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [upserted-entity [[:upsert-entity invoice]] with-invoice (dc/with db upserted-entity) invoice-id (or (-> with-invoice :tempids (get (:db/id invoice))) (:db/id invoice)) journal-entry (invoice->journal-entry (:db-after with-invoice) invoice-id (:db/id invoice)) client-id (-> (dc/pull (:db-after with-invoice) [#:invoice{:client [:db/id]}] invoice-id) :invoice/client :db/id)] (into upserted-entity (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id invoice)]] {:client/ledger-last-change (current-date db), :db/id client-id}]))))))))"}} #:db{:ident :upsert-ledger, :fn #db/fn{:lang :clojure, :imports [[java.util UUID Date]], :requires [[datomic.api :as dc]], :params [db ledger-entry], :code "(let [extant-read (quote [: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]}])] (letfn [(-random-tempid [] (do (dc/tempid :db.part/user))) (get-line-items-after [db journal-entry] (do (for [jel (:journal-entry/line-items journal-entry) next-jel (->> (dc/index-pull db {:selector [:db/id :journal-entry-line/client+account+location+date], :index :avet, :start [:journal-entry-line/client+account+location+date (:journal-entry-line/client+account+location+date jel) (:db/id jel)]}) (take-while (fn line-must-match-client-account-location [result] (and (= (take 3 (:journal-entry-line/client+account+location+date result)) (take 3 (:journal-entry-line/client+account+location+date jel))) (not= (:db/id jel) (:db/id result))))) (take 2)) :when next-jel] (:db/id next-jel)))) (current-date [db] (do (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q (quote [:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]]) db last-tx))] date))) (calc-client+account+location+date [je jel] (do [(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)]))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [ledger-entry (genfn-coerce-arg ledger-entry)] (do (assert (:journal-entry/date ledger-entry) (format \"Must at least provide date when updating ledger: %s\" (pr-str ledger-entry))) (assert (:journal-entry/client ledger-entry) \"Must at least provide client when updating ledger\") (let [extant-entry (or (when-let [original-entity (:journal-entry/original-entity ledger-entry)] (dc/pull db extant-read [:journal-entry/original-entity original-entity])) (when-let [external-id (:journal-entry/external-id ledger-entry)] (dc/pull db extant-read [:journal-entry/external-id external-id])))] (cond-> [[:upsert-entity (into (-> ledger-entry (assoc :db/id (or (:db/id ledger-entry) (:db/id extant-entry) (-random-tempid))) (update :journal-entry/line-items (fn [lis] (mapv (fn* [p1__112289#] (-> p1__112289# (assoc :journal-entry-line/date (:journal-entry/date ledger-entry)) (assoc :journal-entry-line/client (:journal-entry/client ledger-entry)))) lis)))))] {:client/ledger-last-change (current-date db), :db/id (:journal-entry/client ledger-entry)}])))))))"}} #:db{:ident :upsert-transaction, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db transaction], :code "(let [my-transaction {:transaction/bank-account 17592232681223, :transaction/date (read-string \"#inst \\\"2024-02-24T08:00:00.000-00:00\\\"\"), :transaction/matched-rule 17592233159891, :transaction/client 17592232577980, :transaction/status \"POSTED\", :transaction/plaid-merchant {:plaid-merchant/name \"Rotten Robbie\", :db/id \"b2776792-9e2b-46e8-a9c8-bf80abea359e\"}, :db/id \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc\", :transaction/id \"11a4a13e713d63f476009027e9a53e217e13d0192a37df8ab96c0eed4bdbe996\", :transaction/description-original \"Rotten Robbie #03\", :transaction/approval-status #:db{:id 17592231963877, :ident :transaction-approval-status/approved}, :transaction/amount -84.43, :transaction/accounts [{:transaction-account/account 17592231963549, :db/id \"c402c7b3-c11b-484b-b670-bd48f79a3e5f\", :transaction-account/location \"CB\", :transaction-account/amount 84.43}], :transaction/raw-id \"gQypbv5946F08op74wZmidDg8qD8Q1fM6gEBP\", :transaction/vendor 17592232627053} my-journal #:journal-entry{:vendor 17592232627053, :amount 84.43, :date (read-string \"#inst \\\"2024-02-24T08:00:00.000-00:00\\\"\"), :alternate-description \"Rotten Robbie #03\", :original-entity \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc\", :client 17592232577980, :source \"transaction\", :line-items [{:journal-entry-line/credit 84.43, :journal-entry-line/account 17592232681223, :db/id \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-0\", :journal-entry-line/location \"A\"} {:journal-entry-line/account 17592231963549, :db/id \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-1\", :journal-entry-line/debit 84.43, :journal-entry-line/location \"CB\"} {:journal-entry-line/account 17592231963549, :db/id \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-2\", :journal-entry-line/debit 84.43, :journal-entry-line/location \"CB\"}], :cleared true}] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (transaction->journal-entry [db transaction-id raw-transaction-id] (do (let [entity (dc/pull db [:transaction/client :transaction/date :transaction/description-original :db/id :transaction/vendor :transaction/amount :transaction/cleared-against #:transaction{:bank-account [:db/id #:bank-account{:type [:db/ident]}], :approval-status [:db/ident], :accounts [:transaction-account/account :transaction-account/location :transaction-account/amount]}] transaction-id) decreasing? (< (or (:transaction/amount entity) 0.0) 0.0) credit-from-bank? decreasing? debit-from-bank? (not decreasing?)] (when (and (not (= :transaction-approval-status/excluded (:db/ident (:transaction/approval-status entity)))) (not (= :transaction-approval-status/suppressed (:db/ident (:transaction/approval-status entity)))) (:transaction/amount entity) (not (< -0.001 (:transaction/amount entity) 0.001))) (-remove-nils #:journal-entry{:vendor (:db/id (:transaction/vendor entity)), :amount (Math/abs (:transaction/amount entity)), :date (:transaction/date entity), :alternate-description (:transaction/description-original entity), :original-entity raw-transaction-id, :client (:db/id (:transaction/client entity)), :cleared-against (:transaction/cleared-against entity), :source \"transaction\", :line-items (into [(-remove-nils {:journal-entry-line/credit (when credit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/account (:db/id (:transaction/bank-account entity)), :db/id (str raw-transaction-id \"-\" 0), :journal-entry-line/debit (when debit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/location \"A\"})] (map-indexed (fn [i a] (-remove-nils {:journal-entry-line/credit (when debit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/account (:db/id (:transaction-account/account a)), :db/id (str raw-transaction-id \"-\" (inc i)), :journal-entry-line/debit (when credit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/location (:transaction-account/location a)})) (if (seq (:transaction/accounts entity)) (:transaction/accounts entity) [#:transaction-account{:amount (:transaction/amount entity)}]))), :cleared true})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [transaction (genfn-coerce-arg transaction)] (do (let [upserted-entity [[:upsert-entity (dissoc transaction :transaction/payment :import-batch/_entry)]] with-transaction (dc/with db upserted-entity) transaction-id (or (-> with-transaction :tempids (get (:db/id transaction))) (:db/id transaction)) journal-entry (transaction->journal-entry (:db-after with-transaction) transaction-id (:db/id transaction))] (into [[:upsert-entity transaction]] (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id transaction)]]]))))))))"}} #:db{:ident :upsert-sales-summary, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db summary], :code "(let [] (letfn [(summary->journal-entry [db summary-id] (do (let [summary (dc/pull db (quote [:sales-summary/client :sales-summary/date #:sales-summary{:items [:sales-summary-item/category :ledger-mapped/account :ledger-mapped/amount #:ledger-mapped{:ledger-side [:db/ident]}]}]) 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 (:db/ident (:ledger-mapped/ledger-side item)) (fnil + 0.0) (:ledger-mapped/amount item 0.0))) {:account account} acc-items)))) _ (clojure.pprint/pprint aggregated) line-items (mapv (fn [{:as m, :keys [account]}] (cond-> {:journal-entry-line/account account, :db/id (str (java.util.UUID/randomUUID)), :journal-entry-line/location \"A\"} (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-debits (reduce + 0.0 (map (fn* [p1__112291#] (get p1__112291# :ledger-side/debit 0.0)) aggregated)) total-credits (reduce + 0.0 (map (fn* [p1__112293#] (get p1__112293# :ledger-side/credit 0.0)) aggregated)) _ (clojure.pprint/pprint [total-debits total-credits])] (when (and (seq line-items) (= (Math/round (* 1000 total-debits)) (Math/round (* 1000 total-credits)))) #:journal-entry{:date (:sales-summary/date summary), :original-entity summary-id, :client (:db/id (:sales-summary/client summary)), :line-items line-items, :source \"sales-summary\", :amount total-debits})))) (current-date [db] (do (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q (quote [:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]]) db last-tx))] date)))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [summary (genfn-coerce-arg summary)] (do (let [upserted-summary [[:upsert-entity summary]] db-after (-> (dc/with db upserted-summary) :db-after) summary-id (:db/id summary) client-id (-> (dc/pull db-after [#:sales-summary{:client [:db/id]}] summary-id) :sales-summary/client :db/id) journal-entry (summary->journal-entry db-after summary-id)] upserted-summary))))))"}}] \ No newline at end of file diff --git a/src/clj/auto_ap/jobs/sales_summaries.clj b/src/clj/auto_ap/jobs/sales_summaries.clj index 0fcb23ed..e759078b 100644 --- a/src/clj/auto_ap/jobs/sales_summaries.clj +++ b/src/clj/auto_ap/jobs/sales_summaries.clj @@ -334,20 +334,35 @@ (apply mark-dirty [:client/code "NGDG"] (last-n-days 30)) - (apply mark-dirty [:client/code "NGPG"] (last-n-days 30)) + (dirty-sales-summaries [:client/code "NGWH"]) - (mark-all-dirty 50) + + (apply mark-dirty [:client/code "NGWH"] (last-n-days 5)) + + (iol-ion.tx.upsert-sales-summary-ledger/summary->journal-entry (dc/db conn) 17592314245819) + + + (iol-ion.tx.upsert-sales-summary-ledger/upsert-sales-summary (dc/db conn) {:db/id 17592314241429}) + + + + (mark-all-dirty 5) (delete-all) + (sales-summaries-v2) + + 1 + + (dc/q '[:find (pull ?sos [* {:sales-summary/sales-items [*]}]) :in $ :where [?sos :sales-summary/client [:client/code "NGHW"]] [?sos :sales-summary/date ?d] [(= ?d #inst "2024-04-10T00:00:00-07:00")]] (dc/db conn)) - + (dc/q '[:find ?n ?p2 (sum ?total) :with ?c :in $ [?clients ?start-date ?end-date] @@ -360,18 +375,21 @@ (dc/db conn) [[(auto-ap.datomic/pull-attr (dc/db conn) :db/id [:client/code "NGHW"])] #inst "2024-04-11T00:00:00-07:00" #inst "2024-04-11T00:00:00-07:00"]) - (dc/q '[:find ?n + (dc/q '[:find ?n :in $ [?clients ?start-date ?end-date] :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] [?e :sales-order/line-items ?li] - [?li :order-line-item/item-name ?n] ] + [?li :order-line-item/item-name ?n]] (dc/db conn) [[(auto-ap.datomic/pull-attr (dc/db conn) :db/id [:client/code "NGCL"])] #inst "2024-04-11T00:00:00-07:00" #inst "2024-04-24T00:00:00-07:00"]) - -@(dc/transact conn [{:db/id :sales-summary/total-tax :db/ident :sales-summary/total-tax-legacy} - {:db/id :sales-summary/total-tip :db/ident :sales-summary/total-tip-legacy}]) -(auto-ap.datomic/transact-schema conn) + @(dc/transact conn [{:db/id :sales-summary/total-tax :db/ident :sales-summary/total-tax-legacy} + {:db/id :sales-summary/total-tip :db/ident :sales-summary/total-tip-legacy}]) + + (auto-ap.datomic/transact-schema conn) + + + ) -- 2.49.1 From b02aec35464190d9916e980c4334d9e106f4ee50 Mon Sep 17 00:00:00 2001 From: Bryce Date: Sat, 16 May 2026 00:13:42 -0700 Subject: [PATCH 5/5] Move sales summaries from admin to pos menu - Move SSR handler from auto-ap.ssr.admin.sales-summaries to auto-ap.ssr.pos.sales-summaries - Move route namespace from auto-ap.routes.admin.sales-summaries to auto-ap.routes.pos.sales-summaries - Update nav to use main-aside-nav with POS breadcrumbs - Use pos.common date-range-field* filter component - Remove wrap-admin/wrap-client-redirect-unauthenticated from middleware - Add Summaries to Sales sidebar menu --- src/clj/auto_ap/ssr/components/aside.clj | 19 ++++--- src/clj/auto_ap/ssr/core.clj | 24 ++++----- .../ssr/{admin => pos}/sales_summaries.clj | 49 +++++-------------- .../{admin => pos}/sales_summaries.cljc | 6 +-- src/cljc/auto_ap/ssr_routes.cljc | 2 +- 5 files changed, 41 insertions(+), 59 deletions(-) rename src/clj/auto_ap/ssr/{admin => pos}/sales_summaries.clj (94%) rename src/cljc/auto_ap/routes/{admin => pos}/sales_summaries.cljc (68%) diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index 6e30d822..8bdfe276 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -6,7 +6,8 @@ [auto-ap.routes.admin.excel-invoices :as ei-routes] [auto-ap.routes.admin.import-batch :as ib-routes] [auto-ap.routes.admin.transaction-rules :as transaction-rules] - [auto-ap.routes.admin.vendors :as v-routes] + [auto-ap.routes.admin.vendors :as v-routes] + [auto-ap.routes.pos.sales-summaries :as ss-routes] [auto-ap.routes.invoice :as invoice-route] [auto-ap.routes.ledger :as ledger-routes] [auto-ap.routes.outgoing-invoice :as oi-routes] @@ -90,8 +91,8 @@ (#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new ::invoice-route/import-page :invoice-glimpse :invoice-glimpse-textract-invoice} (:matched-route request)) "invoices" - (#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts} (:matched-route request)) - "sales" + (#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts ::ss-routes/page} (:matched-route request)) + "sales" (#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page} (:matched-route request)) "payments" (#{::ledger-routes/all-page ::ledger-routes/external-page ::ledger-routes/external-import-page ::ledger-routes/balance-sheet ::ledger-routes/cash-flows ::ledger-routes/profit-and-loss} (:matched-route request)) @@ -207,12 +208,18 @@ :hx-boost "true"} "Refunds") + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-cash-drawer-shifts) + "?date-range=week") + :active? (= :pos-cash-drawer-shifts (:matched-route request)) + :hx-boost "true"} + "Cash drawer shifts") (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes - :pos-cash-drawer-shifts) + ::ss-routes/page) "?date-range=week") - :active? (= :pos-cash-drawer-shifts (:matched-route request)) + :active? (= ::ss-routes/page (:matched-route request)) :hx-boost "true"} - "Cash drawer shifts")))) + "Summaries")))) (menu-button- {"@click.prevent" "if (selected == 'payments') {selected = null } else { selected = 'payments'} " :icon svg/payments} diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 12cf76c1..611ff0a4 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -12,7 +12,7 @@ [auto-ap.ssr.admin.excel-invoice :as admin-excel-invoices] [auto-ap.ssr.admin.history :as history] [auto-ap.ssr.admin.import-batch :as import-batch] - [auto-ap.ssr.admin.sales-summaries :as admin-sales-summaries] + [auto-ap.ssr.pos.sales-summaries :as pos-sales-summaries] [auto-ap.ssr.admin.transaction-rules :as admin-rules] [auto-ap.ssr.admin.vendors :as admin-vendors] [auto-ap.ssr.auth :as auth] @@ -85,17 +85,17 @@ (into company-1099/key->handler) (into invoice/key->handler) (into import-batch/key->handler) - (into pos-sales/key->handler) - (into pos-expected-deposits/key->handler) - (into pos-tenders/key->handler) - (into pos-cash-drawer-shifts/key->handler) - (into pos-refunds/key->handler) - (into users/key->handler) - (into admin-accounts/key->handler) - (into admin-excel-invoices/key->handler) - (into admin/key->handler) - (into admin-jobs/key->handler) - (into admin-sales-summaries/key->handler) + (into pos-sales/key->handler) + (into pos-expected-deposits/key->handler) + (into pos-tenders/key->handler) + (into pos-cash-drawer-shifts/key->handler) + (into pos-refunds/key->handler) + (into pos-sales-summaries/key->handler) + (into users/key->handler) + (into admin-accounts/key->handler) + (into admin-excel-invoices/key->handler) + (into admin/key->handler) + (into admin-jobs/key->handler) (into admin-vendors/key->handler) (into admin-clients/key->handler) (into admin-rules/key->handler) diff --git a/src/clj/auto_ap/ssr/admin/sales_summaries.clj b/src/clj/auto_ap/ssr/pos/sales_summaries.clj similarity index 94% rename from src/clj/auto_ap/ssr/admin/sales_summaries.clj rename to src/clj/auto_ap/ssr/pos/sales_summaries.clj index 186780c8..fed3d987 100644 --- a/src/clj/auto_ap/ssr/admin/sales_summaries.clj +++ b/src/clj/auto_ap/ssr/pos/sales_summaries.clj @@ -1,4 +1,4 @@ -(ns auto-ap.ssr.admin.sales-summaries +(ns auto-ap.ssr.pos.sales-summaries (:require [auto-ap.datomic :refer [apply-pagination apply-sort-3 conn merge-query pull-many @@ -6,9 +6,7 @@ [auto-ap.datomic.accounts :as d-accounts] [auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.query-params :refer [wrap-copy-qp-pqp]] - [auto-ap.routes.admin.sales-summaries :as route] - [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] + [auto-ap.routes.pos.sales-summaries :as route] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]] [auto-ap.ssr.components :as com] @@ -16,6 +14,8 @@ [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]] [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.pos.common + :refer [date-range-field*]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers clj-date-schema @@ -48,24 +48,8 @@ "hx-target" "#entity-table" "hx-indicator" "#entity-table"} - #_[:fieldset.space-y-6 - (date-range-field {:value {:start (:start-date (:query-params request)) - :end (:end-date (:query-params request))} - :id "date-range"}) - (com/field {:label "Source"} - (com/select {:name "source" - :class "hot-filter w-full" - :value (:source (:query-params request)) - :placeholder "" - :options (ref->select-options "import-source" :allow-nil? true)})) - - #_(com/field {:label "Code"} - (com/text-input {:name "code" - :id "code" - :class "hot-filter" - :value (:code (:query-params request)) - :placeholder "11101" - :size :small}))]]) + [:fieldset.space-y-6 + (date-range-field* request)]]) (def default-read '[:db/id * @@ -134,6 +118,7 @@ + (defn total-debits [items] (->> items (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %))) @@ -149,7 +134,7 @@ (def grid-page (helper/build {:id "entity-table" :id-fn :db/id - :nav com/admin-aside-nav + :nav com/main-aside-nav :fetch-page fetch-page :page-specific-nav filters :query-schema query-schema @@ -159,11 +144,11 @@ :db/id (:db/id entity))} svg/pencil)]) :oob-render - (fn [_request] - []) + (fn [request] + [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :admin)} - "Admin"] + :company)} + "POS"] [:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} @@ -219,10 +204,6 @@ :primary :red)} "Total: " (format "$%,.2f" total-credits))]]))}]})) -;; 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)) @@ -526,8 +507,4 @@ (wrap-apply-sort grid-page) (wrap-merge-prior-hx) (wrap-schema-enforce :query-schema query-schema) - (wrap-schema-enforce :hx-schema query-schema) - (wrap-admin) - (wrap-client-redirect-unauthenticated))))) - - + (wrap-schema-enforce :hx-schema query-schema))))) diff --git a/src/cljc/auto_ap/routes/admin/sales_summaries.cljc b/src/cljc/auto_ap/routes/pos/sales_summaries.cljc similarity index 68% rename from src/cljc/auto_ap/routes/admin/sales_summaries.cljc rename to src/cljc/auto_ap/routes/pos/sales_summaries.cljc index 9e1e8889..be26c7ff 100644 --- a/src/cljc/auto_ap/routes/admin/sales_summaries.cljc +++ b/src/cljc/auto_ap/routes/pos/sales_summaries.cljc @@ -1,9 +1,7 @@ -(ns auto-ap.routes.admin.sales-summaries) +(ns auto-ap.routes.pos.sales-summaries) (def routes {"" {:get ::page :put ::edit-wizard-submit} "/table" ::table - ["/" [#"\d+" :db/id]] {:get ::edit-wizard } - "/edit/navigate" ::edit-wizard-navigate - "/edit/sales-summary-item" ::new-summary-item}) \ No newline at end of file + "/edit/sales-summary-item" ::new-summary-item}) diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index 247734f6..0f2ec820 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -12,7 +12,7 @@ [auto-ap.routes.transactions :as t-routes] [auto-ap.routes.admin.clients :as ac-routes] - [auto-ap.routes.admin.sales-summaries :as ss-routes] + [auto-ap.routes.pos.sales-summaries :as ss-routes] [auto-ap.routes.admin.transaction-rules :as tr-routes])) (def routes {"impersonate" :impersonate -- 2.49.1