fix(ledger): ignore external import rows with no debit or credit

A pasted row with both amount columns empty carries no accounting
information, but it still became a line item: it tripped the "Line item
amount 0.0 must be greater than 0." warning, which demoted the entire
journal entry to "ignored" -- so the good rows around it were dropped
and any existing entry with that external id was retracted.

Drop those rows in table->entries before add-errors runs, so they never
reach the balance check, :amount, or :line-items. An entry whose rows are
all blank disappears completely: not imported, not retracted, not
counted. Only genuinely empty values qualify (nil or a whitespace-only
string, since form input decodes "" to nil); an explicit 0 still warns as
before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 12:59:47 -07:00
parent 785c6b3731
commit bcb1978f44
2 changed files with 100 additions and 3 deletions

View File

@@ -526,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)

View File

@@ -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"