feat(ledger): remove a whole ledger entry from the external import grid

The external ledger import review grid is one row per line item, so a
journal entry spans two or more rows. When one entry fails validation the
whole paste is rejected, and the only way forward was to re-paste without
it.

Each row now carries its entry id (client-source-externalId, the same key
table->entries groups on) and the last column gets a trash button that
drops every row sharing that id. The rows are the form inputs, so removing
them from the DOM removes them from the next import post -- which also
works for rows that fail schema validation. Index gaps left behind are
compacted by coerce-vector on the way back in.

Also drops two leftover pprint calls that dumped form-errors to stdout on
every render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 22:44:52 -07:00
parent e6507d5387
commit a17e16e31b
2 changed files with 190 additions and 79 deletions

View File

@@ -13,6 +13,7 @@
[clojure.data.csv :as csv]
[clojure.string :as str]
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[malli.core :as mc]))
(use-fixtures :each wrap-setup)
@@ -619,3 +620,93 @@
(testing "Non-admin cannot bulk-delete"
(is (thrown? Exception (sut/bulk-delete {:identity (user-token)
:form-params {:selected [1]}})))))
;; =============================================================================
;; External Import - removing an entry from the review grid
;; =============================================================================
(defn- import-row [external-id account-code debit credit]
{:external-id external-id
:client-code "REMOVE-TEST"
:source "manual"
:vendor-name "Remove Vendor"
:date (coerce/to-date-time #inst "2021-01-01")
:account-code account-code
:location "HQ"
:debit debit
:credit credit})
(defn- entry-rows
"The two rows a balanced ledger entry is typically pasted as."
[external-id debit-account amount]
[(import-row external-id debit-account amount 0.0)
(import-row external-id 2000 0.0 amount)])
(deftest external-import-remove-button-test
(testing "Every row is tagged with the entry id the importer groups on"
(let [table (vec (concat (entry-rows "ext-a" 1100 100.0)
(entry-rows "ext-b" 1100 50.0)))
html (str (hiccup/html (sut/external-import-table-form*
{:form-params {:table table}
:form-errors {}})))]
(is (= 2 (count (re-seq #"data-entry-id=\"REMOVE-TEST-manual-ext-a\"" html))))
(is (= 2 (count (re-seq #"data-entry-id=\"REMOVE-TEST-manual-ext-b\"" html))))
(testing "and offers a remove button that drops the whole entry"
(is (= 4 (count (re-seq #"removeImportEntryRows\(\$el\)" html))))
(is (= 2 (count (re-seq #"data-remove-entry-id=\"REMOVE-TEST-manual-ext-a\"" html))))
(is (= 2 (count (re-seq #"aria-label=\"Remove ledger entry REMOVE-TEST-manual-ext-a\"" html))))))))
(deftest external-import-remove-entry-then-import-test
(testing "Removing the failing entry lets the remaining entries import"
(let [_ (setup-test-data [(test-client :db/id "remove-client"
:client/code "REMOVE-TEST"
:client/locations ["HQ"])
(test-vendor :db/id "remove-vendor"
:vendor/name "Remove Vendor")
{:db/id "remove-account-1100"
:account/numeric-code 1100
:account/account-set "default"
:account/name "Cash"}
{:db/id "remove-account-2000"
:account/numeric-code 2000
:account/account-set "default"
:account/name "Accounts Payable"}])
;; Three entries, six rows. The middle one posts to an account that
;; does not exist, which is the kind of error a user has to resolve.
table (vec (concat (entry-rows "ext-good-1" 1100 100.0)
(entry-rows "ext-bad" 99999 75.0)
(entry-rows "ext-good-2" 1100 25.0)))
admin (admin-token)
external-id (fn [id] (str "REMOVE-TEST-manual-" id))
imported? (fn [db id]
(boolean (dc/q '[:find ?je .
:in $ ?ext
:where [?je :journal-entry/external-id ?ext]]
db (external-id id))))]
(testing "the bad entry blocks the whole paste"
(is (thrown? Exception (sut/import-ledger {:form-params {:table table}
:identity admin})))
(let [db (dc/db conn)]
(is (not (imported? db "ext-good-1")))
(is (not (imported? db "ext-good-2")))))
(testing "removing it drops both of its rows"
(let [remaining (vec (remove #(= (external-id "ext-bad") (sut/line->id %)) table))]
(is (= 4 (count remaining)))
(testing "and the other two entries import"
(let [result (sut/import-ledger {:form-params {:table remaining}
:identity admin})
db (dc/db conn)]
(is (= 2 (:successful result)))
(is (imported? db "ext-good-1"))
(is (imported? db "ext-good-2"))
(is (not (imported? db "ext-bad")))
(let [entry (dc/pull db
[:journal-entry/amount
{:journal-entry/line-items [:journal-entry-line/debit
:journal-entry-line/credit]}]
[:journal-entry/external-id (external-id "ext-good-1")])]
(is (= 100.0 (:journal-entry/amount entry)))
(is (= 2 (count (:journal-entry/line-items entry))))))))))))