# NumeraBooks 2.11.0 — the four reported issues, plus journal credits

---

## 1 · "25 entries per page and no option to increase it"

**The page limit was never 25.** `LIST_PAGE_SIZE` is 100 and always has been. The 25 you were
seeing is the **Customer/Vendor Center transaction list** — a hard `LIMIT 25` with no page bar
anywhere on the screen, so transaction 26 was unreachable from anywhere in the product.

- That list is now paged, and gained an **"All N in a report →"** button that opens every
  transaction for that name in Transaction Detail.
- A **rows-per-page control** (50 / 100 / 250 / 500 / 1,000 / All) is now in the page bar on every
  paged screen, remembered per machine. It shows even on a single page — that is exactly when
  someone with 120 rows wants the limit raised, and hiding it until page two puts it where it is
  least likely to be found.
- `All` is offered because people ask for it. It is genuinely heavy on a large file: the general
  ledger on your reference file is 20,400 postings in one DOM assignment.

## 2 · "only payroll liability and paychecks are skipped, 600+ transactions"

Two independent causes, both real:

- **The payroll policy checkbox was unticked by default and never remembered.** Unticked, *every*
  paycheque and payroll liability transaction is blocked and counted as "skipped". That alone is
  your 600. The acceptance is now **stored against the company** by policy version, with the date —
  so it is asked once and recorded, not re-asked every run with the destructive answer as default.
  Until it is ticked, the card now states in red exactly what will be excluded.
- **The report-based payroll path returned silently** when a run carried no Journal reports: no
  exception, no counter, nothing on the result screen. So "this file has no payroll" and "this file
  has payroll and we could not read any of it" looked identical. It now reports itself, and explains
  the two causes — a cancelled connector run, or a cutover date later than your earliest payroll
  (the reports are pulled *after* the main export, so a run stopped early produces a complete-looking
  file with no payroll in it).

Two latent bugs found and fixed while in there:

- **"Import everything except payroll" didn't work.** The object path honoured it; the report path
  read `opts.reports` directly and never consulted the include set, so unticking payroll excluded it
  from one path and imported it from the other.
- **Double-post risk.** The two paths key idempotency differently — object path on QuickBooks'
  TxnID, report path on its transaction number — so the namespaces never intersect. On a file where
  the payroll subscription *is* active, every paycheque would have posted twice, each path believing
  it was first, with the books still balancing. The object path now records what it posted under the
  key the report path uses.

## 3 · "no option to reclassify transactions"

Correct — the only thing called "reclassify" moved account *types* on the chart of accounts.

`E.reclassifyTxns()` moves account and/or class on a set of transactions, reachable from all three
places you asked for: **Transaction Detail** (tick rows, or take all N matching the filter),
the **document-list bulk bar**, and **one row** from the general ledger. Always previews first.

Two things that needed care:

- **An account lives in two tables.** `txn_line.account_id` is what the document shows;
  `ledger.account_id` is what every report reads. Moving one without the other leaves them
  disagreeing with nothing to flag it, because each is internally consistent.
- **The obvious implementation would have been wrong.** `editDoc()` rebuilds a document by deleting
  and re-posting its rows — which destroys the reconciliation marks pointing at them. Reclassifying
  a year of cleared cheques that way would silently un-reconcile every one. This is a targeted
  UPDATE, so the ledger rows keep their identity and recon marks and allocations survive.

It refuses locked documents (all imported payroll), closed periods, reconciled transactions and
voids — each with a reason saying what to do instead.

Also fixed en route: **`ensureAccount()` returned nothing**, so every caller written as
`const id = ensureAccount(...)` got `undefined` — which is why the import workbench's "create
accounts that don't exist yet" option appeared to do nothing. The account *was* created; the id came
back undefined and the caller read that as failure.

## 4 · "bug while recon, we are not able to change the dates"

A re-render collision. The date inputs sat **inside** the element `rcLoad()` rewrites. Chromium
fires `change` on a date input the moment all three segments hold any value — so typing "2026" into
the year completed the field at year 0002, fired the handler, rebuilt the container, and destroyed
the input under your cursor. The remaining digits went nowhere.

The controls now live in the outer card; only the row lists re-render. Two related traps also fixed:

- The statement date **reset to today on every visit**. It now seeds from that account's last
  session — opening a half-finished statement and finding today's date is how marks get restamped
  into the wrong month.
- **Re-saving silently restamped earlier marks.** Save June, change the box to July, save again, and
  every June mark was rewritten as July — the June statement vanished from the history. You are now
  warned before that happens, with the count and the dates.

## 5 · "journals made instead of a payment cannot be applied as a credit on an open invoice"

