PRD: Purchase Orders
| Module | Inventory | PRD ID | PRD-PO-001 |
| Status | Superseded | FEAT | PO |
| Epic | — | Plane | — |
| Date | 2026-07-13 | Version | v1.0 |
| Packages | @nx/inventory | URD | PO |
| Surface | Client · Owner/Mgr | ||
| Owner | Phát Nguyễn | ||
What a purchase order is
A merchant orders from a vendor with a purchase order raised on the Client app: one vendor for the whole order, one inventory location that will receive the goods, and a list of items with quantity, unit price and purchase unit. Several merchants sit under one organizer.
The purchase order is the document behind every arrival of goods from outside. It is also the only place inventory valuation is born.
The four numbers in stock
Lifted from Stock locations & levels so there is no need to open another PRD. Every item at every location carries four numbers:
| Number | What the system calls it | Meaning |
|---|---|---|
| On-hand | on-hand | Goods physically sitting in the location |
| Reserved | reserved | Already promised to another document, no longer sellable to anyone else |
| Available | available | The part still sellable |
| Incoming | incoming | Left the source location, not yet arrived at the destination |
The invariant, true at every moment:
Available = On-hand - ReservedGoods ordered from a vendor but not yet delivered are in none of these four numbers. Incoming only counts goods in transit between two locations inside the system.
Why every arrival needs a document behind it
Without a purchase order, every stock increase is a hand-typed number, and nobody can trace back why stock went up, or at what cost.
Goods arriving in batches is an ordinary day. You order 100 cases, the vendor drops 60 this morning and promises the rest next week. A system that demands a full receipt before it records anything gets worked around: staff type a number that fits and fix it later, or abandon the feature. So a purchase order is receivable in parts, and a short delivery is an ordinary event rather than an error.
One purchase order, end to end
Order 70 cases of milk at VND 50,000 a case. The location is empty. The vendor delivers in two batches, and the second batch is 5 cases over, at a price that has gone up.
| What happens | Ordered | Cumulatively received | On-hand | Inventory valuation |
|---|---|---|---|---|
| Send the order to the vendor | 70 | 0 | 0 | none yet |
| Batch 1: 50 cases at VND 50,000 | 70 | 50 | 50 | VND 50,000 |
| Batch 2: 25 cases at VND 62,000 | 70 | 75 | 75 | VND 54,000 |
- Goods are sellable the moment they land. The 50 cases of the first batch sell immediately, with no wait for the order to complete.
- The second batch delivers 25 cases while only 20 were outstanding. The 5 surplus cases are still recorded: the goods are in the location, so the books have to say so.
- Inventory valuation is blended by quantity weight: (50 × 50,000 + 25 × 62,000) divided by 75 is VND 54,000. Valuation only changes at receipt.
- Every line has now received its full ordered quantity, so the order moves to
Completedon its own, with nothing to click.
The easiest thing to get wrong
A Completed order cannot be cancelled
The goods are already in the location. Cancelling does not pull them back off the shelf, so cancelling a finished order is a meaningless action.
To send goods back to the vendor, raise a vendor-return ticket. A correction is a new document, never a state change on a document that is already closed.
Cancel only applies to an order that is still live: Draft, Sent, Received. And cancelling a partially received order does not reverse the stock already recorded: the goods that arrived stay in stock and the ledger lines stand untouched.
1. Goals & Non-goals
Goals
- Raise a purchase order against one vendor, declare lines with quantity, unit price and purchase unit, and send the order out.
- Suggest the price and purchase unit per line from the vendor-item purchasing record, so nothing has to be retyped on every order.
- Receive partially, across several batches, accumulating into each line's cumulative received quantity.
- Have every receipt write an inventory ledger line, raise the receiving location's on-hand in base units, and recompute the item's weighted moving average cost.
- Write the price and unit just received back onto the vendor-item record.
- Show quantity and price variance warnings on the order without blocking the receipt.
- Anchor the vendor payable to the quantity received, not the quantity ordered.
Non-goals
- Three-way matching against the vendor's real invoice and posting the difference through the Finance module. The purchase order only records the variance as an observable number.
- Landed-cost allocation (freight, import duty) into each line's inventory valuation.
- Capturing lot, expiry and serial at the moment of receipt, which belongs to Lot, expiry & picking strategy.
- Automatic purchase suggestions from a reorder point, which belongs to Replenishment.
- Returning goods to the vendor.
2. Success metrics
| Metric | Target / signal |
|---|---|
| Documented inbound | The bulk of inbound stock arrives through a purchase order rather than a manual stock adjustment |
| Multi-batch receipts | The number of orders received in two or more batches climbs steadily, the sign that merchants use partial receipts instead of typing a number that fits |
| Valuation accuracy | The average cost after each receipt matches a hand calculation on sampled orders |
| Less typing | Most lines keep the price suggested from the vendor record without an edit |
| Useful warnings | Owners open the price and quantity variance warnings rather than dismissing them wholesale |
3. Personas & scenarios
| Persona | Goal in this feature |
|---|---|
| Owner / Manager | Order from the right vendor at the agreed price, and see which orders arrived in full and which are still short |
| Stock keeper | Receive what physically arrives, type the number actually counted off the truck, and not be blocked when the vendor delivers short |
| Accountant / Owner | Know exactly how much is owed to the vendor for the goods actually received |
Core scenario: the owner picks a vendor and a receiving location, adds lines whose prices are already suggested, and sends the order → the vendor delivers the first batch → the stock keeper opens the order, types the quantity actually received per line, hits Receive → stock rises immediately, the average cost is blended again, and the price just paid is written back onto the vendor record → the order is still short, so it stays open, waiting for the next batch.
4. User stories
| # | As a | I want | So that |
|---|---|---|---|
| 01 | owner | to pick a vendor and then add lines whose prices are already filled in | I can order quickly without remembering last time's price |
| 02 | owner | the lines locked once the order has gone to the vendor | nobody quietly edits an order that is already committed |
| 03 | stock keeper | to type the number I actually counted off the truck, even when it is less than what was ordered | the books match the shelf |
| 04 | stock keeper | to receive a second batch against the same order | I do not have to raise a new order for the goods that arrive later |
| 05 | owner | a warning when goods arrive short or at a different price | I call the vendor back instead of being blocked by the system |
| 06 | owner | the average cost blended again whenever goods arrive at a new price | the profit report reflects the real purchase price |
| 07 | owner | the payable computed from what actually arrived | I do not pay for goods that never came |
5. Functional requirements
| # | Requirement | Status | URD ref |
|---|---|---|---|
FR-001 | A purchase order moves through an ordered lifecycle, and every step is a state shown on the order. • Draft - the order is still being written and has not gone to the vendor.• Sent - the order is committed and out with the vendor, waiting for the goods.• Received - at least one batch has arrived but no line is complete yet, and the order stays open for more.• Completed - the order moves here on its own the moment every line has received its full ordered quantity; the owner can also hit Complete by hand when goods arrived short and there is no point waiting.• Closed - the order is settled for bookkeeping and takes no further action.• Cancelled - the order is abandoned and can no longer receive goods. | 🔶 | URD-PO-001 |
FR-002 | An order can be cancelled at the states before the goods have fully arrived, but never from Completed.• Cancel is available from Draft, Sent and Received (partially received).• Completed, Closed and Cancelled cannot be cancelled. Once the goods have fully arrived, the order is done.• To send goods back to the vendor, raise a vendor-return ticket, not a cancellation of the old order. A correction is a new document, never a state change on a document that is already closed. • Cancelling a partially received order does not reverse the stock already recorded: the goods that arrived stay in stock and the ledger lines stand untouched. On a cancelled order the Receive button disappears. | 🔶 | URD-PO-001 |
FR-003 | Lines can only be added, edited or removed while the order is a Draft.• From Sent onward every line is locked: the item, quantity, unit price and unit can no longer change.• Changing the content of an order that has already gone out means cancelling it and raising a new one. | 🔶 | URD-PO-002 |
FR-004 | Raising a purchase order declares a header that applies to the whole order. • Vendor (required, one vendor for the whole order). • Receiving location (required, defaulting to the merchant's default location). • Order date (required, defaulting to today) and expected delivery date (optional). • Currency (defaulting to VND) and payment terms (optional: cash on delivery, prepaid, net N days, end of month). • Order name and note are optional. The order code is generated by the system and is unique within the merchant. | 🔶 | URD-PO-003 |
FR-005 | Each line of the order declares one item to buy. • Item (required): a product variant or a material. • Ordered quantity (required, greater than 0).• Purchase unit (required): case, six-pack, kg, piece. The system works out how many base units that is, the user never types a conversion factor. • Unit price per purchase unit (required). Line tax and line discount are optional. • The line total is computed by the system and cannot be typed by hand. | 🔶 | URD-PO-003 |
FR-006 | Picking an item onto an order that already has a vendor pre-fills the purchasing data of that vendor-item pair. • The unit price and purchase unit come from the last saved purchase price. • The minimum order quantity and the pack size are shown, so the owner can see whether they are ordering below the minimum. • Every suggested value stays editable before the line is saved. • A vendor-item pair with no data yet leaves the fields empty for the owner to fill in. | 🚧 | URD-PO-004 |
FR-007 | Receiving supports partial receipts across several batches against the same purchase order. • Each receipt records the quantity actually received per line in that batch. Not every line has to be received. • The quantity just received is added to the line's cumulative received quantity. • Receiving more than the ordered quantity is still recorded, the same as a stock transfer. When the vendor over-delivers the goods are already in the location; blocking it at the form only stops the stock keeper recording the truth. • The excess is shown on the purchase order detail screen, so the person who raised the order can reconcile it. | 🔶 | URD-PO-005 |
FR-008 | Every receipt writes straight into the receiving location's stock. • The item's on-hand at the receiving location rises by exactly the quantity received, converted to base units. • Goods are sellable the moment they land, with no wait for the order to complete. • One inventory ledger line is written per item, carrying the purchase-receipt reason and pointing back at the source purchase order. | 🔶 | URD-PO-006 · URD-LDG-001 |
FR-009 | Receiving recomputes the item's weighted moving average (AVCO) cost. • The new cost is the old stock value plus the value of the batch just received, divided by the total quantity after the receipt. • Inventory valuation is a single number for the whole merchant, never split per location. • If stock before the receipt is 0 or negative, the price just paid replaces the old cost outright. | 🚧 | URD-PO-008 · URD-VAL-005 |
FR-010 | Receiving writes the price just paid back onto the vendor-item record. • The unit price, unit of measure, order timestamp and receipt timestamp of this purchase are stored as the last purchase price. • The next purchase order for the same vendor-item pair suggests exactly that number. | 🔶 | URD-PO-007 · URD-VEN-004 |
FR-011 | The purchase order screen warns when the quantity or price actually received differs from what was ordered, and never blocks the receipt. • Quantity variance: the cumulative received quantity differs from the line's ordered quantity, shown on that line together with the shortfall or the excess. • Price variance: the unit price on the order differs from the price actually paid, shown as the difference per unit. • The warning is information the owner uses to call the vendor back, not an error that has to be cleared before anything else can happen. | 🚧 | URD-PO-009 |
FR-012 | Receiving short against the ordered quantity blocks nothing and forces no closure. • The order stays open after a short receipt, and keeps accumulating into the cumulative received quantity whenever the vendor delivers again. • The system does not generate a separate "shortfall" document for what never arrived. • A permanent shortfall, once the vendor stops delivering, is not settled by editing the old order. The merchant raises a stock count to bring stock back to reality. | 🚧 | URD-PO-013 |
FR-013 | The order's vendor payable is computed from the cumulative received quantity. • The amount owed is anchored to each line's cumulative received quantity, not to the ordered quantity. • Goods that have not arrived carry no payable yet. A later batch adds exactly what was just received to the order's payable. | 🚧 | URD-PO-010 |
FR-014 | A vendor invoice arriving after the receipt date with a different price is only recorded as a variance on the order. • The variance shows on the order as a price warning. • The inventory valuation of issues that already happened stays as it was and is never recomputed. | 🚧 | URD-PO-011 · URD-VAL-010 |
FR-015 | The merchant runs the whole purchase order lifecycle on the Client app. • The list screen has columns for order code, vendor, receiving location, order date, expected delivery date, total and state. It filters by state and vendor. • The detail screen shows each line with three numbers: ordered, cumulatively received, remaining. • The Receive button opens a form for typing this batch's actual quantities, and only appears when the order is in a state that allows receiving. | 🚧 | URD-PO-001 · URD-PO-005 |
FR-016 | Purchase orders are per-merchant data. • Every order and line is only visible inside the user's own merchant. • Deletion is a soft delete and is restorable. | 🔶 | URD-PO-012 |
5.1 Acceptance criteria
- Create a new purchase order, pick a vendor and a receiving location, add two lines, save → the order is a
Draft, has a unique order code, and its total equals the sum of the two line totals. - While in
Draft, edit the quantity on one line and delete the other → both actions succeed and the total updates accordingly. - Send the order, then reopen the detail screen → the state is
Sent, the line fields are no longer editable, and the add-line button is gone. - An order at
Sentwith no receipt yet, hit Cancel → the order moves toCancelledand the Receive button disappears. - A partially received order (60 of 100 units), hit Cancel → the order moves to
Cancelled; the 60 units received stay in stock and the ledger line of that receipt is not reversed. - An order already at
Completed, open the detail screen → there is no Cancel button; to send goods back, raise a vendor-return ticket. - An order already at
Closed, open the detail screen → there is no Cancel button. - Pick an item previously bought from the selected vendor → the unit price and purchase unit fill in from the last purchase price, with the minimum order quantity shown alongside; edit the unit price by hand and save → the line keeps the edited price.
- Pick an item never bought from that vendor → the unit price and purchase unit stay empty, with no error.
- An order for 100 units, receive 60 in the first batch → the line shows ordered 100, received 60, remaining 40; the order moves to
Receivedand stays open. - Receive a second batch of 40 units against the same order → the cumulative received quantity is 100 and the remaining is 0; every line is now complete, so the order moves to
Completedon its own with no further click. - An order already at
Completed, whether it moved there on its own or the owner hit Complete after a short delivery, open the detail screen → the Receive button is gone. - From
Completedhit Close → the order moves toClosed; receiving, editing and cancelling are all switched off, and the order is read-only. - An order for 70 units with 50 already received, type an actual quantity of 25 for this batch in the receiving form → the system records it, the cumulative received quantity becomes 75, and the excess of 5 units is shown on the order detail screen.
- Receive 60 cases of an item declared as 1 case = 24 cans → the item's on-hand at the receiving location rises by 1,440 cans; the item's inventory ledger shows a new line carrying the purchase-receipt reason and pointing at that purchase order code.
- Immediately after the receipt, sell that item on the POS → the sale goes through, with no wait for the order to complete.
- An item holding 100 units at a cost of VND 10,000, receive another 100 units at VND 12,000 → the item's average cost becomes VND 11,000.
- An item at zero stock, receive 50 units at VND 12,000 → the average cost becomes VND 12,000, with no divide-by-zero error.
- Receive a vendor-item pair at a unit price of VND 55,000 → reopening that item's vendor data shows a last purchase price of VND 55,000 with the new receipt timestamp; raise a new order for the same pair → the unit price fills in at VND 55,000.
- An order for 100 units at VND 50,000, receive 95 units at VND 52,000 → the receipt succeeds; the order screen warns about the 5 missing units and about the price being VND 2,000 per unit higher.
- An order received at 95 of 100 units, open the order's payable → the amount is computed from the 95 units received, not the 100 ordered.
- The vendor says the rest is not coming → the order stays at
Received, the system neither closes it nor generates any "shortfall" document; the merchant raises a stock count to bring stock back to reality. - A vendor invoice arrives later stating a higher price than the receipt → the order screen shows a price variance warning; reopen the orders already sold before that → the inventory valuation recorded on them is unchanged.
- Open the purchase order list and filter by state
Received→ only orders with at least one receipt appear, each row showing the vendor, receiving location and total. - Delete a purchase order → it disappears from the list but is still restorable; log in as a different merchant → the order is not visible.
6. Non-functional requirements
| Aspect | Requirement |
|---|---|
| Stock integrity | One receipt raises stock exactly once; the same batch submitted repeatedly still writes one ledger line and adds to stock once |
| Data integrity | The cumulative received quantity and the stock level are raised in one atomic update, so two people receiving at the same time on two machines cannot skew the number |
| Traceability | Every receipt leaves an inventory ledger line pointing at the purchase order code and line code, still readable months later |
| Point-in-time snapshot | The ledger line records the unit of measure, conversion factor and inventory valuation as they were at the moment of receipt; a later change to a unit or a price never rewrites history |
| Merchant scoping | Every order and line is confined to the user's own merchant, soft-deleted and restorable |
| Precision | Money and quantities are computed to 4 decimal places |
| i18n | State labels and display labels are bilingual |
7. UX & flows
Every purchase order screen lives in the Client app, aimed at owners and stock keepers. Purchase orders have no Back Office screen.
8. Data & domain
| Concept | Role |
|---|---|
| Purchase order | The ordering document: one vendor, one receiving location, order date, expected delivery date, currency, payment terms, state |
| Purchase order line | One item to buy: ordered quantity, unit price, purchase unit, tax, discount, cumulative received quantity |
| Vendor | The party selling to the merchant, picked once for the whole order |
| Vendor-item record | The purchasing data of one vendor and item pair: last purchase price, minimum order quantity, expected lead time, pack size |
| Receipt batch | One physical arrival of goods: the quantity actually received per line, accumulated into the cumulative received quantity |
| Inventory ledger line | An append-only record of every stock change, carrying the purchase-receipt reason and pointing back at the source order |
| Inventory valuation | The item's average cost, blended again on every receipt |
| Reconciliation warning | An observable number on the order: ordered versus received quantity, ordered price versus price actually paid |
Conceptual level only - the full data model lives in the inventory domain model.
9. Dependencies & assumptions
Depends on
| # | Feature | What it depends on |
|---|---|---|
| 01 | VEN Vendors | the order picks one vendor, and its lines take their suggested price from the vendor-item record. |
| 02 | LOC Inventory locations | the order declares the location that will receive the goods; every merchant always has a default location. |
| 03 | UOM Units of measure & conversion | the system converts the purchase unit into base units, the user never types a factor. |
| 04 | STK Stock items & levels | receiving raises on-hand at the receiving location. |
| 05 | LDG Inventory ledger | every receipt writes one ledger line. |
| 06 | VAL Inventory valuation | receiving is the only event that changes the average cost. |
| 07 | CNT Stock count | where the merchant settles a permanent shortfall. |
Assumptions
| # | Assumption | What breaks if it is wrong |
|---|---|---|
| 01 | One purchase order buys from one vendor and delivers to one location. | An order touching several vendors or several receiving locations no longer traces the payable and the valuation back to a single source, and the header model in this version has nowhere to declare it. |
| 02 | A vendor delivering short is an ordinary event, not an error to block. | Block a short receipt and the stock keeper types a number that fits and corrects it later, so stock diverges from the shelf from the very first arrival. |
| 03 | Merchants accept inventory valuation as a single number for the whole merchant, not split per location. | A merchant buying into several locations at different prices sees the wrong gross margin per location; the way out is the per-location costing switch in Inventory valuation, and that is a one-way decision. |
| 04 | The vendor's paper invoice arrives after the receipt date, and may state a different price than the one paid at receipt. | An invoice arriving before the receipt leaves the price variance warning with nothing to compare against, and the valuation still has to be fixed at the price paid on receipt rather than the price on the invoice. |
10. Release plan & criteria
| Aspect | Plan |
|---|---|
| Phase | P2 (Inventory) - see the URD feature catalog |
| Rollout | Every merchant; no feature flag |
| Migration | No legacy data to convert; purchase orders are new documents raised from the day the feature opens |
| Release criteria | Raising, sending, receiving two batches and cancelling an order all work end to end on the Client app; stock and inventory valuation after a receipt match a hand calculation on sampled orders; neither a short receipt nor an over-receipt blocks anything |
| Monitoring | Orders received in more than one batch, the share of lines keeping the suggested price, the gap between system inventory valuation and a hand calculation |
References
- URD: Purchase Orders · Vendors · Inventory Valuation · Inventory ledger
- Related: Stock Count · Units of measure & conversion · Stock items & levels
- Module: Inventory - URD
- Developer: @nx/inventory · domain model
Risks & open questions
| # | Risk / question | Mitigation / status |
|---|---|---|
| 01 | The same receipt batch is clicked twice or reprocessed, raising stock twice | A unique constraint on the inventory ledger line's business key; one batch can only write one line per item |
| 02 | Short orders staying open forever, growing the order list without end | An order received in full moves to Completed on its own so nothing hangs; a short order carries a quantity variance warning that shows what is still outstanding, and the owner hits Complete once there is no point waiting |
| 03 | Owners not understanding why a shortfall has to be settled with a stock count instead of an edit to the order | The order screen explains it right there in the quantity variance warning, with a shortcut into raising a stock count |
| 04 | Receiving an item that is already at negative stock, producing a meaningless average cost | When stock before the receipt is zero or negative, the price just paid replaces the old cost outright (FR-009) |
| 05 | An over-receipt against the ordered quantity inflates stock for no visible reason | The quantity actually received is recorded as it is (FR-007); the excess is shown on the order detail screen with a quantity variance warning (FR-011), and is reconciled with a stock count |
| 06 | Freight and import duty are not allocated into inventory valuation, understating the cost of imported goods | Open: decide whether landed-cost allocation belongs in a later increment |
| 07 | Capturing lot and expiry right at the receiving form is the natural place for it, but it sits in another feature's scope | Open: wire the receiving form into lot and expiry capture once LSE is ready |
FAQ
| # | Question | Answer |
|---|---|---|
| 01 | The vendor delivered short, what do I have to do? | Nothing at all. Type the quantity that actually arrived and the order stays open. When they deliver the rest tomorrow, reopen the same order and receive again; the received quantity keeps accumulating. |
| 02 | If they never deliver the rest, does the shortfall hang on the order forever? | The order stays open, and the quantity variance warning shows you which orders are still short. Hit Complete to settle it once you stop waiting. Stock is brought back to reality with a stock count, not by editing the old order. |
| 03 | When does an order move to Completed on its own? | The moment the last receipt brings every line up to its ordered quantity. You do not click anything. You only hit Complete by hand when goods arrived short and you are not waiting any longer. |
| 04 | Can I receive more than I ordered? | Yes. When the vendor over-delivers the goods are already in the location, and the books have to say so. The excess is shown on the order detail screen with a quantity variance warning, so you can call the vendor back. |
| 05 | Can an order still be cancelled after a partial receipt? | Yes, as long as it is not Completed. Cancelling never pulls goods back out of the location: what arrived stays in stock and the ledger stands untouched. A Completed order cannot be cancelled - the goods have fully arrived; to send them back, raise a vendor-return ticket. |
| 06 | Are goods sellable as soon as they arrive? | Immediately. Stock rises with every receipt, with no wait for the order to complete. |
| 07 | Why can I not edit lines after sending the order? | Because a sent order is a commitment to the vendor. To change it, cancel the order and raise a new one. |
| 08 | When does my inventory valuation change? | Only at receipt. Every time goods arrive at a new price, the system blends that price into the old average cost weighted by quantity. Moving goods between two locations never changes valuation. |
| 09 | Does receiving at a price other than the one ordered get blocked? | No. The system records the price actually paid and shows a price variance warning on the order so you can call the vendor back. |
| 10 | If the vendor's invoice arrives later with a different price, is inventory valuation recomputed? | Not for issues that already happened. The difference is only recorded on the purchase order as an observable number. |