Status: BUILT, DEPLOYED and VERIFIED LIVE — 24/24 (2026-09-22). Stage 3 changes what the order build proposes,
and it is live ontest-client-pharmacy. The two items below were parked "until I see everything in operation" and
were built as their recorded starting positions (model A, and deliberate routing to a non-usable price); either is
reversible in one line when Matthew's walk-through says otherwise.PARKED (Matthew, 2026-09-22): the creation button and the stale-price question below. "I cannot give answers to
these until I see everything in operation" — so this stage waits on his walk-through of stages 1 and 2, and the
starting positions recorded here (recommendation A, and a line may be routed to a supplier whose only price is
stale or unstated by the pharmacist's deliberate choice) are what would be built if he does not amend them. Both
are one-line reversals, and the same shape as every other parked item in this work.
Parent plan: supply-orders-part3-pricing ·
previous stage: supply-orders-pricing-phase2-visible ·
the v1 ordering build: supply-orders-spec.
Matthew, 2026-09-22: "I cannot give answers to these until I see everything in operation." So the choice below is
recorded, not decided, and the recommendation is the starting position if it is never amended. The build waits on his
walk-through of stages 1 and 2 (both live on test-client-pharmacy).
The build screen today builds one order for one supplier — Order.supplier is a single FK, OrderLine carries
only item + quantity, and the export is that supplier's file, which is also what a goods-in receipt is matched
against. Price-aware routing can be implemented two ways:
DRAFT → EXPORTED → RECEIVED lifecycle and the audit trail are allRecommendation: (A) now, (B) as a one-line follow-up if the evening run asks for it. Either way the routing rules
below are the same; only the button differs. Matthew's call — parked 2026-09-22.
Also parked with it, both one-line and both carried as starting positions until Matthew has used stages 1 and 2:
a line may be routed, by the pharmacist's deliberate choice, to a supplier whose only price is stale or of unstated
basis (the app never proposes such a price — rule 1 below — but the pharmacist may pick that supplier, which is what
keeps the stale rule from becoming a wall); and the build screen keeps the same parked column set as stage 2
(best price, spread, count, freshness), so the parked columns question stays parked in exactly one place.
OrderBuildView (pharmacy_stock/views.py) groups services.reorder_suggestions() rows byitem.preferred_supplier and posts one supplier; the code column comes fromorder_profiles.code_for(item, profile). OrderEditView is where lines are dropped; OrderExportView is POST-onlyPharmacyStockManageRequiredMixin (RP / Technician / BM) — the Counterpharmacy_stock/price_view.py is the single price answer (latest live file per supplier, best price andNOT_STATED shown but never comparable, working-day freshness, "No supplierOrderLine has no price field at all today, and Order no value — the numbers have to be added.PharmacyAuditEvent.Action carries ORDER_CREATED / UPDATED / EXPORTED / CANCELLED / RECEIVED; the nextpharmacy migration is 0019 and the next pharmacy_stock migration is 0006.Stated once, so screen, tests and guide cannot drift:
PARSED, its basis is statedPER_PACK / PER_UNIT) and it is not stale. A NOT_STATED or stale price is shown but is never silentlyPER_UNIT price against a PER_PACK one.preferred_supplier and say so on the rowpip_code, else barcode) for that item, or the proposal falls back to one that can, with the reason shown. TheW1 — the order's numbers (pharmacy_stock.0006). Add to OrderLine: unit_price (Decimal, null),
price_basis (same choices as SupplierPriceFile.PriceBasis, blank/null), price_recorded_at (Date, null) and
price_supplier (FK Supplier, null, PROTECT). The amount is copied by value on purpose — a later purge of
price rows (stage 5) must not be able to change or break an order record; the FK is to the supplier, which is never
deleted. Add to Order: routing_summary (CharField/blank) so the record says in words how it was routed, and
value_total derived on read, not stored.
W2 — the routing service. pharmacy_stock/order_routing.py: given the reorder rows and the price view, return one
proposal per line — {stock_item, quantity, proposed_supplier, unit_price, price_basis, price_recorded_at, reason} — implementing rules 1–5 above. One query set for the whole screen, and no ERPNext client and no cache
write (a build is draft-only, exactly as today).
W3 — the build screen. /stock/orders/new/ rows gain: the proposed supplier (a select, defaulted), the unit price
with its basis, the best price and spread from stage 2, and the reason when the proposal is not a price winner. The
screen then shows the basket grouped by the chosen supplier with each group's line count and value, and a button
per group creating that supplier's draft (DRAFT, ORD-YYYYMMDD-XXXXXX, snapshot fields written at creation).
Dropping a line stays as it is today (on the draft's edit screen, where the copy already points).
W4 — the order screens. order_detail.html and order_list.html show the value from the snapshots and the reason
once, and the detail page keeps its Received / Difference columns. No price is added to the export file, and
nothing changes in the DRAFT → EXPORTED → RECEIVED lifecycle or the receipt link.
W5 — audit. Add ORDER_ROUTED to PharmacyAuditEvent.Action (migration pharmacy.0019), recorded when a draft is
created by routing: the supplier, the line count, the value, how many lines kept the proposal and how many were
overridden, and how many fell back for want of a usable price. ORDER_CREATED stays as the creation event.
W6 — roles. PharmacyStockManageRequiredMixin throughout (RP / Technician / BM). The Counter Assistant is
refused the build, the drafts and the export as today (403) — routing must not widen a surface.
W7 — tests. Cheapest usable wins; a NOT_STATED price is never taken silently; a stale price never wins without
the pharmacist's act, and an item with only stale prices falls back to the preferred supplier with the reason;
a cheaper supplier that cannot code the item is not proposed and the reason names the code rule; an override is kept;
the snapshot survives deleting the price rows (by value, proven); the export file is byte-identical in shape to
today's for the same input; quantity rules unchanged (fractional/zero refused, nothing created on an error); RBAC
refusals; no ERPNext client constructed, no cache write; zero pricing tables in the central database.
W8 — report back in from-opencode with the routed example and the exported file, for live verification.
No change to the export file format; no purchase order in ERPNext; no automatic ordering or scheduling; no minimum
order value check (that is stage 4); no retention or purge (stage 5); no margin/reimbursement guardrail (waiting on
the Drug Tariff); no Counter Assistant surface; no dm+d data of any kind.
Deployed by Hermes: feature/pharmacy-price-ordering pushed, the app engine fast-forwarded dev to abd61de,
pharmacy.0019 and pharmacy_stock.0006 applied on the apex and the client tenant, both seeds re-run (the stale
and one-working-day demo items and the stale-only supplier now exist). Verifiers: the new
/root/work/veripath-ops/scripts/verify-pharmacy-price-ordering.py (24/24) plus the stage 2 verifier, which now
passes 24/24 as well because the demo data reaches every state, and stage 1's 35/35 as a regression.
What the live run proved:
DEMO-IBU-400, qty 32, unit_price = 1.0500, basis = PER_PACK, from = Synthetic Wholesaler Three (DEMO),ProductCode,Quantity then code,quantity, every line matching[A-Za-z0-9-]+,\d+, no price text anywhere, named for the supplier and the order reference.ORDER_ROUTED per created draft, carrying the counts and the value (1 line(s), 0 kept the proposal, 1 overridden, 0 with no usable price; value £33.60), with ORDER_CREATED and ORDER_EXPORTED as before.Tidying done during verification: the run created example orders on the demo tenant; the duplicate draft it left
was cancelled with a reason (never deleted) so the walk-through opens with one routed draft and one exported order
rather than a pile of test records. The audit rows for all of it remain.
Guides still deferred: C6 gains the routing step and the Business-Manager guide gains the routing summary — both
wait for the walk-through pages, per Matthew's 2026-09-22 decision.