Add vendor pre-population for bulk code and individual edit forms

- Add vendor-changed HTMX handlers for both bulk code and individual edit
- Pre-populate default account at 100% when vendor is selected and no accounts exist
- Fix render-accounts-section to render from step-params correctly
- Change bulk code vendor-changed from hx-get to hx-post to include form data
- Add routes for vendor-changed endpoints
- Update e2e tests to cover vendor pre-population
- Run lein cljfmt fix across codebase
This commit is contained in:
2026-05-21 14:45:19 -07:00
parent 8bd0cee1b1
commit ba87805d4c
210 changed files with 8694 additions and 9627 deletions

View File

@@ -1,9 +1,9 @@
(ns auto-ap.ssr.invoices
(:require
[auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-3
audit-transact audit-transact-batch conn merge-query
observable-query pull-many]]
:refer [add-sorter-fields apply-pagination apply-sort-3
audit-transact audit-transact-batch conn merge-query
observable-query pull-many]]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.datomic.bank-accounts :as d-bank-accounts]
[auto-ap.datomic.clients :as d-clients]
@@ -23,7 +23,7 @@
[auto-ap.routes.payments :as payment-route]
[auto-ap.routes.transactions :as transaction-routes]
[auto-ap.routes.utils
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
[auto-ap.rule-matching :as rm]
[auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes]
@@ -41,13 +41,13 @@
[auto-ap.ssr.components.date-range :as dr]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers assert-schema
clj-date-schema dissoc-nil-transformer entity-id
form-validation-error html-response main-transformer
many-entity modal-response money percentage
ref->enum-schema round-money strip wrap-entity
wrap-implied-route-param wrap-merge-prior-hx
wrap-schema-enforce]]
:refer [apply-middleware-to-all-handlers assert-schema
clj-date-schema dissoc-nil-transformer entity-id
form-validation-error html-response main-transformer
many-entity modal-response money percentage
ref->enum-schema round-money strip wrap-entity
wrap-implied-route-param wrap-merge-prior-hx
wrap-schema-enforce]]
[auto-ap.time :as atime]
[auto-ap.utils :refer [by dollars-0? dollars=]]
[bidi.bidi :as bidi]
@@ -63,7 +63,6 @@
[malli.util :as mut]
[slingshot.slingshot :refer [try+]]))
(defn exact-match-id* [request]
(if (nat-int? (:exact-match-id (:query-params request)))
[:div {:x-data (hx/json {:exact_match (:exact-match-id (:query-params request))}) :id "exact-match-id-tag"}
@@ -105,9 +104,9 @@
(:db/id (:client request))))
:class "filter-trigger"}))
(dr/date-range-field {:value {:start (:start-date (:query-params request))
:end (:end-date (:query-params request))}
:id "date-range"
:apply-button? true})
:end (:end-date (:query-params request))}
:id "date-range"
:apply-button? true})
(com/field {:label "Check #"}
(com/text-input {:name "check-number"
:id "check-number"
@@ -122,7 +121,7 @@
:value (:invoice-number (:query-params request))
:placeholder "e.g., ABC-456"
:size :small}))
(com/field {:label "Amount"}
[:div.flex.space-x-4.items-baseline
(com/money-input {:name "amount-gte"
@@ -143,7 +142,6 @@
:size :small})])
(exact-match-id* request)]])
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
(let [valid-clients (extract-client-ids (:clients request)
(:client-id request)
@@ -165,7 +163,6 @@
(some-> (:start-date query-params) coerce/to-date)
(some-> (:end-date query-params) coerce/to-date)]]}
(:client-id query-params)
(merge-query {:query {:in ['?client-id]
:where ['[?e :invoice/client ?client-id]]}
@@ -177,7 +174,6 @@
'[?client-id :client/code ?client-code]]}
:args [(:client-code query-params)]})
(:start (:due-range query-params)) (merge-query {:query {:in '[?start-due]
:where ['[?e :invoice/due ?due]
'[(>= ?due ?start-due)]]}
@@ -188,14 +184,13 @@
'[(<= ?due ?end-due)]]}
:args [(coerce/to-date (:end (:due-range query-params)))]})
(:import-status query-params)
(merge-query {:query {:in ['?import-status]
:where ['[?e :invoice/import-status ?import-status]]}
:args [(:import-status query-params)]})
(not (:import-status query-params))
(merge-query {:query { :where ['[?e :invoice/import-status :import-status/imported]]} })
(merge-query {:query {:where ['[?e :invoice/import-status :import-status/imported]]}})
(:status route-params)
(merge-query {:query {:in ['?status]
@@ -269,7 +264,6 @@
(apply-sort-3 (assoc query-params :default-asc? false))
(apply-pagination query-params))))
(defn hydrate-results [ids db _]
(let [results (->> (pull-many db default-read ids)
(group-by :db/id))
@@ -279,31 +273,30 @@
refunds))
(defn sum-outstanding [ids]
(->>
(dc/q {:find ['?id '?o]
:in ['$ '[?id ...]]
:where ['[?id :invoice/outstanding-balance ?o]]}
(dc/db conn)
ids)
(->>
(dc/q {:find ['?id '?o]
:in ['$ '[?id ...]]
:where ['[?id :invoice/outstanding-balance ?o]]}
(dc/db conn)
ids)
(map last)
(reduce
+
0.0)))
(defn sum-total-amount [ids]
(->>
(dc/q {:find ['?id '?o]
:in ['$ '[?id ...]]
:where ['[?id :invoice/total ?o]]
}
(dc/db conn)
ids)
(map last)
(reduce
+
0.0)))
(->>
(dc/q {:find ['?id '?o]
:in ['$ '[?id ...]]
:where ['[?id :invoice/total ?o]]}
(dc/db conn)
ids)
(map last)
(reduce
+
0.0)))
(defn fetch-page [request]
(let [db (dc/db conn)
@@ -351,7 +344,6 @@
(assoc-in [:query-params :start] 0)
(assoc-in [:query-params :per-page] 250))))
:else
selected)]
ids))
@@ -369,21 +361,20 @@
(defn can-undo-autopayment [invoice]
(try+
(assert-can-undo-autopayment invoice)
(assert-can-undo-autopayment invoice)
true
(catch [:type :warning] {}
false)))
false)))
(defn pay-button* [params]
(let [ids (:ids params)
ids (if (seq ids)
ids (if (seq ids)
(map first
(dc/q '[:find ?i
:in $ [?i ...]
:where (not [?i :invoice/scheduled-payment])]
(dc/db conn)
ids))
(dc/q '[:find ?i
:in $ [?i ...]
:where (not [?i :invoice/scheduled-payment])]
(dc/db conn)
ids))
ids)
selected-client-count (if (seq ids)
(ffirst
@@ -417,18 +408,17 @@
outstanding-balances)
total (reduce + 0.0 vendor-totals)
paying-credit? (and (> (count ids) 1)
(= 1 (count vendor-totals))
at-least-one-positive-payment
(dollars-0? total))]
(= 1 (count vendor-totals))
at-least-one-positive-payment
(dollars-0? total))]
[:div (cond-> {:hx-target "this"
:hx-trigger "click from:#pay-button"
:x-tooltip "{allowHTML: true, content: () => $refs.template.innerHTML, appendTo: $root}"}
paying-credit? (assoc :hx-post (bidi/path-for ssr-routes/only-routes ::route/pay-using-credit))
(not paying-credit? ) (assoc :hx-get (bidi/path-for ssr-routes/only-routes
::route/pay-wizard)))
(not paying-credit?) (assoc :hx-get (bidi/path-for ssr-routes/only-routes
::route/pay-wizard)))
(com/button {:color :primary
:id "pay-button"
:disabled (or (= (count (:ids params)) 0)
@@ -445,14 +435,13 @@
(cond
paying-credit?
"Pay invoices using credit"
(> (count ids) 0)
(format "Pay %d invoices ($%,.2f)"
(count ids)
(or total 0.0))
(or (= 0 (count ids))
(> selected-client-count 1))
(list "Pay " (com/badge {} "!"))
@@ -474,13 +463,11 @@
:else
[:div "Click to choose a bank account"])]]))
(defn pay-button [request]
(html-response
(pay-button* {:ids (selected->ids request
(:query-params request))})))
;; TODO test as a real user
(def grid-page
(helper/build {:id "entity-table"
@@ -493,9 +480,9 @@
:oob-render
(fn [request]
[(assoc-in (dr/date-range-field {:value {:start (:start-date (:query-params request))
:end (:end-date (:query-params request))}
:id "date-range"
:apply-button? true}) [1 :hx-swap-oob] true)
:end (:end-date (:query-params request))}
:id "date-range"
:apply-button? true}) [1 :hx-swap-oob] true)
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)])
:query-schema query-schema
:parse-query-params (fn [p]
@@ -547,12 +534,11 @@
:db/id (:db/id entity))}
svg/undo))
(when (and (can? (:identity request) {:subject :invoice :activity :edit})
(can-undo-autopayment entity)
)
(can-undo-autopayment entity))
(com/button {:hx-put (bidi/path-for ssr-routes/only-routes
::route/undo-autopay
:db/id (:db/id entity))}
"Undo autopay"))])
::route/undo-autopay
:db/id (:db/id entity))}
"Undo autopay"))])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
"Invoices"]]
@@ -573,7 +559,7 @@
(= 1 (count (:client/locations (:client args))))))
:render (fn [x] [:div.flex.items-center.gap-2 (-> x :invoice/client :client/name)
(map #(com/pill {:color :primary} (-> % :invoice-expense-account/location))
(:invoice/expense-accounts x)) ])}
(:invoice/expense-accounts x))])}
{:key "vendor"
:name "Vendor"
:sort-key "vendor"
@@ -593,13 +579,12 @@
:name "Due"
:show-starting "xl" ;; xl:table-cell
:render (fn [{:invoice/keys [due]}]
(if-let [due-date (some-> due (atime/unparse-local atime/normal-date)) ]
(let [
today (time/now)
(if-let [due-date (some-> due (atime/unparse-local atime/normal-date))]
(let [today (time/now)
[start end] (if (time/before? due today)
[due today]
[today due])
i (time/interval start end )
i (time/interval start end)
days (if (time/before? due today)
(- (time/in-days i))
(time/in-days i))]
@@ -607,23 +592,23 @@
[:div.text-primary-700 "today"]
(> days 0)
[:div.text-primary-700 (format "in %d days", days)]
:else
[:div.text-red-700 (format "%d days ago", (- days))]))))}
:else
[:div.text-red-700 (format "%d days ago", (- days))]))))}
{:key "status"
:name "Status"
:render (fn [{:invoice/keys [status scheduled-payment]}]
(cond (= status :invoice-status/paid)
(com/pill {:color :primary} "Paid")
(= status :invoice-status/voided)
(com/pill {:color :red} "Voided")
scheduled-payment
(com/pill {:color :yellow} "Scheduled")
(com/pill {:color :primary} "Paid")
(= status :invoice-status/voided)
(com/pill {:color :red} "Voided")
(= status :invoice-status/unpaid)
(com/pill {:color :secondary} "Unpaid")
:else
""))}
scheduled-payment
(com/pill {:color :yellow} "Scheduled")
(= status :invoice-status/unpaid)
(com/pill {:color :secondary} "Unpaid")
:else
""))}
{:key "accounts"
:name "Account"
:show-starting "lg"
@@ -656,32 +641,32 @@
:class "w-8"
:render (fn [i]
(link-dropdown
(into []
(concat (->> i
:invoice/payments
(map :invoice-payment/payment)
(filter (fn [p]
(not= :payment-status/voided
(:payment/status p))))
(mapcat (fn [p]
(cond-> [{:link (hu/url (bidi/path-for ssr-routes/only-routes
::payment-route/all-page)
{:exact-match-id (:db/id p)})
:content (str (format "$%,.2f" (:payment/amount p))
(some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}]
(:payment/transaction p) (conj {:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page)
{:exact-match-id (:db/id (first (:payment/transaction p)))})
:color :secondary
:content "Transaction"})))))
(when (:invoice/journal-entry i)
[{:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page)
{:exact-match-id (:db/id (first (:invoice/journal-entry i)))})
:color :yellow
:content "Ledger entry"}])
(when (:invoice/source-url i)
[{:link (:invoice/source-url i)
:color :secondary
:content "File"}])))))}]}))
(into []
(concat (->> i
:invoice/payments
(map :invoice-payment/payment)
(filter (fn [p]
(not= :payment-status/voided
(:payment/status p))))
(mapcat (fn [p]
(cond-> [{:link (hu/url (bidi/path-for ssr-routes/only-routes
::payment-route/all-page)
{:exact-match-id (:db/id p)})
:content (str (format "$%,.2f" (:payment/amount p))
(some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}]
(:payment/transaction p) (conj {:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page)
{:exact-match-id (:db/id (first (:payment/transaction p)))})
:color :secondary
:content "Transaction"})))))
(when (:invoice/journal-entry i)
[{:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page)
{:exact-match-id (:db/id (first (:invoice/journal-entry i)))})
:color :yellow
:content "Ledger entry"}])
(when (:invoice/source-url i)
[{:link (:invoice/source-url i)
:color :secondary
:content "File"}])))))}]}))
(def row* (partial helper/row* grid-page))
@@ -722,18 +707,16 @@
(defn undo-autopay [{:as request :keys [identity entity]}]
(let [invoice entity
id (:db/id entity)
_ (assert-can-see-client identity (:db/id (:invoice/client invoice)))
]
_ (assert-can-see-client identity (:db/id (:invoice/client invoice)))]
(alog/info ::undoing-autopay :transaction :tx)
(assert-can-undo-autopayment invoice)
(audit-transact
[[:upsert-invoice {:db/id id
:invoice/status :invoice-status/unpaid
:invoice/outstanding-balance (:invoice/total entity)
:invoice/scheduled-payment nil}]]
(audit-transact
[[:upsert-invoice {:db/id id
:invoice/status :invoice-status/unpaid
:invoice/outstanding-balance (:invoice/total entity)
:invoice/scheduled-payment nil}]]
identity)
(html-response
(row* identity (dc/pull (dc/db conn) default-read id) {:flash? true
:request request})
@@ -847,8 +830,6 @@
id)
(count all-ids)))
(defn bulk-delete-dialog-confirm [request]
(alog/peek (:form-params request))
(let [ids (selected->ids request (:form-params request))
@@ -861,8 +842,7 @@
(count ids))})})))
#_(defn pay-invoices-from-balance [context {invoices :invoices
client-id :client_id} _]
)
client-id :client_id} _])
(defn pay-using-credit [request]
(alog/peek (:form-params request))
@@ -896,7 +876,6 @@
0.001))
invoices)
total-to-pay (reduce + 0 (map :invoice/outstanding-balance invoices-to-be-paid))
_ (when (<= total-to-pay 0.001)
(throw (ex-info "Select some invoices that need to be paid" {:type :form-validation})))
@@ -926,8 +905,6 @@
[total-to-pay []])))
(into {}))
vendor-id (:db/id (:invoice/vendor (first invoices)))
payment {:db/id (str vendor-id)
:payment/amount total-to-pay
@@ -949,7 +926,6 @@
:notification (format "Successfully paid %d invoices."
(count invoices))})})))
(defn does-amount-exceed-outstanding? [amount outstanding-balance]
(let [outstanding-balance (round-money outstanding-balance)
amount (round-money amount)]
@@ -1018,12 +994,11 @@
:to (mm/encode-step-key :payment-details)})}
"Credit")
(com/button {:x-ref "button"
"@click.prevent.capture" "$tooltip($refs.tooltip.innerHTML, {allowHTML: true, onMount(i) {htmx.process(i.popper)}, interactive:true, theme:\"light\", timeout:5000})" }
"@click.prevent.capture" "$tooltip($refs.tooltip.innerHTML, {allowHTML: true, onMount(i) {htmx.process(i.popper)}, interactive:true, theme:\"light\", timeout:5000})"}
"Pay"))
[:template { :x-ref "tooltip"}
[:div.flex.flex-col.gap-2 {
:data-key "vis"
:class "p-4 w-max" }
[:template {:x-ref "tooltip"}
[:div.flex.flex-col.gap-2 {:data-key "vis"
:class "p-4 w-max"}
(when (= :bank-account-type/check
(:bank-account/type bank-account))
(com/button {:color :primary
@@ -1094,7 +1069,6 @@
:can-handwrite? can-handwrite?
:credit-only? credit-only?}))
(defn can-handwrite? [invoices]
(let [selected-vendors (set (map (comp :db/id :invoice/vendor) invoices))]
(and
@@ -1110,7 +1084,6 @@
(reduce + 0.0 (map :invoice/outstanding-balance is))))
(every? #(<= % 0.0))))
(defrecord ChoosePaymentMethodModal [linear-wizard]
mm/ModalWizardStep
(step-name [_]
@@ -1195,7 +1168,7 @@
(format "Pay in full ($%,.2f)" total)))}
{:value "advanced"
:content "Customize payments"}]})
[:div.space-y-4
(fc/with-field :invoices
(com/validated-field
@@ -1345,7 +1318,6 @@
(:snapshot multi-form-state)
mt/strip-extra-keys-transformer)
_ (assert-schema payment-form-schema snapshot)
_ (exception->4xx
@@ -1354,7 +1326,7 @@
(= "" (:check-number snapshot)))
(throw (Exception. "Check number is required")))
true))
result (exception->4xx
#(do
(when (:handwritten-date snapshot)
@@ -1378,7 +1350,7 @@
:payment-type/credit
:else :payment-type/debit)
identity
(:handwritten-date snapshot))
(:handwritten-date snapshot))
(catch Exception e
(println e))))))]
(modal-response
@@ -1455,11 +1427,10 @@
(defn redirect-handler [target-route]
(fn handle [request]
{:status 302
:headers {"Location" (str (hu/url (bidi.bidi/path-for ssr-routes/only-routes
:headers {"Location" (str (hu/url (bidi.bidi/path-for ssr-routes/only-routes
target-route)
(:query-params request)))}}))
(defn initial-bulk-edit-state [request]
(mm/->MultiStepFormState {:search-params (:query-params request)
:expense-accounts [{:db/id "123"
@@ -1479,7 +1450,7 @@
(com/typeahead {:name name
:placeholder "Search..."
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
{ :purpose "invoice"})
{:purpose "invoice"})
:id name
:x-model x-model
:value value
@@ -1487,8 +1458,6 @@
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
client-id)))})])
;; TODO clientize
(defn all-ids-not-locked [all-ids]
(->> all-ids
@@ -1527,7 +1496,7 @@
(com/validated-field
{:errors (fc/field-errors)
:x-hx-val:account-id "accountId"
:hx-vals (hx/json {:name (fc/field-name) })
:hx-vals (hx/json {:name (fc/field-name)})
:x-dispatch:changed "accountId"
:hx-trigger "changed"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
@@ -1536,7 +1505,7 @@
(location-select* {:name (fc/field-name)
:account-location (:account/location (cond->> (:account @value)
(nat-int? (:account @value)) (dc/pull (dc/db conn)
'[:account/location])))
'[:account/location])))
:value (fc/field-value)}))))
(fc/with-field :percentage
(com/data-grid-cell
@@ -1544,10 +1513,10 @@
(com/validated-field
{:errors (fc/field-errors)}
(com/money-input {:name (fc/field-name)
:class "w-16 amount-field"
:value (some-> (fc/field-value)
(* 100)
(long))}))))
:class "w-16 amount-field"
:value (some-> (fc/field-value)
(* 100)
(long))}))))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
@@ -1587,7 +1556,7 @@
:hx-get (bidi/path-for ssr-routes/only-routes
::route/bulk-edit-new-account)
:row-offset 0
:index (count (fc/field-value)) }
:index (count (fc/field-value))}
"New account")
(com/data-grid-row {}
(com/data-grid-cell {})
@@ -1617,7 +1586,6 @@
:next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Save"))
:validation-route ::route/new-wizard-navigate))))
(defn maybe-code-accounts [invoice account-rules valid-locations]
(with-precision 2
(let [accounts (vec (mapcat
@@ -1667,9 +1635,9 @@
(navigate [this step-key]
(assoc this :current-step step-key))
(get-current-step [this]
(if current-step
(mm/get-step this current-step)
(mm/get-step this :accounts)))
(if current-step
(mm/get-step this current-step)
(mm/get-step this :accounts)))
(render-wizard [this {:keys [multi-form-state] :as request}]
(mm/default-render-wizard
this request
@@ -1683,44 +1651,43 @@
(get-step [this step-key]
(let [step-key-result (mc/parse mm/step-key-schema step-key)
[step-key-type step-key] step-key-result]
(get {:accounts (->AccountsStep this) }
(get {:accounts (->AccountsStep this)}
step-key)))
(form-schema [_]
(mc/schema [:map
[:expense-accounts
(many-entity {:min 1}
[:account entity-id]
[:location [:string {:min 1 :error/message "required"}]]
[:percentage percentage])]]))
(many-entity {:min 1}
[:account entity-id]
[:location [:string {:min 1 :error/message "required"}]]
[:percentage percentage])]]))
(submit [this {:keys [multi-form-state request-method identity] :as request}]
(let [selected-ids (selected->ids (assoc request :query-params (:search-params (:snapshot multi-form-state))) (:search-params (:snapshot multi-form-state)))
all-ids (all-ids-not-locked selected-ids)
invoices (pull-many (dc/db conn) '[:db/id :invoice/total {:invoice/client [:client/locations]}] (vec all-ids)) ]
(assert-percentages-add-up (:snapshot multi-form-state))
(doseq [a (-> multi-form-state :snapshot :expense-accounts)
:let [{:keys [:account/location :account/name]} (dc/pull (dc/db conn) [:account/location :account/name] (:account a))]]
(when (and location (not= location (:location a)))
(let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)]
(throw (ex-info err {:validation-error err})))))
(alog/info ::bulk-code :count (count all-ids))
(audit-transact-batch
(map (fn [i]
[:upsert-invoice {:db/id (:db/id i)
:invoice/expense-accounts (maybe-code-accounts i (-> multi-form-state :snapshot :expense-accounts) (-> i :invoice/client :client/locations))}])
invoices)
(:identity request))
(html-response
[:div]
:headers (cond-> {"hx-trigger" (hx/json { "modalclose" ""
"invalidated" ""
"notification" (str "Successfully coded " (count all-ids) " invoices.")})
"hx-reswap" "outerHTML"})))))
(let [selected-ids (selected->ids (assoc request :query-params (:search-params (:snapshot multi-form-state))) (:search-params (:snapshot multi-form-state)))
all-ids (all-ids-not-locked selected-ids)
invoices (pull-many (dc/db conn) '[:db/id :invoice/total {:invoice/client [:client/locations]}] (vec all-ids))]
(assert-percentages-add-up (:snapshot multi-form-state))
(doseq [a (-> multi-form-state :snapshot :expense-accounts)
:let [{:keys [:account/location :account/name]} (dc/pull (dc/db conn) [:account/location :account/name] (:account a))]]
(when (and location (not= location (:location a)))
(let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)]
(throw (ex-info err {:validation-error err})))))
(alog/info ::bulk-code :count (count all-ids))
(audit-transact-batch
(map (fn [i]
[:upsert-invoice {:db/id (:db/id i)
:invoice/expense-accounts (maybe-code-accounts i (-> multi-form-state :snapshot :expense-accounts) (-> i :invoice/client :client/locations))}])
invoices)
(:identity request))
(html-response
[:div]
:headers (cond-> {"hx-trigger" (hx/json {"modalclose" ""
"invalidated" ""
"notification" (str "Successfully coded " (count all-ids) " invoices.")})
"hx-reswap" "outerHTML"})))))
(def bulk-edit-wizard (->BulkEditWizard nil nil))
(defn bulk-edit-total* [request]
(let [total (->> (-> request
:multi-form-state
@@ -1740,7 +1707,7 @@
(filter number?)
(reduce + 0.0))
balance (- 100.0
(* 100.0 total))]
(* 100.0 total))]
[:span {:class (when-not (dollars= 0.0 balance)
"text-red-300")}
(format "%.1f%%" balance)]))
@@ -1769,31 +1736,31 @@
::route/legacy-voided-invoices (redirect-handler ::route/voided-page)
::route/legacy-new-invoice (redirect-handler ::route/new-wizard)
::route/bulk-edit (-> mm/open-wizard-handler
(mm/wrap-wizard bulk-edit-wizard)
(mm/wrap-init-multi-form-state initial-bulk-edit-state))
::route/bulk-edit-submit (-> mm/submit-handler
(mm/wrap-wizard bulk-edit-wizard)
(mm/wrap-decode-multi-form-state)
(wrap-must {:subject :invoice :activity :bulk-edit}))
::route/bulk-edit-total (-> bulk-edit-total
(mm/wrap-wizard bulk-edit-wizard)
(mm/wrap-decode-multi-form-state)
(wrap-must {:subject :invoice :activity :bulk-edit}))
::route/bulk-edit-balance (-> bulk-edit-balance
(mm/wrap-wizard bulk-edit-wizard)
(mm/wrap-decode-multi-form-state)
(wrap-must {:subject :invoice :activity :bulk-edit}))
::route/bulk-edit-new-account (->
(add-new-entity-handler [:step-params :expense-accounts]
(fn render [cursor request]
(bulk-edit-account-row*
{:value cursor }))
(fn build-new-row [base _]
(assoc base :invoice-expense-account/location "Shared")))
(wrap-schema-enforce :query-schema [:map
[:client-id {:optional true}
[:maybe entity-id]]]))
(mm/wrap-wizard bulk-edit-wizard)
(mm/wrap-init-multi-form-state initial-bulk-edit-state))
::route/bulk-edit-submit (-> mm/submit-handler
(mm/wrap-wizard bulk-edit-wizard)
(mm/wrap-decode-multi-form-state)
(wrap-must {:subject :invoice :activity :bulk-edit}))
::route/bulk-edit-total (-> bulk-edit-total
(mm/wrap-wizard bulk-edit-wizard)
(mm/wrap-decode-multi-form-state)
(wrap-must {:subject :invoice :activity :bulk-edit}))
::route/bulk-edit-balance (-> bulk-edit-balance
(mm/wrap-wizard bulk-edit-wizard)
(mm/wrap-decode-multi-form-state)
(wrap-must {:subject :invoice :activity :bulk-edit}))
::route/bulk-edit-new-account (->
(add-new-entity-handler [:step-params :expense-accounts]
(fn render [cursor request]
(bulk-edit-account-row*
{:value cursor}))
(fn build-new-row [base _]
(assoc base :invoice-expense-account/location "Shared")))
(wrap-schema-enforce :query-schema [:map
[:client-id {:optional true}
[:maybe entity-id]]]))
::route/undo-autopay (-> undo-autopay
(wrap-entity [:route-params :db/id] default-read)