Add vendor pre-population for bulk code and individual edit forms
- Add vendor-changed HTMX handlers for both bulk code and individual edit - Pre-populate default account at 100% when vendor is selected and no accounts exist - Fix render-accounts-section to render from step-params correctly - Change bulk code vendor-changed from hx-get to hx-post to include form data - Add routes for vendor-changed endpoints - Update e2e tests to cover vendor pre-population - Run lein cljfmt fix across codebase
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
(ns auto-ap.ssr.ledger
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [audit-transact audit-transact-batch conn pull-many
|
||||
remove-nils]]
|
||||
:refer [audit-transact audit-transact-batch conn pull-many
|
||||
remove-nils]]
|
||||
[auto-ap.datomic.accounts :as a]
|
||||
[auto-ap.graphql.utils :refer [assert-admin assert-can-see-client
|
||||
exception->notification notify-if-locked]]
|
||||
@@ -11,7 +11,7 @@
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
[auto-ap.routes.ledger :as route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-client-redirect-unauthenticated]]
|
||||
:refer [wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.solr :as solr]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
@@ -30,11 +30,11 @@
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||||
html-response main-transformer money strip
|
||||
wrap-form-4xx-2 wrap-implied-route-param
|
||||
wrap-merge-prior-hx wrap-schema-decode
|
||||
wrap-schema-enforce]]
|
||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||||
html-response main-transformer money strip
|
||||
wrap-form-4xx-2 wrap-implied-route-param
|
||||
wrap-merge-prior-hx wrap-schema-decode
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[bidi.bidi :as bidi]
|
||||
@@ -50,8 +50,6 @@
|
||||
[malli.core :as mc]
|
||||
[slingshot.slingshot :refer [throw+]]))
|
||||
|
||||
|
||||
|
||||
(comment
|
||||
(mc/decode query-schema
|
||||
{:start " "}
|
||||
@@ -67,14 +65,10 @@
|
||||
(assoc-in [:query-params :start] 0)
|
||||
(assoc-in [:query-params :per-page] 250))))
|
||||
|
||||
|
||||
:else
|
||||
selected)]
|
||||
ids))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn delete [{invoice :entity :as request identity :identity}]
|
||||
(exception->notification
|
||||
#(when-not (= :invoice-status/unpaid (:invoice/status invoice))
|
||||
@@ -101,10 +95,9 @@
|
||||
identity)
|
||||
|
||||
(html-response (ledger.common/row* (:identity request) (dc/pull (dc/db conn) default-read (:db/id invoice))
|
||||
{:class "live-removed"})
|
||||
{:class "live-removed"})
|
||||
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))}))
|
||||
|
||||
|
||||
(defn wrap-ensure-bank-account-belongs [handler]
|
||||
(fn [{:keys [query-params client] :as request}]
|
||||
(let [bank-account-belongs? (get (set (map :db/id (:client/bank-accounts client)))
|
||||
@@ -131,7 +124,7 @@
|
||||
(clojure.pprint/pprint (fc/field-errors))
|
||||
(when (seq (fc/field-value))
|
||||
|
||||
[:div {:x-data (hx/json { "showTable" false})}
|
||||
[:div {:x-data (hx/json {"showTable" false})}
|
||||
[:form {:hx-post (bidi.bidi/path-for ssr-routes/only-routes ::route/external-import-import)
|
||||
:autocomplete "off"}
|
||||
(when (:just-parsed? request)
|
||||
@@ -140,7 +133,7 @@
|
||||
[:div.inline-flex.gap-2
|
||||
(->> (:form-errors request)
|
||||
:table
|
||||
( #(if (map? %) ( vals %) %))
|
||||
(#(if (map? %) (vals %) %))
|
||||
(mapcat identity)
|
||||
(group-by last)
|
||||
(map (fn [[k v]]
|
||||
@@ -148,12 +141,12 @@
|
||||
(com/pill {:color :yellow}
|
||||
(format "%d warnings" (count v)))
|
||||
(com/pill {:color :red}
|
||||
(format "%d errors" (count v)))))))] ])
|
||||
(format "%d errors" (count v)))))))]])
|
||||
[:div.flex.gap-4.items-center
|
||||
(com/checkbox {"@click" "showTable=!showTable"}
|
||||
"Show table")
|
||||
(com/button {:color :primary} "Import")]
|
||||
[:div { :x-show "showTable"}
|
||||
[:div {:x-show "showTable"}
|
||||
(com/data-grid-card {:id "ledger-import-data"
|
||||
:route nil
|
||||
:title "Data to import"
|
||||
@@ -230,11 +223,11 @@
|
||||
(let [errors (seq (fc/field-errors))]
|
||||
(cond errors
|
||||
[:div
|
||||
{ "x-tooltip" "{content: ()=>$refs.tt.innerHTML , allowHTML: true}"}
|
||||
{"x-tooltip" "{content: ()=>$refs.tt.innerHTML , allowHTML: true}"}
|
||||
[:div.w-8.h-8.rounded-full.p-2.flex.items-start {:class
|
||||
(if (seq (filter
|
||||
(fn [[_ status]]
|
||||
|
||||
|
||||
(= :error status))
|
||||
errors))
|
||||
"bg-red-50 text-red-300"
|
||||
@@ -246,29 +239,29 @@
|
||||
[:li m])]]]
|
||||
:else
|
||||
nil))]))))}
|
||||
|
||||
|
||||
[:div.flex.m-4.flex-row-reverse
|
||||
(com/button {:color :primary} "Import")])]]])))])
|
||||
|
||||
(defn external-import-text-form* [request]
|
||||
(fc/start-form
|
||||
(or (:form-params request) {}) (:form-errors request)
|
||||
[:form#parse-form {:x-data (hx/json {"clipboard" nil})
|
||||
:hx-post (bidi.bidi/path-for ssr-routes/only-routes ::route/external-import-parse)
|
||||
:hx-swap "outerHTML"
|
||||
:hx-trigger "pasted"}
|
||||
(fc/with-field :table
|
||||
[:div
|
||||
(com/errors {:errors (fc/field-errors)})
|
||||
(com/text-area {:x-model "clipboard" :name (fc/field-name) :value (fc/field-value) :class "hidden"})])
|
||||
(com/button {"@click.prevent" "clipboard = (await getclpboard()); $nextTick(() => $dispatch('pasted'))"
|
||||
"x-on:paste.document" "clipboard = (await getclpboard()); console.log(clipboard); $nextTick(() => $dispatch('pasted'))"}
|
||||
"Load from clipboard")]))
|
||||
|
||||
(fc/start-form
|
||||
(or (:form-params request) {}) (:form-errors request)
|
||||
[:form#parse-form {:x-data (hx/json {"clipboard" nil})
|
||||
:hx-post (bidi.bidi/path-for ssr-routes/only-routes ::route/external-import-parse)
|
||||
:hx-swap "outerHTML"
|
||||
:hx-trigger "pasted"}
|
||||
(fc/with-field :table
|
||||
[:div
|
||||
(com/errors {:errors (fc/field-errors)})
|
||||
(com/text-area {:x-model "clipboard" :name (fc/field-name) :value (fc/field-value) :class "hidden"})])
|
||||
(com/button {"@click.prevent" "clipboard = (await getclpboard()); $nextTick(() => $dispatch('pasted'))"
|
||||
"x-on:paste.document" "clipboard = (await getclpboard()); console.log(clipboard); $nextTick(() => $dispatch('pasted'))"}
|
||||
"Load from clipboard")]))
|
||||
|
||||
(defn external-import-form* [request]
|
||||
[:div#forms {:hx-target "this"
|
||||
:hx-swap "outerHTML"}
|
||||
(when (and (not (:just-parsed? request))
|
||||
(when (and (not (:just-parsed? request))
|
||||
(seq (->> (:form-errors request)
|
||||
:table
|
||||
vals
|
||||
@@ -289,14 +282,14 @@
|
||||
:client (:client request)
|
||||
:identity (:identity request)
|
||||
:request request}
|
||||
(com/breadcrumbs {}
|
||||
(com/breadcrumbs {}
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes ::route/all-page)}
|
||||
"Ledger"]
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes ::route/external-import-page)}
|
||||
"Import"])
|
||||
#_(when (:above-grid grid-spec)
|
||||
( (:above-grid grid-spec) request))
|
||||
|
||||
((:above-grid grid-spec) request))
|
||||
|
||||
[:script
|
||||
(hiccup/raw
|
||||
"async function getclpboard() {
|
||||
@@ -306,7 +299,7 @@
|
||||
console.log(r)
|
||||
return await r.text()
|
||||
}")]
|
||||
|
||||
|
||||
(external-import-form* request)
|
||||
[:div #_{:x-data (hx/json {:selected [] :all_selected false :type (:entity-name grid-spec)})
|
||||
"x-on:copy" "if (selected.length > 0) {$clipboard(JSON.stringify({'type': type, 'selected': selected}))}"
|
||||
@@ -322,23 +315,21 @@
|
||||
#_(if (string? (:title grid-spec))
|
||||
(:title grid-spec)
|
||||
((:title grid-spec) request))))
|
||||
|
||||
|
||||
|
||||
(defn trim-header [t]
|
||||
(if (->> t
|
||||
first
|
||||
(map clojure.string/lower-case)
|
||||
(filter #{"id" "client" "source" "vendor" "date" "account" "location" "debit"})
|
||||
seq)
|
||||
(drop 1 t)
|
||||
t))
|
||||
first
|
||||
(map clojure.string/lower-case)
|
||||
(filter #{"id" "client" "source" "vendor" "date" "account" "location" "debit"})
|
||||
seq)
|
||||
(drop 1 t)
|
||||
t))
|
||||
|
||||
(defn tsv->import-data [data]
|
||||
(if (string? data)
|
||||
(with-open [r (io/reader (char-array data))]
|
||||
(into [] (filter (fn filter-row [r]
|
||||
(seq (filter (comp not-empty #(str/replace % #"\s+" "")) r))))
|
||||
(seq (filter (comp not-empty #(str/replace % #"\s+" "")) r))))
|
||||
(trim-header (csv/read-csv r :separator \tab))))
|
||||
data))
|
||||
|
||||
@@ -347,52 +338,45 @@
|
||||
[:bank-account
|
||||
[:string]]]))
|
||||
|
||||
|
||||
|
||||
(def parse-form-schema (mc/schema
|
||||
[:map
|
||||
[:map
|
||||
[:table {:min 1 :error/message "Clipboard should contain rows to import"
|
||||
:decode/string tsv->import-data}
|
||||
:decode/string tsv->import-data}
|
||||
[:vector {:coerce? true}
|
||||
[:map { :decode/arbitrary (fn [t]
|
||||
[:map {:decode/arbitrary (fn [t]
|
||||
(if (vector? t)
|
||||
(into {} (map vector [:external-id :client-code :source :vendor-name :date :account-code :location :debit :credit] t))
|
||||
t))}
|
||||
[:external-id [:string {:title "external id"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:client-code [:string {:title "client code"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:source [:string {:title "source"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:vendor-name [:string {:min 1 :decode/string strip}]]
|
||||
[:date [:and clj-date-schema
|
||||
[:any {:title "date"}]]]
|
||||
[:account-code account-schema]
|
||||
|
||||
[:location [:string { :min 1
|
||||
:max 2
|
||||
:decode/string strip}]]
|
||||
[:debit [:maybe money]]
|
||||
[:credit [:maybe money]]]]
|
||||
|
||||
[:external-id [:string {:title "external id"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:client-code [:string {:title "client code"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:source [:string {:title "source"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:vendor-name [:string {:min 1 :decode/string strip}]]
|
||||
[:date [:and clj-date-schema
|
||||
[:any {:title "date"}]]]
|
||||
[:account-code account-schema]
|
||||
|
||||
[:location [:string {:min 1
|
||||
:max 2
|
||||
:decode/string strip}]]
|
||||
[:debit [:maybe money]]
|
||||
[:credit [:maybe money]]]]
|
||||
|
||||
#_[:string {:decode/string tsv->import-data
|
||||
:error/message "Clipboard should contain rows to import"}]]]))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(defn external-import-parse [request]
|
||||
(html-response
|
||||
( external-import-form* (assoc request :just-parsed? true))))
|
||||
(html-response
|
||||
(external-import-form* (assoc request :just-parsed? true))))
|
||||
|
||||
(defn line->id [{:keys [source external-id client-code]}]
|
||||
(str client-code "-" source "-" external-id))
|
||||
|
||||
|
||||
(defn add-errors [entry all-vendors all-accounts client-locked-lookup all-client-bank-accounts all-client-locations]
|
||||
(let [vendor (all-vendors (:vendor-name entry))
|
||||
locked-until (client-locked-lookup (:client-code entry))
|
||||
@@ -413,29 +397,29 @@
|
||||
entry (cond
|
||||
(not locked-until)
|
||||
(all-row-error (str "Client '" (:client-code entry) "' not found."))
|
||||
|
||||
|
||||
(not vendor)
|
||||
(all-row-error (str "Vendor '" (:vendor-name entry) "' not found."))
|
||||
|
||||
|
||||
(and locked-until
|
||||
(and (not (t/after? (:date entry)
|
||||
(coerce/to-date-time locked-until)))
|
||||
(not (t/equal? (:date entry)
|
||||
(coerce/to-date-time locked-until)))))
|
||||
(all-row-error (str "Client's data is locked until " locked-until))
|
||||
(not (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry)))
|
||||
(reduce (fnil + 0.0 0.0) 0.0 (map :credit (:line-items entry)))))
|
||||
(all-row-error (str "Debits '"
|
||||
(reduce (fnil + 0.0 0.0) 0 (map :debit (:line-items entry)))
|
||||
"' and credits '"
|
||||
(reduce (fnil + 0.0 0.0) 0 (map :credit (:line-items entry)))
|
||||
"' do not add up."))
|
||||
(dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry)))
|
||||
0.0)
|
||||
(all-row-error (str "Cannot have ledger entries that total $0.00") :warn)
|
||||
|
||||
:else
|
||||
entry)]
|
||||
(not (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry)))
|
||||
(reduce (fnil + 0.0 0.0) 0.0 (map :credit (:line-items entry)))))
|
||||
(all-row-error (str "Debits '"
|
||||
(reduce (fnil + 0.0 0.0) 0 (map :debit (:line-items entry)))
|
||||
"' and credits '"
|
||||
(reduce (fnil + 0.0 0.0) 0 (map :credit (:line-items entry)))
|
||||
"' do not add up."))
|
||||
(dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry)))
|
||||
0.0)
|
||||
(all-row-error (str "Cannot have ledger entries that total $0.00") :warn)
|
||||
|
||||
:else
|
||||
entry)]
|
||||
(update
|
||||
entry
|
||||
:line-items
|
||||
@@ -466,7 +450,6 @@
|
||||
(:account-code ea))))
|
||||
(row-error ea (str "Bank Account '" (:account-code ea) "' not found."))
|
||||
|
||||
|
||||
(and matching-account
|
||||
(:account/location matching-account)
|
||||
(not= (:account/location matching-account)
|
||||
@@ -494,12 +477,11 @@
|
||||
(let [lines-with-indexes (for [[i l] (map vector (range) table)]
|
||||
(assoc l :index i))]
|
||||
(into []
|
||||
(for [
|
||||
[_ lines] (group-by line->id lines-with-indexes)
|
||||
:let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]]
|
||||
(for [[_ lines] (group-by line->id lines-with-indexes)
|
||||
:let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]]
|
||||
(add-errors {:source source
|
||||
:indices (map :index lines)
|
||||
:external-id (line->id line)
|
||||
:external-id (line->id line)
|
||||
:client-code client-code
|
||||
:date date
|
||||
:note note
|
||||
@@ -515,9 +497,9 @@
|
||||
:debit debit
|
||||
:credit credit})
|
||||
lines)}
|
||||
all-vendors
|
||||
all-accounts
|
||||
client-locked-lookup
|
||||
all-vendors
|
||||
all-accounts
|
||||
client-locked-lookup
|
||||
all-client-bank-accounts
|
||||
all-client-locations)))))
|
||||
|
||||
@@ -645,7 +627,7 @@
|
||||
good-entries (filter (fn [e] (and (not (:error (entry-error-types e))) (not (:warn (entry-error-types e))))) entries)
|
||||
bad-entries (filter (fn [e] (:error (entry-error-types e))) entries)
|
||||
form-errors (reduce (fn [acc [path m status]]
|
||||
(update-in acc path conj [ m status]))
|
||||
(update-in acc path conj [m status]))
|
||||
{}
|
||||
errors)
|
||||
_ (when (seq bad-entries)
|
||||
@@ -654,7 +636,7 @@
|
||||
{:type :field-validation
|
||||
:form-errors form-errors
|
||||
:form-params form-params})))
|
||||
|
||||
|
||||
retraction (mapv (fn [x] [:db/retractEntity [:journal-entry/external-id (:external-id x)]])
|
||||
good-entries)
|
||||
ignore-retraction (->> ignored-entries
|
||||
@@ -696,21 +678,20 @@
|
||||
|
||||
(defn external-import-import [request]
|
||||
(let [result (import-ledger request)]
|
||||
(html-response
|
||||
[:div
|
||||
(html-response
|
||||
[:div
|
||||
(external-import-form* (assoc request :form-errors (:form-errors result)))]
|
||||
:headers {"hx-trigger" (hx/json { "notification" (format "%d successful, %d with warnings. Any ledger entries with warnings have been removed." (:successful result) (:ignored result))})})))
|
||||
|
||||
:headers {"hx-trigger" (hx/json {"notification" (format "%d successful, %d with warnings. Any ledger entries with warnings have been removed." (:successful result) (:ignored result))})})))
|
||||
|
||||
(def key->handler
|
||||
(merge
|
||||
(merge
|
||||
(apply-middleware-to-all-handlers
|
||||
(->
|
||||
{::route/all-page (-> (helper/page-route grid-page)
|
||||
(wrap-implied-route-param :external? false))
|
||||
::route/external-page (-> (helper/page-route grid-page)
|
||||
(wrap-implied-route-param :external? true))
|
||||
|
||||
|
||||
::route/table (helper/table-route grid-page)
|
||||
::route/csv (helper/csv-route grid-page)
|
||||
::route/external-import-page external-import-page
|
||||
@@ -739,4 +720,4 @@
|
||||
profit-and-loss/key->handler
|
||||
cash-flows/key->handler
|
||||
investigate/key->handler
|
||||
new/key->handler))
|
||||
new/key->handler))
|
||||
Reference in New Issue
Block a user