working on import page
This commit is contained in:
@@ -911,23 +911,24 @@
|
||||
(let [[c [l]] (get-square-client-and-location "NGFA")]))
|
||||
|
||||
(clojure.data.csv/write-csv *out*
|
||||
(for [c (get-square-clients)
|
||||
l (:client/square-locations c)
|
||||
:when (:square-location/client-location l)
|
||||
bad-row (try (->> @(search c l (coerce/to-date-time #inst "2024-04-01T00:00:00-07:00") (coerce/to-date-time #inst "2024-04-15T23:59:00-07:00"))
|
||||
(filter #(not (should-import-order? %)))
|
||||
(map #(first (deref (order->sales-order c l %))))
|
||||
(filter (fn already-exists [o]
|
||||
(when (:sales-order/external-id o)
|
||||
(seq (dc/q '[:find ?i
|
||||
:in $ ?ei
|
||||
:where [?i :sales-order/external-id ?ei]]
|
||||
(dc/db conn)
|
||||
(:sales-order/external-id o)))))))
|
||||
(catch Exception e
|
||||
[]))]
|
||||
[(:client/code c) (atime/unparse-local (clj-time.coerce/to-date-time (:sales-order/date bad-row)) atime/normal-date) (:sales-order/total bad-row) (:sales-order/tax bad-row) (:sales-order/tip bad-row) (:db/id bad-row)])
|
||||
:separator \tab)
|
||||
(for [c (get-square-clients)
|
||||
l (:client/square-locations c)
|
||||
:when (:square-location/client-location l)
|
||||
bad-row (try (->> @(search c l (coerce/to-date-time #inst "2024-04-01T00:00:00-07:00") (coerce/to-date-time #inst "2024-04-15T23:59:00-07:00"))
|
||||
(filter #(not (should-import-order? %)))
|
||||
(map #(first (deref (order->sales-order c l %))))
|
||||
(filter (fn already-exists [o]
|
||||
(when (:sales-order/external-id o)
|
||||
(seq (dc/q '[:find ?i
|
||||
:in $ ?ei
|
||||
:where [?i :sales-order/external-id ?ei]]
|
||||
(dc/db conn)
|
||||
(:sales-order/external-id o)))))))
|
||||
(catch Exception e
|
||||
[]))]
|
||||
[(:client/code c) (atime/unparse-local (clj-time.coerce/to-date-time (:sales-order/date bad-row)) atime/normal-date) (:sales-order/total bad-row) (:sales-order/tax bad-row) (:sales-order/tip bad-row) (:db/id bad-row)])
|
||||
:separator \tab)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
(defn main-aside-nav- [request]
|
||||
(let [selected (cond
|
||||
(#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new} (:matched-route request))
|
||||
(#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new ::invoice-route/import-page} (:matched-route request))
|
||||
"invoices"
|
||||
|
||||
(#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts} (:matched-route request))
|
||||
@@ -138,8 +138,10 @@
|
||||
(when (can? (:identity request)
|
||||
{:subject :invoice
|
||||
:activity :import})
|
||||
(menu-button- {:href (bidi/path-for client-routes/routes
|
||||
:import-invoices)} "Import"))
|
||||
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/import-page)
|
||||
:active? (= ::invoice-route/import-page (:matched-route request))
|
||||
:hx-boost "true"} "Import"))
|
||||
|
||||
|
||||
(when (can? (:identity request)
|
||||
|
||||
@@ -263,6 +263,8 @@
|
||||
:identity (:identity request)
|
||||
:request request}
|
||||
(apply com/breadcrumbs {} (:breadcrumbs grid-spec))
|
||||
(when (:above-grid grid-spec)
|
||||
( (:above-grid grid-spec) request))
|
||||
[:div {:x-data (hx/json {:selected [] :all_selected false})
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
:x-init "$watch('selected', s=> $dispatch('selectedChanged', {selected: s, all_selected: all_selected}) );
|
||||
@@ -313,6 +315,12 @@
|
||||
[:=>
|
||||
[:cat [:map-of :keyword :any]]
|
||||
[:map-of :keyword :any]]]
|
||||
[:above-grid
|
||||
{:optional true
|
||||
:default (fn [request])}
|
||||
[:=>
|
||||
[:cat request-spec]
|
||||
vector?]]
|
||||
[:oob-render
|
||||
{:optional true
|
||||
:default (fn [request])}
|
||||
|
||||
569
src/clj/auto_ap/ssr/invoice/import.clj
Normal file
569
src/clj/auto_ap/ssr/invoice/import.clj
Normal file
@@ -0,0 +1,569 @@
|
||||
(ns auto-ap.ssr.invoice.import
|
||||
(:require [auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3 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 route]
|
||||
[auto-ap.routes.payments :as payment-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.invoice.common :refer [default-read]]
|
||||
[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 wrap-implied-route-param]]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[bidi.bidi :as bidi]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clojure.string :as str]
|
||||
[datomic.api :as dc]
|
||||
[hiccup.util :as hu]
|
||||
[hiccup2.core :as hiccup]
|
||||
[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 filters [request]
|
||||
[:form#invoice-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 "e.g., 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 "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)]))
|
||||
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 route-params)
|
||||
(merge-query {:query {:in ['?status]
|
||||
:where ['[?e :invoice/status ?status]]}
|
||||
:args [(:status route-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 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]}]]]
|
||||
[: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 pay-button* [params]
|
||||
(let [ids (:ids params)
|
||||
selected-client-count (if (seq ids)
|
||||
(ffirst
|
||||
(dc/q '[:find (count ?c)
|
||||
:in $ [?i ...]
|
||||
:where [?i :invoice/client ?c]]
|
||||
(dc/db conn)
|
||||
ids))
|
||||
|
||||
0)
|
||||
vendor-totals (if (seq ids)
|
||||
(->>
|
||||
(dc/q '[:find ?i ?v ?ob
|
||||
:in $ [?i ...]
|
||||
:where [?i :invoice/vendor ?v]
|
||||
[?i :invoice/outstanding-balance ?ob]]
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(reduce (fn [acc [_ v ob]]
|
||||
(update acc v (fnil + 0) ob))
|
||||
{})
|
||||
(vals)))
|
||||
all-credits-or-debits (or (every? #(<= % 0.0) vendor-totals)
|
||||
(every? #(>= % 0.0) vendor-totals))
|
||||
total (reduce + 0.0 vendor-totals)]
|
||||
|
||||
|
||||
[:div {:hx-target "this"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
::route/pay-wizard)
|
||||
:hx-trigger "click from:#pay-button"
|
||||
:x-data (hx/json {:popper nil
|
||||
:hovering false})
|
||||
"x-init" "popper = Popper.createPopper($refs.button, $refs.tooltip, {placement: 'bottom', strategy: 'fixed', modifiers: [{name: 'preventOverflow'}, {name: 'offset', options: {offset: [0, 10]}}]});"}
|
||||
(com/button {:color :primary
|
||||
:id "pay-button"
|
||||
:disabled (or (= (count (:ids params)) 0)
|
||||
(not= 1 selected-client-count)
|
||||
(not all-credits-or-debits))
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
"hx-include" "#invoice-filters"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/pay-button)
|
||||
:hx-swap "outerHTML"
|
||||
:hx-trigger "selectedChanged from:body, htmx:afterSwap from:#entity-table"
|
||||
"@mouseover" "hovering=true; $nextTick(() => popper.update())"
|
||||
"@mouseout" "hovering=false;"
|
||||
:x-ref "button"
|
||||
:minimal-loading? true
|
||||
:class "relative"}
|
||||
(if (> (count (:ids params)) 0)
|
||||
|
||||
(format "Pay %d invoices ($%,.2f)"
|
||||
(count (:ids params))
|
||||
(or total 0.0))
|
||||
"Pay")
|
||||
(when (or (= 0 (count ids))
|
||||
(> selected-client-count 1))
|
||||
(com/badge {} "!")))
|
||||
[:div (hx/alpine-appear {:x-ref "tooltip"
|
||||
|
||||
:x-show "hovering"
|
||||
:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4"})
|
||||
(cond
|
||||
(not all-credits-or-debits)
|
||||
[:div "All vendor totals must be either positive or negative."]
|
||||
(= 0 (count ids))
|
||||
[:div "Please select some invoices to pay"]
|
||||
(> selected-client-count 1)
|
||||
[:div "Can only pay for one client at a time"]
|
||||
:else
|
||||
[:div "Click to choose a bank account"])]]))
|
||||
|
||||
|
||||
(defn pay-button [request]
|
||||
(html-response
|
||||
(pay-button* {:ids (selected->ids request
|
||||
(:query-params request))})))
|
||||
|
||||
|
||||
;; TODO test as a real user
|
||||
(def grid-page
|
||||
(helper/build {:id "entity-table"
|
||||
:nav com/main-aside-nav
|
||||
:check-boxes? true
|
||||
:page-specific-nav filters
|
||||
:above-grid (fn [request]
|
||||
(com/content-card {}
|
||||
[:div.px-4.py-3.space-y-4
|
||||
[:h1.text-2xl.mb-3.font-bold "Import new invoices"]
|
||||
|
||||
[:form.bg-blue-100.border-2.border-dashed.rounded-lg.border-blue-300.p-4.max-w-md.w-md.text-center.cursor-pointer
|
||||
{:action (bidi/path-for ssr-routes/only-routes
|
||||
::route/import-page)
|
||||
:method "POST"
|
||||
:id "upload"}
|
||||
"Drop files to upload here"]
|
||||
[:script
|
||||
(hiccup/raw
|
||||
"
|
||||
ezcater_dropzone = new Dropzone (\"#upload\", {
|
||||
success: function (file, response) {
|
||||
document.getElementById(\"page-notification\").innerHTML = response;
|
||||
document.getElementById(\"page-notification\").style[\"display\"] = \"block\";
|
||||
},
|
||||
acceptedFiles: '.xls,.xlsx,.pdf,.csv',
|
||||
disablePreviews: true
|
||||
});")]]))
|
||||
|
||||
: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)]
|
||||
[(com/pill {:color :primary} "Outstanding: "
|
||||
(format "$%,.2f" outstanding))
|
||||
(com/pill {:color :secondary} "Total: "
|
||||
(format "$%,.2f" total))
|
||||
|
||||
(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" "#invoice-filters"
|
||||
:color :red}
|
||||
"Void selected"))
|
||||
(when (can? (:identity request) {:subject :invoice :activity :pay})
|
||||
(pay-button* {:ids (selected->ids request
|
||||
(:query-params request))}))
|
||||
(when (can? (:identity request) {:subject :invoice :activity :create})
|
||||
(com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-wizard)}
|
||||
"New invoice"))]))
|
||||
: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)}
|
||||
"Invoices"]]
|
||||
:title (fn [r]
|
||||
(str
|
||||
(some-> r :route-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 client]}]
|
||||
[:div.flex.flex-col.gap-y-2
|
||||
(when (first expense-accounts)
|
||||
[:div.flex-initial
|
||||
(com/pill {:color :primary}
|
||||
(:account/name
|
||||
(d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id (:invoice-expense-account/account (first expense-accounts))))
|
||||
|
||||
(:db/id client))))])
|
||||
(when (> (count expense-accounts) 1)
|
||||
[:div.flex-initial
|
||||
(com/pill {:color :secondary}
|
||||
"+ " (dec (count expense-accounts)) " more")])])}
|
||||
|
||||
{:key "outstanding"
|
||||
:name "Outstanding"
|
||||
:sort-key "outstanding-balance"
|
||||
:class "text-right"
|
||||
:render (fn [{:invoice/keys [outstanding-balance total]}]
|
||||
[:div
|
||||
(some->> outstanding-balance (format "$%,.2f"))
|
||||
(when-not (dollars= outstanding-balance total)
|
||||
[:div.text-xs.text-gray-400 (format "of $%,.2f" total)])])}
|
||||
{:key "links"
|
||||
:name "Links"
|
||||
:show-starting "lg"
|
||||
:class "w-8"
|
||||
:render (fn [i]
|
||||
(link-dropdown
|
||||
(concat (->> i
|
||||
:invoice/payments
|
||||
(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))
|
||||
|
||||
(def key->handler
|
||||
{::route/import-page
|
||||
(->
|
||||
(helper/page-route grid-page :parse-query-params? false)
|
||||
(wrap-implied-route-param :status nil))})
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.invoice.common :refer [default-read]]
|
||||
[auto-ap.ssr.invoice.new-invoice-wizard :as new-invoice-wizard]
|
||||
[auto-ap.ssr.invoice.import :as invoice-import]
|
||||
[auto-ap.ssr.pos.common :refer [date-range-field*]]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.utils
|
||||
@@ -1249,7 +1250,8 @@
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
|
||||
::route/table (helper/table-route grid-page :parse-query-params? false)}
|
||||
(merge new-invoice-wizard/key->handler))
|
||||
(merge new-invoice-wizard/key->handler)
|
||||
(merge invoice-import/key->handler))
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
(def routes {"" {:get ::all-page
|
||||
"/unpaid" ::unpaid-page
|
||||
"/paid" ::paid-page
|
||||
"/voided" ::voided-page}
|
||||
"/voided" ::voided-page
|
||||
"/import" ::import-page}
|
||||
"/new" {:get ::new-wizard
|
||||
:post ::new-invoice-submit
|
||||
:put ::new-invoice-submit
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
(defn expense-accounts-field-v2 [{value :value on-change :on-change allowance :allowance expense-accounts :value client :client max-value :max locations :locations disabled :disabled percentage-only? :percentage-only? :or {percentage-only? false} vendor-id :vendor-id}]
|
||||
[form-builder/virtual-builder {:value value
|
||||
:schema schema
|
||||
|
||||
Reference in New Issue
Block a user