Materials Docket Import
Bulk-import a batch of material delivery dockets from a CSV or XLSX file straight into the materials tab, instead of hand-entering each delivery one at a time. Every row is validated server-side, valid dockets are created (and auto-submitted where possible), and a clear per-row report tells you exactly what happened to the rest — nothing is silently dropped.
A material docket is not flat: it is a header (supplier, contract, date, supervisor, delivery reference) plus N material line-items (material, quantity, optional cost-code). The importer reconstructs that header → lines structure from your file, resolves human-friendly references to the right records, creates the dockets, attempts to submit them, and reports the outcome of each one.
Out of scope: project-location matching, mobile import, and editing existing dockets via file. This flow is create-only and web-only.
Where it lives
Open the diary’s Materials tab (/admin/diaries/{diaryId} → Materials) and click Import deliveries. A drawer opens that walks you through: pick a file → upload → live progress → result. You can download a ready-to-fill template (XLSX or CSV) from the first step.
Imports run as a durable background job, so the drawer shows live progress while the batch is processed and refreshes itself without polling.
File formats
There are two supported shapes. Both converge on the same internal model after parsing, so use whichever is easiest for you to author.
XLSX (primary) — two sheets
The XLSX format uses two sheets linked by a docket_ref column: a Dockets sheet (one row per docket header) and a Lines sheet (one row per material line). The docket_ref is an author-chosen key (e.g. D1) that ties a header to its lines; it must be unique within the file.
Sheet Dockets — one row per docket header:
| Column | Required | Accepts | Notes |
|---|---|---|---|
docket_ref | Yes | text | Author-chosen key (e.g. D1) linking to Lines; unique within the file. |
supplier | Yes | supplier name or UUID | Must be a supplier linked to the project. |
contract | Yes | contract name or UUID | Materials are always resolved within a contract scope, so every docket must name one. |
date | Yes | YYYY-MM-DD text | Enter dates as text YYYY-MM-DD (XLSX numeric date serials are also accepted; an Excel cell formatted as a date may export as a display string that won’t parse). |
supervisor | Conditional | name or UUID | Required unless diary_id supplies one. Must be a supervisor on the project (Docket Supervisor). Display names are not unique, so you may need to paste a UUID. |
diary_id | No | UUID only | If set, the supervisor is derived from the diary. |
shift_type | No | day | afternoon | night | |
delivery_docket_no | No¹ | text | Supplier’s delivery reference. |
source_location | No¹ | text | |
delivery_location | No¹ | text | |
notes | No | text | Free text. |
¹ Optional to create, but required to auto-submit — see Outcomes below.
Sheet Lines — one row per material line:
| Column | Required | Accepts | Notes |
|---|---|---|---|
docket_ref | Yes | text | Must match a Dockets.docket_ref. |
material | Yes | name / supplier SKU / UUID | Resolved within the docket’s contract. |
qty | Yes | number ≥ 0 | The entered quantity. |
cost_code | No | cost-code or UUID | Optional cost allocation, resolved within the project. |
CSV (fallback) — one denormalized sheet
CSV uses a single denormalized sheet: every header and line column on every row, with rows grouped by docket_ref. The header values are taken from the first row of each group; subsequent rows in the same group need only docket_ref, material, qty, and cost_code. Any header columns on those follow-up rows must be blank or identical — a mismatch is reported as a per-docket error.
docket_ref,supplier,contract,date,supervisor,delivery_docket_no,source_location,delivery_location,material,qty,cost_code
D1,Boral,Main Works,2026-06-29,R. Jones,BD-001,Wacol,Site A,20mm Aggregate,50,CC-100
D1,,,,,,,,Cement,12,CC-100
D2,Boral,Main Works,2026-06-29,R. Jones,BD-002,Wacol,Site A,Sand,30,CC-200In the example above, D1 is one docket with two lines (20mm Aggregate + Cement) and D2 is a separate docket with one line (Sand).
Templates
The drawer offers two downloadable templates — import-template.xlsx (two sheets) and import-template.csv (denormalized) — each pre-filled with the correct headers and one example docket group. Start from one of these to avoid header-mapping mistakes.
References: name/code or UUID
Every reference cell is hybrid — it accepts either a human-friendly name/code or a UUID. Finance can type the names they already know; exports and power users can paste UUIDs. Resolution happens server-side, tenant-scoped:
- supplier → matched on the supplier organization name (must be linked to the project), or its UUID.
- contract → matched on the contract name for the resolved supplier, or its UUID. Contract names are not guaranteed unique, so a duplicate name raises an ambiguity error.
- material → matched on supplier SKU first, then name, within the docket’s contract, or its UUID. Because every material belongs to a contract,
contractis always required. - supervisor → matched against the project’s eligible members, or its UUID. Display names are not unique, so name matches often hit ambiguity — paste a supervisor UUID when that happens.
- cost_code → matched on the activity’s
cost_codein the project, or its UUID. Cost codes are not unique, so this can also raise ambiguity.
If a value matches nothing, you get a “not found” error; if it matches more than one record, you get an “ambiguous” error. Both name the column, the value, and the docket_ref/line so you can fix the sheet. Reference failures are row-level — they never abort the whole file.
Outcomes
The import is partial-success: every valid docket is created, and everything else is collected into a structured report. A finished import gives you a summary (Total / Submitted / Drafted / Skipped / Failed) plus per-row detail tables.
Each docket lands in one of these states:
- Submitted — the docket was complete (it had
delivery_docket_no,source_location,delivery_location, at least one line, and passed submit validation), so it was created and auto-submitted. - Drafted (flagged) — the docket was created but could not be submitted, because a submit field was missing or a submit-validation rule failed. It lands as a flagged draft with the reason and the missing fields listed, so nothing is lost — you can finish it by hand.
- Skipped (duplicate) — a docket with the same project + supplier +
delivery_docket_noalready exists. Duplicates are skipped and reported, so re-uploading an overlapping file is safe and never double-inserts. - Failed — a row-level problem (missing required field, unresolved/ambiguous reference, bad
qty, unknownshift_type, unparseable date, a line whosedocket_refhas no header, a docket with no lines, etc.). The failure names thedocket_ref/row and the reason.
A file-level problem — a missing required sheet/column, an unparseable workbook, an empty/headers-only file — fails the whole job with a friendly message rather than partially importing.
An import never spins forever. One that could not start says Import couldn't start — please retry; one that stopped part-way is marked failed within about 10 minutes (Import interrupted — please retry); any other failure that is not about your file says Import failed — please retry. Upload the file again to retry — dockets the first attempt already created are skipped as duplicates.
Caps
A single file may contain at most 1,000 dockets and 10,000 line rows. Exceeding either limit is a file-level failure with a clear message — the importer never silently truncates.
Spec & plan
- Spec:
docs/superpowers/specs/2026-06-29-issue-95-bulk-upload-material-dockets-design.md(§4 file format, §5 references, §10 outcomes & caps)