Processes and shelf life¶
Physical flows such as expiry, inspections, and returns. Guide: Shelf life and inventory processes.
ShelfLife
¶
ShelfLife(
shelf_life_days: int,
opening_lots: DataFrame,
opening_expiry_handling: str = "reject",
*,
sku_column: str = "unique_id",
)
Bases: InventoryProcess
FIFO shelf life as an InventoryProcess.
Stock is tracked as dated lots in a FIFOLotLedger. Each period:
1. before demand, lots aged ``shelf_life_days`` or more expire; the
units leave on_hand before receipts, the order decision and demand,
and are recorded in ``expired_units``;
2. every receipt (pipeline arrivals, then zero-lead-time receipts)
becomes a lot dated with the current period date;
3. after demand, fulfilled demand and cleared backlog consume the
oldest lots first, and the ledger is checked against on_hand.
Other processes' flows and callback adjustments are mirrored: removals
consume the oldest lots, and additions need an explicit, unexpired
received_date (never invented).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shelf_life_days
|
int
|
calendar days a lot remains usable (integer >= 1). |
required |
opening_lots
|
DataFrame
|
DataFrame with |
required |
opening_expiry_handling
|
str
|
|
'reject'
|
sku_column
|
str
|
SKU column of |
'unique_id'
|
ledger holds the lots of the latest run (the last branch of a
comparison).
InventoryProcess
¶
Base class for a physical process that changes on-hand stock.
Subclasses set name (unique within a run) and flows (a tuple of
Flow), then override only the hooks they need. The engine calls the
exact instances passed to run and resets them before every run, so
run-local state (a lot ledger, a random generator) belongs in reset.
Flow hooks return ProcessFlows or None:
before_demand(context) start of period, before receipts and ordering
after_demand(context) after demand is served, before callbacks
Observation hooks return None:
reset(context) before the first period of each run
on_receipt(context) stock just received (context.received)
check(context) assert an internal mirror of on-hand stock;
runs after demand and after each accepted
callback adjustment batch
Composition hooks, for processes that mirror stock (for example lots): validate_stock_change(change, context) raise to reject a change on_stock_change(change, context) mirror it; may return evidence text for the audit
Processes run in list order; each sees the stock left by the ones before it. A process is told about every other process's flows and every callback adjustment, but not about its own flows.
before_demand
¶
before_demand(
context: ProcessContext,
) -> Optional[ProcessFlows]
Report flows at the start of the period.
on_receipt
¶
on_receipt(context: ProcessContext) -> None
Observe units received this period (context.received).
after_demand
¶
after_demand(
context: ProcessContext,
) -> Optional[ProcessFlows]
Report flows after demand is served.
validate_stock_change
¶
validate_stock_change(
change: StockChange, context: ProcessContext
) -> None
Raise to reject an on-hand change before anything is applied.
on_stock_change
¶
on_stock_change(
change: StockChange, context: ProcessContext
) -> Optional[str]
Mirror an on-hand change made by another process or a callback.
check
¶
check(context: ProcessContext) -> None
Assert that internal state still agrees with context.on_hand.
Flow
dataclass
¶
One named, signed physical flow that a process may report.
direction is "inflow" (adds on-hand units) or "outflow"
(removes on-hand units). category="expiry" marks an outflow as
expiry: it is recorded in the canonical expired_units event table column
and priced by waste_cost. Every other flow is "general" and is
recorded in process_inflow_units / process_outflow_units.
ProcessFlows
¶
ProcessFlows(
quantities: Mapping[str, object],
received_dates: Optional[Mapping[str, object]] = None,
)
Quantities a process reports for its declared flows at one phase.
quantities maps a declared flow name to nonnegative per-SKU units,
given as a mapping or a pd.Series indexed by SKU. Omitted SKUs and
zero values mean no flow. The flow's direction comes from its Flow
declaration, never from the sign of the quantity.
received_dates optionally gives inflows a lot date, per flow, as one
timestamp or per SKU (a mapping or a pd.Series indexed by SKU).
Processes that track lot ages (such as ShelfLife) require it for every
inflow they observe.
ProcessContext
dataclass
¶
ProcessContext(
phase: str,
period: int,
demand_period: Optional[int],
date: Timestamp,
run_window: str,
unique_id: Index,
on_hand: Series,
backorders: Series,
on_order: Series,
received: Optional[Series] = None,
demand: Optional[Series] = None,
fulfilled: Optional[Series] = None,
backorders_fulfilled: Optional[Series] = None,
)
Read-only per-SKU view of the run at one process phase.
Every quantity is a float pd.Series indexed by SKU identifier, in the
state's SKU order. received, demand, fulfilled and
backorders_fulfilled are None where the phase has no such value:
before_demand: none of them (the period has not received or sold);on_receipt:receivedholds the units being received now;after_demandandcallback_adjustment: all four hold this period's values.
period and date identify the demand period being processed
(the event-table coordinates); demand_period is its zero-based
index. The context is a copy: changing it never changes engine state.
StockChange
dataclass
¶
StockChange(
unique_id: object,
quantity: float,
received_date: Timestamp,
origin: str,
source: str,
)
One on-hand change made by something other than the notified process.
quantity is signed: positive units were added, negative removed.
origin is "process" (another process's flow; source is
"<process>.<flow>") or "callback" (an accepted
InventoryAdjustmentResult row; source is the callback class).
received_date is the lot date given for an addition, else NaT.
FIFOLotLedger
¶
Dated-lot ledger for multi-SKU on-hand stock with FIFO consumption.
The ledger mirrors on_hand: every unit on hand belongs to exactly one lot
with a receipt date. Consumption removes from the oldest lot first;
expiry removes lots whose age reached shelf_life_days.
seed_from_lots
¶
seed_from_lots(
inventory: InventoryStateDataFrame,
opening_lots: DataFrame,
) -> None
Seed explicit opening lots and assert they equal opening on-hand.
receive
¶
Add a lot of qty units received on date (zero is skipped with a warning).
expire
¶
Remove lots aged >= shelf_life_days; return expired qty per SKU.