Correct, and there was not one cause but three. Any one of them alone would have hidden the
feature completely, which is why it looked like something nobody had built rather than something
broken.

**The picker was a hard-coded type list, copied three times.** `loadOpenDocs`, `applyOpenCredits`
and `applyCreditToDoc` each carried their own `type IN ('credit_memo','payment_in')`. A journal
was not in the list, so it was missing from all three screens at once — and fixing one would have
left the other two wrong. There is now one `creditSources()` definition and all three use it.

**A journal is always worth zero.** `creditOpen()`, `docOpen()` and `openAmount()` measured a
document as `total − allocated`, and `createJournal()` has never set a total. So even had the
journal reached the picker, it would have been filtered straight back out as having nothing to
give. What a journal is worth is not its total but its **net posting to the control account**, and
the side it lands on decides which way it faces:

| | means | so it is |
|---|---|---|
| credits A/R | the customer owes less | a credit, to apply |
| debits A/R | the customer owes more | a document, to settle |
| debits A/P | we owe the vendor less | a credit, to apply |
| credits A/P | we owe the vendor more | a document, to settle |

**The Name column on the journal form was decorative.** Whatever you typed was folded into the
line's memo text and thrown away — a string on a ledger row, linked to nothing. So a journal
written against A/R with the customer named looked filed correctly and was not filed at all: it
sat on the control account belonging to nobody, never reached that customer's aging, and could
never be applied. The name now resolves to a real party, and **a journal line posting to A/R or
A/P is refused without one**, naming the line and saying why. This is the same rule QuickBooks
applies to a General Journal Entry, and it is the root cause rather than a symptom of it.

Migrated history is exempt: a QuickBooks file carries control-account entries older than this rule,
and refusing them would drop real transactions to enforce a policy that can only apply going
forward. They import as they are and show on the aging as exceptions, which is where an
unattributable receivable belongs.

### Two things found on the way

**MIG-043 was half-done.** It had already relaxed the *type* guard so a receipt could settle a
journal on A/R — a bounced cheque, a rebilled cost — but not the *amount* guard, which read the
same always-zero total. The type check passed and the settlement then failed with "only 0.00 is
open on that document". Same root cause, opposite direction; both now fixed.

**Cash-basis P&L counted journal credits as cash.** The direct pass excluded a hard-coded list of
accrual document types, which was a list of the ones that existed when it was written. A journal
posted to A/R is one too. Left in, a 400 allowance against an invoice subtracted 400 from
cash-basis income *immediately*, while the invoice separately recognised income in proportion to
cash collected — the same adjustment counted twice, and a statement showing negative income before
a penny had moved. Journals on a control account are now recognised as their cash arrives, like
every other accrual document, so a sale entered as a journal still reaches the cash-basis P&L.

### Guards added, because type no longer announces the side

While only credit memos and vendor credits could be a source, the party check was enough — their
type already fixed which control account was involved. "Journal" does not. A vendor's A/P journal
applied to an invoice would relieve Accounts Receivable for a debt sitting in Accounts Payable,
and it would still balance, so nothing downstream would ever have complained. Both sides of an
application must now resolve to the same control account, and a journal that touches neither is
refused outright rather than linked to something it has no relationship with.


---

## Verification

**3,530 assertions across 66 suites, 0 failing.** New: `reclassify` (37) and `jecredit` (71).

`npm test` itself was also wrong. It was a hand-written chain of `node tests/x && node tests/y`,
and **fourteen suites had never been added to it** — find, xlsx, export, txndetail, search, sort,
sortpaged, xlsxread, map, importpost, importcheck, reclassify, migration4 and jecredit. Roughly a
thousand assertions, all green, none of them running on `npm test`. A test that is not in the
runner cannot stop a bad build. The list is no longer written down: `tests/all.js` runs everything
matching `tests/*.test.js`, and treats a suite that crashes before printing a summary as a failure
rather than as silence.

One of my own new checks was wrong and caught by the suite: the "payroll reports missing" warning
fired whenever payroll existed without reports, including when the object path had imported it all
perfectly. It now fires only for records QuickBooks returned *empty* — a false alarm here would
teach people to ignore the message that matters.

## Still to do

Worth checking on your own file after installing:

- Re-run the QuickBooks import with the payroll box ticked and confirm the ~600 transactions land,
  and that the result screen reports them rather than skipping.
- On a converted file, imported A/R journals will mostly have names (the importer reads the
  EntityRef) but some will not. Those show on the aging as exceptions. They cannot be applied until
  they are attributed — which is correct, but it is worth knowing how many there are before someone
  goes looking for a credit that will not appear.
