Packet pricing¶
Each packet (physical stock location) carries two computed price fields:
| Field | Description |
|---|---|
itemValue |
Price per unit (computed) |
totalValue |
itemValue × count |
Both are recalculated automatically whenever a StockOperation is saved.
Calculation order (Packet.calculate())¶
The algorithm runs in three steps:
1. FIFO from priced inflow operations¶
Operations of type add, trans_in, and buy with unit_price > 0 form price layers ordered chronologically. Outflow operations (remove, trans_out, service, sell) consume the oldest layers first (FIFO).
The remaining layers after consuming all outflows give a FIFO value and a FIFO count.
2. Average price for unaccounted units¶
If packet.count > FIFO count (i.e. some units have no matching priced inflow), the gap is filled using the average purchase price across all priced inflow operations:
unaccounted = packet.count − FIFO count
average_price = Σ(quantity × unit_price) / Σ(quantity) [over priced inflows]
total_value = FIFO value + unaccounted × average_price
3. Internal price fallback¶
If there are no priced inflow operations at all (step 2 denominator is zero), the gap is filled using component.internal_price:
total_value = FIFO value + unaccounted × component.internal_price
If internal_price is also not set, the unaccounted units are valued at 0 and itemValue will be 0.
Price source badge¶
The packet detail page shows a badge indicating which pricing method was used:
| Badge | Meaning |
|---|---|
| Purchase (FIFO) | itemValue comes from priced buy/add operations |
| Internal price | itemValue comes from component.internal_price fallback |
| ⚠ Internal price (not applied) | internal_price is set but itemValue is still 0 (packet may be empty or needs recalculation) |
| No price | Neither purchase price nor internal_price is available |
Inventory operations¶
When an inventory (operation_type = 'inventory') is recorded, the current itemValue of the packet is snapshotted into the operation's metadata:
{
"counted_quantity": 10,
"recorded_quantity": 8,
"counted_price": 45.50
}
counted_price captures the price at the moment of counting — it will not change even if subsequent purchases alter the packet's itemValue.
Inventory PDF report¶
The stocktaking PDF report (GET /api/warehouse/stocktaking/<id>/report/) uses counted_price from metadata to value each inventoried packet:
value = counted_quantity × counted_price
For inventory operations recorded before counted_price was introduced, the report falls back to the packet's current itemValue. This fallback will be removed once all legacy operations have been superseded.
Non-inventoried packets are valued using their current itemValue.
component.internal_price¶
internal_price is a manually maintained field on the component. It represents the agreed internal cost per unit and is used as a fallback when no purchase price history exists.
It can be set via the component edit form or via the MCP tool update_component.
Related topics¶
- Packets & stock operations — packet states and the full operation type list
- Inventory (stocktaking) — where
counted_pricesnapshots come from - Purchases — where priced
buyoperations originate