Skip to content

design

Persona surface for the Supply Chain & Parts, Vehicle Operations and Production — Product Specification. Web flow, canonical Atlas shell (1440).

The Procurement Manager creates and manages orders, owns vendor relationships, and approves requisitions. Today every order is entered twice — once in Atlas, once in Polaris — by hand, and orders can’t be edited, can’t take a ₦0 cost, can’t attach an invoice, and are limited to air/sea shipping.

  • Manual order duplication between Atlas and Polaris; stock figures that don’t tally.
  • No single point of entry; requisitions arrive by email with no team context and no reason attached.
  • Cross-team demand for the same item is invisible until it becomes a conflict (the 10-rolls-of-tape case).
  • Orders are frozen once created; no amendment trail; vendor contacts require both phone and email; lead time is tracked nowhere.

Sidebar, seven groups in this order: Overview (Dashboard) · Purchasing (Requests, Orders, Requisitions) · Receiving (Shipments, My Inventory) · Logistics (Delivery notes, Vendors, Logistics contractors) · Exceptions (Quarantine, Variance cases, Vendor returns) · Costing (Item pricing, Bill of materials) · Reports (Insights). Shipments carries a small grey (inbound) hint and Delivery notes an (outbound) hint on the same line as the label (.sb-hint, 11.5px, weight 400, --text-3); the hint sits inside .sb-label, so it hides with the label when the sidebar collapses, and the feedback form reads the label without it. Dashboard is the landing view (the procurement Inventory page is merged into it). Breadcrumbs on every drill-down. Only Requests carries a sidebar count badge, and it counts requests at Requested (renderDashCounts); it disappears at zero. The word “waybill” is not a module name anywhere in this console. The former Planning group (Bill of Materials, Procurement Orders) was removed as out of scope for this persona.

Requests vs requisitions — the inbound demand from Production/Retrofit is a Request (the inventory-side term) and has its own sidebar entry; it becomes a Requisition (the Polaris term) only once approved and synced to Polaris. “Requisition” is therefore reserved for Polaris-side surfaces (the sync mapping, the Polaris link).

One list per status set in the flow’s JS, read by every badge, filter option and status tile: REQ_STAGES, ORDER_STATUSES (which also builds STATUS_BADGE), PREQ_STATUSES, SHIP_STATUSES, URGENCIES, and TEAMS for the requester’s team. Status tiles (TILE_REG, renderTiles) sit above each register: a tile counts the rows passing the page’s other filters, clicking it sets the register’s status filter, clicking it again clears it, and All comes first and clears it. Field rows that pair a value with a qualifier or an action (a badge and its kind, a reference and View) keep an 8px gap between them; the vendor form’s Channel / Detail pair takes the full row width so a whole email address shows. Below 760px the sidebar collapses to its icon rail, tiles and two-up grids stack, and nothing scrolls sideways.

