Compare commits
4 Commits
33cfbab54a
...
integreat-
| Author | SHA1 | Date | |
|---|---|---|---|
| e9970bd41a | |||
| 366781e818 | |||
| bcb1978f44 | |||
| 785c6b3731 |
@@ -315,21 +315,55 @@
|
||||
[[(:db/id a) (:db/id (:account-client-override/client o))]
|
||||
(:account-client-override/name o)])
|
||||
(:account/client-overrides a))))
|
||||
(into {}))]
|
||||
(into {}))
|
||||
|
||||
;; A client's bank account and the financial account it posts to share a
|
||||
;; numeric code. Reports key their rows off that code, so the two would
|
||||
;; otherwise render as two rows for the same account. Resolve every
|
||||
;; account at a shared code down to the bank account's name. Where a
|
||||
;; client has two bank accounts on one code (a data-entry error, but it
|
||||
;; happens) the lowest :bank-account/sort-order wins, then the lowest
|
||||
;; :db/id, so the name is stable across runs.
|
||||
bank-name-by-code (->> (dc/q {:find ['(pull ?b [:db/id :bank-account/name
|
||||
:bank-account/numeric-code
|
||||
:bank-account/sort-order])]
|
||||
:in ['$ '?c]
|
||||
:where ['[?c :client/bank-accounts ?b]]}
|
||||
(dc/db conn)
|
||||
client-id)
|
||||
(map first)
|
||||
(filter (every-pred :bank-account/numeric-code :bank-account/name))
|
||||
(sort-by (juxt #(or (:bank-account/sort-order %) Long/MAX_VALUE)
|
||||
:db/id))
|
||||
(reduce (fn [m b]
|
||||
(let [code (:bank-account/numeric-code b)]
|
||||
(cond-> m
|
||||
(not (contains? m code))
|
||||
(assoc code (:bank-account/name b)))))
|
||||
{}))]
|
||||
(fn [a]
|
||||
{:name (or (:bank-account/name (bank-accounts a))
|
||||
(overrides-by-client [a client-id])
|
||||
(:account/name (accounts a)))
|
||||
:account_type (or (:db/ident (:account/type (accounts a)))
|
||||
({:bank-account-type/check :account-type/asset
|
||||
:bank-account-type/cash :account-type/asset
|
||||
:bank-account-type/credit :account-type/liability}
|
||||
(:db/ident (:bank-account/type (bank-accounts a))))
|
||||
:account-type/asset ;; DEFAULT TO ASSET, for things like unknown
|
||||
)
|
||||
:numeric_code (or (:account/numeric-code (accounts a))
|
||||
(:bank-account/numeric-code (bank-accounts a)))
|
||||
:client_id client-id})))
|
||||
(let [account (accounts a)
|
||||
bank-account (bank-accounts a)
|
||||
numeric-code (or (:account/numeric-code account)
|
||||
(:bank-account/numeric-code bank-account))
|
||||
bank-name (or (bank-name-by-code numeric-code)
|
||||
(:bank-account/name bank-account))]
|
||||
{:name (or bank-name
|
||||
(overrides-by-client [a client-id])
|
||||
(:account/name account))
|
||||
;; Whether :name above came from a bank account. Reports that pool
|
||||
;; several clients into one row per code use this to pick the label,
|
||||
;; since only some of those clients may have a bank account at the code.
|
||||
:bank_account_name? (some? bank-name)
|
||||
:account_type (or (:db/ident (:account/type account))
|
||||
({:bank-account-type/check :account-type/asset
|
||||
:bank-account-type/cash :account-type/asset
|
||||
:bank-account-type/credit :account-type/liability}
|
||||
(:db/ident (:bank-account/type bank-account)))
|
||||
:account-type/asset ;; DEFAULT TO ASSET, for things like unknown
|
||||
)
|
||||
:numeric_code numeric-code
|
||||
:client_id client-id}))))
|
||||
|
||||
(defn find-mismatch-index []
|
||||
(reduce + 0
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
:account-type (:account_type account)
|
||||
:numeric-code (:numeric_code account)
|
||||
:name (:name account)
|
||||
:bank-account-name? (:bank_account_name? account)
|
||||
:period (coerce/to-date d)}))
|
||||
args (assoc (:query-params request)
|
||||
:periods (map coerce/to-date (filter identity date)))
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
:account-type (:account_type account)
|
||||
:numeric-code (:numeric_code account)
|
||||
:name (:name account)
|
||||
:bank-account-name? (:bank_account_name? account)
|
||||
:period {:start (coerce/to-date (:start p)) :end (coerce/to-date (:end p))}}))
|
||||
args (assoc (:form-params request)
|
||||
:periods (map (fn [d] {:start (coerce/to-date (:start d)) :end (coerce/to-date (:end d))}) periods))
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
:account-type (:account_type account)
|
||||
:numeric-code (:numeric_code account)
|
||||
:name (:name account)
|
||||
:bank-account-name? (:bank_account_name? account)
|
||||
:sample sample
|
||||
:period {:start (coerce/to-date (:start p)) :end (coerce/to-date (:end p))}}))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -267,12 +267,33 @@
|
||||
account))
|
||||
accounts))))
|
||||
|
||||
(defn used-accounts [pnl-datas]
|
||||
(defn used-accounts
|
||||
"One entry per numeric code in play, with the name to label its row.
|
||||
|
||||
Rows are keyed on the code alone, never on [code name]: the amount a row
|
||||
reports is the total for its code, so two names at one code would print the
|
||||
same figure twice and the section would stop footing to its own subtotal.
|
||||
|
||||
`build-account-lookup` has already resolved a client's shared codes down to
|
||||
the bank account's name, so the name here is normally unanimous. It can
|
||||
still differ across a multi-client report, where only some of the clients
|
||||
have a bank account at the code. A bank account's name wins there; failing
|
||||
that the most common name does, ties broken alphabetically so the report is
|
||||
stable across runs."
|
||||
[pnl-datas]
|
||||
(->>
|
||||
pnl-datas
|
||||
(mapcat :data)
|
||||
(map #(select-keys % [:numeric-code :name]))
|
||||
(set)
|
||||
(map #(select-keys % [:numeric-code :name :bank-account-name?]))
|
||||
(group-by :numeric-code)
|
||||
(map (fn [[numeric-code entries]]
|
||||
{:numeric-code numeric-code
|
||||
:name (->> (or (seq (filter :bank-account-name? entries))
|
||||
entries)
|
||||
(map :name)
|
||||
frequencies
|
||||
(sort-by (juxt (comp - val) key))
|
||||
ffirst)}))
|
||||
(sort-by :numeric-code)))
|
||||
|
||||
(defn subtotal-by-column-row [pnl-datas title & [cell-args]]
|
||||
@@ -477,11 +498,11 @@
|
||||
(map
|
||||
(fn [p]
|
||||
(let [pnl-data (-> p (filter-numeric-code numeric-code numeric-code))
|
||||
this-name-exists? (->> (:data p)
|
||||
(filter (comp #{name} :name))
|
||||
seq)]
|
||||
;; Code, not name: the row is keyed on the code, and a
|
||||
;; client can reach it under a name another client won.
|
||||
this-code-exists? (seq (:data pnl-data))]
|
||||
(merge
|
||||
(if this-name-exists?
|
||||
(if this-code-exists?
|
||||
{:format :dollar
|
||||
:filters (:filters pnl-data)
|
||||
:value (aggregate-accounts pnl-data)}
|
||||
|
||||
129
test/clj/auto_ap/ledger/reports_test.clj
Normal file
129
test/clj/auto_ap/ledger/reports_test.clj
Normal file
@@ -0,0 +1,129 @@
|
||||
(ns auto-ap.ledger.reports-test
|
||||
(:require
|
||||
[auto-ap.ledger.reports :as sut]
|
||||
[clojure.test :refer [deftest is testing]]))
|
||||
|
||||
;; A client's bank account and the financial account it posts to share a
|
||||
;; numeric code. Reports key their rows off that code, so before the fix the
|
||||
;; pair rendered as two rows carrying the same code-level total, and the
|
||||
;; section stopped footing to its own subtotal.
|
||||
|
||||
(def ^:private period #inst "2026-08-14")
|
||||
|
||||
(defn- account
|
||||
[numeric-code name amount]
|
||||
{:client-id 1
|
||||
:location "M"
|
||||
:numeric-code numeric-code
|
||||
:name name
|
||||
:amount amount
|
||||
:debits 0.0
|
||||
:credits amount
|
||||
:count 1
|
||||
:account-type :account-type/liability
|
||||
:period period})
|
||||
|
||||
(defn- bank-account
|
||||
"Same as `account`, but its name came from a :bank-account rather than the
|
||||
chart of accounts — which is what earns it the row label."
|
||||
[numeric-code name amount]
|
||||
(assoc (account numeric-code name amount) :bank-account-name? true))
|
||||
|
||||
(defn- pnl-data
|
||||
[data]
|
||||
(sut/->PNLData {:periods [period]} data {1 "CLIENT"}))
|
||||
|
||||
(defn- labels
|
||||
[rows]
|
||||
(map (comp :value first) rows))
|
||||
|
||||
(deftest used-accounts-collapses-a-shared-numeric-code
|
||||
(testing "two names on one code yield a single entry, named for the bank account"
|
||||
(is (= [{:numeric-code 21010 :name "Capital One CC - 3196"}]
|
||||
(sut/used-accounts
|
||||
[{:data [(bank-account 21010 "Capital One CC - 3196" -100.0)
|
||||
(account 21010 "Accounts Payable 10" -150.0)]}]))))
|
||||
|
||||
(testing "the bank account wins even when the financial name is more common"
|
||||
(is (= [{:numeric-code 21010 :name "Capital One CC - 3196"}]
|
||||
(sut/used-accounts
|
||||
[{:data [(bank-account 21010 "Capital One CC - 3196" -100.0)]}
|
||||
{:data [(account 21010 "Accounts Payable 10" -150.0)]}
|
||||
{:data [(account 21010 "Accounts Payable 10" -150.0)]}]))))
|
||||
|
||||
(testing "distinct codes are left alone, ordered by code"
|
||||
(is (= [{:numeric-code 21009 :name "Due to Grand Ventures"}
|
||||
{:numeric-code 21010 :name "Capital One CC - 3196"}]
|
||||
(sut/used-accounts
|
||||
[{:data [(bank-account 21010 "Capital One CC - 3196" -100.0)
|
||||
(account 21009 "Due to Grand Ventures" 25.0)]}]))))
|
||||
|
||||
(testing "with no bank account in play the most common name wins"
|
||||
(is (= [{:numeric-code 21010 :name "Accounts Payable 10"}]
|
||||
(sut/used-accounts
|
||||
[{:data [(account 21010 "Due to Sandwich Monkey" -100.0)]}
|
||||
{:data [(account 21010 "Accounts Payable 10" -150.0)]}
|
||||
{:data [(account 21010 "Accounts Payable 10" -150.0)]}]))))
|
||||
|
||||
(testing "an even split breaks alphabetically so runs are reproducible"
|
||||
(is (= [{:numeric-code 21010 :name "Accounts Payable 10"}]
|
||||
(sut/used-accounts
|
||||
[{:data [(account 21010 "Due to Sandwich Monkey" -100.0)]}
|
||||
{:data [(account 21010 "Accounts Payable 10" -150.0)]}])))
|
||||
(is (= [{:numeric-code 21010 :name "Amex - 41001"}]
|
||||
(sut/used-accounts
|
||||
[{:data [(bank-account 21010 "BofA CC - 8779" -100.0)]}
|
||||
{:data [(bank-account 21010 "Amex - 41001" -150.0)]}]))
|
||||
"two bank accounts still resolve to one, deterministically")))
|
||||
|
||||
(deftest balance-sheet-renders-one-row-per-shared-code
|
||||
(let [report (sut/summarize-balance-sheet
|
||||
(pnl-data [(bank-account 21010 "Capital One CC - 3196" -100.0)
|
||||
(account 21010 "Accounts Payable 10" -150.0)
|
||||
(account 21009 "Due to Grand Ventures" 25.0)]))
|
||||
rows (:rows report)]
|
||||
|
||||
(testing "the shared code appears once, under the bank account's name"
|
||||
(is (= 1 (count (filter #(= "Capital One CC - 3196:21010" %) (labels rows)))))
|
||||
(is (not (some #{"Accounts Payable 10:21010"} (labels rows)))))
|
||||
|
||||
(testing "its row carries the total for the code, not one account's share"
|
||||
(is (= -250.0
|
||||
(->> rows
|
||||
(filter #(= "Capital One CC - 3196:21010" (:value (first %))))
|
||||
first
|
||||
second
|
||||
:value))))
|
||||
|
||||
(testing "the detail rows foot to the section subtotal"
|
||||
(let [detail (->> rows
|
||||
(filter #(re-find #":\d+$" (str (:value (first %)))))
|
||||
(map (comp :value second)))
|
||||
subtotal (->> rows
|
||||
(filter #(= "Liabilities" (:value (first %))))
|
||||
(keep (comp :value second))
|
||||
last)]
|
||||
(is (= 2 (count detail))
|
||||
"three accounts across two codes collapse to two rows")
|
||||
(is (= -225.0 (reduce + 0.0 detail) subtotal)
|
||||
"before the fix 21010 printed twice and the rows overshot the subtotal")))))
|
||||
|
||||
(deftest balance-sheet-keeps-a-clients-figure-when-another-client-named-the-code
|
||||
(testing "a column is blank only when that client has nothing at the code"
|
||||
(let [rows (:rows (sut/summarize-balance-sheet
|
||||
(sut/->PNLData
|
||||
{:periods [period]}
|
||||
[(assoc (bank-account 21010 "Capital One CC - 3196" -100.0) :client-id 1)
|
||||
(assoc (account 21010 "Accounts Payable 10" -150.0) :client-id 2)
|
||||
(assoc (account 21009 "Due to Grand Ventures" 25.0) :client-id 1)]
|
||||
{1 "ONE" 2 "TWO"})))
|
||||
shared (->> rows
|
||||
(filter #(re-find #":21010$" (str (:value (first %)))))
|
||||
first)
|
||||
absent (->> rows
|
||||
(filter #(re-find #":21009$" (str (:value (first %)))))
|
||||
first)]
|
||||
(is (= [-100.0 -150.0] (map :value (drop-last (rest shared))))
|
||||
"client TWO reaches 21010 under a name client ONE won, and still shows its balance")
|
||||
(is (= [25.0 ""] (map :value (drop-last (rest absent))))
|
||||
"client TWO has nothing at 21009 and stays blank"))))
|
||||
@@ -1,10 +1,98 @@
|
||||
(ns auto-ap.ledger-test
|
||||
(:require
|
||||
[auto-ap.integration.util :refer [wrap-setup]]
|
||||
[clojure.test :as t]))
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.integration.util :refer [test-account test-bank-account test-client
|
||||
wrap-setup]]
|
||||
[auto-ap.ledger :refer [build-account-lookup]]
|
||||
[clojure.test :as t]
|
||||
[datomic.api :as dc]))
|
||||
|
||||
(t/use-fixtures :each wrap-setup)
|
||||
|
||||
(t/deftest build-account-lookup-prefers-the-bank-account-name
|
||||
(t/testing "a bank account and the financial account sharing its code resolve to one name"
|
||||
(let [{:strs [client bank financial]}
|
||||
(:tempids @(dc/transact conn
|
||||
[(test-account :db/id "financial"
|
||||
:account/name "Accounts Payable 10"
|
||||
:account/numeric-code 21010
|
||||
:account/type :account-type/liability)
|
||||
(test-client :db/id "client"
|
||||
:client/bank-accounts
|
||||
[(test-bank-account :db/id "bank"
|
||||
:bank-account/name "Capital One CC - 3196"
|
||||
:bank-account/numeric-code 21010
|
||||
:bank-account/type :bank-account-type/credit)])]))
|
||||
lookup (build-account-lookup client)]
|
||||
|
||||
(t/is (= "Capital One CC - 3196" (:name (lookup bank))))
|
||||
(t/is (= "Capital One CC - 3196" (:name (lookup financial)))
|
||||
"the financial account takes the bank account's name, so reports render one row")
|
||||
|
||||
(t/testing "and both are flagged as bank-named for downstream report labelling"
|
||||
(t/is (true? (:bank_account_name? (lookup bank))))
|
||||
(t/is (true? (:bank_account_name? (lookup financial)))))
|
||||
|
||||
(t/testing "without changing the code or the account type"
|
||||
(t/is (= 21010 (:numeric_code (lookup bank))))
|
||||
(t/is (= 21010 (:numeric_code (lookup financial))))
|
||||
(t/is (= :account-type/liability (:account_type (lookup financial))))))))
|
||||
|
||||
(t/deftest build-account-lookup-leaves-unshared-codes-alone
|
||||
(t/testing "an account with no bank account at its code keeps its own name"
|
||||
(let [{:strs [client financial]}
|
||||
(:tempids @(dc/transact conn
|
||||
[(test-account :db/id "financial"
|
||||
:account/name "Sales Taxes Payable"
|
||||
:account/numeric-code 23000
|
||||
:account/type :account-type/liability)
|
||||
(test-client :db/id "client"
|
||||
:client/bank-accounts
|
||||
[(test-bank-account :db/id "bank"
|
||||
:bank-account/name "Capital One CC - 3196"
|
||||
:bank-account/numeric-code 21010
|
||||
:bank-account/type :bank-account-type/credit)])]))
|
||||
lookup (build-account-lookup client)]
|
||||
(t/is (= "Sales Taxes Payable" (:name (lookup financial))))
|
||||
(t/is (false? (:bank_account_name? (lookup financial))))))
|
||||
|
||||
(t/testing "another client's bank account never renames this client's accounts"
|
||||
(let [{:strs [mine financial]}
|
||||
(:tempids @(dc/transact conn
|
||||
[(test-account :db/id "financial"
|
||||
:account/name "Accounts Payable 10"
|
||||
:account/numeric-code 21010
|
||||
:account/type :account-type/liability)
|
||||
(test-client :db/id "mine")
|
||||
(test-client :db/id "theirs"
|
||||
:client/bank-accounts
|
||||
[(test-bank-account :db/id "bank"
|
||||
:bank-account/name "Capital One CC - 3196"
|
||||
:bank-account/numeric-code 21010
|
||||
:bank-account/type :bank-account-type/credit)])]))
|
||||
lookup (build-account-lookup mine)]
|
||||
(t/is (= "Accounts Payable 10" (:name (lookup financial))))
|
||||
(t/is (false? (:bank_account_name? (lookup financial)))))))
|
||||
|
||||
(t/deftest build-account-lookup-breaks-ties-between-bank-accounts
|
||||
(t/testing "two bank accounts on one code resolve deterministically by sort-order"
|
||||
(let [{:strs [client first-bank second-bank]}
|
||||
(:tempids @(dc/transact conn
|
||||
[(test-client :db/id "client"
|
||||
:client/bank-accounts
|
||||
[(test-bank-account :db/id "second-bank"
|
||||
:bank-account/name "US Bank 2974"
|
||||
:bank-account/numeric-code 13101
|
||||
:bank-account/sort-order 3)
|
||||
(test-bank-account :db/id "first-bank"
|
||||
:bank-account/name "Fremont Gyro HB Main 8576"
|
||||
:bank-account/numeric-code 13101
|
||||
:bank-account/sort-order 0)])]))
|
||||
lookup (build-account-lookup client)]
|
||||
(t/is (= "Fremont Gyro HB Main 8576"
|
||||
(:name (lookup first-bank))
|
||||
(:name (lookup second-bank)))))))
|
||||
|
||||
(t/deftest entity-change->ledger
|
||||
#_(t/testing "Should code an expected deposit"
|
||||
(let [{:strs [ed ccp receipts-split client]}
|
||||
|
||||
@@ -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