Follow one late supplier confirmation from purchase order to cash, name every document, posting, interface and team, and package it as a solution design.
Every topic in Unit 1B looked at one process. This one follows one problem through all of them.
On Wednesday 28 October 2026, a supplier of Calder Pumpworks, the fictional pump maker this unit follows, confirms a motor order for 18 November. The plant had counted on 2 November. Nothing is broken in SAP. Every system does its job. And yet, two weeks later, a water utility gets its pumps eleven working days late and Calder's cash arrives fifteen days late.
The chain looks like this:
The purchase order gets a later confirmed date.
Planning (MRP) flags that the motors come too late for a production order.
The production order moves back two weeks.
The availability check can no longer promise the customer's date, so the sales order waits.
Delivery, billing and cash all move with it.
Each step belongs to a different team, and most of them learn about it last. The skill this topic teaches is seeing the whole chain from the first signal: naming each document, posting, interface and team, then deciding where software can help and where a person must decide.
Exceptions are where an enterprise loses money quietly. The late motors cost Calder nothing on the day they are confirmed. The cost appears later, in other departments, and nobody connects it back:
Who feels it
When
What they see
Buyer
28 October
A confirmation 12 working days late, one of many that day
Production planner
29 October
An exception message on a purchase order and a production order that can't start
Customer service
Days later, if anyone tells them
Brennock Water Utility's order for 16 pumps can't ship on 13 November
Controller
End of November
67,200.00 USD of revenue lands with one working day to spare
Treasury
December
Cash arrives 15 days later than forecast
The business case for tracing is simple. The earlier the chain is seen, the more options are open. On 28 October, Calder can still ask the supplier to expedite, split the customer's order or move pumps from orders that are blocked anyway. By 13 November, the only option left is an apology.
This is also the job of a forward deployed engineer or solution architect on an SAP program. A process expert knows one link. Someone has to know the chain.
As of October 2026, SAP S/4HANA has a standard tool at every link. None of them shows the whole chain on its own:
Supplier confirmations. A confirmation control key on the purchase order item says which confirmations the supplier must send and whether planning uses them. Monitor Supplier Confirmations shows what is missing (SAP Learning). Confirmations can arrive by EDI as order responses (ORDRSP). When a confirmed date falls outside tolerance, SAP's ERP 4.6C documentation says the system warns and doesn't update the document, so a person has to look; check how your release behaves (SAP Library).
Alerts to the right person.Situation Handling has purchasing templates for a pending supplier confirmation and for a quantity deficit in a supplier's delivery (SAP Learning).
Planning. MRP raises exception messages such as 10 "reschedule in" when a receipt comes after it is needed (SAP KBA 1698291).
Production. The material availability check at order release sets a material shortage status and fills a missing parts list (SAP Learning).
Customer promises. In advanced available-to-promise (aATP), backorder processing re-checks sales order confirmations when supply changes, for example when a production order is late. It can give confirmed quantities to some orders and take them from others. SAP notes that aATP may need its own license (SAP Learning).
For AI, SAP's Sapphire 2026 guide lists an Order Lifecycle Assistant that monitors fulfillment risk (general availability planned for Q3 2026), a Planning Assistant with exception management agents and a Business Network Assistant (both planned for Q4 2026), and an Accounts Receivable Assistant (planned for Q2 2026) (SAP). These are plans published at Sapphire 2026, and some of those quarters have already passed, so check each one's status, edition and license before a design depends on it. Joule agents and agent orchestration explains how such agents are built and combined.
flowchart LR
C[Supplier confirms<br/>18 Nov, not 2 Nov] --> M[MRP: message 10<br/>motors too late]
M --> P[Production order<br/>moves 16 days]
P --> A[Availability check:<br/>customer date fails]
A --> S[Sales order waits;<br/>customer told]
S --> D[Delivery and<br/>goods issue later]
D --> B[Invoice later]
B --> K[Cash later]
Step
Team
Document
Posts to finance?
Supplier confirms late
Buyer
Confirmation on the purchase order item
No
MRP sees the gap
Production planner, buyer
Exception messages
No
Production order moves
Production planner
Changed order dates; missing parts at release
No
Customer date fails
Customer service, sales manager
Sales order schedule lines with new confirmed dates
No
Customer is told
Customer service
Changed order confirmation
No
Motors arrive, pumps are built
Warehouse, production
Goods receipts, goods issues, confirmations
Yes: inventory and order costs
Pumps ship
Warehouse, transport
Delivery, goods issue, freight order
Yes: cost of goods sold
Invoice
Billing
Billing document
Yes: receivable and revenue
Cash
Receivables, treasury
Incoming payment
Yes: bank and receivable
Notice the pattern: the first five steps post nothing to finance. The exception travels through plans and promises. Finance sees it only when the goods move, weeks later.
"A late supplier is a purchasing problem." It becomes a production, sales and finance problem within days. Purchasing is only where it starts.
"SAP will tell the customer." SAP re-confirms the order if the availability check is re-run. Telling the customer, and choosing what to offer, is a person's job.
"If nothing posted, nothing happened." The costly part of this exception moves only plans and promises. Finance sees it weeks later.
"An agent should fix the chain." It can detect, explain, price options and draft messages. Changing supplier dates, production orders or customer allocations is a decision with owners.
Supplier confirmation: the supplier's answer to a purchase order, with the quantity and date it commits to.
Confirmation control key: the purchase order setting that says which confirmations are expected and which ones planning uses.
Exception message: MRP's flag on a situation it can't solve, such as a receipt that comes too late.
Pegging: the link from a receipt to the requirement it covers, used to find which customer a late receipt affects.
Availability check (ATP): the check that decides what quantity a sales order can be promised, and when.
Backorder processing: re-running the availability check for many orders after supply or demand changes.
Solution package: the set of documents an architect brings to a design review: summary, landscape, process trace, interfaces, risks and AI opportunities.
Pick one answer for each question. The explanation appears after you choose.
1A supplier confirms motors 12 working days late. Which team usually feels the cost first?
Answer: D. The confirmation posts nothing and changes no price. The first real effect is a production order that can't start, which then moves the customer's date. Finance sees it only when goods move.
2Why does the trace matter most on the day the confirmation arrives?
Answer: D. On 28 October Calder can still expedite, split the order or reallocate pumps. By the delivery date, only an apology is left. Early sight buys options.
3Which SAP capability re-checks customer promises after supply changes, for many orders at once?
Answer: A. Backorder processing re-runs the availability check when supply or demand changes, such as a late production order. The other three cover the supplier and production links, not customer promises.
4In the trace, which steps post to finance?
Answer: B. The confirmation, MRP, rescheduling and the new customer promise change plans, not values. Goods receipts, goods issues, the invoice and the payment post.
5A vendor demos an agent that moves pumps from one customer's order to another's on its own. What should you ask for?
Answer: D. Taking confirmed quantities from one customer to give another affects customers and credit decisions. The agent can price the option; a sales manager decides, and the decision is logged.
6What does the solution package give a design review that a process diagram alone doesn't?
Answer: A. The package names every interface, risk, AI opportunity and design decision for one real chain. That is what an architecture board needs to judge a design; a diagram shows only the flow.
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.
An exception is a change in one promise that invalidates the promises built on it.
A purchase order schedule line promises motors on a date. A production order promises pumps, counting on those motors. A sales order schedule line promises the customer a date, counting on those pumps. A billing plan and a cash forecast count on the delivery. Each promise sits in a different document, owned by a different team, often in a different system.
When the supplier's promise changes, SAP updates the first document and flags the second. The rest stay as they are until someone, or some job, re-checks them. Tracing is following the "counting on" links forward, from receipt to requirement, until you reach a customer and money. In SAP those links have names: pegging from receipts to requirements in planning, reservations from production orders to components, and confirmations from the availability check to sales order schedule lines.
The rest of this topic follows that chain for one Calder purchase order, then packages it as a design.
On Monday 26 October, MRP at Calder's Rockford plant CP11 flagged purchase order 4500000002 (20 motors CP-MTR15 from Orrin Motor Works, 905.00 USD each) as too late. The buyer asked Orrin to deliver on 2 November. The planner kept production order 1100019 (20 ES-200 pumps) on 3 to 9 November, counting on those motors. Sales confirmed a new order, 1000011, for Brennock Water Utility: 16 ES-200 pumps at 4,200.00 USD, requested for 13 November, complete delivery only.
On Wednesday 28 October, Orrin's order confirmation arrives through SAP Business Network: 20 motors on 18 November.
The confirmed receipt moves; requirements before it are uncovered; message 10 appears
Order 1100019 needs 20 motors on 3 November; one is free
Production order
Planner changes the basic dates; the availability check at release reports missing parts
1100019: 3 to 9 November becomes 19 to 25 November
Sales order schedule lines
The availability check re-confirms: the confirmed date and quantity change
1000011: confirmed 13 November becomes 30 November
Outbound delivery, goods issue
Created later, from the new date
Goods issue 27 November instead of 12 November
Billing document and receivable
Billing date and net due date move
Invoice 27 November, due 27 December
Three SAP details shape how far the delay travels:
Confirmation categories and planning. The confirmation control key defines confirmation categories, each with flags for whether it is relevant to MRP and to goods receipt, and a monitoring period (SAP Learning). If Orrin's confirmation category isn't MRP-relevant, planning keeps using 2 November and the chain stays hidden until the motors fail to arrive.
Tolerance on EDI confirmations. For confirmations by EDI, SAP's ERP documentation says the system checks quantities, prices and dates against tolerances set in "Set up Confirmation Control". When acknowledged values differ, it issues a warning and the purchasing document isn't updated; this is the ERP 4.6C library, and the topic hasn't confirmed the same for current S/4HANA or SAP Business Network (SAP Library). A 12-day slip can sit in an inbound message, unprocessed, until someone reads it. Check how your release and channel behave.
Re-checking customer promises. A sales order keeps its confirmation until the availability check runs again. SAP's help says backorder processing exists for exactly this case: supply or demand changes, such as a late production order, and without a reaction confirmed quantities can exceed what is available (SAP Help Portal).
Motors are valued at a standard price of 920.00 in the pack; the PO price is 905.00
Production of 20 pumps
Components and activities to the order; finished goods at standard
57,000.00 finished goods
As in plan to produce
Goods issue of 16 pumps
Cost of goods sold, finished goods inventory down
45,600.00
The pumps leave Calder
Invoice
Receivable and revenue
67,200.00
Revenue recognized at billing in this course
Payment
Bank and receivable cleared
67,200.00
NT30, paid on the due date (course assumption)
The amounts don't change because of the delay. The dates do. That is why the exception is invisible in finance until the month closes: revenue moves from 12 to 27 November and cash from 12 to 27 December. If the goods issue slipped two more working days, the revenue would land in December.
These are the interfaces the trace crosses in Calder's landscape. The script lists them with an owner, and the solution package keeps them as interfaces.csv.
ID
From → to
Carries
What breaks if it fails
I01
SAP Business Network → S/4HANA CP10
Purchase orders, order confirmations, ship notices
The late date never reaches planning
I02
S/4HANA → MES (Rockford) and back
Released orders out, confirmations back
The shop floor builds to the old plan; actual costs arrive late
As of 29 October 2026, these released interfaces carry each link of the trace. The CDS views come from SAP's Cloudification Repository (file objectReleaseInfoLatest.json read 29 October 2026), which lists each one as released (Cloudification Repository). The OData API names and fields come from SAP's generated API models, version 2.1.0 (SAP Cloud SDK model; the production order model is in @sap/cloud-sdk-op-vdm-production-order-v2-service).
BillingDocumentDate, CustomerPaymentTerms, the receivable's due date
For detection, an agent shouldn't poll every list. S/4HANA publishes business events that apps receive through SAP Event Mesh or SAP Cloud Application Event Hub; event names are listed on the SAP Business Accelerator Hub, for example sap.s4.beh.businesspartner.v1.BusinessPartner.Changed.v1 (CAP documentation). Look up the purchase order, production order and sales order events for your release there, and see Event-driven agents with SAP events for the pattern.
You will write one script that follows the late confirmation through Calder: planning, production, the customer's promise, delivery, billing and cash. It prices four ways to recover, maps where AI helps, lists the interfaces and writes a solution package you can show in a design review. An optional step reads sales order schedule lines from SAP's sandbox.
flowchart LR
P[calder_pack/<br/>names, standard prices] --> S[trace_exception.py]
SC[Scenario in the script<br/>PO, orders, stock,<br/>course rules] --> S
S --> T[Impact chain]
S --> O[Recovery options]
S --> A[AI map, interfaces]
S --> K[calder_solution_package/]
X[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 reads supplier and customer names and the standard prices of the ES-200 pump (2,850.00 USD) and the motor (920.00 USD) from the pack, and stops if they differ.
In VS Code, create a new file, paste the script below, and save it as trace_exception.py inside the unit1b folder.
Read the blocks under "course rules" and "the scenario" first. They are Calder's situation written as Python data. Every value is invented. Order 1100019, purchase orders 4500000002 and 4500000010, deliveries 80000013 and 80000014, the blocked orders 1000002 and 1000007, the test bench outage and the Larkspur freight rates come from earlier Unit 1B topics. Order 1000011, the confirmation and the 8% cost of cash are new and are course values.
"""Trace one late supplier confirmation across Calder, from purchase order to cash.
Calder Pumpworks is a fictional company. The pack (unit1b/calder_pack/) and the
scenario below are made up for the course. Field names follow SAP's released APIs
where the script mirrors one (A_PurchaseOrderScheduleLine, A_ProductionOrder_2,
A_SalesOrderScheduleLine, A_BillingDocument), but the scheduling, availability and
cash logic is a teaching sketch, not SAP's algorithm.
Run it from your course folder:
python unit1b/trace_exception.py the impact chain, step by step
python unit1b/trace_exception.py --options compare four ways to recover
python unit1b/trace_exception.py --ai where AI detects, explains or acts, and who decides
python unit1b/trace_exception.py --interfaces the interfaces the trace crosses
python unit1b/trace_exception.py --confirmed 2026-11-09 what if the supplier had confirmed another date
python unit1b/trace_exception.py --write write the solution package to unit1b/calder_solution_package/
python unit1b/trace_exception.py --sandbox read sales order schedule lines in SAP's sandbox (needs SAP_API_KEY)
Everything except --sandbox uses only Python's built-in modules and needs no account and no network.
"""
import argparse
import csv
import json
import os
import sys
import urllib.error
import urllib.request
from datetime import date, timedelta
from pathlib import Path
HERE = Path(__file__).resolve().parent
PACK = HERE / "calder_pack"
OUT = HERE / "calder_solution_package"
SANDBOX = "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/"
AS_OF = date(2026, 10, 28) # Wednesday: Orrin's order confirmation arrives
# ------------------------------------------------------------ course rules (fictional)
GR_DAYS = 1 # working days from goods receipt to available (motors and pumps)
ASSEMBLY_DAYS = 5 # working days order 1100019 needs from start to finish
PICK_DAYS = 1 # material available -> goods issue
TRANSIT_DAYS = 1 # goods issue -> at the customer
PAYMENT_DAYS = 30 # NT30: net due 30 days after the invoice
COST_OF_CASH = 0.08 # yearly rate used to price a cash delay (course value)
TEST_OUTAGE = (date(2026, 11, 9), date(2026, 11, 11)) # maintenance order 4000123 on TEST1
# ------------------------------------------------------------ the scenario (fictional)
# On Monday 26 October, MRP flagged purchase order 4500000002 "reschedule in". The buyer
# asked Orrin to deliver on 2 November, and the planner kept order 1100019 on 3-9 November,
# counting on those 20 motors. This slice pegs the PO to order 1100019; Calder's other
# motor demand is covered by PO 4500000010 and stays out of the slice.
PO = {"PurchaseOrder": "4500000002", "PurchaseOrderItem": "10", "Material": "CP-MTR15",
"Supplier": "20000001", "ScheduleLineOrderQuantity": 20, "NetPriceAmount": 905.00,
"ScheduleLineDeliveryDate": "2026-11-02", "Channel": "SAP Business Network"}
CONFIRMATION = {"ConfirmedDate": "2026-11-18", "ConfirmedQuantity": 20, "Received": "2026-10-28"}
MOTOR_STOCK_FREE = 1 # 5 in stock, 4 reserved for order 1100021 today
PROD_ORDER = {"ManufacturingOrder": "1100019", "Material": "CP-ES200", "TotalQuantity": 20,
"MfgOrderPlannedStartDate": "2026-11-03", "Status": "created", "MotorsPerPump": 1}
# ES-200 supply and demand at CP11 for the availability check (ATP), as of 28 October.
ES200_ELEMENTS = [ # date, quantity (+ supply, - demand), element
("2026-10-28", +12, "stock"),
("2026-10-28", -2, "delivery 80000013 (order 1000009)"),
("2026-10-28", -4, "delivery 80000014 (order 1000010)"),
("2026-10-30", +10, "production order 1100018"),
("2026-10-30", -6, "sales order 1000002 Hallam (credit block)"),
("2026-11-09", -2, "sales order 1000007 Brennock (delivery block HC)"),
]
ORDER = {"SalesOrder": "1000011", "SoldToParty": "10000001", "Material": "CP-ES200",
"Quantity": 16, "NetPricePerUnit": 4200.00, "RequestedDeliveryDate": "2026-11-13",
"CompleteDeliveryIsDefined": True, "CustomerPaymentTerms": "NT30"}
# Freight for an extra shipment, from the warehouse topic's Larkspur agreement FA-LFL-2026.
FREIGHT = {"per_shipment": 400.00, "per_kg": 0.35, "fuel": 0.18, "pump_kg": 180, "pallet_kg": 25,
"pumps_per_pallet": 2}
EXPEDITE = {"date": "2026-11-09", "price": 950.00} # what the buyer would ask Orrin for
BLOCKED = {"1000002": ("Hallam Fluid Systems", 6, 24288.00, "credit block"),
"1000007": ("Brennock Water Utility", 2, 8400.00, "delivery block HC, customer hold")}
def d(text: str) -> date:
return date.fromisoformat(text)
def add_wd(day: date, n: int) -> date:
"""Move n working days (Monday-Friday) forward, or back if n is negative."""
step = 1 if n >= 0 else -1
while n:
day += timedelta(days=step)
if day.weekday() < 5:
n -= step
return day
def wd_between(a: date, b: date) -> int:
"""Working days from a to b (positive if b is later)."""
n, day = 0, a
while day < b:
day += timedelta(days=1)
n += day.weekday() < 5
return n
def load_pack() -> dict:
"""Read names and standard prices from the Unit 1B sample pack."""
try:
products = json.loads((PACK / "products.json").read_text(encoding="utf-8"))["Products"]
partners = json.loads((PACK / "business_partners.json").read_text(encoding="utf-8"))["BusinessPartners"]
except FileNotFoundError:
sys.exit("The sample pack is missing. Run: python unit1b/make_calder_pack.py")
std = {p["Product"]: p["to_Valuation"][0]["StandardPrice"] for p in products}
names = {bp["BusinessPartner"]: bp["BusinessPartnerFullName"] for bp in partners}
if std.get("CP-ES200") != 2850.00 or std.get("CP-MTR15") != 920.00:
sys.exit("products.json has other standard prices than this topic expects. Rewrite the pack.")
return {"std": std, "names": names}
# ------------------------------------------------------------ the chain
def production(motors_available: date) -> dict:
"""Schedule order 1100019 from the date its motors are available."""
start = max(d(PROD_ORDER["MfgOrderPlannedStartDate"]), motors_available)
finish = add_wd(start, ASSEMBLY_DAYS - 1)
lo, hi = TEST_OUTAGE
return {"start": start, "finish": finish, "available": add_wd(finish, GR_DAYS),
"test_clash": lo <= finish <= hi or start <= hi and finish >= lo}
def atp_date(elements: list, need: int) -> date:
"""Earliest date from which `need` pumps stay free on every later date."""
days, running = {}, 0
for when, qty, _ in sorted(elements):
running += qty
days[d(when)] = running
dates = sorted(days)
for i, day in enumerate(dates):
if min(days[x] for x in dates[i:]) >= need:
return day
return None
def schedule_order(mad: date) -> dict:
"""Confirm the sales order from its material availability date (MAD)."""
requested = d(ORDER["RequestedDeliveryDate"])
if add_wd(mad, PICK_DAYS + TRANSIT_DAYS) <= requested:
delivery, gi = requested, add_wd(requested, -TRANSIT_DAYS)
else:
gi = add_wd(mad, PICK_DAYS)
delivery = add_wd(gi, TRANSIT_DAYS)
return {"mad": mad, "gi": gi, "delivery": delivery, "invoice": gi,
"due": gi + timedelta(days=PAYMENT_DAYS)}
def es200_with(receipt: date) -> list:
return ES200_ELEMENTS + [(receipt.isoformat(), +PROD_ORDER["TotalQuantity"], "production order 1100019")]
def run(confirmed: date) -> dict:
"""The whole chain for one confirmed motor date, before and after."""
before_prod = production(add_wd(d(PO["ScheduleLineDeliveryDate"]), GR_DAYS))
before = schedule_order(atp_date(es200_with(before_prod["available"]), ORDER["Quantity"]))
after_prod = production(add_wd(confirmed, GR_DAYS))
after = schedule_order(atp_date(es200_with(after_prod["available"]), ORDER["Quantity"]))
value = ORDER["Quantity"] * ORDER["NetPricePerUnit"]
days_late = (after["due"] - before["due"]).days
return {"before_prod": before_prod, "after_prod": after_prod, "before": before, "after": after,
"value": value, "cash_days": days_late,
"cash_cost": round(value * COST_OF_CASH * days_late / 365, 2),
"late_wd": wd_between(before["delivery"], after["delivery"]),
"po_late_wd": wd_between(d(PO["ScheduleLineDeliveryDate"]), confirmed)}
def revenue_note(r: dict) -> str:
"""Does the invoice still land in the month the plan expected?"""
planned = r["before"]["invoice"]
last = date(planned.year + (planned.month == 12), planned.month % 12 + 1, 1) - timedelta(days=1)
while last.weekday() >= 5:
last -= timedelta(days=1)
month = planned.strftime("%B")
if r["after"]["invoice"] > last:
return f"revenue moves out of {month}"
return f"{month} revenue holds by {wd_between(r['after']['invoice'], last)} working day(s)"
def steps(r: dict, pack: dict) -> list:
"""The trace: one row per step, with documents, postings, interfaces and teams."""
orrin, brennock = pack["names"]["20000001"], pack["names"]["10000001"]
std_pump, std_motor = pack["std"]["CP-ES200"], pack["std"]["CP-MTR15"]
bp, ap, b, a = r["before_prod"], r["after_prod"], r["before"], r["after"]
qty, po_qty = ORDER["Quantity"], PO["ScheduleLineOrderQuantity"]
return [
{"n": 1, "step": "Supplier confirms late", "team": "Buyer",
"what": f"{orrin} confirms PO {PO['PurchaseOrder']} item {PO['PurchaseOrderItem']}: "
f"{po_qty} x {PO['Material']} on {CONFIRMATION['ConfirmedDate']}, asked for "
f"{PO['ScheduleLineDeliveryDate']} ({r['po_late_wd']} working days late)",
"document": "Order confirmation on the PO item (supplier confirmation)",
"posting": "None", "interface": f"I01 {PO['Channel']} -> S/4HANA CP10"},
{"n": 2, "step": "MRP sees the gap", "team": "Production planner, buyer",
"what": f"Night MRP run: message 10 (reschedule in) on PO {PO['PurchaseOrder']}; order "
f"{PROD_ORDER['ManufacturingOrder']} needs {po_qty} motors on {bp['start']}, "
f"{MOTOR_STOCK_FREE} free in stock",
"document": "Exception messages in the stock/requirements list",
"posting": "None", "interface": "Inside S/4HANA"},
{"n": 3, "step": "Production moves", "team": "Production planner, supervisor",
"what": f"Order {PROD_ORDER['ManufacturingOrder']} ({PROD_ORDER['TotalQuantity']} x "
f"{PROD_ORDER['Material']}): was {bp['start']} to {bp['finish']}, now "
f"{ap['start']} to {ap['finish']}; pumps available {ap['available']} instead of {bp['available']}"
+ ("; the test bench clash in the week of 9 November disappears" if bp["test_clash"] and not ap["test_clash"] else "")
+ ("; the build overlaps the test bench outage, 9-11 November" if ap["test_clash"] else ""),
"document": "Production order dates changed; release waits for motors (missing parts)",
"posting": "None until goods issue and confirmations", "interface": "I02 S/4HANA -> MES (at release)"},
{"n": 4, "step": "Availability check fails the date", "team": "Customer service, sales manager",
"what": f"Sales order {ORDER['SalesOrder']} ({brennock}, {qty} x {ORDER['Material']}, complete delivery) "
f"was confirmed for {b['delivery']}; re-checked, it confirms {a['delivery']}",
"document": "Schedule lines: confirmed date and quantity change",
"posting": "None", "interface": "I03 S/4HANA -> CRM; I04 output to customer"},
{"n": 5, "step": "Customer is told", "team": "Customer service",
"what": f"New date {a['delivery']} goes to {brennock}; {r['late_wd']} working days late",
"document": "Changed order confirmation (output)", "posting": "None",
"interface": "I04 S/4HANA output -> customer (email PDF)"},
{"n": 6, "step": "Motors arrive", "team": "Warehouse, buyer",
"what": f"Goods receipt of {po_qty} motors on {CONFIRMATION['ConfirmedDate']}",
"document": "Material document (goods receipt)",
"posting": f"Inventory {po_qty * std_motor:,.2f} at standard; GR/IR {po_qty * PO['NetPriceAmount']:,.2f}; "
f"price difference {po_qty * (PO['NetPriceAmount'] - std_motor):,.2f}",
"interface": "I01 ship notice; inside S/4HANA"},
{"n": 7, "step": "Pumps are built", "team": "Production, cost accounting",
"what": f"Components issued, operations confirmed, {PROD_ORDER['TotalQuantity']} pumps received {ap['finish']}",
"document": "Goods issues, confirmations, goods receipt",
"posting": f"Finished goods {PROD_ORDER['TotalQuantity'] * std_pump:,.2f} at standard; order credited",
"interface": "I02 MES -> S/4HANA (confirmations)"},
{"n": 8, "step": "Delivery and goods issue", "team": "Warehouse, transport planner",
"what": f"Delivery and goods issue {a['gi']} instead of {b['gi']}",
"document": "Outbound delivery, warehouse tasks, freight order, goods issue",
"posting": f"Cost of goods sold {qty * std_pump:,.2f}; finished goods inventory -{qty * std_pump:,.2f}",
"interface": "I05 S/4HANA -> EWM (RKDC); I06 TM -> carrier"},
{"n": 9, "step": "Billing", "team": "Billing, accounts receivable",
"what": f"Invoice {a['invoice']} instead of {b['invoice']}; {revenue_note(r)}",
"document": "Billing document; journal entry",
"posting": f"Receivable {r['value']:,.2f}; revenue -{r['value']:,.2f}",
"interface": "I07 S/4HANA -> data warehouse (revenue forecast)"},
{"n": 10, "step": "Cash", "team": "Accounts receivable, treasury",
"what": f"Due {a['due']} instead of {b['due']}: {r['value']:,.2f} USD arrives {r['cash_days']} days later",
"document": "Incoming payment clears the receivable", "posting": f"Bank {r['value']:,.2f}; receivable -{r['value']:,.2f}",
"interface": "I08 bank statement -> S/4HANA"},
]
def show_trace(r: dict, pack: dict) -> None:
print(f"One exception, traced across Calder (fictional), as of {AS_OF}\n")
for s in steps(r, pack):
print(f"{s['n']:>2}. {s['step']} [{s['team']}]")
print(f" {s['what']}")
print(f" Document: {s['document']}")
print(f" Posting: {s['posting']}")
print(f" Crosses: {s['interface']}\n")
print(f"Impact: {ORDER['Quantity']} pumps for order {ORDER['SalesOrder']} arrive {r['late_wd']} working days late; "
f"{r['value']:,.2f} USD of cash {r['cash_days']} days later "
f"(cost of the delay at {COST_OF_CASH:.0%} a year: {r['cash_cost']:,.2f} USD).")
print("Ripple stops at CP10: impeller and seal kit stock cover order 1100019, so CP20 sees no change.")
print("Synthetic data: the trace shows the method, not results from an SAP system.")
# ------------------------------------------------------------ recovery options
def options(r: dict, pack: dict) -> list:
"""Four ways to recover, each priced. The script compares; a person decides."""
value, b, a = r["value"], r["before"], r["after"]
rows = [{"option": "A Accept the new date", "delivery": a["delivery"].isoformat(),
"cash_late": r["cash_days"], "extra": 0.0, "cash_cost": r["cash_cost"],
"decides": "Sales manager", "risk": f"Brennock waits; {revenue_note(r)}"}]
# B: ask Orrin to expedite at a higher price
exp = run(d(EXPEDITE["date"]))
extra = round(PO["ScheduleLineOrderQuantity"] * (EXPEDITE["price"] - PO["NetPriceAmount"]), 2)
rows.append({"option": f"B Expedite to {EXPEDITE['date']} at {EXPEDITE['price']:.2f}",
"delivery": exp["after"]["delivery"].isoformat(), "cash_late": exp["cash_days"],
"extra": extra, "cash_cost": exp["cash_cost"], "decides": "Buyer, production planner",
"risk": "Orrin may refuse" + ("; build overlaps the test bench outage" if exp["after_prod"]["test_clash"] else "")})
# C: split the order: what is free now ships on time, the rest later
free_now = free_on_time(es200_with(r["after_prod"]["available"]))
late_part = ORDER["Quantity"] - free_now
pallets = -(-late_part // FREIGHT["pumps_per_pallet"])
kg = late_part * FREIGHT["pump_kg"] + pallets * FREIGHT["pallet_kg"]
freight = round((FREIGHT["per_shipment"] + FREIGHT["per_kg"] * kg) * (1 + FREIGHT["fuel"]), 2)
late_value = late_part * ORDER["NetPricePerUnit"]
split_cost = round(late_value * COST_OF_CASH * r["cash_days"] / 365, 2)
rows.append({"option": f"C Split: {free_now} on time, {late_part} later", "delivery": f"{b['delivery']} / {a['delivery']}",
"cash_late": r["cash_days"], "extra": freight, "cash_cost": split_cost,
"decides": "Brennock (complete delivery), customer service",
"risk": f"Second shipment {kg} kg; Brennock must accept a partial delivery"})
# D: take confirmations from the two blocked orders (what backorder processing can do)
held = sum(q for _, q, _, _ in BLOCKED.values())
free_with_blocked = free_now + held
rows.append({"option": f"D Reallocate {held} pumps from blocked orders", "delivery": b["delivery"].isoformat() if free_with_blocked >= ORDER["Quantity"] else a["delivery"].isoformat(),
"cash_late": 0 if free_with_blocked >= ORDER["Quantity"] else r["cash_days"], "extra": 0.0, "cash_cost": 0.0,
"decides": "Sales manager, credit analyst",
"risk": f"Orders {', '.join(BLOCKED)} ({sum(v for _, _, v, _ in BLOCKED.values()):,.2f} USD) move to {a['delivery']} if released earlier"})
return rows
def free_on_time(elements: list) -> int:
"""Pumps that can ship on the requested date without touching later commitments."""
latest_mad = add_wd(d(ORDER["RequestedDeliveryDate"]), -(PICK_DAYS + TRANSIT_DAYS))
days, running = {}, 0
for when, qty, _ in sorted(elements):
running += qty
days[d(when)] = running # end-of-day position
at_mad = [q for day, q in sorted(days.items()) if day <= latest_mad][-1]
return max(0, min([at_mad] + [q for day, q in days.items() if day > latest_mad]))
def show_options(r: dict, pack: dict) -> None:
print(f"Ways to recover order {ORDER['SalesOrder']} (fictional; requested {ORDER['RequestedDeliveryDate']})\n")
print(f" {'Option':<42} {'Delivery':<23} {'Cash late':>9} {'Extra USD':>10} {'Delay USD':>10} Decides")
for o in options(r, pack):
print(f" {o['option']:<42} {o['delivery']:<23} {str(o['cash_late']) + ' d':>9} "
f"{o['extra']:>10,.2f} {o['cash_cost']:>10,.2f} {o['decides']}")
print(f" {'':<42} risk: {o['risk']}")
print("\n The script prices the options. A person chooses: none of them is the agent's call.")
print(" Synthetic data: the numbers show the method, not results from an SAP system.")
# ------------------------------------------------------------ AI map and interfaces
AI_MAP = [ # step, pattern, what the AI does, who decides, what SAP standard already gives
(1, "Detect", "Rule: confirmed date later than requested or than the pegged need",
"Nobody: plain automation is enough", "Monitor Supplier Confirmations; situation templates"),
(2, "Explain", "Follow pegging PO -> production order -> sales order; say it in plain words",
"Buyer and planner read it", "MRP exception messages, Manage Material Coverage"),
(3, "Act (propose)", "Propose new order dates and flag capacity clashes",
"Production planner reschedules", "Capacity evaluation, missing parts list"),
(4, "Act (propose)", "Price the recovery options A-D",
"Sales manager, credit analyst", "Backorder processing, Release for Delivery"),
(5, "Act (draft)", "Draft the message to Brennock and the expedite request to Orrin",
"Customer service and buyer send them", "Output management"),
(6, "None", "Plain automation: the receipt posts from the ship notice",
"Warehouse", "Inbound delivery, goods receipt"),
(7, "None", "Plain automation", "Production supervisor", "Confirmations from MES"),
(8, "None", "Plain automation: dates flow to EWM and TM", "Warehouse, transport planner", "EWM, TM"),
(9, "Explain", "Flag the revenue now one working day from month end",
"Controller", "Billing due list"),
(10, "Predict", "Predict the payment date from Brennock's payment history",
"Treasury uses it in the cash forecast", "Receivables and cash management apps"),
]
INTERFACES = [ # id, from, to, carries, how (course design), owner
("I01", "SAP Business Network", "S/4HANA CP10", "PO, order confirmation, ship notice", "Network documents", "Procurement IT"),
("I02", "S/4HANA CP10", "MES (Rockford)", "Released orders out; confirmations back", "Released-order download (Unit 5B)", "Manufacturing IT"),
("I03", "S/4HANA CP10", "CRM", "Sales order status and confirmed dates", "Released API or events (Unit 5B)", "Sales IT"),
("I04", "S/4HANA CP10", "Customer", "Order confirmation (changed)", "Output management, email PDF", "Customer service"),
("I05", "S/4HANA CP10", "EWM RKDC (decentralized)", "Outbound deliveries; goods issue back", "Delivery replication (queues)", "Logistics IT"),
("I06", "TM in CP10", "Carriers", "Freight order, tender, invoice", "Carrier EDI or portal", "Transport planner"),
("I07", "S/4HANA CP10", "Data warehouse / Datasphere", "Open orders, billing for the revenue forecast", "CDS extraction", "Analytics team"),
("I08", "Bank", "S/4HANA CP10", "Bank statement with the payment", "Bank file", "Treasury"),
("I09", "S/4HANA CP10", "Agent on BTP (to-be)", "Business events plus read-only APIs", "Event broker, OData (Unit 5B)", "Integration lead"),
]
def show_ai() -> None:
print("Where AI fits, and who decides (fictional Calder trace)\n")
print(f" {'#':>2} {'Pattern':<14} {'What the AI does':<74} Who decides")
for n, pattern, does, decides, _ in AI_MAP:
print(f" {n:>2} {pattern:<14} {does:<74} {decides}")
print("\n The agent reads, explains, prices and drafts. It never changes a PO, an order or a confirmation.")
def show_interfaces() -> None:
print("Interfaces the trace crosses (Calder landscape, fictional)\n")
for i in INTERFACES:
print(f" {i[0]} {i[1]} -> {i[2]}: {i[3]} [{i[4]}; owner: {i[5]}]")
# ------------------------------------------------------------ the solution package
DESIGN_LINKS = [ # area, topic, what the design applies to this trace
("Landscape and deployment", "landscape-and-deployment-options",
"The trace runs in CP10's S/4HANA private edition system. CP20 stays on ECC until its rollout into CP10 "
"(option A, July 2027); today the ripple stops at CP10 because impeller stock covers the order. "
"The agent runs side by side on BTP, outside the ERP core."),
("Fit-to-standard and extension choices", "fit-to-standard-and-extension-choices",
"Detection and recovery use standard first: confirmation control, MRP exception messages, backorder processing. "
"The agent is a side-by-side extension that reads released APIs. Nothing writes supplier dates into PO items "
"outside the standard apps (the R07 decision)."),
("Roles and authorizations", "roles-and-authorization-design",
"The agent gets its own technical user with display rights only (course label AGT_SUPPLY_WATCH). Buyers, planners "
"and sales managers keep the change rights; no role lets the agent change a PO, a production order or a sales order."),
("Data migration", "data-migration-strategy",
"When CP20 joins CP10's system, its open POs and sales orders arrive through the migration cockpit with new numbers "
"from the key map. Supplier history for the agent must follow the key map, or merged suppliers look new."),
("Reporting and analytics", "reporting-and-analytics-architecture",
"The worklist and options read live data (embedded analytics). Supplier on-time confirmation over 36 months and "
"the cash effect of late supply belong in the enterprise data layer (Datasphere in the course decision)."),
("Non-functional requirements", "non-functional-design-for-sap",
"Detect within 15 minutes of the confirmation (course target). Keep agent calls inside the course limit of 60 per "
"minute with headroom. Log every proposal and decision for audit. Keep agent reads out of the month-end batch window."),
]
RISKS = [
("Agent proposes from stale data", "Read at decision time; show the as-of time on every proposal", "Integration lead"),
("Agent changes SAP data", "Display-only technical user; changes happen in SAP apps by the owner", "Security lead"),
("Customer told a wrong date", "Customer service reviews and sends every draft; dates come from the schedule line", "Customer service lead"),
("Reallocation hurts another customer", "Sales manager and credit analyst approve option D; log the reason", "Sales manager"),
("Confirmation never arrives (silent risk)", "Pending-confirmation monitoring, not only late confirmations", "Buyer"),
("ECC plant out of view", "Until July 2027, CP20 changes reach CP10 only through intercompany EDI; add them to the trace", "Solution architect"),
]
def package_files(r: dict, pack: dict) -> dict:
"""The solution package as {file name: text}."""
rows = steps(r, pack)
fence = "`" * 3
readme = ["# Calder: one late supplier confirmation, traced (solution package)", "",
"Fictional company and data from the Orchestrate course. Synthetic numbers show the method, not results from an SAP system.", "",
"## One-page solution design summary", "",
f"**Problem.** On {AS_OF}, {pack['names']['20000001']} confirmed PO {PO['PurchaseOrder']} for "
f"{CONFIRMATION['ConfirmedDate']}, {r['po_late_wd']} working days after the date the plant counted on. "
f"Order {ORDER['SalesOrder']} for {pack['names']['10000001']} now ships {r['late_wd']} working days late "
f"and {r['value']:,.2f} USD of cash arrives {r['cash_days']} days later. Today nobody sees the chain until the customer calls.", "",
"**Goal and KPI.** See the impact within 15 minutes of the confirmation and put priced options in front of "
"the people who decide. KPI: share of late confirmations whose customer impact is known the same day "
"(baseline to measure; target set with the business).", "",
"**Scope.** CP10 purchasing, planning, sales, delivery, billing and receivables at plant CP11. Out of scope: "
"changes to SAP data by the agent; CP20 until its rollout.", "",
"| Design area | Applied to this trace | Topic |", "| --- | --- | --- |"]
readme += [f"| {area} | {text} | https://erpfde.com/topics/{slug}/ |" for area, slug, text in DESIGN_LINKS]
readme += ["", "## Contents", "", "- [Landscape](landscape.md)", "- [Process trace](process_trace.md)",
"- [Interfaces](interfaces.csv)", "- [Risks](risks.md)", "- [AI opportunities](ai_opportunities.md)", ""]
landscape = ["# Landscape (fictional Calder)", "", fence + "mermaid", "flowchart LR",
" BN[SAP Business Network] -->|I01 confirmation| S4[S/4HANA CP10<br/>purchasing, MRP, sales, billing]",
" S4 -->|I02 orders| MES[MES Rockford]", " S4 -->|I03 status| CRM[CRM]",
" S4 -->|I04 confirmation| CU[Brennock]", " S4 -->|I05 deliveries| EWM[EWM RKDC]",
" S4 -->|I06 freight| CA[Carriers]", " S4 -->|I07 extraction| DW[Datasphere / warehouse]",
" BK[Bank] -->|I08 statement| S4", " S4 -.->|I09 events, read APIs| AG[Agent on BTP<br/>read-only]",
" AG -.->|worklist, drafts| PPL[Buyer, planner,<br/>customer service]",
" ECC[ECC CP20<br/>until July 2027] -.->|intercompany EDI| S4", fence, ""]
trace = ["# Process trace", "", f"As of {AS_OF}. Synthetic data.", "",
"| # | Step | Team | What happens | Document | Posting | Interface |", "| --- | --- | --- | --- | --- | --- | --- |"]
trace += [f"| {s['n']} | {s['step']} | {s['team']} | {s['what']} | {s['document']} | {s['posting']} | {s['interface']} |" for s in rows]
trace += ["", "## Recovery options", "", "| Option | Delivery | Cash late (days) | Extra USD | Delay USD | Decides | Risk |",
"| --- | --- | --- | --- | --- | --- | --- |"]
trace += [f"| {o['option']} | {o['delivery']} | {o['cash_late']} | {o['extra']:,.2f} | {o['cash_cost']:,.2f} | {o['decides']} | {o['risk']} |"
for o in options(r, pack)]
risks = ["# Risks and controls", "", "| Risk | Control | Owner |", "| --- | --- | --- |"]
risks += [f"| {a} | {b} | {c} |" for a, b, c in RISKS]
ai = ["# AI opportunities", "", "| Step | Pattern | What the AI does | Who decides | SAP standard to check first |",
"| --- | --- | --- | --- | --- |"]
ai += [f"| {n} | {p} | {w} | {who} | {std} |" for n, p, w, who, std in AI_MAP]
return {"README.md": "\n".join(readme), "landscape.md": "\n".join(landscape),
"process_trace.md": "\n".join(trace) + "\n", "risks.md": "\n".join(risks) + "\n",
"ai_opportunities.md": "\n".join(ai) + "\n"}
def write_package(r: dict, pack: dict) -> None:
OUT.mkdir(exist_ok=True)
for name, text in package_files(r, pack).items():
(OUT / name).write_text(text, encoding="utf-8")
print(f" wrote calder_solution_package/{name}")
with open(OUT / "interfaces.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["id", "from", "to", "carries", "how", "owner"])
writer.writerows(INTERFACES)
print(" wrote calder_solution_package/interfaces.csv")
print("\nOpen calder_solution_package/README.md first. All data is fictional.")
# ------------------------------------------------------------ optional: SAP's sandbox
def read_env_key(name: str) -> str:
if os.environ.get(name):
return os.environ[name]
env = Path(".env")
if env.exists():
for line in env.read_text(encoding="utf-8").splitlines():
key, sep, value = line.partition("=")
if sep and key.strip() == name:
return value.strip().strip('"').strip("'")
return ""
def sandbox() -> int:
"""Count schedule lines confirmed for less than ordered in SAP's demo data."""
key = read_env_key("SAP_API_KEY")
if not key:
sys.exit("SAP_API_KEY is not set. Add it to .env (course setup topic), or run without --sandbox.")
fields = "SalesOrder,SalesOrderItem,ScheduleLine,RequestedDeliveryDate,ConfirmedDeliveryDate,ScheduleLineOrderQuantity,ConfdOrderQtyByMatlAvailCheck"
url = SANDBOX + f"API_SALES_ORDER_SRV/A_SalesOrderScheduleLine?$top=100&$select={fields}&$format=json"
request = urllib.request.Request(url, headers={"APIKey": key, "Accept": "application/json"})
try:
with urllib.request.urlopen(request, timeout=30) as response:
lines = json.load(response)["d"]["results"]
except urllib.error.HTTPError as err:
sys.exit(f"Sandbox answered HTTP {err.code}. 401 means the key is wrong; see the table in the topic.")
except urllib.error.URLError as err:
sys.exit(f"Cannot reach the sandbox: {err.reason}. Check your network or proxy, or use the pack.")
short = [ln for ln in lines
if float(ln.get("ConfdOrderQtyByMatlAvailCheck") or 0) < float(ln.get("ScheduleLineOrderQuantity") or 0)]
print(f"SAP sandbox: {len(lines)} schedule lines read (SAP demo data, not Calder's)")
print(f" {len(short)} confirmed for less than the quantity on that line")
for ln in short[:5]:
print(f" order {ln['SalesOrder']} item {ln['SalesOrderItem']} line {ln['ScheduleLine']}: "
f"ordered {ln['ScheduleLineOrderQuantity']}, confirmed {ln['ConfdOrderQtyByMatlAvailCheck']}")
print(" A line with zero confirmed is often followed by another line that confirms a later date.")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description="Trace one late supplier confirmation at Calder (fictional).")
parser.add_argument("--options", action="store_true", help="compare ways to recover")
parser.add_argument("--ai", action="store_true", help="where AI fits and who decides")
parser.add_argument("--interfaces", action="store_true", help="list the interfaces the trace crosses")
parser.add_argument("--confirmed", metavar="YYYY-MM-DD", help="try another confirmed date")
parser.add_argument("--write", action="store_true", help="write the solution package")
parser.add_argument("--sandbox", action="store_true", help="read schedule lines in SAP's sandbox")
args = parser.parse_args()
if args.sandbox:
return sandbox()
pack = load_pack()
try:
confirmed = d(args.confirmed or CONFIRMATION["ConfirmedDate"])
except ValueError:
sys.exit("Write the date as YYYY-MM-DD, for example 2026-11-09.")
if args.confirmed:
CONFIRMATION["ConfirmedDate"] = confirmed.isoformat()
r = run(confirmed)
if args.options:
show_options(r, pack)
elif args.ai:
show_ai()
elif args.interfaces:
show_interfaces()
elif args.write:
write_package(r, pack)
else:
show_trace(r, pack)
return 0
if __name__ == "__main__":
sys.exit(main())
You should see exactly this. The script has no random parts, so your output must match:
One exception, traced across Calder (fictional), as of 2026-10-28
1. Supplier confirms late [Buyer]
Orrin Motor Works confirms PO 4500000002 item 10: 20 x CP-MTR15 on 2026-11-18, asked for 2026-11-02 (12 working days late)
Document: Order confirmation on the PO item (supplier confirmation)
Posting: None
Crosses: I01 SAP Business Network -> S/4HANA CP10
2. MRP sees the gap [Production planner, buyer]
Night MRP run: message 10 (reschedule in) on PO 4500000002; order 1100019 needs 20 motors on 2026-11-03, 1 free in stock
Document: Exception messages in the stock/requirements list
Posting: None
Crosses: Inside S/4HANA
3. Production moves [Production planner, supervisor]
Order 1100019 (20 x CP-ES200): was 2026-11-03 to 2026-11-09, now 2026-11-19 to 2026-11-25; pumps available 2026-11-26 instead of 2026-11-10; the test bench clash in the week of 9 November disappears
Document: Production order dates changed; release waits for motors (missing parts)
Posting: None until goods issue and confirmations
Crosses: I02 S/4HANA -> MES (at release)
4. Availability check fails the date [Customer service, sales manager]
Sales order 1000011 (Brennock Water Utility, 16 x CP-ES200, complete delivery) was confirmed for 2026-11-13; re-checked, it confirms 2026-11-30
Document: Schedule lines: confirmed date and quantity change
Posting: None
Crosses: I03 S/4HANA -> CRM; I04 output to customer
5. Customer is told [Customer service]
New date 2026-11-30 goes to Brennock Water Utility; 11 working days late
Document: Changed order confirmation (output)
Posting: None
Crosses: I04 S/4HANA output -> customer (email PDF)
6. Motors arrive [Warehouse, buyer]
Goods receipt of 20 motors on 2026-11-18
Document: Material document (goods receipt)
Posting: Inventory 18,400.00 at standard; GR/IR 18,100.00; price difference -300.00
Crosses: I01 ship notice; inside S/4HANA
7. Pumps are built [Production, cost accounting]
Components issued, operations confirmed, 20 pumps received 2026-11-25
Document: Goods issues, confirmations, goods receipt
Posting: Finished goods 57,000.00 at standard; order credited
Crosses: I02 MES -> S/4HANA (confirmations)
8. Delivery and goods issue [Warehouse, transport planner]
Delivery and goods issue 2026-11-27 instead of 2026-11-12
Document: Outbound delivery, warehouse tasks, freight order, goods issue
Posting: Cost of goods sold 45,600.00; finished goods inventory -45,600.00
Crosses: I05 S/4HANA -> EWM (RKDC); I06 TM -> carrier
9. Billing [Billing, accounts receivable]
Invoice 2026-11-27 instead of 2026-11-12; November revenue holds by 1 working day(s)
Document: Billing document; journal entry
Posting: Receivable 67,200.00; revenue -67,200.00
Crosses: I07 S/4HANA -> data warehouse (revenue forecast)
10. Cash [Accounts receivable, treasury]
Due 2026-12-27 instead of 2026-12-12: 67,200.00 USD arrives 15 days later
Document: Incoming payment clears the receivable
Posting: Bank 67,200.00; receivable -67,200.00
Crosses: I08 bank statement -> S/4HANA
Impact: 16 pumps for order 1000011 arrive 11 working days late; 67,200.00 USD of cash 15 days later (cost of the delay at 8% a year: 220.93 USD).
Ripple stops at CP10: impeller and seal kit stock cover order 1100019, so CP20 sees no change.
Synthetic data: the trace shows the method, not results from an SAP system.
How to read it:
Steps 1 to 5 change dates, not money. The confirmation is 12 working days late. The production order can't start until the motors are available on 19 November, so it finishes on 25 November and the pumps are ready on 26 November. The availability check then confirms 30 November for Brennock's order.
One side effect is good. The old dates put the end of order 1100019 in the test bench's maintenance outage from 9 to 11 November, the overload the plan-to-produce topic found. The new dates miss it. A trace should report good news too, or a planner will undo it.
Steps 6 to 10 move money on new dates. The amounts are the same as before. The invoice lands on 27 November, one working day before November's last working day, and the cash arrives 15 days later.
The cost of the delay is small; the risk isn't. At the course's 8% a year, 15 days on 67,200.00 USD is 220.93 USD. The bigger costs are a customer who planned an installation and a month-end revenue figure with one day of slack.
The ripple stops at CP10. Rockford's impeller and seal kit stock covers the order, so Augsburg's ECC system sees no change. Say where a trace stops and why; reviewers will ask.
Ways to recover order 1000011 (fictional; requested 2026-11-13)
Option Delivery Cash late Extra USD Delay USD Decides
A Accept the new date 2026-11-30 15 d 0.00 220.93 Sales manager
risk: Brennock waits; November revenue holds by 1 working day(s)
B Expedite to 2026-11-09 at 950.00 2026-11-19 6 d 900.00 88.37 Buyer, production planner
risk: Orrin may refuse; build overlaps the test bench outage
C Split: 8 on time, 8 later 2026-11-13 / 2026-11-30 15 d 1,108.02 110.47 Brennock (complete delivery), customer service
risk: Second shipment 1540 kg; Brennock must accept a partial delivery
D Reallocate 8 pumps from blocked orders 2026-11-13 0 d 0.00 0.00 Sales manager, credit analyst
risk: Orders 1000002, 1000007 (32,688.00 USD) move to 2026-11-30 if released earlier
The script prices the options. A person chooses: none of them is the agent's call.
Synthetic data: the numbers show the method, not results from an SAP system.
Four options, four different owners:
A, accept. No extra cost, 15 days of cash and a waiting customer. The sales manager owns the customer conversation.
B, expedite. The buyer asks Orrin for 9 November at the expedite price from the procure-to-pay topic, 950.00 USD instead of 905.00: 900.00 USD more. The order then builds from 10 to 16 November, straight into the test bench outage. The buyer can't decide this alone; the planner must accept the capacity problem.
C, split. Eight pumps are free without touching other promises, so eight ship on time and eight on 30 November. The second shipment costs 1,108.02 USD under the Larkspur agreement from the warehouse topic. Brennock asked for complete delivery, so Brennock has to agree.
D, reallocate. Two blocked orders hold confirmed pumps: Hallam's six, blocked for credit, and Brennock's own two, on customer hold. Taking their confirmations gives Brennock all 16 on time, at no cost. If Hallam's credit block is released before 26 November, Hallam waits. This is what backorder processing strategies do in aATP, and it is a sales manager's and a credit analyst's decision.
D looks best on paper. That is exactly why it needs a person: it trades one customer's promise against another's, using a credit decision that isn't made yet.
Where AI fits, and who decides (fictional Calder trace)
# Pattern What the AI does Who decides
1 Detect Rule: confirmed date later than requested or than the pegged need Nobody: plain automation is enough
2 Explain Follow pegging PO -> production order -> sales order; say it in plain words Buyer and planner read it
3 Act (propose) Propose new order dates and flag capacity clashes Production planner reschedules
4 Act (propose) Price the recovery options A-D Sales manager, credit analyst
5 Act (draft) Draft the message to Brennock and the expedite request to Orrin Customer service and buyer send them
6 None Plain automation: the receipt posts from the ship notice Warehouse
7 None Plain automation Production supervisor
8 None Plain automation: dates flow to EWM and TM Warehouse, transport planner
9 Explain Flag the revenue now one working day from month end Controller
10 Predict Predict the payment date from Brennock's payment history Treasury uses it in the cash forecast
The agent reads, explains, prices and drafts. It never changes a PO, an order or a confirmation.
Interfaces the trace crosses (Calder landscape, fictional)
I01 SAP Business Network -> S/4HANA CP10: PO, order confirmation, ship notice [Network documents; owner: Procurement IT]
I02 S/4HANA CP10 -> MES (Rockford): Released orders out; confirmations back [Released-order download (Unit 5B); owner: Manufacturing IT]
I03 S/4HANA CP10 -> CRM: Sales order status and confirmed dates [Released API or events (Unit 5B); owner: Sales IT]
I04 S/4HANA CP10 -> Customer: Order confirmation (changed) [Output management, email PDF; owner: Customer service]
I05 S/4HANA CP10 -> EWM RKDC (decentralized): Outbound deliveries; goods issue back [Delivery replication (queues); owner: Logistics IT]
I06 TM in CP10 -> Carriers: Freight order, tender, invoice [Carrier EDI or portal; owner: Transport planner]
I07 S/4HANA CP10 -> Data warehouse / Datasphere: Open orders, billing for the revenue forecast [CDS extraction; owner: Analytics team]
I08 Bank -> S/4HANA CP10: Bank statement with the payment [Bank file; owner: Treasury]
I09 S/4HANA CP10 -> Agent on BTP (to-be): Business events plus read-only APIs [Event broker, OData (Unit 5B); owner: Integration lead]
The first table is the heart of the AI design: four steps need no AI at all, detection is a rule, and the valuable work is explaining, pricing and drafting. Where AI fits, and where it doesn't below explains each line.
Impact: 16 pumps for order 1000011 arrive 4 working days late; 67,200.00 USD of cash 6 days later (cost of the delay at 8% a year: 88.37 USD).
Ripple stops at CP10: impeller and seal kit stock cover order 1100019, so CP20 sees no change.
Synthetic data: the trace shows the method, not results from an SAP system.
Scroll up to step 3: a smaller delay isn't simply better, because the build now overlaps the test bench outage. Try --confirmed 2026-10-30 too: the impact drops to zero, because the motors come before they are needed.
wrote calder_solution_package/README.md
wrote calder_solution_package/landscape.md
wrote calder_solution_package/process_trace.md
wrote calder_solution_package/risks.md
wrote calder_solution_package/ai_opportunities.md
wrote calder_solution_package/interfaces.csv
Open calder_solution_package/README.md first. All data is fictional.
Open unit1b/calder_solution_package/README.md and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS) for the preview. It starts with the one-page solution design summary: problem, goal and KPI, scope, and a table that applies each Unit 1B design topic to this trace. Open landscape.md to see the diagram, and interfaces.csv in a spreadsheet.
Save your work:
git add unit1b/trace_exception.py unit1b/calder_solution_package
git commit -m "Trace one late supplier confirmation across Calder"
#Step 9 (optional): Read schedule lines in SAP's sandbox
This step reads real SAP demo data: sales order schedule lines, where the availability check records what it confirmed.
Check that .env in your course folder has a line SAP_API_KEY=... from the course setup.
Run:
Windows (PowerShell):
python unit1b\trace_exception.py --sandbox
macOS / Linux:
python unit1b/trace_exception.py --sandbox
You should see a count of schedule lines and up to five lines where the confirmed quantity is below the line's quantity. The shape is below; the values in angle brackets depend on the sandbox's demo data, which changes:
SAP sandbox: <number> schedule lines read (SAP demo data, not Calder's)
<number> confirmed for less than the quantity on that line
order <order> item <item> line <line>: ordered <quantity>, confirmed <quantity>
A line with zero confirmed is often followed by another line that confirms a later date.
An empty list, 0 confirmed for less, is a valid answer: the demo orders on that page are fully confirmed. The sandbox is read-only and its data isn't Calder's.
Where it sits. At Calder (fictional), every step of this trace runs in CP10's S/4HANA system, private edition under RISE. Around it sit SAP Business Network for suppliers, a MES at Rockford, a CRM, decentralized EWM at the distribution center, TM for carriers, a data warehouse and the bank. CP20 on ECC joins only through intercompany EDI until its rollout in July 2027. The to-be design adds one component: a tracing agent on BTP that receives business events, reads released APIs and writes a worklist with priced options. People act in SAP's own apps.
flowchart LR
BN[SAP Business Network] -->|confirmations| S4[S/4HANA CP10]
S4 <--> MES[MES Rockford]
S4 --> CRM[CRM]
S4 <--> EWM[EWM RKDC]
S4 -->|events, read-only APIs| AG[Tracing agent<br/>on BTP]
AG --> WL[Worklist and drafts<br/>buyer, planner, sales]
WL -->|owner acts in SAP app| S4
ECC[ECC CP20] -.->|intercompany EDI| S4
Non-functional requirements
Requirement
Typical target (course value)
What breaks if missed
Detection latency
Impact known within 15 minutes of the confirmation
Options B to D close while nobody looks
Data freshness
Proposals read live data and show their as-of time
A planner acts on yesterday's promise
Security
Display-only technical user for the agent
An agent with change rights is an uncontrolled planner
Volume and rate
Within the course limit of 60 calls a minute, with headroom
Month-end bursts throttle the agent, or the agent slows SAP
Audit
Every proposal, decision and decider logged
Nobody can explain why Hallam waited
Cost
Calls per exception measured and budgeted
A busy morning of confirmations costs more than the delays
Integration and identity. Confirmations enter through SAP Business Network (I01). Business events tell the agent that a purchase order changed; it then reads the confirmation, the supply and demand elements, the production order and the sales order schedule lines through released APIs, under its own technical user. Drafts to customers and suppliers go to customer service and the buyer, who send them from their own tools under their own identity. No user's identity is borrowed by the agent; Agent permissions and SAP authorizations covers why.
Context: A late supplier confirmation reaches customers and cash through five teams. Standard tools flag each link: pending confirmations, MRP messages, missing parts, backorder processing. Nobody sees the chain.
Options: (1) standard only: switch on the situation templates and backorder processing runs, and train teams; (2) a side-by-side tracing agent that reads released APIs and proposes priced options; (3) a custom program in S/4HANA that changes dates and confirmations automatically.
Decision: (1) first, then (2) for the chain view. Configure confirmation control and backorder processing properly before building anything; add the agent only for the cross-team impact and the options. Reject (3): it changes promises without owners and writes outside released objects.
Consequences: The agent's value depends on clean configuration upstream. Its proposals need an owner per option type. Once SAP's Order Lifecycle or Planning Assistant is available and licensed for Calder's edition, re-check how much of (2) it covers.
What a design review will ask
Which confirmation categories are MRP-relevant today, and for which suppliers?
How does the agent find the customer behind a receipt: pegging, reservations, or its own logic?
Who approves each option type, and where is the approval recorded?
What happens to the trace when CP20 joins CP10's system in July 2027?
Which part of this would SAP's planned assistants already cover, and when?
A rule compares the confirmed date with the requested date and the pegged need
Nobody: it only raises the case
MRP gap
Explain
Follows pegging to the production and sales orders and writes the impact in plain words
Buyer and planner read it
Production order
Act (propose)
Proposes new dates and flags capacity clashes such as the test bench outage
Production planner reschedules
Customer promise
Act (propose)
Prices options A to D
Sales manager, with the credit analyst for D
Customer and supplier messages
Act (draft)
Drafts the message to Brennock and the expedite request to Orrin
Customer service and buyer send them
Receipts, production, delivery
Plain automation is enough
Dates flow through standard documents and interfaces
Warehouse, production, transport
Billing
Explain
Flags revenue now one working day from month end
Controller
Cash
Predict
Predicts the payment date from the customer's payment history
Treasury uses it in the forecast
Three rules follow from the table:
Detection is a rule, not a model. Comparing two dates needs no AI. Spending model calls on it adds cost and a failure mode.
The value is in explaining and pricing. Following pegging across five documents, putting numbers on options and writing them up for five audiences is slow for people and well suited to an agent that reads released APIs. The order exception agent builds this pattern three ways.
Every change has an owner. Supplier dates, production order dates, customer allocations and messages to customers stay with the buyer, planner, sales manager and customer service. Human approval in AI workflows covers how to enforce that in code.
Tracing on requested dates. If confirmations aren't MRP-relevant, the trace follows a date nobody promised. Check confirmation control first.
Stopping at your own module. A buyer's view ends at the purchase order. The cost is in sales and finance.
Ignoring good news. The delay removed a capacity clash. A trace that reports only damage invites someone to "fix" the wrong thing.
Treating option D as free. Reallocation costs another customer's promise. Price it with the risk, and let the owner decide.
One trace, no pattern. One late motor order is an anecdote. The business case comes from counting how many confirmations a month reach a customer, which needs supplier history in the analytics layer.
Hard-coding Calder's rules. Working days, pick and transit times and checking rules are configuration and factory calendars in a real system. Read them; don't copy the script's constants.
Make the solution package yours and test one change to the design.
Run python unit1b/trace_exception.py --write (Windows: python unit1b\trace_exception.py --write) if you haven't yet.
Open unit1b/calder_solution_package/README.md. Under the design table, add a heading Your decisions with one line per design area, linking the file you wrote in that topic's exercise if you did it, for example calder_nfr.md for non-functional requirements.
In process_trace.md, add a column Owner's next action to the recovery options table, with one sentence per option.
Add a heading Recommendation with three lines: the option you would put first to the sales manager, why, and what must be true for it to work.
In trace_exception.py, change "ConfirmedDate": "2026-11-18" to "2026-11-25" and run the script without options and with --options. Note the invoice date and what happened to November's revenue. Change the date back.
Add a heading Stress test to the README with two lines: the invoice date with a 25 November confirmation, and which design area must react (hint: reporting, and who owns the revenue forecast).
In risks.md, add one risk of your own with a control and an owner.
Preview all files and commit them with Git.
Done when:unit1b/calder_solution_package/ has the README with Your decisions, Recommendation and a Stress test that says a 25 November confirmation moves the invoice to 4 December and November's 67,200.00 USD of revenue into December, process_trace.md with the Owner's next action column filled in for all four options, and risks.md with seven risks. Keep the package: the Unit 14 capstone builds its exception platform on traces like this one.
Pick one answer for each question. The explanation appears after you choose.
1Orrin's confirmation for 18 November arrives by EDI, but the plan still shows 2 November. What do you check first?
Answer: B. SAP's EDI confirmation handling warns and doesn't update the purchasing document when values deviate beyond tolerance. Until someone processes it, planning keeps the old date. A category that isn't MRP-relevant has the same effect.
2Which link does an agent follow to find the customer behind a late purchase order?
Answer: D. Pegging connects a receipt to the requirement it covers: here the production order's reservation, and then the sales order confirmed against the production order's output. The control key only says which confirmations are expected.
3The script confirms order 1000011 for 30 November. What does it assume, which a real system reads from configuration?
Answer: C. Pick and transit times and factory calendars are configuration and master data in SAP. The script hard-codes simple values; a real trace reads them, or its dates are wrong.
4Option D gives Brennock all 16 pumps on time at no cost. Why shouldn't the agent apply it?
Answer: C. Reallocation takes confirmed pumps from Hallam and from Brennock's held order. It depends on a credit decision not yet made. A sales manager and a credit analyst decide; the agent prices it and logs the decision.
5Why is detection in this trace a rule rather than a model call?
Answer: D. The check is whether the confirmed date is later than the requested or pegged date. A rule does that exactly and cheaply. Model calls belong to explaining and drafting, where language helps.
6You design the tracing agent's access. What does it get?
Answer: A. The agent reads and proposes, so it needs display rights under its own identity, which keeps its actions auditable. Borrowed users and change rights make it an unaccountable planner; emergency access is for incidents.
7Orrin had confirmed 9 November instead. The delay shrinks to four working days. What new problem does the trace show?
Answer: B. With motors on 9 November, order 1100019 builds from 10 to 16 November, overlapping the test bench maintenance on 9 to 11 November. Smaller delays can create new conflicts, which is why the trace re-checks capacity.
8A design review asks what happens to this trace in July 2027. What is the best answer?
Answer: C. Under option A, CP20 joins CP10's S/4HANA system as a company code. Its open orders arrive with new numbers from the key map, and the trace must cover Augsburg's plant without the EDI hop.
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.
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
Monitoring Purchase Orders (SAP Learning)— Acknowledgment required indicator; confirmation control key sets expected confirmations, sequence and MRP and GR relevance; confirmations entered in the PO, as inbound deliveries or electronically; Monitor Supplier Confirmations app and ME2A; reminders ME91F and ME91FF
Receiving Acknowledgments via EDI (SAP Library, MM-PUR-GF-CON)— ORDRSP message type; acknowledgment-only records just the number; with a confirmation control key, quantities, prices and dates are checked against tolerances; deviations raise a warning and the document is not updated; acknowledgment applies to the item, not the schedule line
Deploying Situation Handling (SAP Learning, purchasing)— templates PROC_PENDINGSUPPLIERCONF_V2 (pending supplier confirmation) and PROC_POITEMCONFQTYDEFICIT (quantity deficit in supplier's delivery); recipients by responsibility management; My Situations, notifications, Monitor Situations
Using Advanced Available-To-Promise (aATP) in SAP S/4HANA (SAP Learning)— product availability check, product allocation, alternative-based confirmation; backorder processing rechecks confirmations when supply changes, e.g. a late production order; strategies Win, Gain, Redistribute, Improve, Fill, Lose; Release for Delivery; aATP may need its own license
SAP Sapphire 2026 Innovation News Guide (SAP)— Order Lifecycle Assistant GA planned Q3 2026; Planning Assistant and Business Network Assistant GA planned Q4 2026; Accounts Receivable Assistant GA planned Q2 2026; Cash and Treasury Assistant early adopter Q2 2026
Cloudification Repository (SAP on GitHub)— objectReleaseInfoLatest.json read 29 October 2026; I_POSupplierConfirmationAPI01, I_PurOrdScheduleLineAPI01, I_MRPExceptionMessage, I_SupplyDemandItemTP, I_ManufacturingOrder, I_SalesOrderScheduleLine, I_DeliveryDocument, I_BillingDocument, I_OperationalAcctgDocItem released
SAP Cloud SDK generated models for sales order, purchase order and production order APIs (npm, version 2.1.0)— A_SalesOrderScheduleLine ConfirmedDeliveryDate, ConfdOrderQtyByMatlAvailCheck; A_SalesOrder CompleteDeliveryIsDefined; A_PurchaseOrderScheduleLine ScheduleLineDeliveryDate; A_PurchaseOrderItem SupplierConfirmationControlKey; A_ProductionOrder_2 MfgOrderScheduledStartDate and function releaseOrder are in the separate package @sap/cloud-sdk-op-vdm-production-order-v2-service 2.1.0; npm marks these pregenerated clients as no longer maintained