Adds new ledger page

This commit is contained in:
2024-08-22 22:59:47 -07:00
parent d9bd09a9cc
commit ddb1d99cd3
7 changed files with 714 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
(ns auto-ap.ssr.components.aside
(:require [auto-ap.client-routes :as client-routes]
[auto-ap.graphql.utils :refer [is-admin?]]
[auto-ap.permissions :refer [can?]]
[auto-ap.routes.admin.clients :as ac-routes]
[auto-ap.routes.admin.excel-invoices :as ei-routes]
@@ -7,6 +8,7 @@
[auto-ap.routes.admin.transaction-rules :as transaction-rules]
[auto-ap.routes.admin.vendors :as v-routes]
[auto-ap.routes.invoice :as invoice-route]
[auto-ap.routes.ledger :as ledger-routes]
[auto-ap.routes.outgoing-invoice :as oi-routes]
[auto-ap.routes.payments :as payment-routes]
[auto-ap.ssr-routes :as ssr-routes]
@@ -87,11 +89,13 @@
(let [selected (cond
(#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new ::invoice-route/import-page :invoice-glimpse :invoice-glimpse-textract-invoice} (:matched-route request))
"invoices"
(#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts} (:matched-route request))
"sales"
(#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page} (:matched-route request))
"payments"
(#{::ledger-routes/all-page} (:matched-route request))
"ledger"
:else
nil)]
[:ul {:class "space-y-1"
@@ -270,8 +274,21 @@
"Ledger")
(sub-menu- {:selector "ledger"
:active? (= "ledger" selected)}
(menu-button- {:href (bidi/path-for client-routes/routes
:ledger)} "Register")
(if (is-admin? (:identity request))
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::ledger-routes/all-page)
{:date-range "month"})
:active? (= ::ledger-routes/all-page (:matched-route request))
:hx-boost "true"}
[:div.flex.gap-2
"Register"
(tags/pill- {:color :secondary} "WIP")])
(menu-button- {:href (bidi/path-for client-routes/routes
:ledger)} "Register"))
(when (is-admin? (:identity request))
(menu-button- {:href (bidi/path-for client-routes/routes
:ledger)} "Old Register")
)
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss)} "Profit & Loss")
(menu-button- {:href (bidi/path-for client-routes/routes

View File

@@ -26,6 +26,7 @@
[auto-ap.ssr.indicators :as indicators]
[auto-ap.ssr.invoice.glimpse :as invoice-glimpse]
[auto-ap.ssr.invoices :as invoice]
[auto-ap.ssr.ledger :as l]
[auto-ap.ssr.outgoing-invoice.new :as oin]
[auto-ap.ssr.payments :as payments]
[auto-ap.ssr.pos.cash-drawer-shifts :as pos-cash-drawer-shifts]
@@ -105,5 +106,6 @@
(into dashboard/key->handler)
(into indicators/key->handler)
(into payments/key->handler)
(into oin/route->handler)))
(into oin/route->handler)
(into l/key->handler)))

View File

@@ -0,0 +1,683 @@
(ns auto-ap.ssr.ledger
(:require [auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-3
audit-transact conn merge-query observable-query
pull-many]]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.datomic.bank-accounts :as d-bank-accounts]
[auto-ap.datomic.invoices :as d-invoices]
[auto-ap.graphql.checks :as gq-checks :refer [base-payment
invoice-payments
print-checks-internal
validate-belonging]]
[auto-ap.graphql.utils :refer [assert-can-see-client
assert-not-locked exception->4xx
exception->notification
extract-client-ids notify-if-locked]]
[auto-ap.logging :as alog]
[auto-ap.permissions :refer [can?]]
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
[auto-ap.routes.ledger :as route]
[auto-ap.routes.payments :as payment-route]
[auto-ap.routes.utils
:refer [wrap-client-redirect-unauthenticated]]
[auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.components.multi-modal :as mm]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
[auto-ap.ssr.hiccup-helper :as hh]
[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 [apply-middleware-to-all-handlers assert-schema
clj-date-schema dissoc-nil-transformer entity-id
html-response main-transformer modal-response money
ref->enum-schema round-money strip
wrap-implied-route-param wrap-merge-prior-hx
wrap-schema-enforce]]
[auto-ap.time :as atime]
[auto-ap.utils :refer [by dollars-0?]]
[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]
[malli.transform :as mt]
[malli.util :as mut]))
(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 filters [request]
[:form#ledger-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}))
(com/field {:label "Account"}
(com/typeahead {:name "account"
:id "account"
:url (bidi/path-for ssr-routes/only-routes :account-search)
:value (:account (:query-params request))
:value-fn :db/id
:content-fn #(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id %))
(:db/id (:client request))))}))
(date-range-field* request)
(com/field {:label "Invoice #"}
(com/text-input {:name "invoice-number"
:id "invoice-number"
:class "hot-filter"
:value (:invoice-number (:query-params request))
:placeholder "e.g., ABC-456"
:size :small}))
(com/field {:label "Amount"}
[:div.flex.space-x-4.items-baseline
(com/money-input {:name "amount-gte"
: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}]
(let [valid-clients (extract-client-ids (:clients request)
(:client-id request)
(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 :journal-entry/client ?c]]}
:args [db
(:exact-match-id args)
valid-clients]}
(cond-> {:query {:find []
:in ['$ '[?clients ?start ?end]]
:where '[[(iol-ion.query/scan-ledger $ ?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)]]}
(:only-external args)
(merge-query {:query {:where ['(not [?e :journal-entry/original-entity ])]}})
(seq (:external-id-like args))
(merge-query {:query {:in ['?external-id-like]
:where ['[?e :journal-entry/external-id ?external-id]
'[(.contains ^String ?external-id ?external-id-like)]]}
:args [(:external-id-like args)]})
(seq (:source args))
(merge-query {:query {:in ['?source]
:where ['[?e :journal-entry/source ?source]]}
:args [(:source args)]})
(:vendor args)
(merge-query {:query {:in ['?vendor-id]
:where ['[?e :journal-entry/vendor ?vendor-id]]}
:args [(:db/id (:vendor args))]})
(:invoice-number args)
(merge-query {:query {:in ['?invoice-number]
:where ['[?e :journal-entry/original-entity ?oe]
'[?oe :invoice/invoice-number ?invoice-number]]}
:args [(:invoice-number args)]})
(or (seq (:numeric-code args))
(:account args)
(:bank-account-id args)
(not-empty (:location args)))
(merge-query {:query {:where ['[?e :journal-entry/line-items ?li]]}})
(seq (:numeric-code args))
(merge-query {:query {:in ['[[?from-numeric-code ?to-numeric-code]]]
:where ['[?li :journal-entry-line/account ?a]
'(or-join [?a ?c]
[?a :account/numeric-code ?c]
[?a :bank-account/numeric-code ?c])
'[(>= ?c ?from-numeric-code)]
'[(<= ?c ?to-numeric-code)]]}
:args [(vec (for [{:keys [from to]} (:numeric-code args)]
[(or from 0) (or to 99999)]))]})
(seq (:account args))
(merge-query {:query {:in ['?a3]
:where ['[?li :journal-entry-line/account ?a3] ]}
:args [(:db/id (:account args))]})
(:amount-gte args)
(merge-query {:query {:in ['?amount-gte]
:where ['[?e :journal-entry/amount ?a]
'[(>= ?a ?amount-gte)]]}
:args [(:amount-gte args)]})
(:amount-lte args)
(merge-query {:query {:in ['?amount-lte]
:where ['[?e :journal-entry/amount ?a]
'[(<= ?a ?amount-lte)]]}
:args [(:amount-lte args)]})
(:bank-account-id args)
(merge-query {:query {:in ['?a]
:where ['[?li :journal-entry-line/account ?a]]}
:args [(:bank-account-id args)]})
(:account-id args)
(merge-query {:query {:in ['?a2]
:where ['[?e :journal-entry/line-items ?li2]
'[?li2 :journal-entry-line/account ?a2]]}
:args [(:account-id args)]})
(not-empty (:location args))
(merge-query {:query {:in ['?location]
:where ['[?li :journal-entry-line/location ?location]]}
:args [(:location args)]})
(not-empty (:locations args))
(merge-query {:query {:in ['[?location ...]]
:where ['[?li :journal-entry-line/location ?location]]}
:args [(:locations args)]})
(:sort args) (add-sorter-fields {"client" ['[?e :journal-entry/client ?c]
'[?c :client/name ?sort-client]]
"date" ['[?e :journal-entry/date ?sort-date]]
"vendor" ['[?e :journal-entry/vendor ?v]
'[?v :vendor/name ?sort-vendor]]
"amount" ['[?e :journal-entry/amount ?sort-amount]]
"external-id" ['[?e :journal-entry/external-id ?sort-external-id]]
"source" ['[?e :journal-entry/source ?sort-source]]}
args)
true
(merge-query {:query {:find ['?sort-default '?e]}})))]
(->> (observable-query query)
(apply-sort-3 (assoc query-params :default-asc? false))
(apply-pagination query-params))))
(def default-read
'[:journal-entry/amount
:db/id
[:journal-entry/date :xform clj-time.coerce/from-date]
{
:journal-entry/vendor [:vendor/name :db/id]
:journal-entry/client [:client/name :client/code :db/id]
:journal-entry/line-items [:journal-entry-line/debit
:journal-entry-line/location
:journal-entry-line/credit
{:journal-entry-line/account [:account/name :db/id :account/numeric-code
{[ :account/type :xform iol-ion.query/ident] [:db/ident :db/id]}
{:account/client-overrides [:account-client-override/name
{:account-client-override/client [:db/id]}]}
{[ :bank-account/type :xform iol-ion.query/ident]
[:db/ident :db/id]}]}]}])
(defn hydrate-results [ids db _]
(let [results (->> (pull-many db default-read ids)
(group-by :db/id))
refunds (->> ids
(map results)
(map first))]
refunds))
(defn sum-outstanding [ids]
(->>
(dc/q {:find ['?id '?o]
:in ['$ '[?id ...]]
:where ['[?id :invoice/outstanding-balance ?o]]}
(dc/db conn)
ids)
(map last)
(reduce
+
0.0)))
(defn sum-total-amount [ids]
(->>
(dc/q {:find ['?id '?o]
:in ['$ '[?id ...]]
:where ['[?id :invoice/total ?o]]
}
(dc/db conn)
ids)
(map last)
(reduce
+
0.0)))
(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-outstanding all-ids)
(sum-total-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]]
[:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]]
[:account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :account/name]}]]]
[:check-number {:optional true} [:maybe [:string {:decode/string strip}]]]
[:invoice-number {:optional true} [:maybe [:string {:decode/string strip}]]]
[:status {:optional true} [:maybe (ref->enum-schema "invoice-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]]]]))
(comment
(mc/decode query-schema
{:start " "}
main-transformer))
(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))
(defn render-lines [key {:journal-entry/keys [line-items client]}]
(let [lines (for [jel line-items
:when (and (key jel)
(not (dollars-0? (key jel))))]
jel)]
[:div.grid.grid-cols-2.gap-1.auto-cols-min.grid-flow-row.shrink
(for [jel lines
:let [account (d-accounts/clientize (:journal-entry-line/account jel) (:db/id client))
account-name (:account/name account)]]
(list
(if account-name
[:div.text-left
(:journal-entry-line/location jel) ": "
(:account/numeric-code account)
" - " account-name]
[:div.text-left (com/pill {:color :yellow} "Unassigned")])
[:div.text-right (format "$%,.2f" (key jel))]))
(when-not (= 1 (count lines))
[:div.col-span-2 (com/pill {:color :primary} "Total: " (->> lines
(map #(or (key %) 0.0))
(reduce + 0.0)
(format "$%,.2f")))])]))
;; TODO test as a real user
(def grid-page
(helper/build {:id "entity-table"
:nav com/main-aside-nav
:check-boxes? true
:check-box-warning? (fn [e]
(some? (:invoice/scheduled-payment e)))
:page-specific-nav filters
:fetch-page fetch-page
: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)])
:query-schema query-schema
:parse-query-params (fn [p]
(alog/peek ::PARSE
(mc/decode query-schema p main-transformer)))
:action-buttons (fn [request]
(let [[_ _ outstanding total] (:page-results request)]
[ #_(when (can? (:identity request) {:subject :invoice :activity :bulk-delete})
(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/bulk-delete))
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
"hx-include" "#ledger-filters"
:color :red}
"Void selected")) ]))
:row-buttons (fn [request entity]
[(when (and (= :invoice-status/unpaid (:invoice/status entity))
(can? (:identity request) {:subject :invoice :activity :delete}))
(com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes
::route/delete
:db/id (:db/id entity))
:hx-confirm "Are you sure you want to void this invoice?"}
svg/trash))
(when (and (can? (:identity request) {:subject :invoice :activity :edit})
(#{:invoice-status/unpaid :invoice-status/paid} (:invoice/status entity)))
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
::route/edit-wizard
:db/id (:db/id entity))}
svg/pencil))
(when (and (can? (:identity request) {:subject :invoice :activity :edit})
(#{:invoice-status/voided} (:invoice/status entity)))
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
::route/unvoid
:db/id (:db/id entity))}
svg/undo))])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
"Ledger"]]
:title (fn [r]
(str
(some-> r :route-params :status name str/capitalize (str " "))
"Register"))
:entity-name "register"
:route ::route/table
:break-table (fn [request entity]
(when (= (-> request :query-params :sort first :name) "Vendor")
(-> entity :journal-entry/vendor :vendor/name)))
:headers [{: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 :invoice/client :client/name)
(map #(com/pill {:color :primary} (-> % :invoice-expense-account/location))
(:invoice/expense-accounts x)) ])}
{:key "vendor"
:name "Vendor"
:sort-key "vendor"
:render #(-> % :journal-entry/vendor :vendor/name)}
{:key "date"
:sort-key "date"
:name "Date"
:show-starting "lg"
:render (fn [{:journal-entry/keys [date]}]
(some-> date (atime/unparse-local atime/normal-date)))}
{:key "debit"
:name "Debit"
:sort-key "debit"
:class "text-right"
:render (partial render-lines :journal-entry-line/debit)}
{:key "credit"
:name "Credit"
:sort-key "credit"
:class "text-right"
:render (partial render-lines :journal-entry-line/credit)}
#_{:key "links"
:name "Links"
:show-starting "lg"
:class "w-8"
:render (fn [i]
(link-dropdown
(concat (->> i
:invoice/payments
(map :invoice-payment/payment)
(filter (fn [p]
(not= :payment-status/voided
(:payment/status p))))
(mapcat (fn [p]
(cond-> [{:link (hu/url (bidi/path-for ssr-routes/only-routes
::payment-route/all-page)
{:exact-match-id (:db/id p)})
:content (str (format "$%,.2f" (:payment/amount p))
(some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}]
(:payment/transaction p) (conj {:link (hu/url (bidi/path-for client-routes/routes :transactions)
{:exact-match-id (:db/id (first (:payment/transaction p)))})
:color :secondary
:content "Transaction"})))))
(when (:invoice/journal-entry i)
[{:link (hu/url (bidi/path-for client-routes/routes :ledger)
{:exact-match-id (:db/id (first (:invoice/journal-entry i)))})
:color :yellow
:content "Ledger entry"}])
(when (:invoice/source-url i)
[{:link (:invoice/source-url i)
:color :secondary
:content "File"}]))))}]}))
(def row* (partial helper/row* grid-page))
(defn unvoid-invoice [{:as request :keys [identity entity]}]
(let [invoice entity
id (:db/id entity)
_ (assert-can-see-client identity (:db/id (:invoice/client invoice)))
_ (assert-not-locked (:db/id (:invoice/client invoice)) (:invoice/date invoice))
history (dc/history (dc/db conn))
txs (dc/q {:find ['?tx '?e '?original-status '?original-outstanding '?total '?ea '?ea-amount]
:where ['[?e :invoice/status :invoice-status/voided ?tx true]
'[?e :invoice/status ?original-status ?tx false]
'[?e :invoice/outstanding-balance ?original-outstanding ?tx false]
'[?e :invoice/total ?total ?tx false]
'[?ea :invoice-expense-account/amount ?ea-amount ?tx false]]
:in ['$ '?e]}
history id)
[last-transaction] (->> txs (sort-by first) (last))
tx [[:upsert-invoice
(->> txs
(filter (fn [[tx]] (= tx last-transaction)))
(reduce (fn [new-transaction [_ entity original-status original-outstanding total expense-account expense-account-amount]]
(-> new-transaction
(assoc :db/id entity
:invoice/total total
:invoice/status original-status
:invoice/outstanding-balance original-outstanding)
(update :invoice/expense-accounts (fnil conj []) {:db/id expense-account :invoice-expense-account/amount expense-account-amount})))
{}))]]
_ (audit-transact tx identity)]
(alog/info ::unvoiding-invoice :transaction :tx)
(html-response
(row* identity (dc/pull (dc/db conn) default-read id) {:flash? true
:request request})
:headers (cond-> {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" id)
"hx-reswap" "outerHTML"}))))
(defn delete [{invoice :entity :as request identity :identity}]
(exception->notification
#(when-not (= :invoice-status/unpaid (:invoice/status invoice))
(throw (ex-info "Cannot void an invoice if it is paid. First void the payment." {}))))
(when (->> invoice :invoice/payments
(filter (fn [p]
(not= :payment-status/voided
(:payment/status p))))
seq)
(throw (ex-info "This invoice has linked payments. Void the payments first." {:type :notification})))
(exception->notification
#(assert-can-see-client identity (:db/id (:invoice/client invoice))))
(notify-if-locked (:db/id (:invoice/client invoice))
(:invoice/date invoice))
(audit-transact [[:upsert-invoice {:db/id (:db/id invoice)
:invoice/total 0.0
:invoice/outstanding-balance 0.0
:invoice/status :invoice-status/voided
:invoice/expense-accounts (map (fn [ea] {:db/id (:db/id ea)
:invoice-expense-account/amount 0.0})
(:invoice/expense-accounts invoice))}]]
identity)
(html-response (row* (:identity request) (dc/pull (dc/db conn) default-read (:db/id invoice))
{:class "live-removed"})
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))}))
(defn bulk-delete-dialog [request]
(let [all-selected (:all-selected (:query-params request))
selected (:selected (:query-params request))
ids (cond
all-selected
(:ids (fetch-ids (dc/db conn) (-> request
(assoc-in [:query-params :start] 0)
(assoc-in [:query-params :per-page] 250))))
:else
selected)]
(modal-response
(com/modal {}
(com/modal-card-advanced
{}
(com/modal-body {}
[:div.flex.flex-col.mt-4.space-y-4.items-center
[:div.w-24.h-24.bg-red-50.rounded-full.p-4.text-red-300
svg/alert]
[:div "You are about to void " (count ids) " invoices. Are you sure you want to do this?"]])
(com/modal-footer {} [:div.flex.justify-end (com/button {:color :primary
:hx-vals (hx/json (mc/encode
query-schema
(dissoc (:query-params request) :sort)
(mt/transformer
main-transformer
dissoc-nil-transformer
mt/strip-extra-keys-transformer)))
:hx-delete (hu/url (bidi/path-for ssr-routes/only-routes
::route/bulk-delete-confirm))}
"Void invoices")])))
:headers (-> {}
(assoc "hx-retarget" ".modal-stack")
(assoc "hx-reswap" "beforeend")))))
(defn void-invoices-internal [all-ids id]
(let [all-ids (->> all-ids
(dc/q '[:find (pull ?i [:db/id :invoice/date {:invoice/expense-accounts [:db/id]}])
:in $ [?i ...]
:where (not [_ :invoice-payment/invoice ?i])
[?i :invoice/client ?c]
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
[?i :invoice/date ?d]
[(>= ?d ?lu)]]
(dc/db conn)))
voidable-cash-payments (->> (dc/q '[:find ?p
:in $ [?i ...]
:where [?ip :invoice-payment/invoice ?i]
[?ip :invoice-payment/payment ?p]
[?p :payment/type :payment-type/cash]
[?i :invoice/client ?c]
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
[?i :invoice/date ?d]
[(>= ?d ?lu)]]
(dc/db conn)
(map :db/id all-ids))
(map first))]
(alog/info ::void-payments :count (count voidable-cash-payments))
(gq-checks/void-payments-internal voidable-cash-payments id)
(alog/info ::voiding-invoices :count (count all-ids))
(audit-transact
(->> all-ids
(map
(fn [[i]]
[:upsert-invoice {:db/id (:db/id i)
:invoice/total 0.0
:invoice/outstanding-balance 0.0
:invoice/status :invoice-status/voided
:invoice/expense-accounts (mapv
(fn [iea]
{:db/id (:db/id iea)
:invoice-expense-account/amount 0.0})
(:invoice/expense-accounts i))}])))
id)
(count all-ids)))
(defn bulk-delete-dialog-confirm [request]
(alog/peek (:form-params request))
(let [ids (selected->ids request (:form-params request))
updated-count (void-invoices-internal ids (:identity request))]
(html-response [:div]
:headers {"hx-trigger" (hx/json {:modalclose ""
:notification (format "Successfully voided %d of %d invoices."
updated-count
(count ids))})})))
(defn wrap-status-from-source [handler]
(fn [{:keys [matched-current-page-route] :as request}]
(let [request (cond-> request
(= ::route/paid-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/paid)
(= ::route/unpaid-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/unpaid)
(= ::route/voided-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/voided)
(= ::route/all-page matched-current-page-route) (assoc-in [:route-params :status] nil))]
(handler request))))
(def key->handler
(apply-middleware-to-all-handlers
(->
{::route/all-page (-> (helper/page-route grid-page :parse-query-params? false)
(wrap-implied-route-param :status nil))
::route/table (helper/table-route grid-page :parse-query-params? false)})
(fn [h]
(-> h
(wrap-copy-qp-pqp)
(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-client-redirect-unauthenticated)))))

View File

@@ -701,3 +701,4 @@
1

View File

@@ -0,0 +1,4 @@
(ns auto-ap.routes.ledger)
(def routes {"" {:get ::all-page}
"/table" ::table })

View File

@@ -8,6 +8,7 @@
[auto-ap.routes.payments :as p-routes]
[auto-ap.routes.dashboard :as d-routes]
[auto-ap.routes.invoice :as i-routes]
[auto-ap.routes.ledger :as l-routes]
[auto-ap.routes.admin.clients :as ac-routes]
[auto-ap.routes.admin.sales-summaries :as ss-routes]
[auto-ap.routes.admin.transaction-rules :as tr-routes]))
@@ -49,6 +50,7 @@
"/import-batch" ib-routes/routes
"/transaction-rule" tr-routes/routes
"/excel-invoice" ei-routes/routes}
"ledger" l-routes/routes
"transaction" {"/insights" {"" :transaction-insights
"/table" :transaction-insight-table
["/code/" [#"\d+" :transaction-id]] {:post :transaction-insight-code}