Orchestrate

Report an issue

Something wrong, out of date or unclear? Tell us. Corrections are prioritized over new topics. You don't need to sign in.

0 / 2000

Don't include personal or confidential company details. Reports are reviewed with AI assistance.

One exception, traced across the enterprise

Follow one late supplier confirmation from purchase order to cash, name every document, posting, interface and team, and package it as a solution design.

Updated Oct 29, 2026Foundational 9 minDeep 40 min
Foundational layer · 9 min read

The 60-second version

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.

Why it matters to the business

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.

How SAP does it

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.

The ripple on one page

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.

The deliverable: a solution package

The deep layer turns the trace into a solution package: the kind of document an architect brings to a design review. It holds a one-page solution design summary, a landscape diagram, the process trace, an interface list, risks and AI opportunities. The summary applies each of this unit's design topics to the trace: landscape and deployment, fit-to-standard and extensions, roles, data migration, analytics and non-functional requirements. The same package is the starting point for the Unit 14 capstone.

Questions to ask

  1. When a supplier confirms a later date, how long until the affected customer order is known, and who finds out?
  2. Which suppliers must confirm, through which channel, and are their confirmations relevant for planning?
  3. Do we re-check customer promises automatically when supply changes, or only when someone notices?
  4. Who may move confirmed quantities from one customer to another, and how is that logged?
  5. Which of our interfaces would delay or lose the signal: the supplier network, the MES, the CRM, the warehouse system?
  6. If an AI agent proposes a recovery, which decisions stay with a buyer, a planner and a sales manager?

Common misconceptions

  • "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.

Key terms

All course terms, A to Z, in the glossary

  • 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.

Check yourself

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.
Deep layer · 40 min read

Mental model

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.

Business objects and documents

Before you start: this topic builds on the whole of Unit 1B, especially Procure to pay end to end, Plan to produce end to end, Order to cash end to end, Warehouse and transportation execution and Document flow and how logistics becomes finance. It doesn't repeat them; it connects them.

The scenario

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 documents the trace touches

Object What changes Calder example (fictional)
Purchase order item and its confirmation A confirmation records the supplier's quantity and date. The confirmation control key decides whether planning uses it 4500000002 item 10: 20 × CP-MTR15, requested 2 November, confirmed 18 November
MRP elements and exception messages 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).

Document flow and postings

flowchart LR
  PO[PO 4500000002<br/>confirmed 18 Nov] --> MRP[MRP message 10]
  MRP --> PRO[Production order<br/>1100019 moved]
  PRO --> SO[Sales order 1000011<br/>re-confirmed 30 Nov]
  PO --> GR[GR motors<br/>18 Nov]
  GR --> J1[Inventory,<br/>GR/IR, price diff.]
  PRO --> PGR[GR pumps<br/>25 Nov]
  PGR --> J2[Finished goods<br/>at standard]
  SO --> DL[Delivery,<br/>goods issue 27 Nov]
  DL --> J3[Cost of<br/>goods sold]
  DL --> BI[Invoice 27 Nov]
  BI --> J4[Receivable,<br/>revenue]
  J4 --> PAY[Payment<br/>27 Dec]
Step Posts Amount (fictional) Why
Confirmation, MRP, rescheduling, re-confirmation Nothing Plans and promises
Goods receipt of 20 motors Inventory at standard, GR/IR at the PO price, the difference to price differences 18,400.00 inventory; 18,100.00 GR/IR; -300.00 price difference 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.

Neighbors

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
I03 S/4HANA → CRM Sales order status and confirmed dates The account manager promises the old date
I04 S/4HANA → customer Changed order confirmation (output) The customer learns at the dock
I05 S/4HANA → EWM RKDC (decentralized) and back Outbound deliveries, goods issue Picking plans for pumps that aren't there
I06 TM in CP10 → carriers Freight orders A truck booked for 12 November waits for nothing
I07 S/4HANA → Datasphere or the data warehouse Open orders, billing The revenue forecast still shows 12 November
I08 Bank → S/4HANA Bank statement Cash application and the cash forecast drift
I09 S/4HANA → agent on BTP (to-be) Business events plus read-only APIs Detection falls back to someone reading lists

