From 6f7f1c7815de36bb8a836bdaeb1c341eac292219 Mon Sep 17 00:00:00 2001 From: Bryce Date: Wed, 20 May 2026 20:59:43 -0700 Subject: [PATCH] Add linking, location, import-batch, and potential-duplicates filters to SSR transactions --- src/clj/auto_ap/ssr/transaction/common.clj | 180 ++++++++++++++++----- 1 file changed, 140 insertions(+), 40 deletions(-) diff --git a/src/clj/auto_ap/ssr/transaction/common.clj b/src/clj/auto_ap/ssr/transaction/common.clj index 34c4a9c9..cb034654 100644 --- a/src/clj/auto_ap/ssr/transaction/common.clj +++ b/src/clj/auto_ap/ssr/transaction/common.clj @@ -42,8 +42,15 @@ [:description {:optional true} [:maybe [:string {:decode/string strip}]]] [:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]] [:bank-account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :bank-account/numeric-code]}]]] - [:account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :account/name]}]]] - #_[:status {:optional true} [:maybe (ref->enum-schema "transaction-status")]] + [:account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :account/name]}]]] + [:linked-to {:optional true} [:maybe [:enum "payment" "expected-deposit" "invoice" "none"]]] + [:location {:optional true} [:maybe [:string {:decode/string strip}]]] + [:potential-duplicates {:optional true} + [:maybe [:boolean {:decode/string {:enter #(cond (= % "on") true + (= % "") false + :else + (boolean %))}}]]] + #_[:status {:optional true} [:maybe (ref->enum-schema "transaction-status")]] [:exact-match-id {:optional true} [:maybe entity-id]] [:all-selected {:optional true :default nil} [:maybe :boolean]] [:selected {:optional true :default nil} [:maybe [:vector {:coerce? true} @@ -167,13 +174,59 @@ :where ['[?import-batch-id :import-batch/entry ?e]]} :args [(:import-batch-id args)]}) - (:unresolved args) + (:unresolved args) (merge-query {:query {:where ['[?e :transaction/date] '(or-join [?e] (not [?e :transaction/accounts]) (and [?e :transaction/accounts ?tas] (not [?tas :transaction-account/account]))) ]}}) + (seq (:location args)) + (merge-query {:query {:in ['?location] + :where ['[?e :transaction/accounts ?tas] + '[?tas :transaction-account/location ?location]]} + :args [(:location args)]}) + + (= (:linked-to args) "payment") + (merge-query {:query {:where ['[?e :transaction/payment]]}}) + + (= (:linked-to args) "expected-deposit") + (merge-query {:query {:where ['[?e :transaction/expected-deposit]]}}) + + (= (:linked-to args) "invoice") + (merge-query {:query {:where ['[?e :transaction/payment ?p] + '[_ :invoice-payment/payment ?p]]}}) + + (= (:linked-to args) "none") + (merge-query {:query {:where ['(not [?e :transaction/payment]) + '(not [?e :transaction/expected-deposit])]}}) + + (:potential-duplicates args) + (merge-query (let [bank-account-id (:db/id (:bank-account args)) + _ (when-not bank-account-id + (throw (ex-info "In order to select potential duplicates, you must choose a bank account." + {:validation-error "In order to select potential duplicates, you must choose a bank account."}))) + duplicate-ids (->> (dc/q '[:find ?tx ?amount ?date + :in $ ?ba + :where + [?tx :transaction/bank-account ?ba] + [?tx :transaction/amount ?amount] + [?tx :transaction/date ?date] + (not [?tx :transaction/approval-status :transaction-approval-status/suppressed])] + db + bank-account-id) + (group-by (fn [[_ amount date]] + [amount date])) + (filter (fn [[_ txes]] + (> (count txes) 1))) + (vals) + (mapcat identity) + (map first) + set)] + {:query {:in '[[?e ...]] + :where []} + :args [duplicate-ids]})) + (:status route-params) (merge-query {:query {:in ['?status] :where ['[?e :transaction/approval-status ?status]]} @@ -223,6 +276,18 @@ svg/x)]])] [:div {:id "exact-match-id-tag"}])) +(defn import-batch-id* [request] + (when-let [import-batch-id (:import-batch-id (:query-params request))] + [:div {:x-data (hx/json {:import_batch_id import-batch-id}) :id "import-batch-id-tag"} + (com/hidden {:name "import-batch-id" + "x-model" "import_batch_id"}) + (com/pill {:color :primary} + [:span.inline-flex.space-x-2.items-center + [:div (str "Batch " import-batch-id)] + [:div.w-3.h-3 + (com/link {"@click" "import_batch_id=null; $nextTick(() => $dispatch('change'))"} + svg/x)]])])) + (defn bank-account-filter* [request] @@ -275,40 +340,74 @@ (bank-account-filter* request) (date-range-field* request) - (com/field {:label "Description"} - (com/text-input {:name "description" - :id "description" - :class "hot-filter" - :value (:description (:query-params request)) - :placeholder "e.g., Groceries" - :size :small})) + (com/field {:label "Description"} + (com/text-input {:name "description" + :id "description" + :class "hot-filter" + :value (:description (:query-params request)) + :placeholder "e.g., Groceries" + :size :small})) - (com/field {:label "Amount"} - [:div.flex.space-x-4.items-baseline - (com/money-input {:name "amount-gte" - :id "amount-gte" - :hx-preserve "true" - :class "hot-filter w-20" - :value (:amount-gte (:query-params request)) - :placeholder "0.01" - :size :small}) - [:div.align-baseline - "to"] - (com/money-input {:name "amount-lte" - :hx-preserve "true" - :id "amount-lte" - :class "hot-filter w-20" - :value (:amount-lte (:query-params request)) - :placeholder "9999.34" - :size :small})]) - (when (is-admin? (:identity request)) - [:div.mt-4 {:x-data (hx/json {:unresolvedOnly (:unresolved (:query-params request))})} - (com/hidden {:name "unresolved" - ":value" "unresolvedOnly ? 'on' : ''"}) - (com/checkbox {:value (:unresolved (:query-params request)) - :x-model "unresolvedOnly"} - "Unresolved only")]) - (exact-match-id* request)]]) + (com/field {:label "Location"} + (com/text-input {:name "location" + :id "location" + :class "hot-filter" + :value (:location (:query-params request)) + :placeholder "SC" + :size :small})) + + (com/field {:label "Amount"} + [:div.flex.space-x-4.items-baseline + (com/money-input {:name "amount-gte" + :id "amount-gte" + :hx-preserve "true" + :class "hot-filter w-20" + :value (:amount-gte (:query-params request)) + :placeholder "0.01" + :size :small}) + [:div.align-baseline + "to"] + (com/money-input {:name "amount-lte" + :hx-preserve "true" + :id "amount-lte" + :class "hot-filter w-20" + :value (:amount-lte (:query-params request)) + :placeholder "9999.34" + :size :small})]) + + (com/field {:label "Linking"} + (com/radio-card {:size :small + :name "linked-to" + :value (or (:linked-to (:query-params request)) "") + :options [{:value "" + :content "All"} + {:value "none" + :content "None"} + {:value "invoice" + :content "Invoice"} + {:value "expected-deposit" + :content "Expected Deposit"} + {:value "payment" + :content "Payment"}]})) + + (when (is-admin? (:identity request)) + [:div.mt-4 {:x-data (hx/json {:unresolvedOnly (:unresolved (:query-params request))})} + (com/hidden {:name "unresolved" + ":value" "unresolvedOnly ? 'on' : ''"}) + (com/checkbox {:value (:unresolved (:query-params request)) + :x-model "unresolvedOnly"} + "Unresolved only")]) + + (when (is-admin? (:identity request)) + [:div.mt-4 {:x-data (hx/json {:potentialDuplicates (:potential-duplicates (:query-params request))})} + (com/hidden {:name "potential-duplicates" + ":value" "potentialDuplicates ? 'on' : ''"}) + (com/checkbox {:value (:potential-duplicates (:query-params request)) + :x-model "potentialDuplicates"} + "Same Amount + Date")]) + + (import-batch-id* request) + (exact-match-id* request)]]) (def grid-page @@ -318,10 +417,11 @@ :page-specific-nav filters :fetch-page fetch-page :query-schema query-schema - :oob-render - (fn [request] - [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true) - (assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)]) + :oob-render + (fn [request] + [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true) + (assoc-in (exact-match-id* request) [1 :hx-swap-oob] true) + (some-> (import-batch-id* request) (assoc-in [1 :hx-swap-oob] true))]) :action-buttons (fn [request] [ (com/button {:color :primary