10 · Costing — stock cost
Updated
Solution(2) layer. Depends on Inventory and
Purchasing — and only on them.
Purpose#
Two questions that Inventory does not answer: what price to
write off at and what amount is on hand. The Stock register stores
quantity by cell; value lives here and is tracked by item, because moving stock
between cells of one store does not change money.
The model has no documents of its own. Movements are written by receipt postings (Purchasing) and output postings (Production); cost is written off by sale posting.
The process-level description is costing.
What is created (object map)#
| Kind | Name | Purpose |
|---|---|---|
| Model | Costing |
module model, layer Solution(2) |
| Dictionary | CostingSettings |
method settings and the standard cost price type (singleton) |
| Register | ItemCostFifo |
FIFO layers by item: quantity and amount |
| Register | InventoryValue |
stock: quantity and value |
| TotalDriver | CostingIssue |
layer write-off on issue |
| TotalDriver | CostingValuation |
stock valuation |
| Service | StandardCostService |
standard cost on a date (a price of the type CostingSettings.StandardPriceType) |
| Service | SurplusCostingService |
inventory surplus valuation |
| NumberSequence | CostingSettingsSeq |
numbering |
| Job | MoveStandardCosts |
moves rows of the retired standard cost dictionary into price history |
| CustomReport | StandardCostVariance |
write-offs against the standard on the date |
| Menu | menu.json |
module menu |
| — reused — | Item, Store, Stock (Inventory), Money |
not created here |
Total: 1 dictionary, 2 registers, 2 totals drivers, 2 services, 1 job, 1 report, and 6 integration tests. No documents.
Costing has no reports, and this is not an omission in the description. An earlier revision listed five: stock valuation, cost breakdown by BOM, gross margin, procurement vs consumption, and labor allocation. None of them is built, and the mechanism of user reports over movement tables by SQL is not used for them. A list of intentions in the object map reads like an inventory and has already been read that way once.
model.json#
Solution(2) layer. Dependencies: Inventory, Purchasing.
The edge direction matters. Costing does not depend on Sales; the
opposite is true: Sales declares a dependency on Costing. Therefore gross margin
is calculated in Sales by GrossMarginService, not here. The reverse edge would
close a cycle. An earlier revision declared dependencies on Sales, Production,
and HR; none of the three is present in model.json.
Production is also not a dependency, although its postings write to these
registers: access goes through runtime service resolution, without a declared
edge. Otherwise Production ↔ Costing would lock each other in a cycle.
Dictionaries#
CostingSettings#
One record per tenant: valuation method, rounding, and StandardPriceType —
the price type whose prices are the standard (planned) unit cost.
The standard cost is not a dictionary of its own (since Costing 2.0.0): it is a
dated price in Inventory price history, selected on the
document date by its [from, to) window. Empty StandardPriceType means the
oldest active Base type with direction Internal.
Registers#
| Register | Dimensions | Analytics | Resources |
|---|---|---|---|
ItemCostFifo |
Item |
— | Quantity, Amount |
InventoryValue |
— | Item |
Qty, Value |
ItemCostFifo is the layer queue: a receipt puts a layer with quantity and
amount; an issue removes it by the "first in, first out" rule. The breakdown is
a dimension, not an analytic, because the totals driver calculates the
write-off and the driver sees only register coordinates.
InventoryValue answers "what amount is on hand", and an analytic breakdown is
enough: no driver reads it.
Example#
Item Widget, FIFO method. Two receipts from Purchasing
put layers, then a realization from Sales
issues 120 pcs; the CostingIssue driver removes them from the oldest layer
first.
| Event | ItemCostFifo — layers (Qty @ Amount) |
ItemCostFifo — total (Quantity / Amount) |
InventoryValue (Qty / Value) |
|---|---|---|---|
| Before scenario | — | 0 / 0.00 | 0 / 0.00 |
| Receipt No. 1: 100 pcs at 10.00 | Layer1: 100 @ 1000.00 | 100 / 1000.00 | 100 / 1000.00 |
| Receipt No. 2: 50 pcs at 12.00 | Layer1: 100 @ 1000.00 · Layer2: 50 @ 600.00 | 150 / 1600.00 | 150 / 1600.00 |
Realization: issue 120 pcs (CostingIssue: all Layer1 first, then 20 from Layer2) |
Layer1: 0 @ 0.00 · Layer2: 30 @ 360.00 | 30 / 360.00 | 30 / 360.00 |
Cost of the 120 pcs written off = 1000.00 (all Layer1) + 240.00 (20 of 50
at the Layer2 price, 20 × 12.00) = 1240.00. This is the amount the
realization assigns to revenue as COGS (GrossMarginService),
not the average 1600.00 / 150 = 10.67: FIFO writes off layers at their entry
price, not at a general average.
Surplus (SurplusCostingService). Inventory count finds another 5 pcs of
Widget that were not in any receipt. The surplus is valued at the current
average of the open layers, 360.00 / 30 = 12.00: ItemCostFifo gets
Layer3: 5 @ 60.00, and InventoryValue becomes 35 / 420.00. An item with
no layers at all is valued at its standard cost on the
count date; with no standard either, the layer is created at zero. Without this service the surplus would be written off at zero
cost and would understate the cost of the next sale by the same amount.
Totals drivers#
CostingIssue removes layers on issue. CostingValuation calculates stock
value. The driver changes the implementation of the calculation, not the
contract: the valuation method is switched by a setting, and consumers do not
change.
Services#
StandardCostService— standard cost on a date, plan-vs-actual variance.SurplusCostingService— valuation of surplus found by inventory count: there is no receipt layer, so cost comes from a separate rule.
Business logic and invariants#
Cost is per item, quantity is per cell. Stock knows where the item is;
InventoryValue knows how much it is worth. Moving between cells of one store
does not change value and should not generate a movement here.
Value is written off only through movements. Derived registers are not written directly when a document is reposted.
Receipt value is in the entity's currency. ReceiptCostTx and
ReceiptFifoTx take each line amount × PurchaseOrder.ExchangeRate (the
supplier currency's rate on the receipt date; 1 when the supplier bills in the
entity's own currency), so cost lots, InventoryValue and the cost of sales
they feed are never in a foreign currency.
Surplus is not free. Goods found by inventory count have no layer; without
SurplusCostingService, they would be written off at zero cost and understate
cost of sales.
Trap that cost time. A totals driver does not see movements written by
bypassing the register movement manager in an event handler: it reads document
transactions. Inventory once wrote off an amount that way, and the balance fell
from 10 to 7 while no rows appeared in ItemCostFifo.
Menu#
Totals (ItemCostFifo, InventoryValue) · Settings (CostingSettings) · Reports (StandardCostVariance).
Links with other modules#
- Outgoing: Inventory — item and store; Purchasing — receipt as the source of layers.
- Incoming: Sales declares a dependency and calculates margin; Production writes to the registers through runtime resolution, without an edge.
- Ledger: Costing writes the cost legs of stock documents into
GLitself —InventoryCostGLServiceturns theItemCostFifocost a document issued or received into the document's own GL movements (write-off ofGoodsIssue,StockAdjustment,StockCount; surplus; Sales cost of sales and Production WIP call it too). See Who writes the general ledger.
Objects#
Generated from workspace metadata by node tools/gen-reference.mjs; edit the metadata, not this block.
Dictionaries#
CostingSettings — Costing settings
| Field | Caption | Type | Required |
|---|---|---|---|
CostingMethod |
Costing method (AVG/FIFO) | String(128) | No |
ID |
ID | String(50) | Yes |
RoundCosts |
Round costs | Boolean | No |
StandardPriceType |
Standard cost price type | → PriceType |
No |
ValuationMethod |
Costing method | CostingMethodKindEdt |
No |
StandardCost — Standard cost
Dated standard unit cost of an item. Gaps are allowed; overlapping windows of the same item are not.
| Field | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
Yes |
EffectiveFrom |
Effective from | DateTime | Yes |
EffectiveTo |
Effective to | DateTime | No |
ID |
Code | String(50) | Yes |
Item |
Item | → Item |
Yes |
Registers#
InventoryValue — Inventory value
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
Qty |
Qty | Qty |
— |
Value |
Value | Money |
— |
Analytics:
| Analytic | Caption | Type | Required |
|---|---|---|---|
InventoryValue-Item |
InventoryValue-Item | — | Yes |
ItemCostFifo — Item cost (FIFO)
Dimensions:
| Dimension | Caption | Type | Required |
|---|---|---|---|
Item |
Item | → Item |
— |
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
— |
Quantity |
Quantity | Qty |
— |