Follow demand through MRP, production orders, confirmations and settlement in SAP, read the exception messages planners live with, and decide where AI helps.
Plan to produce is everything between "we expect to sell this" and "the finished product is on the shelf, and we know what it really cost". In SAP it runs as a chain: demand, a planning run, planned orders, production orders, components issued, work confirmed, finished goods received, and the order's costs settled.
The heart of it is material requirements planning (MRP). MRP compares what is needed with what is in stock or on the way. Where something is short, it proposes a planned order to make it or a purchase requisition to buy it, dated backward from the need.
Where MRP can't solve a problem, it raises exception messages: this order should come earlier, that one isn't needed, this stock is below the safety level. Reading that list every morning is the planner's daily work.
Three ideas carry this topic:
MRP plans with unlimited capacity. It assumes every machine and person is free. Whether the plant can actually build it is a separate check.
Exceptions are the interface. A planner doesn't read the whole plan. They read what MRP flagged, so the order and quality of that list decide how good planning is.
Production closes in finance. Every order collects its real costs and is credited at the standard cost. The difference is a production variance, and someone has to explain it.
Plan to produce decides whether customers get products on time, how much inventory the company carries, and whether product costs are what finance thinks. Operations watches on-time order completion; finance watches inventory value and production variances.
Here is Monday morning at Calder Pumpworks, the fictional pump maker this unit follows. On 26 October 2026, the MRP results for the Rockford plant show these problems near the top of the list:
What MRP or the capacity check shows
What it means
Who acts
A production order for four ES-300 pumps finishes 6 November, but they are needed 27 October
A customer order will ship late
Production planner, with sales
Six more ES-300 pumps should have started last week
MRP had to schedule them forward; they arrive two days late
Production planner
Two motor orders arrive after the motors are needed, and one isn't even confirmed
Assembly will stop for lack of motors
Buyer
Motor stock is below its safety level
No buffer left for a bad week
Buyer and planner
The test bench is loaded at 125% in the week it goes down for maintenance
MRP didn't know the bench would be out
Production supervisor, with maintenance
A seal kit order for 100 kits isn't needed at all
Cash tied up in stock nobody will use soon
Buyer
The business decision is not "run MRP more often". It is who reads which messages, in what order, and how fast each reaches the person who can fix it.
As of October 2026, SAP S/4HANA covers the whole chain as standard. Each step is documented in SAP's training:
Demand. Forecasts or manual entries become planned independent requirements (PIRs) for a material in a plant. Forecasts can come from SAP Integrated Business Planning (IBP) or from planning inside S/4HANA (SAP Learning, Public Edition). A planning strategy, such as make-to-stock or make-to-order, says how PIRs and sales orders interact (SAP Learning).
MRP.MRP Live runs the planning inside the SAP HANA database. It is started in the Schedule MRP Runs app or with transaction MD01N, and planners review results in Monitor Stock/Requirements List (MD04) (SAP Learning).
Production orders. Planned orders are converted into production orders close to production, one by one or in bulk (SAP Learning). Release, after availability and capacity checks, allows goods issues and confirmations (SAP Learning).
Shop floor. Operators confirm yield, scrap and time, which charges the order with actual costs (SAP Learning, Public Edition). A manufacturing execution system such as SAP Digital Manufacturing can take this over and report back (SAP Learning).
Costing. At period end, controlling calculates work in process for open orders and variances for finished ones, then settles them (SAP Learning).
For planning beyond what MRP does, SAP offers production planning and detailed scheduling (PP/DS), embedded in S/4HANA, for finite scheduling of bottleneck resources (SAP Learning), and SAP IBP for network-wide supply planning (SAP Learning).
For AI, SAP's Sapphire 2026 innovation guide lists a Planning Assistant in Joule, with agents for exception management, demand fulfillment and lead-time analysis, as planned for general availability in Q4 2026, and a Manufacturing Assistant as planned for Q2 2026 (SAP). Both dates are SAP's plans as published for Sapphire in 2026, and the Q2 date has passed since. Check their current status and licensing before you plan on them. Where AI creates value in S/4HANA places them in the wider picture.
"MRP knows what the plant can build." It doesn't. MRP and lead time scheduling assume infinite capacity. Overloads show up only in a capacity check or an advanced planning tool.
"An exception message is an error." It is a suggestion. "Reschedule in" means a receipt is too late; a person decides whether to move it.
"The standard cost is the real cost." It is the planned cost. The variance on each order is the difference, and it is where scrap, rework and slow lines become visible in money.
"An AI agent should reschedule orders." It can rank messages, explain them and propose a fix. Moving a production order or cancelling a supplier's order is a person's decision.
Pick one answer for each question. The explanation appears after you choose.
1Calder's MRP proposes six ES-300 pumps for a week when the assembly line is already full. Why didn't MRP see that?
Answer: B. MRP and lead time scheduling assume infinite capacity. Overloads show up when a planner or supervisor runs a capacity check or uses an advanced planning tool.
2A planner sees "reschedule in" on a production order. What is MRP telling them?
Answer: C. Reschedule in means a receipt is later than its requirement. MRP flags it and leaves the decision to a person, because moving an order affects capacity and components.
3The planners at a plant get 900 exception messages each morning and fix about a hundred. What should the plant improve first?
Answer: A. Planners can't read everything, so the order of the list decides what gets fixed. Ranking by customer impact and cost puts the messages that matter in front of the person who can act.
4What does a production variance on a finished order tell finance?
Answer: A. The order collects actual costs and is credited at standard for what it delivered. The difference is the variance, which shows scrap, extra hours or price changes in money.
5Calder's test bench goes down for maintenance in a week full of test work. What link would have caught it earlier?
Answer: A. MRP doesn't know about the outage. When maintenance plans reduce the work center's available capacity, the capacity check shows the overload before the week starts.
6A vendor pitches an agent that moves production orders and cancels supplier orders on its own. What should you ask for?
Answer: B. Rescheduling an order or cancelling a supplier's order affects customers, suppliers and capacity. An agent can rank and explain; a planner or buyer decides and the decision is logged.
7Calder wants to schedule its bottleneck test bench to the minute, with finite capacity. Which SAP option fits?
Answer: D. PP/DS plans with exact times and can treat resources as finite. MRP Live assumes infinite capacity, and SAP IBP's supply planning works at the network level, with detailed scheduling done in S/4HANA.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Think of plan to produce as a promise that is checked twice: once for material, once for money.
Material. MRP turns demand into a time-phased list of receipts: make this many by this date, buy that many by that date. Each receipt is checked against the date it is needed. Where it is too late, too early or not needed at all, MRP raises an exception message. MRP checks material only. Capacity is a second check, and the shop floor's confirmations are the third.
Money. Each production order is a small cost account. Goods issues and confirmations debit it with actual costs. The goods receipt credits it at the standard price. Whatever is left at period end is either work in process (the order isn't finished) or a variance (it is), and settlement moves it to finance.
Everything in this topic, every worklist and every AI idea, reads one of those two checks. An exception message says the material promise is at risk. A variance says the money promise was broken, and by how much.
#Demand: planned independent requirements and planning strategies
A planned independent requirement (PIR) is expected demand for a material in a plant, from a forecast or entered by hand. It can trigger production before any sales order exists. Forecasts can be released from SAP IBP or from planning in S/4HANA and transferred to MRP as PIRs. Sales orders and stock transfer requirements from other sites are the other independent requirements (SAP Learning, Public Edition).
The planning strategy decides how PIRs and sales orders interact. SAP's training lists, among others: 10 make-to-stock with net requirements planning, 11 gross requirements planning, 20 make-to-order, 40 planning with final assembly, 50 planning without final assembly and 70 planning with assemblies. It also says what reduces the PIRs for each: for 10 and 40, goods issues; for 11, goods receipts (SAP Learning). Strategy 40 uses consumption: incoming sales orders consume the PIRs, so the forecast and the orders aren't counted twice. In make-to-order strategies, production starts only when the sales order arrives (SAP Learning).
At Calder (fictional), standard pumps are planned with final assembly: monthly PIRs for ES-200 and ES-300 at plant CP11, consumed by the month's sales orders. Engineered pumps are made to order. Consumption periods and requirement types are configuration; the script in this topic uses a simplified "same month" rule.
MRP does three things for each material: it explodes the BOM and schedules backward from the requirement date, it calculates net requirements (requirements against stock and receipts), and it creates procurement proposals for what is short (SAP Learning). For in-house production it creates planned orders, which are later converted to production orders. For bought materials, MRP Live creates schedule lines when a valid scheduling agreement exists and purchase requisitions otherwise (SAP Learning).
Scheduling works backward. From the requirement date, MRP subtracts the goods receipt processing time to get the order's finish date, then the in-house production time to get its start date. If the start date falls in the past, the system switches to forward scheduling (SAP Help, R/3). That is why a late proposal arrives late, and why it comes with a message.
MRP Live runs the calculation for receipts, requirements and proposals in one database procedure. It reads the planning file, which lists materials with changes since the last run, and plans by low-level code, finished products first. Materials it can't handle go to classic MRP automatically. MRP Live creates no MRP lists; planners read Monitor Stock/Requirements List (MD04) instead. Planning Mode controls what happens to non-fixed proposals from the last run; fixed ones stay as they are (SAP Learning).
When MRP finds a situation it can't solve, it writes an exception message on the element. SAP's knowledge base names the rescheduling messages: 10 reschedule in, 15 reschedule out, 20 cancel process, 25 excess stock, 26 excess in an individual segment and 30 plan process according to reschedule (SAP KBA 1698291). SAP's older help lists others with the same numbers, including 05 opening date in the past, 06 start date in the past, 07 finish date in the past, 50 no BOM exists and 96 stock below safety stock (SAP Help).
Planners work the messages in Manage Material Coverage and Monitor Material Coverage - Net and Individual Segments, or in MD04. Situation handling can push some of them to a team: the template SAP_PP_MRP_MATERIAL_EXCEPTION covers cancel-process and excess-stock situations and opens Manage Material Coverage (SAP Community, SAP S/4HANA RIG team).
#From planned order to production or process order
A planned order is internal: the planner can change, reschedule or delete it. Conversion creates the production order. It can be done one at a time, collectively by plant, MRP controller or opening date, or partially, when one planned order becomes several production orders. On conversion, the planned order is deleted and replaced, and the components' dependent requirements become reservations. The planned order's opening date says when to convert (SAP Learning).
Level
Holds
Calder example (fictional)
Header
Material, plant, quantity, order type, basic dates, production version, status, MRP controller, production supervisor
Order 1100021: 4 × CP-ES300 at CP11, 28 October to 5 November, released
Item
What the order delivers and to which storage location
4 pumps to finished-goods stock
Operations
Routing steps at work centers, with standard times
0010 assemble at ASSY1 (2.0 h each), 0020 test at TEST1 (0.75 h each)
Components
Reserved materials, quantities, requirement dates, backflush flag
4 × CP-MTR15
Costs
Planned costs from the cost estimate; actual costs as they post
Planned 16,400.00 USD for 4 pumps
In SAP's model of the Production Order API, the header carries status flags such as OrderIsReleased, OrderIsPartiallyConfirmed, OrderIsDelivered, OrderIsTechnicallyCompleted and SettlementRuleIsCreated, plus MfgOrderPlannedStartDate, MfgOrderPlannedEndDate and MfgOrderConfirmedYieldQty. Components carry RequiredQuantity, WithdrawnQuantity, MatlCompIsMarkedForBackflush, ReservationIsFinallyIssued and GoodsMovementType (SAP Cloud SDK model, 2022).
Process industries use process orders with master recipes instead of routings; SAP's manufacturing execution and warehouse integrations support both order types (SAP Learning). Calder makes discrete products, so this topic uses production orders.
Release is a status change from created to released. Printing shop papers, goods issues and confirmations all need it. Release usually follows a component availability check and a capacity evaluation; if components are short, the order can't run until more are procured or taken from a less important order. Orders can be released one at a time, operation by operation, or collectively in Manage Production Orders (F2336), and released orders can be downloaded to a manufacturing execution system (SAP Learning).
MRP and lead time scheduling of planned and production orders are infinite: they respect non-working time but not other orders' load, which can cause overloads. Capacity evaluation compares a work center's available capacity with the operations' requirements; capacity leveling schedules finitely into free periods. SAP's tools include Manage Work Center Capacity, which replaces the classic CM* transactions, the Capacity Scheduling Table and the Capacity Scheduling Board. Dispatching an operation firms the planned order or gives the production order operation the status DSPT, so lead time scheduling no longer moves it (SAP Learning).
At order creation, the system reserves each component. Components are issued manually, for example in Post Goods Movement (MIGO) or Pick Components for Production Orders (CO27), or by backflush when the operation is confirmed, for low-value parts and flow lines. Each issue reduces the reservation, charges the component's cost to the order and creates a material document (SAP Learning).
With extended warehouse management (EWM), stock moves from the warehouse to a production supply area first. SAP describes staging methods for pick parts, release order parts and crate parts, and two integration models: delivery-based, or the advanced model with a production material request that covers staging and consumption in one document (SAP Learning). Calder's Rockford distribution center runs EWM; Warehouse and transportation execution covers it.
Operators confirm yield, scrap and rework per operation, with times for setup, machine and labor. A partial confirmation reports progress; a final one closes the operation. The control key of each operation says whether it must, can or can't be confirmed, and whether it is a milestone. A confirmation updates the status to partially confirmed (PCNF) or confirmed (CNF), records actual dates, charges activity costs to the order from the work center's data, backflushes components and, if configured, posts the goods receipt. Apps include Confirm Production Order and Confirm Production Order Operation (SAP Learning, Public Edition).
The goods receipt adds the finished quantity to stock, sets the order to partially delivered or delivered and credits the order. It is posted manually in Post Goods Movement (MIGO) or automatically with a confirmation (SAP Learning).
If the material has an inspection type for lot origin 04, the goods receipt creates an inspection lot; an early variant creates it at release, so inspection can run alongside production. With "post to inspection stock", the goods wait in quality inspection until the usage decision moves them to unrestricted or blocked stock (SAP Learning). At Calder, ES-300 pumps get a final inspection; ES-200 pumps don't.
#Period end: work in process, variances, settlement
For an order without the delivered or technically completed status, the period-end run calculates work in process (WIP); with no goods receipt yet, WIP equals the actual costs to date. For a delivered or technically completed order, it calculates variances instead. SAP's training groups them on the input side as input price, input quantity, resource usage and remaining variance, and on the output side as lot size, mixed price, output price and remaining variance (SAP Learning). SAP's training also refers to scrap variances, which depend on a scrap indicator on the variance key (SAP Learning).
Settlement clears the order. For a material with standard price control, it debits price differences and credits the inventory change account, cancels WIP from earlier periods, updates the material ledger and can pass variances by category to margin analysis. A price difference splitting profile maps variance categories to G/L accounts, so the income statement shows why production cost more or less (SAP Learning).
flowchart LR
PIR[PIR ES-200<br/>Nov 20] --> PLO[Planned order<br/>0000050011]
PLO --> PRO[Production order<br/>1100015]
PRO --> GI[Goods issue<br/>components]
GI --> J1[Journal<br/>inventory to order]
PRO --> CF[Confirmations<br/>ASSY1, TEST1]
CF --> J2[Activity costs<br/>to order]
PRO --> GR[Goods receipt<br/>10 pumps]
GR --> J3[Journal<br/>FG at standard,<br/>order credited]
J3 --> SET[Settlement<br/>variance]
Step
Posts to finance
Why
PIR, MRP run, planned order, conversion, release
Nothing
Plans and commitments
Goods issue of components
Component inventory leaves stock and is charged to the order
The material is used up
Confirmation
Activity cost (hours × rate) charged to the order from the work center's cost center
Labor and machine time were spent
Overhead (period end)
Overhead charged to the order
Indirect costs are allocated
Goods receipt
Finished goods at standard price; the order is credited
The product exists and has a planned value
Usage decision
A stock move (inspection to unrestricted or blocked)
Same value, different stock type
WIP calculation (open orders)
WIP to the balance sheet
Costs belong to unfinished goods
Settlement (finished orders)
Variance to price differences; WIP cancelled
The order's balance is cleared
At Calder (fictional), order 1100015 for 10 ES-200 pumps collected 28,888.00 USD of actual costs and was credited 28,500.00 USD at the 2,850.00 standard price. Settlement posts the 388.00 USD variance to price differences: 38.00 for one extra seal kit and 350.00 for 2.5 extra assembly hours. You compute this in Step 6.
As of 24 October 2026, these released interfaces carry each step. The CDS views come from SAP's Cloudification Repository (file read 24 October 2026), which also marks the classic tables AFKO, AFPO, AFVC, AFRU, RESB, MKAL, CRHD and MARC as not to be released and names a successor for each, for example I_ManufacturingOrder for AFKO and I_MfgOrderConfirmation for AFRU (Cloudification Repository). The OData API names and fields come from SAP's generated API models, published in 2022 (SAP Cloud SDK model, 2022).
Standard cost, actual, plan and target costs, event-based WIP and variances
The repository also lists released event types for production orders (created, changed, released) and for PIRs (created, changed). Check their exact names in the event catalog on the SAP Business Accelerator Hub, and see Event-driven agents with SAP events for how to react to them. The functions ConvertPlndOrder and ReleaseOrder change SAP data; use them only in a design where a person triggers the action.
You will write one script that plays Calder's MRP run on Monday 26 October 2026. It nets demand for two pumps and three components, creates planned orders, raises exception messages and ranks them into a planner's worklist. It checks the load on the assembly line and the test bench against their capacity. And it traces two finished production orders to settlement, explaining each variance. An optional step reads planned orders from SAP's sandbox.
flowchart LR
P[calder_pack/<br/>products, prices] --> S[plan_to_produce.py]
SC[Scenario in the script<br/>stock, PIRs, orders,<br/>capacity, costs] --> S
S --> N[Netting and<br/>exception messages]
S --> W[Ranked worklist]
S --> C[Capacity per week]
S --> V[Order trace and<br/>variance split]
S --> M[calder_mrp_worklist.md]
K[SAP sandbox<br/>optional] --> S
Rewrite the pack, so you start from the same data as this topic:
Windows (PowerShell):
python unit1b\make_calder_pack.py
macOS / Linux:
python unit1b/make_calder_pack.py
The last lines should read:
Calder sample pack 1.0: order 1000001, net 29,100.00 USD, cost 19,600.00 USD.
All data is fictional. Next: python check_unit1b.py
The script checks the pumps' standard prices (2,850.00 and 4,100.00 USD) in products.json before it runs, because the cost estimates must add up to them.
In VS Code, create a new file, paste the script below, and save it as plan_to_produce.py inside the unit1b folder.
Read the blocks MATERIALS, STOCK, PIRS, SALES_ORDERS, PRODUCTION_ORDERS, PURCHASE_ORDERS, CAPACITY_PER_DAY and COST_ESTIMATE first. They are Calder's scenario written as Python data. Every value is invented. The planning data, BOMs and routings come from the master data topic, the sales orders from the order-to-cash topic and purchase order 4500000002 from the procure-to-pay topic. Lot size labels such as FX10, activity rates and the maintenance order are the course's own, not SAP values.
"""Plan to produce at Calder: net demand, read the MRP exceptions, trace one order to settlement.
Calder Pumpworks is a fictional company. The pack (unit1b/calder_pack/) and the
scenario below (stock, demand, open orders, capacity, costs) are made up for the
course. Field names follow SAP's released APIs where the script mirrors one
(A_PlannedOrder, A_ProductionOrder_2, A_PlannedIndepRqmtItem), but the planning
logic is a teaching sketch of MRP, not SAP's algorithm. Exception numbers 06, 07,
10, 15, 20 and 96 are the ones SAP documents for those situations; which ones a
system shows, and how they are grouped, is configuration.
Run it from your course folder:
python unit1b/plan_to_produce.py run MRP and print the planner's worklist
python unit1b/plan_to_produce.py --material CP-MTR15 show one material's netting, line by line
python unit1b/plan_to_produce.py --capacity work center load per week
python unit1b/plan_to_produce.py --order 1100016 trace one production order to settlement
python unit1b/plan_to_produce.py --write write unit1b/calder_mrp_worklist.md
python unit1b/plan_to_produce.py --sandbox count planned orders in SAP's sandbox (needs SAP_API_KEY)
Steps without --sandbox use only Python's built-in modules and need no account and no network.
"""
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import date, timedelta
from pathlib import Path
HERE = Path(__file__).resolve().parent
PACK = HERE / "calder_pack"
AS_OF = date(2026, 10, 26) # Monday morning: the planner reads the MRP results
HORIZON_END = date(2026, 12, 31) # plan this far ahead
RESCHEDULE_OUT_DAYS = 10 # a receipt this many working days too early gets message 15
PLANT = "CP11"
# ------------------------------------------------- plant planning data (fictional)
# From the master data topic (A_ProductSupplyPlanning shape). Times are working days.
# LotSize is a course label: "EX" lot-for-lot, "FX10" fixed lot of 10.
MATERIALS = {
"CP-ES200": {"Make": True, "InHouseDays": 5, "GRDays": 1, "Safety": 4, "LotSize": "FX10"},
"CP-ES300": {"Make": True, "InHouseDays": 6, "GRDays": 1, "Safety": 2, "LotSize": "EX"},
"CP-MTR15": {"Make": False, "DeliveryDays": 21, "GRDays": 1, "Safety": 6, "LotSize": "EX"},
"CP-IMP200": {"Make": False, "DeliveryDays": 28, "GRDays": 2, "Safety": 8, "LotSize": "EX"},
"CP-SK200": {"Make": False, "DeliveryDays": 10, "GRDays": 1, "Safety": 50, "LotSize": "EX"},
}
# Bills of material from the master data topic (trimmed to the pack's components).
BOM = {"CP-ES200": {"CP-IMP200": 1, "CP-MTR15": 1, "CP-SK200": 1}, "CP-ES300": {"CP-MTR15": 1}}
# Routings: hours per pump at each work center (master data topic).
ROUTING = {"CP-ES200": {"ASSY1": 1.5, "TEST1": 0.5}, "CP-ES300": {"ASSY1": 2.0, "TEST1": 0.75}}
LOW_LEVEL = ["CP-ES200", "CP-ES300", "CP-MTR15", "CP-IMP200", "CP-SK200"] # plan top-down
# ------------------------------------------------------------ the scenario
STOCK = {"CP-ES200": 12, "CP-ES300": 3, "CP-MTR15": 5, "CP-IMP200": 30, "CP-SK200": 300}
# Planned independent requirements, after A_PlannedIndepRqmtItem: one quantity per month,
# dated on the month's first working day. Calder plans pumps with final assembly
# (strategy 40), so sales orders in a month consume that month's PIR.
PIRS = [
{"Product": "CP-ES200", "PlndIndepRqmtPeriodStartDate": "2026-11-02", "PlannedQuantity": 20},
{"Product": "CP-ES200", "PlndIndepRqmtPeriodStartDate": "2026-12-01", "PlannedQuantity": 20},
{"Product": "CP-ES300", "PlndIndepRqmtPeriodStartDate": "2026-11-02", "PlannedQuantity": 6},
{"Product": "CP-ES300", "PlndIndepRqmtPeriodStartDate": "2026-12-01", "PlannedQuantity": 6},
]
# Open sales order items from the order-to-cash topic (requested dates).
SALES_ORDERS = [
("1000002", "CP-ES200", 6, "2026-10-30"),
("1000003", "CP-ES300", 2, "2026-11-02"),
("1000004", "CP-ES300", 5, "2026-10-27"),
("1000005", "CP-SK200", 40, "2026-10-28"),
("1000007", "CP-ES200", 2, "2026-11-09"),
]
# Open production orders, after A_ProductionOrder_2 (dates are basic start and finish).
PRODUCTION_ORDERS = [
{"ManufacturingOrder": "1100018", "Material": "CP-ES200", "TotalQuantity": 10,
"MfgOrderPlannedStartDate": "2026-10-16", "MfgOrderPlannedEndDate": "2026-10-22",
"Status": "released, assembly confirmed, test open", "ComponentsIssued": True},
{"ManufacturingOrder": "1100019", "Material": "CP-ES200", "TotalQuantity": 20,
"MfgOrderPlannedStartDate": "2026-11-03", "MfgOrderPlannedEndDate": "2026-11-09",
"Status": "created", "ComponentsIssued": False},
{"ManufacturingOrder": "1100021", "Material": "CP-ES300", "TotalQuantity": 4,
"MfgOrderPlannedStartDate": "2026-10-28", "MfgOrderPlannedEndDate": "2026-11-05",
"Status": "released", "ComponentsIssued": False},
]
# Open purchase orders (procure-to-pay topic, plus 4500000008 and 4500000009 for this topic).
PURCHASE_ORDERS = [
("4500000002", "CP-MTR15", 20, "2026-11-05", "Orrin Motor Works, not confirmed"),
("4500000008", "CP-IMP200", 40, "2026-11-02", "Calder GmbH (intercompany), confirmed"),
("4500000010", "CP-MTR15", 30, "2026-11-19", "Orrin Motor Works, confirmed"),
("4500000009", "CP-SK200", 100, "2026-11-12", "Ridgeline Bearing Supply, confirmed"),
]
# Work center capacity in hours per working day, and a plant maintenance order that takes
# the test bench out of service (the maintenance topic later in Unit 1B covers these).
CAPACITY_PER_DAY = {"ASSY1": 7.5, "TEST1": 4.0}
OUTAGES = [("TEST1", "2026-11-09", "2026-11-11", "maintenance order 4000123: test bench overhaul")]
# Standard cost estimate per pump, after I_ProductCostEstimate. Materials come from the
# pack's standard prices; "other BOM items" stands for the castings and parts the course
# BOM leaves out. Activity rates and overhead are course values. Totals equal the pack's
# standard prices: 2,850.00 and 4,100.00 USD.
COST_ESTIMATE = {
"CP-ES200": [("material", "CP-IMP200", 1, 335.00), ("material", "CP-MTR15", 1, 920.00),
("material", "CP-SK200", 1, 38.00), ("material", "other BOM items", 1, 1050.00),
("activity", "ASSY1", 1.5, 140.00), ("activity", "TEST1", 0.5, 180.00),
("overhead", "overhead", 1, 207.00)],
"CP-ES300": [("material", "CP-MTR15", 1, 920.00), ("material", "other BOM items", 1, 2200.00),
("activity", "ASSY1", 2.0, 140.00), ("activity", "TEST1", 0.75, 180.00),
("overhead", "overhead", 1, 565.00)],
}
# Two finished orders from October, for the trace and the variance explanation.
DONE_ORDERS = {
"1100015": {"Material": "CP-ES200", "TotalQuantity": 10, "Delivered": 10, "Scrap": 0,
"Events": [("2026-09-28", "Planned order 0000050011", "MRP run", "10 x CP-ES200, start 2026-10-05"),
("2026-10-02", "Production order 1100015", "Production planner", "converted from the planned order; reservations for 3 components"),
("2026-10-05", "Release", "Production supervisor", "availability check OK; shop papers printed"),
("2026-10-05", "Goods issue (staging)", "Warehouse", "components issued to the order"),
("2026-10-08", "Confirmation 0010 ASSY1", "Operator", "yield 10, 17.5 h (plan 15.0)"),
("2026-10-09", "Confirmation 0020 TEST1", "Operator", "yield 10, 5.0 h; final confirmation"),
("2026-10-09", "Goods receipt", "Operator", "10 to unrestricted stock (no inspection type for ES-200)"),
("2026-10-31", "Settlement", "Cost accounting", "variance to price differences")],
"Actual": [("material", "CP-IMP200", 10, 335.00), ("material", "CP-MTR15", 10, 920.00),
("material", "CP-SK200", 11, 38.00), ("material", "other BOM items", 10, 1050.00),
("activity", "ASSY1", 17.5, 140.00), ("activity", "TEST1", 5.0, 180.00),
("overhead", "overhead", 10, 207.00)],
"Notes": {"CP-SK200": "one seal kit damaged at fitting", "ASSY1": "new fitter on the line"}},
"1100016": {"Material": "CP-ES300", "TotalQuantity": 4, "Delivered": 3, "Scrap": 1,
"Events": [("2026-10-01", "Planned order 0000050012", "MRP run", "4 x CP-ES300, start 2026-10-12"),
("2026-10-08", "Production order 1100016", "Production planner", "converted; reservation for CP-MTR15"),
("2026-10-12", "Release", "Production supervisor", "availability check OK"),
("2026-10-12", "Goods issue (staging)", "Warehouse", "4 motors issued to the order"),
("2026-10-15", "Confirmation 0010 ASSY1", "Operator", "yield 4, 8.0 h"),
("2026-10-16", "Confirmation 0020 TEST1", "Operator", "yield 3, scrap 1, 4.0 h (one retest)"),
("2026-10-16", "Goods receipt", "Operator", "3 to quality inspection, lot 040000000007"),
("2026-10-19", "Usage decision", "Quality engineer", "accepted: 3 to unrestricted stock"),
("2026-10-19", "Delivery completed", "Production planner", "no more receipts expected"),
("2026-10-31", "Settlement", "Cost accounting", "variance to price differences")],
"Actual": [("material", "CP-MTR15", 4, 920.00), ("material", "other BOM items", 4, 2200.00),
("activity", "ASSY1", 8.0, 140.00), ("activity", "TEST1", 4.0, 180.00),
("overhead", "overhead", 4, 565.00)],
"Notes": {"scrap": "pump failed the pressure test", "TEST1": "one retest before the pump was scrapped"}},
}
STD_PRICE = {"CP-ES200": 2850.00, "CP-ES300": 4100.00} # checked against products.json at start
OWNER = {
"10": "Production planner (move the order earlier, or tell sales)",
"15": "Buyer (ask the supplier to deliver later)",
"20": "Buyer (cancel or reduce the order)",
"96": "Buyer (expedite) and production planner",
"06": "Production planner (convert now, check capacity)",
"07": "Production supervisor (finish the order or reschedule it)",
"10P": "Buyer (expedite the purchase order)",
"06P": "Buyer (order now, ask for a shorter lead time)",
"CAP": "Production supervisor, with maintenance planning",
}
TEXT = {"06": "start date in the past", "07": "finish date in the past", "10": "reschedule in",
"15": "reschedule out", "20": "cancel process", "96": "stock below safety stock"}
# --------------------------------------------------------------- helpers
def d(text: str) -> date:
return date.fromisoformat(text)
def money(x: float) -> str:
return f"{x:,.2f}"
def workdays_back(day: date, n: int) -> date:
"""Go back n working days (Monday to Friday; the course ignores holidays)."""
while n > 0:
day -= timedelta(days=1)
if day.weekday() < 5:
n -= 1
return day
def workdays_forward(day: date, n: int) -> date:
while n > 0:
day += timedelta(days=1)
if day.weekday() < 5:
n -= 1
return day
def workdays_between(a: date, b: date) -> int:
"""Working days from a to b (negative if b is earlier)."""
sign, lo, hi = (1, a, b) if b >= a else (-1, b, a)
n, day = 0, lo
while day < hi:
day += timedelta(days=1)
if day.weekday() < 5:
n += 1
return sign * n
def supplier(rec: dict) -> str:
return f" from {rec['who']}" if rec.get("who") else ""
def check_pack() -> None:
try:
products = json.loads((PACK / "products.json").read_text(encoding="utf-8"))
except FileNotFoundError:
sys.exit(f"Cannot read {PACK}. Run: python unit1b/make_calder_pack.py (Unit 1B setup).")
for p in products["Products"]:
if p["Product"] in STD_PRICE and p["to_Valuation"][0]["StandardPrice"] != STD_PRICE[p["Product"]]:
sys.exit(f"The standard price of {p['Product']} in the pack has changed. "
"Run python unit1b/make_calder_pack.py again.")
for material, lines in COST_ESTIMATE.items():
total = round(sum(q * r for _, _, q, r in lines), 2)
if total != STD_PRICE[material]:
sys.exit(f"Cost estimate for {material} adds up to {money(total)}, not {money(STD_PRICE[material])}.")
# --------------------------------------------------------------- demand
def independent_demand(material: str) -> list:
"""Sales orders plus the part of each month's PIR that sales orders didn't consume."""
rows = []
for so, mat, qty, day in SALES_ORDERS:
if mat == material:
rows.append({"date": d(day), "qty": qty, "what": f"sales order {so}", "customer": True})
for pir in PIRS:
if pir["Product"] != material:
continue
start = d(pir["PlndIndepRqmtPeriodStartDate"])
in_month = sum(q for _, m, q, day in SALES_ORDERS
if m == material and d(day).year == start.year and d(day).month == start.month)
rest = max(pir["PlannedQuantity"] - in_month, 0)
note = f"PIR {start:%b} (rest of {pir['PlannedQuantity']} after {in_month} on orders)"
if rest:
rows.append({"date": start, "qty": rest, "what": note, "customer": False})
return rows
# --------------------------------------------------------------- MRP (teaching sketch)
def plan_material(material: str, demand: list, receipts: list) -> dict:
"""Net one material: check existing receipts, then create proposals for what is still short.
demand: [{"date", "qty", "what", "customer"}]
receipts: [{"date", "qty", "what", "kind"}] kind: production order / purchase order
"""
m = MATERIALS[material]
demand = sorted(demand, key=lambda r: (r["date"], r["what"]))
receipts = sorted(receipts, key=lambda r: (r["date"], r["what"]))
available = STOCK[material] - m["Safety"]
messages = []
if STOCK[material] < m["Safety"]:
messages.append({"code": "96", "material": material, "element": "stock",
"detail": f"stock {STOCK[material]} is below safety stock {m['Safety']}", "value": 0})
def need_date(covered: float):
"""First demand date on which cumulative demand exceeds what is already covered."""
cum = 0
for row in demand:
cum += row["qty"]
if cum > covered:
return row["date"], row
return None, None
covered = available
for rec in receipts:
when, row = need_date(covered)
rec["needed"] = when
if when is None:
messages.append({"code": "20", "material": material, "element": rec["what"],
"detail": f"{rec['qty']} due {rec['date']}{supplier(rec)}, not needed before {HORIZON_END}",
"value": rec["qty"], "receipt": rec})
elif when < rec["date"]:
messages.append({"code": "10" if rec["kind"] == "production order" else "10P",
"material": material, "element": rec["what"],
"detail": f"{rec['qty']} due {rec['date']}{supplier(rec)}, needed {when} for {row['what']}",
"value": rec["qty"], "receipt": rec, "customer": row["customer"],
"late_days": workdays_between(when, rec["date"])})
elif workdays_between(rec["date"], when) > RESCHEDULE_OUT_DAYS:
messages.append({"code": "15", "material": material, "element": rec["what"],
"detail": f"{rec['qty']} due {rec['date']}{supplier(rec)}, not needed until {when}",
"value": rec["qty"], "receipt": rec})
covered += rec["qty"]
proposals = []
cum = 0
by_date = {}
for row in demand: # one proposal per day at most: merge the day's demand
day = by_date.setdefault(row["date"], {"date": row["date"], "qty": 0, "what": [], "customer": False})
day["qty"] += row["qty"]
day["what"].append(row["what"])
day["customer"] = day["customer"] or row["customer"]
for row in by_date.values():
row["what"] = " + ".join(row["what"])
cum += row["qty"]
short = cum - covered
if short <= 0:
continue
qty = short
if m["LotSize"].startswith("FX"):
lot = int(m["LotSize"][2:])
qty = -(-short // lot) * lot # round up to whole lots
finish = workdays_back(row["date"], m["GRDays"])
start = workdays_back(finish, m["InHouseDays"] if m["Make"] else m["DeliveryDays"])
prop = {"material": material, "qty": qty, "needed": row["date"], "start": start, "finish": finish,
"kind": "planned order" if m["Make"] else "purchase requisition", "for": row["what"]}
if start < AS_OF: # SAP switches to forward scheduling when the start date is in the past
prop["start"] = AS_OF
prop["finish"] = workdays_forward(AS_OF, m["InHouseDays"] if m["Make"] else m["DeliveryDays"])
prop["available"] = workdays_forward(prop["finish"], m["GRDays"])
messages.append({"code": "06" if m["Make"] else "06P", "material": material, "element": f"new {prop['kind']}",
"detail": f"{qty} needed {row['date']} for {row['what']}; "
f"forward-scheduled, available {prop['available']}",
"value": qty, "customer": row["customer"],
"late_days": workdays_between(row["date"], prop["available"])})
else:
prop["available"] = row["date"]
proposals.append(prop)
covered += qty
return {"material": material, "demand": demand, "receipts": receipts,
"proposals": proposals, "messages": messages}
def run_mrp() -> dict:
"""Plan materials by low-level code: finished pumps first, then their components."""
results, dependent = {}, {mat: [] for mat in MATERIALS}
for material in LOW_LEVEL:
m = MATERIALS[material]
receipts = []
for po in PRODUCTION_ORDERS:
if po["Material"] == material:
end = d(po["MfgOrderPlannedEndDate"])
avail = workdays_forward(end, m["GRDays"])
receipts.append({"date": max(avail, AS_OF), "qty": po["TotalQuantity"], "kind": "production order",
"what": f"production order {po['ManufacturingOrder']}", "order": po})
for number, mat, qty, day, who in PURCHASE_ORDERS:
if mat == material:
receipts.append({"date": d(day), "qty": qty, "kind": "purchase order",
"what": f"purchase order {number}", "who": who})
demand = independent_demand(material) + dependent[material]
res = plan_material(material, demand, receipts)
# finish date in the past for production orders still open
for po in PRODUCTION_ORDERS:
if po["Material"] == material and d(po["MfgOrderPlannedEndDate"]) < AS_OF:
res["messages"].append({"code": "07", "material": material,
"element": f"production order {po['ManufacturingOrder']}",
"detail": f"finish {po['MfgOrderPlannedEndDate']}, status {po['Status']}",
"value": po["TotalQuantity"], "customer": False})
results[material] = res
# explode BOMs: open production orders (not yet issued) and new planned orders
for comp, per in BOM.get(material, {}).items():
for po in PRODUCTION_ORDERS:
if po["Material"] == material and not po["ComponentsIssued"]:
dependent[comp].append({"date": max(d(po["MfgOrderPlannedStartDate"]), AS_OF),
"qty": per * po["TotalQuantity"], "customer": False,
"what": f"reservation for order {po['ManufacturingOrder']}"})
for i, prop in enumerate(res["proposals"], 1):
dependent[comp].append({"date": prop["start"], "qty": per * prop["qty"], "customer": False,
"what": f"planned order {material}/{i}"})
return results
# --------------------------------------------------------------- worklist
def worklist(results: dict) -> list:
"""One line per message, most urgent first: customer impact, then lateness, then date."""
rows = []
for res in results.values():
for msg in res["messages"]:
late = msg.get("late_days", 0)
score = (5 if msg.get("customer") else 0) + min(late, 10) / 2
if msg["code"] in ("96", "07"):
score += 2
if msg["code"] in ("15", "20"):
score += 0.5
rows.append({**msg, "score": score, "owner": OWNER[msg["code"]]})
return sorted(rows, key=lambda r: (-r["score"], r["material"], r["element"]))
def capacity(results: dict) -> dict:
"""Hours per work center and week (Monday). Course rule: each operation's load falls in
the week of the order's finish date; SAP schedules each operation on its own dates."""
load = {}
orders = [(po["Material"], po["TotalQuantity"], d(po["MfgOrderPlannedEndDate"]), po["ManufacturingOrder"])
for po in PRODUCTION_ORDERS if d(po["MfgOrderPlannedEndDate"]) >= AS_OF]
for res in results.values():
for i, prop in enumerate(res["proposals"], 1):
if prop["kind"] == "planned order":
orders.append((prop["material"], prop["qty"], prop["finish"], f"planned {prop['material']}/{i}"))
for material, qty, finish, label in orders:
week = finish - timedelta(days=finish.weekday())
for wc, hours in ROUTING[material].items():
entry = load.setdefault((wc, week), {"hours": 0.0, "orders": []})
entry["hours"] += hours * qty
entry["orders"].append(label)
table = {}
for (wc, week), entry in sorted(load.items()):
days = [week + timedelta(days=i) for i in range(5)]
down = sum(1 for day in days for w, a, b, _ in OUTAGES if w == wc and d(a) <= day <= d(b))
avail = CAPACITY_PER_DAY[wc] * (5 - down)
table[(wc, week)] = {"load": entry["hours"], "available": avail, "orders": entry["orders"], "down": down}
return table
def print_worklist(results: dict) -> list:
rows = worklist(results)
print(f"MRP results for plant {PLANT} as of {AS_OF} (fictional Calder data)\n")
print("New proposals:")
for res in results.values():
for p in res["proposals"]:
late = "" if p["available"] == p["needed"] else f" (needed {p['needed']})"
print(f" {p['kind']:<21} {p['material']:<10} {p['qty']:>4} start {p['start']} "
f"available {p['available']}{late}")
print("\nException messages, most urgent first:\n")
print(f" # {'Msg':<4}{'Material':<11}{'Element':<27}{'Owner'}")
for i, r in enumerate(rows, 1):
code = r["code"].rstrip("P")
print(f" {i:<2} {code:<4}{r['material']:<11}{r['element']:<27}{r['owner']}")
print(f" {'':<2} {'':<4}{'':<11}{TEXT[code]}: {r['detail']}")
cap = capacity(results)
over = [(k, v) for k, v in cap.items() if v["load"] > v["available"]]
print("\nCapacity overloads (load above available hours):")
for (wc, week), v in over:
pct = 100 * v["load"] / v["available"] if v["available"] else float("inf")
print(f" {wc} week of {week}: {v['load']:.1f} h load, {v['available']:.1f} h available ({pct:.0f}%)"
f"{'; ' + str(v['down']) + ' days down for maintenance' if v['down'] else ''}")
if not over:
print(" none")
customer = sum(1 for r in rows if r.get("customer"))
print(f"\n {len(rows)} messages, {customer} of them touch a customer order; {len(over)} capacity overload(s).")
print(" Synthetic data: the list shows the method, not results from an SAP system.")
return rows
def show_material(results: dict, material: str) -> None:
if material not in results:
sys.exit(f"Material {material} is not in the scenario. Use one of: {', '.join(results)}")
res, m = results[material], MATERIALS[material]
print(f"{material} at {PLANT}: stock {STOCK[material]}, safety stock {m['Safety']}, "
f"lot size {m['LotSize']} (fictional)\n")
events = [(r["date"], -r["qty"], r["what"]) for r in res["demand"]]
events += [(r["date"], r["qty"], r["what"]) for r in res["receipts"]]
events += [(p["available"], p["qty"], f"NEW {p['kind']} (start {p['start']})") for p in res["proposals"]]
level = STOCK[material]
print(f" {'Date':<11}{'Element':<58}{'Qty':>6}{'Projected':>11}")
print(f" {AS_OF!s:<11}{'stock':<58}{'':>6}{level:>11}")
for day, qty, what in sorted(events, key=lambda e: (e[0], e[1] < 0, e[2])):
level += qty
flag = " < safety" if level < m["Safety"] else ""
print(f" {day!s:<11}{what[:56]:<58}{qty:>+6}{level:>11}{flag}")
print("\n Messages:")
for msg in res["messages"] or [{"code": "--", "detail": "none"}]:
code = msg["code"].rstrip("P")
print(f" {code} {TEXT.get(code, '')}{': ' if code in TEXT else ''}{msg['detail']}")
def print_capacity(results: dict) -> None:
print(f"Work center load per week at {PLANT} (fictional; load counted in the week each order finishes)\n")
print(f" {'Work center':<12}{'Week of':<12}{'Load h':>8}{'Avail h':>9}{'Use':>7} Orders")
for (wc, week), v in capacity(results).items():
pct = f"{100 * v['load'] / v['available']:.0f}%" if v["available"] else "n/a"
mark = " OVER" if v["load"] > v["available"] else ""
print(f" {wc:<12}{week!s:<12}{v['load']:>8.1f}{v['available']:>9.1f}{pct:>7} "
f"{', '.join(v['orders'])}{mark}")
for wc, a, b, why in OUTAGES:
print(f"\n {wc} is down {a} to {b}: {why}.")
print(" MRP plans with infinite capacity; this check is where the overload shows.")
# --------------------------------------------------------------- one order, cost and variance
def explain_order(number: str) -> None:
if number not in DONE_ORDERS:
sys.exit(f"Order {number} is not in the scenario. Use one of: {', '.join(DONE_ORDERS)}")
o = DONE_ORDERS[number]
material, qty, good, scrap = o["Material"], o["TotalQuantity"], o["Delivered"], o["Scrap"]
print(f"Production order {number}: {qty} x {material}, delivered {good}, scrap {scrap} (fictional)\n")
for day, what, who, note in o["Events"]:
print(f" {day} {what:<26}{who:<22}{note}")
estimate = {name: (q, r) for _, name, q, r in COST_ESTIMATE[material]}
actual = {name: (q, r) for _, name, q, r in o["Actual"]}
total_actual = round(sum(q * r for q, r in actual.values()), 2)
credit = round(good * STD_PRICE[material], 2)
variance = round(total_actual - credit, 2)
print(f"\nCosts on the order (USD):")
print(f" actual debits {money(total_actual):>12} goods issues, confirmations, overhead")
print(f" credit from GR {money(-credit):>12} {good} x {money(STD_PRICE[material])} standard price")
print(f" balance (variance) {money(variance):>12} settled to price differences at period end")
# Variance split: target = cost estimate x quantity that went through the order.
print("\nWhy (variance split, course version of SAP's categories):")
rows = []
if scrap:
scrap_cost = round(scrap * STD_PRICE[material], 2)
rows.append(("scrap", "all", scrap_cost, f"{scrap} pump scrapped at the last operation, valued at target"))
for name, (q_est, rate) in estimate.items():
q_act, rate_act = actual[name]
target_qty = q_est * qty # target for everything that went through the order, scrap included
qvar = round((q_act - target_qty) * rate, 2)
pvar = round((rate_act - rate) * q_act, 2)
if qvar:
rows.append(("input quantity", name, qvar, f"{q_act:g} used vs {target_qty:g} planned"
+ (f"; {o['Notes'][name]}" if name in o["Notes"] else "")))
if pvar:
rows.append(("input price", name, pvar, f"rate {money(rate_act)} vs {money(rate)}"))
explained = round(sum(v for _, _, v, _ in rows), 2)
if round(variance - explained, 2):
rows.append(("remaining", "", round(variance - explained, 2), "not explained by the rules above"))
for cat, name, value, why in rows:
print(f" {cat:<15}{name:<17}{money(value):>10} {why}")
if scrap:
print(f" (scrap reason: {o['Notes']['scrap']})")
print(f" {'total':<32}{money(variance):>10}")
print("\n Synthetic numbers for a fictional order, not from an SAP system.")
# --------------------------------------------------------------- markdown
FENCE = "`" * 3 # a Markdown code fence, built so this script can sit inside one
def write_markdown(results: dict, rows: list) -> Path:
out = HERE / "calder_mrp_worklist.md"
lines = [f"# Calder MRP worklist, plant {PLANT}, {AS_OF}", "",
"Fictional data from the plan-to-produce topic. Shows the method, not results from SAP.", "",
FENCE + "mermaid", "flowchart LR",
" D[Demand<br/>PIRs, sales orders] --> M[MRP run]", " M --> PL[Planned orders<br/>requisitions]",
" M --> EX[Exception messages]", " PL --> PO[Production orders]",
" PO --> GI[Components issued] --> CF[Confirmations] --> GR[Goods receipt]",
" GR --> ST[Settlement<br/>variances]", FENCE, "",
"| # | Msg | Material | Element | What | Owner |", "| --- | --- | --- | --- | --- | --- |"]
for i, r in enumerate(rows, 1):
code = r["code"].rstrip("P")
lines.append(f"| {i} | {code} | {r['material']} | {r['element']} | {TEXT[code]}: {r['detail']} | {r['owner']} |")
lines += ["", "## Capacity", "", "| Work center | Week of | Load h | Available h |", "| --- | --- | --- | --- |"]
for (wc, week), v in capacity(results).items():
lines.append(f"| {wc} | {week} | {v['load']:.1f} | {v['available']:.1f} |")
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
return out
# --------------------------------------------------------------- SAP sandbox (optional)
def read_env_key(name: str) -> str:
value = os.environ.get(name, "")
env = Path.cwd() / ".env"
if not value and env.exists():
for line in env.read_text(encoding="utf-8").splitlines():
if line.strip().startswith(f"{name}="):
value = line.split("=", 1)[1].strip().strip('"').strip("'")
return value
def sandbox() -> None:
key = read_env_key("SAP_API_KEY")
if not key:
sys.exit("SAP_API_KEY is not set. Add it to .env (course setup) or skip this step.")
base = "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/API_PLANNED_ORDERS/A_PlannedOrder"
query = urllib.parse.urlencode({"$top": "50", "$format": "json",
"$select": "PlannedOrder,Material,MRPPlant,TotalQuantity,"
"PlndOrderPlannedStartDate,PlannedOrderIsFirm,PlannedOrderIsConvertible"})
req = urllib.request.Request(f"{base}?{query}", headers={"APIKey": key, "Accept": "application/json"})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as err:
sys.exit(f"Sandbox answered HTTP {err.code}. Check the key, or try again later.")
except (urllib.error.URLError, TimeoutError) as err:
sys.exit(f"Cannot reach the sandbox ({err}). Check the network or proxy.")
items = data.get("d", {}).get("results", [])
plants, firm = {}, 0
for it in items:
plants[it.get("MRPPlant", "")] = plants.get(it.get("MRPPlant", ""), 0) + 1
firm += 1 if it.get("PlannedOrderIsFirm") else 0
print(f"SAP sandbox: {len(items)} planned orders read (SAP demo data, not Calder's)\n")
print(" MRPPlant: " + ", ".join(f"'{p or '(empty)'}' x{n}" for p, n in sorted(plants.items())))
print(f" firmed: {firm} of {len(items)}")
for it in items[:3]:
print(f" e.g. {it.get('PlannedOrder')} {it.get('Material')} qty {it.get('TotalQuantity')}")
# --------------------------------------------------------------- main
def main() -> None:
parser = argparse.ArgumentParser(description="Plan to produce at Calder (fictional data).")
parser.add_argument("--material", help="show one material's netting, e.g. CP-MTR15")
parser.add_argument("--capacity", action="store_true", help="work center load per week")
parser.add_argument("--order", help="trace one finished production order, e.g. 1100016")
parser.add_argument("--write", action="store_true", help="write unit1b/calder_mrp_worklist.md")
parser.add_argument("--sandbox", action="store_true", help="count planned orders in SAP's sandbox")
args = parser.parse_args()
if args.sandbox:
sandbox()
return
check_pack()
if args.order:
explain_order(args.order)
return
results = run_mrp()
if args.material:
show_material(results, args.material)
elif args.capacity:
print_capacity(results)
else:
rows = print_worklist(results)
if args.write:
print(f"\nwrote unit1b/{write_markdown(results, rows).name}")
if __name__ == "__main__":
main()
You should see exactly this. The script has no random parts, so your output must match:
MRP results for plant CP11 as of 2026-10-26 (fictional Calder data)
New proposals:
planned order CP-ES200 10 start 2026-11-23 available 2026-12-01
planned order CP-ES300 6 start 2026-10-26 available 2026-11-04 (needed 2026-11-02)
planned order CP-ES300 6 start 2026-11-20 available 2026-12-01
Exception messages, most urgent first:
# Msg Material Element Owner
1 10 CP-ES300 production order 1100021 Production planner (move the order earlier, or tell sales)
reschedule in: 4 due 2026-11-06, needed 2026-10-27 for sales order 1000004
2 06 CP-ES300 new planned order Production planner (convert now, check capacity)
start date in the past: 6 needed 2026-11-02 for PIR Nov (rest of 6 after 2 on orders) + sales order 1000003; forward-scheduled, available 2026-11-04
3 10 CP-MTR15 purchase order 4500000010 Buyer (expedite the purchase order)
reschedule in: 30 due 2026-11-19 from Orrin Motor Works, confirmed, needed 2026-11-03 for reservation for order 1100019
4 10 CP-MTR15 purchase order 4500000002 Buyer (expedite the purchase order)
reschedule in: 20 due 2026-11-05 from Orrin Motor Works, not confirmed, needed 2026-10-26 for planned order CP-ES300/1
5 10 CP-ES200 production order 1100019 Production planner (move the order earlier, or tell sales)
reschedule in: 20 due 2026-11-10, needed 2026-11-02 for PIR Nov (rest of 20 after 2 on orders)
6 07 CP-ES200 production order 1100018 Production supervisor (finish the order or reschedule it)
finish date in the past: finish 2026-10-22, status released, assembly confirmed, test open
7 96 CP-MTR15 stock Buyer (expedite) and production planner
stock below safety stock: stock 5 is below safety stock 6
8 15 CP-IMP200 purchase order 4500000008 Buyer (ask the supplier to deliver later)
reschedule out: 40 due 2026-11-02 from Calder GmbH (intercompany), confirmed, not needed until 2026-11-23
9 20 CP-SK200 purchase order 4500000009 Buyer (cancel or reduce the order)
cancel process: 100 due 2026-11-12 from Ridgeline Bearing Supply, confirmed, not needed before 2026-12-31
Capacity overloads (load above available hours):
TEST1 week of 2026-11-09: 10.0 h load, 8.0 h available (125%); 3 days down for maintenance
9 messages, 2 of them touch a customer order; 1 capacity overload(s).
Synthetic data: the list shows the method, not results from an SAP system.
How to read it:
New proposals are what MRP adds. The first ES-300 planned order should have started on 22 October to be ready on 2 November. That date is past, so the script schedules it forward from today, as SAP does, and it arrives on 4 November with message 06.
Message 10 on order 1100021 is order 1000004 from the order-to-cash topic seen from the plant: Brennock wants five ES-300 on 27 October, three are in stock with two of them held as safety stock, and the order that covers the rest finishes on 6 November. MRP can't pull a released order forward by itself. The planner has to decide whether the plant can, and sales has to tell the customer if it can't.
Messages 3, 4 and 7 are one problem: motors. Five are in stock against a safety stock of six, and the two purchase orders arrive after the line needs them. Purchase order 4500000002 is the unconfirmed one from the procure-to-pay worklist. Here it shows why a missing confirmation matters to the plant: MRP is planning on a date nobody promised.
Messages 8 and 9 are the quiet ones: impellers that come three weeks early and seal kits that aren't needed at all. They cost cash, not customers, so they rank last.
The ranking is the script's own: a customer order at stake first, then how late, then stock and past-date problems, then early or unneeded receipts. SAP lets you group and prioritize messages in configuration; the order a plant chooses is a business decision.
CP-MTR15 at CP11: stock 5, safety stock 6, lot size EX (fictional)
Date Element Qty Projected
2026-10-26 stock 5
2026-10-26 planned order CP-ES300/1 -6 -1 < safety
2026-10-28 reservation for order 1100021 -4 -5 < safety
2026-11-03 reservation for order 1100019 -20 -25 < safety
2026-11-05 purchase order 4500000002 +20 -5 < safety
2026-11-19 purchase order 4500000010 +30 25
2026-11-20 planned order CP-ES300/2 -6 19
2026-11-23 planned order CP-ES200/1 -10 9
Messages:
96 stock below safety stock: stock 5 is below safety stock 6
10 reschedule in: 20 due 2026-11-05 from Orrin Motor Works, not confirmed, needed 2026-10-26 for planned order CP-ES300/1
10 reschedule in: 30 due 2026-11-19 from Orrin Motor Works, confirmed, needed 2026-11-03 for reservation for order 1100019
This is the shape of MD04: stock, then each requirement and receipt in date order, with the projected stock after each. The motor's requirements are dependent: they come from the pumps' planned orders and from the reservations of production orders whose components aren't issued yet. Projected stock is negative from 26 October to 19 November. Order 1100019 needs 20 motors on 3 November, and only the 30 due on 19 November cover it. That is the shortage the availability check will report when someone tries to release the order. Try --material CP-ES300 and --material CP-IMP200 too.
Work center load per week at CP11 (fictional; load counted in the week each order finishes)
Work center Week of Load h Avail h Use Orders
ASSY1 2026-11-02 20.0 37.5 53% 1100021, planned CP-ES300/1
ASSY1 2026-11-09 30.0 37.5 80% 1100019
ASSY1 2026-11-30 27.0 37.5 72% planned CP-ES200/1, planned CP-ES300/2
TEST1 2026-11-02 7.5 20.0 38% 1100021, planned CP-ES300/1
TEST1 2026-11-09 10.0 8.0 125% 1100019 OVER
TEST1 2026-11-30 9.5 20.0 48% planned CP-ES200/1, planned CP-ES300/2
TEST1 is down 2026-11-09 to 2026-11-11: maintenance order 4000123: test bench overhaul.
MRP plans with infinite capacity; this check is where the overload shows.
Nothing in MRP's output warned about the test bench. MRP planned order 1100019 into the week of 9 November as if the bench were free, and a maintenance order takes it out for three of those five days. Ten hours of testing meet eight hours of bench. The fix is a decision between three people: move the order earlier (which the motor shortage in Step 5 doesn't allow), move the maintenance, or test on overtime. The script counts each order's hours in the week it finishes, which is cruder than SAP's per-operation scheduling but enough to see the overload.
#Step 7: Trace an order to settlement and explain its variance
Trace the ES-200 order from October:
Windows (PowerShell):
python unit1b\plan_to_produce.py --order 1100015
macOS / Linux:
python unit1b/plan_to_produce.py --order 1100015
You should see:
Production order 1100015: 10 x CP-ES200, delivered 10, scrap 0 (fictional)
2026-09-28 Planned order 0000050011 MRP run 10 x CP-ES200, start 2026-10-05
2026-10-02 Production order 1100015 Production planner converted from the planned order; reservations for 3 components
2026-10-05 Release Production supervisor availability check OK; shop papers printed
2026-10-05 Goods issue (staging) Warehouse components issued to the order
2026-10-08 Confirmation 0010 ASSY1 Operator yield 10, 17.5 h (plan 15.0)
2026-10-09 Confirmation 0020 TEST1 Operator yield 10, 5.0 h; final confirmation
2026-10-09 Goods receipt Operator 10 to unrestricted stock (no inspection type for ES-200)
2026-10-31 Settlement Cost accounting variance to price differences
Costs on the order (USD):
actual debits 28,888.00 goods issues, confirmations, overhead
credit from GR -28,500.00 10 x 2,850.00 standard price
balance (variance) 388.00 settled to price differences at period end
Why (variance split, course version of SAP's categories):
input quantity CP-SK200 38.00 11 used vs 10 planned; one seal kit damaged at fitting
input quantity ASSY1 350.00 17.5 used vs 15 planned; new fitter on the line
total 388.00
Synthetic numbers for a fictional order, not from an SAP system.
Now the ES-300 order with a scrapped pump, --order 1100016:
Production order 1100016: 4 x CP-ES300, delivered 3, scrap 1 (fictional)
2026-10-01 Planned order 0000050012 MRP run 4 x CP-ES300, start 2026-10-12
2026-10-08 Production order 1100016 Production planner converted; reservation for CP-MTR15
2026-10-12 Release Production supervisor availability check OK
2026-10-12 Goods issue (staging) Warehouse 4 motors issued to the order
2026-10-15 Confirmation 0010 ASSY1 Operator yield 4, 8.0 h
2026-10-16 Confirmation 0020 TEST1 Operator yield 3, scrap 1, 4.0 h (one retest)
2026-10-16 Goods receipt Operator 3 to quality inspection, lot 040000000007
2026-10-19 Usage decision Quality engineer accepted: 3 to unrestricted stock
2026-10-19 Delivery completed Production planner no more receipts expected
2026-10-31 Settlement Cost accounting variance to price differences
Costs on the order (USD):
actual debits 16,580.00 goods issues, confirmations, overhead
credit from GR -12,300.00 3 x 4,100.00 standard price
balance (variance) 4,280.00 settled to price differences at period end
Why (variance split, course version of SAP's categories):
scrap all 4,100.00 1 pump scrapped at the last operation, valued at target
input quantity TEST1 180.00 4 used vs 3 planned; one retest before the pump was scrapped
(scrap reason: pump failed the pressure test)
total 4,280.00
Synthetic numbers for a fictional order, not from an SAP system.
Order 1100015 is the clean case: the variance is small and every dollar has a cause someone wrote down at the time. Order 1100016 is the expensive one: one pump failed the pressure test after all the work was done, so a whole pump's target cost shows as scrap. The script values scrap at the full target cost because it happened at the last operation. Without the operator's scrap quantity and reason in the confirmation, the 4,280.00 USD would appear in finance as one unexplained number. That is why confirmations matter to controlling as much as to the shop floor.
Write the worklist file:
Windows (PowerShell):
python unit1b\plan_to_produce.py --write
macOS / Linux:
python unit1b/plan_to_produce.py --write
The last line should read wrote unit1b/calder_mrp_worklist.md. Open the file in VS Code and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS) to preview it: a process diagram, the nine messages with owners and the weekly load table.
#Step 8 (optional): Read planned orders from SAP's sandbox
This step reads SAP's demo data, not Calder's. It needs your sandbox key and the network.
Make sure .env in the course folder has a line SAP_API_KEY="...".
Run:
Windows (PowerShell):
python unit1b\plan_to_produce.py --sandbox
macOS / Linux:
python unit1b/plan_to_produce.py --sandbox
The output has this shape. Values in angle brackets come from SAP's demo data and change over time:
SAP sandbox: <n> planned orders read (SAP demo data, not Calder's)
MRPPlant: '<plant>' x<count>
firmed: <count> of <n>
e.g. <planned order> <material> qty <quantity>
The script reads up to 50 planned orders from API_PLANNED_ORDERS with $select. A valid "empty" result is 0 planned orders read: the sandbox simply has none at that moment. The sandbox is read-only, so you can't convert anything there.
Planning data, BOMs and routings from the master data topic: lead times, safety stock, lot size, hours per pump
STOCK, PIRS, SALES_ORDERS
Stock at CP11, monthly PIRs, and the open sales orders from the order-to-cash topic
PRODUCTION_ORDERS, PURCHASE_ORDERS
Open receipts MRP must check: three production orders and four purchase orders
independent_demand()
Lets the month's sales orders consume the month's PIR, so demand isn't counted twice
plan_material()
Netting: finds the date each receipt is needed, raises 10, 15 or 20, then creates proposals for what is still short, scheduled backward, or forward with 06 when the start is past
run_mrp()
Plans by low-level code and explodes BOMs: planned orders and unissued reservations become the components' demand; adds 07 for open orders past their finish date
worklist()
Ranks messages: customer orders first, then lateness, then stock and past dates, then early or unneeded receipts; names one owner
capacity(), --capacity
Hours per work center and week against available hours, minus the maintenance outage
explain_order(), --order
The order's events, its debits and credit, and the variance split into scrap, input quantity, input price and remaining
--material, --write
One material's netting as an MD04-like list; the worklist as a Markdown file
sandbox()
Reads up to 50 sandbox planned orders with $select and counts plants and firmed orders
Where it sits. At Calder (fictional), plan to produce for CP11 runs inside S/4HANA: demand, MRP Live, production orders and costing. The shop floor reports through a manufacturing execution system (MES), and the Rockford distribution center stages components through EWM. Plant maintenance plans test bench outages in the same S/4HANA system. CP20 in Augsburg plans and makes impellers on ECC. Anything that ranks or explains reads released interfaces and writes only to a worklist; orders are changed in SAP's own apps.
flowchart LR
IBP[Demand planning<br/>forecast] -->|PIRs| S4[S/4HANA CP11<br/>MRP Live, orders,<br/>costing]
S4 -->|released orders| MES[MES<br/>shop floor]
MES -->|confirmations,<br/>goods movements| S4
S4 <-->|staging| EWM[EWM<br/>Rockford DC]
PM[Plant maintenance<br/>outages] -.->|capacity| S4
ECC[ECC CP20<br/>impellers] -.->|intercompany| S4
S4 -->|released APIs, CDS| TRI[Exception triage<br/>read-only]
TRI --> WL[Worklist per planner]
WL -->|planner acts in SAP app| S4
Non-functional requirements that decide the design
Requirement
Typical target
If missed
MRP run time
The nightly run finishes before the morning shift starts planning
Planners work on yesterday's plan
Exception volume
Each planner's daily list is short enough to finish, ranked by impact
The easy messages get fixed, not the important ones
Confirmation latency
Shop floor confirmations reach S/4HANA within minutes to hours
Stock and order status are wrong; MRP plans on stale data
Capacity accuracy
Work center capacity includes shifts and planned maintenance
Overloads appear on the day, not the week before
Period-end close
WIP and variances run and settle within the close calendar
Inventory and margin reports are late or wrong
Security
Planners change only their MRP controllers' materials; cost data is restricted
Unauthorized order changes; exposed product costs
Integration and identity. An MES such as SAP Digital Manufacturing receives materials, work centers, BOMs, routings and production orders from S/4HANA, and sends back confirmations with yield, scrap and time, goods issues and receipts, inspection results and status updates (SAP Learning). When planning moves to SAP IBP and PP/DS, IBP builds the network supply plan and PP/DS schedules planned and production orders finitely in the plant, sending changed dates back to IBP (SAP Learning). A triage service reads with the planner's identity where possible, so authorizations on plant and MRP controller apply (grounding with authorizations). Integration patterns are covered in how SAP systems talk.
Context: Calder's Rockford plant has one bottleneck, the test bench, and many standard components. Planners see dozens of MRP messages a day and catch capacity problems late. Augsburg is still on ECC.
Options: (a) MRP Live only, with capacity checked by hand in Manage Work Center Capacity; (b) MRP Live for components and PP/DS for the pumps that load the test bench, with finite scheduling; (c) move supply planning to SAP IBP and schedule in PP/DS.
Decision: (b). PP/DS is aimed at critical products on bottleneck resources, and SAP's training says to plan all products that share those resources together. Components stay in MRP Live. IBP waits until Augsburg is on S/4HANA and Calder needs network-wide planning.
Consequences: Pump materials need PP/DS settings and the test bench a finite resource. Planners learn a second tool. Maintenance outages must reach the bench's capacity. Licensing for PP/DS must be checked with SAP for Calder's edition before the design is final.
What a design review will ask
Which materials does MRP Live plan, which does PP/DS or IBP plan, and who owns the boundary?
How do maintenance orders reduce work center capacity, and how far ahead?
How quickly do shop floor confirmations and goods movements reach S/4HANA, and what happens when the MES is down?
Can any component reschedule, convert, release or cancel an order without a planner's decision?
How will you measure success: on-time order completion, exceptions closed per day, variance explained within the close?
Deterministic rules on master data; AI shouldn't replace them
Exception triage
Rank, retrieve and explain
Strong
Rank by customer and cost impact, gather the order, component and capacity facts, propose the action and the owner. SAP lists Planning Assistant agents for exception management as planned for Q4 2026 (SAP)
Simulation belongs in pMRP or PP/DS; AI can summarize the options
Shortage at release
Retrieve and explain
Strong
Link the short component to its purchase order, confirmation and supplier history
Confirmations
Extract
Moderate
Turn shift notes and scrap reasons into structured confirmation data for a person to check
Variance explanation
Retrieve and explain
Strong
Join the variance categories with confirmations, scrap reasons and goods issues; explaining model predictions covers how to show the reasons
Unusual variances
Detect anomalies
Moderate
Catch orders whose variance doesn't fit their history; see anomaly detection
Production master data
Retrieve and suggest
Moderate
SAP's Sapphire 2026 guide lists production master data AI capabilities with the Manufacturing Assistant and a smart recommender for manufacturing BOMs (SAP)
The line is the same at every step: AI forecasts, ranks, explains and drafts; a planner converts, reschedules and cancels, a supervisor releases, a buyer changes supplier dates and a cost accountant settles. Human approval in AI workflows and agent permissions show how to enforce that in code. The capstone exception management platform builds MRP exception triage end to end.
Turn the worklist into a planner's action note and test one fix yourself.
Run python unit1b/plan_to_produce.py --write (Windows: python unit1b\plan_to_produce.py --write). Copy unit1b/calder_mrp_worklist.md to unit1b/calder_mrp_actions.md.
In the copy, add a column Action to the message table. For each line write one sentence: what the owner should do today.
Add a column AI could with one of: rank, explain, predict, extract or nothing. Use the table under Where AI fits to choose.
In plan_to_produce.py, find order 1100019. Change "MfgOrderPlannedStartDate": "2026-11-03" to "2026-10-27" and "MfgOrderPlannedEndDate": "2026-11-09" to "2026-10-30". This is the planner moving the order earlier.
Run python unit1b/plan_to_produce.py and then python unit1b/plan_to_produce.py --capacity. Note how many messages remain, whether the test bench is still overloaded, and the date purchase order 4500000010 is now needed.
Change the two dates back.
In the action note, add a heading Fix tested with three lines: what moving 1100019 solved, what it made worse, and who has to agree before the planner does it.
Run python unit1b/plan_to_produce.py --order 1100016 and add a heading Variance note with two lines a cost accountant could send to the plant manager: the amount and its two causes.
Add a heading ECC note with one line on what a group view of impeller supply needs from CP20. Preview the file and commit it with Git.
Done when:unit1b/calder_mrp_actions.md has the nine-line message table with Action and AI could filled in for every line, a Fix tested section that says the run drops to 8 messages, the test bench overload is gone, and purchase order 4500000010 is now needed on 2026-10-27 (so the motor shortage gets worse), a Variance note naming 4,100.00 USD of scrap and 180.00 USD of extra test time, and an ECC note. Keep it: the capstone at the end of Unit 1B traces one exception across the whole enterprise, and this note is its plan-to-produce part.
Pick one answer for each question. The explanation appears after you choose.
1Calder's ES-300 is planned with strategy 40. Brennock orders two ES-300 in November, when the PIR is six. How many pumps does MRP plan for November?
Answer: B. With planning with final assembly, sales orders consume the PIRs of their period. The script plans the two ordered pumps plus the four left of the PIR, six in all.
2A new planned order should have started on 22 October, and MRP runs on 26 October. What does SAP do?
Answer: A. When backward scheduling gives a start date in the past, the system switches to forward scheduling. The order arrives late, and message 06 tells the planner why.
3Purchase order 4500000002 gets "reschedule in" in the motor netting. What should happen next?
Answer: B. MRP flags the purchase order but doesn't change it. Only the supplier can deliver earlier, so the buyer chases it; the order is also unconfirmed, so its date is a hope.
4You need production orders with their status in custom code on an S/4HANA cloud edition. What do you read?
Answer: D. The Cloudification Repository lists AFKO as not to be released, with I_ManufacturingOrder as its successor, and both views as released. Released views survive upgrades; table access in custom code doesn't fit clean core.
5The test bench is at 125% in the week of 9 November, yet MRP raised no message about it. Why?
Answer: D. MRP and lead time scheduling don't check other orders' load. The overload shows only in a capacity evaluation, which in the script is the --capacity option.
6Order 1100016 made three good pumps out of four and leaves a 4,280.00 USD variance. What made it explainable?
Answer: C. The confirmation recorded one scrapped pump and the failed pressure test, so the variance splits into scrap and extra test time. Without that, finance sees one number with no cause.
7An agent ranks Calder's MRP messages well. The plant asks it to also move production orders when it is confident. What do you design?
Answer: A. Moving an order changes capacity, components and customer dates. The agent proposes with its evidence; the planner approves in SAP, and the decision is logged with its reason.
8Calder considers PP/DS for the test bench. Which statement belongs in the ADR's consequences?
Answer: C. The decision plans the bench's pumps in PP/DS and leaves components in MRP Live. That needs PP/DS settings on those materials and a finite resource, and planners learn a second tool.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
Orchestrate Lab is a free practice ERP in your browser, built around the fictional Calder Pumpworks. These are the places in it where you do what this topic teaches.
Stage 5 of 11PlanThe whole stage practices this topic, on Planned demand, MRP run and list and Planned orders.
Stage 7 of 11BuildThe whole stage practices this topic, on Planned orders, Production orders, Material plant data and 1 more.
Stage 4 of 11Create master dataPractice it on Material plant data, Bills of material, Routings and 1 more.
Stage 8 of 11Take ordersPractice it on Material plant data, Bills of material and MRP run and list.
ExercisePlanner's morningOne planning run, five exception messages, one of every kind: rank them, fix each where it is fixed, run planning again and explain what is left.
ChapterProduct lifecycleChange how a product is built without stopping the line: an engineering change record with a reason and a date, a redesigned ES-200 impeller with its plant data, price and supplier, and a bill of material with two dated lives, the old impeller ending and the new one starting on the change's date, so every order reads the bill valid on the day it starts; then the planner runs MRP across the date and reads the old impeller's run-out and the new one's first order.
ChapterQualityInspect what Calder buys before anyone uses it: an inspection setup and characteristics for seal kits, goods receipts that go to quality inspection and open inspection lots, results measured on a sample, and the usage decision that releases a good delivery to unrestricted stock or blocks a bad one, while planning counts only what can be used. Then the final inspection of a pump Calder builds: ES-300 tested at production receipt before anyone can sell it, and an order that confirms only the pumps that passed until a recheck after the decision. Then the complaint: a supplier's invoice blocked for quality, a quality notification, a return delivery and the credit memo that settles it. Then the supplier's replacement inspected and its invoice released by the decision, and the supplier's quality record read for what it can and can't say. Last, ES-300's final inspection fed by the test bench: one measurement per pump, recorded once against its serial number.
ChapterShop floorRun Rockford's floor from its own system: released production orders become dispatch lists per work center, in operation order with their scheduled dates, hours and the parts to issue; operators start and finish each operation by shift and confirm it with the good quantity, the scrap and its reason, and the hours; cheap parts are backflushed by the confirmation; the test bench's machine time is costed apart from the technician's; every pump gets a serial number and its own bench results; a pump that fails twice is repaired on its own rework order; every stop gets a reason, each work center's week reads as OEE, a breakdown goes to the maintenance planner, and in the end the floor runs its own system, fed by messages from the core, including cancellations the floor must agree to, and sending its confirmations back the same way, each of which can fail and must be fixed.
ChapterTransportationPlan Calder's freight from Rockford: carriers and their agreements, freight units from deliveries, freight orders, the carrier chosen by cost, loads with stops, truck capacity and a departure worked back from the dates, the final charges with stops and detention after free time, tendering each load to a carrier that has to accept it, following the truck by its execution events, and settling the freight: each carrier invoice checked against the agreed charges, the freight cost shared among the deliveries so each customer's margin shows what its freight really cost, and inbound freight on purchased motors planned on the purchase order and carried into the motors' cost.
ChapterWarehouseLay out Calder's Rockford distribution center: storage types, sections and bins, then put stock away, pick, pack and count it, put people on the work and watch it.
Stages build on each other: a stage link opens the core path at that stage. To start there without doing the earlier stages yourself, use Do this stage for me on the stage before it. A chapter starts inside your own company once the core path is done (or the lab finishes the core for you first); drills and exercises start from their own prepared copy. Everything is saved in this browser only.
Sources
Planning Using MRP Live (SAP Learning)— MD01N or Schedule MRP Runs; one database procedure; planning file; low-level code order; materials MRP Live can't handle go to classic MRP; no MRP lists, use Monitor Stock/Requirements List (MD04); forecast-based planning (VV) not supported
Creating Production Orders (SAP Learning)— individual, collective and partial conversion of planned orders; the planned order is deleted and replaced; dependent requirements become reservations; planned opening date
Releasing Production Orders (SAP Learning)— release is a status change that allows printing, goods issues and confirmations; component availability check and capacity evaluation before release; collective release in Manage Production Orders (F2336); download to an MES
Period-End Closing for Production Order in MTO Scenario (SAP Learning)— WIP without delivery completed or technically completed status; variances at delivery or technical completion; input-side categories input price, input quantity, resource usage, remaining; output-side lot size, mixed price, output price, remaining
Outlining SAP S/4HANA Capacity Planning (SAP Learning)— lead time scheduling and MRP assume infinite capacity; capacity evaluation and leveling; Manage Work Center Capacity, Capacity Scheduling Table and Board; dispatched operations
Cloudification Repository (SAP on GitHub)— objectReleaseInfoLatest.json read 24 October 2026; I_PlannedOrder, I_ManufacturingOrder, I_MfgOrderConfirmation, I_ReservationDocumentItem, I_ActivePlndIndepRqmtItem, I_WorkCenterCapacity released; AFKO, AFPO, AFRU, AFVC, RESB, MKAL, CRHD, MARC not to be released, with successors