progress on bulk activities
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,457 +1,128 @@
|
|||||||
(ns auto-ap.ssr.transaction
|
(ns auto-ap.ssr.transaction
|
||||||
(:require
|
(:require
|
||||||
[auto-ap.datomic
|
[auto-ap.datomic
|
||||||
:refer [add-sorter-fields apply-pagination apply-sort-4 conn
|
:refer [audit-transact audit-transact-batch conn pull-attr
|
||||||
merge-query observable-query pull-many]]
|
pull-many]]
|
||||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
|
||||||
[auto-ap.logging :as alog]
|
[auto-ap.logging :as alog]
|
||||||
[auto-ap.permissions :refer [wrap-must]]
|
[auto-ap.permissions :refer [wrap-must]]
|
||||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||||
[auto-ap.routes.invoice :as invoice-routes]
|
|
||||||
[auto-ap.routes.ledger :as ledger-routes]
|
|
||||||
[auto-ap.routes.payments :as payment-routes]
|
|
||||||
[auto-ap.routes.transactions :as route]
|
[auto-ap.routes.transactions :as route]
|
||||||
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated]]
|
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated]]
|
||||||
|
[auto-ap.rule-matching :as rm]
|
||||||
[auto-ap.ssr-routes :as ssr-routes]
|
[auto-ap.ssr-routes :as ssr-routes]
|
||||||
[auto-ap.ssr.components :as com]
|
[auto-ap.ssr.components :as com]
|
||||||
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
|
[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]]
|
||||||
[auto-ap.ssr.hx :as hx]
|
|
||||||
[auto-ap.ssr.ledger :refer [wrap-ensure-bank-account-belongs]]
|
[auto-ap.ssr.ledger :refer [wrap-ensure-bank-account-belongs]]
|
||||||
[auto-ap.ssr.pos.common :refer [date-range-field*]]
|
[auto-ap.ssr.transaction.bulk-code :as bulk-code :refer [all-ids-not-locked]]
|
||||||
[auto-ap.ssr.svg :as svg]
|
[auto-ap.ssr.transaction.common :refer [bank-account-filter* fetch-ids
|
||||||
[auto-ap.ssr.transaction.edit :as edit]
|
grid-page query-schema
|
||||||
|
wrap-status-from-source]]
|
||||||
|
[auto-ap.ssr.transaction.edit :as edit :refer [transaction-account-row*]]
|
||||||
[auto-ap.ssr.utils
|
[auto-ap.ssr.utils
|
||||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
:refer [apply-middleware-to-all-handlers entity-id html-response
|
||||||
entity-id html-response strip wrap-implied-route-param
|
many-entity modal-response percentage ref->enum-schema
|
||||||
wrap-merge-prior-hx wrap-schema-enforce]]
|
wrap-implied-route-param wrap-merge-prior-hx
|
||||||
[auto-ap.time :as atime]
|
wrap-schema-enforce]]
|
||||||
[bidi.bidi :as bidi]
|
[bidi.bidi :as bidi]
|
||||||
[clj-time.coerce :as coerce]
|
|
||||||
[clj-time.core :as time]
|
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[datomic.api :as dc]
|
[datomic.api :as dc]
|
||||||
[hiccup.util :as hu]
|
[iol-ion.tx :refer [random-tempid]]
|
||||||
[malli.core :as mc]))
|
[malli.core :as mc]))
|
||||||
|
|
||||||
(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"}
|
|
||||||
(com/hidden {:name "exact-match-id"
|
|
||||||
"x-model" "exact_match"})
|
|
||||||
(com/pill {:color :primary}
|
|
||||||
[:span.inline-flex.space-x-2.items-center
|
|
||||||
[:div "exact match"]
|
|
||||||
[:div.w-3.h-3
|
|
||||||
(com/link {"@click" "exact_match=null; $nextTick(() => $dispatch('change'))"}
|
|
||||||
svg/x)]])]
|
|
||||||
[:div {:id "exact-match-id-tag"}]))
|
|
||||||
|
|
||||||
(defn bank-account-filter* [request]
|
|
||||||
[:div {:hx-trigger "clientSelected from:body"
|
|
||||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/bank-account-filter)
|
|
||||||
:hx-target "this"
|
|
||||||
:hx-swap "outerHTML"}
|
|
||||||
(when (:client request)
|
|
||||||
(let [bank-account-belongs-to-client? (get (set (map :db/id (:client/bank-accounts (:client request))))
|
|
||||||
(:db/id (:bank-account (:query-params request))))]
|
|
||||||
(com/field {:label "Bank Account"}
|
|
||||||
(com/radio-card {:size :small
|
|
||||||
:name "bank-account"
|
|
||||||
:value (or (when bank-account-belongs-to-client?
|
|
||||||
(:db/id (:bank-account (:query-params request))))
|
|
||||||
"")
|
|
||||||
:options
|
|
||||||
(into [{:value ""
|
|
||||||
:content "All"}]
|
|
||||||
(for [ba (:client/bank-accounts (:client request))]
|
|
||||||
{:value (:db/id ba)
|
|
||||||
:content (:bank-account/name ba)}))}))))])
|
|
||||||
|
|
||||||
(defn bank-account-filter [request]
|
(defn bank-account-filter [request]
|
||||||
(html-response (bank-account-filter* request)))
|
(html-response (bank-account-filter* request)))
|
||||||
|
|
||||||
(defn filters [request]
|
|
||||||
[:form#transaction-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
|
||||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
|
||||||
::route/table)
|
|
||||||
"hx-target" "#entity-table"
|
|
||||||
"hx-indicator" "#entity-table"}
|
|
||||||
|
|
||||||
(com/hidden {:name "status"
|
|
||||||
:value (some-> (:status (:query-params request)) name)})
|
|
||||||
[:fieldset.space-y-6
|
|
||||||
(com/field {:label "Vendor"}
|
|
||||||
(com/typeahead {:name "vendor"
|
|
||||||
:id "vendor"
|
|
||||||
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
|
||||||
:value (:vendor (:query-params request))
|
|
||||||
:value-fn :db/id
|
|
||||||
:content-fn :vendor/name}))
|
|
||||||
(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 "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})])
|
|
||||||
(exact-match-id* request)]])
|
|
||||||
|
|
||||||
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
|
|
||||||
(alog/peek ::ROUTE_PARAMS route-params)
|
|
||||||
(let [valid-clients (extract-client-ids (:clients request)
|
|
||||||
(:client-id request)
|
|
||||||
(:client-id query-params)
|
|
||||||
(when (:client-code request)
|
|
||||||
[:client/code (:client-code request)]))
|
|
||||||
args query-params
|
|
||||||
query
|
|
||||||
(if (:exact-match-id args)
|
|
||||||
{:query {:find '[?e]
|
|
||||||
:in '[$ ?e [?c ...]]
|
|
||||||
:where '[[?e :transaction/client ?c]]}
|
|
||||||
:args [db
|
|
||||||
(:exact-match-id args)
|
|
||||||
valid-clients]}
|
|
||||||
(cond-> {:query {:find []
|
|
||||||
:in ['$ '[?clients ?start ?end]]
|
|
||||||
:where '[[(iol-ion.query/scan-transactions $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]}
|
|
||||||
:args [db
|
|
||||||
[valid-clients
|
|
||||||
(some-> (:start-date query-params) coerce/to-date)
|
|
||||||
(some-> (:end-date query-params) coerce/to-date)]]}
|
|
||||||
|
|
||||||
(seq (:description args))
|
|
||||||
(merge-query {:query {:in ['?description]
|
|
||||||
:where ['[?e :transaction/description-original ?do]
|
|
||||||
'[(clojure.string/lower-case ?do) ?do2]
|
|
||||||
'[(.contains ?do2 ?description)]]}
|
|
||||||
:args [(str/lower-case (:description args))]})
|
|
||||||
|
|
||||||
(:amount-gte args)
|
|
||||||
(merge-query {:query {:in ['?amount-gte]
|
|
||||||
:where ['[?e :transaction/amount ?a]
|
|
||||||
'[(>= ?a ?amount-gte)]]}
|
|
||||||
:args [(:amount-gte args)]})
|
|
||||||
|
|
||||||
(:amount-lte args)
|
|
||||||
(merge-query {:query {:in ['?amount-lte]
|
|
||||||
:where ['[?e :transaction/amount ?a]
|
|
||||||
'[(<= ?a ?amount-lte)]]}
|
|
||||||
:args [(:amount-lte args)]})
|
|
||||||
|
|
||||||
(:db/id (:bank-account args))
|
|
||||||
(merge-query {:query {:in ['?ba]
|
|
||||||
:where ['[?e :transaction/bank-account ?ba]]}
|
|
||||||
:args [(:db/id (:bank-account args))]})
|
|
||||||
|
|
||||||
(:vendor args)
|
|
||||||
(merge-query {:query {:in ['?vendor-id]
|
|
||||||
:where ['[?e :transaction/vendor ?vendor-id]]}
|
|
||||||
:args [(:db/id (:vendor args))]})
|
|
||||||
(:import-batch-id args)
|
|
||||||
(merge-query {:query {:in ['?import-batch-id]
|
|
||||||
:where ['[?import-batch-id :import-batch/entry ?e]]}
|
|
||||||
:args [(:import-batch-id args)]})
|
|
||||||
|
|
||||||
(:status route-params)
|
|
||||||
(merge-query {:query {:in ['?status]
|
|
||||||
:where ['[?e :transaction/approval-status ?status]]}
|
|
||||||
:args [(:status route-params)]})
|
|
||||||
|
|
||||||
(:sort args) (add-sorter-fields {"client" ['[?e :transaction/client ?c]
|
|
||||||
'[?c :client/name ?sort-client]]
|
|
||||||
"vendor" '[(or-join [?e ?sort-vendor]
|
|
||||||
(and
|
|
||||||
[?e :transaction/vendor ?v]
|
|
||||||
[?v :vendor/name ?sort-vendor])
|
|
||||||
(and [(missing? $ ?e :transaction/vendor)]
|
|
||||||
[(ground "") ?sort-vendor]))]
|
|
||||||
"date" ['[?e :transaction/date ?sort-date]]
|
|
||||||
"amount" ['[?e :transaction/amount ?sort-amount]]
|
|
||||||
"description" ['[?e :transaction/description-original ?sort-description]]}
|
|
||||||
args)
|
|
||||||
|
|
||||||
true
|
|
||||||
(merge-query {:query {:find ['?sort-default '?e]}})))]
|
|
||||||
|
|
||||||
(->> (observable-query query)
|
|
||||||
(apply-sort-4 (assoc query-params :default-asc? true))
|
|
||||||
(apply-pagination query-params))))
|
|
||||||
|
|
||||||
(def default-read
|
|
||||||
'[:transaction/amount
|
|
||||||
:transaction/description-original
|
|
||||||
:transaction/description-simple
|
|
||||||
[ :transaction/date :xform clj-time.coerce/from-date]
|
|
||||||
[ :transaction/post-date :xform clj-time.coerce/from-date]
|
|
||||||
:transaction/type
|
|
||||||
:transaction/status
|
|
||||||
:transaction/client-overrides
|
|
||||||
:db/id
|
|
||||||
{:transaction/vendor [:vendor/name :db/id]
|
|
||||||
:transaction/client [:client/name :client/code :db/id [ :client/locked-until :xform clj-time.coerce/from-date]]
|
|
||||||
:transaction/bank-account [:bank-account/numeric-code :bank-account/name]
|
|
||||||
:transaction/accounts [{:transaction-account/account [:account/name :db/id]}
|
|
||||||
:transaction-account/location
|
|
||||||
:transaction-account/amount]
|
|
||||||
:transaction/matched-rule [:matched-rule/name]
|
|
||||||
:transaction/payment [:db/id [:payment/date :xform clj-time.coerce/from-date]]}])
|
|
||||||
|
|
||||||
(defn hydrate-results [ids db _]
|
|
||||||
(let [results (->> (pull-many db default-read ids)
|
|
||||||
(group-by :db/id))
|
|
||||||
results (->> ids
|
|
||||||
(map results)
|
|
||||||
(map first))]
|
|
||||||
results))
|
|
||||||
|
|
||||||
(defn sum-amount [ids]
|
|
||||||
(->>
|
|
||||||
(dc/q {:find ['?id '?a]
|
|
||||||
:in ['$ '[?id ...]]
|
|
||||||
:where ['[?id :transaction/amount ?a]]}
|
|
||||||
(dc/db conn)
|
|
||||||
ids)
|
|
||||||
(map last)
|
|
||||||
(reduce + 0.0)))
|
|
||||||
|
|
||||||
(defn fetch-page [request]
|
|
||||||
(let [db (dc/db conn)
|
|
||||||
{ids-to-retrieve :ids matching-count :count
|
|
||||||
all-ids :all-ids} (fetch-ids db request)]
|
|
||||||
|
|
||||||
[(->> (hydrate-results ids-to-retrieve db request))
|
|
||||||
matching-count
|
|
||||||
(sum-amount all-ids)]))
|
|
||||||
|
|
||||||
(def query-schema (mc/schema
|
|
||||||
[:maybe [:map {:date-range [:date-range :start-date :end-date]}
|
|
||||||
[:sort {:optional true} [:maybe [:any]]]
|
|
||||||
[:per-page {:optional true :default 25} [:maybe :int]]
|
|
||||||
[:start {:optional true :default 0} [:maybe :int]]
|
|
||||||
[:amount-gte {:optional true} [:maybe :double]]
|
|
||||||
[:amount-lte {:optional true} [:maybe :double]]
|
|
||||||
[:client-id {:optional true} [:maybe entity-id]]
|
|
||||||
[:import-batch-id {:optional true} [:maybe entity-id]]
|
|
||||||
[: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]}]]]
|
|
||||||
#_[: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}
|
|
||||||
entity-id]]]
|
|
||||||
[:start-date {:optional true}
|
|
||||||
[:maybe clj-date-schema]]
|
|
||||||
[:end-date {:optional true}
|
|
||||||
[:maybe clj-date-schema]]]]))
|
|
||||||
|
|
||||||
|
|
||||||
(def grid-page
|
|
||||||
(helper/build {:id "entity-table"
|
|
||||||
:nav com/main-aside-nav
|
|
||||||
:check-boxes? true
|
|
||||||
: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)])
|
|
||||||
:action-buttons (fn [request]
|
|
||||||
[#_(com/button {:color :primary
|
|
||||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
|
||||||
::route/new)}
|
|
||||||
"Add Transaction")])
|
|
||||||
:row-buttons (fn [request entity]
|
|
||||||
(let [client (:transaction/client entity)
|
|
||||||
locked-until (:client/locked-until client)
|
|
||||||
tx-date (:transaction/date entity)
|
|
||||||
is-locked (and locked-until tx-date (time/before? tx-date locked-until))]
|
|
||||||
(if is-locked
|
|
||||||
[ [:div.p-3.rounded-full.bg-gray-50.text-gray-400.w-6.h-6.box-content
|
|
||||||
svg/lock]]
|
|
||||||
[(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
|
|
||||||
::route/edit-wizard
|
|
||||||
:db/id (:db/id entity))}
|
|
||||||
svg/pencil)])))
|
|
||||||
|
|
||||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
|
||||||
"Transactions"]]
|
|
||||||
:title (fn [r]
|
|
||||||
"Transaction")
|
|
||||||
:entity-name "register"
|
|
||||||
:route ::route/table
|
|
||||||
:csv-route ::route/csv
|
|
||||||
:table-attributes (fn [_]
|
|
||||||
{:hx-trigger "refreshTable from:body"
|
|
||||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/table)
|
|
||||||
:hx-target "#entity-table"})
|
|
||||||
:break-table (fn [request entity]
|
|
||||||
(cond
|
|
||||||
(= (-> request :query-params :sort first :name) "Vendor")
|
|
||||||
(or (-> entity :transaction/vendor :vendor/name)
|
|
||||||
"No vendor")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
:else nil))
|
|
||||||
:page->csv-entities (fn [[transactions]]
|
|
||||||
transactions)
|
|
||||||
:headers [{:key "id"
|
|
||||||
:name "Id"
|
|
||||||
:render-csv :db/id
|
|
||||||
:render-for #{:csv}}
|
|
||||||
{:key "client"
|
|
||||||
:name "Client"
|
|
||||||
:sort-key "client"
|
|
||||||
:hide? (fn [args]
|
|
||||||
(and (= (count (:clients args)) 1)
|
|
||||||
(= 1 (count (:client/locations (:client args))))))
|
|
||||||
:render (fn [x] [:div.flex.items-center.gap-2 (-> x :transaction/client :client/name)])
|
|
||||||
:render-csv (fn [x] (-> x :transaction/client :client/name))}
|
|
||||||
{:key "vendor"
|
|
||||||
:name "Vendor"
|
|
||||||
:sort-key "vendor"
|
|
||||||
:render (fn [e]
|
|
||||||
#_(alog/peek :vend e)
|
|
||||||
(or
|
|
||||||
(-> e :transaction/vendor :vendor/name)
|
|
||||||
[:span.italic.text-gray-400 (-> e :transaction/description-simple)]))
|
|
||||||
:render-csv (fn [e] (or (-> e :transaction/vendor :vendor/name)
|
|
||||||
(-> e :transaction/description-simple)))}
|
|
||||||
{:key "description"
|
|
||||||
:name "Description"
|
|
||||||
:sort-key "description"
|
|
||||||
:render :transaction/description-original
|
|
||||||
:render-csv :transaction/description-original}
|
|
||||||
{:key "date"
|
|
||||||
:sort-key "date"
|
|
||||||
:name "Date"
|
|
||||||
:show-starting "lg"
|
|
||||||
:render (fn [{:transaction/keys [date]}]
|
|
||||||
(some-> date (atime/unparse-local atime/normal-date)))}
|
|
||||||
{:key "amount"
|
|
||||||
:name "Amount"
|
|
||||||
:sort-key "amount"
|
|
||||||
:class "text-right"
|
|
||||||
:render #(format "$%,.2f" (:transaction/amount %))
|
|
||||||
:render-csv :transaction/amount}
|
|
||||||
{:key "links"
|
|
||||||
:name "Links"
|
|
||||||
:show-starting "lg"
|
|
||||||
:class "w-8"
|
|
||||||
:render (fn [i]
|
|
||||||
(let [db (dc/db conn)
|
|
||||||
journal-entries (when (:db/id i)
|
|
||||||
(dc/q '[:find (pull ?je [:db/id :journal-entry/id])
|
|
||||||
:in $ ?t-id
|
|
||||||
:where
|
|
||||||
[?je :journal-entry/original-entity ?t-id]]
|
|
||||||
db
|
|
||||||
(:db/id i)))
|
|
||||||
linked-invoices (when (and (:db/id i) (:transaction/payment i))
|
|
||||||
(dc/q '[:find (pull ?inv [:db/id :invoice/invoice-number :invoice/total])
|
|
||||||
:in $ ?payment-id
|
|
||||||
:where
|
|
||||||
[?ip :invoice-payment/payment ?payment-id]
|
|
||||||
[?ip :invoice-payment/invoice ?inv]]
|
|
||||||
db
|
|
||||||
(:db/id (:transaction/payment i))))]
|
|
||||||
(link-dropdown
|
|
||||||
(cond-> []
|
|
||||||
;; Payment link
|
|
||||||
(:transaction/payment i)
|
|
||||||
(conj
|
|
||||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
|
||||||
::payment-routes/all-page)
|
|
||||||
{:exact-match-id (:db/id (:transaction/payment i))})
|
|
||||||
:color :primary
|
|
||||||
:content (format "Payment '%s'" (-> i :transaction/payment :payment/date (atime/unparse-local atime/normal-date)))})
|
|
||||||
|
|
||||||
;; Journal entry links
|
|
||||||
(seq journal-entries)
|
|
||||||
(concat
|
|
||||||
(for [[je] journal-entries]
|
|
||||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page)
|
|
||||||
{:exact-match-id (:db/id je)})
|
|
||||||
:color :yellow
|
|
||||||
:content "Ledger entry"}))
|
|
||||||
|
|
||||||
;; Invoice links
|
|
||||||
(seq linked-invoices)
|
|
||||||
(concat
|
|
||||||
(for [[inv] linked-invoices]
|
|
||||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
|
||||||
::invoice-routes/all-page)
|
|
||||||
{:exact-match-id (:db/id inv)})
|
|
||||||
:color :secondary
|
|
||||||
:content (format "Invoice '%s'" (:invoice/invoice-number inv))}))
|
|
||||||
|
|
||||||
))))
|
|
||||||
:render-for #{:html}}]}))
|
|
||||||
|
|
||||||
(def row* (partial helper/row* grid-page))
|
(def row* (partial helper/row* grid-page))
|
||||||
|
|
||||||
;; Handlers
|
;; Handlers
|
||||||
|
|
||||||
|
|
||||||
(def page (helper/page-route grid-page))
|
(def page (helper/page-route grid-page))
|
||||||
|
|
||||||
(defn wrap-status-from-source [handler]
|
|
||||||
(fn [{:keys [matched-current-page-route] :as request}]
|
|
||||||
(let [ request (cond-> request
|
|
||||||
(= ::route/unapproved-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/unapproved)
|
|
||||||
(= ::route/approved-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/approved)
|
|
||||||
(= ::route/requires-feedback-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/requires-feedback)
|
|
||||||
(= ::route/page matched-current-page-route) (assoc-in [:route-params :status] nil))]
|
|
||||||
(handler request))))
|
|
||||||
(def table (helper/table-route grid-page))
|
(def table (helper/table-route grid-page))
|
||||||
|
|
||||||
(def csv (helper/csv-route grid-page))
|
(def csv (helper/csv-route grid-page))
|
||||||
|
|
||||||
|
;; Bulk action handlers
|
||||||
|
(defn bulk-delete [request]
|
||||||
|
(let [all-selected (:all-selected (:form-params request))
|
||||||
|
suppress (:suppress (:form-params request))
|
||||||
|
selected (:selected (:form-params request))
|
||||||
|
_ (alog/info ::selected-and-suppress :qp (:form-params request))
|
||||||
|
ids (cond
|
||||||
|
all-selected
|
||||||
|
(:ids (fetch-ids (dc/db conn) (-> request
|
||||||
|
(assoc-in [:form-params :start] 0)
|
||||||
|
(assoc-in [:form-params :per-page] 250))))
|
||||||
|
:else
|
||||||
|
selected)
|
||||||
|
all-ids (all-ids-not-locked ids)
|
||||||
|
db (dc/db conn)]
|
||||||
|
|
||||||
|
(alog/info ::bulk-delete-transactions
|
||||||
|
:count (count all-ids)
|
||||||
|
:sample (take 3 all-ids))
|
||||||
|
|
||||||
|
;; First retract journal entries and handle payment relationships
|
||||||
|
(audit-transact
|
||||||
|
(mapcat (fn [i]
|
||||||
|
(let [transaction (dc/pull db [:transaction/payment
|
||||||
|
:transaction/expected-deposit
|
||||||
|
:db/id] i)
|
||||||
|
payment-id (-> transaction :transaction/payment :db/id)
|
||||||
|
expected-deposit-id (-> transaction :transaction/expected-deposit :db/id)]
|
||||||
|
(cond->> [[:db/retractEntity [:journal-entry/original-entity i]]]
|
||||||
|
payment-id (into [{:db/id payment-id
|
||||||
|
:payment/status :payment-status/pending}
|
||||||
|
[:db/retract (:db/id transaction) :transaction/payment payment-id]])
|
||||||
|
expected-deposit-id (into [{:db/id expected-deposit-id
|
||||||
|
:expected-deposit/status :expected-deposit-status/pending}
|
||||||
|
[:db/retract (:db/id transaction) :transaction/expected-deposit expected-deposit-id]]))))
|
||||||
|
all-ids)
|
||||||
|
(:identity request))
|
||||||
|
|
||||||
|
;; Then retract or suppress the transactions
|
||||||
|
(audit-transact
|
||||||
|
(mapcat (fn [i]
|
||||||
|
(let [transaction-tx (if suppress
|
||||||
|
{:db/id i
|
||||||
|
:transaction/approval-status :transaction-approval-status/suppressed}
|
||||||
|
[:db/retractEntity i])]
|
||||||
|
[transaction-tx
|
||||||
|
[:db/retractEntity [:journal-entry/original-entity i]]]))
|
||||||
|
all-ids)
|
||||||
|
(:identity request))
|
||||||
|
|
||||||
|
(html-response
|
||||||
|
(com/success-modal {:title "Transactions Updated"}
|
||||||
|
[:p (str "Successfully " (if suppress "suppressed" "deleted") " " (count all-ids) " transactions.")])
|
||||||
|
:headers {"hx-trigger" "invalidated"})))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(def key->handler
|
(def key->handler
|
||||||
(merge edit/key->handler
|
(merge edit/key->handler
|
||||||
(apply-middleware-to-all-handlers
|
bulk-code/key->handler
|
||||||
{::route/page page
|
(apply-middleware-to-all-handlers
|
||||||
::route/approved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/approved))
|
{::route/page page
|
||||||
::route/unapproved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/unapproved))
|
::route/approved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/approved))
|
||||||
::route/requires-feedback-page (-> page (wrap-implied-route-param :status :transaction-approval-status/requires-feedback))
|
::route/unapproved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/unapproved))
|
||||||
::route/table table
|
::route/requires-feedback-page (-> page (wrap-implied-route-param :status :transaction-approval-status/requires-feedback))
|
||||||
::route/csv csv
|
::route/table table
|
||||||
::route/bank-account-filter bank-account-filter}
|
::route/csv csv
|
||||||
(fn [h]
|
::route/bank-account-filter bank-account-filter
|
||||||
(-> h
|
::route/bulk-delete (-> bulk-delete
|
||||||
(wrap-copy-qp-pqp)
|
(wrap-schema-enforce :form-schema query-schema))}
|
||||||
(wrap-apply-sort grid-page)
|
(fn [h]
|
||||||
(wrap-ensure-bank-account-belongs)
|
(-> h
|
||||||
(wrap-status-from-source)
|
(wrap-copy-qp-pqp)
|
||||||
(wrap-merge-prior-hx)
|
(wrap-apply-sort grid-page)
|
||||||
(wrap-schema-enforce :query-schema query-schema)
|
(wrap-ensure-bank-account-belongs)
|
||||||
(wrap-schema-enforce :hx-schema query-schema)
|
(wrap-status-from-source)
|
||||||
(wrap-must {:activity :view :subject :transaction})
|
(wrap-merge-prior-hx)
|
||||||
(wrap-client-redirect-unauthenticated))))))
|
(wrap-schema-enforce :query-schema query-schema)
|
||||||
|
(wrap-schema-enforce :hx-schema query-schema)
|
||||||
|
(wrap-must {:activity :view :subject :transaction})
|
||||||
|
(wrap-client-redirect-unauthenticated))))))
|
||||||
348
src/clj/auto_ap/ssr/transaction/bulk_code.clj
Normal file
348
src/clj/auto_ap/ssr/transaction/bulk_code.clj
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
(ns auto-ap.ssr.transaction.bulk-code
|
||||||
|
(:require
|
||||||
|
[auto-ap.datomic
|
||||||
|
:refer [audit-transact-batch conn pull-attr pull-many]]
|
||||||
|
[auto-ap.logging :as alog]
|
||||||
|
[auto-ap.permissions :refer [wrap-must]]
|
||||||
|
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||||
|
[auto-ap.routes.transactions :as route]
|
||||||
|
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated]]
|
||||||
|
[auto-ap.rule-matching :as rm]
|
||||||
|
[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.components.multi-modal :as mm :refer [wrap-wizard]]
|
||||||
|
[auto-ap.ssr.form-cursor :as fc]
|
||||||
|
[auto-ap.ssr.grid-page-helper :refer [wrap-apply-sort]]
|
||||||
|
[auto-ap.ssr.hx :as hx]
|
||||||
|
[auto-ap.ssr.ledger :refer [wrap-ensure-bank-account-belongs]]
|
||||||
|
[auto-ap.ssr.svg :as svg]
|
||||||
|
[auto-ap.ssr.transaction.common :refer [grid-page query-schema
|
||||||
|
selected->ids
|
||||||
|
wrap-status-from-source]]
|
||||||
|
[auto-ap.ssr.transaction.edit :as edit :refer [account-typeahead*
|
||||||
|
location-select*]]
|
||||||
|
[auto-ap.ssr.utils
|
||||||
|
:refer [apply-middleware-to-all-handlers entity-id
|
||||||
|
form-validation-error html-response percentage
|
||||||
|
ref->enum-schema wrap-merge-prior-hx wrap-schema-enforce]]
|
||||||
|
[bidi.bidi :as bidi]
|
||||||
|
[datomic.api :as dc]
|
||||||
|
[iol-ion.query :refer [dollars=]]
|
||||||
|
[iol-ion.tx :refer [random-tempid]]
|
||||||
|
[malli.core :as mc]))
|
||||||
|
|
||||||
|
(defn transaction-account-row* [{:keys [value client-id]}]
|
||||||
|
(com/data-grid-row
|
||||||
|
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value))))
|
||||||
|
:accountId (fc/field-value (:account value))})
|
||||||
|
:data-key "show"
|
||||||
|
:x-ref "p"}
|
||||||
|
hx/alpine-mount-then-appear)
|
||||||
|
(fc/with-field :db/id
|
||||||
|
(com/hidden {:name (fc/field-name)
|
||||||
|
:value (fc/field-value)}))
|
||||||
|
(fc/with-field :account
|
||||||
|
(com/data-grid-cell
|
||||||
|
{}
|
||||||
|
(com/validated-field
|
||||||
|
{:errors (fc/field-errors)}
|
||||||
|
(account-typeahead* {:value (fc/field-value)
|
||||||
|
:client-id client-id
|
||||||
|
:name (fc/field-name)
|
||||||
|
:x-model "accountId"}))))
|
||||||
|
(fc/with-field :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) }
|
||||||
|
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-target "find *"
|
||||||
|
:hx-swap "outerHTML"}
|
||||||
|
(location-select* {:name (fc/field-name)
|
||||||
|
:account-location (:account/location (cond->> (:account @value)
|
||||||
|
(nat-int? (:account @value)) (dc/pull (dc/db conn)
|
||||||
|
'[:account/location])))
|
||||||
|
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
|
||||||
|
:value (fc/field-value)}))))
|
||||||
|
(fc/with-field :percentage
|
||||||
|
(com/data-grid-cell
|
||||||
|
{}
|
||||||
|
(com/validated-field
|
||||||
|
{:errors (fc/field-errors)}
|
||||||
|
(com/money-input {:name (fc/field-name)
|
||||||
|
:class "w-16"
|
||||||
|
: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))))
|
||||||
|
|
||||||
|
(defn initial-bulk-edit-state [request]
|
||||||
|
(mm/->MultiStepFormState {:search-params (:query-params request)
|
||||||
|
:accounts []}
|
||||||
|
[]
|
||||||
|
{:search-params (:query-params request)
|
||||||
|
:accounts []}))
|
||||||
|
|
||||||
|
|
||||||
|
(defn all-ids-not-locked
|
||||||
|
"Filters transaction IDs to only those that aren't locked (client locked date earlier than transaction date)"
|
||||||
|
[all-ids]
|
||||||
|
(->> all-ids
|
||||||
|
(dc/q '[:find ?t
|
||||||
|
:in $ [?t ...]
|
||||||
|
:where
|
||||||
|
[?t :transaction/client ?c]
|
||||||
|
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
|
||||||
|
[?t :transaction/date ?d]
|
||||||
|
[(>= ?d ?lu)]]
|
||||||
|
(dc/db conn))
|
||||||
|
(map first)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(def bulk-code-schema
|
||||||
|
(mc/schema [:map
|
||||||
|
[:vendor {:optional true} [:maybe entity-id]]
|
||||||
|
[:approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")] ]
|
||||||
|
[:accounts {:optional true}
|
||||||
|
[:maybe
|
||||||
|
[:vector {:coerce? true}
|
||||||
|
[:map [:account entity-id]
|
||||||
|
[:location [:string {:min 1 :error/message "required"}]]
|
||||||
|
[:percentage percentage]]]] ]]))
|
||||||
|
|
||||||
|
|
||||||
|
(defn maybe-code-accounts [transaction account-rules valid-locations]
|
||||||
|
(with-precision 2
|
||||||
|
(let [accounts (vec (mapcat
|
||||||
|
(fn [ar]
|
||||||
|
(let [cents-to-distribute (int (Math/round (Math/abs (* (:percentage ar)
|
||||||
|
(:transaction/amount transaction)
|
||||||
|
100))))]
|
||||||
|
(if (= "Shared" (:location ar))
|
||||||
|
(->> valid-locations
|
||||||
|
(map
|
||||||
|
(fn [cents location]
|
||||||
|
{:db/id (random-tempid)
|
||||||
|
:transaction-account/account (:account ar)
|
||||||
|
:transaction-account/amount (* 0.01 cents)
|
||||||
|
:transaction-account/location location})
|
||||||
|
(rm/spread-cents cents-to-distribute (count valid-locations))))
|
||||||
|
[(cond-> {:db/id (random-tempid)
|
||||||
|
:transaction-account/account (:account ar)
|
||||||
|
:transaction-account/amount (* 0.01 cents-to-distribute)}
|
||||||
|
(:location ar) (assoc :transaction-account/location (:location ar)))])))
|
||||||
|
account-rules))
|
||||||
|
accounts (mapv
|
||||||
|
(fn [a]
|
||||||
|
(update a :transaction-account/amount
|
||||||
|
#(with-precision 2
|
||||||
|
(double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP)))))
|
||||||
|
accounts)
|
||||||
|
leftover (with-precision 2 (.round (bigdec (- (Math/abs (:transaction/amount transaction))
|
||||||
|
(Math/abs (reduce + 0.0 (map #(:transaction-account/amount %) accounts)))))
|
||||||
|
*math-context*))
|
||||||
|
accounts (if (seq accounts)
|
||||||
|
(update-in accounts [(dec (count accounts)) :transaction-account/amount] #(+ % (double leftover)))
|
||||||
|
[])]
|
||||||
|
accounts)))
|
||||||
|
|
||||||
|
(defrecord AccountsStep [linear-wizard]
|
||||||
|
mm/ModalWizardStep
|
||||||
|
(step-name [_]
|
||||||
|
"Bulk Code")
|
||||||
|
(step-key [_]
|
||||||
|
:accounts)
|
||||||
|
|
||||||
|
(edit-path [_ _]
|
||||||
|
[])
|
||||||
|
|
||||||
|
(step-schema [_]
|
||||||
|
(mm/form-schema linear-wizard))
|
||||||
|
|
||||||
|
(render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]
|
||||||
|
(let [_ (alog/peek ::SEARCH_PARAMS (:search-params snapshot))
|
||||||
|
selected-ids (selected->ids (assoc request :query-params (:search-params snapshot)) (:search-params snapshot))
|
||||||
|
all-ids (all-ids-not-locked selected-ids)]
|
||||||
|
(mm/default-render-step
|
||||||
|
linear-wizard this
|
||||||
|
:head [:div.p-2 "Bulk editing " (count all-ids) " transactions"]
|
||||||
|
:body (mm/default-step-body
|
||||||
|
{}
|
||||||
|
[:div
|
||||||
|
#_(com/hidden {:name "ids" :value (pr-str ids)})
|
||||||
|
|
||||||
|
[:div.space-y-4.p-4
|
||||||
|
[:div.grid.grid-cols-2.gap-4
|
||||||
|
|
||||||
|
;; Vendor field
|
||||||
|
[:div
|
||||||
|
(fc/with-field :vendor
|
||||||
|
(com/validated-field {:label "Vendor"
|
||||||
|
:errors (fc/field-errors)}
|
||||||
|
(com/typeahead {:name (fc/field-name)
|
||||||
|
:placeholder "Search for vendor..."
|
||||||
|
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||||
|
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})))]
|
||||||
|
|
||||||
|
;; Status field
|
||||||
|
[:div
|
||||||
|
(fc/with-field :approval-status
|
||||||
|
(com/validated-field {:label "Status"
|
||||||
|
:errors (fc/field-errors)}
|
||||||
|
(com/select {:name (fc/field-name)
|
||||||
|
:options [["" "No Change"]
|
||||||
|
["approved" "Approved"]
|
||||||
|
["unapproved" "Unapproved"]
|
||||||
|
["suppressed" "Suppressed"]
|
||||||
|
["requires_feedback" "Requires Feedback"]]})))]
|
||||||
|
|
||||||
|
;; Accounts section
|
||||||
|
[:div.col-span-2.pt-4
|
||||||
|
[:h3.text-lg.font-medium.mb-3 "Expense Accounts"]
|
||||||
|
|
||||||
|
[:div.space-y-3#account-entries
|
||||||
|
(fc/with-field :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 %}))
|
||||||
|
|
||||||
|
(com/data-grid-new-row {:colspan 4
|
||||||
|
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||||
|
::route/bulk-code-new-account)
|
||||||
|
:row-offset 0
|
||||||
|
:index (count (fc/field-value))}
|
||||||
|
"New account")
|
||||||
|
)))
|
||||||
|
|
||||||
|
;; Button to add more accounts
|
||||||
|
]]]]])
|
||||||
|
:footer
|
||||||
|
(mm/default-step-footer linear-wizard this :validation-route ::route/new-wizard-navigate
|
||||||
|
:next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Save"))
|
||||||
|
:validation-route ::route/new-wizard-navigate))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn assert-percentages-add-up [{:keys [accounts]}]
|
||||||
|
(let [account-total (reduce + 0 (map (fn [x] (:percentage x)) accounts))]
|
||||||
|
(when-not (dollars= 1.0 account-total)
|
||||||
|
(form-validation-error (str "Expense account total (" account-total ") does not equal 100%")))))
|
||||||
|
|
||||||
|
(defrecord BulkCodeWizard [_ current-step]
|
||||||
|
mm/LinearModalWizard
|
||||||
|
(hydrate-from-request
|
||||||
|
[this request]
|
||||||
|
this)
|
||||||
|
(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)))
|
||||||
|
(render-wizard [this {:keys [multi-form-state] :as request}]
|
||||||
|
(mm/default-render-wizard
|
||||||
|
this request
|
||||||
|
:form-params
|
||||||
|
(-> mm/default-form-props
|
||||||
|
(assoc :hx-put
|
||||||
|
(str (bidi/path-for ssr-routes/only-routes ::route/bulk-code-submit))))
|
||||||
|
:render-timeline? false))
|
||||||
|
(steps [_]
|
||||||
|
[:accounts])
|
||||||
|
(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)}
|
||||||
|
step-key)))
|
||||||
|
(form-schema [_]
|
||||||
|
bulk-code-schema)
|
||||||
|
(submit [this {:keys [multi-form-state request-method identity] :as request}]
|
||||||
|
(let [ 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 ids)
|
||||||
|
vendor (-> request :multi-form-state :snapshot :vendor)
|
||||||
|
approval-status (-> request :multi-form-state :snapshot :approval-status)
|
||||||
|
accounts (-> request :multi-form-state :snapshot :accounts) ]
|
||||||
|
(when (seq accounts)
|
||||||
|
(assert-percentages-add-up (:snapshot multi-form-state)))
|
||||||
|
(alog/peek ::ACCOUNTS (-> request :multi-form-state :snapshot))
|
||||||
|
|
||||||
|
;; Get transactions and filter for locked ones
|
||||||
|
(let [db (dc/db conn)
|
||||||
|
transactions (pull-many db [:db/id :transaction/amount {:transaction/client [:db/id]}] (vec all-ids))
|
||||||
|
|
||||||
|
;; Get client locations
|
||||||
|
client->locations (->> (map (comp :db/id :transaction/client) transactions)
|
||||||
|
(distinct)
|
||||||
|
(dc/q '[:find (pull ?e [:db/id :client/locations])
|
||||||
|
:in $ [?e ...]]
|
||||||
|
db)
|
||||||
|
(map (fn [[client]]
|
||||||
|
[(:db/id client) (:client/locations client)]))
|
||||||
|
(into {}))]
|
||||||
|
|
||||||
|
(audit-transact-batch
|
||||||
|
(map (fn [t]
|
||||||
|
(let [locations (client->locations (-> t :transaction/client :db/id))]
|
||||||
|
[:upsert-transaction (cond-> t
|
||||||
|
approval-status
|
||||||
|
(assoc :transaction/approval-status approval-status)
|
||||||
|
|
||||||
|
vendor
|
||||||
|
(assoc :transaction/vendor vendor)
|
||||||
|
|
||||||
|
(seq accounts)
|
||||||
|
(assoc :transaction/accounts
|
||||||
|
(maybe-code-accounts t accounts locations)))]))
|
||||||
|
transactions)
|
||||||
|
(:identity request))
|
||||||
|
|
||||||
|
;; Return success modal
|
||||||
|
(html-response
|
||||||
|
(com/success-modal {:title "Transactions Coded"}
|
||||||
|
[:p (str "Successfully coded " (count all-ids) " transactions.")])
|
||||||
|
:headers {"hx-trigger" "refreshTable"})))))
|
||||||
|
|
||||||
|
(def bulk-code-wizard (->BulkCodeWizard nil nil))
|
||||||
|
|
||||||
|
(def key->handler
|
||||||
|
(apply-middleware-to-all-handlers
|
||||||
|
{::route/bulk-code (-> mm/open-wizard-handler
|
||||||
|
(mm/wrap-wizard bulk-code-wizard)
|
||||||
|
(mm/wrap-init-multi-form-state initial-bulk-edit-state))
|
||||||
|
::route/bulk-code-new-account (->
|
||||||
|
(add-new-entity-handler [:step-params :accounts]
|
||||||
|
(fn render [cursor request]
|
||||||
|
(transaction-account-row*
|
||||||
|
{:value cursor}))
|
||||||
|
(fn build-new-row [base _]
|
||||||
|
(assoc base :location "Shared")))
|
||||||
|
(wrap-schema-enforce :query-schema [:map
|
||||||
|
[:client-id {:optional true}
|
||||||
|
[:maybe entity-id]]]))
|
||||||
|
::route/bulk-code-submit (-> mm/submit-handler
|
||||||
|
(wrap-wizard bulk-code-wizard)
|
||||||
|
(mm/wrap-decode-multi-form-state))}
|
||||||
|
(fn [h]
|
||||||
|
(-> h
|
||||||
|
(wrap-copy-qp-pqp)
|
||||||
|
(wrap-ensure-bank-account-belongs)
|
||||||
|
(wrap-status-from-source)
|
||||||
|
(wrap-apply-sort grid-page)
|
||||||
|
(wrap-merge-prior-hx)
|
||||||
|
(wrap-schema-enforce :query-schema query-schema)
|
||||||
|
(wrap-schema-enforce :hx-schema query-schema)
|
||||||
|
(wrap-must {:activity :bulk-code :subject :transaction})
|
||||||
|
(wrap-client-redirect-unauthenticated)))))
|
||||||
466
src/clj/auto_ap/ssr/transaction/common.clj
Normal file
466
src/clj/auto_ap/ssr/transaction/common.clj
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
(ns auto-ap.ssr.transaction.common
|
||||||
|
(:require
|
||||||
|
[auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-4
|
||||||
|
conn merge-query observable-query pull-many]]
|
||||||
|
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||||
|
[auto-ap.routes.invoice :as invoice-routes]
|
||||||
|
[auto-ap.routes.ledger :as ledger-routes]
|
||||||
|
[auto-ap.routes.payments :as payment-routes]
|
||||||
|
[auto-ap.routes.transactions :as route]
|
||||||
|
[auto-ap.ssr-routes :as ssr-routes]
|
||||||
|
[auto-ap.ssr.components :as com]
|
||||||
|
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
|
||||||
|
[auto-ap.ssr.grid-page-helper :as helper]
|
||||||
|
[auto-ap.ssr.hx :as hx]
|
||||||
|
[auto-ap.ssr.pos.common :refer [date-range-field*]]
|
||||||
|
[auto-ap.ssr.svg :as svg]
|
||||||
|
[auto-ap.ssr.utils :refer [clj-date-schema entity-id strip]]
|
||||||
|
[auto-ap.time :as atime]
|
||||||
|
[bidi.bidi :as bidi]
|
||||||
|
[clj-time.coerce :as coerce]
|
||||||
|
[clj-time.core :as time]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[datomic.api :as dc]
|
||||||
|
[hiccup.util :as hu]
|
||||||
|
[malli.core :as mc]))
|
||||||
|
|
||||||
|
(def query-schema (mc/schema
|
||||||
|
[:maybe [:map {:date-range [:date-range :start-date :end-date]}
|
||||||
|
[:sort {:optional true} [:maybe [:any]]]
|
||||||
|
[:per-page {:optional true :default 25} [:maybe :int]]
|
||||||
|
[:start {:optional true :default 0} [:maybe :int]]
|
||||||
|
[:amount-gte {:optional true} [:maybe :double]]
|
||||||
|
[:amount-lte {:optional true} [:maybe :double]]
|
||||||
|
[:client-id {:optional true} [:maybe entity-id]]
|
||||||
|
[:import-batch-id {:optional true} [:maybe entity-id]]
|
||||||
|
[: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]}]]]
|
||||||
|
#_[: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}
|
||||||
|
entity-id]]]
|
||||||
|
[:start-date {:optional true}
|
||||||
|
[:maybe clj-date-schema]]
|
||||||
|
[:end-date {:optional true}
|
||||||
|
[:maybe clj-date-schema]]]]))
|
||||||
|
|
||||||
|
(def default-read
|
||||||
|
'[:transaction/amount
|
||||||
|
:transaction/description-original
|
||||||
|
:transaction/description-simple
|
||||||
|
[ :transaction/date :xform clj-time.coerce/from-date]
|
||||||
|
[ :transaction/post-date :xform clj-time.coerce/from-date]
|
||||||
|
:transaction/type
|
||||||
|
:transaction/status
|
||||||
|
:transaction/client-overrides
|
||||||
|
:db/id
|
||||||
|
{:transaction/vendor [:vendor/name :db/id]
|
||||||
|
:transaction/client [:client/name :client/code :db/id [ :client/locked-until :xform clj-time.coerce/from-date]]
|
||||||
|
:transaction/bank-account [:bank-account/numeric-code :bank-account/name]
|
||||||
|
:transaction/accounts [{:transaction-account/account [:account/name :db/id]}
|
||||||
|
:transaction-account/location
|
||||||
|
:transaction-account/amount]
|
||||||
|
:transaction/matched-rule [:matched-rule/name]
|
||||||
|
:transaction/payment [:db/id [:payment/date :xform clj-time.coerce/from-date]]}])
|
||||||
|
|
||||||
|
(defn hydrate-results [ids db _]
|
||||||
|
(let [results (->> (pull-many db default-read ids)
|
||||||
|
(group-by :db/id))
|
||||||
|
results (->> ids
|
||||||
|
(map results)
|
||||||
|
(map first))]
|
||||||
|
results))
|
||||||
|
|
||||||
|
(defn sum-amount [ids]
|
||||||
|
(->>
|
||||||
|
(dc/q {:find ['?id '?a]
|
||||||
|
:in ['$ '[?id ...]]
|
||||||
|
:where ['[?id :transaction/amount ?a]]}
|
||||||
|
(dc/db conn)
|
||||||
|
ids)
|
||||||
|
(map last)
|
||||||
|
(reduce + 0.0)))
|
||||||
|
|
||||||
|
(defn all-ids-not-locked
|
||||||
|
"Filters transaction IDs to only those that aren't locked (client locked date earlier than transaction date)"
|
||||||
|
[all-ids]
|
||||||
|
(->> all-ids
|
||||||
|
(dc/q '[:find ?t
|
||||||
|
:in $ [?t ...]
|
||||||
|
:where
|
||||||
|
[?t :transaction/client ?c]
|
||||||
|
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
|
||||||
|
[?t :transaction/date ?d]
|
||||||
|
[(>= ?d ?lu)]]
|
||||||
|
(dc/db conn))
|
||||||
|
(map first)))
|
||||||
|
|
||||||
|
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
|
||||||
|
|
||||||
|
(let [valid-clients (extract-client-ids (:clients request)
|
||||||
|
(:client-id request)
|
||||||
|
(:client-id query-params)
|
||||||
|
(when (:client-code request)
|
||||||
|
[:client/code (:client-code request)]))
|
||||||
|
args query-params
|
||||||
|
query
|
||||||
|
(if (:exact-match-id args)
|
||||||
|
{:query {:find '[?e]
|
||||||
|
:in '[$ ?e [?c ...]]
|
||||||
|
:where '[[?e :transaction/client ?c]]}
|
||||||
|
:args [db
|
||||||
|
(:exact-match-id args)
|
||||||
|
valid-clients]}
|
||||||
|
(cond-> {:query {:find []
|
||||||
|
:in ['$ '[?clients ?start ?end]]
|
||||||
|
:where '[[(iol-ion.query/scan-transactions $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]}
|
||||||
|
:args [db
|
||||||
|
[valid-clients
|
||||||
|
(some-> (:start-date query-params) coerce/to-date)
|
||||||
|
(some-> (:end-date query-params) coerce/to-date)]]}
|
||||||
|
|
||||||
|
(seq (:description args))
|
||||||
|
(merge-query {:query {:in ['?description]
|
||||||
|
:where ['[?e :transaction/description-original ?do]
|
||||||
|
'[(clojure.string/lower-case ?do) ?do2]
|
||||||
|
'[(.contains ?do2 ?description)]]}
|
||||||
|
:args [(str/lower-case (:description args))]})
|
||||||
|
|
||||||
|
(:amount-gte args)
|
||||||
|
(merge-query {:query {:in ['?amount-gte]
|
||||||
|
:where ['[?e :transaction/amount ?a]
|
||||||
|
'[(>= ?a ?amount-gte)]]}
|
||||||
|
:args [(:amount-gte args)]})
|
||||||
|
|
||||||
|
(:amount-lte args)
|
||||||
|
(merge-query {:query {:in ['?amount-lte]
|
||||||
|
:where ['[?e :transaction/amount ?a]
|
||||||
|
'[(<= ?a ?amount-lte)]]}
|
||||||
|
:args [(:amount-lte args)]})
|
||||||
|
|
||||||
|
(:db/id (:bank-account args))
|
||||||
|
(merge-query {:query {:in ['?ba]
|
||||||
|
:where ['[?e :transaction/bank-account ?ba]]}
|
||||||
|
:args [(:db/id (:bank-account args))]})
|
||||||
|
|
||||||
|
(:vendor args)
|
||||||
|
(merge-query {:query {:in ['?vendor-id]
|
||||||
|
:where ['[?e :transaction/vendor ?vendor-id]]}
|
||||||
|
:args [(:db/id (:vendor args))]})
|
||||||
|
(:import-batch-id args)
|
||||||
|
(merge-query {:query {:in ['?import-batch-id]
|
||||||
|
:where ['[?import-batch-id :import-batch/entry ?e]]}
|
||||||
|
:args [(:import-batch-id args)]})
|
||||||
|
|
||||||
|
(:status route-params)
|
||||||
|
(merge-query {:query {:in ['?status]
|
||||||
|
:where ['[?e :transaction/approval-status ?status]]}
|
||||||
|
:args [(:status route-params)]})
|
||||||
|
|
||||||
|
(:sort args) (add-sorter-fields {"client" ['[?e :transaction/client ?c]
|
||||||
|
'[?c :client/name ?sort-client]]
|
||||||
|
"vendor" '[(or-join [?e ?sort-vendor]
|
||||||
|
(and
|
||||||
|
[?e :transaction/vendor ?v]
|
||||||
|
[?v :vendor/name ?sort-vendor])
|
||||||
|
(and [(missing? $ ?e :transaction/vendor)]
|
||||||
|
[(ground "") ?sort-vendor]))]
|
||||||
|
"date" ['[?e :transaction/date ?sort-date]]
|
||||||
|
"amount" ['[?e :transaction/amount ?sort-amount]]
|
||||||
|
"description" ['[?e :transaction/description-original ?sort-description]]}
|
||||||
|
args)
|
||||||
|
|
||||||
|
true
|
||||||
|
(merge-query {:query {:find ['?sort-default '?e]}})))]
|
||||||
|
|
||||||
|
(->> (observable-query query)
|
||||||
|
(apply-sort-4 (assoc query-params :default-asc? true))
|
||||||
|
(apply-pagination query-params))))
|
||||||
|
|
||||||
|
(defn fetch-page [request]
|
||||||
|
(let [db (dc/db conn)
|
||||||
|
{ids-to-retrieve :ids matching-count :count
|
||||||
|
all-ids :all-ids} (fetch-ids db request)]
|
||||||
|
|
||||||
|
[(->> (hydrate-results ids-to-retrieve db request))
|
||||||
|
matching-count
|
||||||
|
(sum-amount all-ids)]))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(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"}
|
||||||
|
(com/hidden {:name "exact-match-id"
|
||||||
|
"x-model" "exact_match"})
|
||||||
|
(com/pill {:color :primary}
|
||||||
|
[:span.inline-flex.space-x-2.items-center
|
||||||
|
[:div "exact match"]
|
||||||
|
[:div.w-3.h-3
|
||||||
|
(com/link {"@click" "exact_match=null; $nextTick(() => $dispatch('change'))"}
|
||||||
|
svg/x)]])]
|
||||||
|
[:div {:id "exact-match-id-tag"}]))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defn bank-account-filter* [request]
|
||||||
|
[:div {:hx-trigger "clientSelected from:body"
|
||||||
|
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/bank-account-filter)
|
||||||
|
:hx-target "this"
|
||||||
|
:hx-swap "outerHTML"}
|
||||||
|
(when (:client request)
|
||||||
|
(let [bank-account-belongs-to-client? (get (set (map :db/id (:client/bank-accounts (:client request))))
|
||||||
|
(:db/id (:bank-account (:query-params request))))]
|
||||||
|
(com/field {:label "Bank Account"}
|
||||||
|
(com/radio-card {:size :small
|
||||||
|
:name "bank-account"
|
||||||
|
:value (or (when bank-account-belongs-to-client?
|
||||||
|
(:db/id (:bank-account (:query-params request))))
|
||||||
|
"")
|
||||||
|
:options
|
||||||
|
(into [{:value ""
|
||||||
|
:content "All"}]
|
||||||
|
(for [ba (:client/bank-accounts (:client request))]
|
||||||
|
{:value (:db/id ba)
|
||||||
|
:content (:bank-account/name ba)}))}))))])
|
||||||
|
|
||||||
|
|
||||||
|
(defn filters [request]
|
||||||
|
[:form#transaction-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||||
|
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||||
|
::route/table)
|
||||||
|
"hx-target" "#entity-table"
|
||||||
|
"hx-indicator" "#entity-table"}
|
||||||
|
|
||||||
|
(com/hidden {:name "status"
|
||||||
|
:value (some-> (:status (:query-params request)) name)})
|
||||||
|
[:fieldset.space-y-6
|
||||||
|
(com/field {:label "Vendor"}
|
||||||
|
(com/typeahead {:name "vendor"
|
||||||
|
:id "vendor"
|
||||||
|
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||||
|
:value (:vendor (:query-params request))
|
||||||
|
:value-fn :db/id
|
||||||
|
:content-fn :vendor/name}))
|
||||||
|
(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 "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})])
|
||||||
|
(exact-match-id* request)]])
|
||||||
|
|
||||||
|
|
||||||
|
(def grid-page
|
||||||
|
(helper/build {:id "entity-table"
|
||||||
|
:nav com/main-aside-nav
|
||||||
|
:check-boxes? true
|
||||||
|
: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)])
|
||||||
|
:action-buttons (fn [request]
|
||||||
|
[
|
||||||
|
(com/button {:color :primary
|
||||||
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/bulk-code)
|
||||||
|
:hx-target "#modal-holder"
|
||||||
|
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||||
|
"hx-include" "#transaction-filters"
|
||||||
|
}
|
||||||
|
"Code")
|
||||||
|
(com/button {:color :primary
|
||||||
|
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-delete)
|
||||||
|
:hx-target "#modal-holder"
|
||||||
|
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||||
|
"hx-include" "#transaction-filters"
|
||||||
|
:hx-confirm "Are you sure you want to delete these transactions?"}
|
||||||
|
"Delete")
|
||||||
|
(com/button {:color :primary
|
||||||
|
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-delete)
|
||||||
|
:hx-target "#modal-holder"
|
||||||
|
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||||
|
"hx-include" "#transaction-filters"
|
||||||
|
:hx-confirm "Are you sure you want to suppress these transactions?"}
|
||||||
|
"Suppress")])
|
||||||
|
:row-buttons (fn [request entity]
|
||||||
|
(let [client (:transaction/client entity)
|
||||||
|
locked-until (:client/locked-until client)
|
||||||
|
tx-date (:transaction/date entity)
|
||||||
|
is-locked (and locked-until tx-date (time/before? tx-date locked-until))]
|
||||||
|
(if is-locked
|
||||||
|
[ [:div.p-3.rounded-full.bg-gray-50.text-gray-400.w-6.h-6.box-content
|
||||||
|
svg/lock]]
|
||||||
|
[(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||||
|
::route/edit-wizard
|
||||||
|
:db/id (:db/id entity))}
|
||||||
|
svg/pencil)])))
|
||||||
|
|
||||||
|
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
||||||
|
"Transactions"]]
|
||||||
|
:title (fn [r]
|
||||||
|
"Transaction")
|
||||||
|
:entity-name "register"
|
||||||
|
:route ::route/table
|
||||||
|
:csv-route ::route/csv
|
||||||
|
:table-attributes (fn [_]
|
||||||
|
{:hx-trigger "refreshTable from:body"
|
||||||
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/table)
|
||||||
|
:hx-target "#entity-table"})
|
||||||
|
:break-table (fn [request entity]
|
||||||
|
(cond
|
||||||
|
(= (-> request :query-params :sort first :name) "Vendor")
|
||||||
|
(or (-> entity :transaction/vendor :vendor/name)
|
||||||
|
"No vendor")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
:else nil))
|
||||||
|
:page->csv-entities (fn [[transactions]]
|
||||||
|
transactions)
|
||||||
|
:headers [{:key "id"
|
||||||
|
:name "Id"
|
||||||
|
:render-csv :db/id
|
||||||
|
:render-for #{:csv}}
|
||||||
|
{:key "client"
|
||||||
|
:name "Client"
|
||||||
|
:sort-key "client"
|
||||||
|
:hide? (fn [args]
|
||||||
|
(and (= (count (:clients args)) 1)
|
||||||
|
(= 1 (count (:client/locations (:client args))))))
|
||||||
|
:render (fn [x] [:div.flex.items-center.gap-2 (-> x :transaction/client :client/name)])
|
||||||
|
:render-csv (fn [x] (-> x :transaction/client :client/name))}
|
||||||
|
{:key "vendor"
|
||||||
|
:name "Vendor"
|
||||||
|
:sort-key "vendor"
|
||||||
|
:render (fn [e]
|
||||||
|
#_(alog/peek :vend e)
|
||||||
|
(or
|
||||||
|
(-> e :transaction/vendor :vendor/name)
|
||||||
|
[:span.italic.text-gray-400 (-> e :transaction/description-simple)]))
|
||||||
|
:render-csv (fn [e] (or (-> e :transaction/vendor :vendor/name)
|
||||||
|
(-> e :transaction/description-simple)))}
|
||||||
|
{:key "description"
|
||||||
|
:name "Description"
|
||||||
|
:sort-key "description"
|
||||||
|
:render :transaction/description-original
|
||||||
|
:render-csv :transaction/description-original}
|
||||||
|
{:key "date"
|
||||||
|
:sort-key "date"
|
||||||
|
:name "Date"
|
||||||
|
:show-starting "lg"
|
||||||
|
:render (fn [{:transaction/keys [date]}]
|
||||||
|
(some-> date (atime/unparse-local atime/normal-date)))}
|
||||||
|
{:key "amount"
|
||||||
|
:name "Amount"
|
||||||
|
:sort-key "amount"
|
||||||
|
:class "text-right"
|
||||||
|
:render #(format "$%,.2f" (:transaction/amount %))
|
||||||
|
:render-csv :transaction/amount}
|
||||||
|
{:key "links"
|
||||||
|
:name "Links"
|
||||||
|
:show-starting "lg"
|
||||||
|
:class "w-8"
|
||||||
|
:render (fn [i]
|
||||||
|
(let [db (dc/db conn)
|
||||||
|
journal-entries (when (:db/id i)
|
||||||
|
(dc/q '[:find (pull ?je [:db/id :journal-entry/id])
|
||||||
|
:in $ ?t-id
|
||||||
|
:where
|
||||||
|
[?je :journal-entry/original-entity ?t-id]]
|
||||||
|
db
|
||||||
|
(:db/id i)))
|
||||||
|
linked-invoices (when (and (:db/id i) (:transaction/payment i))
|
||||||
|
(dc/q '[:find (pull ?inv [:db/id :invoice/invoice-number :invoice/total])
|
||||||
|
:in $ ?payment-id
|
||||||
|
:where
|
||||||
|
[?ip :invoice-payment/payment ?payment-id]
|
||||||
|
[?ip :invoice-payment/invoice ?inv]]
|
||||||
|
db
|
||||||
|
(:db/id (:transaction/payment i))))]
|
||||||
|
(link-dropdown
|
||||||
|
(cond-> []
|
||||||
|
;; Payment link
|
||||||
|
(:transaction/payment i)
|
||||||
|
(conj
|
||||||
|
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||||
|
::payment-routes/all-page)
|
||||||
|
{:exact-match-id (:db/id (:transaction/payment i))})
|
||||||
|
:color :primary
|
||||||
|
:content (format "Payment '%s'" (-> i :transaction/payment :payment/date (atime/unparse-local atime/normal-date)))})
|
||||||
|
|
||||||
|
;; Journal entry links
|
||||||
|
(seq journal-entries)
|
||||||
|
(concat
|
||||||
|
(for [[je] journal-entries]
|
||||||
|
{:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page)
|
||||||
|
{:exact-match-id (:db/id je)})
|
||||||
|
:color :yellow
|
||||||
|
:content "Ledger entry"}))
|
||||||
|
|
||||||
|
;; Invoice links
|
||||||
|
(seq linked-invoices)
|
||||||
|
(concat
|
||||||
|
(for [[inv] linked-invoices]
|
||||||
|
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||||
|
::invoice-routes/all-page)
|
||||||
|
{:exact-match-id (:db/id inv)})
|
||||||
|
:color :secondary
|
||||||
|
:content (format "Invoice '%s'" (:invoice/invoice-number inv))}))
|
||||||
|
|
||||||
|
))))
|
||||||
|
:render-for #{:html}}]}))
|
||||||
|
(defn wrap-status-from-source [handler]
|
||||||
|
(fn [{:keys [matched-current-page-route] :as request}]
|
||||||
|
(let [ request (cond-> request
|
||||||
|
(= ::route/unapproved-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/unapproved)
|
||||||
|
(= ::route/approved-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/approved)
|
||||||
|
(= ::route/requires-feedback-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/requires-feedback)
|
||||||
|
(= ::route/page matched-current-page-route) (assoc-in [:route-params :status] nil))]
|
||||||
|
(handler request))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defn selected->ids [request params]
|
||||||
|
(let [all-selected (:all-selected params)
|
||||||
|
selected (:selected params)
|
||||||
|
ids (cond
|
||||||
|
all-selected
|
||||||
|
(:ids (fetch-ids (dc/db conn) (-> request
|
||||||
|
(assoc :query-params params)
|
||||||
|
(assoc-in [:query-params :start] 0)
|
||||||
|
(assoc-in [:query-params :per-page] 250))))
|
||||||
|
|
||||||
|
|
||||||
|
:else
|
||||||
|
selected)]
|
||||||
|
ids))
|
||||||
@@ -156,8 +156,8 @@
|
|||||||
(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
|
(cond-> { :purpose "transaction"}
|
||||||
:purpose "invoice"})
|
client-id (assoc :client-id client-id)))
|
||||||
:id name
|
:id name
|
||||||
:x-model x-model
|
:x-model x-model
|
||||||
:value value
|
:value value
|
||||||
@@ -190,8 +190,8 @@
|
|||||||
(com/validated-field
|
(com/validated-field
|
||||||
{:errors (fc/field-errors)
|
{:errors (fc/field-errors)
|
||||||
:x-hx-val:account-id "accountId"
|
:x-hx-val:account-id "accountId"
|
||||||
:hx-vals (hx/json {:name (fc/field-name)
|
:hx-vals (hx/json (cond-> {:name (fc/field-name) }
|
||||||
:client-id client-id})
|
client-id (assoc :client-id client-id)))
|
||||||
:x-dispatch:changed "accountId"
|
:x-dispatch:changed "accountId"
|
||||||
:hx-trigger "changed"
|
: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)
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
(com/validated-field
|
(com/validated-field
|
||||||
{:errors (fc/field-errors)}
|
{:errors (fc/field-errors)}
|
||||||
(com/money-input {:name (fc/field-name)
|
(com/money-input {:name (fc/field-name)
|
||||||
:class "w-16 amount-field"
|
:class "w-16"
|
||||||
:value (fc/field-value)}))))
|
:value (fc/field-value)}))))
|
||||||
(com/data-grid-cell {:class "align-top"}
|
(com/data-grid-cell {:class "align-top"}
|
||||||
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
||||||
@@ -733,7 +733,6 @@
|
|||||||
(com/data-grid-header {:class "w-32"} "Location")
|
(com/data-grid-header {:class "w-32"} "Location")
|
||||||
(com/data-grid-header {:class "w-16"} "$")
|
(com/data-grid-header {:class "w-16"} "$")
|
||||||
(com/data-grid-header {:class "w-16"})]}
|
(com/data-grid-header {:class "w-16"})]}
|
||||||
(println "WE ARE NOW HERE" (fc/field-value))
|
|
||||||
(fc/cursor-map #(transaction-account-row* {:value %
|
(fc/cursor-map #(transaction-account-row* {:value %
|
||||||
:client-id (-> request :entity :transaction/client :db/id)}))
|
:client-id (-> request :entity :transaction/client :db/id)}))
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,12 @@
|
|||||||
:put ::edit-wizard-navigate
|
:put ::edit-wizard-navigate
|
||||||
"/unapproved" ::unapproved-page
|
"/unapproved" ::unapproved-page
|
||||||
"/requires-feedback" ::requires-feedback-page
|
"/requires-feedback" ::requires-feedback-page
|
||||||
"/approved" ::approved-page}
|
"/approved" ::approved-page
|
||||||
|
"/bulk-delete" ::bulk-delete
|
||||||
|
"/bulk-suppress" ::bulk-suppress
|
||||||
|
"/bulk-code" {:get ::bulk-code
|
||||||
|
:put ::bulk-code-submit
|
||||||
|
"/new-account" ::bulk-code-new-account}}
|
||||||
"/new" {:get ::new
|
"/new" {:get ::new
|
||||||
:post ::new-submit
|
:post ::new-submit
|
||||||
"/location-select" ::location-select
|
"/location-select" ::location-select
|
||||||
|
|||||||
2
tasks
2
tasks
@@ -1,7 +1,5 @@
|
|||||||
* Add tests for edit transaction.
|
* Add tests for edit transaction.
|
||||||
* Make it so you can create a new vendor again.
|
* Make it so you can create a new vendor again.
|
||||||
* Check permissions on ledger, transactions, reports
|
* Check permissions on ledger, transactions, reports
|
||||||
* make locked transactions clearer on the transaction table
|
|
||||||
* Make locked transactions not look butt ugly with errors
|
|
||||||
* Implement bulk actions
|
* Implement bulk actions
|
||||||
* Get rid of new transaction button
|
* Get rid of new transaction button
|
||||||
Reference in New Issue
Block a user