Supply — inventory policy (EOQ, safety stock, service level)
An order policy stated as the four numbers that actually run it: how much to order, when to order it, how much to hold against variability, and what that costs per year. Typed FlowScript for supply chains. Keywords: supply chain, logistics, network, supplier.
Make it your own.
// An order policy stated as the four numbers that actually run it: how
// much to order, when to order it, how much to hold against variability,
// and what that costs per year.
//
// Bulk base malt for a 40 hL brewhouse. The economic order quantity is
// about two tanker loads; the reorder point is three weeks of demand plus
// four and a half days of safety stock. Every one of those follows from
// demand, cost and lead time, so raising the holding rate or shortening
// the lead time moves the whole policy rather than one cell of it.
//
// Money carries no unit here — the unit table has no currencies — so the
// currency is declared once as text and the cost arithmetic stays on plain
// numbers. Physical quantities keep their units: lead time in days, stock
// in kilogrammes.
inventory_policy pale_malt {
title: "Base malt order policy — Maris Otter, bulk"
item: "Maris Otter pale ale malt, delivered in 15 t bulk tankers"
currency: "GBP"
stock_unit: "kg"
policy: "Continuous review (s, Q) — order a fixed quantity when stock on hand plus on order falls to the reorder point"
annual_demand: 480000
weeks_operating: 48
weekly_demand: = annual_demand / weeks_operating
demand_sd_weekly: 1800 source "Standard deviation of weekly issues, 2 brewing years"
order_cost: 120 source "Goods-in labour, tanker slot and QA sampling per delivery"
unit_cost: 0.58
holding_rate: 22%
holding_cost: = unit_cost * holding_rate / 100[%]
economic_order_quantity: = sqrt(2 * annual_demand * order_cost / holding_cost)
orders_per_year: = annual_demand / economic_order_quantity
cycle_stock: = economic_order_quantity / 2
tanker_load: 15000
tankers_per_order: = economic_order_quantity / tanker_load
lead_time: 21 d
lead_time_weeks: = lead_time / 7[d]
demand_over_lead_time: = weekly_demand * lead_time_weeks
// Demand variability accumulates as the square root of the lead time,
// which is why halving the lead time does not halve the safety stock.
sigma_over_lead_time: = demand_sd_weekly * sqrt(lead_time_weeks)
service_level: 98%
z: 2.054 source "Standard normal one-sided 98%"
safety_stock: = z * sigma_over_lead_time
reorder_point: = demand_over_lead_time + safety_stock
average_stock: = cycle_stock + safety_stock
weeks_of_cover: = average_stock / weekly_demand
annual_holding_cost: = average_stock * holding_cost
annual_ordering_cost: = orders_per_year * order_cost
total_policy_cost: = annual_holding_cost + annual_ordering_cost
silo_capacity: 45000
headroom: = silo_capacity - (reorder_point + economic_order_quantity - demand_over_lead_time)
}
// What a shorter lead time would be worth, since the supplier has offered
// a fortnightly slot at a price.
policy_option shorter_lead_time {
of: pale_malt
label: "Fortnightly contracted slot — lead time 14 days"
lead_time: 14 d
lead_time_weeks: = lead_time / 7[d]
demand_over_lead_time: = pale_malt.weekly_demand * lead_time_weeks
sigma_over_lead_time: = pale_malt.demand_sd_weekly * sqrt(lead_time_weeks)
safety_stock: = pale_malt.z * sigma_over_lead_time
reorder_point: = demand_over_lead_time + safety_stock
safety_stock_released: = pale_malt.safety_stock - safety_stock
annual_saving: = safety_stock_released * pale_malt.holding_cost
supplier_premium: 900
net_benefit: = annual_saving - supplier_premium
verdict: "Refuse it. A shorter lead time releases about 1.2 t of safety stock, and malt is cheap enough to hold that the released stock is worth GBP 150 a year against a GBP 900 premium. The same fourteen days on a hop contract, where the unit cost is forty times higher, would be worth signing."
}
policy_option higher_service {
of: pale_malt
label: "99.5% cycle service level — no stockout in a brewing year"
z: 2.576 source "Standard normal one-sided 99.5%"
safety_stock: = z * pale_malt.sigma_over_lead_time
additional_stock: = safety_stock - pale_malt.safety_stock
additional_cost: = additional_stock * pale_malt.holding_cost
verdict: "About 8 days of extra cover for roughly the cost of one lost brew. Worth it only if a stockout genuinely stops the brewhouse, which it does: there is no second malt."
}
supply_chain malt_supply {
product: "Maris Otter pale ale malt, bulk"
currency: "GBP"
title: "Malt supply into the brewhouse"
}
sc_node maltings {
name: "Warminster Maltings"
role: "supplier"
location: "Wiltshire"
capacity: 4000000
}
sc_node haulier {
name: "Bulk tanker haulage"
role: "supplier"
location: "Contracted, 2 slots per week"
capacity: 120000
}
sc_node silo {
name: "Brewery malt silo, 45 t"
role: "warehouse"
location: "Bermondsey"
capacity: 45000
}
sc_node brewhouse {
name: "40 hL brewhouse"
role: "factory"
location: "Bermondsey"
capacity: 10000
}
sc_link malt_to_haulier {
from: maltings
to: haulier
lead_time_days: 14
cost_per_unit: 0.02
mode: "truck"
}
sc_link haulier_to_silo {
from: haulier
to: silo
lead_time_days: 7
cost_per_unit: 0.04
mode: "truck"
}
sc_link silo_to_brewhouse {
from: silo
to: brewhouse
lead_time_days: 1
cost_per_unit: 0.01
mode: "auger"
}
note eoq_assumption {
text: "EOQ assumes demand is steady and the whole order lands at once, which is true here and often is not. It is also famously flat near the optimum: ordering 25000 or 36000 kg instead of the computed 30000 costs under 2% of the policy cost, so round to whole tanker loads without guilt."
anchor: pale_malt
}
view chain: supply_map