Status: READY TO BUILD (2026-09-21). This replaces the draft of the same day. The three questions are answered
below as recommended — each is reversible and none of them blocks the build; Matthew has them and will amend
here or in from-hermes if he disagrees.The build does not wait for a wholesaler template. v1 ships the profile registry below with one generic profile;
a real template is added as one entry when Matthew pulls it from a wholesaler's own upload screen. Nothing is
invented in the meantime — see Do not build to invented layouts.
| Thing | Whose | State |
|---|---|---|
| Supply ordering is the next work item, after the guides' group C | Matthew | given 2026-09-21 |
| "Records must be updated accordingly when orders are being made" | Matthew | his named constraint |
| Ordering first, expired-stock / inbound-import after | Matthew | given 2026-09-21 |
| The three questions below | Matthew | answered as recommended, awaiting his confirmation |
| The build itself (W1–W9) | OpenCode | this page |
| The C6 guide, C1's amendment, live verification | Hermes | after deploy |
DRAFT → EXPORTED → RECEIVED (or CANCELLED), tied toGENERIC only, and the first real wholesaler profile is added the| Need | State today |
|---|---|
| Code for the file | StockItem.item_code exists; no PIP code; barcode is free text captured as data only (D3.2) |
| Split by supplier | No preferred supplier on StockItem. The supplier screen now exists (9bfb19c, Finding 7 closed) but items are not linked to a supplier |
| Export settings per supplier | Supplier is name / contact_name / telephone / email / is_active only — no code, account reference or profile |
| What to reorder | StockAlertThreshold.low_stock_level, which the low-stock report already uses |
| How much is on hand | The cached positions (fast, and what C2/C3 show) — not a live ERPNext call per item; see W4 |
| Where the file lands | Nothing leaves the app today; Phase 3 named procurement out of scope and the module map still lists it under Later |
W1 — catalogue keys (migration pharmacy_stock.0017). Add to StockItem: pip_code (CharField, blank — the
7-digit UK product code, leading zeros preserved), preferred_supplier (FK to Supplier, null, PROTECT), and
order_up_to_level (Decimal, null). All three appear on the item's admin and are documented as "whoever looks after
stock sets these".
W2 — supplier profile fields (Ticket E). Add to Supplier: supplier_code (choices AAH / ALLIANCE /
PHOENIX / OTHER, default OTHER), account_reference (CharField, blank — the pharmacy's own account number with
that wholesaler), export_profile (CharField choice, default GENERIC), integration_profile (choices CSV_EXPORT
default / DIRECT_API, the latter a placeholder for later, clearly labelled as not implemented) and
minimum_order_value (Decimal, null, display only — the app holds no purchase prices and must not pretend to
check it). Extend the existing supplier screens (9bfb19c) rather than adding new ones.
W3 — the export-profile registry. pharmacy_stock/order_profiles.py, a module-level dict keyed by profile name.
Each entry carries four things and nothing else:
| Entry field | Meaning |
|---|---|
headers |
the two column labels, in order, exactly as they go in the file |
code_field |
which StockItem field supplies the code (pip_code, barcode) |
verified |
where the layout came from — "generic two-column" for v1, or the wholesaler and the date its template was seen |
filename_suffix |
e.g. order.csv, so the download is recognisable |
v1 ships one entry: GENERIC — code = pip_code else barcode, and no header row.
Adding a wholesaler later is one dict entry plus a supplier edit — no view, model or template change.
This dict is the only place a column name or a file name appears; no layout is hard-coded in a view or template.
Corrected 2026-10-02. This line read "headers
ProductCode,Quantity" — what the portal's importer calls the
columns, not a header the app writes. The file contract
of 2026-09-29 settled the standard export as headerless, andorder_profiles.pynow says so in its own words
("the standard two-column order file is headerless"). A header is the exception, added only for a wholesaler whose
upload screen has actually been seen.
W4 — the reorder list and the order build. /stock/orders/new/:
StockAlertThreshold.low_stock_level, groupedpreferred_supplier. Use the cache, never a live call per item — a real pharmacy holds thousands of itemspositions_from_erpnext is two REST calls per item per warehouse.max(order_up_to_level − on_hand, 1) when the level is set, elsemax(low_stock_level − on_hand, 1). Every line is editable and every line can be dropped — the app proposes,item_code the portal will not recognise.ORD-YYYYMMDD-XXXXXX, matching theGIN- shape).W5 — the order models. Order (supplier, reference, status, created_by/at, last exported by/at, export count,
cancel reason) and OrderLine (stock item, quantity, position). Both live in pharmacy_stock, beside suppliers,
items and receipts — one app, no cross-app foreign key in either direction, one migration, and the receipt link on
W7 stays inside it. Status: DRAFT, EXPORTED, RECEIVED, CANCELLED. Lines are editable only in DRAFT. No
delete anywhere — cancel with a reason, mirroring the supplier screen's no-delete rule. Append-only audit on every
transition.
W6 — the export itself. A POST-only button on the order page (/stock/orders/<pk>/export/):
StreamingHttpResponse, text/csv, Content-Disposition naming the file<supplier>-order-<reference>.csv, X-Content-Type-Options: nosniff), UTF-8 without BOM, LF endings, commaEXPORTED, increments the export count, records actor and time, and writes an ORDER_EXPORTEDW7 — the order screens, and the link back from the delivery. /stock/orders/ (list: supplier, reference, status,
lines, exported, receipt) and /stock/orders/<pk>/ (detail: lines, ordered vs received, the export button, cancel).
Add GoodsInReceipt.order (nullable FK, SET_NULL) so a receipt can answer "which order was this?": the goods-in
form gains an optional Against order selector, filtered to that supplier's EXPORTED orders, and posting the
receipt sets the order to RECEIVED. The order page then shows ordered vs received per item and the difference —
which is where a short delivery becomes visible (Finding 6).
W8 — roles and audit. Manage everything (list, detail, build, export, cancel, and the receipt link) as
RP, Technician, Business Manager — PharmacyStockManageRequiredMixin, the goods-in gate. No Counter Assistant
surface. Audit: ORDER_CREATED, ORDER_UPDATED, ORDER_EXPORTED, ORDER_CANCELLED, plus the receipt link event.
No patient content anywhere — there is none in an order.
W9 — tests, evidence, report back. Tests for: the profile's header labels and code field reaching the file; a PIP
code's leading zeros preserved; a whole-number positive quantity; refusal of zero/negative/fractional and of an
all-flagged order; the no-supplier-code line being excluded and flagged; the order lifecycle and that lines are locked
outside DRAFT; RBAC refusals for the Counter Assistant; no ERPNext call and no cache write on export; and the
standing boundary assertion — zero ordering tables in the central database.
Live on test-client-pharmacy: build an order from the demo low-stock items, export it, and byte-compare the
received file against the generated CSV (header, code format, whole-number quantities); confirm the order record
shows EXPORTED with actor, time and export count, and that the audit rows exist; record a delivery against the order
and confirm ordered-vs-received displays; confirm no new Stock Entry in ERPNext and an unchanged position cache
around the export; and check the central database still holds zero ordering tables.
This feature gets a C6 guide ("order stock and export the order file") on the Coverage Plan once it is verified,
and C1 gains the "against an order" step. That is Hermes's work, after the deploy is verified.