inventory — Stock and What It Is Worth · Area Specification
last conformance run: 5/5 scenarios pass · 6 acts mapped 2026-10-24 18:56 UTC
Spec
Status: 0.1. What the stock on hand is worth and what each sale cost: every stock move kept, the moving average cost derived from what was paid, cost of goods sold on each shipment, physical counts with reasons, and a valuation that ties to the books. It completes the cost side that the p2p area's receipts start (P2P-5, P2P-13).
1. Calibration: who this is for
The same small business as o2c and p2p: one stockroom, one currency, a few hundred items at most, stock bought on purchase orders and sold on orders. The owner wants to know what the shelves are worth, what a sale actually earned, and where the missing units went. Their accountant wants the inventory balance and cost of goods sold to come from the record, not from a spreadsheet at year end.
2. Scope
In: the stock move record, the moving average cost, the cost of opening stock, physical counts, cost of goods sold per shipment, valuation as of a date, and their postings.
Out (§9): FIFO and specific identification, landed cost, several stockrooms, lots and serial numbers, reorder points, and standard costing.
3. Entities and lifecycles
- stock move (core's): every change to the quantity on hand, as a row: the item, the day, the signed quantity, what it was (opening, receipt, shipment, return, count, adjustment), its reference (GR-0001, D-0003) and, when the move itself knows it, the unit cost. Only core's stock door writes moves, so they always add up to what is on hand. A move is never edited.
- moving average cost: derived per item by replaying its moves: a costed inflow updates the average; an outflow leaves at the average of that moment; a count difference is valued at it. Never typed.
- opening cost: what one unit of stock that came in without a cost was worth (stock on hand before the record, or a correction received by hand). Stated once per item, with a reason; the record then derives everything after it.
- count: what a person counted on the shelf for one item on a day, with a reason. The difference from the record becomes a count move.
4. The acts
Writes: set_opening_cost, count_stock.
Reads: stock (every item: on hand, average cost, value), item_stock (one item's moves with the running quantity, average and value), valuation (as of a date, tied to the books), cost_of_sales (each shipment line in a period: sold at, cost, margin).
Moves themselves are written by the acts that move stock: p2p's receive_goods, o2c's ship_order and record_return, core's receive_stock, and count_stock here.
5. Invariants
- STK-1 Money is integer cents; one currency.
- STK-2 Every change to the quantity on hand is a stock move, written by core's stock door; the moves of an item add up to its quantity on hand.
- STK-3 Cost is derived, never typed: the moving average from costed inflows. The only cost a person or agent states is the opening cost of stock that came in without one, with a reason.
- STK-4 Each unit leaves at the average of that moment; cost of goods sold is that cost, per shipment line.
- STK-5 Stock without a cost is named, with its quantity, never valued at zero in silence. Its sales are left out of cost of goods sold and listed as left out.
- STK-6 A count records what was counted, with a reason; the difference is a move valued at the average, posted to Inventory Adjustments.
- STK-7 Valuation as of a date equals the Inventory balance the journal derives for that date.
- STK-8 The journal derives balanced: a shipment debits Cost of Goods Sold and credits Inventory; a return reverses it; a count difference moves between Inventory and Inventory Adjustments; costed opening stock debits Inventory and credits Opening Balance Equity. Goods received on an order are posted by the p2p area (P2P-13), never twice.
- STK-9 Every write is a logged command with an actor; refusals are logged too.
6. Required read models
stock (with the uncosted quantity per item), item_stock, valuation (per item and in total, with the journal's Inventory balance beside it), cost_of_sales (per shipment line, with the totals).
7. Contract vs freedom
Contract: moves only through core; the moving average; costs derived except the opening cost; uncosted stock named; valuation ties to the journal. Freedom: how a count is reported, how the pages sort.
8. Conformance (scenarios)
01 the average moves with receipts and shipments leave at it · 02 opening stock without a cost is named and left out until its cost is stated, then everything derives · 03 a count records the shrinkage at the average · 04 a return comes back at the average · 05 valuation ties to the journal's Inventory balance.
9. Deferred, with reasons
| Item | Why deferred, not rejected |
|---|---|
| FIFO, specific identification | Moving average first; a method choice needs a design partner with a reason (tax or industry). |
| Landed cost | Freight allocated across items needs its own design; freight posts as a service line today. |
| Several stockrooms | Transfers and per-location stock; one stockroom fits the calibration. |
| Lots, serial numbers, expiry | Traceability for regulated goods; a different customer. |
| Reorder points | Advice, not record. The record says what is on hand and on order. |
| Standard costing | A manufacturer's tool; the calibration buys and resells. |
---
Change log: 0.1 (2026-10-24) — first version: stock moves, moving average, opening cost, counts, cost of goods sold, valuation tied to the books.
Acts
| Act | Kind | Required |
|---|---|---|
set_opening_cost | write | item unit_cost reason |
count_stock | write | item counted reason |
stock | read | none |
item_stock | read | item |
valuation | read | none |
cost_of_sales | read | none |
Invariants
| Id | Invariant |
|---|---|
STK-1 | Money is integer cents; one currency. |
STK-2 | Every change to the quantity on hand is a stock move, written by core's stock door; the moves add up to on hand. |
STK-3 | Cost is derived (moving average), never typed, except the opening cost of stock that came in without one. |
STK-4 | Each unit leaves at the average of that moment; cost of goods sold is that cost, per shipment line. |
STK-5 | Stock without a cost is named with its quantity, never valued at zero in silence. |
STK-6 | A count records what was counted, with a reason; the difference is valued at the average. |
STK-7 | Valuation as of a date equals the journal's Inventory balance for that date. |
STK-8 | The journal derives balanced: shipments to cost of goods sold, counts to adjustments, opening stock to equity. |
STK-9 | Every write is a logged command with an actor; refusals are logged too. |
Scenarios
Each scenario is a file of acts with expected outcomes. ok means the act must succeed with the listed fields; refused means the act must be refused with a sentence containing the listed text. Refusals are contract.
the average moves with what was paid; shipments leave at it pass
create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_po | ok | |
| 2 | issue_po | ok | |
| 3 | receive_goods | ok | 40 arrive at $48.00: a costed inflow |
| 4 | create_po | ok | |
| 5 | issue_po | ok | |
| 6 | receive_goods | ok | 20 arrive at $54.00: a costed inflow |
| 7 | item_stock | ok qty=60 average_cost=5000 value=300000 | |
| 8 | create_order | ok | |
| 9 | confirm_order | ok | |
| 10 | ship_order | ok | each unit leaves at the average of this moment |
| 11 | cost_of_sales | ok revenue=80000 cost=50000 margin=30000 | sold at $80, cost $50 |
| 12 | valuation | ok value=250000 ties=true | STK-7 the 50 left, worth $2,500, tie to Inventory in the books |
stock without a cost is named until its opening cost is stated pass
create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | item_stock | ok cost_known=false uncosted_qty=10 | |
| 2 | create_order | ok | |
| 3 | confirm_order | ok | |
| 4 | ship_order | ok | each unit leaves at the average of this moment |
| 5 | cost_of_sales | ok count=1 cost=0 | STK-5 left out and listed, never costed at zero in silence |
| 6 | set_opening_cost | refused reason is required | |
| 7 | set_opening_cost | ok cost_known=true value=27000 | |
| 8 | cost_of_sales | ok cost=18000 margin=14000 | the earlier sale is now costed: derived, not re-entered |
| 9 | valuation | ok ties=true |
a count records the shrinkage at the average pass
create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_po | ok | |
| 2 | issue_po | ok | |
| 3 | receive_goods | ok | 30 arrive at $48.00: a costed inflow |
| 4 | count_stock | ok difference=-2 difference_value=-9600 | |
| 5 | count_stock | ok difference=0 | |
| 6 | count_stock | refused zero or more | |
| 7 | valuation | ok value=134400 ties=true |
a return comes back at the average pass
create_customer, create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_po | ok | |
| 2 | issue_po | ok | |
| 3 | receive_goods | ok | 20 arrive at $50.00: a costed inflow |
| 4 | create_order | ok | |
| 5 | confirm_order | ok | |
| 6 | ship_order | ok | each unit leaves at the average of this moment |
| 7 | record_return | ok | |
| 8 | item_stock | ok qty=17 average_cost=5000 value=85000 | |
| 9 | valuation | ok ties=true |
valuation on a past day ties to the books on that day pass
create_item| # | Act | Expect | Why |
|---|---|---|---|
| 1 | create_po | ok | |
| 2 | issue_po | ok | |
| 3 | receive_goods | ok | |
| 4 | receive_goods | ok | |
| 5 | valuation | ok value=144000 ties=true | |
| 6 | valuation | ok value=240000 ties=true |
These pages are generated from the files in specs/ in the repository; scenarios are executed by src/conformance.js on every build. Written and maintained by Peter Varga, who also runs Portland AI Works. github.com/alviso/saybooks, AGPL-3.0.