Slug What it does
dashboard KPIs, all counted live from the seeded registers by renderDashCounts(): Open requests (requests at Requested only · “Awaiting Procurement”), Urgent requests (Requested and marked High or Urgent · “Marked high or urgent”), Orders in transit (orders at Shipped · “A shipment is in transit”), Avg vendor lead time (one decimal, e.g. 7.0 d · “Ordered to delivered, per order”: date ordered to the delivered date of the order’s shipment, averaged over delivered orders). Action: Review requests. Order lifecycle strip, each stage counting the register it opens with a second muted N in total line: Requests (Requested only → Requests) · Requisitions (Pending approval → Requisitions) · Ordered (orders at Ordered → Orders) · Shipped (shipments In transit → Shipments) · Delivered (delivery notes In transit or Partially delivered → Delivery notes) · Used (serialized units consumed · 7d; .pipe-cell.is-static, not clickable, no hover lift, no total). Hosts a read-only Stock snapshot table that mirrors the Inventory items table (Item · Category · Location · Tracking · UOM · On-hand · ATP · Faulty · Date counted · Status) with category, warehouse/location and status filters + search in the .tbl-card-head (search left, filters right) and Export — the Location column names which warehouse holds the stock (no central warehouse) and its #stock-loc filter scopes the table. Rows do not navigate.
notifications Notification center reached from the topbar bell (no sidebar item, and no duplicate dashboard button — the bell is the single way in). .pstat KPIs + a #notif-seg category .filt-select over the canonical .ntf-row list (icon chip · title · sub · action · time · unread dot), grouped under .ntf-sec day headings — never .dw-row, and no longer the #notif-tbl table it was before the sweep. Filtering hides a day heading whose rows have all gone. Categories: Threshold (auto-raised from Inventory’s low/critical thresholds, in-app + email), Request (new inbound), Requisition — only requisition approvals (pending/shipment notifications were removed since those statuses don’t change automatically here); requisition rows action Open in Polaris (quicklink), not the sync console. Mark-all-read clears the bell dot.
requisitions (view slug) → Requests Inbound queue from Production/Retrofit (requests are Inventory-owned — Procurement does not raise them). Status tiles above the queue (#tiles-req): All (“Every request”) · Requested (“With Procurement”, amber) · Ordered (“With the vendor”, blue) · Delivery note issued (“With the carrier”, blue) · Received (“With Inventory”, green). Columns: Request ID · Item · Requested by · Location · Qty · Urgency · Requested · Request status — the unit of measure Inventory sends with the request rides beside the quantity rather than taking its own column (“10 rolls”, “600 m”, via plUnit), and does the same on the request detail and on every order line raised from it, so a buyer orders in the right unit — the Location comes from the requester’s profile (shown in the row-detail dialog too), and Requested by shows the actual floor requester’s name with their team as a small badge beneath it, rendered from TEAMS (core’s TeamGroup: Production facility · Retrofit station · Telematics · Maintenance · Inventory · Delivery · Unclassified). For a transfer the Inventory Manager escalated (seeded REQ-9110, data-escalated), the requester and team are the floor person behind it, and the request detail’s Source line names the escalation (Reason moved into the row-detail dialog; the all-teams filter and inline Action column were removed). Export to CSV. Request lifecycle: Requested → Ordered → Delivery note issued → Received. The one action is Accept & create order (Requested rows, single via detail or bulk) — opens the order form prefilled with a line per request, each tied to its Request ID, advancing them to Ordered. Delivery notes are no longer raised from here — they auto-issue when the vendor shipment is delivered (see below). Requests arrive already reconciled by Inventory, so there is no Procurement-side conflict step.
waybills (view slug) → Delivery notes Delivery notes page (Purchasing group). A delivery note is procurement → the requesting locations’ warehouses (distinct from a shipment, which is vendor → procurement). There is no central warehouse — a request is tied to a location and its inventory lives in that location’s warehouse (Sango request → Sango warehouse, Ikeja request → Ikeja warehouse); “warehouse” is the Inventory side. It is auto-generated when a vendor shipment is marked Delivered — auto-numbered DN-####, tied to that order’s requests — and Inventory is notified in-app and by email; the tied requests flip to Delivery note issued. No manual creation. List columns: Delivery note · Requests · Items · Destination · Vehicle · Date created · Status; multi-value cells show first value + “+N” (full list in the detail — keep table rows short, no long text/reasons in tables). State .filt-select (Pending dispatch / In transit / Partially delivered / Delivered) + search + Export. The destination comes from the tied requests — each request’s location resolves to that location’s warehouse (locToWarehouse). The detail is arranged as per-warehouse leg cards (renderWbLegs): a compact summary card up top (Source order · Vendor freight ref [from the source shipment’s data-ref] · Requests · Date created · Date received · Status), then one card per destination warehouse — each holds that warehouse’s own vehicle (Logistics · Vehicle type · Vehicle plate no. — a note can use a different vehicle per warehouse; editable only while pending), its item lines (Item · Request · Qty requested [with UOM] · Qty purchased [what’s sent] · per-item Status), and a leg status badge. Neither quantity is editable. Confirm action sits in-content (right-aligned, not the page header). On Confirm split (confirmSplitDispatch) the auto-created note splits into one list row per destination warehouse (leg key DN-####-<WH>) and the combined pending row is replaced. A warehouse leg dispatches (In transit) only if a vehicle was assigned to it — goods for one warehouse may not be ready when another’s are, so a leg left without a vehicle splits out as Pending dispatch and is shipped later from its own leg (open it → Mark in transit, markLegInTransit, assign the vehicle). Dispatching a leg notifies the Inventory Manager in-app + email. Statuses: Pending dispatch → In transit → Partially delivered → Delivered — a leg reads Partially delivered when some (not all) of its items are accepted, Delivered when all are; accepted items carry a received date. Acceptance is Inventory-side — Procurement has no accept control (no button); the Partially-delivered / Delivered states are seeded/represented here (live changes happen on the Inventory side).
orders Orders workspace. Status tiles above the table (#tiles-ord): All (“Every order”) · Draft (“Not sent”, purple) · Requisition pending (“Sent to Polaris”, amber) · Requisition approved (“Ready to ship”, green) · Ordered (no sub, blue); the filter dialog’s Status select (#ord-f-status) holds every order status from ORDER_STATUSES. An order can start either way: from the Requests queue via Accept & create order (single or bulk), which prefills a line per request; the workspace header carries Export / Shipments / Requisitions only. Multi-line orders: one vendor per order, one or many item lines (add/remove lines), each line tied to a Request ID (prefilled when created from requests). Item picker searches the current catalog (no free-text products), per-line UOM/qty/unit cost (numeric-only, zero cost allowed), the field carrying the last unit cost paid for that item beneath it. One vendor and one currency per order (NGN/USD/CAD/AED); the form shows per-line totals and a single order total. The list carries a Currency column and a currency .filt-select (#ord-f-ccy, All / NGN / USD / CAD / AED) alongside the stage/vendor/method/urgency filters, and every amount is rendered in its row’s own symbol. The order detail states the order Currency, and its line items carry the unit beside the quantity. Multi-file invoice/image upload, shipment Air/Sea/Road. Each order carries a short Polaris description auto-generated from its line items (10× Buck converter, 5× AC charger), shown in the order detail and the send-to-Polaris summary. A line priced at zero (it came with another line, or free of charge) stays on the order to be received and is left out of the description; an order where every line is zero lists them all. Export to CSV. One unified Status column (Stage + Requisition status merged; no Acknowledgement column): Draft → Requisition pending → (Requisition rejected) / Requisition approved → Ordered → Shipped → Partially received → Delivered. A rejected requisition (with reason + notification) becomes editable and can be re-sent to Polaris. The order detail carries an audit trail (actor · time · action) across create/edit/send/sync/approve/reject/ship/deliver. Send to Polaris: only drafts are selectable; both single (from the order) and bulk go through a summary confirmation card that shows each order’s requester and inventory location (derived from the tied request, resolved to that location’s warehouse) so they travel to Polaris on the requisition, then each order raises its own requisition (never summed) and moves to Requisition pending. Vendor selection is smart — the order form auto-selects the item’s preferred-source vendor, still changeable. Draft edits persist and edit the same row in place (never spawn a duplicate row for the same PO). The form shows a per-line total and a prominent order total; the invoice uploader takes multiple documents and images (no delivered-goods photo). Sync to Polaris button on a sent order that isn’t confirmed in Polaris: idempotent — it reuses the order’s one requisition ref (PLR-REQ-####), never creating a duplicate; a further sync pulls the approval (which happens on the Polaris side) and fires email + in-app notifications. Editing is allowed only while the requisition is not yet approved. Whole row opens the detail, which shows Requester + Location (the values sent to Polaris) and lists the line items (item, qty, unit price, line total, request tie); a draft created from several requests shows a compact “Draft · from REQ-xxxx +N” sub (never the full list). Discarding a draft deletes its row and releases its tied requests back to Requested. All editable date fields are native calendar pickers. No change-history/amend flow. Once approved, Create shipment stamps the date ordered, moves the order to Ordered, and the rest of the lifecycle follows the shipment status.
bom (view slug) · Bill of materials What one rider takes of each item, and what that costs. The cost per rider is in the table’s foot, beside the row count, because every figure a summary card held was a roll-up of the column a few rows below it. The foot adds up the lines actually on screen, so narrowing to one business unit totals that unit and leaving them all totals a whole rider’s build, and it names what it is missing and what of it is not a purchase (“1 line quoted · 3 lines not priced”). The shipping-method picker sits with the other two selects in the card head and reads the rate card on Item pricing; its per-rider share lands in its own currency, never spread over the goods’, and the foot says it is in there. Lines: Business unit · Item (category, and the other units that run it, beneath) · Tracking · Needed/rider · Unit cost (the item’s last, with its date) · Cost per rider (needed x unit cost) · Note. Per currency, never across it · the parts are bought in naira and in dollars and core holds no rate with an as-of date, so a unit that buys in both states both. An item nobody has bought has no cost, so its line offers Add a cost and the total does not carry it. The total says beneath itself what it is missing and what of it is not a purchase (“1 line quoted · 1 line not priced”) rather than reading as the settled cost. The note is the one prose cell in the console and the only one that wraps (.tbl td.bom-note): nowrap is right for every other column and sets a 500px column for a sentence. The trailing action column was removed · it repeated the row’s own click.
pricing (view slug) · Item pricing What every catalog item has cost, buy by buy (Purchasing group). A price is what an order committed to, so the register’s drafts, its pending requisitions and the one Polaris rejected set no price and are absent; only an order that reached a vendor does, which is the line core draws when it counts purchase orders. History older than the order register’s window lives in PRICE_BOOK and is read through the same orderCosting, so a landed figure here and a landed figure on the order detail cannot disagree. Tiles partition the catalog: Priced items (bought at least once, its sub naming the unchanged, the not-comparable and the first buys so the four figures close) · Price rose · Price fell · No price history; each tile sets the movement filter it counts and carries the .pstat.on ring while it is the filter. Two tabs (.pseg): Items and Shipping. Items columns: Item (category and buy count beneath) · Unit cost (what the vendor charged, with its date) · Cost per rider (what one rider takes of it, times that cost) · Change (carrying the figure it was struck against, and when). The vendor belongs to the item’s own record rather than the register: a name and a range in two columns were a worse version of the By vendor card the record already carries. The previous cost sits under the Change badge rather than in its own column, because a badge reading “Up 13.6%” and a column reading the figure it is 13.6% above are one fact. No actual unit cost: the logistics a line carries is apportioned by its share of that order’s value, so what a unit landed at depends on what else was bought alongside it and is a fact about the order rather than about the item. The order detail states it per line, beside the logistics share it came from. Search over item, category and vendor; the filter dialog carries category, vendor, currency, price movement and a last-bought date range, every option built from the rows themselves. An item nobody has bought can be given a cost. Its Unit cost cell carries an Add a cost link (and the record an Add / Edit button) opening dlg-item-cost: unit cost, currency, who quoted it, a note. It is labelled Quoted in every cell it reaches, its Change badge reads Quoted rather than No price history, it never counts as a buy, and the first purchase order supersedes it. Export to CSV. Row opens the item’s price record.

Shipping tab: the rate card freight is costed from. A rate names its load (20ft container, 10t truck, air pallet), what that load costs, and the rider kits it carries; Cost per rider is the division between the two, printed rather than asserted. Freight is quoted on a load and not on a rider, so a rate that stated a per-rider figure directly would be hiding the arithmetic, and the two figures side by side are what show the bigger load coming out cheaper per rider. The kit count is optional: a haulier quotes a load and a price, and how many riders’ worth fit is worked out later, so a rate can be on file before anyone has counted it. Such a rate shows Not stated and the bill of materials lists it disabled rather than offering a choice that would add nothing. Add and edit through dlg-ship-rate, which previews the division as it is typed; method filter and Export on the card. The rates live here rather than in Settings because freight is a price. Neither card head carries a subtitle: a card headed Cost per rider over a column headed Cost per rider does not also need a line saying where the figure came from, and the rates table’s own columns already say what a rate is. The sort-order subs on the item record stay, because “Newest first” and “Cheapest first” are facts about the rows rather than commentary on the page. | | price-detail (view slug) · Item price | One item’s whole price record. Back returns to where it was opened from and names it: Item pricing, or Insights when a row on Items bought or Spend by item opened it. A location select (the warehouses this item was bought for) and a period select in the page head (This month · Last 3 months · Last 6 months · This year · All time, plus Custom range when Insights handed over its own dates) narrows what was bought; opened from Item pricing it reads All time, opened from Insights it carries that tab’s period. Tiles: Qty bought (in the item’s unit, “Across N orders”) · Spent (the lead currency, sub the average paid per unit, or the other currencies when there are several; a period of only zero-priced lines reads No charge) · Unit cost (vendor and date) · Lowest unit cost (the best it has been bought at, with who and when). The unit cost and lowest are the item’s own and do not follow the period. Bought per month draws one bar a month across the period, zero where nothing came in, each month hovering its quantity, orders and spend. Purchase history lists every buy in the period newest first, with a Total row for the quantity · Order (marked where the line was priced as a lump) · Date · Vendor · Qty · Unit cost · Actual unit cost (with the logistics share that produced it beneath, or a line saying the order carried none) · Against the buy before. The trend was its own card and was the same rows drawn again: Date, Order and Unit cost are all on the history already. What it drew was no use either · three buys at 71, 66 and 60 scaled from zero are three nearly identical full bars under a heading promising movement · so the column states the movement instead, in the words the register’s Change badge uses. The oldest buy reads First buy; a buy in a different currency from the one before it cannot be compared and says so. By vendor lists every vendor it was bought from in the period with the currency, buys, Qty bought, last, lowest and highest unit cost and last bought, most bought first; a zero-priced line adds to the quantity and not to the prices. The record is three cards: the chart, the history and By vendor. | | polaris-requisitions (view slug) → Requisitions | Log of the requisitions raised in Polaris — goods requisitions from this persona’s orders, and logistics requisitions raised here on a delivery note leg. Status tiles (#tiles-preq): All (“Every requisition”) · Pending approval (amber) · Approved (green) · Rejected (red), no subs. The first column after the ref is Raised against: the PO or the delivery note, with the kind (Goods / Logistics) on its own line beneath the reference, so the list holds both without a tenth column. A Goods and logistics filter sits beside status and currency. Approvals still happen in Polaris (Purchasing group, its own sidebar item; the dead “Polaris Requisitions” quick-link on Orders now routes here). No manager action on this page — approvals happen in Polaris. Columns: Requisition (PLR-REQ-####) · Raised against · Vendor (the destination warehouse on its own line beneath) · Requester · Amount · Raised · Status (Pending approval / Approved / Rejected). Search + status .filt-select + a currency .filt-select (#preq-ccy) in the card head, + Export. Each requisition carries the requester and the inventory location it serves (the warehouse at the request’s location) — these travel from the tied request when the order is sent to Polaris (see Orders). Rows are live-driven (upsertRequisitionRow, keyed by order): sending an order appends its row as Pending approval with an Awaiting ref placeholder; syncing fills the real PLR-REQ-####; pulling the approval flips it to Approved; a resend returns it to Pending approval (upsert in place, never duplicated). A page-level Open in Polaris control shows a toast (no external navigation). | | proc-inventory (view slug) → My Inventory | Procurement’s own custody stock (Receiving group) — goods that have landed but are not yet accepted by the warehouses. Possession = received − accepted: a unit enters here when its vendor shipment is marked Delivered and leaves when the destination warehouse accepts the delivery note (so in-transit-to-warehouse units still count as in custody). .pstat KPIs: In custody · Awaiting dispatch · In transit · Quarantined. Table (item-level aggregate): Item · Category · Source (PO · vendor) · Destination warehouse · Awaiting dispatch · In transit · Quarantined · In custody — with search + a warehouse .filt-select + Export. No cost/value shown (consistent with the warehouse surfaces). Wired to the lifecycle (custodyAdjust): marking a vendor shipment Delivered adds its goods here as awaiting dispatch; dispatching a delivery note moves those units awaiting → in transit; Inventory accepting the note clears them. KPIs recompute live on load and on every change. Row → dlg-custody shows the full item detail (Item · Category · Source · Vendor · Destination · In custody · Awaiting dispatch · In transit · Quarantined). No Create-delivery-note action (delivery notes are raised on shipment delivery). The one action is Flag quarantine (dlg-custody-qtn: quantity + required reason) which creates a real Awaiting release record on the Quarantine page and moves the units to Quarantined. | | shipments | Vendor → procurement inbound tracking. Status tiles (#tiles-shp): All (“Every shipment”) · In transit (blue) · Partially received (amber) · Delivered (green), no subs. Shipments are created from an approved order (Create shipment in the order detail). The Action column was removed — the whole row opens a shipment detail dialog that also surfaces the underlying order items + value, where you add carrier / transport details (mode + method-aware Vehicle/Vessel/Flight and Waybill/AWB/B-L reference, carrier, driver/contact) and change delivery status (In transit → Partially received → Delivered, stamping the delivery date) with an optional units-received capture — Partially received keeps the shortfall outstanding on the order — which mirrors back onto the linked order’s Status. The shipment detail lays its Shipment + Carrier cards side by side (responsive grid), then the Delivery status card full-width, with a single Update delivery status action at the bottom (not the page header). It shows the tied Requests (full list — detail only, no table column) and the tied Delivery note with a View link (demo continuity: shipment → the note it raised). Receiving is per item and incremental. Choosing Partially received (or Delivered) reveals a receipt table — Item · Destination · Requested · Received so far · Receive now (input, capped at the outstanding). Saving records what arrived now, raises a delivery note for just those items (split per warehouse, added to custody), and marks the shipment Partially received — or Delivered once every item is fully received. Delivered receives all outstanding in one go. Re-opening a partially-received shipment shows the running “received so far” and lets you receive more, raising the next delivery note (SHP_RECV tracks cumulative; raiseDeliveryNote is shared by full + partial). So one shipment can tie to several delivery notes (shown on the detail). One success message per receipt. A shipment can carry data-req directly (when its order isn’t in the table) — e.g. seeded SHP-4473 (requests to Ibadan/Abuja/Sango) for testing the split. | | vendors | Supplier lifecycle. Directory columns: Vendor · Contact channels · Avg lead time · Onboarding docs (uploaded count / Missing) · Preferred for (one chip + “+N”) · Date last ordered. No vendor Status column and no Scorecard column (scorecard lives in the detail only). Search filter + Export to CSV. One dialog for create and edit (full parity): vendor name, location, address, country, multiple contact channels (add/remove rows, at least one must be populated), numeric-only avg lead time (days), onboarding-document upload, preferred-item chips, and lead-time history (edit only). Scorecard is computed live, not entered: on-time delivery = deliveries within the promised lead time (from lead-time history), defect rate = the vendor’s returned units over units received; both recompute when the vendor detail opens. The order form flags a selected vendor with no onboarding documents on file. Qualification no longer carries a category. | | vendor-returns | RMA tracking. .pstat (awaiting pickup · picked up · credit received · replacement shipped). #vr-tbl: RMA · Item · Vendor · Qty · Source · Reason · Return status · Credit/replacement · Currency · Date raised. The card head carries a wired RMA search (#vr-search) plus the state and currency .filt-selects, all combined in applyVrFilters(). Fed automatically from Quarantine → Return to vendor, and a New vendor return button raises an individual RMA manually (auto reference via nextRma(), opens Awaiting pickup); row → Advance status + record credit-note/replacement ref. | | quarantine | Damaged/faulty units held for resolution. Release options are Return to vendor or Scrap only — Accept-into-stock was removed (accepting into the warehouse is Inventory’s call, not Procurement’s). Return-to-vendor raises a tracked RMA in Vendor returns. | | insights | Insights (Reports group). Page head “Insights”. Tabs Requests | Items bought | Spend | Cycle time (.pseg) share one row with the active tab’s controls, right-aligned (.filt-end): the Requests tab carries Filter + Export; Items bought carries a period select (This month · Last 3 months · Last 6 months, the default · This year · All time · Custom range) + Filter + Export; Spend and Cycle time carry the note No currency or date filter + Export. Requests tab, computed from the Requests queue rows (renderInsRequests): tiles Requests (“Raised in this scope”) · Most requested item (item name; sub “N requests · X% of all”) · Warehouses (“Receiving these requests”); a Requests by team table (Team · Requests · Share bar), then a two-up grid of Most requested items (Item · Requests · Teams, e.g. “Production facility 1 · Retrofit station 1” · Quantity with its unit) and Requests by warehouse (Warehouse · Requests · Share bar). Each table has its own search; the filter dialog scopes all three by request stage, urgency, raised from and raised to; Export writes one CSV with a Breakdown column. Items bought tab, every order line in the period per item: tiles Items bought (“Across N orders”) · Most often bought (the item on the most orders; sub “N orders · quantity”) · Bought at no charge (items on a line priced at zero, named in the sub). Table Items bought (Item · Category · Qty bought in the item’s unit, with “N at no charge” beneath where some came free · Orders · Last bought · Spent, one figure per currency or No charge), most bought first, with its own search; the filter dialog narrows by category, location (the warehouse the goods were bought for: a line raised from a request goes to that request’s warehouse, an ad-hoc line to the order’s destination), vendor, currency and the bought-from and bought-to dates, and typing dates sets the period to the preset they match or Custom range. A row opens the item’s record on the same period and location. Spend tab: three tiles (Committed spend · Purchase orders, counted from the Orders table · Spend on logistics), Spend by category and Spend by vendor side by side, then Spend on logistics by haulier, then Spend by item (Item · Category · Spent · Share of that currency’s spend · Qty bought · Orders), one row per item per currency, the biggest currency first and the most spent first inside it, with its own search, location select, currency select and period select (default Last 6 months) in the card head and Export; zero-priced lines spent nothing and are not on it. A row opens the item’s record on that period and location. Cycle time tab: the Cycle time by stage table, its subtitle carrying the request → order average (“Median days per stage · request → order averages 2.4 d”). No Vendors tab and no currency switch. | | sync | Atlas·Polaris mapping. Each order syncs as its own Polaris requisition — amounts are never merged into a single cross-order total. Sync log, discrepancy review queue. |

Deep-linkable via ?scenario=<slug>.

Getting the goods to a warehouse costs money, and that cost is not on the purchase order for the goods — it belongs to the leg that carried them. So the Procurement Manager raises it from the delivery note leg it is a cost of: one requisition per destination, because a note going to three warehouses is three hauliers, three vehicles and three prices.

  • The action sits on the leg card in the delivery note detail, beside that leg’s vehicle and its item lines. A leg that already has one shows the requisition instead — ref, amount, haulier and approval status — and does not offer the action again.
  • The dialog opens with the haulier already filled from whoever is carrying that leg (the saved dispatch record, or what is typed into the leg while it is still pending), so the amount is the only figure that has to be entered. It carries the delivery note, the destination and the goods on the leg as context, and takes what the cost covers, the currency and a note.
  • Raising one puts it in the same Polaris queue as a goods requisition, at Pending approval, with the haulier as the vendor being paid, Procurement as the requester and the destination warehouse as the location.
  • The leg is handed to the handler by index (_legKeys), never as a quoted attribute — a warehouse name is free text and would break the onclick it was interpolated into.
  • The action lives in the leg’s header row, beside that leg’s status badge. A button dropped between the vehicle fields and the item table sat flush against the table and read as a section of its own; the head already carries that row’s controls and its spacing.
  • A logistics requisition is a requisition, so it is raised with the details one normally carries: a description, the requester and an urgency, alongside the haulier being paid and the amount. The description composes itself from the leg — cost basis, destination, delivery note and line count — and follows the cost basis until the manager types over it, after which it is left alone. It reads on the leg, on the requisition list under the ref, and travels with the record.
  • A goods requisition describes itself the same way, from the lines of the order it was raised against, falling back to the requests that order was raised from. A requisition whose order is not on this console shows no description rather than an invented one.
  • Freight reaches Insights (renderLogisticsSpend, Spend tab): a Spend on logistics tile and a Spend on logistics by haulier table, both computed from the requisitions raised rather than seeded, so raising one moves the figure. Goods spend and logistics spend stay separate lines: a purchase order and a haulage bill are not the same commitment.
  • The Currency column came off the requisitions table — the amount already carries its symbol, so the column repeated it and pushed the table to nine columns. The currency filter reads the row’s own data, so it still bites.
  • Two tables here declare data-canon="allow-static-rows", because neither a haulier nor a vendor has a record to open: Spend on logistics by haulier and vendors quoted-vs-actual.

Every persona console carries the same Send feedback action: a topbar .icon-action (message icon) opens the centered #dlg-feedback dialog — Type (Bug / Improvement) · Impact (Blocking / Slows me down / Minor) · Where (the persona’s own pages, preselected to the page it was raised from) · Details · optional Screenshot. sendFeedback() validates the four required fields and confirms with a toast. Submissions land on the Product Manager · Tricycle Hardware console’s Tech feedback queue, which triages them by type, impact and area.

FR-R1 (request → order conversion), FR-R3 (Atlas↔Polaris sync — now per-order, one requisition per order), FR-R4 (zero cost), FR-R5 (edit-before-send + amendment/version trail), FR-R6 (attachments), FR-R8 (urgency + approver notification), FR-W1 (flexible vendor contacts), FR-W2 (air/sea/road), FR-W3 (lead time), FR-W6 (request reason visible), FR-X1 (auto team tag), FR-X2 (conflict detect + split reason), FR-D1/D2/D5/D6 (dashboard, filters + dates, KPI cards, sidebar). Item entry is now catalog-picker only (superseding the old free-text ad-hoc entry) and orders carry a currency (NGN/USD/CAD/AED).

Out of scope for this persona (removed): BOM library + BOM-driven Procurement Orders / FR-R9 traceability — the former Planning group.

Deferred (follow-up, per the “high-value only” scope): per-table export on every list (FR-D3), KPI rows on the remaining list pages, column glossary tooltips, and shipment/request detail drill-downs.

Links to Polaris Requisitions and the Inventory Manager flow for an end-to-end demo.

Inventory ↔ Procurement logic gap analysis (vs PRD)

Section titled “Inventory ↔ Procurement logic gap analysis (vs PRD)”

Tracing each handoff both directions against the PRD, after this build.

Inventory → Procurement

  • Threshold breach → request: Inventory raises the request from its low/critical thresholds (FR-R1/R2, FR-V-thresholds); Procurement’s Requests queue + Notifications reflect it. Aligned. Requests are Inventory-owned and pre-reconciled — no Procurement conflict step (a deliberate simplification vs the old split flow).
  • Request → order → requisition: Accept-and-create-order ties order↔request and advances the request; sync raises one Polaris requisition per order (FR-R10/R11, FR-R3). Aligned.

Procurement → Inventory

  • Order → shipment → receipt: goods receipt / 3-way match is deferred (removed this iteration). Batches are no longer a Procurement surface — the Inventory Manager receives items from Procurement and owns batch creation, so the procurement-side Batches list/detail was removed (Shipments · Quarantine · Vendor returns stay). Gap flagged: the populate-on-receive link (FR-V5) between Procurement’s inbound flow and Inventory’s Receiving still needs a shared, live receipt object — today Inventory Receiving and Procurement are parallel, not linked.
  • Quarantine → vendor return → credit: quarantine “Return to vendor” now raises a tracked RMA (FR-F3) through to credit/replacement. Gap flagged: the RMA credit does not yet write back to Inventory’s faulty-stock count decrement.

Polaris seam (finance)

  • Order ↔ requisition sync + requisition status (FR-R3/R11); amounts/AP/invoicing/budget are Polaris-side and intentionally out of this persona.

PRD mismatches to reconcile

  • FR-R7 (ad-hoc free-text product entry) is superseded — orders are catalog-picker-only now. PRD should mark FR-R7 as replaced.
  • FR-R9 (BOM → Procurement Order traceability) was removed from this persona (Planning group dropped). PRD should scope FR-R9 elsewhere or defer.
  • New surfaces added to the PRD FR list: supplier lifecycle/scorecards (FR-R13), vendor RMA (FR-R14), PO acknowledgement (FR-R15), spend analytics (FR-R17). GRN / 3-way match is deferred (not implemented).

Per PRD §11 — a Department Head / approver tier above the requesting personas (authorization signatures on the paper form) is a natural next surface.

The flow was swept to the Atlas shell and canon atoms. What changed structurally:

  • Topbar — the fixed three-level “Atlas / Procurement / Dashboard” path became the canonical two-part crumb (#crumb-parent / #crumb-parent-sep / #crumb-current) driven by setCrumb() from goto(), so only pages reached by a back button show a parent. Added the .loc-pill (“All warehouses” — procurement buys for the network), and the bare .user-pill became the full .acct-wrap: profile head, Switch role (searchable #role-dlg), Settings, Logout. The loose Sign out icon is gone; Logout lives in the menu.
  • Settings — a two-pane .settings-layout page (My Profile · Notifications · Preferences · Security) now exists; the account menu’s Settings item previously called an openSettings() that did not exist.
  • Filters — every one of the eleven .filt-bars above a card is gone. Filters live in the .tbl-card-head. The nine .pseg strips became .filt-select dropdowns (a segment strip is a detail-page control, never a table filter); each select keeps the id of the strip it replaced so the existing filter functions read it unchanged. Where a head would carry three or more filter controls (Orders, Requests, Shipments) they collapse behind one Filter button + the shared #dlg-filters modal, with a badge counting the filters actually narrowing the table.
  • Form fields — app forms use the canonical .fld block (.fld > label + typed control), with .fld-row for two-up rows and a .req span for required markers. It replaced .inp, and replaced .f-row and .f-lbl — the login’s own field pattern, never canon for an app form. The dormant login keeps its separate .login-f-row.
  • Removed — three .tbl-foot pagers whose buttons carried no onclick at all, over tables that fit on one page. Six classes had markup but no CSS rule: replaced .ord-chk and replaced .ord-status with data- hooks, since both are JS selectors rather than styling; then removed .cbx, removed .rsm-body--split (a mobile scaffold class, never canon on web), removed .muted and removed .card-meta, each redundant beside .td-sub.

Lint: 0 errors / 0 warnings, baseline entry removed (zero tolerance).

An order’s value is calculated, not typed

Section titled “An order’s value is calculated, not typed”

Create and edit order take a discount, which is optional, and a logistics amount, which is not: moving goods to a warehouse always costs somebody something, so the figure is required and has to be more than zero, and an order can never total nothing. The order total recalculates as subtotal - discount + logistics, with the discount clamped so it can never exceed the subtotal, and the total bar spells the arithmetic out beside the figure. Both persist on the order, so editing a draft reopens with the amounts that were saved.

A line’s total is its own arithmetic unless somebody says otherwise. Qty times unit price is the line total; ticking Set the line total on a line opens a field, prefilled with that figure, for the case where a vendor prices a line as a lump. Clearing the box puts the line back on its arithmetic. An override rides on the order and reads as overridden wherever the line total shows.

Every subtotal reads lineValue(), which is where a line’s override lives, so an overridden line is that figure in the form’s total, in the value saved on the order, in the orders row, on the order detail’s value card, in the logistics apportionment and in what the shipment quotes. Four of those used to multiply qty by price on their own, so a line priced as a lump read one figure in the breakdown and another in the total directly above it. The order value a shipment shows is the payable total · goods, less any discount, plus the logistics · the same figure the row and the requisition carry.

An order carries its shipment kind, local or international, chosen on create and edit and defaulting to local, since a road delivery inside the country is the ordinary case. The orders list filters on it, and the order detail states it beside the shipment method. A row that has never been told reads its kind from its method: air or sea is international, road is local.

The kind travels with the shipment (shipKindOf, shipKindBadge, shpKindCells), because it decides how the leg is handled: the shipments table states it under the method, the shipment record states it in the Shipment block, the shipments filter narrows on it, and a shipment created from an order inherits the order’s. A shipment reads its own if it has one, otherwise its order’s, otherwise its method’s. The method filter reads the badge rather than the whole cell, so the kind sitting beneath it cannot break the match. A created shipment also takes its number from one past the highest already on the table, so it cannot be born with an id a seeded row already holds.

An ad hoc order says where the goods are going

Section titled “An ad hoc order says where the goods are going”

A request carries its location and that location names its warehouse, so an order built from requests already knows its destination. An order Procurement raises on its own knows nothing, and the location that travels to Polaris on the requisition read as a dash.

Create and edit carry a Destination warehouse select (cr-dest, crDestSync, crDestFromTies) over the four warehouses. It is required while no line is tied to a request, and the moment a line names one it locks and reads that request’s warehouse, since the request has already decided. Untie the line and it is the PM’s choice again. The destination persists on the order, states itself on the order detail as the location sent to Polaris, and a direct order’s requester reads Procurement rather than a dash.

A shipment of a direct order has lines to receive (shpLines): with no tied request the receipt is built from the order’s own lines, going to the warehouse the order named, each keyed by the order and the line’s position so two lines of the same item keep their own received counts. Without that a direct order arrived with nothing to receive it against and no delivery note could be raised.

An item’s unit cost is what the vendor charged. What it costs standing on the shelf is that plus its share of getting the order here, and the share is the line’s share of the order’s value (orderCosting): a line worth 18% of the goods carries 18% of the logistics. A bag at 4,500 on an order carrying 100,000 of delivery does not cost 4,500; it costs 6,265, and that is the figure the warehouse receives it at.

A discount rides the same ratio, so each line carries its share of the net of discount and logistics and the costed lines still add up to the order total. Shares are whole currency with the rounding remainder landing on the largest line, so the column reconciles to the order rather than to a figure nobody can tie back.

The order detail states it per line: unit cost, line total, logistics share with the percentage of the order it represents, total cost, and actual unit cost. The create and edit form states the actual unit cost under each line as the numbers are typed, so the effect of the freight is visible before the order is saved.

That calculated total is the order’s value everywhere downstream: the orders row, the send-to-Polaris summary and the requisition raised in Polaris all read the same number, because the requisition is raised against what is actually payable.

The detail page carries an Order value card - subtotal, discount, logistics, order total - so the arithmetic is visible without reopening the form. An amount that was never set reads as a dash rather than a zero.

The order detail says what one order cost. Nothing said what the same item cost the last time it was bought, or what anyone else charges for it, so a buyer pricing a line had the figure on another page or in their head. Item pricing is that record, and the figure also appears under the box the cost is typed into: “Last unit cost 5,000 · Lagos Fasteners Ltd. · 28 Jun 2026”, or “Never bought on an order” for an item that is new.

A line at zero is a buy and not a price. An order line may legitimately charge nothing: a vendor replacing faulty stock, a free-replacement backfill. Taking the newest buy whatever it charged would let one of those set the item’s unit cost to zero, its cost per rider to zero and its change to Down 100%, wiping a real price with a figure nobody ever paid. The price is the newest buy that charged something. The zero line stays on the purchase history, because it happened and the units are on the shelf, reading No charge; the buy before it in the history skips past it to the last one that charged, so a free replacement between two orders does not break the chain. The by-vendor comparison is of charged buys alone, since a zero would take the Lowest column every time and a vendor who only ever gave it free has no price to be cheapest with. An item every one of whose buys was free reads Never charged, which is neither a price nor no history, and can be given a quoted cost like anything else without one.

A price is what an order committed to. A draft nobody sent is a quotation. The register holds drafts, two pending requisitions and one Polaris rejected; none of them sets a price, and only the three orders that reached a vendor do. Core draws the same line when it counts purchase orders, and a register that counted the drafts would report prices nobody has agreed to pay.

Two prices in different currencies do not compare. Core holds no exchange rate with an as-of date and refuses to convert its own spend figures for that reason, so a percentage struck across a currency change would be invented. The row reads Currency changed and sends the reader to the history, where every buy is stated in the money it was bought in. The vendor comparison on the record is grouped by the money each vendor quoted in, for the same reason.

Unit cost and actual unit cost, and only the first belongs to the item. A line’s unit cost is what the vendor charged per unit; its actual unit cost is that plus the line’s share of the order’s logistics. That share is apportioned by the line’s value against the ORDER’s, so the same item at the same price lands at a different figure depending on what else was on the order. It is a fact about an order line, which is where the order detail states it, beside the logistics share it came from and on the purchase history where every row is an order line. It is not on the item sheet, where it would read as a property of the item and is not one.

The change is struck on the unit cost for the same reason: comparing two landed figures would report a shift in the freight ratio, or in what else happened to be bought that day, as a move in the vendor’s price.

Costing is not a step in the buying. Purchasing reads as a sequence: requests become orders become requisitions. Item pricing and the bill of materials are what a buyer consults before and between those steps, not stages within them, so sitting inside the group they made the run look longer than it is. They have their own group under the process groups.

Freight is quoted on a load, not on a rider. A 20ft container costs what it costs whether it carries eighty kits or a hundred and twenty, so a rate names the load it prices and the rider kits that load holds, and the per-rider share is the division between them. Printing both is what makes the bigger load visibly the cheaper one per kit. The share lands in its own currency on the cost per rider rather than being spread over the goods, for the same reason nothing else here is converted.

A rider’s worth of parts is the bill of materials times the unit cost. The BOM already holds the quantity one rider takes; with the last unit cost beside it each line states what a rider’s worth costs, and the table’s foot adds up whatever is on screen. There is no summary card: every figure one would hold is a roll-up of the column a few rows below it. Two things keep the foot honest: an item nobody has bought has no cost, so its line offers Add a cost and the foot says how many it is missing instead of treating them as free, and a unit buying in two currencies has its total stated in both.

A quote is not a price, and the console says which it has. Everything else here rests on a price being what an order committed to, so a figure typed in for an item nobody has bought cannot be shown the same way. It is labelled Quoted in every cell it reaches, it is counted apart from the buys on a unit’s total, and the first purchase order supersedes it. Without that, a plan costed off quotations would be indistinguishable from one costed off invoices.

The register reads both catalogues. ITEMS is the order form’s list and the stock snapshot is the warehouse’s, and they are not the same names: Electric motor and Motor control units are on one, Main harness, Throttle and Insulation tape on the other. The register read only the first, so the two items the bill of materials cannot price were not on it to be given a cost. It reads the union, which is a patch over a split that wants fixing at the source.

The by-vendor comparison keeps every vendor. Scoping it to the last buy’s currency listed one vendor for an item bought five times from three, which answers the wrong question on a card whose entire purpose is comparison. Each vendor appears with the currency it priced in, and cheapest is read inside a currency rather than across one.

The minor unit shows where it changes the figure. A fuse bought at 3 dollars with 56 cents of freight on it is not a 4 dollar fuse; 45 kobo on a 1,444 naira landed cost is a column full of noise. The decimals appear when rounding would move the number by more than half a percent and are left off when it would not.

The trend is a column, and not a bar. It was Date, Order and Unit cost with a bar beside them, and the purchase history states all three, so two of the record’s three tables held the same rows. Worse, the bar said nothing: scaled from zero, three buys at 71, 66 and 60 draw at 100%, 93% and 85%, which is three full green lines a reader cannot tell apart. Scaling from the minimum instead would have made a 9% move look enormous. The movement between two buys is a number, so the column states it a row at a time, in the same words the register uses for the same comparison struck once over the whole record.

And the warehouse reads the price. Inventory does not buy, so Inventory’s item record carries the current unit cost read only, with no history and nothing editable. A stock adjustment is worth money and the warehouse had no figure for it.

Every discount an order carries is recorded against the vendor that gave it. The vendors table has a Discounts column totalling them in naira - the same FX base Spend analytics uses, since a vendor can discount in more than one currency - over the number of orders they came off; a vendor who has never discounted reads as a dash. The vendor record lists them order by order with the naira equivalent and a total. Saving an order writes the entry, and editing that order rewrites its own entry rather than stacking a second.

The leg says who hauled it, and what arrived

Section titled “The leg says who hauled it, and what arrived”

A delivery note leg was hauled by a name typed into a box, so nothing could be asked of the haulier afterwards and nothing priced the run in advance. Logistics contractors is a registry beside Vendors: the contractor, its contact, the vehicles it runs with plate and usual driver, the warehouses it covers, and a rate card per route (CONTRACTORS, renderContractors, openContractorDetail).

Logistics fulfilled by on the leg is a select over that registry (legContractorPick). Picking one fills Vehicle type, Vehicle plate no., Driver name and Phone number from its first vehicle, all still editable. Where the driver is the person already on the contractor, Same as the contractor contact number reads the number off the registry rather than having it typed again, and keeps reading it while it stays ticked, so changing the contractor changes the number (legDriverSameAsContact). A haulier used once is still a typed name, through One-off haulier, and Add new contractor adds one without leaving the dispatch (dlg-new-contractor). It holds only what a dispatch cannot do without, which is also what the contractor record requires: name, contact name, phone, and one vehicle with its type and plate. A contractor with no vehicle cannot be dispatched with, so the record holds the same bar. Everything else, the email, the driver and the rate card, is filled in on the contractor record rather than in the dialog. Every picker reads the same registry (syncContractorPickers), so one added at a dispatch is in the requisition dialog immediately.

A shipment names one too. Carrier and transport details gain a Logistics contractor select over the same registry, with the contractor’s vehicle and driver beside it (sdContractorPick). The carrier is who moved the freight; the contractor is the haulier moving it on the ground, and a shipment can have both. Picking one fills its vehicle and driver, the hint reads back its contact, and the choice persists on the shipment.

The logistics requisition prices itself: picking the contractor reads its rate for that destination and fills the amount (contractorRate, lrHaulierPick), and says so under the field. A route with no rate on file leaves the amount to be typed.

The contractor record carries legs hauled, variance rate, open claims and spend, each computed from the board rather than typed: legs come from the dispatches plus the contractor’s history, the variance rate is the share of those legs that arrived short or over, and open claims are the variance cases still running against it.

The leg item table states Qty sent and, beside it, Qty received (legRecvCell), naming the gap as short or over. A leg whose count disagreed carries a Variance badge on its card and on the delivery notes board, linking to the case.

Procurement never counts and configures nothing. The count is the Inventory Manager’s, taken when the delivery note leg is received, and any difference from what was sent, by any amount, opens a case automatically. There is no tolerance, no Receiving variance setting and no write-off or approval value: every case is worked the same way whatever its value, and a write-off always takes a reason.

The case reaches Procurement and the requester who raised the request in-app and by email, the line carrying item, expected, counted, variance, delivery note leg, vendor, contractor, vehicle and destination, with the case one click away (notifyVariance).

The investigation is the two enquiries that actually happen

Section titled “The investigation is the two enquiries that actually happen”

Variance cases sits beside Quarantine. A case carries its ref, delivery note leg, item lines as qty ordered / qty sent / qty received / variance, the same words the delivery note uses, plus vendor, contractor, vehicle and plate, destination warehouse and status, which is Open or Closed. How it closed is the outcome, not a second terminal status, and the case list carries it under the badge. It pulls through the photo taken at receipt and the vendor freight reference already captured on the note. Inventory reads the same case and never works it.

The investigation is made by a person, on the phone, over email or standing in the yard, and it runs over days, so it is a note added to as the work happens rather than two boxes to fill (vcAddNote, renderVcNotes). Each entry is stamped with who wrote it and the day, date and time, newest first, and the case list says how many notes a case carries. An open case takes new notes; an attributed or closed one keeps the trail and stops taking them.

Nobody owns a case and nothing is due on a date. The trail says who has been working it and when they last did, which is the same question an assignment and a due date were being asked to answer.

Four outcomes, each landing where that kind of claim already lives:

  • Vendor short-shipped raises a credit or replacement on the existing vendor return path, as a new RMA row against that vendor (vcOutcomeVendor).
  • Lost in carriage raises a deduction against that leg’s logistics requisition (vcOutcomeContractor). With no requisition raised on the leg yet, it is held against the next.
  • Warehouse count error requests a recount and the adjustment that follows, and raises no claim on anybody.
  • Unresolved writes off with a required reason, and still records the vendor and the contractor, so a haulier that keeps being unresolved accumulates a pattern rather than a clean sheet.

Attributing settles the argument, not the account. Naming who it belongs to records the outcome and the day, and a case that is owed money stays open until the credit, the replacement or the deduction is recorded (vcAttribute, vcSettle). A recount or a write-off owes nothing, so both close on the spot. An open case carries its outcome beside its age, so the board says a case is settled against the haulier and still waiting for the money.

Both ways a return settles are money back. Advancing it to Credit received asks for the credit note, Replacement shipped asks for the shipment carrying the goods, and either one recovers the case in full and closes it. The return row states the same figure back.

The record reads in one glance. Four figures across the top, variance, value at risk, recovered and status; then the case and the decision side by side, with the outcome and its settlement in the same card so the button sits beside the money it moves; then what was counted, then the investigation. The photo taken at receipt sits on the case card rather than in a card of its own.

Recovery follows the document it is owed on. A vendor claim settles when its return reaches Credit received or Replacement shipped, and advancing the RMA writes the recovery onto the case with the credit note and the day (rmaSettleCase); the return row states the same figure back. A haulier deduction settles when the requisition carrying it is approved, pulled from Polaris on the leg the way an order’s requisition is (pullLogReqApproval). A deduction attributed before that leg had a requisition is held and lands on the one raised next. One writer records every recovery (vcSettle), so the case and the document it points at cannot disagree, and the case reads that document’s live status while it is still moving. Typing a settlement by hand stays, for cash refunds and anything settled off-system.

A closed case records what it got back. A case attributed to a vendor or a haulier is owed the value of what went missing, so the record carries owed, recovered, how it came back (credit note, free replacement, deduction applied, cash refund), the document and the day it settled (renderVcRecovery, confirmRecovery). A part recovery states the shortfall and stays open to more. Every recovery writes itself into the trail. The board carries a Recovered column against each case and a tile in money rather than a count, so the process can show what the chasing earned back rather than only what is open.

Backfilling asks who bears it. A closed case offers Backfill shortfall, which asks the question before an order is started (openBackfill): the vendor replaces it free of charge, and the order is raised at zero cost with the value recorded as recovered; the vendor issues a credit instead, and the credit note is the recovery with no order at all; or we buy it again and pay, which is the only path that spends. The default follows the outcome, free where the vendor short-shipped. Supply is never re-raised automatically, and the request the leg served keeps the close it already has.

The value is what the missing units actually cost, not what the vendor charged for them: the landed unit cost, vendor price plus that line’s share of getting the order here, times the quantity that did not arrive (varActualUnit). It is computed from the order’s own costing, the same figure the order detail shows as the actual unit cost, and the line prints both so the gap is visible. PO-2216, the order DN-0230 was raised from, is seeded with its logistics so the number comes out of the costing rather than off the case.

An open case carries its age, in red, from the day the warehouse raised it (varDaysOpen), on the list and beside the status. A closed one carries its outcome and the day it closed. That is the number that drives chasing, and it is why the case needs no due date.

The bill of materials is what a unit runs on, not what it last asked for

Section titled “The bill of materials is what a unit runs on, not what it last asked for”

Buying was driven off whatever each unit happened to request that week, with no standing statement of what a unit needs to run at all. Bill of materials holds that, a unit at a time: the item, the quantity it runs on, and a note beside every line. The note is the part a buyer cannot get from the item name — the substitute that is acceptable, the vendor who holds the mould, why the quantity is what it is — so Loctite 243 reads “243 and not 270, the 270 will not come apart at the next service” and a 45-day motor reads “order against the plan, not the shortfall”. A line with nothing to add says No note rather than reading empty. The quantity is per rider. A line says what one rider’s build takes — one motor, two connectors, a quarter of a roll of tape — so the order is that figure against the riders planned, rather than a quarterly total that has to be re-derived every time the plan moves.

A line can only name an item the system already holds. The sheet carries the business unit, the item and the quantity; the category, tracking and unit of measure are read off the catalogue rather than typed, so a list cannot introduce an item nobody can order, or disagree with the catalogue about what a thing is counted in. A row naming something the catalogue does not hold is skipped and named rather than silently added.

And the business unit is a team the system already holds, not free text. The unit filter is built from that list rather than from whatever the rows happen to say, so a unit with no lines yet still appears; a sheet naming something that is not a unit — a department that was renamed, a site, a typo — is skipped and named the same way an unknown item is. Inventory and Delivery hold and move stock rather than build against a rider, so neither runs a list. Sample sheet is written out of both lists: a row for every business unit, spelled as the system spells it, against an item the catalogue holds, so neither the shape nor the names have to be guessed.

Insights breaks down in one card, a view at a time

Section titled “Insights breaks down in one card, a view at a time”

Requests used to be three stacked cards — by team, by item, by warehouse — which made one population three tables and left no room for the columns that tell you anything. One card now, the grouping chosen in its head: Items · Teams · Warehouses · Requested vs used, each carrying its count.

A parent row opens into what it is made of: an item into the teams that asked for it, a team into the items it asked for, with Expand all beside the search. A line carries its cadence, not just its count — how often it is asked for, with the usual gap riding underneath, and when it was last asked. Nine columns is one past what the canon carries, so the gap folds under how often rather than taking a column of its own; the two are the same fact told twice.

A pattern worth a second look is named, in the estate’s own words: Oversized and Repeat while open in red, Split requests, Rising and Mostly urgent in amber. The filter reads All signals · Any signal · each one, and the flagged count sits beside it.

Requested vs used is where the leak shows. Per team: what was asked of Procurement, what was asked of the warehouse, what the warehouse issued, and what the floor actually put on a unit. Retrofit station reads 44% and flags Under half used; Production reads 93% and does not. The export follows the view on screen rather than dumping all four.

A request names the authority it travels on

Section titled “A request names the authority it travels on”

A new-item request already named who approved it; an ordinary one named nobody, so the buyer could see the team and the requester but not the manager who said yes. Every request carries its approver and the detail states it beside the team it came from.

The note can be printed where it was raised

Section titled “The note can be printed where it was raised”

The warehouse has had Print on its own copy of a delivery note all along. Procurement raises the note and had no way to put one on paper, so the same action, in the same words, sits on the record it is created from.

The queue names the warehouse that raised it and the floor that wanted it

Section titled “The queue names the warehouse that raised it and the floor that wanted it”

“Requested by” showed a floor technician, which is not who asks Procurement for anything. The floor asks Inventory; Inventory fulfils what it holds and raises the rest here. So the column names the Inventory Manager, and the people who actually wanted the thing have a column of their own.

One purchase line can carry several of them. The warehouse batches the same item across teams, so a request for 120 Anderson connectors is 90 for the retrofit station and 30 for the line. The row reads 2 requesters with both teams under it, and the record names each person, their team and their share of the quantity. A request from one team reads as that one person, as before.

Every request names the manager who approved it, in a column rather than only on the record, and a request for an item Inventory has flagged says that the flag is why it waited. Location left the table for the record: a buyer decides what to buy on the item, the quantity and the urgency, and nine columns of that beat ten with the destination in the middle.

The bill of materials sits with the buying, and an item two units run on is two lines

Section titled “The bill of materials sits with the buying, and an item two units run on is two lines”

Bill of materials sits under Requisitions, after Requests and Orders, because it is the standing statement the other two are worked against.

An item several units need is a line per unit, not one line summed, because the quantity and the reason differ: the line takes a quarter of a roll of tape per build and a conversion takes a third, and Maintenance replaces connectors in pairs whether or not both are burnt. The page sorts by unit so each unit’s list reads as a block, and an item that appears under more than one says so under its name: Also run by Retrofit station.

The budget is the bill of materials against a plan

Section titled “The budget is the bill of materials against a plan”

Budget is a tab on the Bill of materials, not a page of its own, because it reads the same lines against a plan: the same items, the same quantities per rider, multiplied by a rider count and dated. The strip is the one Item pricing uses, .pseg with data-tab over data-pane panes, and the page actions switch with the tab. A plan is a rider count and a date, and the date is not decoration: it is what turns a total into an order-by date per item.

A budget is not need times riders. It is need times riders, minus what is available to promise, minus what is already with a vendor, priced at what the item actually costs, plus freight. Each of those is the difference between a budget and a multiplication. Available comes from the stock snapshot’s ATP rather than its on-hand, because stock already spoken for cannot be planned twice, and an item held at two locations adds rather than the second row replacing the first.

On order is what a vendor has and the shelf does not (BG_ONORDER): Ordered and Shipped. Draft and the requisition states are not with a vendor yet, Delivered is already counted in the stock, and Partially received cannot be split without a received quantity per line, so it is left out rather than counted twice.

IT GROUPS BY ITEM, NOT BY BILL-OF-MATERIALS LINE. The bill keeps a line per unit on purpose, but the shelf is held per item, so a budget drawn a line at a time subtracts the same stock twice: Anderson connector read 385 to buy under Maintenance and 385 under Retrofit against a real shortfall of 1,385. The split rides with the unit list under the item name, Retrofit 2 · Maintenance 2, rather than taking a second line in its own column, because .tbl td does not wrap and that line set the column a hundred pixels wider than its numbers need.

Order by is the deadline less that vendor’s lead time, red once it has passed, with the vendor and the lead named under it. The vendor is whoever last sold the item, else whoever lists it as a preferred line. Three items have neither, and the cell says no vendor on file rather than implying the order can wait.

Freight is a line, not a markup (bgFreight). A load is a step: the plan needs whole containers and the cost jumps with the last one, so a percentage of spend would be wrong in both directions, which is why the item sheet carries no landed cost either.

One currency, when you ask for it. The default is each currency on its own, because core holds no rate and a total that joined them would be a number nobody can reproduce. Show in NGN or USD converts everything, including the Cost column and the freight, so the column still adds up to the total beneath it, and the rate and the date it was read print under the figures that used them (bgFxNote): 1 USD = NGN 1,500 · rate as at 30 Jul 2026. It is quoted the way a person quotes it whichever way the page is reading, so a plan read in dollars still says 1 USD = NGN 1,500 and not 1 NGN = $0.001, and a basket that never needed converting prints no rate at all. The native price stays on the Cost sub line, since that is what the invoice will say, and the exported file carries the rate as its last row.

What it refuses to do. It will not total across two currencies unsolicited, and it will not present a confident figure over a basket it could only price in part: the Parts tile says how many items it priced, the foot names the items still to buy with no price, and a tile whose basket spans two currencies leads with the larger and names the other by its code rather than its symbol, because two symbols in one tile is indistinguishable from a value printing its unit twice.

The tiles follow the filters, the way the bill of materials foot does: narrow to one business unit and the Parts and Budget figures cost that unit. Freight does not, because it is quoted on a load of whole rider kits and is not attributable to a subset of the items, so under a filter that tile says where its figure lives rather than printing the whole plan beside one unit’s parts.

Nothing to buy is not a cost of zero: a covered item shows its unit price and no line total.

Print put the whole console on the paper: sidebar, topbar, the page behind it and every control that only works on a screen. The sheet is now the note and nothing else.

Every export carries · and names with accents, and the file is UTF-8. Excel assumes otherwise unless the file says so in its first three bytes, so a sample sheet opened as rider’s. Every CSV this console writes now carries the mark.

Nine columns, and what each of them is for

Section titled “Nine columns, and what each of them is for”

The queue names who raised it (the Inventory Manager, because the warehouse is who asks Procurement to buy), who originally wanted it with their team, where it is going, and who approved it. Those four are what a buyer decides on, so they are columns rather than things to open a record for.

The approver is a person. A title alone does not tell a buyer who to ring, so the cell names the person with their title under it: Femi Adeyinka, Head of Production.

And it is usually empty. Most of the catalogue is handed over on the request alone — only a new item, or one Inventory has flagged, takes an approval at all. The attribute comes off the requests that never took one, rather than being kept and hidden, so the column being filled is itself the statement that this request needed a manager. The record spells it out: Not required — the warehouse fulfils this item on the request alone.

Urgency rides under the status. Nine columns is the canon’s budget for this table and ten fit badly, so the one fact here that qualifies another rather than standing alone sits under the badge it qualifies. The table is laid out rather than left to its content: fixed widths, so a long item name ellipsizes in its own column (and reads in full on hover) instead of pushing the row sideways.