How these are built (EDI, released APIs, events, queues) is covered in how SAP systems talk; monitoring them is covered in the integration lead's toolkit.

Where it breaks

The trace itself fails in predictable places. Each row is a reason a real Calder would see the late motors only on 13 November.

Symptom Usual cause Who resolves it What it costs Calder (scenario)
Planning still uses 2 November Confirmation category not MRP-relevant, or no confirmation control key Purchasing configuration owner The chain stays hidden until the goods receipt is missed
The confirmation sits in an inbound queue Date outside the confirmation tolerance; the document isn't updated Buyer Same as above, plus a message nobody reads
Message 10 drowns in the morning list Exceptions not ranked by customer impact Production planner Order 1100019 is noticed when the motors don't come
Order 1000011 stays confirmed for 13 November Availability not re-checked after the production order moved Customer service, aATP configuration Brennock learns at the promised date
CRM shows the old date Status interface I03 batches nightly or only on creation Sales IT Account manager confirms a wrong date by phone
Option D is never considered Nobody sees that two blocked orders hold 8 pumps Sales manager 15 days of cash on 67,200.00 USD, avoidable at no cost
Nobody confirms at all Pending confirmations not monitored Buyer The silent version of this whole topic

How it is exposed

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).

Link OData API (A2X) Released CDS views What to read
Purchase order and confirmation API_PURCHASEORDER_PROCESS_SRV I_PurchaseOrderItemAPI01, I_PurOrdScheduleLineAPI01, I_POSupplierConfirmationAPI01, I_SupplierConfControlKey SupplierConfirmationControlKey, ScheduleLineDeliveryDate, confirmed dates and quantities
Planning I_SupplyDemandItemTP, I_MRPExceptionMessage Supply and demand elements per material; the message catalog
Production order API_PRODUCTION_ORDER_2_SRV I_ManufacturingOrder MfgOrderScheduledStartDate, MfgOrderScheduledEndDate, OrderIsReleased
Sales order promise API_SALES_ORDER_SRV I_SalesOrderScheduleLine RequestedDeliveryDate, ConfirmedDeliveryDate, ConfdOrderQtyByMatlAvailCheck, CompleteDeliveryIsDefined
Delivery API_OUTBOUND_DELIVERY_SRV I_DeliveryDocument Planned goods issue date
Billing and receivable API_BILLING_DOCUMENT_SRV I_BillingDocument, I_OperationalAcctgDocItem 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.

Build it yourself

Before you start: finish Set up your computer for this course and Set up for Unit 1B. This walkthrough uses the Calder sample pack from the Unit 1B setup.

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

What you need

  • The course folder orchestrate-course with its .venv, and the Unit 1B sample pack.
  • For Steps 1 to 8: no account, no key and no network. The script uses only Python's built-in modules, so requirements.txt doesn't change.
  • For Step 9 (optional): your free SAP Business Accelerator Hub key as SAP_API_KEY in .env, from the course setup.
  • Cost: free. Time: about 45 minutes.

Step 1: Open your course folder and turn on the virtual environment

  1. Open VS Code, then File > Open Folder and choose orchestrate-course.
  2. Open a terminal with Terminal > New Terminal.
  3. Turn on the virtual environment:
    • Windows (PowerShell):

      .venv\Scripts\Activate.ps1
    • macOS / Linux:

      source .venv/bin/activate

You should see (.venv) at the start of the prompt.

Step 2: Make sure the sample pack is current

  1. 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
  2. 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.

Step 3: Create the script

  1. In VS Code, create a new file, paste the script below, and save it as trace_exception.py inside the unit1b folder.
  2. 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())

Step 4: Trace the exception

  1. Run the script:

    • Windows (PowerShell):

      python unit1b\trace_exception.py
    • macOS / Linux:

      python unit1b/trace_exception.py
  2. 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.

Step 5: Price the ways to recover

  1. Run:

    • Windows (PowerShell):

      python unit1b\trace_exception.py --options
    • macOS / Linux:

      python unit1b/trace_exception.py --options
  2. You should see:

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.

