NOVUS / RESTAURANT

Documentation

The import contract, exactly

Every column the purchase importer accepts, what a row must satisfy, and how a price cell is read - generated from the parser and tested against it.

Last reviewed against the product on .

This is the exact contract the purchase importer holds a file to. It is generated from the same constant the parser reads, and a test fails if this page and the parser ever disagree, so a file formatted to match this page will be read.

Two files you can download

Rather than describe a compatible file, here is one. Both are fictional and neither contains a real supplier or a real price.

  • A sample purchase file that imports in full, also available as XLSX and JSON. It deliberately includes the formats that most often go wrong: a thousands separator, a currency symbol, a derived total with no total column, and a decimal comma.
  • A failure sample in which every row is refused, one per documented reason. This is the more useful of the two for checking your own process, because it proves the refusals are visible rather than silent.

Accepted files

CSV, TSV, XLSX, and PDFs containing real text. A scanned PDF is a picture of an invoice; nothing can read a price out of it without guessing, so it is refused rather than approximated.

Columns

Header matching ignores case and punctuation, so Unit Price, unit_price and UNIT-PRICE are the same column. Where several names are listed, the first one carrying a non-empty value wins.

Columns the importer recognises
FieldRequiredAccepted header names
SupplierYessupplier, supplier name, vendor, vendor name
ItemYesitem, item name, product, product name, description, ingredient
DateYesdate, purchase date, invoice date, order date
TotalYes*total, total price, line total, extended cost, amount
QuantityNoquantity, qty, volume
Unit priceNounit price, price, price each, cost per unit
UnitNounit, uom, unit of measure
CurrencyNocurrency, currency code

Measured from PURCHASE_COLUMN_ALIASES in src/lib/imports/normalizer.ts.

* A total is required, but it does not have to be a column. If quantity and unit price are both present and readable, the total is derived from them.

What a row must satisfy

A row is stored only when it has a supplier, an item, a date that parses, and a total of zero or more. A row failing any of those is rejected and counted in the import run's rejected total; it is never stored with a missing or zero value standing in for the real one.

Numbers

This is the part that decides whether your file imports cleanly.

How a price cell is read
CellRead asWhy
1234.561234.56Unambiguous
1,234.561234.56Both separators present, so the rightmost is the decimal point
$1,234.561234.56Currency symbols and spaces are ignored
1.234,561234.56Both separators again, read the same way
1,234,5671234567A decimal separator cannot repeat, so these group
(123.45)-123.45Accounting notation for a negative
1,234REFUSED1234 in London, 1.234 in Berlin, and no way to tell

Measured from the numeric reader in src/lib/imports/normalizer.ts, verified by tests/unit/import-number-formats.test.ts.

Dates

Any format JavaScript's date parser accepts, which includes 2026-09-12 and 12 Sep 2026. ISO order is safest: 03/04/2026 is read differently in different places, and the importer cannot know which one your supplier meant.

Currency

Recorded exactly as supplied and upper-cased. It is not converted. Two rows in different currencies are never added together anywhere on this site.

Duplicate imports

Re-uploading the same file does not double your costs. An import run is fingerprinted and a repeat is reported as a duplicate rather than processed again.

The seven kits

Seven fictional files, so you can see what a compatible export looks like before touching your own data. Each downloads as CSV, XLSX or JSON from /api/imports/kits?kit=<id>, and no account is needed.

Four are upload-ready. Every row in them is round-trip tested against the real importer by tests/unit/import-kits.test.ts, and each carries deliberate failure rows alongside the good ones, because a file that only succeeds tells you nothing about what happens when yours does not.

  • suppliers - several suppliers and items with an explicit line total. The baseline: if your export can be made to look like this, it imports.
  • ingredient-master - a price list with no Total column, so the total is derived from quantity and unit price. This is the shape a supplier catalogue actually arrives in.
  • pricing - one ingredient across several dates at a moving price, which is what the price-change comparison reads.
  • sales-mix - a till export with gross, discounts and tax kept apart, plus the void and comp lines a real export always carries.

Add &rows=clean to leave the failure rows out.

Three are templates, and are labelled so. This site imports purchases and sales and nothing else: menus, recipes and stock counts are entered on their own pages. menu, recipe-costing and inventory are spreadsheet shapes to start from, not files this site can accept, and the download says that in its own method line.

A date hazard worth knowing

2026-09-01 is always read correctly. A slash date with a two-digit year is not refused - it is read by the browser's own rules, which put the month first. 01/09/26 becomes 9 January 2026, not 1 September, and nothing warns you. Every kit above uses ISO dates for this reason, and so should your export.

Next: import a supplier file end to end.