///|
/// Discrete-time replenishment policy.
///
/// - `LotForLot` orders exactly the net requirement.
/// - `FixedQuantity(size)` orders whole batches of `size`.
/// - `Multiple(m)` rounds the order up to a multiple of `m`.
/// - `MinimumMultiple(minimum, m)` satisfies the minimum, then rounds to `m`.
/// - `PeriodOrder(n)` covers the net requirements of the next `n` buckets.
/// - `MinMax(minimum, maximum)` replenishes up to `maximum` whenever the
/// projected balance would fall below `minimum`.
/// - `ReorderPoint(level, quantity)` uses the ERP "reorder level + reorder
/// quantity" rule: when the balance after demand falls below `level`, order
/// `quantity` (or the net requirement when that is larger).
pub(all) enum LotRule {
LotForLot
FixedQuantity(Qty)
Multiple(Qty)
MinimumMultiple(Qty, Qty)
PeriodOrder(Int)
MinMax(Qty, Qty)
ReorderPoint(Qty, Qty)
} derive(Eq, @debug.Debug)
///|
/// How the per-bucket safety floor is derived.
///
/// - `Fixed(q)` is a constant floor.
/// - `DaysOfDemand(n)` is `n` buckets of average gross demand.
/// - `PercentOfDemand(p)` is `p` percent of the bucket's gross demand.
pub(all) enum SafetyStock {
Fixed(Qty)
DaysOfDemand(Int)
PercentOfDemand(Int)
} derive(Eq, @debug.Debug)
///|
/// One planning item. `unit` is a label only; BOM rows that use a different
/// label must be converted through `Request.conversions`.
///
/// A `phantom` item is a pass-through: it never carries stock or orders, its
/// gross demand is exploded into its components in the same bucket.
pub(all) struct Item {
sku : String
unit : String
on_hand : Qty
safety : SafetyStock
/// Upper inventory target; `Qty::zero()` disables excess reporting.
max_stock : Qty
lead_time : Int
/// Usable fraction of a planned order, in per-mille: `900` means the order
/// yields 90% good units and must be inflated accordingly.
yield_per_mille : Int
lot : LotRule
phantom : Bool
} derive(Eq, @debug.Debug)
///|
/// A parent unit consumes `units_per` child units at its order release, plus
/// `scrap_per_mille` of extra consumption (`50` means 5% scrap).
pub(all) struct Component {
parent : String
child : String
units_per : Qty
/// Empty means "the child's base unit"; otherwise a conversion from this
/// label to the child's `unit` must exist in `Request.conversions`.
usage_unit : String
scrap_per_mille : Int
} derive(Eq, @debug.Debug)
///|
/// `factor` is how many child base units one usage unit contains, for example
/// grams to kilograms is `Qty::milli(1L)`.
pub(all) struct UnitConversion {
from : String
to : String
factor : Qty
} derive(Eq, @debug.Debug)
///|
/// IDs must be unique within demands, and separately within receipts.
pub(all) struct Movement {
id : String
item : String
bucket : Int
quantity : Qty
} derive(Eq, @debug.Debug)
///|
/// What to do when a planned release would start before bucket zero.
pub(all) enum ReleasePolicy {
/// Reject the entire calculation (default, safest).
Strict
/// Clamp the release to bucket zero, keep the plan and record an incident.
AllowLate
} derive(Eq, @debug.Debug)
///|
pub(all) struct Request {
horizon : Int
items : Array[Item]
bom : Array[Component]
demands : Array[Movement]
receipts : Array[Movement]
conversions : Array[UnitConversion]
policy : ReleasePolicy
} derive(@debug.Debug)
///|
pub(all) enum Source {
Demand(String)
ParentOrder(String, Int)
} derive(Eq, @debug.Debug)
///|
/// Gross demand contribution, before netting with stock and receipts.
pub(all) struct Peg {
item : String
bucket : Int
quantity : Qty
source : Source
} derive(Eq, @debug.Debug)
///|
/// One planning bucket. `net` is the good-unit requirement, `planned_receipt`
/// is the order quantity (start quantity), `received` is the usable part of
/// that order that reaches stock, and `planned_release` is the order quantity
/// released `lead_time` buckets earlier.
pub(all) struct Bucket {
index : Int
gross : Qty
scheduled : Qty
net : Qty
planned_receipt : Qty
received : Qty
planned_release : Qty
projected : Qty
} derive(Eq, @debug.Debug)
///|
pub(all) struct ItemPlan {
item : String
buckets : Array[Bucket]
} derive(Eq, @debug.Debug)
///|
pub(all) struct Order {
item : String
receipt_bucket : Int
release_bucket : Int
quantity : Qty
/// True when the required release fell before bucket zero and the policy
/// `AllowLate` clamped it to bucket zero.
late : Bool
} derive(Eq, @debug.Debug)
///|
/// A non-fatal planning incident, for example a release clamped to bucket zero.
pub(all) struct Incident {
item : String
receipt_bucket : Int
required_release : Int
actual_release : Int
} derive(Eq, @debug.Debug)
///|
/// Excess-inventory exception: the projected balance exceeds `Item.max_stock`
/// so a planner can review overstock (the frePPLe "excess inventory" workflow).
pub(all) struct StockException {
item : String
bucket : Int
projected : Qty
maximum : Qty
} derive(Eq, @debug.Debug)
///|
/// Plans contain no promises about finite machine or supplier capacity.
pub(all) struct Plan {
horizon : Int
items : Array[ItemPlan]
orders : Array[Order]
pegs : Array[Peg]
incidents : Array[Incident]
exceptions : Array[StockException]
} derive(Eq, @debug.Debug)
///|
pub(all) enum PlanningError {
InvalidHorizon
SizeLimit
InvalidItem(String)
DuplicateItem(String)
UnknownItem(String)
InvalidComponent(String, String)
DuplicateComponent(String, String)
InvalidMovement(String)
DuplicateMovement(String)
Cycle(Array[String])
QuantityLimit(String)
BeforeHorizon(String, Int, Int)
InvalidConversion(String, String)
} derive(Eq, @debug.Debug)
///|
pub extend LotRule with Eq::{equal, not_equal}
///|
pub extend LotRule with @debug.Debug::{to_repr}
///|
pub extend SafetyStock with Eq::{equal, not_equal}
///|
pub extend SafetyStock with @debug.Debug::{to_repr}
///|
pub extend StockException with Eq::{equal, not_equal}
///|
pub extend StockException with @debug.Debug::{to_repr}
///|
pub extend Item with Eq::{equal, not_equal}
///|
pub extend Item with @debug.Debug::{to_repr}
///|
pub extend Component with Eq::{equal, not_equal}
///|
pub extend Component with @debug.Debug::{to_repr}
///|
pub extend UnitConversion with Eq::{equal, not_equal}
///|
pub extend UnitConversion with @debug.Debug::{to_repr}
///|
pub extend Movement with Eq::{equal, not_equal}
///|
pub extend Movement with @debug.Debug::{to_repr}
///|
pub extend ReleasePolicy with Eq::{equal, not_equal}
///|
pub extend ReleasePolicy with @debug.Debug::{to_repr}
///|
pub extend Source with Eq::{equal, not_equal}
///|
pub extend Source with @debug.Debug::{to_repr}
///|
pub extend Peg with Eq::{equal, not_equal}
///|
pub extend Peg with @debug.Debug::{to_repr}
///|
pub extend Bucket with Eq::{equal, not_equal}
///|
pub extend Bucket with @debug.Debug::{to_repr}
///|
pub extend ItemPlan with Eq::{equal, not_equal}
///|
pub extend ItemPlan with @debug.Debug::{to_repr}
///|
pub extend Order with Eq::{equal, not_equal}
///|
pub extend Order with @debug.Debug::{to_repr}
///|
pub extend Incident with Eq::{equal, not_equal}
///|
pub extend Incident with @debug.Debug::{to_repr}
///|
pub extend Plan with Eq::{equal, not_equal}
///|
pub extend Plan with @debug.Debug::{to_repr}
///|
pub extend PlanningError with Eq::{equal, not_equal}
///|
pub extend PlanningError with @debug.Debug::{to_repr}
///|
pub extend Request with @debug.Debug::{to_repr}