PRD: Purchase Orders
| Module | Inventory (CORE-06) | PRD ID | PRD-PO-001 |
| Status | Shipped | Owner | Inventory squad |
| Date | 2026-02-24 | Version | v1.0 |
| Packages | @nx/inventory · @nx/core · @nx/finance · apps/client | URD | PO · POI |
TL;DR
Lets a merchant record what they buy from a vendor as a purchase order (PO), drive it through a clear lifecycle, and receive the goods into stock with a verifiable audit trail. Receipt increases stock at a location, writes immutable movement records, and can post a finance transaction so a purchase shows up in the books. The result: every stock increase is tied back to a vendor, a price, and a discount/tax breakdown - no more ad-hoc, untraceable stock-ins.
1. Context & Problem
Merchants restock by buying goods from vendors, and those goods must land in inventory with a paper trail an owner can audit. Without a purchase-order document, stock can only be moved through ad-hoc tickets: a stock increase cannot be tied back to a vendor, a unit price, or a discount/tax breakdown, and there is no controlled state between "ordered" and "received". This makes cost tracking, vendor reconciliation, and tax reporting unreliable - a hard blocker for the HKD/SME bookkeeping KICKO targets.
This increment builds the PO workflow on top of the existing vendor directory and inventory stock/tracking primitives, and connects goods receipt to the finance layer.
2. Goals & Non-Goals
Goals
- A PO document owned by a vendor, created in a single operation (vendor + line items; vendor required).
- A controlled lifecycle with line edits allowed only while drafting.
- Goods receipt that increases stock and writes immutable movement records, supporting full and incremental receipts.
- Accurate line and PO totals with explicit manual-vs-system discount/tax.
- Finance linkage: a received PO can post a finance transaction with an optional payment.
Non-Goals
- Opening-balance / migration stock import - owned by Opening Balance Import.
- Multi-currency stock valuation.
- A submit-to-vendor / external approval gate.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Traceability | 100% of vendor stock-ins flow through a PO (not ad-hoc tickets) |
| Receipt accuracy | Stock-on-hand after receipt = ordered/received qty; zero stock rows without a matching movement record |
| Finance coverage | Received POs that post a finance transaction (where a cost is recorded) |
| Cycle time | Median time DRAFT → RECEIVED per merchant trends down |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner | Control purchasing, see cost & vendor history, reconcile with finance |
| Inventory staff | Create POs, edit lines, receive goods into the right location |
| Accountant (via Finance) | See purchases posted as finance transactions |
Core scenarios: create a PO for a vendor → adjust lines while drafting → move to processing → receive goods (full or partial) → stock rises with an audit trail and an optional finance posting → close the PO.
5. User Stories
- As inventory staff, I want to create a PO against a vendor with its line items in one step, so the order is tied to a supplier and a price.
- As inventory staff, I want to edit PO lines while the PO is in DRAFT, so I can correct quantities and prices before committing.
- As inventory staff, I want to receive goods against a PO, so stock increases at the chosen location with a movement record.
- As inventory staff, I want to receive incrementally or replace a received quantity, so partial deliveries are handled correctly.
- As an owner, I want a received PO to record a finance transaction (optionally with a payment), so purchases reflect in the books.
- As an owner, I want to cancel a non-terminal PO, so mistaken orders don't pollute stock or finance.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Create a PO owned by a vendor (vendor required) with line items in one aggregate operation | URD-PO-001..003 |
| FR-2 | Lifecycle DRAFT → PROCESSING → RECEIVED → COMPLETED → CLOSED; cancel from any non-terminal state; PROCESSING → DRAFT revert | URD-PO-004..006 |
| FR-3 | Line items editable only in DRAFT; aggregate create/edit (PO + lines) in a single call | URD-PO-007 · URD-POI-001 |
| FR-4 | Goods receipt increments stock at the location and writes immutable movement records | URD-PO-008..009 |
| FR-5 | Two receipt modes: OVERRIDE (replace received qty) and ACCUMULATIVE (add to received qty) | URD-PO-010 |
| FR-6 | Per-line total = price × qty × multiplier + tax − discount; lines accumulate by (itemType, itemId, uom); a zeroed line is soft-deleted | URD-POI-002..004 |
| FR-7 | PO total = subtotal − discount + tax; discount/tax accept a manual override, flagged manual-vs-system | URD-PO-011 |
| FR-8 | inventoryLocationId optional; defaults to the merchant's default location | URD-POI-005 |
| FR-9 | A received PO can record a finance transaction with an optional payment and a default finance category | URD-PO-012 |
| FR-10 | A unique PO number is assigned per merchant | URD-PO-002 |
Full requirement text and acceptance criteria live in the Inventory URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | Stock increment and its movement record are written together - no stock change without a matching immutable audit entry |
| Immutability | Movement records are append-only; corrections happen via new entries, never edits |
| Tenancy & authz | All operations scoped per merchant (x-merchant-id); gated by inventory permissions |
| Precision | Monetary/quantity math uses float(value, 4) |
| Consistency | Aggregate create/edit and receive are transactional; partial failures don't leave half-written POs |
| i18n | User-facing labels/statuses are bilingual ({ en, vi }) |
8. UX & Flows
Key screens (in apps/client): PO list, PO create, PO edit, the status-flow control, and a cancel-order confirmation.
9. Data & Domain
| Entity | Role |
|---|---|
PurchaseOrder | The order document - vendor, status, totals, location, optional financeTransaction relation |
PurchaseOrderItem | A line - item ref (itemType, itemId, uom), qty, price, multiplier, tax, discount, line total |
| PO config | Per-merchant PO settings, seeded at startup |
| Movement record | Immutable stock-movement (tracking) entry written on receipt |
Conceptual only - full schema and invariants in the inventory domain model.
10. Dependencies & Assumptions
Depends on
- Vendors (URD-VEN) - a PO requires an existing vendor.
- Stock levels & movement audit (URD-STK · URD-TRK) - receipt builds on stock and tracking primitives.
- Inventory locations (URD-LOC) - stock lands at a location; a merchant default must exist.
- Finance (
@nx/finance) - for the optional transaction/payment posting.
Assumptions
- The merchant has at least one vendor and a default inventory location.
- A finance category exists to classify the purchase posting.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Receipt and stock write could diverge on partial failure | Written transactionally; stock has no entry without a movement record |
| No multi-currency support | Out of scope; document as a constraint for cross-border vendors |
| No approval gate before processing | Accepted for SME scale; revisit if enterprise approval is required |
| Reverting a received PO vs. already-posted finance | Open: define the reversal/compensation path for finance entries |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (foundation) - see URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | None (new entities; PO config seeded at startup) |
| Launch criteria | Create→receive→stock+movement verified end-to-end; finance posting verified; totals match expected math |
| Monitoring | PO volume per merchant, receive error rate, stock-vs-movement consistency checks |
13. FAQ
Can a PO be changed after it leaves DRAFT? No - line items are editable only in DRAFT. Use PROCESSING → DRAFT revert to make changes, or cancel.
OVERRIDE vs ACCUMULATIVE receipt - what's the difference? OVERRIDE replaces the received quantity on the line; ACCUMULATIVE adds to it (for deliveries that arrive in multiple drops).
Does receiving a PO pay the vendor? Not automatically - a received PO can record a finance transaction with an optional payment. Recording a cost and paying it are separate.
What if no location is given? The PO defaults to the merchant's default inventory location.
Can I import opening stock through a PO? No - opening balances are handled by the separate Opening Balance Import feature; there is no ghost-vendor workaround.
References
- URD: Inventory - Purchase Orders · Purchase Order Items
- Builds on: Vendors · Stock Levels · Movement Audit Trail
- Related PRD: Opening Balance Import
- Module: Inventory - overview + traceability
- Developer: @nx/inventory · domain model