# NumeraBooks 2.10.0 — The Import Workbench, and an honest answer to "what didn't import?"

> "a screen while importing … show me what to choose, mapping options and what failed … and if the
> system says everything is imported and then I manually compare and 1,000 invoices are missing …
> I should have the option to import those."

---

## What was wrong

| | Before |
|---|---|
| **Excel** | **The product could write .xlsx but never read one.** The screen labelled "register (Excel/CSV)" accepted pasted text or CSV only — so "import my Excel file" quietly meant "go and save it as CSV first". |
| **Column mapping** | Two of six import paths had a mapping UI, written twice as two independent copies. **Four were hard-coded by column position** — batch bills/invoices demanded exactly Number, Date, Party, Account, Description, Qty, Rate, Tax in that order. A real QuickBooks export, whose columns are in a different order, either failed or imported the date as the customer name. |
| **What failed** | Per-row failure detail existed for **one** path. Everywhere else a rejected row was simply dropped: the bank importer discarded rows with no date silently, the employee importer tracked no errors at all. Nothing was exportable. |
| **Completeness** | The importer already computed the full per-type "source said N, we posted M" arithmetic — `accountedFor`, `unaccounted`, `unaccountedIds`, SHA-256 checksums of every source id — and **never displayed any of it**. The one real reconciliation could only run while the original file was still open in memory, which is not when anyone asks the question. |

---

## What shipped

### IMP-401 · A real .xlsx reader (`src/nbxlsxread.js`)
Zero-dependency, runs in the renderer. The format has four traps that corrupt an import rather than failing it, and each is handled:

- **Shared strings** — text lives in a separate part and cells hold an *index*; read naively, every text column becomes integers.
- **Dates are numbers** — 46261 is a date only because its *style* says so, so the number formats are read.
- **The 1900 leap-year bug** — serials ≤ 60 are off by one.
- **Blank cells are absent from the XML entirely** — so cells are placed by their `r="D2"` reference. Appending in document order shifts every later value one column left, which doesn't throw; it just files the amount under Name.

Includes a pure-JS DEFLATE decoder, because the renderer has no zlib.

### IMP-402 · One mapping engine (`src/nbmap.js`)
Every record type described as fields with types, requirements and header aliases. Guesses from the header *and* the data, so headerless exports still work; every guess is overridable per column.

**Ambiguous dates are refused, not guessed.** `03/04/2026` is 3 April to half the world and 4 March to the other half. Guessing moves transactions into the wrong month, the totals still add up, and nobody notices until a return is wrong — so the file is flagged and you choose.

Amounts parse the way accountants write them: `(1,234.50)`, `1234-`, `500 CR`, `₹2,500`.

### IMP-403/404 · The Import Workbench (`#/importwb`)
Five visible steps: **choose a file → what the rows are → map the columns → preview → import.**

The preview names every row that will *not* import and why, before anything posts. After importing, the same list again — plus a one-click **spreadsheet of everything that is not in your books**, carrying your original columns so you fix the cell in place and re-import the same file.

Documents are stamped with a `source_id` derived from the file, so **re-importing is a no-op rather than a doubling** — which is what makes the recovery path safe.

### IMP-405 · "Check What Imported" (`#/importcheck`)
Compares your books against something **outside** NumeraBooks, because a count from our own tables can only ever agree with itself. Three sources, because each catches a different failure:

1. **A list you export** — from QuickBooks or anywhere; it only needs a column of document numbers. This is the comparison you were doing by hand.
2. **The source ids saved with a batch** — so the question can be asked days later without the original file.
3. **A live QuickBooks re-query** — catches records that never reached the export file at all, the likeliest cause of a silent thousand-invoice gap.

Per type: how many the source had, how many are in the books, how many are missing and what they're worth. Then **Import the missing ones →** loads them straight into the mapping screen.

`complete` means every single record, never a percentage. 99.4% presented as a pass is how a thousand missing invoices get signed off.

---

## Two bugs found while building this

- **XML attribute order is arbitrary.** Excel writes `Id` before `Target` in the workbook relationships; openpyxl writes `Target` before `Id`. A single regex expecting one order silently matched nothing on files from the other, and every tab came back named "Sheet1". Caught only because the fixtures are built by an *independent* writer.
- **`createInvoice` silently ignores unknown fields**, so `source_id` went nowhere and the duplicate guard never fired — importing the same file twice would have doubled the books. Documents are now stamped after creation.

---

## Verification

- **3,430 assertions across 65 suites, 0 failing.** New: `xlsxread` (58), `map` (93), `importpost` (42), `importcheck` (51).
- The completeness test **stages the exact complaint**: 60 invoices and 20 bills where 10 invoices cannot post. It proves the check finds exactly those 10, the recovery imports them, and a second recovery run posts nothing.
- The reader is tested against workbooks built by **openpyxl**, and the pure-JS inflater is exercised with zlib disabled — the path that actually runs in the app.
- **Live, in the installed build**, on a real company file: a .xlsx with QuickBooks-ordered columns, a two-line invoice and two bad rows. Dates decoded, columns auto-mapped, rows 2+3 correctly grouped into one $375 invoice, the bad date named with its row number — and on import it reported **0 posted, 4 not in your books** with a precise reason for each, rather than posting or claiming success. That file's books were left untouched.

## Honest notes

- **The old `.xls` format is not supported** — only `.xlsx`. The workbench says so and tells you to Save As.
- **Unknown customers and accounts stop a document by default.** Creating them silently is how a chart of accounts ends up with "4000 Sales" and "Sales" as two accounts; there are tick-boxes to allow it, and the count created is reported.
- **qbXML and OFX keep their own importers** — they are structured formats, not column-mapped ones — but the workbench links to both so there is one place to start.
- The live-QuickBooks comparison is implemented and tested against supplied records; wiring it to the connector's own re-query is the one piece still to do.
