Status: LIVE and verified (2026-09-22) — 49/49 on
test-client-pharmacy. Stages 1–3 are live; this stage's
W1–W3 are deployed and verified live (see the record at the foot) and W4 was answered in writing and
deliberately not built — its data question is answered below and the column waits on it. The "numbers" half of the
supply-ordering work is therefore complete; Matthew's walk-through of stages 1–4 is the next human step.
Parent plan: supply-orders-part3-pricing ·
previous stage: supply-orders-pricing-phase3-ordering.
Stage 3 answers "who do we buy it from?". Stage 4 answers the two commercial questions the pharmacy asks next:
what is this basket worth, and is it worth placing with this wholesaler? — plus, only if the data question can be
answered honestly, what margin does it leave against NHS reimbursement.
There is no reimbursement price per stock item in this system today, and the demo data hides that. What exists:
PharmacyERPNextConfig.reimbursement_price_list;DEMO-REIMB-AMOX-500 and DEMO-REIMB-IBU-400 while the stock items are DEMO-AMOX-500 / DEMO-IBU-400.So a margin column would, as things stand, be a number computed from a fabricated price against a real
purchase price — worse than no column. Three routes, in order of honesty:
W4 is therefore written as an investigation plus a connector, not as a feature, and it is the one thing this stage
does not promise. Everything else stands on data we already hold.
W1 — the order's value, everywhere it belongs (no new tables). The line snapshots from stage 3
(unit_price, price_basis, price_recorded_at, price_supplier) are the only source. Show the value on the
order list (one column), the order detail (a total plus per-line value already derivable), and keep the
per-supplier group values on the build screen. A line with no snapshot contributes nothing and the order says
"value not known" rather than a misleading total — every order created before stage 3 has no price, and
ORD-20260921-898C9C on the demo tenant is exactly that case. Never invent a value, never backfill one.
W2 — the minimum order value becomes real (advisory). Supplier.minimum_order_value is today documented in the
code as "display only — the app holds no purchase prices and does not check it". This stage makes it a live
advisory warning and changes that help text to match reality: on the build screen each supplier group shows
"£X of a £Y minimum — £Z short" when it falls short, and the order detail repeats it once. It never blocks, never
changes a quantity and never auto-adds lines — the pharmacy may knowingly place a short order and pay the delivery
charge, which is a commercial judgement, not a software one. Unknown minimum (blank field) → no warning, not a
zero-value one. Where a group's value is not known (W1's case), the warning says so instead of comparing a partial
figure to the minimum.
W3 — the copy and the guides. The supplier screen's field help, the build screen's note and the C6 guide all have
to state the same rule: the minimum is the wholesaler's contractual threshold, displayed for the pharmacist's
judgement, not enforced by the app. That wording is the deliverable as much as the warning is, because a warning
that reads like a rule changes behaviour wrongly.
W4 — the margin column: investigate, then connect (do not build a number yet). Deliverable of this work item is a
written answer to three questions, not a screen:
Only when 1–3 are answered does the margin column become a build item, and it is then its own small stage. The
verification for W4 is the answer, on this page, with its evidence.
W5 — tests and the boundary. Value totals from the snapshots (including the no-snapshot case reading "not known");
the minimum-value advisory firing, staying silent when the value is unknown, and never blocking or mutating a line;
the Counter Assistant refused every value surface (the value is commercial data, same gate as the prices — RP /
Technician / BM only); the export file still two columns with no value in it; and the standing boundary assertions —
no ERPNext client constructed, no position-cache write, nothing central.
No change to the export file; no purchase order in ERPNext; no margin column until W4 is answered; no
buy-above-reimbursement guardrail (needs the Drug Tariff); no automatic ordering or top-ups; no retention or purge
(stage 5); no Counter Assistant surface; no dm+d data.
Who the value and the minimum warning are for. My recommendation: the same three roles that already see prices
(Responsible Pharmacist, Dispensing Technician, Business Manager), never the Counter Assistant, and the minimum
warning on the build screen only (with a single repeat on the order detail) — not on the dashboard, not on the
reports, so it stays a decision aid at the moment of ordering rather than a background nag. Matthew's call; recorded as
a starting position if he does not amend it once he has walked through the earlier stages.
W4's deliverable was an answer, not a screen. The three questions, answered with their evidence:
1. What reimbursement price can we legitimately display per stock item? Today none, per item. The only
reimbursement figure in the system is computed per endorsed dispensing token by
pharmacy_accounting.services.compute_expected_reimbursement, which prices a claim from an ERPNext price list named by
PharmacyERPNextConfig.reimbursement_price_list (default "NHS Reimbursement") plus a configurable professional fee.
That list is seeded with synthetic demo values against separate ERPNext items (DEMO-REIMB-AMOX-500,
DEMO-REIMB-IBU-400 in pharmacy_accounting/demo.py) which are not linked to the stock items and whose synthetic
SNOMED codes do not match them. The system holds no NHS Drug Tariff and no price-concession feed; dm+d is
staged but unread, and its AMPP PRICE is a manufacturer list price, not a reimbursement rate. Route 1 now — the
pharmacy supplies its own per-item figures, held client-side per tenant, labelled the pharmacy's own figure, not
the NHS tariff; route 2 (license the Drug Tariff plus concessions) is the real fix. Never dm+d PRICE, never the
synthetic demo list, never central.
2. What links a bought pack to what was dispensed? Nothing does today. The stock item is the pack bought;
reimbursement is for what was dispensed. StockItem carries pip_code / barcode / pack_size; the
reimbursement list is keyed by a different ERPNext item; SnomedItemMapping links a prescribed SNOMED code to a stock
item but is not the reimbursement key. A margin needs an explicit reimbursement figure keyed to the StockItem
with a basis (per pack / per unit — the same PriceBasis stage 1 captured) and a source label, reconciled against
pack_size. Where the pack and the reimbursement unit differ the column must say not comparable, never silently
scale.
3. What the column says in the three states. No figure → "No reimbursement price" (never blank, never £0.00);
basis unstated or pack/unit mismatch → shown with its basis but marked not comparable (the stage 1/2 NOT_STATED
rule); comparable figure → the margin with its source and date and the same freshness treatment, and "not known"
whenever the purchase price is stale or unknown.
Conclusion: the column is not built, and building it now would be dishonest — it would compare a fabricated demo
price against a real purchase price. Its precondition is route 1 (a pharmacy-supplied per-item figure, client-side,
with its basis and source) or route 2 (a licensed tariff); when either exists it is a small stage of its own, reusing
order_value and price_view.
On test-client-pharmacy, synthetic files only: build a draft whose group value is below the supplier's seeded
minimum (the seeded synthetic supplier carries £150) and check the advisory appears and blocks nothing; build one
above it and check the warning is silent; open an order from before stage 3 and check the value reads "not known"
rather than £0.00; check the Counter Assistant is refused the value surfaces; re-run the stage 1, 2 and 3 verifiers as
regressions; and confirm the export file is untouched.
Deployed by Hermes. feature/pharmacy-order-value (commit 9722205) pushed, the app engine fast-forwarded dev
to it, pharmacy_stock.0007 applied on the apex (default and sector_pharmacy) and on the client tenant
(migrate_client --slug test-client-pharmacy --app-label pharmacy_stock), then the three containers restarted — the
repo is bind-mounted, so a compose up -d alone would have left the old code running. No new tables, no backfill.
The pharmacy suite re-run by me on the engine: 443 tests OK.
Verifiers. The new /root/work/veripath-ops/scripts/verify-pharmacy-order-value.py — live OIDC, no mocks —
49/49, its --reads-only mode run through the boundary wrapper (audit / files / rows / cache unchanged), and
the earlier verifiers as regressions: stage 1 35/35, stage 2 24/24, stage 3 24/24 (stages 2 and 3 re-run
after stage 1's import, so the price landscape the import leaves behind is covered too).
What the live run proved:
Synthetic Wholesaler (DEMO) (min £150) with an unpricedSynthetic Wholesaler Three (DEMO) at £35.00 with no minimumSynthetic Wholesaler Two (DEMO) at £25.28 → "£25.28 of a £150.00 minimum — £124.72 short.£0.00, never — — and£30.40 (partial — 1 line(s) without a price) on the detail and £30.40 (partial)Demo data gap found and closed by hand (asked of OpenCode — a seed change). The short advisory had no live
state until this run put a minimum on a second supplier: only Synthetic Wholesaler (DEMO) carries one (£150) and
its only line has no price, so the screen could only ever show unknown. The run set £150.00 on Synthetic Wholesaler Two (DEMO) through the supplier screen (the pharmacy's own field, reversible) so the state is reachable
for Matthew's walk-through; it belongs in seed_pharmacy_stock so a re-seed keeps it. The partial state is reachable
only through the view's re-grouping contract (two posted rows into one supplier) — today's build screen has one line
per group, so a re-seed with a two-line group would make it a screen state too.
Tidy (never delete). The run cancelled its own example drafts with reasons. The tenant now opens with one
exported valued order (ORD-20260922-37CA4E, £33.60), one short draft (ORD-20260922-4E0448, £25.28 at
Wholesaler Two), the pre-stage-3 pair (ORD-20260921-D123C9 draft "Value not known" and ORD-20260921-898C9C
exported) and the cancelled verification trail — one example per state rather than a pile of test records.
One checker fixed, not the app. Stage 1's verifier asserted "the word 'price' appears nowhere on the build
screen" — a boundary stage 2 deliberately reversed. It now asserts the durable thing (the build screen offers no
route to the price import). Same class as the stage 2 stale-price check last round, and recorded here rather than
quietly edited. Separately, stage 1's default --price-date 21/09/2026 collides with its own previous run, so its
"no supersede warning on a first import" check fails on a re-run: run it with a fresh date
(--price-date 22/09/2026).
Still open: W4's column (not built — correctly, see above); the C6 guide and the Business-Manager guide
still owe the routing/value wording, deferred to the post-walk-through pass per Matthew; and Matthew's walk-through
of stages 1–4 is the next human step.
CORRECTION, 2026-09-23 — read this before using anything below. This section was written from a research
note and does not match the release we actually hold. Verified against/srv/datasets/dmd/2026-09-22
(TRUD release 3170926, 8 files, 228 MB), the differences that matter are:
- Prices are not inside the
<AMPP>block. An AMPP block carries onlyAPPID,NM,VPPID,APID,
LEGAL_CATCDandSUBP. The prices sit in a sibling section,MEDICINAL_PRODUCT_PRICE→PRICE_INFO,
keyed by that same pack id, with<PRICE>,<PRICEDT>,<PRICE_PREV>and<PRICE_BASISCD>.- The basis element is
PRICE_BASISCD, notPRICE_BASIS, and its four values come from the lookup file's
PRICE_BASIStable: 0001 NHS indicative price, 0002 no price available, 0003 no price / centrally
funded, 0004 no price / priced when manufactured. Only basis 0001 carries a real price.- The filenames in this section are approximations. The real files are
f_ampp2_<release>.xml,f_amp2_…,
f_vmp2_…,f_vmpp2_…,f_vtm2_…,f_gtin2_…,f_ingredient2_…,f_lookup2_….- One file is missing from the list below entirely:
f_vmpp2— which is where the Drug Tariff block lives
(DRUG_TARIFF_INFO, 8,686 rows, 4,444 carrying a price, with the payment category from the lookup).Also worth knowing: only 92,294 of the 187,156 packs carry a real price (85,825 are "No Price Available"), so
a zero must never be read as a price — three rows claim an indicative price with a value of 0.The plan built on these files, and its §5.2, are the authority:
Central Reference Data (approved 2026-09-23). The
text below is kept as the record of what was written at the time, not as a build instruction.
To populate your ERPNext pricing ledger and run an accurate "buy-above-reimbursement" guardrail, you must target the core files inside the TRUD data distribution pack [NHS TRUD dm+d Group].
When you download and unzip the primary TRUD package (typically named nhsbsa_dmd_XXXXXX.zip), it will yield a set of individual XML data files representing different layers of the NHS Dictionary of Medicines and Devices (dm+d) schema.
The precise layout of which files you need to extract and how they tie into the pricing structure breaks down as follows:
f_ampp_v2.xmlThis is the single most critical file for your pricing engine. In the dm+d hierarchy, prices are strictly held at the AMPP (Actual Medicinal Product Pack) level, since pricing changes based on the manufacturer, brand, and exact quantity configuration (e.g., GlaxoSmithKline Amoxicillin 250mg capsules - pack of 21).
Within f_ampp_v2.xml, your ingestion parser needs to look for the <AMPP> block elements and map out these fields:
<APPID>: The unique SNOMED code identifying that specific pack configuration.<PRICE>: The baseline NHS trade price expressed entirely in pence (e.g., a value of 180 equals £1.80) [NHSBSA dm+d Glossary].<PRICE_BASIS>: A numeric lookup flag mapping to the reason behind that specific price layer (e.g., marking it as a Part VIIIA standard generic tariff or a custom manufacturer trade price list) [NHSBSA dm+d Glossary].If you only extract the AMPP pricing file, your system will just have a list of SNOMED codes and values. To display these prices against clean names inside your dispensary UI or invoice matching module, you must extract three supporting database components from the same zip file:
f_amp_v2.xml: Maps the specific pack (AMPP) back to the Actual Medicinal Product (AMP) descriptive details. This links the price to the real-world commercial brand/manufacturer name.f_vmp_v2.xml: Houses the Virtual Medicinal Product (VMP) master rows (e.g., Amoxicillin 250mg capsules generic baseline).f_lookup_v2.xml: Contains the lookup definitions for your variables. You need this file to translate the <PRICE_BASIS> digit code from the AMPP record into human-readable text strings within your ERPNext analytics.The standard dm+d XML files (f_ampp_v2.xml) track all licensed medicinal products beautifully [NHSBSA dm+d Glossary]. However, as noted in your VeriPath v1 scope list, your application also manages patient walk-in collections, dressings, and appliances.
The pricing for items listed under Part IX of the Drug Tariff (dressings, catheters, needles, hosiery, etc.) does not consistently live inside the main dm+d TRUD database bundle. To get those records completely for free, you must incorporate a secondary automated file processing loop:
Configure your backend file processor task to process the unzipped payloads using this clean blueprint logic:
# Minimal Python conceptual lookup map for your extraction script
TARGET_TRUD_FILES = {
"f_ampp_v2.xml": "Extracts structural Pack SNOMED IDs, Price (in pence), and Price Basis Codes.",
"f_amp_v2.xml": "Extracts real brand and manufacturer text mappings.",
"f_vmp_v2.xml": "Extracts generic structural molecule naming categories.",
"f_lookup_v2.xml": "Translates numeric price basis variables into readable strings."
}
By explicitly pulling these four files out of the weekly TRUD zip download and overlaying your monthly Part IX appliance CSV values, your ERPNext ledger will achieve absolute pricing coverage across every single item stocked on your pharmacy shelves.