preliminary investigate
This commit is contained in:
@@ -1,50 +1,46 @@
|
|||||||
(ns auto-ap.ssr.ledger
|
(ns auto-ap.ssr.ledger
|
||||||
(:require [auto-ap.client-routes :as client-routes]
|
(:require [auto-ap.datomic
|
||||||
[auto-ap.datomic
|
:refer [audit-transact audit-transact-batch conn pull-many
|
||||||
:refer [add-sorter-fields apply-pagination apply-sort-4
|
remove-nils]]
|
||||||
audit-transact audit-transact-batch conn merge-query
|
|
||||||
observable-query pull-many remove-nils]]
|
|
||||||
[auto-ap.datomic.accounts :as d-accounts]
|
|
||||||
[auto-ap.datomic.accounts :as a]
|
[auto-ap.datomic.accounts :as a]
|
||||||
[auto-ap.graphql.utils :refer [assert-admin assert-can-see-client
|
[auto-ap.graphql.utils :refer [assert-admin assert-can-see-client
|
||||||
exception->notification
|
exception->notification
|
||||||
extract-client-ids notify-if-locked]]
|
notify-if-locked]]
|
||||||
[auto-ap.logging :as alog]
|
[auto-ap.logging :as alog]
|
||||||
[auto-ap.permissions :refer [can? 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-route]
|
|
||||||
[auto-ap.routes.ledger :as route]
|
[auto-ap.routes.ledger :as route]
|
||||||
[auto-ap.routes.utils
|
[auto-ap.routes.utils
|
||||||
:refer [wrap-client-redirect-unauthenticated]]
|
:refer [wrap-client-redirect-unauthenticated]]
|
||||||
[auto-ap.solr :as solr]
|
[auto-ap.solr :as solr]
|
||||||
[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.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.hx :as hx]
|
||||||
[auto-ap.ssr.ledger.balance-sheet :as balance-sheet]
|
[auto-ap.ssr.ledger.balance-sheet :as balance-sheet]
|
||||||
|
[auto-ap.ssr.ledger.common :refer [bank-account-filter
|
||||||
|
default-read fetch-ids
|
||||||
|
grid-page query-schema]]
|
||||||
|
[auto-ap.ssr.ledger.investigate :as investigate]
|
||||||
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
|
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
|
||||||
[auto-ap.ssr.pos.common :refer [date-range-field*]]
|
|
||||||
[auto-ap.ssr.svg :as svg]
|
[auto-ap.ssr.svg :as svg]
|
||||||
[auto-ap.ssr.ui :refer [base-page]]
|
[auto-ap.ssr.ui :refer [base-page]]
|
||||||
[auto-ap.ssr.utils
|
[auto-ap.ssr.utils
|
||||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||||||
entity-id html-response main-transformer money
|
html-response main-transformer money strip
|
||||||
ref->enum-schema strip wrap-form-4xx-2
|
wrap-form-4xx-2 wrap-implied-route-param
|
||||||
wrap-implied-route-param wrap-merge-prior-hx
|
wrap-merge-prior-hx wrap-schema-decode
|
||||||
wrap-schema-decode wrap-schema-enforce]]
|
wrap-schema-enforce]]
|
||||||
[auto-ap.time :as atime]
|
[auto-ap.time :as atime]
|
||||||
[auto-ap.utils :refer [dollars-0? dollars=]]
|
[auto-ap.utils :refer [dollars=]]
|
||||||
[bidi.bidi :as bidi]
|
[bidi.bidi :as bidi]
|
||||||
[clj-time.coerce :as coerce]
|
[clj-time.coerce :as coerce]
|
||||||
[clj-time.core :as t]
|
[clj-time.core :as t]
|
||||||
[clojure.data.csv :as csv]
|
[clojure.data.csv :as csv]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.string :as str]
|
|
||||||
[com.brunobonacci.mulog :as mu]
|
[com.brunobonacci.mulog :as mu]
|
||||||
[datomic.api :as dc]
|
[datomic.api :as dc]
|
||||||
[hiccup.util :as hu]
|
|
||||||
[hiccup2.core :as hiccup]
|
[hiccup2.core :as hiccup]
|
||||||
[iol-ion.utils :refer [by random-tempid]]
|
[iol-ion.utils :refer [by random-tempid]]
|
||||||
[malli.core :as mc]
|
[malli.core :as mc]
|
||||||
@@ -52,337 +48,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(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]
|
|
||||||
(html-response (bank-account-filter* request)))
|
|
||||||
|
|
||||||
(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))))}))
|
|
||||||
|
|
||||||
(bank-account-filter* 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 "Account Code"}
|
|
||||||
[:div.flex.space-x-4.items-baseline
|
|
||||||
(com/int-input {:name "numeric-code-gte"
|
|
||||||
:id "numeric-code-gte"
|
|
||||||
:hx-preserve "true"
|
|
||||||
:class "hot-filter w-20"
|
|
||||||
:value (:numeric-code-gte (:query-params request))
|
|
||||||
:placeholder "40000"
|
|
||||||
:size :small})
|
|
||||||
[:div.align-baseline
|
|
||||||
"to"]
|
|
||||||
(com/int-input {:name "numeric-code-lte"
|
|
||||||
:hx-preserve "true"
|
|
||||||
:id "numeric-code-lte"
|
|
||||||
:class "hot-filter w-20"
|
|
||||||
:value (:numeric-code-lte (:query-params request))
|
|
||||||
:placeholder "50000"
|
|
||||||
: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)]})
|
|
||||||
(:external? route-params)
|
|
||||||
(merge-query {:query { :where ['[?e :journal-entry/external-id]]} })
|
|
||||||
|
|
||||||
(: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 (:numeric-code-lte args)
|
|
||||||
(:numeric-code-gte args)
|
|
||||||
(:account args)
|
|
||||||
(:db/id (:bank-account args))
|
|
||||||
(not-empty (:location args)))
|
|
||||||
(merge-query {:query {:where ['[?e :journal-entry/line-items ?li]]}})
|
|
||||||
|
|
||||||
(or (:numeric-code-gte args)
|
|
||||||
(:numeric-code-lte 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 [(or (:numeric-code-gte args) 0) (or (:numeric-code-lte args) 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)]})
|
|
||||||
|
|
||||||
(:db/id (:bank-account args))
|
|
||||||
(merge-query {:query {:in ['?a]
|
|
||||||
:where ['[?li :journal-entry-line/account ?a]]}
|
|
||||||
:args [(:db/id (:bank-account 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-4 (assoc query-params :default-asc? true))
|
|
||||||
(apply-pagination query-params))))
|
|
||||||
|
|
||||||
(def default-read
|
|
||||||
'[:journal-entry/amount
|
|
||||||
:journal-entry/alternate-description
|
|
||||||
:journal-entry/source
|
|
||||||
:journal-entry/external-id
|
|
||||||
:db/id
|
|
||||||
[:journal-entry/date :xform clj-time.coerce/from-date]
|
|
||||||
{:journal-entry/vendor [:vendor/name :db/id]
|
|
||||||
:journal-entry/original-entity [:invoice/invoice-number
|
|
||||||
:invoice/source-url
|
|
||||||
:transaction/description-original :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/running-balance
|
|
||||||
:journal-entry-line/credit
|
|
||||||
{:journal-entry-line/account [:account/name :db/id :account/numeric-code
|
|
||||||
:bank-account/name :bank-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]]
|
|
||||||
[:numeric-code-gte {:optional true} [:maybe nat-int?]]
|
|
||||||
[:numeric-code-lte {:optional true} [:maybe nat-int?]]
|
|
||||||
[: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/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
|
(comment
|
||||||
(mc/decode query-schema
|
(mc/decode query-schema
|
||||||
{:start " "}
|
{:start " "}
|
||||||
@@ -403,168 +68,10 @@
|
|||||||
selected)]
|
selected)]
|
||||||
ids))
|
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 (or (:account/name account)
|
|
||||||
(:bank-account/name (:journal-entry-line/account jel)))]]
|
|
||||||
(list
|
|
||||||
|
|
||||||
(if account-name
|
|
||||||
[:div {:x-data "popper()" }
|
|
||||||
[:div.text-left.underline.cursor-pointer {:x-ref "source"}
|
|
||||||
(:journal-entry-line/location jel) ": "
|
|
||||||
(or (:account/numeric-code account) (:bank-account/numeric-code account))
|
|
||||||
" - " account-name]
|
|
||||||
(com/tooltip {:x-bind "tooltip" :class "absolute"}
|
|
||||||
"Running Balance: " (some->> (:journal-entry-line/running-balance jel)
|
|
||||||
(format "$%,.2f")))]
|
|
||||||
[:div.text-left (com/pill {:color :yellow} "Unassigned")])
|
|
||||||
[:div.text-right.text-underline (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
|
;; 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]
|
|
||||||
(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]
|
|
||||||
(cond
|
|
||||||
(= (-> request :query-params :sort first :name) "Vendor")
|
|
||||||
(-> entity :journal-entry/vendor :vendor/name)
|
|
||||||
|
|
||||||
(= (-> request :query-params :sort first :name) "Source")
|
|
||||||
(-> entity :journal-entry/source)
|
|
||||||
|
|
||||||
:else nil))
|
|
||||||
: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 :journal-entry/client :client/name) ])}
|
|
||||||
|
|
||||||
{:key "vendor"
|
|
||||||
:name "Vendor"
|
|
||||||
:sort-key "vendor"
|
|
||||||
:render (fn [e] (or (-> e :journal-entry/vendor :vendor/name)
|
|
||||||
[:span.italic.text-gray-400 (-> e :journal-entry/alternate-description)]))}
|
|
||||||
{:key "source"
|
|
||||||
:name "Source"
|
|
||||||
:sort-key "source"
|
|
||||||
:hide? (fn [args]
|
|
||||||
(not (:external? (:route-params args))))
|
|
||||||
:render :journal-entry/source}
|
|
||||||
{:key "external-id"
|
|
||||||
:name "External Id"
|
|
||||||
:sort-key "external-id"
|
|
||||||
:class "max-w-[12rem]"
|
|
||||||
:hide? (fn [args]
|
|
||||||
(not (:external? (:route-params args))))
|
|
||||||
:render (fn [x] [:p.truncate ( :journal-entry/external-id x)])}
|
|
||||||
{: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
|
|
||||||
(cond-> []
|
|
||||||
(-> i :journal-entry/original-entity :invoice/invoice-number)
|
|
||||||
(conj
|
|
||||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
|
||||||
::invoice-route/all-page)
|
|
||||||
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
|
|
||||||
:color :primary
|
|
||||||
:content (format "Invoice '%s'" (-> i :journal-entry/original-entity :invoice/invoice-number))})
|
|
||||||
(-> i :journal-entry/original-entity :invoice/source-url)
|
|
||||||
{:link (-> i :journal-entry/original-entity :invoice/source-url)
|
|
||||||
:color :secondary
|
|
||||||
:content (str "File")}
|
|
||||||
|
|
||||||
(-> i :journal-entry/original-entity :transaction/description-original)
|
|
||||||
(conj
|
|
||||||
{:link (hu/url (bidi/path-for client-routes/routes
|
|
||||||
:transactions)
|
|
||||||
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
|
|
||||||
:color :primary
|
|
||||||
:content (format "Transaction '%s'" (-> i :journal-entry/original-entity :transaction/description-original))}))))}]}))
|
|
||||||
|
|
||||||
(def row* (partial helper/row* grid-page))
|
(def row* (partial helper/row* grid-page))
|
||||||
|
|
||||||
@@ -1233,4 +740,5 @@
|
|||||||
(wrap-schema-enforce :hx-schema query-schema)
|
(wrap-schema-enforce :hx-schema query-schema)
|
||||||
(wrap-must {:activity :import :subject :ledger})
|
(wrap-must {:activity :import :subject :ledger})
|
||||||
(wrap-client-redirect-unauthenticated))))
|
(wrap-client-redirect-unauthenticated))))
|
||||||
balance-sheet/key->handler))
|
balance-sheet/key->handler
|
||||||
|
investigate/key->handler))
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[config.core :refer [env] :as env]
|
[config.core :refer [env] :as env]
|
||||||
[datomic.api :as dc]
|
[datomic.api :as dc]
|
||||||
|
[hiccup.util :as hu]
|
||||||
[iol-ion.utils :refer [by]]
|
[iol-ion.utils :refer [by]]
|
||||||
[malli.core :as mc])
|
[malli.core :as mc])
|
||||||
(:import [java.util UUID]
|
(:import [java.util UUID]
|
||||||
@@ -87,7 +88,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
(defn cell [{:keys [width click-event other-style]} c]
|
(defn cell [{:keys [width investigate-url other-style]} c]
|
||||||
(let [cell-contents (cond
|
(let [cell-contents (cond
|
||||||
|
|
||||||
(= :dollar (:format c))
|
(= :dollar (:format c))
|
||||||
@@ -104,8 +105,9 @@
|
|||||||
:else
|
:else
|
||||||
(str (:value c)))
|
(str (:value c)))
|
||||||
cell-contents (if (:filters c)
|
cell-contents (if (:filters c)
|
||||||
[:a #_{:on-click (dispatch-event [click-event (:filters c)])}
|
(com/link {:hx-get (hu/url investigate-url
|
||||||
cell-contents]
|
(update (:filters c) :numeric-code (fn [nc] (into [] nc))))}
|
||||||
|
cell-contents)
|
||||||
cell-contents)]
|
cell-contents)]
|
||||||
[:td.px-4.py-2
|
[:td.px-4.py-2
|
||||||
(cond-> {:style (cond-> {:width (str width "em")}
|
(cond-> {:style (cond-> {:width (str width "em")}
|
||||||
@@ -141,7 +143,7 @@
|
|||||||
(apply max counts)
|
(apply max counts)
|
||||||
0)))
|
0)))
|
||||||
|
|
||||||
(defn table [{:keys [table widths click-event]}]
|
(defn table [{:keys [table widths investigate-url]}]
|
||||||
(let [cell-count (cell-count table)]
|
(let [cell-count (cell-count table)]
|
||||||
(com/content-card {:class "inline-block overflow-hidden"}
|
(com/content-card {:class "inline-block overflow-hidden"}
|
||||||
[:div { :class "overflow-y-auto h-[70vh] m-4 inline-block"}
|
[:div { :class "overflow-y-auto h-[70vh] m-4 inline-block"}
|
||||||
@@ -153,7 +155,8 @@
|
|||||||
[:tr {:class " dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"}]
|
[:tr {:class " dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"}]
|
||||||
(map
|
(map
|
||||||
(fn [w header i]
|
(fn [w header i]
|
||||||
(cell {:width w :click-event click-event
|
(cell {:width w
|
||||||
|
:investigate-url investigate-url
|
||||||
:other-style {:position "sticky"
|
:other-style {:position "sticky"
|
||||||
:top (* header-row (+ 22 18))}} header))
|
:top (* header-row (+ 22 18))}} header))
|
||||||
widths
|
widths
|
||||||
@@ -177,10 +180,10 @@
|
|||||||
[[] 0]
|
[[] 0]
|
||||||
(concat row (repeat nil)))))]
|
(concat row (repeat nil)))))]
|
||||||
|
|
||||||
(cell {:click-event click-event} c))]))
|
(cell {:investigate-url investigate-url} c))]))
|
||||||
(conj [:tr (for [i (range cell-count)]
|
(conj [:tr (for [i (range cell-count)]
|
||||||
|
|
||||||
( cell {:click-event click-event} {:value " "}))])))
|
( cell {:investigate-url investigate-url} {:value " "}))])))
|
||||||
)])))
|
)])))
|
||||||
|
|
||||||
(defn concat-tables [tables]
|
(defn concat-tables [tables]
|
||||||
@@ -280,7 +283,7 @@
|
|||||||
[:div.text-2xl.font-bold.text-gray-600 (str "Balance Sheet - " (str/join ", " (map :client/name client))) ]
|
[:div.text-2xl.font-bold.text-gray-600 (str "Balance Sheet - " (str/join ", " (map :client/name client))) ]
|
||||||
(table {:widths (cond-> (into [30 ] (repeat 13 client-count))
|
(table {:widths (cond-> (into [30 ] (repeat 13 client-count))
|
||||||
(:include-comparison (:args data)) (into (repeat 13 (* 2 client-count))))
|
(:include-comparison (:args data)) (into (repeat 13 (* 2 client-count))))
|
||||||
:click-event ::investigate-clicked
|
:investigate-url (bidi.bidi/path-for ssr-routes/only-routes ::route/investigate)
|
||||||
:table report} ))))])
|
:table report} ))))])
|
||||||
|
|
||||||
(defn form* [request]
|
(defn form* [request]
|
||||||
|
|||||||
536
src/clj/auto_ap/ssr/ledger/common.clj
Normal file
536
src/clj/auto_ap/ssr/ledger/common.clj
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
(ns auto-ap.ssr.ledger.common
|
||||||
|
(:require [auto-ap.client-routes :as client-routes]
|
||||||
|
[auto-ap.datomic
|
||||||
|
:refer [add-sorter-fields apply-pagination apply-sort-4 conn
|
||||||
|
merge-query observable-query pull-many]]
|
||||||
|
[auto-ap.datomic.accounts :as d-accounts]
|
||||||
|
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||||
|
[auto-ap.permissions :refer [can?]]
|
||||||
|
[auto-ap.routes.invoice :as invoice-route]
|
||||||
|
[auto-ap.routes.ledger :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 html-response main-transformer
|
||||||
|
ref->enum-schema strip]]
|
||||||
|
[auto-ap.time :as atime]
|
||||||
|
[auto-ap.utils :refer [dollars-0?]]
|
||||||
|
[bidi.bidi :as bidi]
|
||||||
|
[clj-time.coerce :as coerce]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[datomic.api :as dc]
|
||||||
|
[hiccup.util :as hu]
|
||||||
|
[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]
|
||||||
|
(html-response (bank-account-filter* request)))
|
||||||
|
|
||||||
|
(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))))}))
|
||||||
|
|
||||||
|
(bank-account-filter* 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 "Account Code"}
|
||||||
|
[:div.flex.space-x-4.items-baseline
|
||||||
|
(com/int-input {:name "numeric-code-gte"
|
||||||
|
:id "numeric-code-gte"
|
||||||
|
:hx-preserve "true"
|
||||||
|
:class "hot-filter w-20"
|
||||||
|
:value (:numeric-code-gte (:query-params request))
|
||||||
|
:placeholder "40000"
|
||||||
|
:size :small})
|
||||||
|
[:div.align-baseline
|
||||||
|
"to"]
|
||||||
|
(com/int-input {:name "numeric-code-lte"
|
||||||
|
:hx-preserve "true"
|
||||||
|
:id "numeric-code-lte"
|
||||||
|
:class "hot-filter w-20"
|
||||||
|
:value (:numeric-code-lte (:query-params request))
|
||||||
|
:placeholder "50000"
|
||||||
|
: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)
|
||||||
|
(:client-id query-params)
|
||||||
|
(when (:client-code request)
|
||||||
|
[:client/code (:client-code request)]))
|
||||||
|
args query-params
|
||||||
|
#_#__ (clojure.pprint/pprint 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)]})
|
||||||
|
(:external? route-params)
|
||||||
|
(merge-query {:query {:where ['[?e :journal-entry/external-id]]}})
|
||||||
|
|
||||||
|
(: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 (:numeric-code-lte args)
|
||||||
|
(:numeric-code-gte args)
|
||||||
|
(seq (:numeric-code args))
|
||||||
|
(:account args)
|
||||||
|
(:db/id (:bank-account args))
|
||||||
|
(not-empty (:location args)))
|
||||||
|
(merge-query {:query {:where ['[?e :journal-entry/line-items ?li]]}})
|
||||||
|
|
||||||
|
(or (:numeric-code-gte args)
|
||||||
|
(:numeric-code-lte 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 [(or (:numeric-code-gte args) 0) (or (:numeric-code-lte args) 99999)]})
|
||||||
|
|
||||||
|
(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 [ (map (juxt :from :to ) (:numeric-code args))]})
|
||||||
|
(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)]})
|
||||||
|
|
||||||
|
(:db/id (:bank-account args))
|
||||||
|
(merge-query {:query {:in ['?a]
|
||||||
|
:where ['[?li :journal-entry-line/account ?a]]}
|
||||||
|
:args [(:db/id (:bank-account 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-4 (assoc query-params :default-asc? true))
|
||||||
|
(apply-pagination query-params))))
|
||||||
|
|
||||||
|
(def default-read
|
||||||
|
'[:journal-entry/amount
|
||||||
|
:journal-entry/alternate-description
|
||||||
|
:journal-entry/source
|
||||||
|
:journal-entry/external-id
|
||||||
|
:db/id
|
||||||
|
[:journal-entry/date :xform clj-time.coerce/from-date]
|
||||||
|
{:journal-entry/vendor [:vendor/name :db/id]
|
||||||
|
:journal-entry/original-entity [:invoice/invoice-number
|
||||||
|
:invoice/source-url
|
||||||
|
:transaction/description-original :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/running-balance
|
||||||
|
:journal-entry-line/credit
|
||||||
|
{:journal-entry-line/account [:account/name :db/id :account/numeric-code
|
||||||
|
:bank-account/name :bank-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)]))
|
||||||
|
(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 (or (:account/name account)
|
||||||
|
(:bank-account/name (:journal-entry-line/account jel)))]]
|
||||||
|
(list
|
||||||
|
|
||||||
|
(if account-name
|
||||||
|
[:div {:x-data "popper()"}
|
||||||
|
[:div.text-left.underline.cursor-pointer {:x-ref "source"}
|
||||||
|
(:journal-entry-line/location jel) ": "
|
||||||
|
(or (:account/numeric-code account) (:bank-account/numeric-code account))
|
||||||
|
" - " account-name]
|
||||||
|
(com/tooltip {:x-bind "tooltip" :class "absolute"}
|
||||||
|
"Running Balance: " (some->> (:journal-entry-line/running-balance jel)
|
||||||
|
(format "$%,.2f")))]
|
||||||
|
[:div.text-left (com/pill {:color :yellow} "Unassigned")])
|
||||||
|
[:div.text-right.text-underline (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")))])]))
|
||||||
|
|
||||||
|
(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]]
|
||||||
|
[:numeric-code {:optional true :decode/string clojure.edn/read-string}
|
||||||
|
[:maybe [:vector [:map [:from nat-int?]
|
||||||
|
[:to nat-int?]]] ]]
|
||||||
|
[:numeric-code-gte {:optional true} [:maybe nat-int?]]
|
||||||
|
[:numeric-code-lte {:optional true} [:maybe nat-int?]]
|
||||||
|
[: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/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]]]]))
|
||||||
|
|
||||||
|
(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]
|
||||||
|
(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]
|
||||||
|
(cond
|
||||||
|
(= (-> request :query-params :sort first :name) "Vendor")
|
||||||
|
(-> entity :journal-entry/vendor :vendor/name)
|
||||||
|
|
||||||
|
(= (-> request :query-params :sort first :name) "Source")
|
||||||
|
(-> entity :journal-entry/source)
|
||||||
|
|
||||||
|
:else nil))
|
||||||
|
: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 :journal-entry/client :client/name) ])}
|
||||||
|
|
||||||
|
{:key "vendor"
|
||||||
|
:name "Vendor"
|
||||||
|
:sort-key "vendor"
|
||||||
|
:render (fn [e] (or (-> e :journal-entry/vendor :vendor/name)
|
||||||
|
[:span.italic.text-gray-400 (-> e :journal-entry/alternate-description)]))}
|
||||||
|
{:key "source"
|
||||||
|
:name "Source"
|
||||||
|
:sort-key "source"
|
||||||
|
:hide? (fn [args]
|
||||||
|
(not (:external? (:route-params args))))
|
||||||
|
:render :journal-entry/source}
|
||||||
|
{:key "external-id"
|
||||||
|
:name "External Id"
|
||||||
|
:sort-key "external-id"
|
||||||
|
:class "max-w-[12rem]"
|
||||||
|
:hide? (fn [args]
|
||||||
|
(not (:external? (:route-params args))))
|
||||||
|
:render (fn [x] [:p.truncate ( :journal-entry/external-id x)])}
|
||||||
|
{: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
|
||||||
|
(cond-> []
|
||||||
|
(-> i :journal-entry/original-entity :invoice/invoice-number)
|
||||||
|
(conj
|
||||||
|
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||||
|
::invoice-route/all-page)
|
||||||
|
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
|
||||||
|
:color :primary
|
||||||
|
:content (format "Invoice '%s'" (-> i :journal-entry/original-entity :invoice/invoice-number))})
|
||||||
|
(-> i :journal-entry/original-entity :invoice/source-url)
|
||||||
|
{:link (-> i :journal-entry/original-entity :invoice/source-url)
|
||||||
|
:color :secondary
|
||||||
|
:content (str "File")}
|
||||||
|
|
||||||
|
(-> i :journal-entry/original-entity :transaction/description-original)
|
||||||
|
(conj
|
||||||
|
{:link (hu/url (bidi/path-for client-routes/routes
|
||||||
|
:transactions)
|
||||||
|
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
|
||||||
|
:color :primary
|
||||||
|
:content (format "Transaction '%s'" (-> i :journal-entry/original-entity :transaction/description-original))}))))}]}))
|
||||||
37
src/clj/auto_ap/ssr/ledger/investigate.clj
Normal file
37
src/clj/auto_ap/ssr/ledger/investigate.clj
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
(ns auto-ap.ssr.ledger.investigate
|
||||||
|
(:require [auto-ap.permissions :refer [wrap-must]]
|
||||||
|
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||||
|
[auto-ap.routes.ledger :as route]
|
||||||
|
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated]]
|
||||||
|
[auto-ap.ssr.components :as com]
|
||||||
|
[auto-ap.ssr.grid-page-helper :refer [table*]]
|
||||||
|
[auto-ap.ssr.ledger.common :refer [grid-page query-schema]]
|
||||||
|
[auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers
|
||||||
|
modal-response wrap-merge-prior-hx
|
||||||
|
wrap-schema-enforce]]))
|
||||||
|
|
||||||
|
(defn investigate [request]
|
||||||
|
(modal-response
|
||||||
|
(com/modal {:class "h-[600px]"}
|
||||||
|
(com/modal-card {}
|
||||||
|
[:div "Ledger entries"]
|
||||||
|
[:div
|
||||||
|
(table*
|
||||||
|
grid-page
|
||||||
|
identity
|
||||||
|
request)]
|
||||||
|
nil)
|
||||||
|
)))
|
||||||
|
|
||||||
|
(def key->handler
|
||||||
|
(apply-middleware-to-all-handlers
|
||||||
|
(->
|
||||||
|
{::route/investigate investigate }
|
||||||
|
)
|
||||||
|
(fn [h]
|
||||||
|
(-> h
|
||||||
|
(wrap-merge-prior-hx)
|
||||||
|
(wrap-schema-enforce :query-schema query-schema)
|
||||||
|
(wrap-schema-enforce :hx-schema query-schema)
|
||||||
|
(wrap-must {:activity :read :subject :ledger})
|
||||||
|
(wrap-client-redirect-unauthenticated)))))
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
"/external-import-new" {"" ::external-import-page
|
"/external-import-new" {"" ::external-import-page
|
||||||
"/parse" ::external-import-parse
|
"/parse" ::external-import-parse
|
||||||
"/import" ::external-import-import}
|
"/import" ::external-import-import}
|
||||||
|
"/investigate" ::investigate
|
||||||
"/table" ::table
|
"/table" ::table
|
||||||
"/bank-account-filter" ::bank-account-filter
|
"/bank-account-filter" ::bank-account-filter
|
||||||
"/reports/balance-sheet" {"" ::balance-sheet
|
"/reports/balance-sheet" {"" ::balance-sheet
|
||||||
|
|||||||
Reference in New Issue
Block a user