invoices page works, needs dialog.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
[auto-ap.routes.admin.transaction-rules :as transaction-rules]
|
||||
[auto-ap.routes.admin.vendors :as v-routes]
|
||||
[auto-ap.routes.payments :as payment-routes]
|
||||
[auto-ap.routes.invoice :as invoice-route]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.hiccup-helper :as hh]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
@@ -78,17 +79,21 @@
|
||||
:icon svg/accounting-invoice-mail}
|
||||
"Invoices")
|
||||
(sub-menu- (hx/alpine-appear {:x-show "open"})
|
||||
(menu-button- {:href (bidi/path-for client-routes/routes
|
||||
:invoices)}
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/page)
|
||||
{:date-range "month"})}
|
||||
"All")
|
||||
(menu-button- {:href (bidi/path-for client-routes/routes
|
||||
:paid-invoices)}
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/page)
|
||||
{:date-range "month"})}
|
||||
"Paid")
|
||||
(menu-button- {:href (bidi/path-for client-routes/routes
|
||||
:unpaid-invoices)}
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/unpaid-page)
|
||||
{:date-range "month"})}
|
||||
"Unpaid")
|
||||
(menu-button- {:href (bidi/path-for client-routes/routes
|
||||
:voided-invoices)}
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/voided-page)
|
||||
{:date-range "month"})}
|
||||
"Voided"))]
|
||||
[:li {:x-data (hx/json {:open false})}
|
||||
(menu-button- {:icon svg/receipt-register-1
|
||||
|
||||
30
src/clj/auto_ap/ssr/components/link_dropdown.clj
Normal file
30
src/clj/auto_ap/ssr/components/link_dropdown.clj
Normal file
@@ -0,0 +1,30 @@
|
||||
(ns auto-ap.ssr.components.link-dropdown
|
||||
(:require [auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]))
|
||||
|
||||
(defn link-dropdown [links]
|
||||
(if (<= (count links) 2)
|
||||
[:div.flex.flex-col.gap-y-2
|
||||
(for [l links]
|
||||
[:div.flex-initial
|
||||
[:a {:href (:link l)}
|
||||
(com/pill {:color (or (:color l) :primary) :class "truncate"}
|
||||
(:content l))]])]
|
||||
[:div {:x-data (hx/json {:popper nil
|
||||
:show false})
|
||||
"@click.outside" "show=false"
|
||||
|
||||
:x-init "popper = Popper.createPopper($refs.link, $refs.tooltip, {placement: 'bottom', strategy: 'fixed'})"}
|
||||
|
||||
(com/a-icon-button {:x-ref "link" "@click.prevent" "show=!show; $nextTick(() => popper.update());" :class "relative"}
|
||||
svg/three-dots
|
||||
(com/badge {} (count links)))
|
||||
[:div.divide-y.divide-gray-200.bg-white.rounded-lg.shadow (hx/alpine-appear {:x-ref "tooltip" :x-show "show" :data-key "show"})
|
||||
[:div {:class "p-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}
|
||||
[:div.flex.flex-col.gap-y-2
|
||||
(for [l links]
|
||||
[:div.flex-initial
|
||||
[:a {:href (:link l)}
|
||||
(com/pill {:color (or (:color l) :primary) :class "truncate w-24 block shrink grow-0"}
|
||||
(:content l))]])]]]]))
|
||||
@@ -28,6 +28,7 @@
|
||||
[auto-ap.ssr.pos.refunds :as pos-refunds]
|
||||
[auto-ap.ssr.pos.sales-orders :as pos-sales]
|
||||
[auto-ap.ssr.pos.tenders :as pos-tenders]
|
||||
[auto-ap.ssr.invoices :as invoice]
|
||||
[auto-ap.ssr.search :as search]
|
||||
[auto-ap.ssr.transaction.insights :as insights]
|
||||
[auto-ap.ssr.users :as users]
|
||||
@@ -81,6 +82,7 @@
|
||||
:admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page))
|
||||
:search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))}
|
||||
(into company-1099/key->handler)
|
||||
(into invoice/key->handler)
|
||||
(into import-batch/key->handler)
|
||||
(into pos-sales/key->handler)
|
||||
(into pos-expected-deposits/key->handler)
|
||||
|
||||
559
src/clj/auto_ap/ssr/invoices.clj
Normal file
559
src/clj/auto_ap/ssr/invoices.clj
Normal file
@@ -0,0 +1,559 @@
|
||||
(ns auto-ap.ssr.invoices
|
||||
(:require [auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query observable-query
|
||||
pull-many]]
|
||||
[auto-ap.graphql.checks :as gq-checks]
|
||||
[auto-ap.graphql.utils :refer [assert-can-see-client
|
||||
exception->notification
|
||||
extract-client-ids notify-if-locked]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.permissions :refer [can?]]
|
||||
[auto-ap.routes.invoice :as route]
|
||||
[auto-ap.routes.payments :as payment-route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
[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 :refer [wrap-apply-sort]]
|
||||
[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 clj-date-schema
|
||||
dissoc-nil-transformer entity-id html-response
|
||||
main-transformer modal-response ref->enum-schema strip
|
||||
wrap-entity wrap-merge-prior-hx wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[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]
|
||||
[malli.transform :as mt]))
|
||||
|
||||
|
||||
(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#payment-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}))
|
||||
(date-range-field* request)
|
||||
(com/field {:label "Check #"}
|
||||
(com/text-input {:name "check-number"
|
||||
:id "check-number"
|
||||
:class "hot-filter"
|
||||
:value (:check-number (:query-params request))
|
||||
:placeholder "10001"
|
||||
:size :small}))
|
||||
(com/field {:label "Invoice #"}
|
||||
(com/text-input {:name "invoice-number"
|
||||
:id "invoice-number"
|
||||
:class "hot-filter"
|
||||
:value (:invoice-number (:query-params request))
|
||||
:placeholder "10001"
|
||||
: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)]])
|
||||
|
||||
|
||||
;; TODO clientize
|
||||
(def default-read '[:db/id
|
||||
:invoice/invoice-number
|
||||
:invoice/total
|
||||
:invoice/source-url
|
||||
|
||||
[:invoice/date :xform clj-time.coerce/from-date]
|
||||
{:invoice/client [:client/code :db/id :client/name]
|
||||
:invoice/expense-accounts [* {:invoice-expense-account/account [:account/name :db/id
|
||||
:account/location
|
||||
{:account/client-overrides [:account-client-override/name
|
||||
{:account-client-override/client [:db/id]}]}]}]
|
||||
[:transaction/_invoices :as :invoice/transaction] [:db/id]
|
||||
[:payment/_invoices :as :invoice/payments] [:db/id :payment/date :payment/amount
|
||||
{[:transaction/_payment :as :payment/transaction] [:db/id]}]
|
||||
[:invoice/status :xform iol-ion.query/ident] [:db/ident]
|
||||
:invoice/vendor [:vendor/name :db/id]}])
|
||||
|
||||
(defn fetch-ids [db {:keys [query-params] :as request}]
|
||||
(let [valid-clients (extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
query
|
||||
(if (:exact-match-id query-params)
|
||||
{:query {:find '[?e]
|
||||
:in '[$ ?e [?c ...]]
|
||||
:where '[[?e :invoice/client ?c]]}
|
||||
:args [db
|
||||
(:exact-match-id query-params)
|
||||
valid-clients]}
|
||||
(cond-> {:query {:find []
|
||||
:in '[$ [?clients ?start ?end]]
|
||||
:where '[[(iol-ion.query/scan-invoices $ ?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)]]}
|
||||
|
||||
|
||||
(:client-id query-params)
|
||||
(merge-query {:query {:in ['?client-id]
|
||||
:where ['[?e :invoice/client ?client-id]]}
|
||||
:args [(:client-id query-params)]})
|
||||
|
||||
(:client-code query-params)
|
||||
(merge-query {:query {:in ['?client-code]
|
||||
:where ['[?e :invoice/client ?client-id]
|
||||
'[?client-id :client/code ?client-code]]}
|
||||
:args [(:client-code query-params)]})
|
||||
|
||||
|
||||
(:start (:due-range query-params)) (merge-query {:query {:in '[?start-due]
|
||||
:where ['[?e :invoice/due ?due]
|
||||
'[(>= ?due ?start-due)]]}
|
||||
:args [(coerce/to-date (:start (:due-range query-params)))]})
|
||||
|
||||
(:end (:due-range query-params)) (merge-query {:query {:in '[?end-due]
|
||||
:where ['[?e :invoice/due ?due]
|
||||
'[(<= ?due ?end-due)]]}
|
||||
:args [(coerce/to-date (:end (:due-range query-params)))]})
|
||||
|
||||
|
||||
(:import-status query-params)
|
||||
(merge-query {:query {:in ['?import-status]
|
||||
:where ['[?e :invoice/import-status ?import-status]]}
|
||||
:args [(:import-status query-params)]})
|
||||
(:status query-params)
|
||||
(merge-query {:query {:in ['?status]
|
||||
:where ['[?e :invoice/status ?status]]}
|
||||
:args [(:status query-params)]})
|
||||
(:vendor query-params)
|
||||
(merge-query {:query {:in ['?vendor-id]
|
||||
:where ['[?e :invoice/vendor ?vendor-id]]}
|
||||
:args [(:db/id (:vendor query-params))]})
|
||||
|
||||
(:account-id query-params)
|
||||
(merge-query {:query {:in ['?account-id]
|
||||
:where ['[?e :invoice/expense-accounts ?iea ?]
|
||||
'[?iea :invoice-expense-account/account ?account-id]]}
|
||||
:args [(:account-id query-params)]})
|
||||
|
||||
(:amount-gte query-params)
|
||||
(merge-query {:query {:in ['?amount-gte]
|
||||
:where ['[?e :invoice/total ?total-filter]
|
||||
'[(>= ?total-filter ?amount-gte)]]}
|
||||
:args [(:amount-gte query-params)]})
|
||||
|
||||
(:amount-lte query-params)
|
||||
(merge-query {:query {:in ['?amount-lte]
|
||||
:where ['[?e :invoice/total ?total-filter]
|
||||
'[(<= ?total-filter ?amount-lte)]]}
|
||||
:args [(:amount-lte query-params)]})
|
||||
|
||||
(not-empty (:invoice-number query-params))
|
||||
(merge-query {:query {:in ['?invoice-number-like]
|
||||
:where ['[?e :invoice/invoice-number ?invoice-number]
|
||||
'[(.contains ^String ?invoice-number ?invoice-number-like)]]}
|
||||
:args [(:invoice-number query-params)]})
|
||||
|
||||
(:scheduled-payments query-params)
|
||||
(merge-query {:query {:in []
|
||||
:where ['[?e :invoice/scheduled-payment]]}
|
||||
:args []})
|
||||
|
||||
(:unresolved query-params)
|
||||
(merge-query {:query {:in []
|
||||
:where ['(or-join [?e]
|
||||
(not [?e :invoice/expense-accounts])
|
||||
(and [?e :invoice/expense-accounts ?ea]
|
||||
(not [?ea :invoice-expense-account/account])))]}
|
||||
:args []})
|
||||
|
||||
(seq (:location query-params))
|
||||
(merge-query {:query {:in ['?location]
|
||||
:where ['[?e :invoice/expense-accounts ?eas]
|
||||
'[?eas :invoice-expense-account/location ?location]]}
|
||||
:args [(:location query-params)]})
|
||||
|
||||
(:sort query-params) (add-sorter-fields {"client" ['[?e :invoice/client ?c]
|
||||
'[?c :client/name ?sort-client]]
|
||||
"vendor" ['[?e :invoice/vendor ?v]
|
||||
'[?v :vendor/name ?sort-vendor]]
|
||||
"description-original" ['[?e :transaction/description-original ?sort-description-original]]
|
||||
"location" ['[?e :invoice/expense-accounts ?iea]
|
||||
'[?iea :invoice-expense-account/location ?sort-location]]
|
||||
"date" ['[?e :invoice/date ?sort-date]]
|
||||
"due" ['[(get-else $ ?e :invoice/due #inst "2050-01-01") ?sort-due]]
|
||||
"invoice-number" ['[?e :invoice/invoice-number ?sort-invoice-number]]
|
||||
"total" ['[?e :invoice/total ?sort-total]]
|
||||
"outstanding-balance" ['[?e :invoice/outstanding-balance ?sort-outstanding-balance]]}
|
||||
query-params)
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]}})))]
|
||||
(->> (observable-query query)
|
||||
(apply-sort-3 (assoc query-params :default-asc? false))
|
||||
(apply-pagination query-params))))
|
||||
|
||||
|
||||
(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 fetch-page [request]
|
||||
(let [db (dc/db conn)
|
||||
{ids-to-retrieve :ids matching-count :count} (fetch-ids db request)]
|
||||
|
||||
[(->> (hydrate-results ids-to-retrieve db request))
|
||||
matching-count]))
|
||||
|
||||
(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]]
|
||||
[:payment-type {:optional true} [:maybe (ref->enum-schema "payment-type")]]
|
||||
[:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/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 "payment-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))
|
||||
|
||||
;; TODO fix parsing of query params
|
||||
(def grid-page
|
||||
(helper/build {:id "entity-table"
|
||||
:nav (com/main-aside-nav)
|
||||
:check-boxes? true
|
||||
: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]
|
||||
[(when (can? (:identity request) {:subject :payment :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" "#payment-filters"
|
||||
:color :red}
|
||||
"Void selected"))])
|
||||
:row-buttons (fn [_ entity]
|
||||
[(when (= :invoice-status/unpaid (:invoice/status entity))
|
||||
(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))])
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
||||
"Invoices"]]
|
||||
:title (fn [r]
|
||||
(str
|
||||
(some-> r :query-params :status name str/capitalize (str " "))
|
||||
"Invoices"))
|
||||
:entity-name "invoices"
|
||||
:route ::route/table
|
||||
:headers [{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [args]
|
||||
(= (count (:clients args)) 1))
|
||||
:render #(-> % :invoice/client :client/name)}
|
||||
{:key "vendor"
|
||||
:name "Vendor"
|
||||
:sort-key "vendor"
|
||||
:render #(-> % :invoice/vendor :vendor/name)}
|
||||
{:key "invoice-number"
|
||||
:name "Invoice number"
|
||||
:sort-key "invoice-number"
|
||||
:render :invoice/invoice-number}
|
||||
{:key "date"
|
||||
:sort-key "date"
|
||||
:name "Date"
|
||||
:show-starting "lg"
|
||||
:render (fn [{:invoice/keys [date]}]
|
||||
(some-> date (atime/unparse-local atime/normal-date)))}
|
||||
{:key "status"
|
||||
:name "Status"
|
||||
:render (fn [{:invoice/keys [status]}]
|
||||
(condp = status
|
||||
:invoice-status/paid
|
||||
(com/pill {:color :primary} "Paid")
|
||||
|
||||
:invoice-status/unpaid
|
||||
(com/pill {:color :secondary} "Unpaid")
|
||||
:invoice-status/voided
|
||||
(com/pill {:color :red} "Voided")
|
||||
nil
|
||||
""))}
|
||||
{:key "accounts"
|
||||
:name "Account"
|
||||
:show-starting "lg"
|
||||
:render (fn [{:invoice/keys [expense-accounts]}]
|
||||
[:div.flex.flex-col.gap-y-2
|
||||
(when (first expense-accounts)
|
||||
[:div.flex-initial
|
||||
(com/pill {:color :primary}
|
||||
(:account/name (:invoice-expense-account/account (first expense-accounts))))])
|
||||
(when (> (count expense-accounts) 1)
|
||||
[:div.flex-initial
|
||||
(com/pill {:color :secondary}
|
||||
"+ " (dec (count expense-accounts)) " more")])])}
|
||||
|
||||
{:key "total"
|
||||
:name "Total"
|
||||
:sort-key "total"
|
||||
:class "text-right"
|
||||
:render (fn [{:invoice/keys [total]}]
|
||||
(some->> total (format "$%,.2f")))}
|
||||
{:key "links"
|
||||
:name "Links"
|
||||
:show-starting "lg"
|
||||
:class "w-8"
|
||||
:render (fn [i]
|
||||
(link-dropdown
|
||||
(concat (->> i
|
||||
:invoice/payments
|
||||
(mapcat (fn [p]
|
||||
(cond-> [{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::payment-route/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/standard-time) (#(str " 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/source-url i)
|
||||
[{:link (:invoice/source-url i)
|
||||
:color :secondary
|
||||
:content "File"}]))))}]}))
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
|
||||
(comment
|
||||
(mc/decode query-schema {"exact-match-id" "123"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
(mc/decode query-schema {} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
(mc/decode query-schema {"exact-match-id" nil} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
(mc/decode query-schema {"exact-match-id" ""} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
(mc/decode query-schema {"start-date" "12/21/2023"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
(mc/decode query-schema {"payment-type" "food"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
(mc/decode query-schema {"vendor" "87"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
|
||||
|
||||
(mc/decode query-schema {"start-date" #inst "2023-12-21T08:00:00.000-00:00"} (mt/transformer main-transformer mt/strip-extra-keys-transformer)))
|
||||
|
||||
;; TODO clientize accounts
|
||||
|
||||
(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." {}))))
|
||||
|
||||
(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))}))
|
||||
|
||||
;; TODO use decoding here
|
||||
(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-payments-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 [all-selected (:all-selected (:form-params request))
|
||||
selected (:selected (:form-params request))
|
||||
ids (cond
|
||||
all-selected
|
||||
(:ids (fetch-ids (dc/db conn) (-> request
|
||||
(assoc :query-params (:form-params request))
|
||||
(assoc-in [:query-params :start] 0)
|
||||
(assoc-in [:query-params :per-page] 250))))
|
||||
|
||||
|
||||
:else
|
||||
selected)
|
||||
updated-count (void-payments-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))})})))
|
||||
|
||||
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{::route/page (helper/page-route grid-page)
|
||||
::route/delete (-> delete
|
||||
(wrap-entity [:route-params :db/id] default-read)
|
||||
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
|
||||
::route/bulk-delete-confirm (-> bulk-delete-dialog-confirm
|
||||
(wrap-schema-enforce :form-schema query-schema)
|
||||
(wrap-admin))
|
||||
::route/bulk-delete (-> bulk-delete-dialog
|
||||
(wrap-admin))
|
||||
|
||||
|
||||
::route/table (helper/table-route grid-page)}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(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)))))
|
||||
@@ -10,13 +10,14 @@
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.permissions :refer [can?]]
|
||||
[auto-ap.routes.payments :as route]
|
||||
[auto-ap.routes.invoice :as invoice-route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.components.bank-account-icon :as bank-account-icon]
|
||||
[auto-ap.ssr.grid-page-helper :as helper :refer [sort->query
|
||||
wrap-apply-sort]]
|
||||
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
|
||||
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.pos.common :refer [date-range-field*]]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
@@ -48,6 +49,7 @@
|
||||
svg/x)]])]
|
||||
[:div {:id "exact-match-id-tag"}]))
|
||||
|
||||
;; TODO use query-params instead of parsed-query-params
|
||||
(defn filters [request]
|
||||
[:form#payment-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||
@@ -56,13 +58,13 @@
|
||||
"hx-indicator" "#entity-table"}
|
||||
|
||||
(com/hidden {:name "status"
|
||||
:value (some-> (:status (:parsed-query-params request)) name)})
|
||||
: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 (:parsed-query-params request))
|
||||
:value (:vendor (:query-params request))
|
||||
:value-fn :db/id
|
||||
:content-fn :vendor/name}))
|
||||
(date-range-field* request)
|
||||
@@ -70,14 +72,14 @@
|
||||
(com/text-input {:name "check-number"
|
||||
:id "check-number"
|
||||
:class "hot-filter"
|
||||
:value (:check-number (:parsed-query-params request))
|
||||
:value (:check-number (:query-params request))
|
||||
:placeholder "10001"
|
||||
:size :small}))
|
||||
(com/field {:label "Invoice #"}
|
||||
(com/text-input {:name "invoice-number"
|
||||
:id "invoice-number"
|
||||
:class "hot-filter"
|
||||
:value (:invoice-number (:parsed-query-params request))
|
||||
:value (:invoice-number (:query-params request))
|
||||
:placeholder "10001"
|
||||
:size :small}))
|
||||
|
||||
@@ -87,7 +89,7 @@
|
||||
:id "amount-gte"
|
||||
:hx-preserve "true"
|
||||
:class "hot-filter w-20"
|
||||
:value (:amount-gte (:parsed-query-params request))
|
||||
:value (:amount-gte (:query-params request))
|
||||
:placeholder "0.01"
|
||||
:size :small})
|
||||
[:div.align-baseline
|
||||
@@ -96,13 +98,13 @@
|
||||
:hx-preserve "true"
|
||||
:id "amount-lte"
|
||||
:class "hot-filter w-20"
|
||||
:value (:amount-lte (:parsed-query-params request))
|
||||
:value (:amount-lte (:query-params request))
|
||||
:placeholder "9999.34"
|
||||
:size :small})])
|
||||
(com/field {:label "Payment Type"}
|
||||
(com/radio {:size :small
|
||||
:name "payment-type"
|
||||
:value (:payment-type (:parsed-query-params request))
|
||||
:value (:payment-type (:query-params request))
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "cash"
|
||||
@@ -248,31 +250,8 @@
|
||||
[(->> (hydrate-results ids-to-retrieve db request))
|
||||
matching-count]))
|
||||
|
||||
(defn- render-links [links]
|
||||
(if (<= (count links) 2)
|
||||
[:div.flex.flex-col.space-y-1
|
||||
(for [l links]
|
||||
[:a {:href (:link l)}
|
||||
(com/pill {:color (or (:color l) :primary) :class "truncate w-24 block shrink grow-0"}
|
||||
(:content l))])]
|
||||
[:div {:x-data (hx/json {:popper nil
|
||||
:show false})
|
||||
"@click.outside" "show=false"
|
||||
|
||||
:x-init "popper = Popper.createPopper($refs.link, $refs.tooltip, {placement: 'bottom', strategy: 'fixed'})"}
|
||||
(com/a-icon-button {:x-ref "link" "@click.prevent" "show=!show; $nextTick(() => popper.update());" :class "relative"}
|
||||
svg/three-dots
|
||||
(com/badge {} (count links)))
|
||||
[:div.divide-y.divide-gray-200.bg-white.rounded-lg.shadow (hx/alpine-appear {:x-ref "tooltip" :x-show "show" :data-key "show"})
|
||||
[:div {:class "p-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}
|
||||
[:div.flex.flex-col.space-y-1
|
||||
(for [l links]
|
||||
[:a {:href (:link l)}
|
||||
(com/pill {:color (or (:color l) :primary) :class "truncate w-24 block shrink grow-0"}
|
||||
(:content l))])]]]]))
|
||||
|
||||
(def query-schema (mc/schema
|
||||
[:maybe [:map {:date-range [:date-range :start-date :end-date] }
|
||||
[: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]]
|
||||
@@ -385,15 +364,15 @@
|
||||
:name "Links"
|
||||
:class "w-8"
|
||||
:render (fn [p]
|
||||
(render-links (concat (->> p :payment/invoices (map (fn [invoice]
|
||||
{:link (hu/url (bidi/path-for client-routes/routes
|
||||
:invoices)
|
||||
(link-dropdown (concat (->> p :payment/invoices (map (fn [invoice]
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/page)
|
||||
{:exact-match-id (:db/id invoice)})
|
||||
:content (str "Inv. " (:invoice/invoice-number invoice))})))
|
||||
(some-> p :transaction/_payment ((fn [t]
|
||||
[{:link (hu/url (bidi/path-for client-routes/routes
|
||||
:transactions)
|
||||
{:exact-match-id 1})
|
||||
{:exact-match-id (:db/id (first t))})
|
||||
:color :secondary
|
||||
:content "Transaction"}]))))))}]}))
|
||||
|
||||
|
||||
12
src/cljc/auto_ap/routes/invoice.cljc
Normal file
12
src/cljc/auto_ap/routes/invoice.cljc
Normal file
@@ -0,0 +1,12 @@
|
||||
(ns auto-ap.routes.invoice)
|
||||
(def routes {"" {:get ::page}
|
||||
"/bulk-delete" {:get ::bulk-delete
|
||||
:delete ::bulk-delete-confirm}
|
||||
["/" [#"\d+" :db/id]] {:delete ::delete}
|
||||
"/table" ::table
|
||||
|
||||
"/glimpse" {"" {:get :invoice-glimpse
|
||||
:post :invoice-glimpse-upload
|
||||
["/" [#"\w+" :textract-invoice-id]] {:get :invoice-glimpse-textract-invoice
|
||||
"/create" {:post :invoice-glimpse-create-invoice}
|
||||
"/update" {:patch :invoice-glimpse-update-textract-invoice}}}}})
|
||||
@@ -5,6 +5,7 @@
|
||||
[auto-ap.routes.indicators :as indicator-routes]
|
||||
[auto-ap.routes.admin.vendors :as v-routes]
|
||||
[auto-ap.routes.payments :as p-routes]
|
||||
[auto-ap.routes.invoice :as i-routes]
|
||||
[auto-ap.routes.admin.clients :as ac-routes]
|
||||
[auto-ap.routes.admin.transaction-rules :as tr-routes]))
|
||||
|
||||
@@ -12,11 +13,7 @@
|
||||
"logout" :logout
|
||||
"search" :search
|
||||
"indicators" indicator-routes/routes
|
||||
"invoice" {"/glimpse" {"" {:get :invoice-glimpse
|
||||
:post :invoice-glimpse-upload
|
||||
["/" [#"\w+" :textract-invoice-id]] {:get :invoice-glimpse-textract-invoice
|
||||
"/create" {:post :invoice-glimpse-create-invoice}
|
||||
"/update" {:patch :invoice-glimpse-update-textract-invoice}}}}}
|
||||
|
||||
"account" {"/search" {:get :account-search}}
|
||||
"admin" {"" :auto-ap.routes.admin/page
|
||||
"/client" ac-routes/routes
|
||||
@@ -66,6 +63,7 @@
|
||||
"/table" {:get :pos-cash-drawer-shift-table}}}
|
||||
|
||||
"payment" p-routes/routes
|
||||
"invoice" i-routes/routes
|
||||
"vendor" {"/search" :vendor-search}
|
||||
;; TODO Include IDS in routes for company-specific things, as opposed to headers
|
||||
"company" {"" :company
|
||||
|
||||
Reference in New Issue
Block a user