URD: Inventory
| Module | Version | Updated |
|---|---|---|
CORE-06 | v2.0 | 2026-07-13 |
What the Inventory module does
Define the user-facing requirements for Inventory management - a merchant knows what they have, where it sits, how it got there, and what it is worth.
The module serves two industries at once, and they need different things. Retail needs to manage a large SKU count, scan barcodes fast at the counter, and never oversell. F&B needs to manage down to raw materials: selling one milk tea deducts milk, deducts tea, deducts tapioca pearls - and needs to know which batch of milk expires soonest so it gets used first.
Five points to understand first
This is the most important part of the document. Understanding the five points below is understanding 80% of the module.
1. Stock is four numbers, not one
A single "how much stock is left" number is not enough, because it cannot answer "how much can I sell."
| Number | Meaning | Example |
|---|---|---|
| On-hand | Stock physically sitting on the shelf, touchable | 10 cans |
| Reserved | Already promised to someone, still on the shelf but not sellable again | 3 cans (2 in a customer's cart at checkout, 1 on a pending transfer ticket) |
| Available | On-hand minus reserved. This is the number that decides whether a sale can go through | 7 cans |
| Incoming | Already left the source location, on its way here. Visible but not yet sellable | 5 cans |
A cashier only cares about available. A stock counter only cares about on-hand. Two people counting come up with two different numbers, and both are right.
2. Every stock change flows through a document
Stock never rises or falls on its own. There's always a document behind it: a purchase order, a receiving ticket, a transfer ticket, a stock count ticket, a write-off ticket - or a sale order.
A document has a lifecycle: draft → submitted → approved → executed → completed. Stock only changes when the document completes, not the moment it is drafted. That's what lets every number be traced back to a document someone signed off on.
3. The ledger is the truth, the stock table is only a summary
The system writes to two places:
- Inventory ledger (
LDG) - every time stock changes, a new line is written, recording: before the change, the delta, after the change, the reason, and the source document. No operation - sale, receiving, transfer, stock count - ever edits or deletes a past ledger line; a correction is a new adjustment line. The operations team has a separate escape hatch to edit or delete a line directly when data needs rescuing - this is an internal tool used during incidents, not a merchant-facing feature. - Stock table (
STK) - the four current numbers. This is the summary for fast reads, because replaying the whole ledger on every sale would be far too slow.
When the two disagree, the ledger is right. The stock table can be rebuilt from the ledger; the ledger cannot be rebuilt from the stock table.
4. A sale reserves first and consumes later
Two separate steps, and this is where overselling gets blocked:
- Reserve - the moment the cashier adds an item to the order. The stock is still on the shelf, but "available" drops immediately. If there is not enough stock, the system rejects right here - before the customer has paid, while it is still easy to fix.
- Consume - when the order completes. The stock actually leaves the shelf. This step never fails for lack of stock, because the stock was already reserved in step 1.
The same goes for prepared items: ringing up "1 milk tea" makes the system explode the bill of materials into milk/tea/tapioca and reserve each ingredient right then. Out of milk shows up at the counter immediately, not after the kitchen finds out while making it.
The default is to block overselling. A merchant can turn on allow oversell per item - for example a merchant enables it for ice and straws (hard to count exactly), but keeps the block on for chicken and milk.
5. Cost is decoupled from quantity
Quantity is counted per location. Cost is not - it is one number for the whole merchant.
That means moving stock between two locations never touches money - only the quantity shifts. Cost only changes when new stock comes in at a different price.
What is in the module
Seventeen feature areas. Click an AREA code to open Details at that area.
AREA | Name | Industry | Status |
|---|---|---|---|
LOC | Inventory Locations | All | Built |
UOM | Units of Measure & Conversion | All | Partial |
STK | Inventory Items & Stock Levels | All | Built |
RSV | Reservation & Oversell Prevention | All | Partial |
VAL | Inventory Valuation | All | Partial |
LSE | Lots, Expiry & Picking Strategy | F&B, Retail | Planned |
LDG | Inventory Ledger | All | Planned |
TKT | Inventory Tickets | All | Planned |
TRF | Stock Transfer | All | Planned |
CNT | Stock Count | All | Planned |
VEN | Vendors | All | Partial |
PO | Purchase Orders | All | Planned |
MAT | Materials | F&B | Partial |
BOM | Bill of Materials | F&B, Manufacturing | Planned |
MFG | Production Orders | Manufacturing | Planned |
RPL | Replenishment | All | Partial |
SYN | Multichannel Sync | Retail | Planned |
1. Definitions
| Term | Definition |
|---|---|
| Inventory Location | A physical place that holds stock: a main warehouse, a store, a bar counter, a kitchen. Can be nested. |
| Inventory Item | An item tracked for stock - either a product variant or a material. |
| Stock Record | The four numbers for an item at a location, split by lot when lot tracking is on. |
| Reservation | A promise to set aside stock for someone: a sale order, a transfer ticket, a production order. Each reservation is tagged with who holds it, so it releases the right share. |
| Allow Oversell | A per-item flag that permits reserving/selling past available stock, even into negative. Off by default. |
| Inventory Ledger | An append-only record; each line is one stock change, with a reason and a source document. |
| Inventory Valuation | An item's cost: weighted moving average (system-computed) or standard cost (manually entered). |
| Weighted Moving Average | Receiving stock at a new price blends it with the existing stock by quantity weight, into a single cost. |
| Lot | A group of stock received or produced together, sharing one expiry. A lot holds many units. |
| Serial | An identifier for each individual unit (a phone's IMEI). One serial, one unit. |
| Picking Strategy | When an item has multiple lots, which lot ships first: earliest expiry (FEFO) or first received (FIFO). |
| Inventory Ticket | The document that controls one instance of stock coming in, going out, or moving. Has a lifecycle; only posts to the ledger once approved. |
| Base Unit | The smallest unit an item is counted in. Stock is always stored in this unit. |
| Material | Something used to make another item, not sold directly: milk, tea, flour. |
| Bill of Materials (BOM) | The recipe for making an item: 1 milk tea needs 50ml milk, 5g tea, 30g tapioca. |
| BOM Explosion | Tracing a sold item down through its bill of materials to raw materials, through multiple tiers if needed. |
| Vendor | A party that sells to the merchant. Price, lead time, and minimum order quantity are declared per vendor-item pair. |
2. Conceptual Model
3. Features
LOC - Inventory Locations Built
Spec: PRD-STK-001 (shared by
LOCandSTK) · 8 requirements · 4 acceptance scenarios
A merchant rarely has just one place to keep stock. A milk tea merchant has a prep counter and a back-of-house ingredient store; a retail chain has a main warehouse and multiple stores. Inventory locations are where the system knows "where the stock sits," letting a merchant build a hierarchy instead of a flat list. Every merchant always has a default location available from day one, even before ever opening the location setup screen - so other operations like selling or receiving are never blocked because "no location is selected."
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-LOC-001 | Must | Merchant creates an inventory location with a display name, choosing a type: physical (main warehouse, store, bar counter, kitchen) or virtual - used for grouping or planning, not tied to real physical stock. | Built |
URD-LOC-002 | Must | A location can nest under a parent location to form a hierarchy (e.g. Main Warehouse > Zone A > Cold Shelf), with no depth limit. | Partial |
URD-LOC-003 | Must | Each merchant has exactly one default location at any given time; setting a different location as default automatically clears the old one's default flag. | Built |
URD-LOC-004 | Must | A merchant who has never created a location still operates normally (selling, receiving) - the system auto-creates a default location the first time one is needed, without forcing the merchant into location setup first. | Built |
URD-LOC-005 | Must | A location moves through a lifecycle: created → active → inactive → archived; an inactive location no longer appears in pickers for receiving/issuing/transferring stock, but its history is preserved. | Built |
URD-LOC-006 | Should | Merchant enters an address for a physical location, used when printing on documents or coordinating delivery. | Built |
URD-LOC-007 | Should | Merchant views the rolled-up stock of a branch in the location tree (e.g. the total stock of "Main Warehouse" including every zone inside it). | Planned |
URD-LOC-008 | Must | The operations team views and manages every merchant's inventory locations in the Back Office, with the same structure and lifecycle merchants manage themselves on the Client. | Built |
AC-LOC-01: Location hierarchy
| Given | When | Then |
|---|---|---|
| Merchant has "Main Warehouse" as a root location | Creates a new location "Cold Zone", picking "Main Warehouse" as parent | "Cold Zone" appears as a child of "Main Warehouse" in the location tree |
AC-LOC-02: Exactly one default location
| Given | When | Then |
|---|---|---|
| Merchant currently has "Store A" as default | Sets "Main Warehouse" as default | "Main Warehouse" becomes default; "Store A" automatically loses its default flag |
AC-LOC-03: Default location auto-created
| Given | When | Then |
|---|---|---|
| A new merchant who has never opened the Inventory Locations screen | Sells their first item | The system auto-creates a default location and deducts stock there, with no "no location selected" error |
AC-LOC-04: A virtual location does not count as physical stock
| Given | When | Then |
|---|---|---|
| Merchant creates a virtual location to group stock being planned for receiving | Views the merchant's total physical-stock report | Stock recorded at the virtual location is not added to the physical stock total - it is a planning number, not real stock on a shelf |
UOM - Units of Measure & Conversion Partial
Spec: PRD-UOM-001 · 12 requirements · 2 acceptance scenarios
A product is rarely sold in just one unit: received by the case, sold retail by the can; a material received by the sack, used by the gram. Stock has to normalize to one shared unit to add and subtract, but people want to see and enter data in the unit they're used to. Units of Measure & Conversion is where the system reconciles both needs: stock stays in a single base unit, while the merchant operates in whatever unit they like and gets it auto-converted both ways - even when two different merchants call "one case" two different quantities.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-UOM-001 | Must | Merchant selects or creates a unit of measure, grouped by: count, weight, volume, length, time, area, other. A shared system-wide unit set is available, and merchants can add their own units on top. | Partial |
URD-UOM-002 | Must | Each item has one base unit; all of that item's stock is always stored and displayed in the base unit, regardless of which unit a document was entered in. | Partial |
URD-UOM-003 | Must | Merchant declares a conversion factor between a unit and a reference unit in the same group (e.g. 1 kg converts to 1000 g); several declared pairs chain together into a multi-tier conversion. | Partial |
URD-UOM-004 | Must | When a receiving/issuing/transfer document is entered in a unit other than the base unit, the system auto-computes the converted quantity from the declared conversion chain - the person entering the document never multiplies the factor or types the converted number by hand. | Planned |
URD-UOM-005 | Must | Each product variant and each material declares three unit roles: base unit, purchasing unit, selling unit - each role picks one unit from the catalog. | Partial |
URD-UOM-006 | Should | For the purchasing or selling role, a merchant can declare an item-specific override: how many base units one packaging unit of this specific item equals (e.g. "1 of my cases = 24 cans"). When declared, this override wins over the catalog's shared conversion factor. | Planned |
URD-UOM-007 | Must | Without an override, the system uses exactly the shared conversion factor declared in the unit catalog (correct for standard measurement units like kg/g, liter/ml); with an override, it uses the item's own number (correct for packaging units like case/pack/crate, since the same name "case" means a different quantity for each item). | Planned |
URD-UOM-008 | Must | The base unit cannot declare its own override - the base unit's factor is always 1 by definition. | Planned |
URD-UOM-009 | Should | Thanks to per-item overrides, a merchant only needs one "case" unit in the shared catalog for many items with different packaging, instead of creating "case-12", "case-24" as separate units. | Planned |
URD-UOM-010 | Must | Materials declare units in the Inventory module; product variants declare units in the Product Catalog module - same three-role structure, different place to operate it. | Partial |
URD-UOM-011 | Must | A unit's conversion factor on an item is immutable once that item has any stock movement. • Editing the factor after goods have been received, sold or counted rewrites history: the old quantities stay put but their meaning changes, and past cost drifts with them. • A vendor that ships a different pack size gets a NEW unit on the item; the existing unit's factor is never edited. | Planned |
URD-UOM-012 | Must | A document freezes both the unit and the base-unit quantity at the moment it is raised. • Purchase-order lines and stock-ticket lines store the chosen unit and the converted quantity, and never recompute it later. • That way an old document always reads back the number it was written with, even if the unit catalogue changes afterwards. | Planned |
AC-UOM-01: Conversion through the shared factor chain
| Given | When | Then |
|---|---|---|
| Base unit is "gram"; unit "kg" declares a factor of 1000 against "gram" | Creates a receiving ticket for 5 kg of material | The system auto-records 5000 grams to stock - the person entering the ticket never multiplies the factor by hand |
AC-UOM-02: Item-specific override wins over the shared factor
| Given | When | Then |
|---|---|---|
| Product Coca picks "case" as its purchasing unit, with an override of 24 cans/case | Creates a purchase order for 2 cases of Coca | The system records 48 cans to stock (base unit is can), using the 24 declared specifically for this product |
| Product Pepsi also picks "case" as its purchasing unit but overrides 20 cans/case | Creates a purchase order for 2 cases of Pepsi | The system records 40 cans - the same unit name "case" produces a different result, because each product declares its own |
STK - Inventory Items & Stock Levels Built
Spec: PRD-STK-001 (shared by
LOCandSTK) · 9 requirements · 4 acceptance scenarios
An inventory item is where the four stock numbers (on-hand, reserved, available, incoming) turn into a concrete record: one item, at one location, split by lot when lot tracking applies. This is the screen a warehouse worker opens every day to know how much is left, who's holding a reservation on it, and when to reorder. The same item can have several inventory items, each one an item-location-lot combination.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-STK-001 | Must | An item (product variant or material) with stock tracking on automatically gets an inventory item the first time the system needs to record stock for it - whether from a product catalog sync or the first receiving. | Built |
URD-STK-002 | Must | Each inventory item at a location carries four numbers: on-hand, reserved, available, incoming. | Partial |
URD-STK-003 | Must | Available always equals on-hand minus reserved, correct at every point in time, even when multiple operations hit the same inventory item concurrently. | Partial |
URD-STK-004 | Must | An item with lot tracking on splits into a separate inventory item per lot - the same item, the same location, but a different lot is a different stock record. Serial tracking splits by individual serial unit. | Planned |
URD-STK-005 | Must | Low-stock alerts use a three-tier threshold, falling through to the first tier that is declared: item-and-location-specific threshold → item-wide threshold (applies across all locations) → the system default threshold. | Built |
URD-STK-006 | Should | Merchant views an item's rolled-up stock across every location, or drills into a single location. | Built |
URD-STK-007 | Must | Merchant or the operations team directly edits an inventory item's on-hand number through an admin screen, without going through an inventory ticket; every edit writes a line to the inventory ledger with the old value, the new value, and the reason. | Built |
URD-STK-008 | Should | Each inventory item records the last time it was counted and the last time it was received, so a merchant can see which inventory items have not been checked in a while. | Planned |
URD-STK-009 | Must | The invariant "available = on-hand - reserved" is enforced directly at the database layer, not left to every write path to compute correctly on its own - not even a code path with the write order wrong can break the invariant. | Planned |
AC-STK-01: An inventory item's four numbers
| Given | When | Then |
|---|---|---|
| Inventory item has on-hand 10, reserved 3 | Views the stock detail screen | Available shows 7 |
AC-STK-02: Alert via the three-tier threshold
| Given | When | Then |
|---|---|---|
| Inventory item A has no item-specific threshold; its item declares a shared threshold of 20 | Item A's available drops to 15 | System raises a low-stock alert, using the shared threshold of 20 |
| Inventory item B declares its own threshold of 5; its item declares a shared threshold of 20 | Item B's available drops to 15 | System does not alert - the item-specific threshold of 5 takes priority, and 15 is still above it |
AC-STK-03: Split by lot
| Given | When | Then |
|---|---|---|
| Item has lot tracking on, with lot A at 10 and lot B at 5 | Views stock detail at a location | Two separate stock lines appear: lot A - 10, lot B - 5, summing to the correct total of 15 |
AC-STK-04: The invariant is enforced at the data layer, not relied on in code
| Given | When | Then |
|---|---|---|
| Inventory item has on-hand 10, reserved 4 (available 6) | Multiple stock writes run concurrently on the same inventory item, including one new write path with the update order wrong | Available always equals on-hand minus reserved at every point it is read - the database itself blocks any write that would break the invariant, with no code needing to self-check it |
RSV - Reservation & Oversell Prevention Partial
Spec: PRD-RSV-001 · 10 requirements · 4 acceptance scenarios
Overselling - promising the same item to two customers at once - is the worst operational failure a merchant can hit. Reservation is the mechanism that blocks it: the moment a cashier rings an item into an order, the matching stock gets reserved and "available" drops immediately - before the customer has even paid. The same reservation mechanism also covers stock transfer tickets (stock on its way) and production orders (materials already committed to a batch) - one mechanism, shared by several operations.
Auto-reservation on ringing up an item is already live, and a merchant can manually adjust the reserved number through an admin screen. But the detail screen for who's reserving how much - breaking it down by order, by which transfer ticket holds what - does not exist yet; a merchant only sees one total number.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-RSV-001 | Must | Adding an item to a cart reserves the matching stock immediately - on-hand stays the same, reserved goes up, available drops, all at once. | Built |
URD-RSV-002 | Must | By default the system blocks reserving or selling past the available number - the request is rejected right at the cart, before the customer pays. | Built |
URD-RSV-003 | Must | Each inventory item can turn on the allow-negative flag to permit reserving/selling past stock, even into negative - used for items that are hard to count exactly, like ice or straws. This flag is off by default. | Built |
URD-RSV-004 | Must | Every reservation is tagged with its specific holder - a sale order, a transfer ticket, or a production order - so it is clear exactly whose reservation it is. | Planned |
URD-RSV-005 | Should | An item can be reserved by multiple sources at once; the detail screen breaks down each share by its own source, not just one total number. • Example: sale order #123 reserves 2 cans, transfer ticket #45 reserves 1 can. • When a merchant sees stock stuck (a large reserved number with no clear cause), they immediately know where to release it, instead of only seeing a total with no way to trace the source. | Planned |
URD-RSV-006 | Must | When a line is removed from a sale order or the order is canceled, the stock reserved specifically for that order is released immediately - available goes back up accordingly, no extra action needed, and no notification pops up. | Built |
URD-RSV-007 | Must | Inventory does not decide when to release a reservation and has no time-based auto-expiry - releasing is always driven by whoever created the reservation (the sale order, the transfer ticket, the production order), following its own lifecycle. | Built |
URD-RSV-008 | Must | A prepared item or a set goes through the exact same reservation mechanism when rung into an order - missing one of its components triggers an out-of-stock message right at the cart, unless that component also has the oversell flag on. | Planned |
URD-RSV-009 | Should | Stock on a transfer ticket is reserved at the source location from the moment the ticket is raised until the goods physically leave the source. • Once the goods leave the source, the reservation record ends and the source's on-hand drops at the same moment, so the source's available does not move a second time. • From then until the destination confirms receipt, the goods sit in the destination's "incoming" number, and the source location remains responsible for them. | Planned |
URD-RSV-010 | Should | Every oversell event is logged so a merchant can reconcile against it. • The log captures both reservations rejected for lack of stock and sales that went negative thanks to the allow-oversell flag. • It filters by item, so a merchant can see which items keep getting rejected or keep going negative - the evidence for either reordering or turning the flag on. | Planned |
AC-RSV-01: Blocking overselling by default
| Given | When | Then |
|---|---|---|
| Inventory item has available 2, oversell not enabled | Cashier adds 3 to the cart | System rejects, reports insufficient stock right at the cart |
AC-RSV-02: Allowing oversell when the flag is on
| Given | When | Then |
|---|---|---|
| Inventory item "ice" has available 0, with oversell enabled | Cashier adds 5 more ice to the cart | System still allows the reservation, reserved goes up, available drops to -5, no error |
AC-RSV-03: Releasing stock on order cancellation
| Given | When | Then |
|---|---|---|
| A sale order is reserving 3 units of an item | Customer cancels the order | Reserved drops by 3 immediately, available goes back up by 3, no notification appears |
AC-RSV-04: Viewing who's reserving stock
| Given | When | Then |
|---|---|---|
| Item X is reserving a total of 3 cans: sale order #123 holds 2, transfer ticket #45 holds 1 | Merchant opens item X's reservation detail screen | Two breakdown lines appear: sale order #123 - 2 cans, transfer ticket #45 - 1 can, summing exactly to the total reserved of 3 cans |
VAL - Inventory Valuation Partial
Spec: PRD-VAL-001 · 13 requirements · 4 acceptance scenarios
Inventory valuation answers: what is one unit of stock worth, so profit and loss compute correctly on a sale. A merchant receives stock in multiple batches, each batch possibly at a different price - the system needs to roll that up into a single number without the merchant doing the math by hand. Two methods are supported side by side: an automatic one based on weighted moving average, and a manual one where the owner types in a fixed cost when they already know the real cost, for example self-produced goods or items with a stable purchase price. Inventory does not pull the Finance module into this - a price variance is just a number recorded for reference, it does not generate any accounting entry.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-VAL-001 | Must | Each item (product variant or material) has exactly one cost per merchant; v1 does not split cost by location - one item has one cost even when it is carried across multiple warehouses. | Planned |
URD-VAL-002 | Must | The system stores two costs in parallel for each item: the weighted moving average cost (system-computed) and the standard cost (owner-entered by hand). | Planned |
URD-VAL-003 | Must | The owner picks the valuation method per item - weighted moving average or standard cost - to decide which cost is used on issue. | Planned |
URD-VAL-004 | Must | The weighted moving average cost is recomputed on every receiving, regardless of which valuation method the item currently uses - this cost always runs in the background so it is ready the moment a merchant switches methods. | Planned |
URD-VAL-005 | Must | Weighted moving average formula: new cost equals (prior stock value plus the value of the batch just received) divided by (prior stock quantity plus the quantity just received). | Planned |
URD-VAL-006 | Must | When stock drops to zero or negative, the system never divides by zero - the prior cost stays frozen until the next receiving, at which point the new cost fully replaces the old one. | Planned |
URD-VAL-007 | Must | Standard cost is entered by the owner on the item screen and does not change automatically with receivings - it only changes when the owner edits it themselves. | Partial |
URD-VAL-008 | Must | When the actual receiving price differs from the configured standard cost, the variance is recorded as an observable number on that receiving's inventory ledger line - it does not create an accounting entry and never passes through the Finance module. | Planned |
URD-VAL-009 | Must | Transferring stock between two locations of the same merchant never changes cost - cost is per-merchant, quantity is what shifts per location. | Planned |
URD-VAL-010 | Should | When a vendor invoice arrives after the receiving date with a price different from the price at receiving, the purchase order shows a price-variance warning; the cost of prior issues stays as it was, not recomputed - the variance only applies to subsequent receivings/issues. | Planned |
URD-VAL-011 | Must | v1 does not support FIFO or LIFO costing - only weighted moving average and standard cost. | Planned |
URD-VAL-012 | Could | A merchant running multiple independent receiving locations can turn on an advanced setting: split cost per location instead of sharing one cost across the whole merchant. | Planned |
URD-VAL-013 | Should | A merchant using standard cost can view a screen that rolls up cost variance over a date range - the total amount paid above or below the configured standard cost, summed from ledger lines carrying a variance. This screen only means something once a real item is actually using standard cost. | Planned |
AC-VAL-01: Recomputing weighted moving average on receiving
| Given | When | Then |
|---|---|---|
| Item has stock of 10 units, weighted moving average cost 20,000/unit (total stock value 200,000) | Receives 5 more units at 26,000/unit (total 130,000) | New weighted moving average cost = (200,000 + 130,000) / (10 + 5) = 22,000/unit |
| Same item, currently configured to use standard cost on issue | Receives more stock as above | The weighted moving average is still recomputed to 22,000 in the background, but the cost used on issue stays the configured standard cost, unchanged |
AC-VAL-02: Freezing cost when stock hits zero
| Given | When | Then |
|---|---|---|
| Item sells out, stock drops to 0, prior weighted moving average cost was 22,000 | Receives 8 more units at 24,000/unit | New weighted moving average cost = 24,000/unit - exactly the price of the batch just received, not blended with the prior stock value (since prior stock was 0) |
AC-VAL-03: Vendor invoice arrives later with a price variance
| Given | When | Then |
|---|---|---|
| A purchase order already received stock at 20,000/unit, cost already updated to this price | Vendor invoice arrives later showing a real price of 21,000/unit | The purchase order shows a price-variance warning; issues that already happened keep their old cost, not recomputed |
| Same situation | Merchant confirms applying the new price | Cost updates to 21,000 for subsequent receivings/issues, not retroactively applied to past ones |
AC-VAL-04: Rolling up standard-cost variance by month
| Given | When | Then |
|---|---|---|
| Item is using standard cost of 20,000/unit; during the month it received 3 batches at real prices of 22,000, 21,000, 23,000 for a combined 150 units | Merchant opens the standard-cost variance screen, picks that month as the range | The screen shows the total amount paid above standard cost, summed from the variance of all 3 receivings that month |
LSE - Lots, Expiry & Picking Strategy Planned
Spec: PRD-LSE-001 · 9 requirements · 4 acceptance scenarios
Not every unit of stock is identical even under the same item code. Milk received today and milk received last week have different expiry dates, and a merchant needs to sell the older milk first to avoid throwing it out. A lot is how the system groups one receiving into a batch that shares one expiry, while a serial is an identifier for an individual unit or a high-value item whose own lifecycle needs tracking, like a phone by IMEI. v1 only turns on lots; serial is already designed but not yet exposed, so it does not need to be rebuilt when its turn to ship comes.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-LSE-001 | Must | Each item can turn on lot tracking, turn on serial tracking, or turn on neither - the two flags are independent, both off by default. | Planned |
URD-LSE-002 | Must | With lot tracking on, each receiving records units into a lot carrying a lot code and an expiry date (for items with a shelf life); receiving the same lot code into the same location adds to the existing lot instead of creating a second one. | Planned |
URD-LSE-003 | Should | With serial tracking on, each unit carries its own identifier and moves through its own states: in stock, sold, and later states (returned, lost...). | Planned |
URD-LSE-004 | Must | Each item with lots picks a picking strategy: earliest expiry first, or first received first. | Planned |
URD-LSE-005 | Must | Picking strategy is a separate setting, fully decoupled from the valuation method (VAL) - changing the picking strategy does not affect how cost is computed, and vice versa. | Planned |
URD-LSE-006 | Must | When two lots share the same expiry date, the system picks by a fixed order: earliest expiry first, then earliest received if still tied, then lot code if still tied. | Planned |
URD-LSE-007 | Must | When selling or issuing an item with multiple lots, the system auto-selects the right lot per the configured strategy - the seller never picks a lot by hand. | Planned |
URD-LSE-008 | Should | The stock screen shows a per-lot breakdown for an item: remaining quantity, received date, expiry date. | Planned |
URD-LSE-009 | Could | The system alerts on lots expiring within a merchant-configured number of days; default 7 days, and the merchant can change this number to suit their industry's needs. | Planned |
AC-LSE-01: Picking the lot with the earliest expiry
| Given | When | Then |
|---|---|---|
| Item has Lot A (5 units, expires 2026-08-01, received 2026-07-01) and Lot B (3 units, expires 2026-07-20, received 2026-07-05) | Sells 4 units | The system deducts all 3 units of Lot B first (earlier expiry), then deducts 1 more unit from Lot A to reach the total |
AC-LSE-02: Tie-break when two lots share the same expiry
| Given | When | Then |
|---|---|---|
| Item has Lot C (expires 2026-08-01, received 2026-07-01) and Lot D (expires 2026-08-01, received 2026-07-03) - same expiry | Sale needs to pick a lot | Lot C is picked first because it was received earlier, even though both share the same expiry |
AC-LSE-03: Serial lifecycle (forward-spec)
| Given | When | Then |
|---|---|---|
| Item has serial tracking on, e.g. a phone tracked by IMEI | Receives 1 unit | That unit carries its own serial code, state "In Stock" |
| Same unit | Sold to a customer | State moves to "Sold", tagged with the matching order |
AC-LSE-04: Expiry alert at the default day count, changeable by the merchant
| Given | When | Then |
|---|---|---|
| Merchant has never opened the expiry-alert configuration | A milk lot has 6 days left before it expires | System raises an expiry alert, using the default of 7 days |
| Merchant opens the configuration, changes the alert threshold to 3 days | A milk lot has 6 days left before it expires | System does not alert, because 6 remaining days is above the 3-day threshold the merchant just set |
LDG - Inventory Ledger Planned
Spec: PRD-LDG-001 · 11 requirements · 4 acceptance scenarios
Every time stock rises or falls, there has to be a document behind it - and the inventory ledger is where each of those documents becomes one line, permanently, never deleted. This is where a merchant traces back: why is today's number what it is, when did it change, for what reason, and which document is behind it. The fast on-screen stock table is just a summary - when there is a dispute or a mismatch, the inventory ledger is always the right answer, and the stock table can be rebuilt from the ledger, never the other way around.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-LDG-001 | Must | Every stock change caused by an operation - receiving, issuing, transfer, adjustment, write-off - creates a new line in the inventory ledger; operations never delete or edit a past line, a correction is a new adjustment line. | Partial |
URD-LDG-002 | Must | Each ledger line records all three numbers: stock before the change, the quantity changed, and stock after the change. | Partial |
URD-LDG-003 | Must | Each ledger line carries a reason picked from a fixed catalog, e.g.: purchase receiving, sale, cart reservation, material consumption, stock count adjustment, write-off/disposal. | Partial |
URD-LDG-004 | Must | Each ledger line references exactly one source document - a purchase order, an inventory ticket, or a sale order - via a document type and a document code. | Partial |
URD-LDG-005 | Must | Each ledger line records the unit of measure, the conversion factor, and the cost at the exact moment it happened, so reading back the history stays correct even if the unit or the price later changes. | Partial |
URD-LDG-006 | Must | The same document, however many times it gets reprocessed (e.g. because the system resends a notification), writes exactly one ledger line per affected stock record - never deducting or adding stock twice. | Planned |
URD-LDG-007 | Must | The inventory ledger is the source of truth for stock; the rolled-up stock table is only a summary for fast reads - when the two disagree, the ledger is always right. | Partial |
URD-LDG-008 | Should | User reviews the full change history of an item at a location, filtered by reason, document, or date range. | Planned |
URD-LDG-009 | Must | The operations team edits or deletes a ledger line directly when handling a data incident - this is an internal operations tool used outside the merchant interface, not a merchant-operable feature. | Built |
URD-LDG-010 | Must | Each ledger line preserves the place-and-goods context of the change: the source location and the destination location, the lot code, the expiry, the serial code, who performed it, when it happened, and a free-text note field. | Partial |
URD-LDG-011 | Must | The inventory ledger is per-merchant data: a user only reads ledger lines belonging to their own merchant, and every filter and every report drawn from the ledger stays within that scope. | Partial |
AC-LDG-01: Preventing duplicate ledger lines
| Given | When | Then |
|---|---|---|
| Order is paid, the system already wrote a ledger line deducting stock for that line item | The payment-success notification gets resent a second time (because the messaging system retries) | No second ledger line gets written for the same document and the same stock record |
AC-LDG-02: Before/delta/after on a receiving
| Given | When | Then |
|---|---|---|
| Stock is currently 10 units | Receives 5 more units per a receiving ticket | Ledger line records before = 10, delta = +5, after = 15, reason = Purchase receiving, document = the receiving ticket's code |
AC-LDG-03: Correcting a mistake with a new adjustment line
| Given | When | Then |
|---|---|---|
| A ledger line recorded the wrong quantity | An operator spots the mistake and needs to correct it | A new ledger line is written to adjust to the correct number; the original mistaken line stays intact in history, never deleted or edited |
AC-LDG-04: The operations escape hatch for direct edit/delete
| Given | When | Then |
|---|---|---|
| A ledger line has a severe data error from a system incident, unfixable with a normal adjustment line | The operations team intervenes directly to edit or delete that line | The line is edited/deleted directly through an internal tool - the merchant never sees this and cannot do it themselves in their interface |
TKT - Inventory Tickets Planned
Spec: PRD-TKT-001 · 18 requirements · 5 acceptance scenarios
Before stock changes, a ticket has to be raised, approved, then executed - stock never jumps on its own with nobody signing off. The inventory ticket is the shared frame for every job that touches stock outside the sale flow: requesting a purchase, receiving goods in, moving goods to another location, issuing for internal use, correcting a figure to match reality, counting stock, returning goods to a vendor, taking goods back from a customer, and writing off damaged stock.
Those nine types share exactly one lifecycle, and each type only walks the part of it that it needs. The words on screen change with the type - a stock transfer calls the approved step "To ship" - but underneath there is a single set of states, never a second one.
A ticket that carries an intent can be executed in several batches. The parent ticket carries the intent, the child tickets record each time goods really leave a location or arrive at one, and the parent's status is derived from its children. The whole frame is append-only: a completed child ticket is never edited and never deleted, and fixing a mistake means raising a new compensating ticket.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-TKT-001 | Must | An inventory ticket is the document for one instance of goods in, goods out or a stock correction; the raiser picks exactly one of nine types at creation. • Purchase request, goods receipt, stock transfer, internal issue, adjustment. • Cycle count, vendor return, customer return, write-off/disposal. • The type is fixed at creation and cannot be changed afterwards. | Partial |
URD-TKT-002 | Must | All nine ticket types share exactly one lifecycle; no type gets a lifecycle of its own. • The steps: Draft, Submitted, Approved, In progress, Completed. • A ticket can be cancelled from any step before completion. | Planned |
URD-TKT-003 | Must | Each ticket type only walks the part of the lifecycle it needs, and no type invents an extra step. • Adjustment: Draft → Completed, with no approval step. • Cycle count: Draft → In progress → Completed. • Stock transfer: walks all five steps. | Planned |
URD-TKT-004 | Must | The status labels on screen are per ticket type, even though there is a single set of states underneath. • A stock transfer reads Approved as "To ship", In progress as "In transit", Completed as "Received". • A cycle count reads In progress as "Counting", Completed as "Applied". • Different words, same model: no state exists outside the five steps above. | Planned |
URD-TKT-005 | Must | Stock only changes when a ticket reaches Completed; drafting, submitting, approving and starting execution never add or subtract a single unit. | Planned |
URD-TKT-006 | Must | The parent ticket carries the intent, and its status is derived from its child tickets. • The parent records both ends of the move (which merchant, which location), the list of goods, and the planned quantities. • One parent takes many issue tickets and many receipt tickets; it is never boxed into exactly one pair. • Nobody sets the parent status by hand; the detail screen has no status button for the parent. | Planned |
URD-TKT-007 | Must | A child ticket is the execution document and belongs to the merchant at the end where it sits. • An issue ticket belongs to the source merchant; a receipt ticket belongs to the destination merchant. • A child ticket walks a short lifecycle, Draft → Completed, because approval already happened on the parent. • Stock and the ledger are written on the child ticket, never on the parent. | Planned |
URD-TKT-008 | Must | The inventory ticket frame is append-only, never overwrite. • A completed child ticket is never edited and never deleted, not even when the parent is later closed or cancelled. • Fixing a mistake means raising a new compensating ticket, for example an adjustment ticket, not editing the old one. | Planned |
URD-TKT-009 | Must | Every ticket carries a header, filled at creation and editable while the ticket is still a Draft. • A ticket code, system-generated and unique, used to look the ticket up and to refer to it elsewhere. • Both ends of the ticket: merchant and location on the issuing side, merchant and location on the receiving side - each type shows only the ends it needs. • A counterparty, a vendor or a customer, shown only for the types that have one. • A reason, picked from a predefined list, mandatory on adjustment and write-off tickets. • A free-text note, optional. | Partial |
URD-TKT-010 | Must | Every line on a ticket names an item, a unit and a quantity, and carries its own line status. • The planned quantity is entered when the ticket is raised; the actual quantity is recorded during execution. • Lot and expiry show for items under lot control; unit price shows on goods receipt and vendor return tickets. • Line status: pending, processing, completed, cancelled - so one ticket can hold finished and unfinished lines at the same time. | Partial |
URD-TKT-011 | Must | When a ticket reaches Completed, each of its lines writes exactly one inventory ledger row, with no duplicate row even if the ticket is processed again. | Planned |
URD-TKT-012 | Must | An actual quantity that differs from the planned quantity never blocks completion. • Issuing less than planned, issuing more than planned, receiving less than what was issued, receiving more than what was issued - all four are allowed. • The ticket completes at the real quantity and stock moves by the real quantity, never forced back to the planned one. | Planned |
URD-TKT-013 | Must | The raiser closes the ticket when goods went out and did not all arrive, and closing raises an adjustment ticket recording the loss. • Closing settles the goods that were issued and will never turn up. • The adjustment ticket carries the exact lost quantity and a reason, so the missing goods always have a document explaining them. • A closed ticket cannot be reopened. | Planned |
URD-TKT-014 | Must | Cancelling a ticket and closing a ticket are two different things and must not be mixed up. • Cancelling only stops the planned quantity that was never executed: it releases the reservation, nothing is lost, and no adjustment ticket is raised. • Closing settles goods that left a location and never arrived: that quantity is a loss and always raises an adjustment ticket. • Cancelling a parent while goods are still in transit still has to settle that in-transit quantity through the closing rule - it can never vanish silently. | Planned |
URD-TKT-015 | Must | The merchant has a ticket book listing its own inventory tickets, filterable by ticket type, status, location and date range, and searchable by ticket code. | Planned |
URD-TKT-016 | Must | Every status transition records its timestamp and the person who performed it, and that trail shows directly on the ticket detail screen. | Planned |
URD-TKT-017 | Must | Inventory tickets are per-merchant data. • A user only sees and acts on tickets belonging to the merchant they are currently working in. • When a move touches two merchants, each side only sees its own child ticket and never the other side's document. | Partial |
URD-TKT-018 | Must | Whoever raised the ticket sees a single row in their list and never has to learn another concept. • That row says the whole job: how much is moving, from where to where, and which step it is at. • The child tickets sit inside the detail screen as a timeline of what has been done. | Planned |
AC-TKT-01: One lifecycle, each ticket type walks part of it
| Given | When | Then |
|---|---|---|
| The user raises an adjustment ticket | They open it and look at the action buttons | Only a Complete button is offered, no Submit and no Approve - an adjustment goes straight from Draft to Completed |
| The user raises a cycle count ticket | They open it and look at the action buttons | A Start counting button, then an Apply result button; there is no separate approval step |
| The user raises a stock transfer ticket | They open it and look at the action buttons | All five steps are there: Submit, Approve, Start execution, Complete, and Cancel |
AC-TKT-02: Same state, different words per ticket type
| Given | When | Then |
|---|---|---|
| A stock transfer ticket has just been approved | The user opens the ticket list | The ticket reads "To ship", not "Approved" |
| The same ticket, goods have left the source location | The user opens the ticket list | The ticket reads "In transit" - still the In progress step of the shared lifecycle, only the wording differs |
AC-TKT-03: Stock only moves when the ticket completes
| Given | When | Then |
|---|---|---|
| A goods receipt ticket sits at Approved | The user opens the Stock screen of the receiving location | The figure has not changed - the quantity is only added to stock when the ticket reaches Completed |
AC-TKT-04: Closing raises an adjustment ticket, cancelling does not
| Given | When | Then |
|---|---|---|
| A transfer of 100 cans has issued 60, the destination has received only 38, and nothing more is coming | The raiser hits Close | The system raises an adjustment ticket recording 22 cans lost, with a reason; the destination's incoming drops to 0 |
| A transfer of 100 cans has just been approved and nothing has been issued yet | The raiser hits Cancel | The 100 cans reserved at the source are released in full; no adjustment ticket is raised, because no goods were lost |
AC-TKT-05: A completed child ticket is never edited or deleted
| Given | When | Then |
|---|---|---|
| An issue ticket completed at 60 cans, but it should have said 50 | The user opens that issue ticket | Every field is locked, there is no Edit button and no Delete button |
| The same situation | The user wants to bring the figure back to the truth | They raise a new adjustment ticket for the 10-can difference; the old issue ticket stays exactly as it was written |
TRF - Stock Transfer Planned
Spec: PRD-TRF-001 · 19 requirements · 3 acceptance scenarios
A stock transfer is about moving real goods between two places, but the hard part is not the movement - it is the window while the goods are in transit, when both ends need to see their own correct number, and it is the question of who owns the document.
A transfer picks all four things: which merchant and which location issues, and which merchant and which location receives. The two ends can be two different merchants within the same organizer, or two locations of the same merchant - one model, no exception.
The transfer ticket (the parent) carries the intent. Goods are issued through one or more issue tickets belonging to the source merchant, and received through one or more receipt tickets belonging to the destination merchant - as many trips as it takes, and each side writes its own document. The four stock numbers fall out of that model: what is reserved at the source is the approved quantity minus what has been issued, and what is incoming at the destination is the total issued minus the total received.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-TRF-001 | Must | A transfer names both ends, and the receiving end is mandatory. • The issuing end: a source merchant and a source location. • The receiving end: a destination merchant and a destination location - a transfer is never allowed to leave the receiving end blank. • A list of lines with a planned quantity for each item. | Planned |
URD-TRF-002 | Must | A transfer between two merchants is a legitimate operation, and a transfer inside one merchant runs on exactly the same model. • Two ends in two different merchants: each side owns its own child tickets. • Two ends in the same merchant: both kinds of child ticket simply belong to that merchant. • One model, with no special case for a same-merchant move. | Planned |
URD-TRF-003 | Must | A transfer ticket walks all five steps of the inventory ticket lifecycle, shown in the words of the transfer business. • Draft and Submitted keep their own words. • Approved reads "To ship"; In progress reads "In transit"; Completed reads "Received". • No state sits outside the shared lifecycle. | Planned |
URD-TRF-004 | Must | The transfer ticket's status is derived from its child tickets; nobody sets it by hand, and the detail screen has no status selector for the transfer. | Planned |
URD-TRF-005 | Must | Goods are issued several times through several issue tickets, one per trip. • Each issue ticket belongs to the source merchant and records exactly what went out on that trip. • An issue ticket walks the short lifecycle Draft → Completed; on completion, the source location's on-hand drops by the issued quantity. | Planned |
URD-TRF-006 | Must | Goods are received several times through several receipt tickets, one per delivery. • Each receipt ticket belongs to the destination merchant and records exactly what was counted that time. • A receipt ticket walks the short lifecycle Draft → Completed; on completion, the destination location's on-hand rises by the received quantity. | Planned |
URD-TRF-007 | Must | What is reserved at the source is the approved quantity minus what has been issued. • Approve 100 and the source reserves 100: on-hand is unchanged, available drops by 100. • Issue 60 and the source's on-hand drops by 60 while the reservation falls to 40 - available does not move again at this step. | Planned |
URD-TRF-008 | Must | What is incoming at the destination is the total issued minus the total received. • The figure shows on the destination's stock screen but never adds to available, so goods that have not arrived cannot be sold. • Issue 60 then 40, receive 60 then 38: incoming goes 60, up to 100, down to 40, and ends at 2. | Planned |
URD-TRF-009 | Must | Issuing more than the planned quantity is allowed: the issue ticket records exactly what really left the location, and the system does not block it for being larger than the quantity named on the transfer. | Planned |
URD-TRF-010 | Must | Receiving more than what was issued is allowed: the receipt ticket records exactly what was really counted at the destination, and the system does not block it for being larger than the total issued. | Planned |
URD-TRF-011 | Must | When not everything arrives, the raiser closes the ticket, and closing raises an adjustment ticket recording the loss. • The loss is the total issued minus the total received at the moment of closing. • The destination's incoming drops to 0, and the loss lands on the source's ledger with a reason. • Goods that were issued and never arrive at all are settled by this same rule. | Planned |
URD-TRF-012 | Must | Cancelling a transfer is allowed, and cancelling only stops the planned quantity that was never issued. • The reservation at the source is released in full and available goes back up; nothing was lost, so no adjustment ticket is raised. • If goods are still incoming at the destination when the cancel happens, that quantity is settled through the closing rule, meaning an adjustment ticket records it as a loss. • Completed issue and receipt tickets stay exactly as they are, never edited and never deleted. | Planned |
URD-TRF-013 | Must | Correcting a transfer always means raising another ticket - another issue trip, another receipt, or an adjustment - and never means editing a child ticket that has already completed. | Planned |
URD-TRF-014 | Must | The source location bears the risk of loss or damage in transit until the destination confirms receipt; before that moment the goods are not counted as stock on the destination's shelf. | Planned |
URD-TRF-015 | Must | Incoming is only used for goods moving between two locations inside the system; goods ordered from a vendor but not yet delivered are a different concept and never add to this figure. | Planned |
URD-TRF-016 | Must | A transfer never changes an item's cost - only the quantity shifts between two locations, and the cost stays a single figure per merchant. | Planned |
URD-TRF-017 | Must | Each person only sees their own job. • Whoever raised the transfer sees a single row, for example "Transfer 20 cans, Location A to Location B, in transit"; the child tickets are detail inside that row. • The destination merchant's stock keeper sees exactly one task of their own, for example "receive 20 cans from Merchant A", and never sees the source merchant's document. | Planned |
URD-TRF-018 | Must | The person in charge can look transfers up without opening each one. • The list filters by status, by merchant and location at either end, and by created-date range. • The detail screen shows, on every line: the planned quantity, the total issued, the total received, what is still reserved and what is still incoming. | Planned |
URD-TRF-019 | Must | Three groups of people can each approve and close a transfer that touches two merchants. • Anyone at the sending merchant. • Anyone at the receiving merchant. • Manager level or above at the headquarters merchant. Leaving it open is deliberate: transfers stall when they wait on one specific person, and a stalled transfer means stock sitting still. | Planned |
AC-TRF-01: The four numbers across a 100-can transfer issued in two trips and received twice
A transfer of 100 cans from Merchant A - Default location to Merchant B - Default location. Before the ticket is raised the source has 300 on-hand (300 available) and the destination has 50 on-hand (50 available). Two trips issue 60 then 40; two deliveries receive 60 then 38 (2 lost in transit).
| Step | Source - On-hand | Source - Reserved | Source - Available | Destination - Incoming | Destination - On-hand |
|---|---|---|---|---|---|
| Before the ticket is raised | 300 | 0 | 300 | 0 | 50 |
| Approved (To ship) | 300 | 100 | 200 | 0 | 50 |
| Trip 1 issues 60 cans (In transit) | 240 | 40 | 200 | 60 | 50 |
| Delivery 1 receives 60 cans | 240 | 40 | 200 | 0 | 110 |
| Trip 2 issues 40 cans | 200 | 0 | 200 | 40 | 110 |
| Delivery 2 receives 38 cans | 200 | 0 | 200 | 2 | 148 |
| The raiser closes the ticket for the 2 lost cans (Received) | 200 | 0 | 200 | 0 | 148 |
AC-TRF-02: The cancel and close rule table
A transfer planned at 100 cans. "Reserved" is the approved quantity minus what has been issued; "Incoming" is the total issued minus the total received. Cancelling stops what was never issued. Closing settles what was issued and never arrived.
| Case | Reserved at source | On-hand at source | Incoming at destination | On-hand at destination | Adjustment ticket for the loss |
|---|---|---|---|---|---|
| Cancel while still Draft | Nothing reserved, unchanged | Unchanged | 0 | Unchanged | No |
| Cancel while Submitted, nobody has approved | Nothing reserved, unchanged | Unchanged | 0 | Unchanged | No |
| Cancel while Approved (To ship), nothing issued yet | Release all 100, back to 0 | Unchanged | 0 | Unchanged | No - no goods ever left |
| Cancel after issuing 60, destination received all 60 | Release the remaining 40, back to 0 | Already down 60 at issue, drops no further | 0 | Already up 60 at receipt, changes no further | No - everything issued arrived |
| Cancel after issuing 60, destination received only 38 | Release the remaining 40, back to 0 | Already down 60, drops no further | 22 is settled, back to 0 | Already up 38, changes no further | Yes - an adjustment ticket for 22 cans lost |
| Cancel after issuing all 100, destination received only 90 | Already back to 0 once everything was issued | Already down 100 | 10 is settled, back to 0 | Already up 90 | Yes - an adjustment ticket for 10 cans lost |
| Close after issuing 100 and receiving only 98, waiting no longer | 0 | Already down 100 | 2 is settled, back to 0 | Already up 98 | Yes - an adjustment ticket for 2 cans lost |
| The ticket already Completed (all 100 received) | 0 | Already down 100 | 0 | Already up 100 | No - and there is no cancel button left; a correction means a new ticket |
AC-TRF-03: Each side only sees its own job
| Given | When | Then |
|---|---|---|
A manager at Merchant A raises a transfer of 20 cans to Merchant B - Default location and the goods have left | The raiser opens the transfer list | Exactly one row shows: "Transfer 20 cans, Merchant A - Default location to Merchant B - Default location, in transit"; the issue and receipt tickets live inside the detail screen |
| The same transfer | The stock keeper at Merchant B opens their own task list | Exactly one task shows: "receive 20 cans from Merchant A", and the source merchant's issue ticket is nowhere in sight |
CNT - Stock Count Planned
Spec: PRD-CNT-001 · 14 requirements · 3 acceptance scenarios
A stock count is when an operator reconciles the books against the real goods on the shelf, but a merchant cannot close its doors to count - customers keep coming and orders keep running. The system solves this by capturing a baseline of the book figure the moment the count ticket opens, then letting sales keep running in parallel while the count is underway.
When the counter submits a tally, the system does not overwrite stock with that number - it works out the variance between the tally and the captured baseline, then applies that variance to stock as it stands at the moment the result is applied. That way goods sold during the count are never mistaken for shrinkage, and real shrinkage is never hidden by goods that were sold.
A stock count is one of the inventory ticket types and shares the ticket frame's lifecycle rather than having one of its own: Draft → In progress → Completed, shown in the words of a count.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-CNT-001 | Must | Create a stock count ticket for a location; the moment it is created, the system captures the current on-hand of every item in the count's scope as the comparison baseline. | Planned |
URD-CNT-002 | Must | The merchant keeps selling, receiving and transferring normally while a count ticket is still open; a stock count never blocks sales on its own. | Planned |
URD-CNT-003 | Should | Each stock count ticket has its own sales-block flag, off by default; a merchant turns it on for a count of high-value goods. | Planned |
URD-CNT-004 | Must | The counter enters the actual tallied quantity for each item in the ticket, one line per item. | Planned |
URD-CNT-005 | Must | Applying a count result works off the variance between the tally and the baseline captured at ticket creation, never an overwrite of stock with the tally. • The variance is the tally minus the baseline. • The variance is added to or subtracted from stock as it stands at the moment the result is applied, not stock as it stood at the moment the baseline was captured. | Planned |
URD-CNT-006 | Must | The variance is written to the inventory ledger as a separate adjustment row, carrying a reason the approver selects. | Planned |
URD-CNT-007 | Should | An item with a sale, a receipt or a transfer between the baseline capture and the result application gets flagged, so the approver notices it while reviewing the ticket before applying. | Planned |
URD-CNT-008 | Should | A stock count ticket can be scoped to an ABC classification group, supporting rolling counts instead of counting the whole location at once. | Planned |
URD-CNT-009 | Must | A stock count ticket shares the inventory ticket lifecycle, walking only the part it needs and relabelling what it shows. • The steps: Draft → In progress → Completed, with no separate approval step. • In progress reads "Counting", Completed reads "Applied". • Stock only changes at the last step; the two earlier steps never touch it. | Planned |
URD-CNT-010 | Could | An item can appear in several different count tickets over time; each ticket captures its own baseline independently of the ones before it. | Planned |
URD-CNT-011 | Must | A stock count ticket's scope is set one of three ways: every item at the location, an ABC classification group, or a hand-picked list of items. | Planned |
URD-CNT-012 | Should | When an item already sitting in an unapplied count ticket is pulled into a second one, the system flags the overlapping scope right at ticket creation and lets the user carry on or drop that item - it warns, it does not forbid. | Planned |
URD-CNT-013 | Should | The stock count ticket list filters by location, ticket status, and created-date range. | Planned |
URD-CNT-014 | Must | An applied count ticket is final and follows the append-only rule of the inventory ticket frame. • The tally cannot be re-edited, the reason cannot be re-edited, and an applied ticket cannot be deleted. • A miscount is fixed by opening a new count ticket: the new ticket captures a new baseline and writes a new adjustment row. | Planned |
AC-CNT-01: Tallied 80 but 5 sold during the count - the variance is right, an overwrite would be wrong
| Given | When | Then |
|---|---|---|
| Item X at location A has 100 units when the count ticket is created (baseline = 100) | While the count ticket is still open, the merchant sells 5 more units (current stock now 95), and the counter physically tallies 80 units on the shelf | Variance = 80 - 100 = -20 |
| The same count ticket | The result is applied | Stock at the moment of applying = current stock 95 plus variance -20 = 75 - correct, because the 5 units already sold are not counted twice |
| If the system overwrote stock directly with the tally instead of working off the variance | The result is applied as an overwrite using the tally of 80 | Stock is set straight to 80 - a 5-unit mismatch against the correct 75, because the 5 units legitimately sold during the count get mistaken for still being on the shelf |
AC-CNT-02: Flagging activity for the approver's attention
| Given | When | Then |
|---|---|---|
| Item Y had no transaction at all while the count ticket was open | The result is applied | No activity flag; the variance reflects true shrinkage or a prior miscount |
| Item X had a sale, a receipt or a transfer between the baseline capture and the result application | The approver opens the count ticket to review before applying | Item X shows an "activity during count" flag for the approver to notice |
AC-CNT-03: An applied count ticket is final
| Given | When | Then |
|---|---|---|
| A count ticket has been applied and a miscount on one item turns up afterwards | The user reopens the applied ticket | Every field is locked, the tally and the reason cannot be edited, and there is no delete button |
| The same situation | The user opens a new count ticket for that item | The new ticket captures a fresh baseline from current stock and writes a new adjustment row into the ledger; the old ticket stays as it was |
VEN - Vendors Partial
Spec: PRD-VEN-001 · 9 requirements · 2 acceptance scenarios
A vendor is the party that sells to the merchant, but a vendor's name alone is not enough to build a correct purchase order at the right price and quantity. Each vendor-item pair carries its own purchasing data: the most recent receiving price, minimum order quantity, lead time, packaging spec - fully separate from the same item's price to customers. This is exactly the data source a purchase order draws on to suggest a price and unit once a merchant picks a vendor for a line item.
Creating and editing a vendor, and assigning items to a vendor, already work end-to-end. But the screens for tiered quantity pricing and for minimum order quantity do not exist yet - the data has a place to live on the backend, but a merchant cannot type it in through the interface.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-VEN-001 | Must | Merchant creates a vendor with a name, description, tax code, default transaction currency, location, and multiple contacts (name, email, phone). | Built |
URD-VEN-002 | Must | Each vendor-item pair (product variant or material) has its own record; this is the authoritative purchasing data source for that item, decoupled from the same item's selling price to customers. | Partial |
URD-VEN-003 | Must | A vendor-item record declares a minimum order quantity, an expected lead time in days, and an ordering unit. • The ordering unit is picked from the units already declared on the item itself. • The vendor row carries NO conversion factor of its own - two vendors must never disagree about how much one case of this item holds. | Partial |
URD-VEN-004 | Must | A vendor-item record stores the most recent receiving price - unit price, unit of measure, order timestamp, receipt timestamp; this price auto-updates every time a purchase order for that item is received. | Built |
URD-VEN-005 | Should | Merchant marks a vendor as preferred for a specific item; each item has at most one preferred vendor at a time - marking a new vendor automatically unmarks the old one. | Partial |
URD-VEN-006 | Should | A vendor-item record can declare tiered pricing by order quantity, each tier carrying its own minimum quantity, unit price, and unit of measure. | Partial |
URD-VEN-007 | Could | A vendor-item record stores the vendor's own item code, separate from the internal item code, for reconciling against invoices or delivery notes received from the vendor. | Planned |
URD-VEN-008 | Must | Vendors and vendor-item records are isolated per merchant and use soft-delete. | Built |
URD-VEN-009 | Should | A vendor has a status lifecycle (Active, Discontinued, Archived), defaulting to Active on creation; the vendor list filters by status and searches by name or tax code. | Partial |
AC-VEN-01: Preferred vendor auto-transfers
| Given | When | Then |
|---|---|---|
| Item X currently has vendor A marked preferred | Merchant marks vendor B as preferred for the same item X | Vendor A automatically loses its preferred mark; B becomes the sole preferred vendor for item X |
AC-VEN-02: Price auto-updates after receiving
| Given | When | Then |
|---|---|---|
| The vendor A - item X record has a most-recent price of 50,000 | A purchase order from A for item X is received at a unit price of 55,000 | The most-recent price on the A - X record updates to 55,000, with a new receipt timestamp |
PO - Purchase Orders Planned
Spec: PRD-PO-001- · 13 requirements · 3 acceptance scenarios
A purchase order is the document that binds a merchant to a vendor for one order: draft the lines, submit, then receive - possibly over multiple batches, since stock does not always arrive all at once. When a vendor is picked for the order, each line item should auto-populate its price and unit from that vendor-item pair's known purchasing data, instead of the merchant retyping it from scratch. Receiving posts to the ledger, updates cost, and writes the price back into the vendor data; when the actual received quantity or price differs from what was ordered, the system only warns rather than blocks, because stock that has already arrived at the warehouse still has to be recorded as received.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-PO-001 | Must | A purchase order moves through a lifecycle: draft, submitted, receiving, completed, closed; it can be canceled at any step that still allows edits. | Partial |
URD-PO-002 | Must | A purchase order only allows adding, editing, or removing line items while in draft; once it moves to the next status, line items lock. | Partial |
URD-PO-003 | Must | Creating a purchase order picks one vendor for the whole order; each line item declares an item, quantity, unit price, unit of measure. | Partial |
URD-PO-004 | Should | When a line item is added to a purchase order, the system auto-suggests the unit price and unit of measure from that vendor-item pair's record; the merchant can edit before saving. | Planned |
URD-PO-005 | Must | Receiving supports partial, multi-batch receipts against the same purchase order. • Each receiving batch records the quantity received per line item, adding to that line's cumulative received quantity. • The cumulative received quantity never exceeds the line's ordered quantity. | Partial |
URD-PO-006 | Must | Every receiving batch posts to the inventory ledger, adding to the receiving location's on-hand by exactly the received quantity, converted to the item's base unit. | Partial |
URD-PO-007 | Must | Every receiving batch updates the vendor-item record's most-recent price (unit price, unit of measure, order timestamp, receipt timestamp), so the next purchase order suggests the correct latest price. | Partial |
URD-PO-008 | Should | Receiving updates the item's weighted moving average cost at the receiving location, blending the new receiving price into the prior cost by quantity weight. | Planned |
URD-PO-009 | Should | The comparison between ordered quantity/price and actual received quantity/price shows as a warning, and never blocks receiving. • Quantity variance: the received quantity differs from the line's ordered quantity. • Price variance: the purchase order's unit price differs from the invoice price or the actual received price. | Planned |
URD-PO-010 | Must | A purchase order's accounts payable balance to the vendor is anchored to the received quantity, not the ordered quantity. | Planned |
URD-PO-011 | Could | A vendor invoice that arrives later with a different price only records the variance on the purchase order - it does not recompute the cost of prior receivings. | Planned |
URD-PO-012 | Must | Purchase orders are isolated per merchant and use soft-delete. | Partial |
URD-PO-013 | Must | Receiving less than what was ordered does not block anything and does not force the order closed. • The purchase order stays open after a short receipt; whatever additional quantity arrives gets recorded, and multiple vendor shipments accumulate into the cumulative received quantity. • The system never auto-generates a separate "shortage" document for the unreceived remainder. • A permanent shortfall (the vendor stops shipping) is not resolved by editing this purchase order - the merchant brings stock back to reality through a stock count (see CNT). | Planned |
AC-PO-01: Partial receiving, received quantity never exceeds ordered
| Given | When | Then |
|---|---|---|
| Line item ordered 100 units, nothing received yet | Receives batch 1 with 60 units | The line's cumulative received quantity is 60; 40 units remain unreceived |
| Same line item | Receives batch 2 with 40 units | The cumulative received quantity is 100, matching the ordered quantity |
AC-PO-02: Quantity and price variance warnings, receiving still allowed
| Given | When | Then |
|---|---|---|
| Line item ordered 100 units at a unit price of 50,000 | Actually receives 95 units at an invoice price of 52,000 | Receiving is still recorded normally; the system shows a warning for a 5-unit variance and a 2,000/unit price variance, without blocking the receiving |
| Purchase order has received 95 of 100 units | Views the order's accounts payable balance | The balance is computed off the 95 received units, not the 100 ordered |
AC-PO-03: A permanent shortfall - order stays open, resolved via a stock count
| Given | When | Then |
|---|---|---|
| Purchase order for 100 units, 60 units received across two batches | Vendor reports no more stock to ship | The purchase order stays open, not auto-closed by the system, and no "shortage" document is generated |
| Same situation | Merchant wants the missing 40 units off the books | Merchant creates a stock count ticket to bring stock in line with reality, without editing the old purchase order |
MAT - Materials Partial
Spec: PRD-MAT-001 · 8 requirements · 3 acceptance scenarios
A material is a raw ingredient, semi-finished good, or consumable used to make something else - milk, tea, flour, plastic cups. Each material decides for itself how it is managed: whether it is stock-tracked, whether it is split by lot, whether it has an expiry. A material does not carry a vendor's name on itself - the link to a vendor goes through a separate price list, since one material can be bought from multiple vendors at different prices and minimum order quantities. Creating a new material, assigning its initial stock across multiple locations, and picking its vendors can all be done in a single save.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-MAT-001 | Must | Each material has a type: raw material, semi-finished good, or consumable. • A semi-finished good is something the kitchen cooks up in a batch and scoops from over time: a pot of broth, fried shallots, pre-sliced boiled meat. It has its own stock, gets counted in a stock count, and has an expiry. • A semi-finished good with stock tracking on is where BOM explosion stops (see BOM) - selling an item deducts directly from the semi-finished good's own stock, it never continues down to the materials used to cook it.• A semi-finished good only gets stock when a production order declares it (see MFG). | Partial |
URD-MAT-002 | Must | Each material has its own set of inventory configuration flags: stock-tracked or not, lot-tracked or not, has an expiry or not, days-to-expiry from receiving, allows oversell or not, and its own low-stock threshold. | Partial |
URD-MAT-003 | Must | A material carries three unit roles: a base unit for storing stock, a purchasing unit for creating purchase orders, a selling unit for computing BOM quantities. These three roles convert through Inventory's shared unit engine. | Partial |
URD-MAT-004 | Must | A material does not carry a vendor's name on itself. To find out who sells this material, look it up through the vendor price list - one material can have multiple vendors, each with its own price, minimum order quantity, and lead time. | Partial |
URD-MAT-005 | Must | Creating a new material can be done in a single save, including stock and vendors. • Declare the material's information and inventory configuration flags. • Declare initial stock at multiple locations at once. • Pick one or more vendors, each with its own price. It's not "create the material, then open another screen to assign stock, then open yet another screen to assign vendors." | Partial |
URD-MAT-006 | Must | Editing a material's information (name, description, category, inventory configuration flags, vendors) is also done in a single save. Stock quantity is not edited through this screen - quantity only changes via receiving, selling, transfer, or stock count. | Partial |
URD-MAT-007 | Should | A per-material low-stock threshold applies if declared; otherwise the material falls back to the system's shared default threshold. | Partial |
URD-MAT-008 | Should | A material that is been archived can no longer have its information or configuration edited. To use it again, the material must be reactivated first. | Planned |
AC-MAT-01: Creating a material with stock at multiple locations and vendors in one save
| Given | When | Then |
|---|---|---|
| Owner drafts material "Fresh Milk", turns on stock tracking, declares initial stock of 20 liters at the Kitchen location and 10 liters at the Counter location, picks 2 vendors with different prices | Clicks Save | The material, its two per-location stock lines, and its two vendor links are all written together in a single save |
AC-MAT-02: No vendor field on the material itself
| Given | When | Then |
|---|---|---|
| Material "Tea" is currently linked to vendor A via the price list | Removes vendor A's link from the price list | Material "Tea" stays fully intact - it has no vendor field of its own to be affected, since the link only lives in the price list |
AC-MAT-03: An archived material is locked for edits
| Given | When | Then |
|---|---|---|
| Material "Glutinous Rice Flour" is archived | Tries to edit this material's low-stock threshold | The system refuses, requiring the material to be reactivated before editing |
BOM - Bill of Materials Planned
Spec: PRD-BOM-001 · 15 requirements · 4 acceptance scenarios
A bill of materials is the quantity of materials needed to make one item. Not every item goes through a bill of materials, and not every item with a bill of materials deducts stock the same way. Two things together decide the behavior: the item's type (regular stock item, kit, combo, or manufactured good) and a single flag saying whether that item is stock-tracked at all - there is no separate mode-selector flag; those two things alone are enough to derive the entire stock-deduction behavior.
| Item type | Stock-tracked | How stock is deducted |
|---|---|---|
| Storable (STORABLE) | Yes | Has its own stock, sells 1 deducts 1 on the item's own stock, never through a bill of materials |
| Storable (STORABLE) | No | Never touches stock |
| Kit (KIT) | Yes | Has no stock of its own - selling 1 item explodes the bill of materials, deducting materials directly |
| Kit (KIT) | No | Never touches stock |
| Combo | Parent is always No | The parent has no stock of its own - it explodes into its child items, each child deducting stock per its own flag |
| Manufactured (MANUFACTURED) | Yes | Has its own finished-good stock; its bill of materials is only used for production orders, never explodes on sale |
| Service / Entitlement (SERVICE / ENTITLEMENT) | Always No | Never touches stock, not allowed to turn on stock tracking |
Only Kit and Manufactured go through a bill of materials. A Combo has no bill of materials of its own - it explodes into child items, and whichever child item has a bill of materials explodes further on its own.
Where BOM explosion stops. This is the single most important rule in this whole area. BOM explosion stops at any component that has its own stock, and only continues through a component that has no stock of its own. The question that decides it is "does this component have its own stock?", not "does this component have a nested bill of materials?".
A pot of broth is the clearest example. In the morning the kitchen cooks a 50-litre pot from 5kg of bones - that is a production order, and it already deducted the 5kg of bones. At lunch, selling one bowl of pho ladles out 200ml, and the system deducts 200ml of broth stock and stops right there. If it kept going down to the bones, the 5kg of bones would get deducted a second time - bone stock would drop twice as fast, while broth stock would never fall no matter how empty the pot got.
| Component in the bill of materials | Has its own stock | Explodes further? |
|---|---|---|
| Raw material (bones, flour, milk) | Yes | No - stops, deducts the material's own stock |
| A semi-finished item with stock tracking on (a pot of broth, fried shallots) | Yes | No - stops, deducts the semi-finished item's own stock |
| A manufactured item with stock tracking on | Yes | No - stops, deducts its own finished-good stock |
| A prepared item with no stock of its own | No | Yes - continues into its own bill of materials |
This rule holds no matter how many levels deep the BOM tree goes: whichever level has its own stock is where it stops.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-BOM-001 | Must | An item's type plus its stock-tracked flag decides how stock is deducted, exactly per the matrix above; there is no other mode-selector flag. | Partial |
URD-BOM-002 | Must | A stock-tracked Storable item keeps its own stock, sells 1 deducts 1 on that same item, never going through a bill of materials whether or not one is attached. | Partial |
URD-BOM-003 | Must | A stock-tracked Kit does not keep its own stock - selling this item explodes the bill of materials and deducts materials directly; the item itself has no stock line of its own. | Partial |
URD-BOM-004 | Must | A Combo is always non-stock-tracked at the parent level - adding it to the cart explodes it into child items right then, each child deducting stock per its own flag. | Partial |
URD-BOM-005 | Must | A Manufactured item keeps its own finished-good stock; its bill of materials is only used when running a production order (see MFG), it never explodes on a direct sale. | Partial |
URD-BOM-006 | Must | Materials are checked and reserved the moment an item is added to the order, not at payment time. • With enough materials, the item is added to the cart and its materials switch to reserved right then. • Short on materials, the shortage is reported right at the counter, before the customer pays, while the cashier can still fix it. | Planned |
URD-BOM-007 | Must | A material with the oversell flag on is let through even without enough stock; other materials in the same bill of materials are still blocked normally if that flag is not on. | Partial |
URD-BOM-008 | Should | The available sellable quantity of an item with a bill of materials is the smallest of the ratios (material remaining / that material's per-unit requirement). When this number hits 0, the item shows out of stock on the menu. | Planned |
URD-BOM-009 | Must | BOM explosion stops at any component that has its own stock. • A component with its own stock (a raw material, a semi-finished item with stock tracking on, a manufactured item with stock tracking on) stops the explosion right there and deducts that component's own stock - it never continues down to the materials that made it, because those were already deducted at production. • It only continues further down when that component does not have its own stock. • The stop condition is "does it have its own stock", not "does it have a nested bill of materials". • This rule holds no matter how many levels deep the tree goes; there is a depth limit and a cycle guard (a bill of materials may not loop back to itself). • The reservation step at tap time and the actual ledger deduction step at completion must explode to the exact same depth, never diverging. | Planned |
URD-BOM-010 | Must | Deduction (materials leaving stock for real) defaults to happening as soon as the order is paid, regardless of whether the merchant uses a kitchen display screen. | Planned |
URD-BOM-011 | Should | A merchant using a kitchen display screen can choose to move the deduction moment to when the kitchen marks the item done, instead of the default of payment time. | Partial |
URD-BOM-012 | Must | If the kitchen cancels an item before marking it done, the materials already reserved for that item are released - they were never deducted. | Partial |
URD-BOM-013 | Must | The structure and lifecycle of a bill of materials. • A bill of materials is a list of component lines for one portion of the finished item; each line is a material or another item, with a quantity, a unit of measure, and an optional flag. • A component line marked optional does not block the sale when it runs short. • A bill of materials has three states: draft, active, retired; only the active one explodes at sale time. • An item has exactly one active bill of materials at a time - activating a new one automatically retires the old one. | Partial |
URD-BOM-014 | Must | Running out of a semi-finished item mid-shift sends its stock negative with a warning, and it must never fall through to the raw materials. • If the broth runs dry but the kitchen keeps selling, broth stock goes negative and the merchant gets a warning, so at close of day it is still possible to reconcile that the kitchen cooked more without logging a production order. • The system must never automatically fall back to deducting the bones, water, and spices - those were already deducted at the production order. • It never blocks the sale: the kitchen can still cook and the customer can still eat - blocking the sale would only push the cashier to find a workaround and leave the data even more wrong. | Planned |
URD-BOM-015 | Must | Changing a recipe quantity must never rewrite history. • Changing a bill of materials's quantity means adding a new version with an effective date, never overwriting the old one. • The quantity used to draw down stock is stamped onto the document itself at the moment it happens, so an old document always reads back the exact quantity that was in effect at that time. • Without this rule, changing a quantity today would change the cost of last month's orders, and old reports could never be reproduced. | Planned |
AC-BOM-01: The item-type matrix decides how stock is deducted
| Given | When | Then |
|---|---|---|
| Item "Canned Soda" is a stock-tracked Storable item, stock is 10 cans | Sells 1 can | The item's own stock drops to 9 cans; no bill of materials is ever consulted |
| Item "Chicken Rice Set" is a Combo of 1 rice portion and 1 chicken portion, both stock-tracked Storable items | Sells 1 set | The Combo has no stock of its own; the rice portion and the chicken portion each get deducted by 1 per their own stock |
AC-BOM-02: A milk tea out of milk - reported right at the counter
| Given | When | Then |
|---|---|---|
| Milk Tea is a stock-tracked Kit; its bill of materials needs 50ml fresh milk per cup; fresh milk has 30ml left, oversell not enabled | Cashier rings "1 milk tea" into the order at the counter | The system checks materials right then, sees a 20ml milk shortfall, reports out of material right at the counter, refusing to add the item to the cart - the customer has not paid yet |
| Same milk tea, milk is sufficient but the ice in the bill of materials has oversell enabled and ice stock is currently at 0 | Rings "1 milk tea" into the order | The item is still added to the cart - ice goes negative but is not blocked, because ice has the oversell flag on |
AC-BOM-03: Deduction per configuration, released back if the kitchen cancels
| Given | When | Then |
|---|---|---|
| A merchant has a kitchen display screen and has turned on deduction at kitchen-done; a milk tea order is already paid, its materials are currently reserved | Kitchen marks that milk tea "done" | The materials are deducted for real, leaving stock, no longer in the reserved state |
| Same order, but the kitchen presses "cancel item" instead of "done", before deduction | Kitchen cancels the item | The materials reserved for that item are released back, available goes back up, they were never deducted |
AC-BOM-04: A pot of broth - explosion stops at the semi-finished item, bones never get deducted twice
| Given | When | Then |
|---|---|---|
| Bone stock is 20kg at the start of the day. Broth is a semi-finished item with stock tracking on, unit litre. Broth BOM: 5kg bones + 55 litres water for 50 litres. Bowl-of-pho BOM: 200ml broth + 180g noodles + 60g beef | The kitchen runs a production order to cook the pot of broth and hits Complete, declaring an actual yield of 47 litres | Bone stock drops to 15kg (deducted exactly once). Broth stock goes up to 47 litres, carrying that batch's lot number and expiry. The cost per litre of broth is computed on the actual 47 litres produced, not the planned 50 |
| Next, broth stock is at 47 litres, bone stock is at 15kg | Sells 1 bowl of pho | Deducts 200ml of broth stock and stops there. Bone stock stays at 15kg - the system never continues down to the bones, water, or spices |
| Keeps selling until broth stock hits 0 | Sells one more bowl of pho | Broth stock goes negative 200ml with a warning. Bone stock stays unchanged - the system never automatically deducts bones to make up for it |
MFG - Production Orders Planned
Spec: PRD-MFG-001 · 9 requirements · 2 acceptance scenarios
A production order drives turning materials into a finished good per a bill of materials - baking one batch of bread, bottling one batch of sauce. An order passes through three milestones: drafted, running, done or canceled midway. Starting the run is when materials actually leave stock; completing is when the finished good actually enters stock, carrying that production run's own lot number and expiry.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-MFG-001 | Must | A production order moves through a lifecycle: drafted → running → done or canceled. It cannot jump straight from drafted to done, it must pass through running first. | Partial |
URD-MFG-002 | Must | Each production order runs against exactly one bill of materials, at one defined location, for a planned quantity. | Partial |
URD-MFG-003 | Must | Starting a production order reserves and deducts materials per the bill of materials times the planned quantity - materials leave stock right at the start, not held back until completion. | Planned |
URD-MFG-004 | Must | Completing a production order increases finished-good stock at the production location by the actual quantity produced. | Planned |
URD-MFG-005 | Must | Completing a production order stamps a lot number and an expiry on the finished-good lot just produced; expiry is computed from the completion date plus the finished good's configured shelf-life days. | Planned |
URD-MFG-006 | Should | Completing a production order records the actual yield and the loss - the variance between planned quantity and actual quantity produced. | Planned |
URD-MFG-007 | Must | Canceling a running production order releases back the materials already reserved but not yet deducted. | Planned |
URD-MFG-008 | Must | The production order management screen on the Client. • A list of orders scoped to the user's own merchant, filterable by status and by location. • Open an order for detail: the material list from the exploded bill of materials, the start and completion timestamps, and the finished lot it produced. | Planned |
URD-MFG-009 | Must | Finished-good cost is computed on the ACTUAL yield, not 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 but only 47 litres come out after evaporation, divide by 47, not by 50 - dividing by 50 reports a cost that is cheaper than reality. • Actual material consumption is allowed to deviate from the BOM quantities: the operator declares the real amount used, and the system warns on a large deviation but never blocks it. That deviation is exactly what a merchant needs to see to know how much the kitchen is wasting. | Planned |
AC-MFG-01: Valid lifecycle
| Given | When | Then |
|---|---|---|
| Production order "Bread Batch 001" is at drafted | Tries to jump straight to done | The system refuses - only drafted → running → done (or canceled) is allowed, no skipping |
AC-MFG-02: Starting deducts materials, completing adds finished goods with a lot
| Given | When | Then |
|---|---|---|
| Production order "Bread Batch 001" runs against a bill of materials needing 5kg flour and 2kg sugar for a planned 100 loaves; stock has enough material | Clicks Start | 5kg flour and 2kg sugar are deducted from stock right away |
| Then, declares 98 loaves actually produced, 2 loaves lost | Clicks Complete | Finished-good stock rises by 98 loaves at the production location, carrying a new lot number and an expiry computed from the finished good's configuration; actual yield of 98 and loss of 2 are recorded on the order |
RPL - Replenishment Partial
Spec: PRD-RPL-001 · 9 requirements · 3 acceptance scenarios
Replenishment suggests to a merchant when to reorder and how much, based on sell-through rate and vendor lead time. This is a suggestion tool, not yet a mandatory step in the purchase order flow - it sits later in Inventory's rollout order.
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-RPL-001 | Should | An item's reorder point is computed as average daily sell-through times vendor lead time, plus safety stock. | Planned |
URD-RPL-002 | Should | Default safety stock is computed by the max-minus-average formula: (highest single-day sales × longest lead time) minus (average daily sales × average lead time). | Planned |
URD-RPL-003 | Should | Merchant can manually enter safety stock per item, overriding the default formula's number. | Planned |
URD-RPL-004 | Could | A standard-deviation-based formula is reserved as a future advanced option, not used as the default formula since it needs a long enough sales history to be trustworthy. | Planned |
URD-RPL-005 | Should | An item is classified into group A, B, or C by consumption value - quantity sold times cost, not by revenue. | Planned |
URD-RPL-006 | Should | A/B/C classification thresholds default to 80/15/5 by share of consumption value; the merchant can adjust these thresholds. | Planned |
URD-RPL-007 | Could | An item's A/B/C group suggests a rolling stock-count frequency for that item - group A counted more often than group C. | Planned |
URD-RPL-008 | Should | An item below its reorder point comes with a suggested order quantity: enough to bring stock back to a level that covers one full delivery cycle, rounded UP to the preferred vendor's packaging spec and minimum order quantity. The merchant can edit that number. | Planned |
URD-RPL-009 | Should | A merchant selects several lines sharing the same vendor from the To Reorder list and raises a purchase order straight from it, carrying the selected lines and quantities across. The system never drafts or sends an order on its own. | Planned |
AC-RPL-01: Computing the default reorder point and safety stock
| Given | When | Then |
|---|---|---|
| Item X averages 20 units/day sold, sells up to 35 units on its busiest day; vendor lead time averages 3 days, up to 5 days at longest | System computes the reorder suggestion | Safety stock = (35 × 5) - (20 × 3) = 115; reorder point = (20 × 3) + 115 = 175 |
AC-RPL-02: ABC classification by configured thresholds
| Given | When | Then |
|---|---|---|
| Merchant adjusts the classification thresholds to 70/20/10 instead of the default 80/15/5 | The system runs ABC classification by consumption value | Items are grouped per the merchant's 70/20/10 thresholds, not the default |
AC-RPL-03: Suggested order quantity and raising a purchase order from the list
| Given | When | Then |
|---|---|---|
| An item has a packaging spec of 24 units per case, a minimum order quantity of 2 cases, and currently needs 30 units replenished | Merchant opens the To Reorder list | The suggested order quantity shows 2 cases (48 units), rounded up, not a loose 30 units |
| Three lines in the To Reorder list share the same vendor | Merchant selects those three lines and hits create purchase order | A new purchase order opens with exactly those three lines, at the suggested quantities, for the selected vendor; the system does not send the order on its own |
SYN - Multichannel Sync Planned
Spec: PRD-SYN-001 · 9 requirements · 2 acceptance scenarios
Multichannel sync keeps stock correct when a merchant sells at the counter and on e-commerce marketplaces at the same time. BANA acts as the single source of truth for stock; marketplaces only receive numbers from BANA, they never write stock back. This feature is designed for the future - BANA does not integrate with any marketplace yet, today's sales channels are all internal (dine-in, takeaway, delivery, kiosk).
| Code | Priority | Requirement | Status |
|---|---|---|---|
URD-SYN-001 | Should | BANA is the single source of truth for stock across all of a merchant's sales channels; stock pushes one-way from BANA down to marketplaces, never the other way. | Planned |
URD-SYN-002 | Must | The number pushed to a marketplace is available (on-hand minus reserved), not on-hand. | Planned |
URD-SYN-003 | Should | The number pushed to a marketplace subtracts an additional configurable buffer, per item or per channel; the default buffer is 0. | Planned |
URD-SYN-004 | Should | An order placed on a marketplace is pulled into BANA and deducts stock like a normal sale order. | Planned |
URD-SYN-005 | Must | After deducting stock for a marketplace order, BANA recomputes available and pushes the latest stock number back to that marketplace. | Planned |
URD-SYN-006 | Must | A marketplace storefront only accepts stock updates from exactly one source - BANA; no other system is allowed to write stock to that storefront, to avoid phantom stock. | Planned |
URD-SYN-007 | Should | Merchant connects a marketplace storefront to BANA as an external sales channel, tied to exactly one inventory location, and manages that channel themselves (pause, resume, remove). | Planned |
URD-SYN-008 | Should | Each item on the storefront is mapped to exactly one inventory item in BANA; an unmapped item never gets its stock synced. | Planned |
URD-SYN-009 | Should | Merchant views the sync history and sync state of each item on each channel, and manually re-pushes when a push has failed. | Planned |
AC-SYN-01: One-way stock push with a buffer
| Given | When | Then |
|---|---|---|
| Item Y has on-hand 50, reserved 5 (available 45), configured buffer 3 | The system syncs stock to a marketplace | The number pushed = 45 - 3 = 42, not 50 |
AC-SYN-02: A marketplace order pulls in, deducts, and pushes back
| Given | When | Then |
|---|---|---|
| A marketplace storefront is currently showing stock 42 for item Y (continuing from AC-SYN-01) | A marketplace order for 2 units comes in, pulled into BANA | BANA deducts stock like a normal sale, recomputes available, then pushes the new stock number back to that marketplace |
4. Constraints
| # | Constraint |
|---|---|
C‑01 | Each merchant has exactly one default inventory location. |
C‑02 | Stock is always stored in an item's base unit. Every quantity on a document is converted to the base unit before posting to the ledger. |
C‑03 | Available = On-hand - Reserved. This invariant holds at every point in time. |
C‑04 | Blocking overselling is the default. Only an item with allow oversell on can sell past stock. |
C‑05 | Operations (sale, receiving, transfer, stock count...) only append to the inventory ledger, correcting mistakes with a new adjustment line, never editing or deleting a past line. The operations team has a separate escape hatch to edit/delete directly when data needs rescuing - an internal tool, not a merchant feature (see URD-LDG-009). |
C‑06 | The same document, however many times it is reprocessed, posts to the ledger exactly once per stock record. |
C‑07 | Cost is per merchant, quantity is per location. An internal transfer never changes cost. |
C‑08 | An inventory ticket only affects stock once completed. |
C‑09 | Every quantity and money calculation uses 4-decimal precision. |
C‑10 | Every record belongs to one merchant and uses soft-delete. |
5. Exclusions
The Inventory module deliberately does not do the following.
| # | Not in scope |
|---|---|
N‑01 | Shelf- or bin-level warehouse management (bin/locator) - locations stop at warehouse and zone level. |
N‑02 | FIFO lot-layer costing. The system uses weighted moving average and standard cost. |
N‑03 | LIFO costing - not supported. |
N‑04 | Multi-currency inventory valuation. |
N‑05 | Offline-first operation at the counter. |
N‑06 | Accounting - the Inventory module records quantity and cost, it does not generate accounting entries. |
Version history
| Date | Author | Description | Ver |
|---|---|---|---|
| 2026-07-13 | Phát Nguyễn | Full rewrite. Restructured 17 features around business capability. Added the "How the system thinks about stock" chapter. | v2.0 |
| 2026-07-13 | Phát Nguyễn | MFG: settled when materials are deducted - starting a run only reserves, completing is what posts to the ledger (URD-MFG-003, URD-MFG-004), so that URD-MFG-007 and URD-RSV-004/005 actually mean something. Added URD-MFG-008 (production order management screen) and a unique order code to URD-MFG-002. | v2.1 |
| 2026-07-13 | Phát Nguyễn | BOM: rewrote URD-BOM-009 - BOM explosion now STOPS at any component with its own stock, replacing the old rule of "drilling all the way to raw materials" (the old rule deducted a batch's materials twice). Added URD-BOM-014 (a semi-finished item running out goes negative with a warning, never falls through to raw materials) and URD-BOM-015 (recipe quantities are versioned, snapshotted onto the document). MAT-001: clarified that a stock-tracked semi-finished item is the explosion stop point. Added URD-MFG-009 (cost computed on actual yield). | v2.2 |