Status: BUILT, DEPLOYED AND VERIFIED LIVE 2026-10-01 — see the record below the work items. The mockup below
was the design OpenCode built from, and the built screen matches it: my own drive of the deployed app passed
39/39 checks and the suite 645 OK. (It was APPROVED on 2026-10-01 with the four-offer cap, and this page
carries the record of the build and its verification.)** Matthew, on seeing the first
render: "that view is pretty much what I was visualizing — so yes let's go ahead with it", with one amendment:
the supplier offers are capped at four (§4), because a pharmacy may hold six or seven accounts and the row must
not become unreadable. The build brief was issued to OpenCode in
from-hermes and has since been built and verified. The two remaining questions are PARKED until he has seen the
live screen (§12) — "park these until I can see the view" — and both are built as the stated starting positions,
each reversible in one line.
Matthew's approach (2026-10-01): "build a view as close to the specification I am talking about and then make
further modifications from there — it's difficult to discuss this without this 'combined view'." So the layout was
rendered first, on dummy data, for him to change by looking at it — and it was changed that way.
Written from Stock Order Views — the research
question, in Matthew's words: "we need to design our UI's accordingly so that the business manager can see clearly
what is needed and how the relevant stock compares with the other suppliers the pharmacy uses" — and from the
state of the live app, which already holds almost all of the data and has none of the one view.Matthew's rulings this session (2026-10-01):
- "we can only use the data that we have" — a supplier's availability is whatever its imported file says,
and nothing more. So there is no "short supply" claim anywhere: a cell states what the file states, and a line
the file does not list is not in their file of , not unavailable.- "I want both — why do we have to choose one or the other?" — the NHS reimbursement column and the
margin column. The either/or was never a design choice; it was the residue of
phase 4's parked
margin question (its W4), which had no reimbursement figure per pack to subtract until phase 7 and the
Drug Tariff ingestion produced one. Both columns are in scope.- The requirement in one line, also his: "what we need is a clear view, or perhaps a collection of views that a
business manager can rely on to make decisions … the current views may hold this information, but there is no
one collective view and any business manager would find this frustrating."Parents: Supply Orders Part 3 (stages 1–4, live) ·
the figure: The Derived Pricing — Phase 7 (live) ·
the comparison: The Client Comparison — Phase 8 (live) ·
the ordering rules: Supply Orders Pricing — Phase 3,
Phase 4 and the
v1 ordering spec.Naming note. This page carries no phase number on purpose: the pricing family reached 1–4 and the
reference-data family 7–8, so a bare "9" would be ambiguous. It is a presentation stage over data that is already
live, and its path is its name.

