9 Commits

Author SHA1 Message Date
de1c154706 Polish sales summary grid and edit dialog
Aligns debit/credit amounts to a right column with tabular-nums;
replaces the in-cell delta and balanced text with chip-style status
indicators; shortens the edit dialog and clarifies its totals/unbalanced
footer rows; gives manual line items a subtle accent so they're
distinguishable from auto-generated rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:13:32 -07:00
31179278e4 Adds a more actionable view 2026-05-17 08:16:11 -07:00
Bryce
455cec7828 Merge branch 'master' of gitea.story-basking.ts.net:notid/integreat 2026-05-16 07:14:01 -07:00
aeb7891efa Merge branch 'master' of codecommit://integreat 2026-05-16 00:25:37 -07:00
Bryce
1b2e2e4da7 Merge branch 'master' of gitea.story-basking.ts.net:notid/integreat 2026-05-16 00:17:01 -07:00
cc31d8849b Feat/Complete Sales Summaries (#5)
## Summary

Completes the automatic sales summary pipeline end-to-end: the `sales-summaries-v2` job now calculates aggregate totals, preserves manual adjustments, and automatically posts balanced journal entries to the ledger.

## What Changed

**New Datomic transaction function** (`upsert-sales-summary-ledger`)
- Transforms detailed `sales-summary-item`s into aggregated `journal-entry` lines grouped by account and ledger side
- Handles the full upsert: posts a new journal entry for summaries with mapped accounts, or retracts the orphaned entry if items no longer qualify

**Enhanced `sales-summaries-v2` job**
- Calculates and stores 13 aggregate total attributes (card/cash/food-app/gift-card payments, refunds, fees, discounts, tax, tip, returns, unknown, net)
- Preserves manual items (`manual? true`) during recalculation — only auto-calculated items are replaced

**Ledger reconciliation**
- `reconcile-ledger` now queries for sales summaries missing journal entries and repairs them via `:upsert-sales-summary-ledger`, alongside existing invoice and transaction repairs

**Schema**
- Added 13 `total-*` attributes on `sales-summary` (all `db.type/double`, no history)
- Registered the new transaction function in `tx.clj` and `datomic.clj`

**Admin UI cleanup**
- Resolved "clientize" and HTMX `client-id` TODOs in the sales summaries admin page
- `new-summary-item` now correctly passes `client-id` via `hx-vals`
- Removed stale TODO comments and placeholder code

## Files Changed (8)

| File | Purpose |
|------|---------|
| `iol_ion/.../upsert_sales_summary_ledger.clj` | New Datomic tx function |
| `iol_ion/.../tx.clj` | Register new tx function |
| `resources/schema.edn` | 13 new `total-*` attributes |
| `src/.../datomic.clj` | Load new tx namespace |
| `src/.../jobs/sales_summaries.clj` | Aggregate totals + manual item preservation |
| `src/.../ledger.clj` | Sales summary repair in `reconcile-ledger` |
| `src/.../ssr/admin/sales_summaries.clj` | UI TODO cleanup |
| `docs/plans/...plan.md` | Implementation plan document |

Co-authored-by: Bryce <bryce@integreatconsult.com>
Reviewed-on: #5
Co-authored-by: Bryce <bryce@brycecovertoperations.com>
Co-committed-by: Bryce <bryce@brycecovertoperations.com>
2026-05-16 00:16:44 -07:00
Bryce
bd82f555c2 Merge branch 'master' of gitea.story-basking.ts.net:notid/integreat 2026-05-15 19:48:34 -07:00
Bryce
ec5e4e2e1d Merge branch 'master' of gitea.story-basking.ts.net:notid/integreat 2026-05-03 09:37:53 -07:00
Bryce
04bc7cae78 total column 2026-04-09 14:32:39 -07:00
7 changed files with 328 additions and 230 deletions

File diff suppressed because one or more lines are too long

View File

@@ -177,8 +177,9 @@
(conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
(conj (conj
(table->pdf report (table->pdf report
(cond-> (into [30 ] (repeat client-count 13)) (cond-> (into [30 ] (repeat client-count 13))
(:include-comparison args) (into (repeat (* 2 client-count) 13)))))) (:include-comparison args) (into (repeat (* 2 client-count) 13))
(and (> client-count 1) (not (:include-comparison args))) (conj 13)))))
output-stream) output-stream)
(.toByteArray output-stream))) (.toByteArray output-stream)))

View File

