diff --git a/src/clj/auto_ap/ssr/transaction/edit.clj b/src/clj/auto_ap/ssr/transaction/edit.clj index 62cd4875..3b9d9e4d 100644 --- a/src/clj/auto_ap/ssr/transaction/edit.clj +++ b/src/clj/auto_ap/ssr/transaction/edit.clj @@ -17,11 +17,11 @@ :refer [wrap-client-redirect-unauthenticated]] [auto-ap.rule-matching :as rm] [auto-ap.solr :as solr] - [auto-ap.ssr-routes :as ssr-routes] - [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]] - [auto-ap.ssr.components :as com] - [auto-ap.ssr.grid-page-helper :as helper] - [auto-ap.ssr.transaction.common :refer [grid-page]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.transaction.common :refer [grid-page]] [auto-ap.ssr.components.multi-modal :as mm] [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.hx :as hx] @@ -69,19 +69,20 @@ [:fn {:error/message "Approved transactions must have accounts assigned."} (fn [{:transaction/keys [accounts approval-status]}] (or (not= approval-status :transaction-approval-status/approved) - (seq accounts)))]]) + (seq accounts)))]]) (def edit-form-schema - (mc/schema - [:and + (mc/schema + [:and [:map [:db/id {:optional true} [:maybe entity-id]] [:action [:enum :apply-rule :unlink-payment :link-unpaid-invoices :link-autopay-invoices :link-payment :manual]] [:transaction/memo {:optional true} [:maybe [:string {:decode/string strip}]]] [:transaction/vendor {:optional true} [:maybe entity-id]] [:transaction/approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")]] + [:amount-mode {:optional true} [:maybe [:enum "$" "%"]]] [:transaction/accounts {:optional true} - [:maybe + [:maybe [:vector {:coerce? true} [:and [:map @@ -102,16 +103,13 @@ [:unlink-payment [:map [:transaction-id entity-id]]] [:link-unpaid-invoices [:map - - [:unpaid-invoice-ids {:decode/string (fn [x] (edn/read-string x))} + [:unpaid-invoice-ids {:decode/string (fn [x] (edn/read-string x))} [:vector {:coerce? true} entity-id]]]] [:link-autopay-invoices [:map - [:autopay-invoice-ids {:decode/string (fn [x] (edn/read-string x))} [:vector {:coerce? true} entity-id]]]] [:link-payment [:map [:payment-id entity-id]]] - [:manual (require-approval [:map])]]])) - + [:manual (require-approval [:map])]]])) (defn clientize-vendor [{:vendor/keys [terms-overrides automatically-paid-when-due default-account account-overrides] :as vendor} client-id] (if (nil? vendor) @@ -122,13 +120,13 @@ client-id))) (map :vendor-terms-override/terms) first) - account (or (->> account-overrides - (filter (fn [to] - (= (->db-id (:vendor-account-override/client to)) - client-id))) - (map :vendor-account-override/account) - first) - default-account) + account (or (->> account-overrides + (filter (fn [to] + (= (->db-id (:vendor-account-override/client to)) + client-id))) + (map :vendor-account-override/account) + first) + default-account) account (d-accounts/clientize account client-id) automatically-paid-when-due (->> automatically-paid-when-due @@ -156,77 +154,83 @@ :else [["Shared" "Shared"]]))] (com/select {:options options - :name name + :name name :value (ffirst options) :class "w-full"}))) (defn account-typeahead* [{:keys [name value client-id x-model]}] [:div.flex.flex-col - (com/typeahead {:name name + (com/typeahead {:name name :placeholder "Search..." - :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) - (cond-> { :purpose "transaction"} - client-id (assoc :client-id client-id))) - :id name - :x-model x-model - :value value - :content-fn (fn [value] - (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) - client-id)))})]) + :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) + (cond-> {:purpose "transaction"} + client-id (assoc :client-id client-id))) + :id name + :x-model x-model + :value value + :content-fn (fn [value] + (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) + client-id)))})]) -(defn transaction-account-row* [{:keys [value client-id]}] +(defn transaction-account-row* [{:keys [value client-id amount-mode total]}] (com/data-grid-row - (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value)))) - :accountId (fc/field-value (:transaction-account/account value))}) + (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value)))) + :accountId (fc/field-value (:transaction-account/account value))}) :data-key "show" - :x-ref "p"} + :x-ref "p"} hx/alpine-mount-then-appear) (fc/with-field :db/id - (com/hidden {:name (fc/field-name) + (com/hidden {:name (fc/field-name) :value (fc/field-value)})) (fc/with-field :transaction-account/account (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} - (account-typeahead* {:value (fc/field-value) + (account-typeahead* {:value (fc/field-value) :client-id client-id - :name (fc/field-name) - :x-model "accountId"})))) + :name (fc/field-name) + :x-model "accountId"})))) (fc/with-field :transaction-account/location (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors) :x-hx-val:account-id "accountId" - :hx-vals (hx/json (cond-> {:name (fc/field-name) } + :hx-vals (hx/json (cond-> {:name (fc/field-name)} client-id (assoc :client-id client-id))) :x-dispatch:changed "accountId" :hx-trigger "changed" - :hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select) + :hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select) :hx-target "find *" :hx-swap "outerHTML"} - (location-select* {:name (fc/field-name) + (location-select* {:name (fc/field-name) :account-location (:account/location (cond->> (:transaction-account/account @value) (nat-int? (:transaction-account/account @value)) (dc/pull (dc/db conn) '[:account/location]))) :client-locations (pull-attr (dc/db conn) :client/locations client-id) - :value (fc/field-value)})))) + :value (fc/field-value)})))) (fc/with-field :transaction-account/amount (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} - (com/money-input {:name (fc/field-name) - :class "w-16" - :value (fc/field-value)})))) + (if (= "%" amount-mode) + (com/text-input {:name (fc/field-name) + :class "w-16" + :value (fc/field-value) + :type "number" + :step "0.01"}) + (com/money-input {:name (fc/field-name) + :class "w-16" + :value (fc/field-value)}))))) (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) (defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}] - (html-response (location-select* {:name name - :value value + (html-response (location-select* {:name name + :value value :account-location (some->> account-id (pull-attr (dc/db conn) :account/location)) :client-locations (some->> client-id @@ -251,8 +255,8 @@ (filter number?) (reduce + 0.0)) balance (- - (Math/abs (-> request :multi-form-state :snapshot :transaction/amount)) - total)] + (Math/abs (-> request :multi-form-state :snapshot :transaction/amount)) + total)] [:span {:class (when-not (dollars= 0.0 balance) "text-red-300")} (format "$%,.2f" balance)])) @@ -263,6 +267,103 @@ (defn account-balance [request] (html-response (account-balance* request))) +(defn ->percentage [amount total] + (when (and amount total (not= total 0)) + (* 100.0 (/ amount total)))) + +(defn percentages->dollars [percentages total] + (let [total-cents (int (* 100 (Math/abs total))) + pct-sum (reduce + 0 percentages) + normalized-pcts (if (zero? pct-sum) + (repeat (count percentages) 0) + (map #(* (/ % pct-sum) 100) percentages)) + individual-cents (map #(int (* total-cents (/ % 100))) normalized-pcts) + short-by (- total-cents (reduce + 0 individual-cents)) + adjustments (concat (take short-by (repeat 1)) (repeat 0)) + final-cents (map + individual-cents adjustments)] + (map #(* 0.01 %) final-cents))) + +(defn convert-accounts-mode [accounts old-mode new-mode total] + (if (= old-mode new-mode) + accounts + (let [amounts (map :transaction-account/amount accounts)] + (map #(assoc %1 :transaction-account/amount %2) + accounts + (case [old-mode new-mode] + ["$" "%"] (map #(->percentage % total) amounts) + ["%" "$"] (percentages->dollars amounts total) + amounts))))) + +(defn account-grid-body* [request] + (let [snapshot (-> request :multi-form-state :snapshot) + amount-mode (or (:amount-mode snapshot) "$") + total (Math/abs (:transaction/amount snapshot))] + (com/data-grid {:headers [(com/data-grid-header {} "Account") + (com/data-grid-header {:class "w-32"} "Location") + (com/data-grid-header {:class "w-16"} + (com/radio-card {:options [{:value "$" :content "$"} + {:value "%" :content "%"}] + :value amount-mode + :name "step-params[amount-mode]" + :hx-post (bidi/path-for ssr-routes/only-routes ::route/toggle-amount-mode) + :hx-target "#account-grid-body" + :hx-swap "outerHTML" + :hx-include "closest form"})) + (com/data-grid-header {:class "w-16"})]} + (fc/cursor-map #(transaction-account-row* {:value % + :client-id (-> request :entity :transaction/client :db/id) + :amount-mode amount-mode + :total total})) + + (com/data-grid-new-row {:colspan 4 + :hx-get (bidi/path-for ssr-routes/only-routes + ::route/edit-wizard-new-account) + :row-offset 0 + :index (count (:transaction/accounts snapshot)) + :tr-params {:hx-vals (hx/json {:client-id (:transaction/client snapshot)})}} + "New account") + (com/data-grid-row {} + (com/data-grid-cell {}) + (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"]) + (com/data-grid-cell {:id "total" + :class "text-right" + :hx-trigger "change from:closest form target:.amount-field" + :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/account-total) + :hx-target "this" + :hx-swap "innerHTML"} + (account-total* request)) + (com/data-grid-cell {})) + + (com/data-grid-row {} + (com/data-grid-cell {}) + (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"]) + (com/data-grid-cell {:id "total" + :class "text-right" + :hx-trigger "change from:closest form target:.amount-field" + :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/account-balance) + :hx-target "this" + :hx-swap "innerHTML"} + (account-balance* request)) + (com/data-grid-cell {})) + + (com/data-grid-row {} + (com/data-grid-cell {}) + (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TRANSACTION TOTAL"]) + (com/data-grid-cell {:class "text-right"} + (format "$%,.2f" total)) + (com/data-grid-cell {}))))) + +(defn toggle-amount-mode [request] + (let [snapshot (-> request :multi-form-state :snapshot) + old-mode (or (:amount-mode snapshot) "$") + new-mode (or (get-in request [:form-params :amount-mode]) "$") + total (Math/abs (:transaction/amount snapshot)) + accounts (convert-accounts-mode (:transaction/accounts snapshot) old-mode new-mode total)] + (html-response + [:div#account-grid-body + (account-grid-body* (assoc-in request [:multi-form-state :snapshot :transaction/accounts] accounts + [:multi-form-state :snapshot :amount-mode] new-mode))]))) + (defrecord BasicDetailsStep [linear-wizard] mm/ModalWizardStep (step-name [_] @@ -288,8 +389,8 @@ :body (mm/default-step-body {} [:div {:x-data (hx/json {:clientId (or (fc/field-value (:transaction/client fc/*current*)) - (:db/id (:client request))) })} - + (:db/id (:client request)))})} + ;; Read-only transaction details [:div.mb-6.border.rounded-lg.p-4.bg-gray-50 [:h3.text-lg.font-semibold.mb-2 "Transaction Details"] @@ -297,51 +398,49 @@ [:div [:div.text-sm.font-medium.text-gray-500 "Amount"] [:div.text-base (format "$%,.2f" (Math/abs (:transaction/amount tx)))]] - + [:div [:div.text-sm.font-medium.text-gray-500 "Date"] [:div.text-base (some-> tx :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))]] - + [:div [:div.text-sm.font-medium.text-gray-500 "Bank Account"] [:div.text-base (or (-> tx :transaction/bank-account :bank-account/name) "-")]] - + [:div [:div.text-sm.font-medium.text-gray-500 "Post Date"] [:div.text-base (some-> tx :transaction/post-date coerce/to-date-time (atime/unparse-local atime/normal-date))]] - + [:div [:div.text-sm.font-medium.text-gray-500 "Original Description"] [:div.text-base (or (:transaction/description-original tx) "-")]] - + [:div [:div.text-sm.font-medium.text-gray-500 "Simplified Description"] [:div.text-base (or (:transaction/description-simple tx) "-")]] - + [:div [:div.text-sm.font-medium.text-gray-500 "Check Number"] [:div.text-base (or (:transaction/check-number tx) "-")]] - + [:div [:div.text-sm.font-medium.text-gray-500 "Status"] [:div.text-base (or (some-> tx :transaction/status) "-")]] - + [:div [:div.text-sm.font-medium.text-gray-500 "Transaction Type"] [:div.text-base (or (some-> tx :transaction/type) "-")]]]] - - - - ;; Transaction Links Section + +;; Transaction Links Section #_[:div.mb-6.border.rounded-lg.p-4.bg-gray-50 [:h3.text-lg.font-semibold.mb-2 "Transaction Links"] (let [tx-id (mm/get-mfs-field multi-form-state :db/id) db-history (dc/history (dc/db conn)) - + ;; Get current and historical payments linked to this transaction - current-payment (when-let [payment-id (-> (dc/pull (dc/db conn) - '[:transaction/payment] - tx-id) + current-payment (when-let [payment-id (-> (dc/pull (dc/db conn) + '[:transaction/payment] + tx-id) :transaction/payment :db/id)] {:entity-id payment-id :active true}) @@ -355,9 +454,9 @@ (map (fn [[id date op]] {:entity-id id :date date :op op})))) - + all-payments historical-payments] - + [:div ;; Payments section [:div.mb-3 @@ -365,27 +464,25 @@ (if (seq all-payments) [:ul.list-disc.pl-6.mt-1 (for [{:keys [entity-id date op]} all-payments - :let [payment (dc/pull (dc/db conn) - '[:db/id :payment/invoice-number - [ :payment/date :xform clj-time.coerce/from-date] - {:payment/vendor [:vendor/name]}] - entity-id)]] + :let [payment (dc/pull (dc/db conn) + '[:db/id :payment/invoice-number + [:payment/date :xform clj-time.coerce/from-date] + {:payment/vendor [:vendor/name]}] + entity-id)]] [:li.text-sm.text-gray-600 {:class (when-not op "line-through")} (if op [:span.text-green-600 "✓ "] "") - (str "Payment #" (:payment/invoice-number payment) " - " + (str "Payment #" (:payment/invoice-number payment) " - " (-> payment :payment/vendor :vendor/name))])] [:p.text-sm.text-gray-500.italic "No payments linked to this transaction"])]])]] - + ;; Invoices section - - - ;; Hidden ID field + +;; Hidden ID field (fc/with-field :db/id (com/hidden {:name (fc/field-name) :value (fc/field-value)})) - - ;; Editable fields section +;; Editable fields section ;; Vendor field ) :footer @@ -410,19 +507,17 @@ :where [?p :payment/client ?client] [?p :payment/status :payment-status/pending]] - (dc/db conn) - client-id))] + (dc/db conn) + client-id))] (filter #(dollars= (Math/abs (:transaction/amount tx)) (:payment/amount %)) payments))) - - (defn get-available-autopay-invoices [request] (let [tx-id (or (-> request :multi-form-state :snapshot :db/id) - (get-in request [:route-params :db/id])) + (get-in request [:route-params :db/id])) tx (when tx-id (d-transactions/get-by-id tx-id)) client-id (-> request :entity :transaction/client :db/id) matches-set (when (and tx client-id) - (i-transactions/match-transaction-to-unfulfilled-autopayments + (i-transactions/match-transaction-to-unfulfilled-autopayments (:transaction/amount tx) client-id))] (when matches-set @@ -441,23 +536,22 @@ [:div.space-y-2 [:label.block.text-sm.font-medium.mb-1 "Select an autopay invoice to apply:"] (com/radio-card {:options (for [match-group invoice-matches] - {:value (pr-str (map :db/id match-group)) - :content (doall (for [invoice match-group] - [:div.ml-3 - [:span.block.text-sm.font-medium (:invoice/invoice-number invoice)] - [:span.block.text-sm.text-gray-500 (-> invoice :invoice/vendor :vendor/name)] - [:span.block.text-sm.font-medium (format "$%.2f" (:invoice/outstanding-balance invoice))]]))}) - :name (fc/with-field :autopay-invoice-ids (fc/field-name )) - :width "w-full"})] - ] + {:value (pr-str (map :db/id match-group)) + :content (doall (for [invoice match-group] + [:div.ml-3 + [:span.block.text-sm.font-medium (:invoice/invoice-number invoice)] + [:span.block.text-sm.text-gray-500 (-> invoice :invoice/vendor :vendor/name)] + [:span.block.text-sm.font-medium (format "$%.2f" (:invoice/outstanding-balance invoice))]]))}) + :name (fc/with-field :autopay-invoice-ids (fc/field-name)) + :width "w-full"})]] [:div.text-center.py-4.text-gray-500 "No matching autopay invoices available for this transaction."])])) (defn get-available-unpaid-invoices [request] (let [tx-id (or (-> request :multi-form-state :snapshot :db/id) - (get-in request [:route-params :db/id])) + (get-in request [:route-params :db/id])) tx (when tx-id (d-transactions/get-by-id tx-id)) client-id (or (get-in request [:multi-form-state :snapshot :transaction/client]) - (get-in request [:client :db/id])) + (get-in request [:client :db/id])) matches-set (when (and tx client-id) (i-transactions/match-transaction-to-unpaid-invoices (:transaction/amount tx) @@ -474,58 +568,56 @@ [:div [:h3.text-lg.font-bold.mb-4 "Available Unpaid Invoices"] [:div #_{:hx-post (bidi/path-for ssr-routes/only-routes ::route/link-unpaid-invoices) - :hx-include "this" - :hx-params "transaction-id, action, unpaid-invoice-ids" - :hx-trigger "linkUnpaidInvoices" - :hx-target "#modal-holder" - :hx-swap "outerHTML"} + :hx-include "this" + :hx-params "transaction-id, action, unpaid-invoice-ids" + :hx-trigger "linkUnpaidInvoices" + :hx-target "#modal-holder" + :hx-swap "outerHTML"} (com/hidden {:name "action" :value "link-unpaid-invoices" :form ""}) #_(com/hidden {:name "transaction-id" - :value (get-in request [:multi-form-state :snapshot :db/id]) - :form ""}) + :value (get-in request [:multi-form-state :snapshot :db/id]) + :form ""}) [:div.space-y-2 [:label.block.text-sm.font-medium.mb-1 "Select an unpaid invoice to apply:"] -(com/radio-card {:options (for [match-group invoice-matches] - {:value (pr-str (map :db/id match-group)) - :content (doall (for [invoice match-group] - [:div.ml-3 - [:span.block.text-sm.font-medium (:invoice/invoice-number invoice)] - [:span.block.text-sm.text-gray-500 (-> invoice :invoice/vendor :vendor/name)] - [:span.block.text-sm.font-medium (format "$%.2f" (:invoice/outstanding-balance invoice))]]))}) - :name (fc/with-field :unpaid-invoice-ids (fc/field-name )) - :width "w-full"}) - ] + (com/radio-card {:options (for [match-group invoice-matches] + {:value (pr-str (map :db/id match-group)) + :content (doall (for [invoice match-group] + [:div.ml-3 + [:span.block.text-sm.font-medium (:invoice/invoice-number invoice)] + [:span.block.text-sm.text-gray-500 (-> invoice :invoice/vendor :vendor/name)] + [:span.block.text-sm.font-medium (format "$%.2f" (:invoice/outstanding-balance invoice))]]))}) + :name (fc/with-field :unpaid-invoice-ids (fc/field-name)) + :width "w-full"})] #_(com/a-button {:color :primary "@click" "$dispatch('linkUnpaidInvoices')"} "Link")]] [:div.text-center.py-4.text-gray-500 "No matching unpaid invoices available for this transaction."])])) (defn get-available-rules [request] (let [tx-id (or (-> request :multi-form-state :snapshot :db/id) - (get-in request [:route-params :db/id])) + (get-in request [:route-params :db/id])) tx (when tx-id (d-transactions/get-by-id tx-id)) - patterns (dc/q '[:find (pull ?r + patterns (dc/q '[:find (pull ?r [:db/id :transaction-rule/description :transaction-rule/note - :transaction-rule/client-group - :transaction-rule/dom-gte :transaction-rule/dom-lte - :transaction-rule/amount-gte :transaction-rule/amount-lte - :transaction-rule/client :transaction-rule/bank-account - :transaction-rule/yodlee-merchant]) + :transaction-rule/client-group + :transaction-rule/dom-gte :transaction-rule/dom-lte + :transaction-rule/amount-gte :transaction-rule/amount-lte + :transaction-rule/client :transaction-rule/bank-account + :transaction-rule/yodlee-merchant]) :where [?r :transaction-rule/description]] - + (dc/db conn))] (when tx (->> patterns (map first) (filter (fn [rule] - (rm/rule-applies? (-> tx - (update :transaction/date coerce/to-date)) - (-> rule - (update :transaction-rule/description #(some-> % iol-ion.query/->pattern)))))))))) - + (rm/rule-applies? (-> tx + (update :transaction/date coerce/to-date)) + (-> rule + (update :transaction-rule/description #(some-> % iol-ion.query/->pattern)))))))))) (defn transaction-rules-view [request] (let [matching-rules (get-available-rules request)] @@ -544,32 +636,32 @@ :content [:div.ml-3 [:span.block.text-sm.font-medium note] [:span.block.text-sm.text-gray-500 description]]}) - :name (fc/with-field :rule-id (fc/field-name )) - :width "w-full"}) ] + :name (fc/with-field :rule-id (fc/field-name)) + :width "w-full"})] #_(com/a-button {"@click" "$dispatch('applyRule')"} "Apply")] [:div.text-center.py-4.text-gray-500 "No matching rules found for this transaction."])])) (defn payment-matches-view [request] (let [payments (get-available-payments request) tx-id (or (-> request :multi-form-state :snapshot :db/id) - (get-in request [:route-params :db/id])) + (get-in request [:route-params :db/id])) tx (when tx-id (d-transactions/get-by-id tx-id)) - payment (dc/pull - (dc/db conn) - '[:payment/amount - :db/id - [:payment/date :xform clj-time.coerce/from-date] - { [ :payment/status :xform iol-ion.query/ident] [:db/ident] + payment (dc/pull + (dc/db conn) + '[:payment/amount + :db/id + [:payment/date :xform clj-time.coerce/from-date] + {[:payment/status :xform iol-ion.query/ident] [:db/ident] + + :payment/vendor [:vendor/name]}] - :payment/vendor [:vendor/name]}] - (-> tx :transaction/payment :db/id))] [:div#payment-matches (if (and payment (:db/id payment)) [:div.my-4.p-4.bg-blue-50.rounded [:h3.text-lg.font-bold.mb-2 "Linked Payment" (com/a-icon-button {:href (hu/url (bidi/path-for ssr-routes/only-routes ::payment-route/all-page) - {:exact-match-id (:db/id payment)})} + {:exact-match-id (:db/id payment)})} svg/external-link)] [:div.space-y-2 [:div.flex.justify-between @@ -595,7 +687,7 @@ :hx-include "this" :hx-swap "outerHTML" :hx-confirm "Are you sure you want to unlink this payment?"} - + (com/a-button {:color :red :size :small "@click" "$dispatch('unlinkPayment')"} "Unlink Payment")]]] (if (seq payments) @@ -604,7 +696,7 @@ [:div.space-y-2 [:label.block.text-sm.font-medium.mb-1 "Select a payment to match:"] (when payments - (let [payment-id-field (fc/with-field :payment-id (fc/field-name ))] + (let [payment-id-field (fc/with-field :payment-id (fc/field-name))] (com/radio-card {:options (for [payment payments] {:value (:db/id payment) :content (str (:payment/invoice-number payment) " - " @@ -612,7 +704,7 @@ " - Amount: $" (format "%.2f" (:payment/amount payment)) " • Date: " (some-> payment :payment/date coerce/to-date-time (atime/unparse-local atime/normal-date)))}) :name payment-id-field - :width "w-full"}))) ]] + :width "w-full"})))]] [:div.text-center.py-4.text-gray-500 "No matching payments available for this transaction."]))])) (defn count-payment-matches [request] @@ -638,8 +730,8 @@ []) (step-schema [_] -#_(require-approval (mut/select-keys (mm/form-schema linear-wizard) #{:transaction/client :transaction/vendor :transaction/memo :transaction/approval-status :db/id})) - (mm/form-schema linear-wizard)) + #_(require-approval (mut/select-keys (mm/form-schema linear-wizard) #{:transaction/client :transaction/vendor :transaction/memo :transaction/approval-status :db/id})) + (mm/form-schema linear-wizard)) (render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}] (mm/default-render-step @@ -650,7 +742,7 @@ [:div (fc/with-field :transaction/memo (com/validated-field - {:label "Memo" + {:label "Memo" :errors (fc/field-errors)} [:div.w-96 (com/text-input {:value (-> (fc/field-value)) @@ -665,7 +757,7 @@ "@unlinked" "canChange=true"} [:div {:class "flex space-x-2 mb-4"} (fc/with-field :action - (com/hidden {:name (fc/field-name) + (com/hidden {:name (fc/field-name) :value (fc/field-value) ":value" "activeForm"})) (com/button-group {:name "method"} @@ -674,29 +766,29 @@ (when (> count 0) (com/badge {:color "green"} (str count)))) "Link to payment") - (com/button-group-button {"@click" "activeForm = 'link-unpaid-invoices'" :value "unpaid" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'link-unpaid-invoices'}" :class "relative" + (com/button-group-button {"@click" "activeForm = 'link-unpaid-invoices'" :value "unpaid" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'link-unpaid-invoices'}" :class "relative" ":disabled" "!canChange"} (let [count (count-unpaid-invoice-matches request)] (when (> count 0) (com/badge {:color "green"} (str count)))) "Link to unpaid invoices") - (com/button-group-button {"@click" "activeForm = 'link-autopay-invoices'" :value "autopay" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'link-autopay-invoices'}" :class "relative" + (com/button-group-button {"@click" "activeForm = 'link-autopay-invoices'" :value "autopay" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'link-autopay-invoices'}" :class "relative" ":disabled" "!canChange"} (let [count (count-autopay-invoice-matches request)] (when (> count 0) (com/badge {:color "green"} (str count)))) "Link to autopay invoices") - (com/button-group-button {"@click" "activeForm = 'apply-rule'" :value "rule" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'apply-rule'}" :class "relative" + (com/button-group-button {"@click" "activeForm = 'apply-rule'" :value "rule" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'apply-rule'}" :class "relative" ":disabled" "!canChange"} (let [count (count-rule-matches request)] (when (> count 0) (com/badge {:color "green"} (str count)))) "Apply rule") - (com/button-group-button {"@click" "activeForm = 'manual'" :value "manual" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'manual'}" + (com/button-group-button {"@click" "activeForm = 'manual'" :value "manual" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'manual'}" ":disabled" "!canChange"} "Manual"))] [:div {:x-show "activeForm === 'link-payment'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} - + (payment-matches-view request)] [:div {:x-show "activeForm === 'link-unpaid-invoices'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} (unpaid-invoices-view request)] @@ -708,24 +800,23 @@ [:div {} (fc/with-field :transaction/vendor (com/validated-field - {:label "Vendor" + {:label "Vendor" :errors (fc/field-errors)} [:div.w-96 - (com/typeahead {:name (fc/field-name) - :error? (fc/error?) - :class "w-96" + (com/typeahead {:name (fc/field-name) + :error? (fc/error?) + :class "w-96" :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :vendor-search) - :value (fc/field-value) - :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})])) - + :url (bidi/path-for ssr-routes/only-routes :vendor-search) + :value (fc/field-value) + :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})])) + ;; Memo field - - - ;; Approval status field + +;; Approval status field (fc/with-field :transaction/approval-status (com/validated-field - {:label "Status" + {:label "Status" :errors (fc/field-errors)} (com/radio-card {:options (mapv (fn [[k v]] {:value (name k) :content v}) transaction-approval-status) @@ -734,64 +825,21 @@ (fc/with-field :transaction/accounts (com/validated-field {:errors (fc/field-errors)} - (com/data-grid {:headers [(com/data-grid-header {} "Account") - (com/data-grid-header {:class "w-32"} "Location") - (com/data-grid-header {:class "w-16"} "$") - (com/data-grid-header {:class "w-16"})]} - (fc/cursor-map #(transaction-account-row* {:value % - :client-id (-> request :entity :transaction/client :db/id)})) - - (com/data-grid-new-row {:colspan 4 - :hx-get (bidi/path-for ssr-routes/only-routes - ::route/edit-wizard-new-account) - :row-offset 0 - :index (count (fc/field-value)) - :tr-params {:hx-vals (hx/json {:client-id (:transaction/client snapshot)})}} - "New account") - (com/data-grid-row {} - (com/data-grid-cell {}) - (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"]) - (com/data-grid-cell {:id "total" - :class "text-right" - :hx-trigger "change from:closest form target:.amount-field" - :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/account-total) - :hx-target "this" - :hx-swap "innerHTML"} - (account-total* request)) - (com/data-grid-cell {})) - - (com/data-grid-row {} - (com/data-grid-cell {}) - (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"]) - (com/data-grid-cell {:id "total" - :class "text-right" - :hx-trigger "change from:closest form target:.amount-field" - :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/account-balance) - :hx-target "this" - :hx-swap "innerHTML"} - (account-balance* request)) - (com/data-grid-cell {})) - - (com/data-grid-row {} - - (com/data-grid-cell {}) - (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TRANSACTION TOTAL"]) - (com/data-grid-cell {:class "text-right"} - (format "$%,.2f" (Math/abs (:transaction/amount snapshot)))) - (com/data-grid-cell {})))))]]]]) + [:div#account-grid-body + (account-grid-body* request)]))]]]]) :footer (mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate :next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Done")) - :validation-route ::route/edit-wizard-navigate)) ) - + :validation-route ::route/edit-wizard-navigate))) + (defmulti save-handler (fn [request] (-> request :multi-form-state :snapshot :action))) (defn- default-update-tx [snapshot transaction] - (merge {:transaction/memo (:transaction/memo snapshot) } + (merge {:transaction/memo (:transaction/memo snapshot)} transaction)) -(defn- save-linked-transaction [{{ snapshot :snapshot} :multi-form-state :as request transaction :entity} payment] +(defn- save-linked-transaction [{{snapshot :snapshot} :multi-form-state :as request transaction :entity} payment] (exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction))) (audit-transact (into [{:db/id (:db/id payment) @@ -801,29 +849,29 @@ [:upsert-transaction (default-update-tx snapshot - {:db/id (:db/id transaction) - :transaction/payment (:db/id payment) - :transaction/vendor (-> payment :payment/vendor :db/id) + {:db/id (:db/id transaction) + :transaction/payment (:db/id payment) + :transaction/vendor (-> payment :payment/vendor :db/id) :transaction/approval-status :transaction-approval-status/approved - :transaction/accounts [{:db/id (random-tempid) - :transaction-account/account (:db/id (d-accounts/get-account-by-numeric-code-and-sets 21000 ["default"])) - :transaction-account/location "A" - :transaction-account/amount (Math/abs (:transaction/amount transaction))}]})]]) + :transaction/accounts [{:db/id (random-tempid) + :transaction-account/account (:db/id (d-accounts/get-account-by-numeric-code-and-sets 21000 ["default"])) + :transaction-account/location "A" + :transaction-account/amount (Math/abs (:transaction/amount transaction))}]})]]) (:identity request))) -(defn- save-memo-only [{{ snapshot :snapshot} :multi-form-state :as request}] +(defn- save-memo-only [{{snapshot :snapshot} :multi-form-state :as request}] (audit-transact [[:upsert-transaction (default-update-tx snapshot {})]] (:identity request))) (defn- is-already-linked-to-this-payment? [transaction payment-id] - (= (pull-attr (dc/db conn) - :transaction/payment + (= (pull-attr (dc/db conn) + :transaction/payment (:db/id transaction)) payment-id)) (defmethod save-handler - :link-payment [{{ {:keys [transaction-id payment-id] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity}] - (let [ payment (d-checks/get-by-id payment-id)] + :link-payment [{{{:keys [transaction-id payment-id] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity}] + (let [payment (d-checks/get-by-id payment-id)] (exception->4xx #(assert-can-see-client (:identity request) (-> transaction :transaction/client :db/id))) (exception->4xx #(assert-can-see-client (:identity request) (-> payment :payment/client :db/id))) @@ -855,7 +903,7 @@ :headers {"hx-trigger" "invalidated"}))) (defmethod save-handler :link-autopay-invoices - [{{ {:keys [autopay-invoice-ids] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity} ] + [{{{:keys [autopay-invoice-ids] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity}] (let [db (dc/db conn) invoice-clients (set (map #(pull-ref db :invoice/client %) autopay-invoice-ids)) invoice-amount (reduce + 0.0 (map #(pull-attr db :invoice/total %) autopay-invoice-ids))] @@ -897,15 +945,15 @@ [:p.text-gray-600.mt-2 "The transaction has been linked to the autopay invoices."]) :headers {"hx-trigger" "invalidated"}))) -(defmethod save-handler :link-unpaid-invoices - [{{ {:keys [unpaid-invoice-ids] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity} ] - (let [ db (dc/db conn) +(defmethod save-handler :link-unpaid-invoices + [{{{:keys [unpaid-invoice-ids] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity}] + (let [db (dc/db conn) invoice-clients (set (map #(pull-ref db :invoice/client %) unpaid-invoice-ids)) invoice-amount (reduce + 0.0 (map #(pull-attr db :invoice/outstanding-balance %) unpaid-invoice-ids))] - + (exception->4xx #(assert-can-see-client (:identity request) (-> transaction :transaction/client :db/id))) (exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction))) - + (when (or (> (count invoice-clients) 1) (not= (-> transaction :transaction/client :db/id) (first invoice-clients))) @@ -914,13 +962,13 @@ :invoice-clients invoice-clients}))) (when-not (dollars= (- (:transaction/amount transaction)) - invoice-amount) + invoice-amount) (throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"}))) - + (when (:transaction/payment transaction) (throw (ex-info "Transaction already linked" {:validation-error "Transaction already linked"}))) - - (let [payment-tx (i-transactions/add-new-payment + + (let [payment-tx (i-transactions/add-new-payment (dc/pull db [:transaction/amount :transaction/date :db/id] (:db/id transaction)) (map (fn [id] (let [entity (dc/pull db [:invoice/vendor :db/id :invoice/total] id)] @@ -933,25 +981,25 @@ (-> transaction :transaction/client :db/id))] (audit-transact (conj payment-tx [:upsert-transaction (default-update-tx snapshot {:db/id (:db/id transaction)})]) (:identity request))) - - (solr/touch-with-ledger (:db/id transaction)) - - (modal-response - (com/success-modal {:title "Transaction linked successfully"} - - [:p.text-gray-600.mt-2 "The transaction has been linked to the autopay invoices."] - [:p.text-gray-600.mt-2 "To view the new payment, click " - (com/link {:href (hu/url (bidi/path-for ssr-routes/only-routes ::payment-route/all-page) - {:exact-match-id (:db/id (pull-attr (dc/db conn) - :transaction/payment - (:db/id transaction)))}) - :hx-boost true} - "here") - " to view it."]) - :headers {"hx-trigger" "invalidated"}))) -(defmethod save-handler - :apply-rule + (solr/touch-with-ledger (:db/id transaction)) + + (modal-response + (com/success-modal {:title "Transaction linked successfully"} + + [:p.text-gray-600.mt-2 "The transaction has been linked to the autopay invoices."] + [:p.text-gray-600.mt-2 "To view the new payment, click " + (com/link {:href (hu/url (bidi/path-for ssr-routes/only-routes ::payment-route/all-page) + {:exact-match-id (:db/id (pull-attr (dc/db conn) + :transaction/payment + (:db/id transaction)))}) + :hx-boost true} + "here") + " to view it."]) + :headers {"hx-trigger" "invalidated"}))) + +(defmethod save-handler + :apply-rule [{{{:keys [rule-id] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity}] (let [transaction-rule (dc/pull (dc/db conn) [:transaction-rule/description @@ -959,35 +1007,35 @@ :transaction-rule/accounts :transaction-rule/approval-status] rule-id)] - + (exception->4xx #(assert-can-see-client (:identity request) (-> transaction :transaction/client :db/id))) (exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction))) - + (let [description-pattern (some-> transaction-rule :transaction-rule/description iol-ion.query/->pattern)] (when (not (rm/rule-applies? transaction {:transaction-rule/description description-pattern})) - (throw (ex-info "Transaction rule does not apply" + (throw (ex-info "Transaction rule does not apply" {:validation-error "Transaction rule does not apply"})))) - + (when (:transaction/payment transaction) - (throw (ex-info "Transaction already associated with a payment" - {:validation-error "Transaction already associated with a payment"}))) - + (throw (ex-info "Transaction already associated with a payment" + {:validation-error "Transaction already associated with a payment"}))) + (let [locations (-> transaction :transaction/client :client/locations) updated-tx (rm/apply-rule {:db/id (:db/id transaction) :transaction/amount (:transaction/amount transaction)} - transaction-rule - locations) + transaction-rule + locations) updated-tx (default-update-tx snapshot updated-tx)] (alog/info ::applying-rule-tx :tx-data updated-tx :transaction transaction :transaction-rule transaction-rule) (audit-transact [[:upsert-transaction updated-tx]] (:identity request))) - + (solr/touch-with-ledger (:db/id transaction)) - - (modal-response - (com/success-modal {:title "Rule applied successfully"} - + + (modal-response + (com/success-modal {:title "Rule applied successfully"} + [:p.text-gray-600.mt-2 "The selected rule has been applied to this transaction."]) :headers {"hx-trigger" "invalidated"}))) @@ -997,7 +1045,7 @@ (let [base-amount (int (/ shared-amount total-locations)) remainder (- shared-amount (* base-amount total-locations))] {:base-amount base-amount - :remainder remainder})) + :remainder remainder})) (defn- spread-account "Spreads the expense account amount across the given locations" @@ -1052,113 +1100,122 @@ (apply-total-delta-to-account ($->cents (:transaction/amount transaction))) (map (fn [ea] (update ea :transaction-account/amount cents->$)))))))) -(defmethod save-handler :manual +(defmethod save-handler :manual [{:as request transaction :entity :keys [multi-form-state]}] (let [tx-data (-> multi-form-state :snapshot (dissoc :action)) - tx-id (:db/id tx-data) - client-id (->db-id (:transaction/client tx-data)) - existing-tx (d-transactions/get-by-id tx-id) - transaction [:upsert-transaction (maybe-spread-locations (assoc tx-data :db/id tx-id))]] - - (alog/info ::transaction transaction :entity transaction) - (exception->4xx #(assert-can-see-client (:identity request) client-id)) - (exception->4xx #(assert-not-locked client-id (:transaction/date existing-tx))) - - (when (and (= :transaction-approval-status/approved (keyword (:transaction/approval-status tx-data))) - (not (seq (:transaction/accounts tx-data)))) - (throw (ex-info "Approved transactions must have accounts assigned." - {:validation-error "Approved transactions must have accounts assigned."}))) + tx-id (:db/id tx-data) + client-id (->db-id (:transaction/client tx-data)) + existing-tx (d-transactions/get-by-id tx-id) + amount-mode (or (:amount-mode tx-data) "$") + total (Math/abs (:transaction/amount existing-tx)) + tx-data (if (= "%" amount-mode) + (update tx-data :transaction/accounts + #(map (fn [account dollar-amount] + (assoc account :transaction-account/amount dollar-amount)) + % + (percentages->dollars (map :transaction-account/amount %) total))) + tx-data) + tx-data (dissoc tx-data :amount-mode) + transaction [:upsert-transaction (maybe-spread-locations (assoc tx-data :db/id tx-id))]] - (when (seq (:transaction/accounts tx-data)) - (let [account-total (reduce + 0 (map :transaction-account/amount (:transaction/accounts tx-data)))] - (when (not (dollars= (Math/abs (:transaction/amount existing-tx)) account-total)) - (throw (ex-info (str "Account total (" account-total ") does not equal transaction amount (" - (Math/abs (:transaction/amount existing-tx)) ")") - {:validation-error "Account totals must match transaction amount."}))))) - - (let [transaction-result (audit-transact [transaction] (:identity request))] - (try - (solr/touch-with-ledger tx-id) - (catch Exception e - (alog/error ::cant-save-solr :error e))) - - (html-response - (row* (:identity request) (d-transactions/get-by-id tx-id) {:flash? true}) - :headers {"hx-trigger" "modalclose" - "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" tx-id) - "hx-reswap" "outerHTML"})))) + (alog/info ::transaction transaction :entity transaction) + (exception->4xx #(assert-can-see-client (:identity request) client-id)) + (exception->4xx #(assert-not-locked client-id (:transaction/date existing-tx))) + + (when (and (= :transaction-approval-status/approved (keyword (:transaction/approval-status tx-data))) + (not (seq (:transaction/accounts tx-data)))) + (throw (ex-info "Approved transactions must have accounts assigned." + {:validation-error "Approved transactions must have accounts assigned."}))) + + (when (seq (:transaction/accounts tx-data)) + (let [account-total (reduce + 0 (map :transaction-account/amount (:transaction/accounts tx-data)))] + (when (not (dollars= (Math/abs (:transaction/amount existing-tx)) account-total)) + (throw (ex-info (str "Account total (" account-total ") does not equal transaction amount (" + (Math/abs (:transaction/amount existing-tx)) ")") + {:validation-error "Account totals must match transaction amount."}))))) + + (let [transaction-result (audit-transact [transaction] (:identity request))] + (try + (solr/touch-with-ledger tx-id) + (catch Exception e + (alog/error ::cant-save-solr :error e))) + + (html-response + (row* (:identity request) (d-transactions/get-by-id tx-id) {:flash? true}) + :headers {"hx-trigger" "modalclose" + "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" tx-id) + "hx-reswap" "outerHTML"})))) (defn unlink-payment [{{{transaction-id :db/id} :snapshot} :multi-form-state :as request}] - - (fc/start-form (:multi-form-state request) (when (:form-errors request) {:step-params (:form-errors request)}) - (let [transaction (dc/pull (dc/db conn) - '[:transaction/approval-status - :transaction/date - :transaction/location - :transaction/vendor - :transaction/accounts - :transaction/status - :transaction/client [:db/id] - {:transaction/payment [:payment/date - {[:payment/status :xform iol-ion.query/ident] [:db/ident]} :db/id]}] - transaction-id) - payment (-> transaction :transaction/payment)] + (fc/start-form (:multi-form-state request) (when (:form-errors request) {:step-params (:form-errors request)}) + (let [transaction (dc/pull (dc/db conn) + '[:transaction/approval-status - (exception->4xx #(assert-can-see-client (:identity request) (-> transaction :transaction/client :db/id))) - (exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction))) + :transaction/date + :transaction/location + :transaction/vendor + :transaction/accounts + :transaction/status + :transaction/client [:db/id] + {:transaction/payment [:payment/date + {[:payment/status :xform iol-ion.query/ident] [:db/ident]} :db/id]}] + transaction-id) + payment (-> transaction :transaction/payment)] - (when (not= :payment-status/cleared (-> payment :payment/status)) - (throw (ex-info "Payment can't be undone because it isn't cleared." - {:validation-error "Payment can't be undone because it isn't cleared."}))) + (exception->4xx #(assert-can-see-client (:identity request) (-> transaction :transaction/client :db/id))) + (exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction))) - (let [is-autopay-payment? (some->> (dc/q {:find ['?sp] - :in ['$ '?payment] - :where ['[?ip :invoice-payment/payment ?payment] - '[?ip :invoice-payment/invoice ?i] - '[(get-else $ ?i :invoice/scheduled-payment "N/A") ?sp]]} - (dc/db conn) (:db/id payment)) - seq - (map first) - (every? #(instance? java.util.Date %)))] - (if is-autopay-payment? - (audit-transact - (-> [{:db/id (:db/id payment) - :payment/status :payment-status/pending} - [:upsert-transaction - {:db/id (:db/id transaction) - :transaction/approval-status :transaction-approval-status/unapproved - :transaction/payment nil - :transaction/vendor nil - :transaction/location nil - :transaction/accounts nil}] - [:db/retractEntity (:db/id payment)]] - (into (map (fn [[invoice-payment]] - [:db/retractEntity invoice-payment]) - (dc/q {:find ['?ip] - :in ['$ '?p] - :where ['[?ip :invoice-payment/payment ?p]]} - (dc/db conn) - (:db/id payment))))) - (:identity request)) - (audit-transact - [{:db/id (:db/id payment) - :payment/status :payment-status/pending} - [:upsert-transaction - {:db/id (:db/id transaction) - :transaction/approval-status :transaction-approval-status/unapproved - :transaction/payment nil - :transaction/vendor nil - :transaction/location nil - :transaction/accounts nil}]] - (:identity request)))) + (when (not= :payment-status/cleared (-> payment :payment/status)) + (throw (ex-info "Payment can't be undone because it isn't cleared." + {:validation-error "Payment can't be undone because it isn't cleared."}))) - (solr/touch-with-ledger (:db/id transaction)) - (html-response (fc/with-field :step-params (payment-matches-view request)) - :headers {"hx-trigger" "unlinked"})))) + (let [is-autopay-payment? (some->> (dc/q {:find ['?sp] + :in ['$ '?payment] + :where ['[?ip :invoice-payment/payment ?payment] + '[?ip :invoice-payment/invoice ?i] + '[(get-else $ ?i :invoice/scheduled-payment "N/A") ?sp]]} + (dc/db conn) (:db/id payment)) + seq + (map first) + (every? #(instance? java.util.Date %)))] + (if is-autopay-payment? + (audit-transact + (-> [{:db/id (:db/id payment) + :payment/status :payment-status/pending} + [:upsert-transaction + {:db/id (:db/id transaction) + :transaction/approval-status :transaction-approval-status/unapproved + :transaction/payment nil + :transaction/vendor nil + :transaction/location nil + :transaction/accounts nil}] + [:db/retractEntity (:db/id payment)]] + (into (map (fn [[invoice-payment]] + [:db/retractEntity invoice-payment]) + (dc/q {:find ['?ip] + :in ['$ '?p] + :where ['[?ip :invoice-payment/payment ?p]]} + (dc/db conn) + (:db/id payment))))) + (:identity request)) + (audit-transact + [{:db/id (:db/id payment) + :payment/status :payment-status/pending} + [:upsert-transaction + {:db/id (:db/id transaction) + :transaction/approval-status :transaction-approval-status/unapproved + :transaction/payment nil + :transaction/vendor nil + :transaction/location nil + :transaction/accounts nil}]] + (:identity request)))) + (solr/touch-with-ledger (:db/id transaction)) + (html-response (fc/with-field :step-params (payment-matches-view request)) + :headers {"hx-trigger" "unlinked"})))) (defrecord EditWizard [_ current-step] mm/LinearModalWizard @@ -1173,7 +1230,7 @@ (mm/get-step this :basic-details))) (render-wizard [this {:keys [multi-form-state] :as request}] (println "HERE XYZ" (:form-errors request)) - (clojure.pprint/pprint (:snapshot multi-form-state) ) + (clojure.pprint/pprint (:snapshot multi-form-state)) (mm/default-render-wizard this request :form-params @@ -1183,7 +1240,7 @@ :render-timeline? false)) (steps [_] [:basic-details - :links ]) + :links]) (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] @@ -1193,31 +1250,30 @@ (form-schema [_] edit-form-schema) (submit [this {:keys [multi-form-state request-method identity] :as request}] - (save-handler request) - )) + (save-handler request))) (def edit-wizard (->EditWizard nil nil)) (defn initial-edit-wizard-state [request] (let [tx-id (-> request :route-params :db/id) - entity (dc/pull (dc/db conn) - '[:db/id - :transaction/vendor - :transaction/client - :transaction/description-original - :transaction/status - :transaction/type - :transaction/memo - { [ :transaction/approval-status :xform iol-ion.query/ident] [:db/ident]} - :transaction/amount - :transaction/accounts] - tx-id) + entity (dc/pull (dc/db conn) + '[:db/id + :transaction/vendor + :transaction/client + :transaction/description-original + :transaction/status + :transaction/type + :transaction/memo + {[:transaction/approval-status :xform iol-ion.query/ident] [:db/ident]} + :transaction/amount + :transaction/accounts] + tx-id) entity (-> entity - (update :transaction/vendor :db/id) - (update :transaction/client :db/id))] + (update :transaction/vendor :db/id) + (update :transaction/client :db/id))] (mm/->MultiStepFormState entity - [] - entity))) + [] + entity))) (def key->handler (apply-middleware-to-all-handlers @@ -1250,18 +1306,26 @@ ::route/account-balance (-> account-balance (mm/wrap-wizard edit-wizard) (mm/wrap-decode-multi-form-state)) + ::route/toggle-amount-mode (-> toggle-amount-mode + (mm/wrap-wizard edit-wizard) + (mm/wrap-decode-multi-form-state)) ::route/edit-wizard-new-account (-> (add-new-entity-handler [:step-params :transaction/accounts] (fn render [cursor request] - (transaction-account-row* - {:value cursor - :client-id (:client-id (:query-params request))})) + (let [snapshot (-> request :multi-form-state :snapshot) + amount-mode (or (:amount-mode snapshot) "$") + total (Math/abs (:transaction/amount snapshot))] + (transaction-account-row* + {:value cursor + :client-id (:client-id (:query-params request)) + :amount-mode amount-mode + :total total}))) (fn build-new-row [base _] (assoc base :transaction-account/location "Shared"))) (wrap-schema-enforce :query-schema [:map [:client-id {:optional true} [:maybe entity-id]]])) - + ::route/unlink-payment (-> unlink-payment (wrap-must {:activity :edit :subject :transaction} (fn get-client [request] (-> request :entity :transaction/client))) (wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read) diff --git a/src/cljc/auto_ap/routes/transactions.cljc b/src/cljc/auto_ap/routes/transactions.cljc index eec3c98f..f90fe6fa 100644 --- a/src/cljc/auto_ap/routes/transactions.cljc +++ b/src/cljc/auto_ap/routes/transactions.cljc @@ -29,9 +29,10 @@ } } "/edit-submit" ::edit-submit "/location-select" ::location-select - "/account-total" ::account-total - "/account-balance" ::account-balance - "/edit-wizard-new-account" ::edit-wizard-new-account + "/account-total" ::account-total + "/account-balance" ::account-balance + "/toggle-amount-mode" ::toggle-amount-mode + "/edit-wizard-new-account" ::edit-wizard-new-account "/match-payment" ::link-payment "/match-autopay-invoices" ::link-autopay-invoices "/match-unpaid-invoices" ::link-unpaid-invoices