measured at e70ff195
Created: 2026-07-16 | Last Updated: 2026-07-19
Pilot instance of
module-scorecard-template.md. Read-only analysis; recommendations only. Evidence isfile:line/ endpoint againstD:\BCPOSGit\qiiubon branchmainat commite70ff195.Re-scored 2026-07-19 (69% at
bdf367d3→ 72% → 74% ate70ff195). PR #884 landed the reorder proposal engine — moving PO-F025 Missing → Implemented (the prior #1 gap) and PO-F028 Missing → Partial; F025 + the lifecycle were live-confirmed against the running app. Then the Receivings Fable review found landed cost (PO-F030) and three-way match (PO-F032) were double-scored / mis-owned — reclassified as Dependencies (→ Receivings / AP), out of PO's denominator, lifting the headline 72 → 74.
| Field | Value |
|---|---|
| Module name | Purchase Orders (PO) |
| File | po-scorecard.md (target: docs/modules/po-scorecard.md) |
| Author / session | Claude (Opus 4.8), coordinated multi-agent analysis |
| Created | 2026-07-16 |
| Last-Updated | 2026-07-19 |
| Repo commit (SHA) analyzed | e70ff195 (branch main; re-scored 2026-07-19 — was bdf367d3) |
| Governing ADRs | 0021 (separate PO/Receiving), 0023 (three-tier history), 0020, 0024, 0038, 0070 (proposed), 0081, 0082, 0085, 0102, 0108 (proposed), 0120 (proposed) + reorder engine design spec/plan (PR #884, #446+#449) |
| Governing design docs | docs/design/inventory-supply-chain.md (Draft), docs/PROGRESS.md Phase 5 |
| Status of this analysis | Adversarially-verified — Fable pass (findings in artifact-phase6-fable-review.md) |
Léelo así (en simple). Esto es una foto temporal, no un contrato — mide cuánto del módulo estaba construido el 2026-07-19 (commit
e70ff195; re-scoreado desde elbdf367d3original).Cuál es la fuente. Este archivo (
po-scorecard.md) es el documento que lista todo lo que el módulo debería tener y su estado; es el que se sintetizó en este análisis (no existía antes). El scorecard HTML no es más que su representación visual — lo que se ve ahí sale de aquí.De dónde salió este archivo (fuentes exactas, no "todo el repo"). Se leyeron de forma dirigida (con citas
file:line, no un barrido exhaustivo):
- Código —
src/QIIUB.Domain/Merchants/PurchaseOrder*.cs,src/QIIUB.Api/Features/PurchaseOrders/*, sus EF configs + migraciones + tests, yapps/qiiub-portal/src/features/purchaseOrders/.- Documentación QIIUB — el design doc
docs/design/inventory-supply-chain.md, los ADRs (0021, 0023, 0070, 0081, 0082, 0102…) ydocs/PROGRESS.md.- Legado — los árboles Markdown de RMS y RMH en
D:\Code\POSRMSyD:\Code\POSRMH(para saber qué features existían antes).- Mercado — investigación web (Lightspeed, Square, Shopify/Stocky, Cin7, NetSuite, RMH actual, Odoo).
La app SÍ se ejecutó el 2026-07-19 para confirmar en vivo la generación por reorden (F025) y el ciclo de vida — se levantó la API + el portal contra SQL local y se manejó la app real (no se hizo en el snapshot original). Cuando el código cambie, los números quedan viejos: hay que regenerar el documento.
Purpose. The Purchase Orders module lets a merchant create, issue, track, and close orders to suppliers for replenishing stock. A PO is the intent-to-buy document: it records who is being ordered from, to which location, what products and quantities at what cost, and moves through a lifecycle (Draft → Submitted → PartiallyReceived → FullyReceived → Closed) as goods arrive. Per ADR-0021, the PO is deliberately only the order — physical arrival is a separate PurchaseReceipt document, and stock distribution is a third Distribution document.
Vertical relevance.
core. Replenishment from suppliers is the primary use.core. Ordering from drug wholesalers/distributors; lot/expiry handled downstream at receipt.core. Ordering materials by location/job.Actors & roles. All PO endpoints require SystemAdmin | Admin | Manager (no Cashier). Surfaces: API (FastEndpoints, complete), portal (qiiub-portal merchant UI, CRUD present). POS and qiiub-admin: no PO surface. Mobile PO approval is a declared future dependency.
Not-in-this-module. Physical receiving, quantity-on-hand updates, and cost-averaging happen in Receivings (PurchaseReceipt). Multi-store allocation happens in Distribution. Supplier master data and per-product supplier links (Supplier, ProductSupplier) belong to Catalog. Reorder thresholds live on LocationProduct in Inventory. Vendor invoicing / GL posting is a future AP module. See §2.
The established route today (from the shipped endpoints), and where each step lives:
POST /purchase-orders. Pick the supplier (PO-F006), the destination location where goods will be received (PO-F007), and optionally the first lines. A DocumentNumber (PO-000142) is minted. Creation methods are now manual (PO-F001) and reorder-based generation (PO-F025, PR #884 — proposal → fill / generate, see §1c); only sales-history generation (PO-F039) is not built.SubTotal recomputes. Editing is locked once past Draft (PO-F013).POST /purchase-orders/{id}/submit (PO-F010). Draft → Submitted. An empty PO is rejected. (No print/email/API transmission yet — PO-F023/F024; see §1c.)PurchaseReceipt, a dependency). Each receipt increments QuantityReceived; the PO auto-moves to PartiallyReceived / FullyReceived (PO-F017). Multiple partial receipts per PO are first-class (ADR-0021).POST /purchase-orders/{id}/close (PO-F012). FullyReceived → Closed, stamping who/when. Blocked if any draft receipt is pending.POST /purchase-orders/{id}/cancel (PO-F011), allowed only from Draft or Submitted.State machine: Draft → Submitted → PartiallyReceived → FullyReceived → Closed; Draft|Submitted → Cancelled. Each transition is guarded independently (no central state-machine class — PO-F009).
In both legacy systems a store user creates a PO by choosing an order-calculation method, then a supplier strategy and an item scope. RMS calls it "Create New Purchase Order"; RMH calls it the "Purchase Order Requisition" wizard (Store Manager → Inventory/Purchasing → Purchase Order Requisition; commit → PO created). QIIUB now offers Manual + reorder-based generation (PR #884, live-confirmed 2026-07-19); only the sales-history generated method (F039) is not built.
| Store-level method | What it does | RMS/RMH | QIIUB |
|---|---|---|---|
| Manual / blank | Add items & quantities by hand | Both | Implemented (PO-F001) |
| By Reorder Information | Add items below reorder point; suggest qty up to restock level | Both | Implemented (PO-F025, PR #884 — worksheet + fill + generate) |
| By Recently Sold (sales history) | Add items/qty sold in a date range | Both | Missing (PO-F039) — sold-30d is a display hint only, not a generation input |
| Supplier strategy: primary / lowest-cost / specified | Which supplier the generated lines go to | Both | Primary implemented — reorder groups by primary supplier (ProductSupplier.IsPrimary); no-primary items go to an Unassigned bucket. Lowest-cost/specified not offered |
| Item scope | Which items an auto-generation considers. In QIIUB this maps to hierarchical categories (Category self-FK tree + ProductCategory N:M; ADR-0044 deliberately collapsed RMH's Dept/Category/Filter split into one tree — no Brand/tags entity yet) + supplier + search |
Both (adapted to QIIUB) | Backend-complete — CategoryPublicId + SupplierPublicId filters on the proposal; portal category picker is a documented fast-follow |
Naming is QIIUB-native, not RMH-literal. RMH calls the store flow "PO Requisition" and modern POS call it "reorder suggestions / automated reordering" (Lightspeed, Stocky, Odoo). The QIIUB feature is named for what it does (reorder-/sales-based generation), and its inputs map to QIIUB's own model (hierarchical categories,
LocationProductreorder point), not to RMH's taxonomy.Fixed Quantity was a central-only variant in RMS/RMH. In QIIUB it's just a minor variant of the same generation idea (fan a fixed qty across the scoped items) — folded with the two generated methods above, not scored on its own. So the real store-PO gap is exactly those two generated methods (PO-F025, PO-F039).
| Module | Direction | What crosses the boundary | Scoring treatment |
|---|---|---|---|
| Catalog — Supplier / SupplierContact | upstream | PO references SupplierId; supplier name/terms/contacts resolved for display |
dependency — out of scope |
| Catalog — ProductSupplier | upstream | Per-product supplier link, SupplierCost, MinimumOrder, SupplierSku, primary-supplier flag |
dependency — out of scope |
| Catalog — Product / ProductVariant | upstream | PO lines reference ProductVariantId |
dependency — out of scope |
| Catalog — UnitOfMeasure | upstream | PurchasingUoMId + conversion snapshot on line |
dependency — out of scope |
| Inventory — LocationProduct | upstream | ReorderPoint / RestockLevel (the inputs auto-reorder PO generation would read) |
dependency — out of scope |
| Document Sequencing | shared-infra | DocumentSequence + DocumentSequenceService mint the PO-000142 number |
shared-infra; PO's use is in-scope (PO-F014), the service itself is a dependency |
| Receivings — PurchaseReceipt | downstream | Receipts increment PurchaseOrderLine.QuantityReceived and drive PO status |
dependency — out of scope |
| Distribution | downstream | Allocates received goods to stores; consumes receipts, not POs directly | dependency — out of scope |
Cross-cutting UI (@qiiub/ui DataTable) |
shared-infra | Configurable grid columns (show/hide/reorder/persist) + custom-field/field-visibility config | dependency — out of scope (belongs to the UI library / a platform config module, not PO) |
| Mobile (Operations) | downstream/future | Planned mobile PO approval | dependency — out of scope |
| AP / Vendor Invoice | downstream/future | QuantityInvoiced / QuantityToInvoice fields reserved for 3-way match |
dependency — out of scope |
Boundary statement. The PO module owns the PurchaseOrder and PurchaseOrderLine tables, every endpoint under Api/Features/PurchaseOrders/, and the PO state machine — the set of statuses and the transition guards. The physical actions that some transitions represent (receiving goods, distributing them) are executed by neighbor modules: the Submitted → PartiallyReceived → FullyReceived transitions are triggered by Receivings completing a receipt, even though the resulting PO status is a PO-owned outcome (single-owner rule — Receivings lists it as a dependency, PO scores it; see PO-F017). The PO module does not own supplier data, product data, reorder thresholds, the numbering service, physical receiving, or distribution.
On "central ordering" (why there is NO separate PO Planner module in QIIUB). RMS/RMH have a separate HQ "PO Planner" (Worksheet 340) because their HQ and stores run on disconnected databases — the worksheet exists to be approved centrally and then downloaded and converted into a store PO. QIIUB is cloud with a single source of truth, so that whole lifecycle is unnecessary. Verified: one merchant has many Locations; the portal identity is merchant-wide + role with no per-location wall; and CreatePurchaseOrder already accepts any location of the merchant. So a head-office user already creates a PO for any store today (covered by PO-F001 + PO-F007), and cross-store visibility is just the existing list under RBAC (PO-F016). The only genuinely missing piece is multi-location "master" bulk create — one action that fans out POs across several locations — which is an in-scope PO feature (PO-F027), naturally modeled on the existing Distribution/DistributionAllocation per-location pattern. (Fixed-quantity generation, distribution-site disbursement, and worksheet approval are RMS/RMH-only artifacts — the first would just be another generation method, the second is the Distribution module, the third does not apply to cloud.)
Boundary-leak checklist (audited in the Fable adversarial pass — see artifact-phase6-fable-review.md):
QuantityToReceive remainder tracking is PO-owned (scored Partial, PO-F026).ReorderProposalService + GET /inventory/reorder-proposal + POST /purchase-orders/generate, live-confirmed 2026-07-19 (PR #884). So PO-F025 is Implemented in §3; only the thresholds themselves remain the dependency.Implemented, PO-F014).CompletePurchaseReceiptEndpoint in Receivings, but the status semantics are PO's; scored once here as PO-F017, listed as a dependency (not re-scored) in the future Receivings doc.Status legend: Implemented / Partial / Missing / Dependency (excluded from score). Source: RMS / RMH / market / QIIUB-original.
This is a comparison map, not a backlog. The feature universe below is drawn from what RMS, RMH, and the modern market offer — it's the yardstick QIIUB is measured against, so the % means "how complete vs. a mature POS," not "how much of a committed plan is done." A
Missingmeans that world had it and QIIUB doesn't (yet) — not that QIIUB must build it. Several legacy items (especially T3, and anything tagged an RMS/RMH artifact like "HQ worksheets") QIIUB may deliberately never do; those are candidates for a human to markN/A. The features QIIUB has actually decided to pursue are the prioritized §7 gaps — everything else is context for that decision.For developers (use the Evidence column as a pointer). Before building any feature, a dev — or their Claude — can start from the row's Evidence: for
Implementedit's thefile:line/endpoint that already exists; forMissingit's the natural code home / nearest existing piece (e.g. PO-F056 → a column onSupplier; PO-F037 → the existingPurchaseOrder.EmployeeId; PO-F027 → theDistribution/DistributionAllocationpattern). That answers "does anything already exist for this?" in one hop, before writing code.
| ID | Name | Description | Source | Status | Evidence | Notes |
|---|---|---|---|---|---|---|
| PO-F001 | Create PO (Draft) | Create a Draft PO header + optional lines | RMS+RMH+market | Implemented | POST /purchase-orders — CreatePurchaseOrder/CreatePurchaseOrderEndpoint.cs:84 |
Mints DocumentNumber; validators enforce column bounds |
| PO-F002 | Add line | Append a line to a Draft PO | RMS+RMH+market | Implemented | POST /purchase-orders/{pid}/lines — AddPurchaseOrderLineEndpoint.cs:60 |
Auto LineNumber, recomputes SubTotal |
| PO-F003 | Edit line | Full-replace edit of a Draft line | RMS+RMH+market | Implemented | PUT /purchase-orders/{poPid}/lines/{pid} — UpdatePurchaseOrderLineEndpoint.cs:62 |
Route-forgery defense; rejects if QuantityReceived>0 |
| PO-F004 | Remove line | Delete a Draft line | RMS+RMH+market | Implemented | DELETE /purchase-orders/{poPid}/lines/{pid} — RemovePurchaseOrderLineEndpoint.cs:30 |
204, recomputes SubTotal |
| PO-F005 | Update header | Edit supplier/location/expected-date/notes on a Draft PO | RMS+RMH+market | Implemented | PUT /purchase-orders/{pid} — UpdatePurchaseOrderEndpoint.cs:44 |
Draft-only |
| PO-F006 | Supplier selection | Assign the supplier a PO orders from | RMS+RMH+market | Implemented | PurchaseOrder.SupplierId FK — PurchaseOrderConfiguration.cs:22-32; validated on create |
Composite FK MerchantId-first |
| PO-F007 | Destination location | The location where the ordered goods will be received (the "ship-to" store/warehouse) | RMS+RMH+market | Implemented | PurchaseOrder.LocationId FK — PurchaseOrderConfiguration.cs |
One location per PO; a head-office user can already pick any location. Creating for many locations at once is the in-scope gap PO-F027 (T2) |
| PO-F008 | Line cost & rollup | Unit cost, line total, PO SubTotal recomputation | RMS+RMH+market | Implemented | PurchaseOrderLine.UnitCost/LineTotal; SubTotal recompute in Add/Update/Remove endpoints |
decimal(18,4)/(18,2) |
| PO-F009 | Status lifecycle | Draft/Submitted/PartiallyReceived/FullyReceived/Closed/Cancelled enum + guards | RMS+RMH+market | Implemented | PurchaseOrderStatus.cs:6-14; per-endpoint source-state predicates |
No central state-machine class — each endpoint guards its own transition |
| PO-F010 | Submit / issue | Draft→Submitted; issue PO to supplier | RMS+RMH+market | Implemented | POST /purchase-orders/{pid}/submit — SubmitPurchaseOrderEndpoint.cs:32 |
Atomic flip; rejects empty POs (400) |
| PO-F011 | Cancel | Draft|Submitted→Cancelled | RMS+RMH+market | Implemented | POST /purchase-orders/{pid}/cancel — CancelPurchaseOrderEndpoint.cs:32 |
Predicate excludes received states |
| PO-F012 | Close | FullyReceived→Closed with accountability stamp | RMS+RMH+market | Implemented | POST /purchase-orders/{pid}/close — ClosePurchaseOrderEndpoint.cs:33 |
Blocks if draft receipts pending; stamps ClosedDate/ClosedByEmployeeId |
| PO-F013 | Draft-only mutability | All header/line edits locked once past Draft | RMS+RMH+market | Implemented | 409 guard in 5 endpoints (Update/Add/UpdateLine/Remove/Batch) | Stricter than RMS (RMS allowed post-issue edits); satisfies RMH "cannot be altered" intent |
| PO-F014 | Document numbering | Human-readable PO-000142 per merchant |
RMS+RMH+market | Implemented | DocumentSequenceService.cs:24-30; S12_DocumentSequencesSeed.cs:21-32 (prefix "PO-") |
Dapper UPDATE...OUTPUT + session_context; sequence service itself is shared-infra dep |
| PO-F015 | View PO detail | Full PO + lines graph with audit attribution | RMS+RMH+market | Implemented | GET /purchase-orders/{pid} — GetPurchaseOrderEndpoint.cs:84 |
Joins Supplier/Location/Employee names + CentralDb audit user names |
| PO-F016 | List / filter / sort | Paginated PO list, filter by supplier/location/status/search, sort, StatCards | RMS+RMH+market | Implemented | GET /purchase-orders — ListPurchaseOrdersEndpoint.cs:91; aggregates tested |
Draft/Open/Closed counts span all pages |
| PO-F017 | Status from receipt coverage | PO auto-transitions to Partially/FullyReceived as receipts complete | RMS+RMH+market | Implemented | CompletePurchaseReceiptEndpoint.cs:296-312 (FullyReceived :309, PartiallyReceived :311; write lives in Receipts module) |
Cross-module write into PO; the PO-side outcome is real and tested. PR #906 added last-cost (LocationProduct.Cost) + off-supplier auto-link (ProductSupplier) around it — both Inventory/Catalog-owned, NOT PO (relevant to the future Receivings scorecard) |
| PO-F018 | Cross-merchant isolation | Every PO op tenant-scoped; no cross-merchant read/write | QIIUB-original (tenancy) | Implemented | Composite PK/FK MerchantId-first; tests CreatePO_CrossMerchantIsolation_NotVisible, CrossMerchant_CannotEditAnotherMerchantsPOLine |
Critical Rule #1 |
| ID | Name | Description | Source | Status | Evidence | Notes |
|---|---|---|---|---|---|---|
| PO-F019 | Expected delivery date | Capture, edit, and track an expected arrival date | market+RMS+RMH | Partial | PurchaseOrder.ExpectedDate field; editable via PUT /purchase-orders/{pid} |
Capture+edit work; the tracking half is absent — no overdue/ETA surfacing, no per-line ETA, no alerting. That gap is why this is Partial, not Implemented |
| PO-F020 | Purchasing UoM | Order in a purchase unit (case/box) distinct from sales unit, snapshot conversion | RMH+market | Implemented | PurchaseOrderLine.PurchasingUoMId + UnitsPerPurchaseUoM; GetPO_LineWithUoM_... test |
Conversion snapshotted at PO creation |
| PO-F021 | Bulk line delete | Multi-select delete of Draft lines | QIIUB-original+market | Implemented | POST /purchase-orders/{poPid}/lines/batch-delete — RemovePurchaseOrderLinesEndpoint.cs:58 |
Atomic, all-or-nothing, ≤100/call |
| PO-F022 | Portal PO UI | Merchant-facing CRUD for POs | market | Implemented | apps/qiiub-portal/src/features/purchaseOrders/ (List/Detail pages, Create/Edit drawers, Submit/Cancel/Close modals, LinesGrid); en/es locales |
A route-index comment claims Create/Edit/Detail deferred to "another chat," but component files exist — comment appears stale |
| PO-F023 | PO printing / PDF confirmation | Generate a printable/PDF PO Confirmation | RMS+RMH+market | Missing | ADR-0070 (Phase 1, GET /purchase-orders/{id}/print, QuestPDF); actively being built per ADR-0070/PR #79 but not merged — zero QuestPDF in src/ at this commit |
Channel 1 of 3 (print / email / electronic). Closest to landing |
| PO-F024 | Email PO to vendor | Email the PO (PDF) to the supplier's contact inbox | RMS+RMH+market | Missing | Designed in ADR-0070 as Phase 2 (POST /api/purchase-orders/{id}/email, behind the AP-module flag) — not built |
Channel 2 of 3. This is a PDF to a human inbox, not machine-to-machine integration (that's PO-F031) |
| PO-F025 | Reorder-based PO generation | Auto-build a PO from items below reorder point (qty up to restock level) | RMS+RMH+market | Implemented | Engine ReorderProposalService.cs:29-167 (below-ROP → qty-up-to-restock ReorderQuantity.cs:18-30, primary-supplier grouping, Draft/Submitted/Partial counted as on-order); GET /inventory/reorder-proposal (GetReorderProposalEndpoint.cs:85); POST /purchase-orders/{id}/reorder-fill (FillFromReorderEndpoint.cs:71); POST /purchase-orders/generate (GeneratePurchaseOrdersEndpoint.cs:141); portal worksheet ReorderWorksheetPage.tsx (route + hooks + en/es i18n); 30 integration + 7 unit tests. Live-confirmed 2026-07-19 (proposal returned real data; generate → 201 PO-000012) |
Landed since bdf367d3 (PR #884, #446+#449) — was the prior #1 gap. Roles SysAdmin/Admin/Manager. Fans out one Draft PO per supplier for a single location (multi-location bulk is still PO-F027). Category filter backend-complete, portal picker a documented fast-follow |
| PO-F026 | Backorder / remainder tracking | Track the outstanding un-received quantity on a partially-received PO | RMH+market | Partial | PurchaseOrderLine.QuantityToReceive remainder; PO stays PartiallyReceived until closed |
Open remainder on the PO line IS PO-owned and IS the backorder-equivalent; what's missing is explicit B/O cancel/disposition + a backorder report |
| PO-F028 | Vendor price applied on PO | Auto-fill line cost from the supplier's catalog price | market+RMH | Partial | Reorder path auto-applies server-side: cost fallback ProductSupplier.SupplierCost ?? LocationProduct.Cost ?? ProductVariant.Cost (ReorderProposalService.cs:136-138), flows through fill (FillFromReorderEndpoint.cs:120), seeds the worksheet; tested + live-confirmed (proposal suggestedUnitCost populated) |
Partial, not Implemented: the manual AddPurchaseOrderLineEndpoint still writes req.UnitCost verbatim (no server lookup); the manual drawer pre-fills cost client-side only (lookupSupplierCost.ts). Auto-apply is enforced only in the reorder path |
| PO-F041 | PO header shipping terms | Ship-via / FOB / freight / estimated shipping on the PO header | RMS+RMH | Missing | No ShipVia/FOB/Freight/EstShipping columns on PurchaseOrder (verified) |
Evergreen home: nullable columns directly on PurchaseOrder (not a RMH-style POA_ShipVia side table). Distinct from charge lines (PO-F035) |
| PO-F056 | Supplier minimum-order enforcement | Validate a PO meets the supplier's minimum order before submit | RMH+market | Missing | No min-order check in any PO endpoint (verified); Supplier has no min-order-amount field; ProductSupplier.MinimumOrder (per-item qty floor) is never read by PO handlers |
Two parts: the field → one nullable MinimumOrderAmount column on Supplier (evergreen, not a side table); the enforcement → a PO-level validation. See §5b |
| PO-F027 | Multi-location / master PO (bulk create) | One action that creates POs across several locations (a "master" order fanned out per store) | RMS+RMH+market | Missing | Single LocationId per PurchaseOrder; no bulk/master-create endpoint. Single-location free-choice PO already works (F001/F007) |
The one genuine "central ordering" gap (see §2). Model on the existing Distribution/DistributionAllocation per-location pattern — evergreen, no HQ worksheet tables |
T2 dependencies (excluded from score) — listed as rows for reproducibility:
| ID | Name | Owner module | Status | Note |
|---|---|---|---|---|
| PO-F044 | Multiple suppliers per product + primary | Catalog (ProductSupplier.IsPrimary) |
Dependency | PO reads it; Catalog owns it |
| PO-F045 | Supplier payment terms | Catalog/Supplier (Supplier.Terms) |
Dependency | Stored on Supplier; no PO-side logic |
| PO-F046 | Partial-receipt execution | Receivings | Dependency | The act of receiving; PO only reflects it (PO-F017/F026) |
| PO-F047 | Cost-variance capture on receipt | Receivings | Dependency | Actual vs ordered cost captured on the receipt line |
| PO-F048 | Receive-without-PO | Receivings (ADR-0021) | Dependency | A receipt may exist with no PO |
| PO-F049 | Supplier min-order dollar VALUE (field) | Catalog/Supplier | Dependency | RMH MinOrderValue field. Confirmed absent from QIIUB Supplier. The field is Catalog's; its enforcement on a PO is PO-F056 |
| PO-F057 | Configurable grid columns | @qiiub/ui DataTable (cross-cutting) |
Dependency | Show/hide/reorder/persist columns. Missing repo-wide; would live in the UI library + a user-preference store, not PO |
| PO-F058 | Configurable / custom fields | Platform config (cross-cutting) | Dependency | Field visibility + custom fields on drawers/detail. Missing; a disabled "Custom Fields" tile exists in Settings (catalog-scoped, no backend). PO-side consumption would be PO work once the infra exists |
| ID | Name | Description | Source | Status | Evidence | Notes |
|---|---|---|---|---|---|---|
| PO-F029 | Multi-level approval workflow | Route a PO for approval by spend/role before issue | market | Missing | No approval status/gate in Features/PurchaseOrders; would be a new pre-Submit state + routing rules |
RMS had HQ-worksheet "Approve"; QIIUB has none. Mobile approval is a separate future dep |
| PO-F030 | Landed cost allocation | Spread freight/duty/tax across received lines into unit cost | RMS+market | Dependency | → Receivings RCV-F020 — landed cost is allocated at the receipt (design §10.3 ReceiptCharge on the receipt) |
Fable C-1: was double-scored here + in Receivings. Owned by Receivings; excluded from PO's denominator |
| PO-F031 | Electronic PO transmission (EDI / API) | Send POs to suppliers machine-to-machine (EDI, supplier API, punchout) — channel 3, beyond print/email | market+QIIUB-original | Missing | Contemplated in docs/design/arch-integration-architecture.md §2.1.1 (stakeholder mtg 2026-04-09; PR pharmacy suppliers: catalog-in / PO-out / ASN-in) — exploratory, [DECISION PENDING], no ADR/roadmap/code |
JJ: direct supplier-API integration is a planned QIIUB direction. Distinct from human email (PO-F024). Would pair with electronic catalog-in + ASN → auto-draft-receipt |
| PO-F032 | Three-way match | Match PO ↔ receipt ↔ vendor invoice | market+RMH | Dependency | → AP (future) — 3-way match is an AP invoice-matching control; QuantityInvoiced/QuantityToInvoice reserved on PO + line for it |
Fable m-3: consolidated to AP for cross-doc consistency (Receivings lists it as RCV-D05 dep); excluded from PO's denominator |
| PO-F033 | Supplier qty-break discounts | Discount tiers by quantity/date on supplier cost | RMH | Missing | No discount-schedule model; would extend ProductSupplier/Supplier pricing (evergreen: a child pricing table) |
RMH POC_SupplierDiscount |
| PO-F034 | Time-bound supplier pricing | Supplier item cost valid for a date range / min qty | RMH | Missing | No date-ranged cost; would extend ProductSupplier with effective-from/to columns |
RMH POC_ItemPrice |
| PO-F035 | Non-inventory charges as lines | Add freight/handling as PO charge lines tied to tax/GL | RMH | Missing | No charge-line concept; would add a line "kind" on PurchaseOrderLine or a sibling charge line |
RMH POA_Charge |
| PO-F036 | Matrix item ordering | Order by size/color grid | RMS+RMH | Missing | ProductVariant + Product.BundleType=Matrix exist; the gap is a matrix-grid ordering UI, not backend |
QIIUB has variants; no matrix-grid order screen |
| PO-F037 | Purchaser / buyer tracking | Attribute a PO to a named purchaser/buyer | RMH | Missing | PurchaseOrder.EmployeeId column already exists (reserved); no FK/nav configured, no write path/test |
Not a schema add — just wire the FK + a write path + UI. Closest-to-free of the T3 gaps |
| PO-F038 | Order worksheet w/ KPI panel | Build a PO alongside sales/order history + GMROI/turns | RMS | Missing | Needs sales/inventory read models (GMROI, turns); none built. A reporting/read-side feature | RMS power-user feature |
| PO-F039 | Sales-history PO generation | Auto-build a PO from units sold in a date range | RMS+RMH | Missing | No generation logic; would read sales history (Sales module) + write draft PO lines. Sibling of PO-F025 | Legacy: RMS/RMH "recently sold" requisition; modern POS: "demand-based purchase suggestions" (Stocky). Second generated method (§1c). Only manual is built |
| PO-F040 | Configurable numbering mask | User-configurable per-doc-type prefix/format | RMS+RMH | Missing | No endpoint/UI to configure masks; the seeded DocumentSequence.Prefix/PadLength are the config surface to expose |
Those seeded values are already credited to PO-F014 (Implemented). Configurability is 0% built — crediting the same infra twice would be double-counting, so Missing |
T3 dependencies (excluded from score) — listed as rows for reproducibility:
| ID | Name | Owner module | Status | Note |
|---|---|---|---|---|
| PO-F050 | Mobile PO approval | Mobile / Operations (future) | Dependency | Declared future QIIUB Operations feature |
| PO-F051 | Mobile PO creation | Mobile (future) | Dependency | Square/RMH offer it; QIIUB POS has no PO surface |
| PO-F052 | Cost-update method on receipt | Receivings / Inventory costing | Dependency | Avg/FIFO/last-cost applied when goods are received |
| PO-F053 | Serial / lot capture at receipt | Receivings | Dependency | Serial + lot/expiry captured on the receipt |
| PO-F054 | Vendor invoice / AP posting / GL | AP (future, design §10.1) | Dependency | QuantityInvoiced/QuantityToInvoice reserved for this |
| PO-F055 | Distribution to multiple stores | Distribution (downstream) | Dependency | Allocates received goods to stores |
N/A — not applicable to QIIUB (excluded from score; listed so a reader sees they were considered, not forgotten):
| ID | Name | Why N/A (not Missing) |
|---|---|---|
| PO-F042 | Related / parent-child PO chain | Superseded by architecture. RMS/RMH spawn a child PO for the un-received remainder because their PO is the receipt; QIIUB keeps the remainder open on the same PO and takes multiple receipts (ADR-0021, PO-F026). It's not a gap — the need is met a better way. Scoring it Missing would penalize a deliberate design choice. |
| PO-F043 | HQ / central PO planning & monitoring | Dissolves in the cloud model (see §2). Cross-store creation already works (any-location PO, PO-F001/F007); cross-store visibility is the existing list under merchant-wide RBAC (PO-F016); the worksheet approve→download→convert lifecycle is an RMS/RMH disconnected-DB artifact with no cloud analogue. The only residual real gap — multi-location bulk create — is scored as PO-F027 (T2). |
Evergreen mapping principle. RMH's purchasing schema is fragmented into many custom side-tables (
POA_*,POC_*,POE_*,Worksheet_*) because RMH was bolted onto an existing RMS database — that sprawl is a migration artifact, not a design to copy. QIIUB is greenfield/evergreen with one clean schema, so every "missing field" in this doc maps to a single natural home on an existing QIIUB table (e.g. supplier min-order amount → a column onSupplier; PO shipping terms → columns onPurchaseOrder; buyer → the existingPurchaseOrder.EmployeeId), not a new RMH-style lookup/side table. Recommendations name that home; they do not import RMH's table structure.
Two tables are owned by this module: PurchaseOrder, PurchaseOrderLine. Both extend BaseEntity (BaseImmutableEntity does not exist in the codebase) — so both carry Id, GlobalId, MerchantId, RowVersion, CreatedAtUtc, UpdatedAtUtc, CreatedByUserId, UpdatedByUserId, IsDeleted and are soft-deletable (not financial-immutable). Composite PK (MerchantId, Id) and PublicId varchar(32) unique (MerchantId, PublicId) come from BaseMerchantEntityConfiguration<T>.
Immutability note. POs are
BaseEntity(soft-delete), which is correct: a PO is an editable operational intent, not a posted financial record. Financial immutability (perfeedback_immutability) attaches downstream at the receipt/invoice layer, not here.
PurchaseOrderConfiguration.cs:7-45| Column | SQL type | Null | Business purpose |
|---|---|---|---|
| Id, MerchantId | bigint, int | no | Composite PK; tenancy |
| PublicId | varchar(32) | no | External API identity (prefix po, ADR-0082) |
| DocumentNumber | varchar(20) | no | Human PO number "PO-000142"; unique per merchant |
| SupplierId | bigint | no | FK → Supplier — who we order from |
| LocationId | bigint | no | FK → Location — destination |
| EmployeeId | bigint | yes | Reserved — intended purchaser/buyer (PO-F037); no write path |
| Status | tinyint | no | Lifecycle (PurchaseOrderStatus 0–5) |
| OrderDate | datetime2(3) | no | When the PO was raised |
| ExpectedDate | datetime2(3) | yes | Expected arrival (PO-F019) |
| SubTotal | decimal(18,2) | no | Sum of line totals |
| Notes | nvarchar(500) | yes | Free-text |
| ClosedDate | datetime2(3) | yes | Accountability — when closed |
| ClosedByEmployeeId | bigint | yes | Accountability — who closed |
| QuantityInvoiced | decimal(18,4) | no | Reserved for AP 3-way match (PO-F032); unwired |
| QuantityToInvoice | decimal(18,4) | no | Reserved for AP; unwired |
Indexes: unique (MerchantId, DocumentNumber); (MerchantId, SupplierId, Status); (MerchantId, LocationId, Status); base unique (MerchantId, PublicId) and (MerchantId, GlobalId). FKs (composite, MerchantId-first): Supplier (Restrict), Location (Restrict).
PurchaseOrderLineConfiguration.cs:7-53| Column | SQL type | Null | Business purpose |
|---|---|---|---|
| Id, MerchantId | bigint, int | no | Composite PK; tenancy |
| PublicId | varchar(32) | no | External identity (prefix poln) for nested-route edits |
| PurchaseOrderId | bigint | no | FK → parent PO |
| LineNumber | int | no | Ordinal within the PO |
| ProductVariantId | bigint | no | FK → what is being ordered |
| QuantityOrdered | decimal(10,4) | no | How many ordered |
| QuantityReceived | decimal(10,4) | no | Accumulator, incremented by receipts (RMH POD_OrderEntry pattern) |
| QuantityToReceive | decimal(10,4) | no | Outstanding remainder (backorder-equivalent, PO-F026) |
| QuantityInvoiced | decimal(10,4) | no | Reserved for AP; unwired |
| QuantityToInvoice | decimal(10,4) | no | Reserved for AP; unwired |
| PurchasingUoMId | bigint | yes | FK → UnitOfMeasure (purchase unit) |
| UnitsPerPurchaseUoM | decimal(10,4) | no | Conversion snapshot (default 1) |
| UnitCost | decimal(18,4) | no | Cost per base unit |
| LineTotal | decimal(18,2) | no | Extended cost |
| Notes | nvarchar(200) | yes | Free-text |
Indexes: (MerchantId, PurchaseOrderId, LineNumber); unique (MerchantId, PurchaseOrderId, PublicId) (ADR-0082 nested-route lookup). FKs: PurchaseOrder, ProductVariant, UnitOfMeasure — all Restrict.
Referenced (dependency-owned, not re-specified here): Supplier, SupplierContact, ProductSupplier (+ MinimumOrder added 2026-07-09), DocumentSequence, Location, ProductVariant, UnitOfMeasure, PurchaseReceipt/PurchaseReceiptLine. See the Catalog / Receivings module docs when they exist.
| Surface | Verb+Route or screen | Impl (file) | Realizes | Roles |
|---|---|---|---|---|
| API | POST /purchase-orders |
CreatePurchaseOrderEndpoint | PO-F001, F006, F007, F008, F014 | Admin/Mgr/SysAdmin |
| API | GET /purchase-orders/{PublicId} |
GetPurchaseOrderEndpoint | PO-F015 | " |
| API | GET /purchase-orders |
ListPurchaseOrdersEndpoint | PO-F016 | " |
| API | PUT /purchase-orders/{PublicId} |
UpdatePurchaseOrderEndpoint | PO-F005, F019 | " |
| API | POST /purchase-orders/{PublicId}/submit |
SubmitPurchaseOrderEndpoint | PO-F010 | " |
| API | POST /purchase-orders/{PublicId}/cancel |
CancelPurchaseOrderEndpoint | PO-F011 | " |
| API | POST /purchase-orders/{PublicId}/close |
ClosePurchaseOrderEndpoint | PO-F012 | " |
| API | POST /purchase-orders/{PublicId}/lines |
AddPurchaseOrderLineEndpoint | PO-F002, F008, F020 | " |
| API | PUT /purchase-orders/{poPid}/lines/{PublicId} |
UpdatePurchaseOrderLineEndpoint | PO-F003, F013 | " |
| API | DELETE /purchase-orders/{poPid}/lines/{PublicId} |
RemovePurchaseOrderLineEndpoint | PO-F004 | " |
| API | POST /purchase-orders/{poPid}/lines/batch-delete |
RemovePurchaseOrderLinesEndpoint | PO-F021 | " |
| API | GET /inventory/reorder-proposal |
GetReorderProposalEndpoint | PO-F025 (proposal), F028 (auto-cost), F039 (sold-30d hint) | Admin/Mgr/SysAdmin |
| API | POST /purchase-orders/{PublicId}/reorder-fill |
FillFromReorderEndpoint | PO-F025 (fill a Draft PO) | " |
| API | POST /purchase-orders/generate |
GeneratePurchaseOrdersEndpoint | PO-F025 (generate one PO per supplier) | " |
| portal | Purchase Orders list/detail + Create/Edit drawers, Submit/Cancel/Close modals | apps/qiiub-portal/src/features/purchaseOrders/ |
PO-F022 (renders F001–F021) | merchant Admin/Manager |
| portal | Reorder worksheet (below-ROP by supplier, editable qty/cost, generate) | apps/qiiub-portal/src/features/inventory/routes/ReorderWorksheetPage.tsx |
PO-F025 (UI) | merchant Admin/Manager |
CompletePurchaseReceiptEndpoint in the Receivings module.What the PO endpoints actually enforce today, and what they don't. (Field-level validation is part of what makes T1 features Implemented; the business gaps below are the actionable part — see PO-F056 and the §7 hardening item.)
Present — field-level (FluentValidation, per endpoint): PublicId prefix checks (po_/poln_/supp_/loc_/pvar_); max-lengths matching DB columns (Notes 500/200); decimal precision/scale + sign bounds via DecimalBounds (QuantityOrdered/UnitsPerPurchaseUoM > 0 as decimal(10,4); UnitCost ≥ 0 as decimal(18,4)); list not-empty, distinct, and ≤100-cap on batch delete; list-query Page/PageSize/SortBy whitelist/SortDirection guards.
Present — business-rule (in handlers): Draft-only mutability guard (5 endpoints → 409); empty-PO rejection on submit (400); atomic status-transition predicates (Submit/Cancel/Close only flip from the allowed source state → 409 otherwise); Close blocks if draft receipts pending (400); route-forgery / line-ownership checks on line edit/delete (404); QuantityReceived > 0 edit/delete block (409); supplier/location/variant/UoM existence resolution (400); duplicate DocumentNumber unique-index violation → 409; RowVersion optimistic-concurrency conflict → 409.
Absent — business validations (verified not present in Features/PurchaseOrders/):
| Missing validation | Effect today | Tracked as |
|---|---|---|
| Supplier minimum-order enforcement | A PO under the supplier's minimum can be submitted | PO-F056 (scored) + Supplier field gap (PO-F049 dep) |
| Duplicate product-line prevention | Two lines for the same ProductVariantId on one PO are allowed |
§7 validation-hardening |
| Supplier-is-active check | A PO can be raised against an inactive supplier (Supplier.IsActive never checked) |
§7 validation-hardening |
| Location-is-active check | Same, for the destination location | §7 validation-hardening |
| Expected-date sanity (not in past) | ExpectedDate accepted with no date validation |
§7 validation-hardening |
| Non-zero total guard | An all-zero-cost PO is legal (only per-field bounds exist) | §7 validation-hardening |
| Credit / payment-terms check | Supplier.Terms never read; no credit logic |
out of scope (AP dep) |
Scoring note: only the supplier-minimum validation is scored as its own feature (PO-F056), because JJ raised it specifically and it maps to a concrete RMH capability. The remaining validation gaps are documented here and bundled as one §7 hardening recommendation rather than split into micro-features — splitting them would pad the denominator and distort the tiers (anti-gaming guard b).
A second axis, orthogonal to the score. The §6 % says "how complete vs a mature POS"; this says "how trustworthy is the built part." A feature is Implemented on behavior + evidence — testedness never enters the §6 statusValue (mixing them would silently move the yardstick). T1 reads 100%, but this is where you see how well-guarded that 100% is.
Inventory (at e70ff195, method counts): 120 integration (Inventory/PurchaseOrderTests.cs core lifecycle + PurchaseOrders/{GetReorderProposal,FillFromReorder,GeneratePurchaseOrders,ListPurchaseOrdersAggregates}Tests.cs) · 15 unit (PurchaseOrders/ReorderQuantityTests.cs, DevSeed/S14_PurchaseOrdersSeedTests.cs) · 0 e2e. Of these, reorder-engine-specific = 30 integration + 7 unit (added by PR #884). Counts are context; the signal is the flow matrix below.
Critical-flow coverage (flow granularity, not per-feature):
| Flow | Tested |
|---|---|
| Create Draft (valid, column-max, empty-lines, wrong-prefix) | ✓ |
| Submit Draft→Submitted (+ empty-PO 400, already-submitted 409) | ✓ |
| Cancel Draft/Submitted→Cancelled (+ already-cancelled 409, 404) | ✓ |
| Close FullyReceived→Closed (+ not-fully-received 409, ClosedBy stamp) | ✓ |
| Line add/edit/remove + batch-delete (Draft-only 409, QtyReceived>0 block, route-forgery) | ✓ |
| List filter/sort/paginate + StatCard aggregates (cross-page, cross-merchant) | ✓ |
| Cross-merchant isolation (create/edit/line/batch) | ✓ |
| Receipt-complete → PO status recalculation (E2E with Receipts) | ✓ |
| Validation bounds (decimal precision/scale, max-lengths, prefixes) | ✓ |
| Reorder proposal (below-ROP grouped by supplier, cost fallback, sold-30d hint, cross-merchant) | ✓ (15 tests + live 2026-07-19) |
| Fill-from-reorder into a Draft PO (Draft-only, skip-existing, cross-merchant) | ✓ (6 tests) |
| Generate POs per supplier from the worksheet (+ merge open, cross-merchant 400/404) | ✓ (9 tests + live: generate → PO-000012) |
| Reorder quantity formula (restock − onHand − onOrder, min-order floor) | ✓ (7 unit) |
| Re-close an already-Closed PO | ✗ |
| Cancel a Partially/FullyReceived PO (409 predicate) | ✗ (inferred from code, not asserted) |
| PublicId / DocumentNumber collision race | ✗ |
Named test gaps (→ bundled into §7 as one "test hardening" row, mirroring §5b): the three ✗ above, plus the reserved-and-unwired fields (EmployeeId, QuantityInvoiced, QuantityToInvoice) which have neither write path nor test. All low-risk, but they are the honest edges of the "T1 100%" claim.
Live confirmation (2026-07-19). Beyond the suite, F025 + the lifecycle were driven against the running app (API dotnet run + portal, local SQL): the reorder proposal returned a real below-ROP worksheet (5 suppliers; suggestedUnitCost auto-filled — confirms F028; soldLast30Days display-only — confirms F039); POST /purchase-orders/generate created PO-000012 (Draft, $14,724.32, 2 lines); submit flipped it to Submitted; the portal rendered the PO list/detail and the populated Reorder worksheet ($176,397.54 across 5 suppliers, editable qty/cost, Unassigned bucket). Screenshots in scratch. This is the confidence axis's strongest tier — live-driven, not just asserted.
Formula (fixed, from template §6): tier weights T1×3, T2×2, T3×1; statusValue Implemented=1.0, Partial=0.5, Missing=0.0; Dependency excluded from numerator and denominator.
| Tier | Implemented | Partial | Missing | Dep-excluded | Scored count | Σ statusValue | Tier % |
|---|---|---|---|---|---|---|---|
| T1 (×3) | 18 | 0 | 0 | 0 | 18 | 18.0 | 100.0% |
| T2 (×2) | 4 | 3 | 5 | 8 | 12 | 5.5 | 45.8% |
| T3 (×1) | 0 | 0 | 10 | 8 | 10 | 0.0 | 0.0% |
T2 scored (12): Implemented F020/F021/F022/F025; Partial F019/F026/F028; Missing F023/F024/F027/F041/F056. → 4×1 + 3×0.5 + 5×0 = 5.5 / 12 = 45.8%. T3 scored (10): all Missing (F029, F031, F033–F040; F030 landed cost + F032 three-way match now Dependency per Fable). → 0.0 / 10 = 0.0%. Excluded: 16
Dependencyrows (incl. F030 landed cost → Receivings, F032 three-way match → AP, per Fable) + 2N/A(F042 superseded, F043 dissolved into RBAC).
Overall weighted:
numerator = 3×18.0 + 2×5.5 + 1×0.0 = 54 + 11 + 0 = 65.0
denominator = 3×18 + 2×12 + 1×10 = 54 + 24 + 10 = 88
overall% = 65.0 / 88 = 73.9%
Interpretation. The PO module is operationally complete at its core (T1 100%): a merchant can fully create, issue, track, and close purchase orders with tenant isolation, numbering, and lifecycle guards, all tested. The reorder proposal engine (F025, PR #884) now also auto-drafts POs from below-reorder-point stock — the prior #1 gap, live-confirmed 2026-07-19. What's still missing is breadth, not foundation — standard-market conveniences (printing, emailing, header shipping terms, min-order validation, multi-location bulk create — T2 46%) and the advanced/ERP tier (approval workflows, EDI, matrix ordering, configurable numbering — T3 0%; landed cost + three-way match are now Receivings/AP dependencies). ~74% is a healthy signal for an early-phase POS, up from 69% (reorder engine shipped + landed-cost/3-way-match reclassified to their owning modules).
Sensitivity / anti-gaming notes:
Missing (which lowered T2), while F043 (central planning) became N/A (dissolved into RBAC + the existing list). Net vs the prior draft: ~70% → ~69%. Changes were driven by evidence, not by the number.N/A, not Missing — a superseded legacy mechanism and a capability that already exists via RBAC are not gaps; scoring them Missing would understate the module by penalizing deliberate design. Listed in the N/A block so the reader sees they were considered.Dependency/N/A used only where genuinely warranted (16 dep + 2 N/A, each justified); (d) every Implemented carries file:line/endpoint evidence, every Partial names what's missing; (e) enumeration reconciled in §9; (f) no feature scored here is scored in a neighbor's doc — corrected by the Fable Receivings review (landed cost PO-F030 → Receivings, three-way match PO-F032 → AP).| Rank | Gap | Tier | Impact | Recommendation | ADR? |
|---|---|---|---|---|---|
| 1 | PO-F023 PO print/PDF confirmation | T2 | High — merchants cannot hand/send a formal PO document; blocks real supplier workflow | Implement ADR-0070 Phase 1 (GET /purchase-orders/{id}/print, QuestPDF). ADR already proposed. |
Promote ADR-0070 to accepted (exists) |
| 2 | PO-F024 Email PO to vendor | T2 | High — pairs with #1; the actual "send to supplier" step | Add email dispatch of the PO PDF (reuse existing SMTP infra) | Likely no — extends within model |
| ✓ | T2 | SHIPPED (PR #884) — reorder proposal engine + worksheet + fill + generate; live-confirmed 2026-07-19. No longer a gap; was the #1. | — (done) | — | |
| 4 | PO-F019 Expected-date tracking | T2 | Medium — field exists but no overdue visibility | Add overdue/ETA surfacing in list + detail; optional per-line ETA | No — over existing schema |
| 5 | PO-F040 Configurable numbering mask | T3 | Medium — infra exists, not user-editable | Expose a DocumentSequence config endpoint/UI |
No — over existing schema |
| — | Validation hardening (§5b) | T2/T3 | Medium — data integrity | Add supplier-minimum (PO-F056), duplicate-line, active-supplier/location, expected-date sanity, non-zero-total checks | Mostly no — over existing schema (min-order needs a Supplier field) |
| — | Test hardening (§5c) | — | Low — confidence | Assert the 3 untested negative-transition/race flows; wire or remove the reserved EmployeeId/QuantityInvoiced/QuantityToInvoice fields |
No ADR |
| — | Multi-location "master" PO bulk create | T2 | Medium (chains/franchises) | One action → POs for several locations; model on the existing Distribution/DistributionAllocation pattern. (Central ordering otherwise already works via RBAC — no separate module.) |
Likely new ADR (a new endpoint/shape) |
| — | PO-F029 approval workflow | T3 | Low now | Defer (Operations mobile). (Landed cost PO-F030 and 3-way match PO-F032 are Dependencies → Receivings / AP, not PO gaps — see §2 / §9.) | requires new ADR |
Quick wins (over existing schema, likely no ADR): #2 email, #4 expected-date tracking, #5 numbering mask, wiring EmployeeId for purchaser tracking (PO-F037).
Net-new (new tables/services/ADRs): multi-store PO, approval workflow (PO-F029). (Landed cost PO-F030 and three-way match PO-F032 are Dependencies → Receivings / AP — not PO net-new; see §2 / §9.) (Auto-reorder — the former marquee net-new — shipped in PR #884.)
Point-in-time snapshot (GHES
Qiiub/qiiub, queried 2026-07-27; 103 open issues). In the promoted framework this is generated by the render tool at build time (GHES query bypart:*+ feature-ID convention), never hand-maintained — closing an issue updates the badge on the next render, like the staleness stamp. Shown here as a point-in-time demo.— untracked= a gap with no open issue (a prompt for a human to file one; the scorecard flags, it does not auto-create).
| Gap (ID) | Tracked by (open GHES issue) |
|---|---|
| PO-F056 supplier minimum-order | #902 "Add supplier minimum order amount ($) to Supplier" |
| One-open-Draft-per-PO DB guard (§5b) | #808 "Race-proof one-open-Draft-per-PO via filtered unique index" |
| PO-F024 email | rides the email outbox being built — #990 (migrate IEmailService onto the outbox) + #986–989 (Resend hardening) |
| Reorder → Reports integration (F025 shipped) | #955 "Route Reports 'Low Stock / Reorder' order qty through the reorder engine" (adjacent) |
| Copy / Duplicate PO follow-ups (shipped feature) | #980 "copy-PO (#972) review follow-ups", #1000 "action-column convention (Duplicate PO)" |
| PO-F023 print, F019 expected-date, F040 numbering-mask, F027 multi-location bulk, PO-level tax | — untracked |
Read: the highest-impact PO gap (min-order F056) is ticketed (#902), and the email path is actively being built; the remaining breadth (print, expected-date, masks, bulk) is untracked.
Contradictions found (repo cross-check):
docs/PROGRESS.md:40-41 attributes the inventory backend to "Phase 5" (90 endpoints/128 tests/22 entities); docs/adr/0081-...:14 says the same numbers shipped in "Phase 6." One is wrong about the phase number. Recommend: reconcile in a docs PR (not this module's concern to fix).:40-41) vs unrelated portal-UI "Phase 5" (:272, :326, :335). Search-ambiguity hazard.PO→Receipt→Distribution first-class, but ADR-0081's four sidebar entries omit Distribution and don't list it as future. Features/Distributions/ has only 4 endpoints (no Update/Cancel); no portal folder. The Phase-5 "pipeline complete" claim holds only at the API layer. Downstream of PO — noted, not scored here.proposed, not shipped — no QuestPDF in src/. Do not cite PO print as existing.inventory-supply-chain.md frozen at "Draft" since 2026-03-13 despite governing an accepted, shipped module and being extended by later ADRs (0070/0081/0102). Of its §10 recommendations, Auto-Reorder (§10.2) has since shipped (PR #884, with its own design spec + impl plan) — so the design doc's "no automated PO generation logic is designed" line is now stale. Vendor Invoice (§10.1) and Landed Cost (§10.3) remain un-designed with no forward pointer.Decisions needed from a human before gaps close:
Distribution/DistributionAllocation) and whether finer per-location purchasing RBAC is ever wanted (additive, not required today).Supplier min-order field), duplicate-line prevention, active-supplier/location checks, expected-date sanity.EmployeeId, QuantityInvoiced, QuantityToInvoice fields' intended owners (Operations mobile / AP) so they're not mistaken for dead columns.Every line item in the RMS/RMH inventory (artifact-phase3) and the market scan (artifact-phase4) mapped to a disposition, so the denominator cannot be inflated by silent omission (template §6 guard e). scored = own §3 row; folded = subsumed by another ID (not double-counted); N/A = out of scope with reason.
| Source feature (RMS/RMH/market) | Disposition |
|---|---|
| Manual PO creation | scored PO-F001 |
| PO gen from reorder-point / min-max | scored PO-F025 |
| PO gen from items sold / sales history | scored PO-F039 |
| Requisition method: Manual/blank | scored PO-F001 (the only built method) |
| Requisition method: Fixed Quantity | folded into the generated methods (PO-F025/F039); a minor variant, was central-only in RMS/RMH |
| Generation item scope (RMS/RMH dept/category/supplier/filter) | folded into PO-F025/F039; in QIIUB maps to hierarchical categories + supplier + search, not RMH's split |
| Supplier selection strategy (primary vs lowest cost) | folded into PO-F025 |
| PO Planner / HQ-generated worksheets | N/A — the approve→download→convert worksheet lifecycle is an RMS/RMH disconnected-DB artifact; no cloud analogue (§2). Central creation = existing any-location PO |
| Master PO / consolidated multi-store | scored PO-F027 (multi-location bulk create — back in PO scope, not a separate module) |
| Distribution-site / warehouse consolidation | N/A — Distribution dep (PO-F055) |
| Order worksheets / suggested qty | folded into PO-F025 + PO-F038 (KPI worksheet) |
| Item ordering info (Reorder No/Min Order/MPQ/Cost per supplier) | N/A — Catalog dep (PO-F044/F049) |
| Reorder Point / Restock Level per store | N/A — Inventory dep (LocationProduct) |
| Multiple suppliers per item + primary | scored PO-F044 (dependency) |
| Supplier min-order dollar VALUE | field → dep PO-F049 (Catalog); PO-level enforcement → scored PO-F056 |
| Supplier payment terms | scored PO-F045 (dependency) |
| Supplier lead-time / ship-via / FOB / freight | scored PO-F041 (PO-header side); supplier-side is dep |
| Supplier qty-break discount schedules | scored PO-F033 |
| Time-bound supplier item pricing | scored PO-F034 |
| "Do Not Order" item flag | N/A — Catalog product flag dep |
| Purchaser / buyer tracking | scored PO-F037 |
| Requisitioner / "Confirming to" free-text | folded into PO-F037 (managed vs free-text variant) |
| PO number auto-gen | scored PO-F014 |
| Custom numbering mask | scored PO-F040 |
| Purchase Unit of Measure | scored PO-F020 |
| Non-inventory Charges as PO lines | scored PO-F035 |
| Est. shipping/other fees (header) vs actual (receipt) | header → PO-F041; actual → dep PO-F047 |
| Landed cost calculation on receipt | dep PO-F030 → Receivings RCV-F020 (landed cost owned at the receipt, Fable); receipt-side costing → dep PO-F052 |
| Cost distribution methods (Qty/Value/Manual) | folded into PO-F030 (dep → Receivings RCV-F020) |
| Cost update method on receipt (None/Last/Weighted Avg) | scored PO-F052 (dependency) |
| Auto-update supplier cost/tax on receipt | N/A — Catalog/Receivings dep |
| Last cost / Replacement cost on item | N/A — Catalog/Inventory dep |
| PO/Invoice GL account mapping | scored PO-F054 (AP dependency) |
| Formal Invoice / Credit Note posting | scored PO-F054 (AP dependency) |
| PO lifecycle / statuses | scored PO-F009 |
| Placement status / issuing a PO | scored PO-F010 |
| Editing rules after issue / release lock | scored PO-F013 |
| Related PO / parent-child chain | N/A — superseded (PO-F042, see N/A block) |
| Partial shipment / partial receiving | dep PO-F046; PO-side remainder → PO-F026 |
| Backorder handling on completion | scored PO-F026 |
| Receive All / quick full receipt | N/A — Receivings dep |
| Commit vs Save on receiving | N/A — Receivings dep |
| Serialized item receiving | scored PO-F053 (dependency) |
| Lot number tracking on receipt | folded into PO-F053 (dependency) |
| Matrix item ordering (size/color grid) | scored PO-F036 |
| Lot Matrix items (bulk pack sizes) | folded into PO-F036 / N/A Catalog product-type |
| Item Assembly / Kit purchasing | N/A — Catalog product-type; no PO-specific behavior |
| PO printing | scored PO-F023 |
| PO emailing | scored PO-F024 |
| PO export to file/Word/Excel | folded into PO-F023 |
| Customizable print templates | folded into PO-F023 (ADR-0070) |
| PO template variables (customization SDK) | N/A — RMS SDK concept; QIIUB has no template-var SDK |
| SavePurchaseOrder hook (customization SDK) | N/A — RMS SDK concept; QIIUB uses sync/domain events |
| Approval workflow for HQ PO worksheets | scored PO-F029 (approval itself); the HQ-worksheet framing is N/A in cloud (PO-F043) |
| Release / auto-release step | folded into PO-F010 (submit/issue) |
| EDI / electronic ordering | scored PO-F031 |
| Import Orders from legacy | N/A — ADR-0038: POs explicitly NOT imported |
Order/qty discrepancy tracking (DiscrepancyStatus) |
folded into PO-F026 (remainder/variance) |
| Inventory Transfer as parallel order type | N/A — separate Transfers module (own definition doc) |
| Central PO/receiving status monitoring | covered by PO-F016 (list/filter) under merchant-wide RBAC — cross-store visibility already works |
| KPI / Order History / Sales History panel | scored PO-F038 (a store-level PO-building worksheet) |
| Three-way match (market) | dep PO-F032 → AP (future) |
| Expected-date / ETA tracking (market) | scored PO-F019 |
| Mobile PO approval / creation (market) | scored PO-F050 / PO-F051 (dependencies) |
| Vendor catalogs / price lists (market) | scored PO-F028 (+ dep PO-F044) |
| Configurable grid columns (market/UX) | N/A — cross-cutting @qiiub/ui DataTable (dep PO-F057) |
| Configurable / custom fields (market/UX) | N/A — cross-cutting platform config (dep PO-F058) |
No source line is left unreconciled.
WIDS365 sweep (2026-07-26 — retroactive 4th source, artifact-po-wids365-sweep.md). WIDS365 (a BCPOS production POS) was added as a source after this scorecard's original RMS/RMH + market pass. Verdict: it adds no confident new denominator row and changes no status — ≈74% stands. WIDS365 has a native PO layer (Wids365-DB: POHeader/PODetail + web_POHeaderUpsert/POClose/PORecalcQtyRcvd/SeqDocNumber + PurchaseOrderController), but every capability maps to an existing PO-Fxxx: header/line CRUD (F001–F005/F021), supplier+store (F006/F007), totals (F008), lifecycle/close (F009/F012), numbering (F014), list/detail (F015/F016), receipt-driven status (F017), QtyRTD remainder (F026), RequiredDate (F019), Requisitioner/ConfirmingTo (F037), print report (F023).
Supplier has no min-order or lead-time field → reinforces PO-F056/F049/F041/F040/F029.POHeader.TotalTax/PODetail.ItemTaxID) — QIIUB PO has no tax column; most consistent home is Receivings/AP (matches the PO-F030 landed-cost dependency + the "tax on receipt → N/A" disposition). If JJ prefers a PO row: PO-F059, T3, Missing → ≈74%→≈73.5% (den +1, num +0). (2) Back-create PO from a posted receiving (web_POInsertFromReceiving) — a Receivings convenience under ADR-0021 receive-without-PO (PO-F048 dep), not a PO-authoring gap. Guard (e) now spans four source families: RMS/RMH + market + QIIUB-original + WIDS365.Plain-language definitions for terms that can't be fully simplified. Kept short on purpose.
| Term | In plain words |
|---|---|
| PO (Purchase Order) | The order you send a supplier saying "I want to buy these items at these prices." |
| Line / PO line | One row on the order: a product, a quantity, a cost. |
| Draft / Submitted / Received / Closed | The stages a PO goes through: being written → sent to the supplier → goods arrived → finished. |
| Receipt (Receivings) | A separate document that records goods physically arriving. One PO can have several receipts (partial deliveries). |
| Distribution | A separate document that splits received goods out to several stores. |
| Reorder point / Restock level | The low-stock trigger and the target level — "when I drop to X, order back up to Y." |
| Backorder / remainder | The part of an order that hasn't arrived yet and is still owed. |
| UoM (Unit of Measure) | The unit you buy in (case, box) vs. sell in (each). |
| SKU / variant | A specific sellable version of a product (size/color); QIIUB calls it a ProductVariant. |
| Supplier / vendor | Who you buy from. (QIIUB uses "Supplier".) |
| Min-order amount | A dollar floor a supplier requires before they'll take an order. |
| Landed cost | The true cost of an item after adding freight, duty, and taxes on top of the item price. |
| Three-way match | An accounting check that the PO, the receipt, and the supplier's invoice all agree before paying. |
| AP (Accounts Payable) | The "money we owe suppliers" side of accounting — a future QIIUB module. |
| GL (General Ledger) | The master accounting record; "GL posting" = writing a transaction into it. |
| EDI / supplier API / punchout | Computer-to-computer ways to send orders straight into a supplier's system (no email/PDF). "Punchout" is one such method. |
| ASN | Advance Ship Notice — an electronic "here's what's on the way" message a supplier sends before delivery. |
| GMROI / turns | Retail metrics: profit per dollar of inventory, and how fast stock sells through. |
| MPQ | Master Pack Quantity — how many units come in one supplier pack/case. |
| FOB / ship-via | Shipping terms on an order: who pays freight from what point, and the carrier. |
| Master PO / multi-location | One action that creates orders for several stores at once. |
| Reorder / requisition generation | Letting the system draft the PO for you (from low-stock or sales history) instead of typing every line. |
| Tier (T1/T2/T3) | How essential a feature is: T1 = can't work without it, T2 = market expects it, T3 = advanced/ERP nice-to-have. |
| Dependency | A feature that really belongs to a neighbor module (e.g. Receivings), so it's not counted in this module's %. |
| N/A (in this doc) | A legacy feature that doesn't apply to QIIUB (superseded by our design), so it's not a gap and not counted. |
| Evergreen | QIIUB is one clean, modern schema built fresh — not carrying old tables forward. So a missing field gets one natural column, not a legacy-style side table. |
| RBAC | Role-based access control — what a user can do based on their role (Admin, Manager, Cashier…). |
| RLS | Row-level security — the database itself blocks one merchant from seeing another's rows. |
| MerchantId / tenant | Each business (merchant) is a separate tenant; every row is tagged with its MerchantId so data never crosses. |
| PublicId | The safe public identifier used in the API/URLs (e.g. po_…), instead of the internal database number. |
Commit (e70ff195) |
The exact saved snapshot of the code this analysis measured (re-scored here from the original bdf367d3). Check out that commit and you see precisely what we saw. |
| ADR | Architecture Decision Record — a short doc in the repo that records a design decision and why. |
| PO Planner (RMS/RMH) | Legacy central-office ordering tool. In QIIUB this is not a separate thing — it's just a PO created by a head-office user (see §2). |
| Master / origin PO chain | Legacy links between a parent PO and spun-off child POs. QIIUB doesn't need it (we keep the remainder on one PO). |