Compare commits
3 Commits
33cfbab54a
...
366781e818
| Author | SHA1 | Date | |
|---|---|---|---|
| 366781e818 | |||
| bcb1978f44 | |||
| 785c6b3731 |
@@ -12,9 +12,14 @@
|
||||
[hiccup2.core :as hiccup]))
|
||||
|
||||
(defn header- [params & rest]
|
||||
(into [:th.px-4.py-3 {:scope "col" :class (:class params)
|
||||
"@click" (format "$dispatch('sorted', {key: '%s'})" (:sort-key params))
|
||||
:style (:style params)}]
|
||||
;; NOTE: only attach the sort dispatcher when there is a :sort-key. Otherwise
|
||||
;; (format "%s" nil) renders the literal string "null", which sails through the
|
||||
;; thead's `event.detail.key || ""` guard and reaches the server as
|
||||
;; toggle-sort=null, ending in a Datomic unbound-variable error on ?sort-null.
|
||||
(into [:th.px-4.py-3 (cond-> {:scope "col" :class (:class params)
|
||||
:style (:style params)}
|
||||
(:sort-key params)
|
||||
(assoc "@click" (format "$dispatch('sorted', {key: '%s'})" (:sort-key params))))]
|
||||
(if (:sort-key params)
|
||||
[(into [:a {:href "#"}] rest)]
|
||||
rest)))
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
(fc/with-field :table
|
||||
(when (seq (fc/field-value))
|
||||
|
||||
[:div {:x-data (hx/json {"showTable" false})}
|
||||
[:div {:x-data (hx/json {"showTable" false "errorsOnly" false})}
|
||||
[:form {:hx-post (bidi.bidi/path-for ssr-routes/only-routes ::route/external-import-import)
|
||||
:autocomplete "off"}
|
||||
(when (:just-parsed? request)
|
||||
@@ -180,8 +180,10 @@
|
||||
[:div.flex.gap-4.items-center
|
||||
(com/checkbox {"@click" "showTable=!showTable"}
|
||||
"Show table")
|
||||
(com/checkbox {:x-model "errorsOnly"}
|
||||
"Only show errors")
|
||||
(com/button {:color :primary} "Import")]
|
||||
[:div {:x-show "showTable"}
|
||||
[:div {:x-show "showTable || errorsOnly"}
|
||||
(com/data-grid-card {:id "ledger-import-data"
|
||||
:route nil
|
||||
:title "Data to import"
|
||||
@@ -199,14 +201,18 @@
|
||||
:rows
|
||||
(fc/cursor-map
|
||||
(fn [r]
|
||||
(let [entry-id (line->id (fc/field-value r))]
|
||||
(let [entry-id (line->id (fc/field-value r))
|
||||
row-errors (seq (fc/field-errors))]
|
||||
;; A ledger entry spans several rows. Each row knows its own
|
||||
;; entry id and drops itself when any of its siblings' remove
|
||||
;; buttons announces that id. Removing the row removes its
|
||||
;; inputs, so the entry is gone from the next import post.
|
||||
(com/data-grid-row {:data-entry-id entry-id
|
||||
:x-data (hx/json {"entryId" entry-id})
|
||||
"@remove-import-entry.window" "if ($event.detail.entryId === entryId) $el.remove()"}
|
||||
;; Clean rows are only hidden (x-show), never removed, so
|
||||
;; "Only show errors" never changes what gets posted.
|
||||
(com/data-grid-row (cond-> {:data-entry-id entry-id
|
||||
:x-data (hx/json {"entryId" entry-id})
|
||||
"@remove-import-entry.window" "if ($event.detail.entryId === entryId) $el.remove()"}
|
||||
(not row-errors) (assoc :x-show "!errorsOnly"))
|
||||
(com/data-grid-cell {}
|
||||
(fc/with-field :external-id
|
||||
(com/validated-field
|
||||
@@ -520,12 +526,23 @@
|
||||
ea)))
|
||||
line-items)))))
|
||||
|
||||
(defn blank-amount?
|
||||
"A row with neither a debit nor a credit carries no amount at all, so it is
|
||||
skipped entirely on import rather than flagged."
|
||||
[{:keys [debit credit]}]
|
||||
(letfn [(blank? [v]
|
||||
(or (nil? v)
|
||||
(and (string? v) (str/blank? v))))]
|
||||
(and (blank? debit) (blank? credit))))
|
||||
|
||||
(defn table->entries [table all-vendors all-accounts client-locked-lookup all-client-bank-accounts all-client-locations]
|
||||
(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 [[_ grouped-lines] (group-by line->id lines-with-indexes)
|
||||
:let [lines (remove blank-amount? grouped-lines)
|
||||
{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]
|
||||
:when (seq lines)]
|
||||
(add-errors {:source source
|
||||
:indices (map :index lines)
|
||||
:external-id (line->id line)
|
||||
|
||||
@@ -248,6 +248,19 @@
|
||||
[?v :vendor/name ?sort-vendor])
|
||||
(and [(missing? $ ?e :transaction/vendor)]
|
||||
[(ground "") ?sort-vendor]))]
|
||||
"bank-account" '[(or-join [?e ?sort-bank-account]
|
||||
(and [?e :transaction/bank-account ?sort-ba]
|
||||
[?sort-ba :bank-account/name ?sort-bank-account])
|
||||
(and [?e :transaction/bank-account ?sort-ba]
|
||||
[(missing? $ ?sort-ba :bank-account/name)]
|
||||
[?sort-ba :bank-account/numeric-code ?sort-ba-code]
|
||||
[(str ?sort-ba-code) ?sort-bank-account])
|
||||
(and [?e :transaction/bank-account ?sort-ba]
|
||||
[(missing? $ ?sort-ba :bank-account/name)]
|
||||
[(missing? $ ?sort-ba :bank-account/numeric-code)]
|
||||
[(ground "") ?sort-bank-account])
|
||||
(and [(missing? $ ?e :transaction/bank-account)]
|
||||
[(ground "") ?sort-bank-account]))]
|
||||
"date" ['[?e :transaction/date ?sort-date]]
|
||||
"amount" ['[?e :transaction/amount ?sort-amount]]
|
||||
"description" ['[?e :transaction/description-original ?sort-description]]}
|
||||
@@ -538,6 +551,7 @@
|
||||
:render-csv (fn [x] (-> x :transaction/client :client/name))}
|
||||
{:key "bank-account"
|
||||
:name "Bank Account"
|
||||
:sort-key "bank-account"
|
||||
:show-starting "lg"
|
||||
:render (fn [x]
|
||||
(let [ba (:transaction/bank-account x)]
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
(:form-params request) (:form-errors request)
|
||||
(fc/with-field :table
|
||||
(when (seq (fc/field-value))
|
||||
[:div.mt-4 {:x-data (hx/json {"showTable" true})}
|
||||
[:div.mt-4 {:x-data (hx/json {"showTable" true "errorsOnly" false})}
|
||||
(when (:just-parsed? request)
|
||||
(parsed-banner request))
|
||||
[:form {:hx-post (bidi/path-for ssr-routes/only-routes ::route/external-import-import)
|
||||
@@ -186,8 +186,9 @@
|
||||
:autocomplete "off"}
|
||||
[:div.flex.gap-4.items-center.my-2
|
||||
(com/checkbox {"@click" "showTable=!showTable"} "Show table")
|
||||
(com/checkbox {:x-model "errorsOnly"} "Only show errors")
|
||||
(com/button {:color :primary :type "submit"} "Import")]
|
||||
[:div {:x-show "showTable"}
|
||||
[:div {:x-show "showTable || errorsOnly"}
|
||||
(com/data-grid-card
|
||||
{:id "transaction-import-data"
|
||||
:route nil
|
||||
@@ -204,8 +205,11 @@
|
||||
(fc/cursor-map
|
||||
(fn [_]
|
||||
(let [row-errors (fc/field-errors)]
|
||||
;; Clean rows are only hidden (x-show), never removed, so
|
||||
;; "Only show errors" never changes what gets posted.
|
||||
(com/data-grid-row
|
||||
{}
|
||||
(cond-> {}
|
||||
(not (seq row-errors)) (assoc :x-show "!errorsOnly"))
|
||||
(com/data-grid-cell {} (fc/with-field :raw-date
|
||||
(com/text-input {:value (fc/field-value) :name (fc/field-name) :class "w-28"})))
|
||||
(com/data-grid-cell {} (fc/with-field :description-original
|
||||
|
||||
@@ -370,6 +370,55 @@
|
||||
(is (= "TEST" (:client-code (first entries))))
|
||||
(is (= 2 (count (:line-items (first entries))))))))
|
||||
|
||||
(deftest table->entries-blank-amount-rows-test
|
||||
(let [line (fn [external-id debit credit account-code]
|
||||
{:source "manual"
|
||||
:client-code "TEST"
|
||||
:external-id external-id
|
||||
:date (coerce/to-date-time #inst "2021-01-01")
|
||||
:vendor-name "Vendor"
|
||||
:debit debit
|
||||
:credit credit
|
||||
:account-code account-code
|
||||
:location "HQ"})
|
||||
entries (fn [table]
|
||||
(sut/table->entries table
|
||||
{"Vendor" {:db/id "vendor-1"}}
|
||||
#{"1100" "1101"}
|
||||
{"TEST" #inst "2000-01-01"}
|
||||
{"TEST" #{}}
|
||||
{"TEST" #{"HQ"}}))]
|
||||
(testing "Should skip rows with no debit and no credit without flagging them"
|
||||
(let [_ (setup-test-data [(test-client :db/id "blank-client-1"
|
||||
:client/code "TEST"
|
||||
:client/locations ["HQ"])])
|
||||
result (entries [(line "ext-blank-1" 100.0 nil 1100)
|
||||
(line "ext-blank-1" nil nil 1101)
|
||||
(line "ext-blank-1" nil 100.0 1101)])]
|
||||
(is (= 1 (count result)))
|
||||
(is (= 2 (count (:line-items (first result)))))
|
||||
(is (= [0 2] (vec (:indices (first result)))))
|
||||
(is (= 100.0 (:amount (first result))))
|
||||
(is (empty? (sut/entry-errors (first result))))))
|
||||
|
||||
(testing "Should treat blank strings in the amount columns as no amount"
|
||||
(let [_ (setup-test-data [(test-client :db/id "blank-client-2"
|
||||
:client/code "TEST"
|
||||
:client/locations ["HQ"])])
|
||||
result (entries [(line "ext-blank-2" 100.0 nil 1100)
|
||||
(line "ext-blank-2" "" " " 1101)
|
||||
(line "ext-blank-2" nil 100.0 1101)])]
|
||||
(is (= 2 (count (:line-items (first result)))))
|
||||
(is (empty? (sut/entry-errors (first result))))))
|
||||
|
||||
(testing "Should drop an entry entirely when every one of its rows is blank"
|
||||
(let [_ (setup-test-data [(test-client :db/id "blank-client-3"
|
||||
:client/code "TEST"
|
||||
:client/locations ["HQ"])])
|
||||
result (entries [(line "ext-blank-3" nil nil 1100)
|
||||
(line "ext-blank-3" nil nil 1101)])]
|
||||
(is (empty? result))))))
|
||||
|
||||
(deftest import-ledger-test
|
||||
(testing "Should upsert hidden vendors and create transactions"
|
||||
(let [_ (setup-test-data [(test-client :db/id "import-client-1"
|
||||
@@ -397,7 +446,44 @@
|
||||
(let [vendor-id (dc/q '[:find ?e .
|
||||
:where [?e :vendor/name "New Vendor Import Unique"]]
|
||||
db-after)]
|
||||
(is vendor-id)))))
|
||||
(is vendor-id))))
|
||||
|
||||
(testing "Should import the entry while ignoring its rows with no debit and no credit"
|
||||
(let [_ (setup-test-data [(test-client :db/id "import-client-2"
|
||||
:client/code "IMPORT-BLANK"
|
||||
:client/locations ["HQ"])
|
||||
{:db/id "import-blank-account-1100"
|
||||
:account/numeric-code 1100
|
||||
:account/account-set "default"
|
||||
:account/name "Cash"}
|
||||
{:db/id "import-blank-account-1101"
|
||||
:account/numeric-code 1101
|
||||
:account/account-set "default"
|
||||
:account/name "Other Cash"}])
|
||||
row (fn [debit credit account-code]
|
||||
{:source "manual"
|
||||
:client-code "IMPORT-BLANK"
|
||||
:external-id "ext-import-blank-1"
|
||||
:date (coerce/to-date-time #inst "2021-01-01")
|
||||
:vendor-name "Blank Row Vendor"
|
||||
:debit debit
|
||||
:credit credit
|
||||
:account-code account-code
|
||||
:location "HQ"})
|
||||
result (sut/import-ledger {:form-params {:table [(row 100.0 nil 1100)
|
||||
(row nil nil 1101)
|
||||
(row nil 100.0 1101)]}
|
||||
:identity (admin-token)})
|
||||
entry (dc/pull (dc/db conn)
|
||||
[:journal-entry/amount
|
||||
{:journal-entry/line-items [:journal-entry-line/debit
|
||||
:journal-entry-line/credit]}]
|
||||
[:journal-entry/external-id "IMPORT-BLANK-manual-ext-import-blank-1"])]
|
||||
(is (= 1 (:successful result)))
|
||||
(is (= 0 (:ignored result)))
|
||||
(is (empty? (:form-errors result)))
|
||||
(is (= 100.0 (:journal-entry/amount entry)))
|
||||
(is (= 2 (count (:journal-entry/line-items entry)))))))
|
||||
|
||||
(deftest import-ledger-with-errors-test
|
||||
(testing "Should throw exception when entries have errors - client not found"
|
||||
|
||||
Reference in New Issue
Block a user