Inventory
One stock count per location, kept in batches with cost and expiry, that the shop, the register, recipes and purchasing all draw from.
Inventory holds every quantity a business has: stock items, the locations they sit at (a branch's stock room, a warehouse), and the batches each unit arrived in, with its cost, expiry date and lot code. Catalog products, POS recipe ingredients and Supply chain orders all point at an Inventory item, so the online shop, the register and purchasing take from the same batches, oldest first. Inventory doesn't know what is sold or bought: it has no products, prices, suppliers or purchase orders. Those Modules add their own facts to the Inventory page instead, such as what an item sells as, which recipes use it and how much is on order.
What it handles
- Stock that leaves in the order it arrived. A sale takes the oldest batch first, at that batch's cost, so cost of goods matches what was paid. A batch past its expiry date can't be sold, though it still counts as on hand. Stock moved to another location keeps its arrival date, cost, expiry and lot code, so it's still sold first there. Tested: "stock leaves first in, first out, at its batch's cost (P1.2, S3, S7)"
- Selling what isn't there yet. An item set to keep selling at zero records the extra units as owed. The next delivery pays them back first, at the new batch's cost. Tested: "units sold past zero are paid back by the next arrival (P1.3, S8)"
- Online baskets holding stock while the customer pays. A checkout holds its units, and a basket with one short line holds nothing at all. The register can't sell held units. A payment that lands after a batch expired takes what is still sellable and records the rest as owed, and a retried payment writes its sale once. When ten paid orders race for one unit of a keep-selling item, the first takes it and the other nine owe theirs instead of being refused. Tested: "checkout holds (D8, D9, S1)" Tested: "payment beats a reservation (round 4)"
- Counting while the shop is open. A stocktake remembers what was expected when the count started, so sales made during the count aren't written off as missing. A shortfall comes out of the oldest batch, a submitted count can't be submitted again, and a second count of the same item at the same location is refused until the first closes. Tested: "a stocktake keeps sales made while staff were counting (P1.8, S7)" Tested: "count lines and overlapping counts (findings 3, 11)"
- Retries and double returns. A delivery sent twice with the same key lands once. The same key used for a different write is refused, not replayed. A sale given back twice returns each unit once. Tested: "idempotent writes" Tested: "idempotency keys (finding 10)" Tested: "a sale given back twice gives each unit back once"
- Who may change stock. Staff without
inventory:writecan't adjust stock, and adding or editing items and locations needsinventory:manage. Staff tied to one branch adjust, move, count and read only their own branch's stock room. They can see shared rooms such as a warehouse but can't edit or stock them. Tested: "who may change stock (P4.4)" - Items sold in whole units. An item counted in pieces refuses 1.5 on every path (receiving, selling, moving, adjusting, discarding, counting and returns) and writes nothing. Tested: "whole-unit items refuse fractions on every path"
What you build on top
- What draws from it. Switch on Catalog to sell stocked products, POS for recipes that use ingredients, and Supply chain for suppliers, purchase orders, receiving and low-stock alerts.
- Facts on the Inventory page. Inventory has no slots. Another Module, or your app, adds a column or a filter to the Inventory page from its own server code, and Inventory never imports it. See Build on a Module.
- Units. Every quantity is in the item's use unit (grams, millilitres, pieces) and every cost is per use unit. Code holding a quantity in packs or kilos converts it once before handing it to Inventory.
- Opening stock. Seed stock by receiving it, the way a delivery arrives, not by writing stock rows. A level written straight into the tables stays hidden from every screen. See Customize seeds.
- Your words. Rename "Inventory" (a kitchen's "Stock", say) with an override.
- In code. Your coding agent imports Inventory only through its public API:
index.tsandserver/index.tsinapps/web/src/modules/inventory/. See Build on a Module.
Limits
- Inventory doesn't buy stock. Suppliers, purchase orders and receiving against an order are Supply chain's.
- Nothing discards expired stock on a schedule. Expired batches stop selling but stay on hand until staff discard them.
- Each stock write stands on its own: there's no transaction across several Inventory writes. A Module that must reserve stock as part of its own write (checkout placing an order) builds that write around Inventory's claim.
Facts
- Kind: feature Module, can be switched on and off
- Needs: Audit log, Branches, Formatting, i18n, Identity, Infra, Standard UI
- Needed by: Catalog, POS, Storefront, Supply chain
- Adds to these when they're on: Nothing
- Added to by: Nothing
- Off by default for these business types: Tour operator, Online classes, Car hire
Pages
| Where | Path |
|---|---|
| Admin | /admin/inventory |
Permissions
Admin holds every key. The other roles listed hold it when the app first gets the Module; after that, roles are yours to change under Settings → Roles & Permissions.
| Permission | What it allows | Organization roles that start with it |
|---|---|---|
inventory:write | Receive, adjust, count, discard and move stock. | Admin, Manager, Staff |
inventory:manage | Manage stock items, their units and stock locations. | Admin, Manager |
Data
- Tables:
stock_location,stock_item,stock_level,stock_batch,stock_operation,stock_movement,stock_hold,stock_count,stock_count_line