A rendered design mockup on dummy data, not a screenshot of anything built. It is the app's own shell and styles
(the pharmacy base template, its own palette, cards and table rules) with the spec's layout applied, so what is being
argued about is the layout rather than the finish. It deliberately shows a pharmacy holding six supplier accounts
so the four-offer cap can be seen working. What to look at, in order:
One layout consequence the mockup makes obvious: the app's <main> is max-width: 880px
(pharmacy/templates/pharmacy/base.html), and this grid needs roughly 1560px to keep a row on one line. The
ordering screen therefore needs a wider container of its own — a class on main, not a new base template — and
that is stated as a work item below rather than left as a surprise on the first build.
The data exists. What does not exist is one place to stand while deciding. Today a business manager assembling
an evening order moves between, at least:
| Where | What it holds |
|---|---|
Stock dashboard (/stock/) |
counts, the low-stock list, dm+d link counts, last position refresh |
Low/zero stock report (/stock/low-stock/) |
the items at or below their alert level (guide C2) |
Build an order (/stock/orders/new/) |
the routed basket, per-supplier values, the minimum advisory, the per-row price columns and the reimbursement cell |
Price files (/stock/prices/, import, detail) |
which files we hold, for which supplier and date, and what they contained |
Suppliers (/stock/suppliers/, detail) |
who we may buy from, their minimum order value, their portal, their price-file history |
A stock item (/stock/items/<pk>/) |
one item's dm+d link, pack and derived reimbursement |
Price concessions (pharmacy_medicines) |
the month's concession lines |
Every one of those screens is a part of the decision and none of them is the decision. The build screen is
closest, and it still asks the manager to open a dropdown per row to compare suppliers, and to scroll to a group
card to see whether a basket clears a wholesaler's minimum.
What this work cannot fix, and must say so on the screen: we hold prices, not stock. VeriPath holds no
wholesaler availability feed of any kind, and any figure of that kind would be invented. The single availability
signal the platform legitimately holds is the supplier's latest imported file — the line is there at that price,
or it is not there, or the row was unreadable — and the screen says exactly that, with the file's date, and no more.
pharmacy_stock/price_view.py is the single price answer and is already shaped for this work: itsREPORT_COLUMNS (best price with supplier · spread · prices on file · freshness · NHS reimbursement) carry theNOT_STATED price, measuresStockSettings), distinguishes No supplier data from Invalid£0.00.figure ·services.reorder_suggestions() produces the "what is needed" rows from the cached positions only (no liveorder_up_to_level. The screen already prints the last position refresh.order_build.html already groups by proposed supplier, shows each group's value and the phase 4 minimumOrder.Status is DRAFT → EXPORTED → RECEIVED (or CANCELLED) and OrderLine.quantity exists, soDRAFT/EXPORTED — is computableStock (pharmacy/templates/pharmacy/base.html), and StockDashboardView already computesThe collective view is the existing build screen, not a new page — it is already the screen where the order is
assembled and it already owns the draft → export → receive lifecycle. /stock/orders/new/ becomes the decision
grid; the other screens keep their jobs and become reachable from it (§6).
Read left to right, it is the order a manager decides in:
Both are advisory, never blocking, and never a substitute for judgement: the app proposes, the pharmacist
decides, and nothing is exported automatically.
reimbursement − best usable purchase price, computed only when both sides are per pack. It is£0.00, blank orpharmacy_medicines/catalogue.py) — so the margin isThe per-row dropdown becomes four offer columns, holding the best four offers for that item, each cell
naming its own supplier and carrying:
PER_PACK / PER_UNIT), because a per-unit price beside a per-pack one is a trap;The cap is four, by Matthew's ruling of 2026-10-01: "we need to limit the number of suppliers to 3/4 … if one has
6 then the suppliers with the best 4 offers get to be displayed" — because a pharmacy may hold six or seven accounts
and a row that keeps growing sideways becomes unreadable. So:
+N more offers not shown, and every candidate stays reachable on the line's own supplier control — the existing select, which listsprice_view.py, beside the column set it governs, so(This also answers the research page's closing question about information density: the grid is dense by design, and
what keeps density from becoming ambiguity is the honesty rules — never a blank, never a £0.00, always a reason —
plus this cap.)
The per-group value card becomes a bar that is always visible at the foot of the workspace, one line per
supplier in the basket:
• Sigma — 12 lines — £25.28 of a £150.00 minimum — £124.72 short — [Create draft]
with its states exactly as phase 4 built them: clear · short (with the shortfall in money) · value not
known (so it is not checked) · no minimum set (no warning, not a zero-value one). It never blocks, never
changes a quantity and never adds a line. There is no cross-supplier total: it would be commercially
meaningless, and one is not built.
Above the grid, one compact strip of links — the collection — each carrying its own freshness line so nobody
has to go looking and nobody reads stale data as current:
| Link | Its freshness line |
|---|---|
| What we are low on (low/zero stock report, guide C2) | as at the last position refresh |
Price data we hold (/stock/prices/) |
N files · newest dated <date> |
Who we may buy from (/stock/suppliers/) |
N suppliers · M without a price file |
This month's NHS concessions (pharmacy_medicines) |
month and publication date |
The item in full (/stock/items/<pk>/) |
from each row's item link |
and one standing sentence, which is Matthew's ruling made visible: "Supplier availability is whatever their
latest price file says — VeriPath holds no wholesaler stock feed."
StockDashboardView already computes the low-stock list and the dm+d link counts, so the dashboard's "Low / zero
stock" card becomes the entry: the count of items needing a decision, the last refresh, and one link —
"Open the ordering view". A pointer, not a second grid: two grids showing the same rows is how screens drift
apart. The dashboard keeps its other cards unchanged.
price_view: the parked column set becomes the best four offers for the item+N more offers not shown count. The reimbursement cell and thereimbursement − best usable per-pack price, with the states in §3.3 and the tooltip that names the rule and the SAF exclusion. No new stored columnDRAFT/EXPORTED, computed in the same querymain is max-width: 880px in pharmacy/templates/pharmacy/base.html; the gridmain, as the mockup shows) ratherOrder/OrderLine beyond the existing snapshots. The snapshots stay asPharmacyStockManageRequiredMixin), exactly as the buildprice_view and the position cache alreadyprice_view change rather than a template loop.Per-item: at most four offers are shown, ranked per item and usable-first, with a supplier outside the four
reachable on the line's supplier control and counted in +N more offers not shown; a row with two offers shows two
and never a blank fifth cell; cheapest usable price marks the best offer; a NOT_STATED price is never comparable and never wins; a
stale price is shown, aged and never silently taken; not in their file of appears for a supplier with a
file that lacks the item, and no cell ever reads "short supply"; the margin appears only where both sides are
per pack, its negative case is shown and marked advisory, and each of the §3.3 states renders its sentence; an
unlinked item suppresses every financial cell and states its reason (the existing phase 8 rule); the on-order figure
counts DRAFT + EXPORTED lines and not RECEIVED or CANCELLED ones; the basket bar's four states including
value not known; the Counter Assistant refused every surface; the export file unchanged — two columns, no
price, no margin; and the boundary assertions above.
Built by OpenCode in pharmacy_booking_app f05fc3f (the view) and 44d8595 (the satellite line and the rig
README), branch feature/wda-supplier-lookup, deployed on VPS2 — read back from the running container: MAX_OFFERS = 4 in price_view.py, main.wide in the base template, the best four offers header in order_build.html, and
the migrations ending at 0014 (so no migration, as the brief required). No new stored column. Verified by
Hermes, not from the handback:
test-client-pharmacy, driven through the real OIDC login by a new committed verifierveripath-ops 7e2be34, scripts/verify-pharmacy-ordering-views-live.py; its parsing sits in three helpers — order_rows, order_lines, grid_rows — checked against the templates' own markup before the drive): 39 checks, 39 passed, none failed.£0.00 anywhere on the page; the basket bar with two supplier lines, its own create-draft action each and theDraft and Exported orders' lines and compared per item, every one of the five rows matched (Ibuprofen 400mgReceived/Cancelled is stated as by construction and by the module's own unit testsmargin_cell() derivesreimbursement − best usable per-pack price and refuses to compare a per-unit price with a per-pack reimbursement;_best_usable() prefers a per-pack price, so a cheap unit price can never win a row; on_order_quantities() countsDRAFT + EXPORTED in one aggregate query.scripts/run_pharmacy_tests.sh against this commit — the count the handback claimed, reproduced on this host rather than taken from the note.One thing the demo tenant cannot show, and it is the seed rather than the code. The tenant holds 7 suppliers, 3
of them without a price file, so no item has a fifth offer and the +N more offers not shown count is not
exercised live — the rows fill their four cells with real offers and not in their file cells instead. The cap
itself is live (four cells, never five), the counter is proved by the deterministic six-supplier test, and the
sentence appears in the code path that would render it. If the live +N is wanted — for the guide figures as much
as for the proof — the demo seed needs two more priced suppliers so one row carries five offers; that is a seed
change, not a build change, and it is Matthew's to ask for.
PARKED (Matthew, 2026-10-01): "I can't really answer those questions now until I see the view … so I'll say park
these until I can see the view." Nothing waits on either: both are built as the starting positions recorded below,
each reversible in one line, and both are re-asked once the screen is live — because the first of them cannot be
answered from a picture at all, only from using the thing.
Answered, and no longer open: the supplier cap is four (§4) — his ruling, the same day.