Step 6: See where AI fits and the interfaces

  1. Run:

    • Windows (PowerShell):

      python unit1b\trace_exception.py --ai
      python unit1b\trace_exception.py --interfaces
    • macOS / Linux:

      python unit1b/trace_exception.py --ai
      python unit1b/trace_exception.py --interfaces
  2. You should see:

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.

Step 7: Try another confirmed date

  1. Suppose Orrin had confirmed 9 November instead. Run:

    • Windows (PowerShell):

      python unit1b\trace_exception.py --confirmed 2026-11-09
    • macOS / Linux:

      python unit1b/trace_exception.py --confirmed 2026-11-09
  2. The last lines should read:

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.

Step 8: Write the solution package

  1. Run:

    • Windows (PowerShell):

      python unit1b\trace_exception.py --write
    • macOS / Linux:

      python unit1b/trace_exception.py --write
  2. You should see:

  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.
  1. 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.
  2. 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.

  1. Check that .env in your course folder has a line SAP_API_KEY=... from the course setup.

  2. Run:

    • Windows (PowerShell):

      python unit1b\trace_exception.py --sandbox
    • macOS / Linux:

      python unit1b/trace_exception.py --sandbox
  3. 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.

What each part of the code does

Part What it does
Course rules and scenario Working-day rules, the purchase order, the confirmation, the production order, the ES-200 supply and demand, order 1000011, freight and blocked orders
add_wd, wd_between Count working days, Monday to Friday
load_pack Reads names and standard prices from the sample pack and checks them
production Schedules order 1100019 from the date its motors are available and checks the test bench outage
atp_date Finds the earliest date from which 16 pumps stay free on every later date: a simple cumulative availability check
schedule_order Turns the availability date into goods issue, delivery, invoice and due dates; confirms the requested date when it can
run Plays the chain twice, with the requested and the confirmed motor date, and compares them
steps, show_trace The ten steps with document, posting, interface and team
options, free_on_time Prices recovery options A to D
AI_MAP, INTERFACES The AI map and the interface list
DESIGN_LINKS, RISKS, package_files, write_package The solution package files
sandbox Optional: reads A_SalesOrderScheduleLine from SAP's sandbox

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found Python isn't on the path, or the venv is off Turn on the virtual environment (Step 1); on macOS/Linux try python3
The sample pack is missing The pack wasn't written Run Step 2
products.json has other standard prices You changed the pack by hand Run make_calder_pack.py again
Write the date as YYYY-MM-DD The --confirmed date is malformed Use a date like 2026-11-09
Output differs from the topic The script or pack was changed Paste the script again and rewrite the pack
SAP_API_KEY is not set No key in .env Add it as in the course setup, or skip Step 9
Sandbox answered HTTP 401 The key is wrong or expired Copy the key again from the SAP Business Accelerator Hub
Cannot reach the sandbox Network or company proxy blocks it Try another network; Steps 1 to 8 work offline

Architecture view

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.

The decision to record (in the ADR form from Writing a solution design doc):

  • 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

  1. Which confirmation categories are MRP-relevant today, and for which suppliers?
  2. How does the agent find the customer behind a receipt: pegging, reservations, or its own logic?
  3. Who approves each option type, and where is the approval recorded?
  4. What happens to the trace when CP20 joins CP10's system in July 2027?
  5. Which part of this would SAP's planned assistants already cover, and when?

Where AI fits, and where it doesn't

Step Pattern What it does Who decides
Confirmation arrives Plain automation is enough 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.

Pitfalls

  • 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.

Exercise

Make the solution package yours and test one change to the design.

  1. Run python unit1b/trace_exception.py --write (Windows: python unit1b\trace_exception.py --write) if you haven't yet.
  2. 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.
  3. In process_trace.md, add a column Owner's next action to the recovery options table, with one sentence per option.
  4. 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.
  5. 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.
  6. 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).
  7. In risks.md, add one risk of your own with a control and an owner.
  8. 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.

Check yourself

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.

Practice this in the lab

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

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in