02 — Planning Layer
The brain. Translates expected/actual demand into a concrete, feasible production plan. Inherits all rules from
00. Three sequential components: MPS → MRP → CRP, with a closed feedback loop.
2.1 The Three Outputs of Planning
Planning, as a whole, produces exactly three deliverables. Keep these as the mental model:
Output 1: Can we accept the order? By when can we deliver? (MPS + CTP)
Output 2: Material schedule — what to buy/make, how much, when (MRP)
Output 3: Detailed production plan — when each stage starts (MRP draft + CRP confirm)
Inputs: customer order, master data (BOM/Routing/WC), current inventory, item settings, material ownership.
2.2 Component: MPS (Master Production Schedule)
Purpose: Decides what to produce, how much, when. Under make-to-order it shifts from "forecaster" to "order translator + acceptance/capacity checker."
Behaviour branches by production_type
if MakeToStock:
demand = apply_forecast_consumption(forecast, actual_orders)
PAB[t] = PAB[t-1] + planned_production[t] - demand
if PAB[t] < safety_stock: suggest production
if MakeToOrder:
ignore forecast; planned_production = confirmed_orders; safety_stock = 0
run CTP check (capacity-based promise)
if AssembleToOrder:
components → MakeToStock; finished assembly → MakeToOrder
if TollManufacturing:
plan capacity only (material is customer's, see 2.3)
Core formula — PAB (Projected Available Balance)
PAB[t] = PAB[t-1] + scheduled_production[t] - demand[t]
demand[t] = (see forecast_consumption + time_fence rules below)
Forecast Consumption (avoid double-counting)
Actual orders consume the forecast, they don't add to it:
consumed = MIN(actual_orders[t], forecast[t])
remaining_forecast = forecast[t] - consumed
total_demand[t] = actual_orders[t] + remaining_forecast
if actual_orders[t] > forecast[t]: total_demand[t] = actual_orders[t]
Config: forecast_consumption_method ∈ {None, Backward, Forward, Both}, consumption_window.
Time Fences (state by distance from today)
distance = t - today
if distance <= DTF (Demand Time Fence): zone=FROZEN → demand = actual_orders only (ignore forecast)
elif distance <= PTF (Planning Time Fence): zone=SLUSHY → MAX/consumption blend, changes need approval
else: zone=LIQUID → demand = forecast (no orders yet)
Prevents "system nervousness": near-term plan is stable, far-term is flexible.
CTP (Capable to Promise) — core of make-to-order
CTP_Check(product P, qty Q, due D):
for op in routing(P):
if op.wc is PRESS-like:
cycles = Q / op.cavity_count
load[wc] += cycles × op.cycle_time + op.setup_time
else:
load[wc] += Q × op.run_time + op.setup_time
for wc in required: earliest[wc] = find_earliest_slot(wc, load[wc])
promised = MAX(earliest[wc]) # bottleneck决定
return promised <= D ? "on time" : ("earliest: " + promised)
Order splitting & batching (Many-to-Many)
- Large order → multiple delivery schedules (e.g. monthly).
- Multiple similar orders → ONE production order (reduce setup/mold changes).
- Links recorded in Pegging table (see 06).
Pegging: { production_order_id, sales_order_id, allocated_quantity }
MPS data model
MPS_Header: { id, plan_name, plant_id, time_bucket∈{Day,Week,Month},
start_date, end_date, frozen_fence, slushy_fence, status }
MPS_Line: { product_id, period_date, forecast_qty, confirmed_orders_qty,
planned_production, projected_balance, safety_stock_target,
available_to_promise }
ATP calculation (3 modes — config)
Discrete: ATP[t] = production[t] - orders until next production
Cumulative: ATP[t] = Σproduction≤t - Σorders≤t
Look-ahead: ATP[t] = production[t] - orders(t .. next production) ← safest
2.3 Component: MRP (Material Requirements Planning)
Purpose: Turn production orders into material needs: what to buy/make, how much, when. It is a process, not a table — produces transient Planned Orders.
Item MRP settings
Item_MRP_Settings: {
item_id, mrp_type∈{Planned,ReorderPoint,NoPlanning},
procurement_type∈{Buy,Make}, material_ownership∈{Own,Customer},
lot_sizing_rule∈{Exact,Fixed,MinMax,EOQ,PeriodOrder}, lot_size, min_lot, max_lot,
safety_stock, lead_time_days, reorder_point
}
Algorithm (level-by-level, top to deepest)
MRP_Run():
sort items by BOM level (finished=0, raw=deepest)
for each item, for each period t:
Gross[t] = MPS_demand (finished) + dependent_demand_from_parents (component)
Available[t] = on_hand + open_POs[t] + open_WOs[t] - reserved[t]
Net[t] = MAX(0, Gross[t] - Available[t] + safety_stock)
Order_Qty = apply_lot_sizing(Net[t], lot_rule)
Release_Date = Required_Date - lead_time
if procurement_type == Make:
explode_BOM → dependent demand for components → recurse deeper
BOM Explosion (recursive, multi-level)
explode_BOM(product P, qty Q):
for line in BOM(P):
required = Q × line.quantity × (1 + line.scrap_pct)
dependent_demand[line.component] += required
if line.component.procurement_type == Make:
explode_BOM(line.component, required)
Lot Sizing (key branch for tooling factories)
apply_lot_sizing(net, rule):
Exact: net
Fixed: round_up(net, lot_size)
MinMax: clamp(net, min, max)
EOQ: economic_order_qty()
PeriodOrder: Σ net over several periods ← consolidate to cut setup/mold changes
Tooling-heavy items use PeriodOrder/Fixed to batch demand and minimize mold mounts.
Toll manufacturing branch
when generating purchase for a component:
if component.material_ownership == Customer:
DO NOT raise purchase order (customer supplies it)
instead: verify customer-supplied quantity is sufficient; if short → alert
else:
raise normal purchase order
Outputs
- Planned Purchase Orders → Procurement module
- Planned Production Orders → Execution layer
- Exception Messages → "will be late", "reschedule in/out", "cancel"
2.4 Component: CRP (Capacity Requirements Planning)
Purpose: MRP plans assuming infinite capacity. CRP injects reality — verifies work centers (and tools, and labor) have enough capacity at the required time. In tooling factories, CRP matters more than MRP (materials are easy; scheduling N tools on M machines is the real problem).
Algorithm
CRP_Run():
# 1. Load per planned order
for order, op:
if op.wc is PRESS-like:
cycles = order.qty / op.cavity_count
load = cycles × op.cycle_time + op.setup_time
else:
load = order.qty × op.run_time + op.setup_time
# 2. Distribute over periods → 3. Aggregate per resource
Total_Load[resource, period] = Σ loads
# 4. Compare
Util = Total_Load / effective_capacity × 100
Util > 100 → OVERLOAD ; Util < 70 → UNDERLOAD ; else OK
# 5. Resolve overload
→ reschedule / overtime / extra shift / defer order / use multi-cavity tool
Resource intersection (Machine + Tool + Labor)
earliest_start = MAX(machine_free_time, tool_free_time, labor_free_time)
The binding constraint is the minimum-availability resource. A tool is a movable resource that must be tracked across machines.
RCCP vs CRP (both supported)
RCCP (Rough Cut): on MPS, critical resources only → used by CTP for order acceptance
CRP (Detailed): on MRP, all resources → used for detailed scheduling
Melamine test case
WC-PRESS week3 capacity 90h. Order 6000 dishes, mold cavity=1, cycle 80s → 133h.
Util=148% → OVERLOAD. Resolution options surfaced, including:
use 4-cavity mold variant → 6000/4 ×80s = 33h ✓
This links a scheduling decision (CRP) to a master-data choice (which mold).
2.5 Known Gap — Detailed Scheduling (APS)
CRP detects overload but does NOT sequence operations hour-by-hour on specific machines. A future Advanced Planning & Scheduling (APS) component should: - Sequence operations on each machine/tool on a timeline (Gantt). - Optimize mold-mount order to minimize changeovers (the hardest problem: ~50 molds on ~4 presses). - Respect machine warm-up, shift patterns, tool availability.
Interface: consumes Planned Orders + Routing + Resource calendars; emits a time-phased schedule that SFC (file 05) executes.
2.6 Closed Loop
CRP finds conflict → feeds back to MPS → adjusts promised_date → re-runs
Costing variances (file 04) → inform standards & future planning