Title rows above the header, merged cells, currency strings, negatives written in brackets, column names nobody standardised. The normalizer looks for the real header rather than assuming row one, matches columns by meaning, and hands back every row it could not read with that row’s number and a reason.
PDF · CSV · XLSX
Every sheet in the workbook. Header junk, merged cells, currency formatting, bracketed negatives, Excel date serials.
The same normalizer, the same alias tables, the same integer-cent amounts. Quoting and embedded commas included.
Text layers, read page by page with each page's character offset kept so a quoted clause resolves to a page.
No OCR and no EDI parser today. A photographed remittance has no text layer to read. We will say so before we say otherwise.
The stub as it arrives, with each coercion marked on the cell it acted on. Then the rows that came out — and the ones that did not, with the number a person would see in the file and the reason.
Rows 1–4 are a title, a generated-on line, a notice and a blank. The first thirty rows are scanned for the one that looks like a table head; columns are then matched by name, not by position.
Symbol, commas and spaces stripped, then held as whole cents for the rest of the pipeline.
Every cell in a merged range takes the anchor's value — which is what a person sees in the sheet.
No category fits a reason that names a person rather than a document. It is surfaced as unknown, not pushed into the nearest bucket.
| invoice_ref | customer | billed | paid | short_pay | category |
|---|---|---|---|---|---|
| INV-90481 | Northline Grocers | $45,000.00 | $42,150.00 | $2,850.00 | unearned_discount |
| INV-90482 | Northline Grocers | $18,400.00 | $16,950.00 | $1,450.00 | Unclassified |
Two of billed, paid and short-pay is enough — the third is derived. Where all three are present they have to tie, and a row that does not tie comes back below rather than being quietly corrected.
missing invoice_ref
Send the export as it comes out of your system. Reformatting it by hand is the step this replaces — and where a row still cannot be read, you get told which row and why.
The first thirty rows are searched for the one that reads like a table head, and it has to carry at least three column names the normalizer recognises. Row one is never assumed.
An exact spelling like “Amt Paid” or “Check Date” takes its field in the first pass. Only then do the vague single words — “amount”, “date”, “code” — get a look at what is left.
Merge a customer name down four rows in Excel and a person reads four customers. So does the normalizer: every cell in the range takes the anchor value rather than three blanks.
$1,234.56 and 1234.56 land on the same integer. (123.45) and 123.45- both read negative. A value with sub-cent precision is reported as a bad cell rather than rounded quietly.
Billed less paid must equal the stated short-pay, or the row comes back with the arithmetic in the reason. Two of the three is enough on its own; the third is then derived.
Totals rows, repeated headers, rows with no invoice reference, amounts that will not parse. They return as a list you can read against your own file, not as a quietly shorter one.
The spreadsheet with the title rows, the merged cells, and the column names someone invented in 2019. If the normalizer cannot read a row, it tells you which row and why — and if it cannot read the file, we would rather find that out now than tell you it can.