PRD: Production Orders
| Module | Inventory | PRD ID | PRD-MFG-001 |
| Status | No item | FEAT | MFG |
| Epic | — | Plane | — |
| Date | 2026-07-13 | Version | v1.0 |
| Packages | @nx/inventory | URD | MFG |
| Surface | Client · Owner/Mgr | ||
| Owner | Phát Nguyễn | ||
What a production order is
A production order (Production Order) is the document of one batch. The merchant owner opens it on the Client app: pick the finished good to make, the bill of materials to follow, the inventory location (InventoryLocation) that does the work, and the planned quantity.
An order only ever runs inside one merchant (Merchant). It ties the two ends together: the materials going out of stock and the finished goods coming into stock, one document, one batch.
The four stock numbers
Lifted from Stock Items & Levels so you do not have 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 at the location |
| Reserved | reserved | Already promised to another document, no longer sellable to anyone else |
| Available | available | The part still on offer |
| Incoming | incoming | Already left the source location, not yet arrived at the destination |
The invariant, true at every moment:
Available = On hand - ReservedStarting a run pushes materials from available into reserved. On hand only drops at completion.
Why a batch needs a document of its own
Some things a merchant does not buy to resell as is, it has to make them: a tray of bread, a pot of broth, a run of bottled sauce. This is nothing like a drink assembled at the counter. A counter drink explodes its recipe the moment the customer orders, gets handed straight over, and the finished item never sits in stock. A tray of bread is made ahead, a hundred at a time, and those hundred loaves live in stock as an item with stock of its own, with a lot number and an expiry date.
Without a production order the merchant is left keying it in by hand: write off the materials with one inventory ticket, add the finished goods with another, and remember on their own that the two tickets belong to the same batch. The books come apart. Nobody can trace how much flour those 100 loaves ate, how many loaves a batch came up short, or which batch the bread in the case came from and how long it is still good for.
A tray of bread from start to finish
Order "Bread batch 001": 100 loaves at the Central Kitchen. The recipe calls for 5kg of flour and 2kg of sugar per 100 loaves. The location holds 6kg of flour. Bread declares a 3-day shelf life. The kitchen gets 98 loaves out of the oven.
| What happens | Flour on hand | Flour reserved | Flour available | Bread on hand |
|---|---|---|---|---|
| Open the order for 100 loaves, the order sits in Draft | 6 | 0 | 6 | 0 |
| Start | 6 | 5 | 1 | 0 |
| Complete, declaring 98 loaves produced | 1 | 0 | 1 | 98 |
- Start only reserves the flour. On hand does not change, and the ledger has no line yet.
- Complete is when the flour leaves the location for real, and when the new bread lot enters stock with an expiry of today plus 3 days.
- Planned 100, actual 98, so a shortfall of 2 is recorded on the order.
- When the recipe nests a semi-finished item that has stock of its own, the order reserves that item itself and never drills down to the materials that made it.
The easiest thing to get wrong
Start RESERVES, it does not DEDUCT
Hit Start: 5kg of flour moves to reserved for the order, available drops immediately, and no sale order and no other production order can touch that slice again. But on hand does not drop by a single gram, and the inventory ledger gets no line at all.
Writing the ledger is the job of Complete, when the operator declares what the batch actually consumed. Deducting early at Start means cancelling an in-progress order has to add the goods back into stock, and the ledger ends up with an out-then-in pair of entries for a batch that never ran.
Consequence: cancelling an in-progress order only releases what was reserved, available goes straight back up, and the ledger stays clean, with no line for that order.
1. Goals & Non-goals
Goals
- Let the merchant owner open, edit, follow and close a production order across its whole lifecycle on the Client app.
- Enforce the draft → in progress → done or cancelled order of the lifecycle, with no path that jumps straight from draft to done.
- Wire the production order into real stock: starting a run reserves the materials for the batch, completing it moves the materials out and the finished goods in, cancelling part-way releases whatever was still held.
- Stamp a lot number and an expiry date on the finished lot that just came out, so goods a merchant makes themselves are tracked for expiry exactly like goods they buy in.
- Record the actual output and the shortfall of every batch.
Non-goals
- Production scheduling, capacity planning, batching by shift - a production order in this release is a single standalone order, not a scheduler.
- Labour, machine time and processing cost rolled into the finished good's inventory valuation - finished-good valuation is out of scope here, see Inventory Valuation.
- Partial production (receiving half the batch when only half is out of the oven) - one order completes once.
- BOM explosion at the point of sale for counter-assembled items - that belongs to Bill of Materials and never goes through a production order.
- Accounting entries for the shortfall - the shortfall is only a number you can observe on the order and in the inventory ledger.
2. Success metrics
| Metric | Target / signal |
|---|---|
| Books that close | Every batch produced traces back to exactly the material it ate, through a single order |
| Correct lifecycle | No order sits in done without having passed through in progress |
| Expiry on self-made goods | Finished goods made in-house carry a lot number and an expiry just like purchased goods, not a "date unknown" pile |
| Visible shortfall | The owner opens an old order and immediately sees how short that batch ran; the shortfall rate falls as the kitchen gets better |
3. Personas & scenarios
| Persona | Goal in this feature |
|---|---|
| Merchant owner (Owner) | Know how much material each batch ate, how short it ran, and which batch the stock on the shelf came from |
| Manager | Open an order per batch, hit Start when the kitchen begins, declare the output when the batch is done |
| Head chef / operator | Read the order and know how many to make, which BOM to follow, at which location |
Core scenario: the manager opens the Production Orders screen on the Client app, creates an order "Bread batch 001" for the finished good Bread, picks the BOM, picks the Central Kitchen location, sets a planned quantity of 100 loaves → the kitchen gets going, the manager hits Start, 5kg of flour and 2kg of sugar are held for this batch and nobody else can take them → the bread comes out, 98 loaves counted, the manager hits Complete and declares 98 → 5kg of flour and 2kg of sugar leave stock for real, Bread on hand at the Central Kitchen goes up by 98 carrying a fresh lot number and an expiry 3 days from today, and a shortfall of 2 is recorded on the order.
4. User stories
| # | As a | I want | So that |
|---|---|---|---|
| 01 | manager | to open a production order for a batch | both the kitchen and the books know what this batch is making, how much, and to which BOM |
| 02 | manager | one Start button to reserve the materials per the BOM and write them off automatically when the batch is done | I never key in ticket lines by hand |
| 03 | manager | the system to tell me right away when the location is short on a material | I find out before the kitchen starts and not half-way through |
| 04 | manager | to declare the quantity that actually came out when I complete the order | finished-good stock is the real number and not the planned one |
| 05 | merchant owner | the lot we produce ourselves to carry its own lot number and expiry | it sells in the right order and nothing expired goes out the door |
| 06 | merchant owner | to see the shortfall of every batch | I know whether the BOM matches reality and whether the kitchen is consistent |
| 07 | manager | to cancel an in-progress order without losing the material I reserved | an abandoned batch does not wreck my stock figures |
5. Functional requirements
| # | Requirement | Status | URD ref |
|---|---|---|---|
FR-001 | A production order is one document, and five fields make it saveable. • The finished good to make: a product variant or a material, required. • The BOM to follow: exactly one BOM per order, required. • The location that produces it: materials come out here and the finished goods go back in here, required. • Planned quantity and its unit, required. • A note from whoever opened it, optional. | 🔶 | URD-MFG-002 |
FR-002 | Every production order carries an order code unique within the merchant, used to name that batch everywhere else: the inventory ledger, the finished lot, the reservation breakdown screen. | 🔶 | URD-MFG-002 · URD-LDG-004 |
FR-003 | A production order passes through exactly four states, in one direction only. • Draft: everything is editable, nothing has touched stock yet. • In progress: materials are reserved for the order, the ledger has not been written. The BOM, the location and the planned quantity can no longer be changed. • Done: materials have left stock, finished goods have entered stock, the order is closed. • Cancelled: the order stopped part-way and is closed. | 🔶 | URD-MFG-001 |
FR-004 | Only three transitions are legal, everything else is rejected. • Draft → In progress, with the Start button. • In progress → Done, with the Complete button. • Draft or In progress → Cancelled, with the Cancel button. • Jumping straight from Draft to Done is rejected. An order already Done or Cancelled can no longer change state. | 🚧 | URD-MFG-001 |
FR-005 | Only two kinds of finished good can carry a production order. • A product variant of the manufactured kind: an item with finished-goods stock of its own, whose BOM only explodes when an order runs. • A material of the semi-finished kind: something you make first and then use to make something else, such as a house-made sauce. • A counter-assembled item is blocked because its BOM explodes at the point of sale. A plain stocked item is blocked because selling one simply takes one off its own stock. • The gate is the item kind, not whether the item happens to have a BOM attached. | 🚧 | URD-BOM-005 · URD-BOM-002 · URD-MAT-001 |
FR-006 | Hitting Start reserves the materials for the order without writing the ledger. • The system explodes the BOM against the planned quantity, stopping at every component that has stock of its own, and reserves against that component's own stock. When the BOM nests a semi-finished item that tracks stock, the order reserves that item itself and never drills down to the materials that were used to make it. • Each material moves to reserved against this very order. Available drops immediately and no other sale order or production order can take that slice. On hand does not change, because the ledger is only written at completion. • If any material is short, the whole order is rejected, with a clear message naming which material is short by how much, and not a single stock row is touched. • Materials with the allow-oversell flag on are let through even when there is not enough on hand. | 🚧 | URD-MFG-003 · URD-BOM-009 · URD-RSV-003 |
FR-007 | The material reserved for a production order is tagged with that exact order. The reservation breakdown for an item therefore opens up: which sale order holds how much, which stock transfer holds how much, which production order holds how much. | 🚧 | URD-RSV-004 · URD-RSV-005 |
FR-008 | Lot-tracked materials are written off following the picking strategy configured for that material, earliest expiry first or earliest received first. The operator never has to pick a lot by hand. Which lots get consumed is settled at completion, when the material actually leaves stock. | 🚧 | URD-LSE-007 |
FR-009 | Hitting Complete moves the materials out of stock for real and raises finished-good stock by exactly the quantity the operator declared. • Materials are written off against the actual consumption the operator declares, not against the amount that was reserved. Reserved goes back to 0 and on hand drops by exactly what was really used. Reserve more than used and the surplus is released; use more than reserved and the difference is deducted on top. • Actual consumption is pre-filled with the reserved amount and can be edited (see FR-016). • The actual finished quantity is required and is pre-filled with the planned quantity. • Finished-good stock goes up at the order's own location, never a different one. | 🚧 | URD-MFG-004 |
FR-010 | Completing an order stamps a lot number and an expiry date on the finished lot that just came out. • The lot number is generated for this batch alone. The operator can edit it before confirming the completion. • The expiry date is the completion date plus the shelf-life days declared on the finished good. A finished good that declares no shelf-life days produces a lot with no expiry. • The lot enters stock like any other lot, so self-made goods are also picked earliest-expiry-first. | 🚧 | URD-MFG-005 · URD-LSE-002 |
FR-011 | Completing an order records the actual output and the shortfall of the batch. • Shortfall is the planned quantity minus the actual quantity. If more came out than planned, the shortfall is 0. • All three numbers - planned, actual and shortfall - show on the order detail screen after it closes, so they can be read back later. | 🚧 | URD-MFG-006 |
FR-012 | Cancelling an order gives back exactly what it took, depending on where the order was. • Cancelling a Draft order simply closes it. Stock does not change, because it was never touched. • Cancelling an In-progress order releases the material that was reserved but never written off, and available goes straight back up. The ledger has no line, because the material was never written off. | 🚧 | URD-MFG-007 |
FR-013 | Every stock change caused by a production order leaves a line in the inventory ledger. • Material leaving stock at completion writes a line with the production-consumption reason. Starting a run only reserves stock, so there is no ledger line yet. • Finished goods entering stock at completion write a line with the production-receipt reason. • Each line points back to the exact production order code that caused it, with the quantity before, the change and the quantity after. | 🚧 | URD-LDG-001 · URD-LDG-003 · URD-LDG-004 |
FR-014 | The Production Orders screen on the Client app lists the orders of the user's own merchant. • Columns: order code, finished good, producing location, planned quantity, actual quantity, state, created date. • Filter by state and by location. • Open an order for detail: the material list from the exploded BOM, the start and completion timestamps, the finished lot that came out of it. | 🚧 | URD-MFG-008 |
FR-015 | Finished-good cost is divided by the actual yield, never by the planned quantity. • The cost of one unit of finished good equals the total cost of the materials actually consumed, divided by the actual quantity produced. • Plan for 50 litres and only 47 litres come out after evaporation: divide by 47. Dividing by 50 reports a cost cheaper than reality. • This cost is the receiving cost of the finished lot that just came out, and it is the value drawn down when a dish that uses that semi-finished good is sold. | 🚧 | URD-MFG-009 |
FR-016 | Actual material consumption is allowed to deviate from the BOM quantities, warned but never blocked. • The operator declares the actual amount of material used for the batch, which can deviate from the BOM quantity times the planned quantity. • A deviation past the threshold shows a warning right on the form, and the order can still complete. • That deviation is exactly what a merchant needs to see to know how much the kitchen is wasting. Locking it hard to the BOM quantity would throw away the whole point of this screen. | 🚧 | URD-MFG-009 |
5.1 Acceptance criteria
- Open the Production Orders screen, create an order for the finished good Bread, pick the BOM, pick the Central Kitchen location, set a planned quantity of 100 loaves → the order saves as Draft with a unique order code, and not a single stock number has changed.
- Open an order in Draft → the Start and Cancel buttons are there, the Complete button is not.
- Try to push a Draft order straight to Done → the system rejects it, the order stays in Draft.
- Open an order that is Done → no state-change button remains; the same goes for a Cancelled order.
- Create an order for a plain stocked item or a counter-assembled item → the system will not let you pick that item as the finished good of a production order.
- Create an order for a material of the semi-finished kind (house-made sauce) → the system lets you pick it as the finished good.
- The BOM calls for 5kg of flour and 2kg of sugar per 100 loaves; the Central Kitchen has 6kg of flour and 3kg of sugar → hit Start → the order moves to In progress, flour shows 5kg reserved with 1kg available, sugar shows 2kg reserved with 1kg available, and on hand is still 6kg of flour and 3kg of sugar.
- Same order, but the location only has 3kg of flour left → hit Start → the system rejects it, reports a 2kg shortage of flour, the order stays in Draft, and neither the flour nor the sugar is reserved or written off by a gram.
- Same order, sugar has allow-oversell on and its on hand is 0 → hit Start → the order runs, sugar's available goes to minus 2kg, and the flour is reserved as usual.
- The BOM nests a semi-finished item that tracks stock (house-made sauce, 3 litres on hand) → hit Start → the system reserves against the sauce's own stock, and the materials used to cook the sauce (tomatoes, oil) are not reserved - they were already deducted at the production order that cooked the sauce.
- The same BOM, but the sauce is declared as a prepared item with no stock of its own → hit Start → the system continues one level further and reserves the tomatoes and the oil at their BOM quantities.
- Open the stock view for flour while the order is running → the reserved figure opens up and shows a "Production order MBA-001" line with the reserved quantity.
- Flour has two lots, lot A expiring 2026-07-20 and lot B expiring 2026-07-25, and the picking strategy is earliest expiry first → hit Complete → lot A is drained first before lot B is touched.
- An in-progress order, the finished good Bread declares 3 shelf-life days → hit Complete and declare 98 loaves produced → the reserved 5kg of flour and 2kg of sugar are written off, flour on hand is 1kg and sugar on hand is 1kg, reserved is back to 0; Bread on hand at the Central Kitchen goes up by 98, and the new lot carries an expiry of today plus 3 days.
- At that same completion step, edit the suggested lot number to the merchant's own code → confirm → the finished lot carries the edited code.
- Complete with 100 planned and 98 actual → the order detail screen shows planned 100, actual 98, shortfall 2.
- Complete with 100 planned and 103 actual → the shortfall shows 0 and finished-good stock goes up by 103.
- Complete an order for a finished good that declares no shelf-life days → the finished lot is still created with a lot number and an empty expiry.
- Cancel an order in Draft → the order closes and stock does not change.
- Cancel an In-progress order → the material that was reserved but never written off is released, the available figure for that material goes straight back up, on hand does not change, the inventory ledger has no line for this order, and the order moves to Cancelled.
- Open the flour's inventory ledger after completing an order → there is one outbound line with the production-consumption reason, pointing at the right order code, with the quantity before, the amount written off and the quantity after.
- Open the Bread's inventory ledger after completion → there is one inbound line with the production-receipt reason, pointing at that same order code.
- Open the Production Orders screen, filter by state In progress and location Central Kitchen → the list shows only in-progress orders at that location, and none from another merchant.
6. Non-functional requirements
| Aspect | Requirement |
|---|---|
| Ordering under concurrent clicks | If two people hit Start on the same order, exactly one transition succeeds and the material is reserved exactly once; the other person is told the order has already changed state |
| All or nothing | One Start either reserves every material or reserves none; one Complete writes off the materials, raises finished-good stock and records the lot in the same breath, with no half-finished state in between |
| No duplicate ledger lines | For the same order, even if the action is repeated because of a flaky network or a double click, exactly one ledger line is written per affected item |
| Merchant scoping | A production order is data private to its merchant; deletes are soft and can be restored |
| Numeric precision | Quantities and BOM ratios are computed to 4 decimal places |
| i18n | State labels and ledger reasons are shown in both Vietnamese and English |
7. UX & flows
The Production Orders screen sits in the Inventory group of the Client app, for the merchant owner and the manager. Cashiers on the POS never see it - once a batch is done, the finished good is just another item with normal stock on the menu.
8. Data & domain
| Concept | Role |
|---|---|
| Production order | The document that drives one batch: finished good, BOM, location, planned quantity, actual quantity, shortfall, state |
| Target finished good | The item being produced - a product variant, or a semi-finished material |
| Bill of materials | The material ratios for one unit of the finished good; multiplied by the planned quantity to get the material needed for the whole batch |
| Order reservation | The slice of material committed to a production order, tagged with that order |
| Finished lot | The lot created when the order completes, carrying that batch's own lot number and expiry |
| Shortfall | Planned quantity minus the quantity actually produced, recorded on the order |
| Inventory ledger line | The trace of each stock change caused by the order: material out at completion, finished goods in at completion - both written at completion, neither at Start |
Conceptual level only - the full schema lives in the inventory domain model.
9. Dependencies & assumptions
Depends on
| # | Feature | What it depends on |
|---|---|---|
| 01 | BOM Bill of Materials | a production order runs against an active BOM; the BOM explodes across levels but stops at every component that has stock of its own. |
| 02 | RSV Stock Reservation | a production order is a reservation holder and shares the reserve-and-release mechanism with sale orders and stock transfers. |
| 03 | LSE Lot, Expiry & Picking Strategy | materials are picked per the configured strategy, and the finished goods are stamped with a lot. |
| 04 | LDG Inventory Ledger | every stock change an order causes writes a ledger line pointing back at the order. |
| 05 | STK Stock Items & Levels and LOC Stock Locations | where the material is written off and the finished goods are received. |
Assumptions
| # | Assumption | What breaks if it is wrong |
|---|---|---|
| 01 | A batch finishes in one go; there is no need to receive part of the finished goods when the batch is only half done. | A workshop running multi-day batches has to wait until the end of the batch before it has anything to sell, or works around it by splitting into several small orders and loses the trace of one batch. |
| 02 | Materials come out of, and finished goods go into, the same location. | A central kitchen that cooks and then hands off to the counter has no document for the hand-off, and the finished-good stock sits in the wrong place until somebody raises a stock transfer. |
| 03 | The shelf-life days of a finished good are declared on the item itself, not re-declared on each order. | A summer batch spoils faster than a winter one, and with shelf life as a constant the lot carries a wrong expiry, so earliest-expiry-first picks in the wrong order. |
| 04 | Labour and processing cost are not rolled into the finished good's inventory valuation in this release. | Finished-good cost comes out lower than reality, so the gross margin on self-made items is reported higher than it really is. |
10. Release plan & criteria
| Aspect | Plan |
|---|---|
| Phase | P2 (Inventory) - see the feature catalogue in the URD |
| Rollout | Open to merchants with items of the manufactured kind; pure retail merchants and cafes that only assemble at the counter do not need it turned on |
| Migration | No real production-order data to migrate |
| Release criteria | One batch runs the whole lifecycle on the Client app: open the order, Start reserves exactly the material the BOM calls for, Complete writes off exactly what was consumed and raises exactly the right finished-good stock with a lot number and an expiry, the shortfall is recorded correctly, and cancelling part-way releases exactly what was held; every stock change leaves a ledger line pointing back at the order |
| Monitoring | Orders run per week per merchant, average shortfall rate per finished good, number of orders cancelled after Start |
References
- URD: Production Orders · Bill of Materials
- Related: Stock Reservation & Oversell Guard · Lot, Expiry & Picking Strategy · Inventory Ledger · Materials
- Module: Inventory - URD
- Developer: @nx/inventory · domain model
Risks & open questions
| # | Risk / question | Mitigation / status |
|---|---|---|
| 01 | Materials are only written off at completion, so while the kitchen is working the ledger still says the flour is sitting in the location even though it is already in the oven | That flour is in the reserved state, so nobody can take it and available already dropped (FR-006); the on-hand number does drift from reality while the batch runs - acceptable, because a batch usually runs within the day |
| 02 | A batch is cancelled part-way because it burned, spoiled or the kitchen abandoned it: the material really did go into the pot but the system releases everything back to available | Cancelling an order releases what was reserved (FR-012); material that was genuinely ruined is written off with a stock count or a write-off ticket, not through the production order |
| 03 | Editing a BOM while an order is in progress can make the material reserved at Start differ from the material written off at Complete | Open: decide to snapshot the BOM ratios onto the order at the moment of Start, and write off strictly against that snapshot |
| 04 | Finished-good cost computed on the planned yield instead of the actual yield would report a cost cheaper than reality | Settled: the unit cost is the total cost of the materials actually consumed divided by the actual yield (FR-015). Plan for 50 litres, get 47 after evaporation, divide by 47. |
| 05 | Partial production (receiving half a batch early) is a real need for larger workshops but is not in this release | Open: watch real merchant demand before opening child tickets the way stock transfer does |
| 06 | A batch coming up short several times in a row can mean the BOM is wrong rather than the kitchen failing | The shortfall is recorded on every order (FR-011), enough data for the owner to go back and check the BOM |
Frequently asked questions
| # | Question | Answer |
|---|---|---|
| 01 | How is a production order different from a counter drink that explodes its BOM at the point of sale? | A counter drink is made the moment the customer orders and handed straight over - the finished item never sits in stock, so it needs no order. A production order is for goods made ahead in bulk and stored: a tray of bread, a pot of broth, a run of sauce. The finished good of an order has stock of its own, a lot number and an expiry. |
| 02 | Why does Start only reserve the materials instead of writing them off? | Because reserving is already enough to stop anyone taking them by mistake: 5kg of flour moves to reserved for the order, flour's available drops immediately, and no other item and no other order can touch that slice. Writing the ledger is closing the books, so it waits until the batch is done - only then do you know whether the batch really ran to the end or was abandoned. This is exactly how stock transfer already works: hold the goods at the source location first, and only write them off when they physically leave. |
| 03 | What if more comes out than planned? | Declare the real number that came out and finished-good stock goes up by exactly that. The shortfall shows 0. The planned quantity only drives how much material is pulled at Start; it does not cap how much can be received into stock. |
| 04 | Can I get the materials back if I cancel part-way? | Yes. An in-progress order has only reserved the materials, never written them off, so cancelling releases everything held, available goes straight back up, and the ledger stays empty. If part of the material was genuinely ruined, that part is written off with a stock count or a write-off ticket, not through the production order. |
| 05 | Where does the finished lot's expiry come from? | From the date you hit Complete, plus the shelf-life days already declared on the finished good itself. Bread declares 3 days, so a batch completed today expires in 3 days. A finished good that declares no shelf-life days produces a lot with a lot number and no expiry. |
| 06 | Can one order make several finished goods at once? | No. One order, one BOM, one finished good, one location. Two things to make means two orders. |
| 07 | When the batch recipe nests a semi-finished item, what does the order reserve? | That semi-finished item itself, if it has stock of its own. The house-made sauce was already cooked by another production order, and that order already deducted the tomatoes and the oil, so this order only reserves the sauce and stops there. Drilling down to the tomatoes as well would deduct the same goods twice. |