@@ -120,7 +120,8 @@
(list (list
[:div.text-2xl.font-bold.text-gray-600 (str "Balance Sheet - " (str/join ", " (map :client/name client))) ] [:div.text-2xl.font-bold.text-gray-600 (str "Balance Sheet - " (str/join ", " (map :client/name client))) ]
(rtable/table {:widths (cond-> (into [30 ] (repeat 13 client-count)) (rtable/table {:widths (cond-> (into [30 ] (repeat 13 client-count))
(> (count date) 1) (into (repeat 13 (* 2 client-count (dec (count date)))))) (> (count date) 1) (into (repeat 13 (* 2 client-count (dec (count date)))))
(and (> client-count 1) (= (count date) 1)) (conj 13))
:investigate-url (bidi.bidi/path-for ssr-routes/only-routes ::route/investigate) :investigate-url (bidi.bidi/path-for ssr-routes/only-routes ::route/investigate)
:table report :table report
:warning (not-empty (str/join "\n " (filter not-empty [warning (:warning report)])))} ))))]) :warning (not-empty (str/join "\n " (filter not-empty [warning (:warning report)])))} ))))])
@@ -201,8 +202,9 @@
(conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
(conj (conj
(table->pdf report (table->pdf report
(cond-> (into [30 ] (repeat client-count 13)) (cond-> (into [30 ] (repeat client-count 13))
(> (count date) 1) (into (repeat (* 2 client-count (dec (count date))) 13 )))))) (> (count date) 1) (into (repeat (* 2 client-count (dec (count date))) 13 ))
(and (> client-count 1) (= (count date) 1)) (conj 13)))))
output-stream) output-stream)
(.toByteArray output-stream))) (.toByteArray output-stream)))

View File

@@ -1,15 +1,17 @@
(ns auto-ap.ssr.pos.sales-summaries (ns auto-ap.ssr.pos.sales-summaries
(:require (:require
[auto-ap.datomic [auto-ap.datomic
:refer [apply-pagination apply-sort-3 conn merge-query pull-many :refer [apply-pagination apply-sort-3 conn merge-query pull-many
query2]] query2]]
[auto-ap.datomic.accounts :as d-accounts] [auto-ap.datomic.accounts :as d-accounts]
[auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.query-params :refer [wrap-copy-qp-pqp]] [auto-ap.query-params :refer [wrap-copy-qp-pqp]]
[auto-ap.client-routes :as client-routes]
[auto-ap.routes.pos.sales-summaries :as route] [auto-ap.routes.pos.sales-summaries :as route]
[auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]] [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
[auto-ap.ssr.components :as com] [auto-ap.ssr.components :as com]
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
[auto-ap.ssr.components.multi-modal :as mm] [auto-ap.ssr.components.multi-modal :as mm]
[auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]] [auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
@@ -18,16 +20,16 @@
:refer [date-range-field*]] :refer [date-range-field*]]
[auto-ap.ssr.svg :as svg] [auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils [auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers clj-date-schema :refer [apply-middleware-to-all-handlers clj-date-schema
default-grid-fields-schema entity-id html-response money default-grid-fields-schema entity-id html-response money
strip temp-id wrap-merge-prior-hx wrap-schema-enforce]] strip temp-id wrap-merge-prior-hx wrap-schema-enforce]]
[auto-ap.time :as atime] [auto-ap.time :as atime]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[clj-time.coerce :as c] [clj-time.coerce :as c]
[clojure.string :as str] [clojure.string :as str]
[datomic.api :as dc] [datomic.api :as dc]
[hiccup.util :as hu] [hiccup.util :as hu]
[iol-ion.query :refer [dollars=]] [iol-ion.query :refer [dollars= dollars-0?]]
[malli.core :as mc] [malli.core :as mc]
[malli.util :as mut])) [malli.util :as mut]))
@@ -38,15 +40,15 @@
[:start-date {:optional true} [:start-date {:optional true}
[:maybe clj-date-schema]] [:maybe clj-date-schema]]
[:end-date {:optional true} [:end-date {:optional true}
[:maybe clj-date-schema]] ] [:maybe clj-date-schema]]]
default-grid-fields-schema)])) default-grid-fields-schema)]))
(defn filters [request] (defn filters [request]
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
"hx-get" (bidi/path-for ssr-routes/only-routes "hx-get" (bidi/path-for ssr-routes/only-routes
::route/table) ::route/table)
"hx-target" "#entity-table" "hx-target" "#entity-table"
"hx-indicator" "#entity-table"} "hx-indicator" "#entity-table"}
[:fieldset.space-y-6 [:fieldset.space-y-6
(date-range-field* request)]]) (date-range-field* request)]])
@@ -55,15 +57,14 @@
* *
[:sales-summary/date :xform clj-time.coerce/from-date] [:sales-summary/date :xform clj-time.coerce/from-date]
{:sales-summary/client [:client/code :client/name :db/id]} {:sales-summary/client [:client/code :client/name :db/id]}
{:sales-summary/items [{[:ledger-mapped/ledger-side :xform iol-ion.query/ident] [:db/ident] {:sales-summary/items [{[:ledger-mapped/ledger-side :xform iol-ion.query/ident] [:db/ident]}
} :ledger-mapped/account
:ledger-mapped/account :ledger-mapped/amount
:ledger-mapped/amount :sales-summary-item/category
:sales-summary-item/category :sales-summary-item/sort-order
:sales-summary-item/sort-order :db/id
:db/id :sales-summary-item/manual?]}
:sales-summary-item/manual?] {:journal-entry/original-entity [:db/id]}])
} ])
(defn fetch-ids [db request] (defn fetch-ids [db request]
(let [query-params (:query-params request) (let [query-params (:query-params request)
@@ -72,27 +73,26 @@
(:client-id query-params) (:client-id query-params)
(when (:client-code query-params) (when (:client-code query-params)
[:client/code (:client-code query-params)])) [:client/code (:client-code query-params)]))
query (cond-> {:query {:find [] query (cond-> {:query {:find []
:in '[$ [?client ...]] :in '[$ [?client ...]]
:where '[[?e :sales-summary/client ?client]]} :where '[[?e :sales-summary/client ?client]]}
:args [db valid-clients]} :args [db valid-clients]}
(or (:start-date query-params) (or (:start-date query-params)
(:end-date query-params)) (:end-date query-params))
(merge-query {:query '{:where [[?e :sales-summary/date ?d]]}}) (merge-query {:query '{:where [[?e :sales-summary/date ?d]]}})
(:start-date query-params) (:start-date query-params)
(merge-query {:query '{:in [?start-date] (merge-query {:query '{:in [?start-date]
:where [[(>= ?d ?start-date)]]} :where [[(>= ?d ?start-date)]]}
:args [(-> query-params :start-date c/to-date)]}) :args [(-> query-params :start-date c/to-date)]})
(:end-date query-params) (:end-date query-params)
(merge-query {:query '{:in [?end-date] (merge-query {:query '{:in [?end-date]
:where [[(< ?d ?end-date)]]} :where [[(< ?d ?end-date)]]}
:args [(-> query-params :end-date c/to-date)]}) :args [(-> query-params :end-date c/to-date)]})
true true
(merge-query {:query {:find ['?sort-default '?e] (merge-query {:query {:find ['?sort-default '?e]
:where ['[?e :sales-summary/date ?sort-default]]}}))] :where ['[?e :sales-summary/date ?sort-default]]}}))]
(cond->> (query2 query) (cond->> (query2 query)
true (apply-sort-3 query-params) true (apply-sort-3 query-params)
@@ -107,7 +107,7 @@
refunds)) refunds))
(defn fetch-page [request] (defn fetch-page [request]
(let [db (dc/db conn) (let [db (dc/db conn)
{ids-to-retrieve :ids matching-count :count} (fetch-ids db request)] {ids-to-retrieve :ids matching-count :count} (fetch-ids db request)]
[(->> (hydrate-results ids-to-retrieve db request)) [(->> (hydrate-results ids-to-retrieve db request))
@@ -116,9 +116,6 @@
(defn sort-items [ss] (defn sort-items [ss]
(sort-by (juxt :ledger-mapped/ledger-side :sales-summary-item/sort-order :sales-summary-item/category) ss)) (sort-by (juxt :ledger-mapped/ledger-side :sales-summary-item/sort-order :sales-summary-item/category) ss))
(defn total-debits [items] (defn total-debits [items]
(->> items (->> items
(filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %))) (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)))
@@ -131,12 +128,17 @@
(map #(:ledger-mapped/amount % 0.0)) (map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0))) (reduce + 0.0)))
(defn truncate [s max-len]
(if (> (count s) max-len)
(str (subs s 0 (- max-len 3)) "...")
s))
(def grid-page (def grid-page
(helper/build {:id "entity-table" (helper/build {:id "entity-table"
:id-fn :db/id :id-fn :db/id
:nav com/main-aside-nav :nav com/main-aside-nav
:fetch-page fetch-page :fetch-page fetch-page
:page-specific-nav filters :page-specific-nav filters
:query-schema query-schema :query-schema query-schema
:row-buttons (fn [_ entity] :row-buttons (fn [_ entity]
[(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes [(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
@@ -144,72 +146,126 @@
:db/id (:db/id entity))} :db/id (:db/id entity))}
svg/pencil)]) svg/pencil)])
:oob-render :oob-render
(fn [request] (fn [request]
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)]) [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:company)} :company)}
"POS"] "POS"]
[:a {:href (bidi/path-for ssr-routes/only-routes [:a {:href (bidi/path-for ssr-routes/only-routes
::route/page)} ::route/page)}
"Sales Summaries"]] "Sales Summaries"]]
:title "Sales Summaries" :title "Sales Summaries"
:entity-name "Daily Summary" :entity-name "Daily Summary"
:route ::route/table :route ::route/table
:headers [{:key "client" :headers [{:key "client"
:name "Client" :name "Client"
:sort-key "client" :sort-key "client"
:hide? (fn [args] :hide? (fn [args]
(= (count (:clients args)) 1)) (= (count (:clients args)) 1))
:render #(-> % :sales-summary/client :client/code)} :render #(-> % :sales-summary/client :client/code)}
{:key "date"
:name "Date"
:sort-key "date"
:render #(some-> % :sales-summary/date (atime/unparse-local atime/normal-date))}
{:key "date" {:key "debits"
:name "Date" :name "Debits"
:sort-key "date" :sort-key "debits"
:render #(some-> % :sales-summary/date (atime/unparse-local atime/normal-date))} :class "w-72 align-top"
:render (fn [ss]
(let [items (:sales-summary/items ss)
debit-items (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)) (sort-items items))
credit-count (count (filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)) items))
total-debits (total-debits items)]
[:div.flex.flex-col.h-full
[:ul.flex-grow
(for [si debit-items]
[:li.flex.items-baseline.gap-2.py-0.5.text-sm.text-gray-700
[:span.flex-1.min-w-0.truncate.text-gray-600
(:sales-summary-item/category si)]
(when-not (:ledger-mapped/account si)
[:span.shrink-0 (com/pill {:color :red} "?")])
[:span.shrink-0.font-mono.tabular-nums.text-right.text-gray-900.whitespace-nowrap
(format "$%,.2f" (:ledger-mapped/amount si))]])
(for [_ (range (max 0 (- credit-count (count debit-items))))]
[:li.py-0.5.text-sm " "])]
[:div.border-t-2.border-gray-300.mt-1.pt-1.flex.justify-between.items-baseline
[:span.text-xs.uppercase.tracking-wider.font-semibold.text-gray-500 "Total"]
[:span.font-mono.tabular-nums.font-bold.text-gray-900
(format "$%,.2f" total-debits)]]]))}
{:key "credits"
:name "Credits"
:sort-key "credits"
:class "w-72 align-top"
:render (fn [ss]
(let [items (:sales-summary/items ss)
credit-items (filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)) (sort-items items))
debit-count (count (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)) items))
total-credits (total-credits items)]
[:div.flex.flex-col.h-full
[:ul.flex-grow
(for [si credit-items]
[:li.flex.items-baseline.gap-2.py-0.5.text-sm.text-gray-700
[:span.flex-1.min-w-0.truncate.text-gray-600
(:sales-summary-item/category si)]
(when-not (:ledger-mapped/account si)
[:span.shrink-0 (com/pill {:color :red} "?")])
[:span.shrink-0.font-mono.tabular-nums.text-right.text-gray-900.whitespace-nowrap
(format "$%,.2f" (:ledger-mapped/amount si))]])
(for [_ (range (max 0 (- debit-count (count credit-items))))]
[:li.py-0.5.text-sm " "])]
[:div.border-t-2.border-gray-300.mt-1.pt-1.flex.justify-between.items-baseline
[:span.text-xs.uppercase.tracking-wider.font-semibold.text-gray-500 "Total"]
[:span.font-mono.tabular-nums.font-bold.text-gray-900
(format "$%,.2f" total-credits)]]]))}
{:key "debits" {:key "balance"
:name "debits" :name "Status"
:sort-key "debits" :sort-key "balance"
:render (fn [ss] :class "w-28 align-top"
(let [total-debits (total-debits (:sales-summary/items ss)) :render (fn [ss]
total-credits (total-credits (:sales-summary/items ss))] (let [items (:sales-summary/items ss)
[:ul total-debits (total-debits items)
(for [si (sort-items (:sales-summary/items ss)) total-credits (total-credits items)
:when (= :ledger-side/debit (:ledger-mapped/ledger-side si))] delta (- total-debits total-credits)
[:li (:sales-summary-item/category si) ": " (format "$%,.2f" (:ledger-mapped/amount si)) balanced? (dollars= total-debits total-credits)
(when-not (:ledger-mapped/account si) missing-account? (some #(not (:ledger-mapped/account %)) items)]
[:span.pl-4 (com/pill {:color :red} [:div.flex.flex-col.items-center.gap-1.pt-2
"missing account")])] (when missing-account?
) [:span.inline-block.text-xs.font-semibold.uppercase.tracking-wider.text-amber-800.bg-amber-100.border.border-amber-300.rounded-sm.px-1.5.py-0.5
[:li (com/pill {:color (if (dollars= total-debits total-credits) "Missing acct"])
:primary (if balanced?
:red)} "Total: " (format "$%,.2f" total-debits))]]))} (when-not missing-account?
{:key "credits" [:span.inline-block.text-xs.font-semibold.uppercase.tracking-wider.text-emerald-800.bg-emerald-100.border.border-emerald-300.rounded-sm.px-1.5.py-0.5
:name "credits" "Balanced"])
:sort-key "credits" [:div.flex.flex-col.items-center
:render (fn [ss] [:span.font-mono.tabular-nums.text-red-700.font-bold.text-sm
(let [total-debits (total-debits (:sales-summary/items ss)) (format "$%,.2f" (Math/abs delta))]
total-credits (total-credits (:sales-summary/items ss))] [:span.text-xs.uppercase.tracking-wider.text-red-600.font-medium.mt-0.5
[:ul (if (> total-debits total-credits) "Debit over" "Credit over")]])]))}
(for [si (sort-items (:sales-summary/items ss))
:when (= :ledger-side/credit (:ledger-mapped/ledger-side si))] {:key "links"
[:li (:sales-summary-item/category si) ": " (format "$%,.2f" (:ledger-mapped/amount si)) :name "Links"
(when-not (:ledger-mapped/account si) :show-starting "lg"
[:span.pl-4 (com/pill {:color :red} :class "w-8"
"missing account")])]) :render (fn [ss]
[:li (com/pill {:color (if (dollars= total-debits total-credits) (let [ledger-entry (:journal-entry/original-entity ss)]
:primary (when (seq ledger-entry)
:red)} "Total: " (format "$%,.2f" total-credits))]]))}]})) (link-dropdown
[{:link (hu/url (bidi/path-for client-routes/routes :ledger)
{:exact-match-id (:db/id (first ledger-entry))})
:color :yellow
:content "Ledger entry"}]))))}]}))
(def row* (partial helper/row* grid-page)) (def row* (partial helper/row* grid-page))
(def table* (partial helper/table* grid-page)) (def table* (partial helper/table* grid-page))
(def edit-schema (def edit-schema
[:map [:map
[:db/id entity-id] [:db/id entity-id]
[:sales-summary/client [:map [:db/id entity-id]]] [:sales-summary/client [:map [:db/id entity-id]]]
[:sales-summary/items [:sales-summary/items
[:vector {:coerce? true} [:vector {:coerce? true}
@@ -233,8 +289,7 @@
:error/path [:credit]} :error/path [:credit]}
(fn [x] (fn [x]
(not (and (:credit x) (not (and (:credit x)
(:debit x))))]]]] ]) (:debit x))))]]]]])
(defn summary-total-row* [request] (defn summary-total-row* [request]
(let [total-credits (-> request (let [total-credits (-> request
@@ -249,16 +304,22 @@
(total-debits))] (total-debits))]
(com/data-grid-row {:id "total-row" (com/data-grid-row {:id "total-row"
:class "bg-slate-50 border-t-2 border-slate-300"
:hx-trigger "change from:closest form target:.amount-field" :hx-trigger "change from:closest form target:.amount-field"
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-total) :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-total)
:hx-target "this" :hx-target "this"
:hx-swap "innerHTML"} :hx-swap "innerHTML"}
(com/data-grid-cell {}) (com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
(com/data-grid-cell {:class "text-right"} (com/data-grid-cell {:class "text-right"}
(format "$%,.2f" total-debits)) [:span.text-xs.uppercase.tracking-wider.font-semibold.text-slate-600
"Total"])
(com/data-grid-cell {:class "text-right"} (com/data-grid-cell {:class "text-right"}
(format "$%,.2f" total-credits))))) [:span.font-mono.tabular-nums.font-bold.text-slate-900
(format "$%,.2f" total-debits)])
(com/data-grid-cell {:class "text-right"}
[:span.font-mono.tabular-nums.font-bold.text-slate-900
(format "$%,.2f" total-credits)])
(com/data-grid-cell {}))))
(defn unbalanced-row* [request] (defn unbalanced-row* [request]
(let [total-credits (-> request (let [total-credits (-> request
@@ -270,44 +331,52 @@
:multi-form-state :multi-form-state
:step-params :step-params
:sales-summary/items :sales-summary/items
(total-debits))] (total-debits))
unbalanced? (not (dollars= total-credits total-debits))
debit-over? (and unbalanced? (> total-debits total-credits))
credit-over? (and unbalanced? (> total-credits total-debits))]
(com/data-grid-row {:id "total-row" (com/data-grid-row {:id "unbalanced-row"
:class (when unbalanced? "bg-red-50 border-t border-red-200")
:hx-trigger "change from:closest form target:.amount-field" :hx-trigger "change from:closest form target:.amount-field"
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-total) :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-total)
:hx-target "this" :hx-target "this"
:hx-swap "innerHTML"} :hx-swap "innerHTML"}
(com/data-grid-cell {}) (com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "UNBALANCED"])
(com/data-grid-cell {:class "text-right"} (com/data-grid-cell {:class "text-right"}
(when (and (when unbalanced?
(not (dollars= total-credits total-debits)) [:span.text-xs.uppercase.tracking-wider.font-semibold.text-red-700
(> total-debits total-credits)) "Out of balance"]))
(format "$%,.2f" (- total-debits total-credits))))
(com/data-grid-cell {:class "text-right"} (com/data-grid-cell {:class "text-right"}
(when (when debit-over?
(and (not (dollars= total-credits total-debits)) [:span.font-mono.tabular-nums.font-bold.text-red-700
(> total-credits total-debits)) (format "$%,.2f" (- total-debits total-credits))]))
(format "$%,.2f" (- total-credits total-debits))))))) (com/data-grid-cell {:class "text-right"}
(when credit-over?
[:span.font-mono.tabular-nums.font-bold.text-red-700
(format "$%,.2f" (- total-credits total-debits))]))
(com/data-grid-cell {}))))
(defn- account-typeahead* (defn- account-typeahead*
[{:keys [name value client-id]}] [{:keys [name value client-id]}]
[:div.flex.flex-col [:div.flex.flex-col
(com/typeahead {:name name (com/typeahead {:name name
:placeholder "Search..." :placeholder "Search..."
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
{:client-id client-id {:client-id client-id
:purpose "invoice"}) :purpose "invoice"})
:value value :value value
:content-fn (fn [value] :content-fn (fn [value]
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
client-id)))})]) client-id)))})])
(defn sales-summary-item-row* [{:keys [value client-id]}] (defn sales-summary-item-row* [{:keys [value client-id]}]
(let [manual? (fc/field-value (:sales-summary-item/manual? value))] (let [manual? (fc/field-value (:sales-summary-item/manual? value))]
(com/data-grid-row (cond-> {:x-ref "p" (com/data-grid-row (cond-> {:x-ref "p"
:x-data (hx/json {})} :x-data (hx/json {})
(fc/field-value (:new? value)) (hx/htmx-transition-appear )) :class (when manual?
"bg-indigo-50/40 border-l-2 border-indigo-300")}
(fc/field-value (:new? value)) (hx/htmx-transition-appear))
(fc/with-field :db/id (fc/with-field :db/id
(com/hidden {:name (fc/field-name) (com/hidden {:name (fc/field-name)
:value (fc/field-value)})) :value (fc/field-value)}))
@@ -315,46 +384,49 @@
(fc/with-field :sales-summary-item/manual? (fc/with-field :sales-summary-item/manual?
(com/hidden {:name (fc/field-name) (com/hidden {:name (fc/field-name)
:value true}))) :value true})))
(com/data-grid-cell {} (com/data-grid-cell {:class "align-top"}
(fc/with-field :sales-summary-item/category (fc/with-field :sales-summary-item/category
(if manual? (if manual?
(com/validated-field {:errors (fc/field-errors)} (com/validated-field {:errors (fc/field-errors)}
(com/text-input {:placeholder "Category/Explanation" (com/text-input {:placeholder "Category/Explanation"
:name (fc/field-name) :name (fc/field-name)
:value (fc/field-value)})) :value (fc/field-value)}))
(list (list
(com/hidden {:name (fc/field-name) (com/hidden {:name (fc/field-name)
:value (fc/field-value)}) :value (fc/field-value)})
(fc/field-value (:sales-summary-item/category value)))))) [:span.text-sm.text-gray-700
(com/data-grid-cell {} (fc/field-value (:sales-summary-item/category value))]))))
(com/data-grid-cell {:class "align-top"}
(fc/with-field :ledger-mapped/account (fc/with-field :ledger-mapped/account
(com/validated-field {:errors (fc/field-errors)} (com/validated-field {:errors (fc/field-errors)}
(account-typeahead* {:value (fc/field-value) (account-typeahead* {:value (fc/field-value)
:client-id client-id :client-id client-id
:name (fc/field-name)})))) :name (fc/field-name)}))))
(com/data-grid-cell {:class "text-right"} (com/data-grid-cell {:class "text-right align-top"}
(if manual? (if manual?
(fc/with-field :debit (fc/with-field :debit
(com/validated-field {:errors (fc/field-errors)} (com/validated-field {:errors (fc/field-errors)}
(com/money-input {:class "w-24" (com/money-input {:class "w-24 text-right font-mono tabular-nums"
:name (fc/field-name) :name (fc/field-name)
:value (fc/field-value)}))) :value (fc/field-value)})))
(when (= (fc/field-value (:ledger-mapped/ledger-side value)) (when (= (fc/field-value (:ledger-mapped/ledger-side value))
:ledger-side/debit) :ledger-side/debit)
(format "$%,.2f" (fc/field-value (:ledger-mapped/amount value)))))) [:span.font-mono.tabular-nums.text-gray-900.text-sm.whitespace-nowrap
(com/data-grid-cell {:class "text-right"} (format "$%,.2f" (fc/field-value (:ledger-mapped/amount value)))])))
(com/data-grid-cell {:class "text-right align-top"}
(if manual? (if manual?
(fc/with-field :credit (fc/with-field :credit
(com/validated-field {:errors (fc/field-errors)} (com/validated-field {:errors (fc/field-errors)}
(com/money-input {:class "w-24" (com/money-input {:class "w-24 text-right font-mono tabular-nums"
:name (fc/field-name) :name (fc/field-name)
:value (fc/field-value)}))) :value (fc/field-value)})))
(when (= (fc/field-value (:ledger-mapped/ledger-side value)) (when (= (fc/field-value (:ledger-mapped/ledger-side value))
:ledger-side/credit) :ledger-side/credit)
(format "$%,.2f" (fc/field-value (:ledger-mapped/amount value)))))) [:span.font-mono.tabular-nums.text-gray-900.text-sm.whitespace-nowrap
(format "$%,.2f" (fc/field-value (:ledger-mapped/amount value)))])))
(com/data-grid-cell {:class "align-top"} (com/data-grid-cell {:class "align-top"}
(when manual? (when manual?
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x)))))) (com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))))
@@ -370,42 +442,42 @@
[]) [])
(step-schema [_] (step-schema [_]
(mut/select-keys (mm/form-schema linear-wizard) #{:db/id :sales-summary/items})) (mut/select-keys (mm/form-schema linear-wizard) #{:db/id :sales-summary/items}))
(render-step (render-step
[this {:keys [multi-form-state] :as request}] [this {:keys [multi-form-state] :as request}]
(mm/default-render-step (mm/default-render-step
linear-wizard this linear-wizard this
:head [:div.p-2 "New invoice"] :head [:div.p-2 "New invoice"]
:body (mm/default-step-body :body (mm/default-step-body
{} {}
[:div [:div
(fc/with-field :db/id (fc/with-field :db/id
(com/hidden {:name (fc/field-name) (com/hidden {:name (fc/field-name)
:value (fc/field-value)})) :value (fc/field-value)}))
(com/data-grid {:headers (com/data-grid {:headers
[(com/data-grid-header {} "Category") [(com/data-grid-header {} "Category")
(com/data-grid-header {} "Account") (com/data-grid-header {} "Account")
(com/data-grid-header {} "Debits") (com/data-grid-header {} "Debits")
(com/data-grid-header {} "Credits") (com/data-grid-header {} "Credits")
(com/data-grid-header {} "")]} (com/data-grid-header {} "")]}
(fc/with-field :sales-summary/items (fc/with-field :sales-summary/items
(list (list
(fc/cursor-map #(sales-summary-item-row* {:value % (fc/cursor-map #(sales-summary-item-row* {:value %
:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state))) })) :client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))}))
(com/data-grid-new-row {:colspan 5 (com/data-grid-new-row {:colspan 5
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item) :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
:row-offset 0 :row-offset 0
:index (count (fc/field-value)) :index (count (fc/field-value))
:tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}} :tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}}
"New Summary Item"))) "New Summary Item")))
(summary-total-row* request) (summary-total-row* request)
(unbalanced-row* request)) ]) (unbalanced-row* request))])
:footer :footer
(mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate) (mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate)
:validation-route ::route/edit-wizard-navigate :validation-route ::route/edit-wizard-navigate
:width-height-class "lg:w-[850px] lg:h-[900px]"))) :width-height-class "lg:w-[920px] lg:h-[640px]")))
(defn attach-ledger [i] (defn attach-ledger [i]
(cond-> i (cond-> i
@@ -424,8 +496,8 @@
(navigate [this step-key] (navigate [this step-key]
(assoc this :current-step step-key)) (assoc this :current-step step-key))
(get-current-step (get-current-step
[this] [this]
(mm/get-step this :main)) (mm/get-step this :main))
(render-wizard [this {:keys [multi-form-state] :as request}] (render-wizard [this {:keys [multi-form-state] :as request}]
(mm/default-render-wizard (mm/default-render-wizard
this request this request
@@ -437,29 +509,28 @@
(steps [_] (steps [_]
[:main]) [:main])
(get-step [this step-key] (get-step [this step-key]
(let [step-key-result (mc/parse mm/step-key-schema step-key) (let [step-key-result (mc/parse mm/step-key-schema step-key)
[step-key-type step-key] step-key-result] [step-key-type step-key] step-key-result]
(->MainStep this))) (->MainStep this)))
(form-schema [_] (form-schema [_]
edit-schema) edit-schema)
(submit [this {:keys [multi-form-state request-method identity] :as request}] (submit [this {:keys [multi-form-state request-method identity] :as request}]
(let [result (:snapshot multi-form-state ) (let [result (:snapshot multi-form-state)
transaction [:upsert-sales-summary {:db/id (:db/id result) transaction [:upsert-sales-summary {:db/id (:db/id result)
:sales-summary/items (map :sales-summary/items (map
(fn [i] (fn [i]
(if (:sales-summary-item/manual? i) (if (:sales-summary-item/manual? i)
(attach-ledger i) (attach-ledger i)
{:db/id (:db/id i) {:db/id (:db/id i)
:ledger-mapped/account (:ledger-mapped/account i) :ledger-mapped/account (:ledger-mapped/account i)}))
})) (:sales-summary/items result))}]]
(:sales-summary/items result))}]]
(clojure.pprint/pprint (:sales-summary/items result)) (clojure.pprint/pprint (:sales-summary/items result))
@(dc/transact conn [ transaction]) @(dc/transact conn [transaction])
(html-response (html-response
(row* identity (dc/pull (dc/db conn) default-read (:db/id result)) (row* identity (dc/pull (dc/db conn) default-read (:db/id result))
{:flash? true {:flash? true
:request request}) :request request})
:headers (cond-> {"hx-trigger" "modalclose" :headers (cond-> {"hx-trigger" "modalclose"
"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id result)) "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id result))
"hx-reswap" "outerHTML"}))))) "hx-reswap" "outerHTML"})))))
@@ -479,8 +550,8 @@
(def key->handler (def key->handler
(apply-middleware-to-all-handlers (apply-middleware-to-all-handlers
(->> (->>
{::route/page (helper/page-route grid-page) {::route/page (helper/page-route grid-page)
::route/table (helper/table-route grid-page) ::route/table (helper/table-route grid-page)
::route/edit-wizard (-> mm/open-wizard-handler ::route/edit-wizard (-> mm/open-wizard-handler
(mm/wrap-wizard edit-wizard) (mm/wrap-wizard edit-wizard)
(mm/wrap-init-multi-form-state initial-edit-wizard-state) (mm/wrap-init-multi-form-state initial-edit-wizard-state)
@@ -498,9 +569,9 @@
(wrap-schema-enforce :query-schema [:map (wrap-schema-enforce :query-schema [:map
[:client-id {:optional true} [:client-id {:optional true}
[:maybe entity-id]]])) [:maybe entity-id]]]))
::route/edit-wizard-submit (-> mm/submit-handler ::route/edit-wizard-submit (-> mm/submit-handler
(mm/wrap-wizard edit-wizard) (mm/wrap-wizard edit-wizard)
(mm/wrap-decode-multi-form-state))}) (mm/wrap-decode-multi-form-state))})
(fn [h] (fn [h]
(-> h (-> h
(wrap-copy-qp-pqp) (wrap-copy-qp-pqp)

View File

@@ -798,30 +798,34 @@
(defn balance-sheet-headers [pnl-data] (defn balance-sheet-headers [pnl-data]
(let [period-count (count (:periods (:args pnl-data)))] (let [period-count (count (:periods (:args pnl-data)))
client-ids (set (map :client-id (:data pnl-data)))
client-count (count client-ids)
show-total? (and (> client-count 1) (= 1 period-count))]
(cond-> [] (cond-> []
(> (count (set (map :client-id (:data pnl-data)))) 1) (> client-count 1)
(conj (into [{:value "Client"}] (conj (cond-> (into [{:value "Client"}]
(mapcat identity
(for [client client-ids]
(cond-> [{:value (str (-> pnl-data :client-codes (get client)))}]
(> period-count 1)
(into (apply concat (repeat (dec period-count) ["" ""])))))))
show-total? (conj {:value "Total" :bold true :border [:left]})))
(mapcat identity
(for [client (set (map :client-id (:data pnl-data))) ]
(cond-> [{:value (str (-> pnl-data :client-codes (get client)))}]
(> period-count 1)
(into (apply concat (repeat (dec period-count) ["" ""]))))))))
true true
(conj (into [{:value "Period Ending"}] (conj (cond-> (into [{:value "Period Ending"}]
(for [client (set (map :client-id (:data pnl-data))) (for [client client-ids
[index p] (map vector (range) (:periods (:args pnl-data))) [index p] (map vector (range) (:periods (:args pnl-data)))
:let [is-first? (= 0 index) :let [is-first? (= 0 index)
period-date (date->str p) period-date (date->str p)
period-headers (if (or is-first? period-headers (if (or is-first?
(not (:include-deltas (:args pnl-data)))) (not (:include-deltas (:args pnl-data))))
[{:value period-date}] [{:value period-date}]
[{:value period-date} [{:value period-date}
{:value "+/-"}])] {:value "+/-"}])]
header period-headers] header period-headers]
header)))))) header))
show-total? (conj {:value (date->str (first (:periods (:args pnl-data)))) :border [:left]}))))))
(defn append-deltas [table] (defn append-deltas [table]
(->> table (->> table
@@ -890,12 +894,33 @@
:rows table}))) :rows table})))
) )
(defn add-total-border [rows]
(map (fn [row]
(let [last-idx (dec (count row))]
(map-indexed
(fn [i cell]
(if (= i last-idx)
(let [borders (or (:border cell) [])]
(assoc cell :border (conj borders :left)))
cell))
row)))
rows))
(defn summarize-balance-sheet [pnl-data] (defn summarize-balance-sheet [pnl-data]
(let [pnl-datas (for [client-id (set (map :client-id (:data pnl-data))) (let [client-ids (set (map :client-id (:data pnl-data)))
p (:periods (:args pnl-data))] client-count (count client-ids)
(-> pnl-data period-count (count (:periods (:args pnl-data)))
(filter-client client-id) show-total? (and (> client-count 1) (= 1 period-count))
(filter-period p)))] pnl-datas (for [client-id client-ids
p (:periods (:args pnl-data))]
(-> pnl-data
(filter-client client-id)
(filter-period p)))
total-data (when show-total?
(-> pnl-data
(filter-period (first (:periods (:args pnl-data))))
(assoc :cell-args {:bold true})))
pnl-datas (concat pnl-datas (when total-data [total-data]))]
(let [table (-> [] (let [table (-> []
(into (detail-rows pnl-datas (into (detail-rows pnl-datas
:assets :assets
@@ -912,10 +937,11 @@
(negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
pnl-datas) pnl-datas)
"Retained Earnings"))) "Retained Earnings")))
table (if (and (> (count (:periods (:args pnl-data))) 1) table (if (and (> period-count 1)
(:include-deltas (:args pnl-data))) (:include-deltas (:args pnl-data)))
(append-deltas table) (append-deltas table)
table)] table)
table (if show-total? (add-total-border table) table)]
{:warning (warning-message pnl-data) {:warning (warning-message pnl-data)
:header (balance-sheet-headers pnl-data) :header (balance-sheet-headers pnl-data)
:rows table})) :rows table}))

View File

@@ -265,7 +265,8 @@ NOTE: Please review the transactions we may have question for you here: https://
[:div.notification.is-warning.is-light [:div.notification.is-warning.is-light
(:warning report)]) (:warning report)])
[rtable/table {:widths (cond-> (into [30 ] (repeat 13 client-count)) [rtable/table {:widths (cond-> (into [30 ] (repeat 13 client-count))
(:include-comparison args) (into (repeat 13 (* 2 client-count)))) (:include-comparison args) (into (repeat 13 (* 2 client-count)))
(and (> client-count 1) (not (:include-comparison args))) (conj 13))
:click-event ::investigate-clicked :click-event ::investigate-clicked
:table report}]])) :table report}]]))

View File

@@ -1,5 +1,2 @@
#!/bin/bash #!/bin/bash
sudo docker run --rm -ti -v ~/dev/integreat/data/solr:/var/solr --network=bridge -p 8983:8983 bryce-solr sudo docker run --rm -ti -v ~/dev/integreat/data/solr:/var/solr --network=bridge -p 8983:8983 679918342773.dkr.ecr.us-east-1.amazonaws.com/integreat-solr
#sudo podman container run --user 1000 --privileged --volume /home/notid/dev/integreat/data/solr:/var/solr -p 8983:8983 bryce-solr