Order to cash is everything between a customer asking for something and the money arriving in the bank. In SAP it runs as a chain of documents: quotation, sales order, delivery, goods issue, invoice and payment.
At each link, the system checks something before it lets the order move on:
Price: what does this customer pay for this product today?
Availability: can we ship the quantity on the date they want?
Credit: is it safe to ship more to this customer?
Completeness: is every field the next step needs filled in?
When a check fails, the order stops and waits for a person. A credit analyst, a pricing clerk or a planner frees it. Meanwhile the goods sit in the warehouse and the invoice isn't sent.
Three ideas carry this topic:
The order is a promise, not money. Cash only moves when goods ship, the invoice goes out and the customer pays.
Stuck orders are the cost. Each block is a control that protects the company, but every hour an order waits delays revenue and cash.
Every step has an owner. Sales, shipping, billing, credit and accounts receivable each own one part. Most problems sit on the handoffs between them.
Order to cash decides how fast sales become cash. Finance tracks this as days sales outstanding (DSO): how long, on average, customers take to pay. Sales tracks on-time delivery. Both depend on orders flowing without stops.
Here is a snapshot from Calder Pumpworks, the fictional pump maker this unit follows. On 23 October 2026, Calder's order-to-cash worklist shows seven items stuck:
What is stuck
Why
Who frees it
Value held (USD)
Six pumps for distributor Hallam
Over the credit limit
Credit analyst
24,288.00
Five ES-300 pumps for Brennock
Only one can ship on the date wanted
Order fulfillment with planning
24,600.00
Two ES-300 pumps for Brennock
Payment terms missing on the order
Sales order team
12,300.00
Two ES-200 pumps for Brennock
Customer asked to hold the delivery
Customer service
8,400.00
One pump already shipped
Invoice blocked until the customer accepts it
Billing
4,200.00
40 seal kits for Hallam
No price for the aftermarket channel
Pricing team
not priced
A short payment from Hallam
Customer paid 1,150 less than invoiced
Accounts receivable
1,150.00
The business decision is not "remove the blocks". A credit block that stops shipments to a customer who can't pay is doing its job. The decision is how fast each block reaches the right person, with the facts they need. That is also where most AI proposals for order to cash aim.
As of October 2026, SAP S/4HANA covers the whole chain as standard. SAP's sell-from-stock process runs from a sales order with pricing and an availability check, through picking and goods issue, to the invoice and the payment that clears the customer's account (SAP Learning). Apps such as Manage Sales Orders - Version 2 and Sales Order Fulfillment Issues show the orders and their problems.
Pricing uses the condition technique: a set of rules that finds the right price, discount and surcharge records for each item (SAP Learning).
Availability (ATP, available-to-promise) checks stock and planned receipts when the order is entered. The advanced version, aATP, adds backorder processing, product allocation and release for delivery (SAP Learning). Licensing differs by edition. In SAP S/4HANA Cloud Public Edition, basic ATP is in the standard license and the advanced functions need an extra one. In Private Edition and on-premise, aATP must be activated and also needs an additional license (SAP Learning).
Credit runs in SAP Credit Management. Basic credit checks are included. Scorecards, automatic credit limits, workflow and external credit data need a separate license (SAP Learning). A credit analyst releases or rejects blocked orders in Manage Documented Credit Decisions (SAP Learning).
Receivables, disputes and collections live in finance. A customer's short payment can open a dispute case linked to the open invoice (SAP Learning).
SAP has also released AI features for this process. In its Q1 2026 release highlights, SAP lists these for SAP S/4HANA Cloud Public Edition (SAP News):
AI-assisted sales order creation from PDF or image purchase orders (generally available)
AI-assisted processing of payment advices (generally available)
A Dispute Resolution Agent (beta in Q1 2026; by October 2026 SAP described its general availability as rolling out in phases, SAP News)
"A sales order is revenue." It is a commitment. Revenue posts at billing, and cash arrives at payment.
"Blocks are errors to eliminate." Most blocks are controls. The goal is fast, well-informed decisions, not zero blocks.
"ATP says the stock is there." ATP confirms against stock and planned receipts, minus what earlier orders already claimed. A confirmed date can rely on a receipt that hasn't happened yet.
"Credit management is a finance topic only." It decides whether sales can ship. Sales, shipping and finance all feel a credit block.
"An AI agent should release blocked orders." It can prepare the decision and explain it. Releasing credit is a person's decision with an audit trail.
Pick one answer for each question. The explanation appears after you choose.
1Calder saves a sales order for six pumps. What has changed in the company's books?
Answer: C. The sales order posts nothing in finance. Cost appears at goods issue, the receivable and revenue at billing, and cash at payment.
2Hallam's new order goes over its credit limit and is blocked. What is the best way to treat the block?
Answer: C. A credit block protects Calder from shipping to a customer who may not pay. The business goal is a fast, informed decision by the credit analyst, not removing the control.
3An order item shows one pump confirmed for the date wanted and four for two weeks later. What happened?
Answer: C. ATP confirms what is free on the requested date and proposes later dates for the rest. The split shows as schedule lines, and only confirmed lines can be delivered.
4An order can be delivered but the invoice can't be created. Which cause fits?
Answer: C. SAP's incompleteness checks are typed by step. Missing billing data, such as payment terms, lets the order ship but stops the invoice.
5A vendor pitches an AI agent that releases credit-blocked orders automatically. What should you ask for?
Answer: A. Releasing credit is a financial decision with an audit trail. An agent can gather the exposure, payment history and order facts and recommend, while a credit analyst decides.
6Calder's customers take longer to pay this quarter. Which order-to-cash measure shows it directly?
Answer: D. DSO measures how long customers take to pay after billing. Blocked orders delay billing, which hurts cash too, but DSO is the direct measure of payment speed.
7Which part of order to cash did SAP list as generally available AI for S/4HANA Cloud Public Edition in Q1 2026?
Answer: D. SAP's Q1 2026 release highlights list AI-assisted sales order creation from unstructured purchase orders as generally available. The Dispute Resolution Agent was beta, and none of the other options were announced.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Think of order to cash as one promise, made more specific at every step, with a gate in front of each step.
The quotation promises a price.
The sales order promises a quantity, a date and a price to one customer.
The delivery promises those goods leave from this dock.
The goods issue says they left.
The invoice turns the promise into a claim on the customer.
The payment settles the claim.
Each gate is a check that runs on data other teams own: price records from pricing, stock and receipts from planning, credit limits from credit management, terms from the business partner. When a gate closes, the order carries a status that says why. Every block, worklist and AI triage tool in this process reads those statuses. If you know which gate closed and who owns its data, you know who has to act.
Inquiries and quotations are SAP's two pre-sales document categories. An inquiry records what a customer asked about; a quotation is the offer. SAP notes that using them is not mandatory (SAP Learning). When a quotation is only partly ordered, a reason for rejection on the remaining items closes it. The reasons ("too expensive", "delivery date too late") are kept, so lost business can be analyzed.
An order created with reference to a quotation copies its data under copying control, defined at header, item and schedule-line level, with requirements such as "same sold-to party" and "item not rejected" (SAP Learning). The reference is what lets document flow show "this order came from that quotation".
Every sales document belongs to a sales area (sales organization, distribution channel, division) and needs a customer and at least one item. The user enters the sold-to party, material and quantity; the system proposes the rest from master data (SAP Learning).
Each condition applied, with its type, rate and amount
Brennock's contract price for CP-ES200
Every item that is physically delivered needs at least one schedule line, and there can be several when the quantity is confirmed in parts (SAP Learning). In SAP's model of the Sales Order API, the schedule line carries RequestedDeliveryDate, ConfirmedDeliveryDate, ScheduleLineOrderQuantity, ConfdOrderQtyByMatlAvailCheck and its own DelivBlockReasonForSchedLine. The header carries DeliveryBlockReason, HeaderBillingBlockReason, TotalCreditCheckStatus and OverallSDProcessStatus; the item carries ItemBillingBlockReason and SalesDocumentRjcnReason (SAP Cloud SDK model, 2022).
The order's pricing procedure is chosen from the sales area, a key on the order type and a key on the customer. The procedure lists the allowed condition types (price, discounts, surcharges, freight, tax) in sequence, with subtotals. Each condition type has an access sequence: an ordered list of condition tables, each with its own key, such as customer plus material, then material alone. The search stops at the first valid condition record (SAP Learning). A condition can be marked mandatory; if a mandatory price is not found, pricing is incomplete and the order can't proceed normally.
This is why "the price is wrong" usually means "a different record was found first", or "no record exists for this key". Condition records are master data, covered in the master data topic.
If the check is active for the material, it runs when the order is created. It compares the requested quantity with the cumulative ATP quantity on the requirement date in the delivering plant. If it is covered, the quantity is reserved and the schedule line is confirmed. If not, the system proposes later dates or a partial confirmation, or leaves the line unconfirmed. SAP's lesson is blunt: only confirmed schedule lines can be delivered (SAP Learning).
Advanced ATP (aATP) builds on the same concept and adds (SAP Learning):
Capability
What it does
Calder use
Product availability check
The basic check, with scope of check and scheduling
Every pump order
Backorder processing
Re-checks confirmations when supply or demand changes
A test-bench outage delays ES-300 output
Product allocation
Caps how much a customer group or region can get
Ration ES-300 pumps between utilities and distributors
Alternative-based confirmation
Confirms from another plant
Not used: only CP11 assembles pumps
Release for delivery
Lets an order fulfillment manager prioritize scarce stock across due orders
Month-end shortages
A related feature, supply protection, reserves quantities for groups of customers in time buckets (SAP Learning). Check the edition and license before designing on any of these: the licensing note under How SAP does it applies.
In S/4HANA, credit checks from sales call SAP Credit Management (FIN-FSCM-CR). Each customer needs the business partner role UKM000 for it. Credit limits and exposure are kept per credit segment, and each credit control area maps to one segment. For sales document types, only credit check type D (or no check) remains (SAP Learning).
Checks can be set for sales document types and for delivery types, at delivery creation and at goods issue (SAP Learning). An order above the limit is blocked. A credit analyst opens it in Manage Documented Credit Decisions and chooses Release, Recheck or Reject (with a reason). Manage Credit Accounts shows the partner's credit profile, and Manage Credit Limit Requests handles limit changes with approval (SAP Learning).
The incompleteness log lists fields missing for a later step. Incompleteness procedures are typed: general, delivery, billing, price, goods movement, picking and packing. An order missing billing data, such as payment terms, can still be delivered but not billed (SAP Learning).
Blocks are deliberate stops. A sales document type can propose a delivery or billing block by default (SAP Learning), and a customer master can carry an order block (SAP Learning). A billing block stops the invoice and writes an error log when billing runs (SAP Learning). Block reasons are configuration: read their codes and texts from the system.
An outbound delivery references at least one sales order. It controls picking and packing (both optional), transport planning (optional) and goods issue. A delivery has no schedule lines; each order schedule line due for shipping can become a delivery item. The Pick Outbound Delivery app copies the picked quantity into the delivery, after which goods issue can be posted. Goods issue changes stock quantity and value, creates the accounting documents and puts the delivery on the billing due list (SAP Learning). With extended warehouse management, a partial goods issue isn't possible; the delivery is split instead (SAP Learning). The warehouse side is covered in Warehouse and transportation execution.
Goods are billed with reference to the delivery; services with reference to the order. Saving the invoice creates the accounting documents and updates profitability analysis, statistics and credit exposure (SAP Learning). Billing runs from the billing due list, collectively or one by one, or in background jobs, using apps such as Create Billing Documents - Billing Due List Items and Manage Billing Documents (SAP Learning).
The invoice is sent by output management. In S/4HANA, the default is SAP's output management based on BRFplus rules, with print, email, XML and (on-premise) IDoc channels. ECC used the condition technique with table NAST, which can be re-enabled in S/4HANA through Customizing (SAP Learning).
Each invoice becomes an open item on the customer's account, posted through the reconciliation account in the customer master. Payment terms set the due date. When the customer pays, the payment and the invoice are cleared (SAP Learning). Payments arrive by electronic bank statement, lockbox in the US, or manual entry in Post Incoming Payments. A short payment is handled two ways (SAP Learning):
As a partial payment: the invoice stays open.
As a residual item: the invoice is cleared and a new item holds the difference.
Small differences can be written off automatically within tolerance groups.
Payment deductions found while processing incoming payments can create a dispute case at once. A case can also be created from an open item, stays linked to it, and can be closed by a later payment (SAP Learning). Manage Dispute Cases records the reason, root cause, processor and an SLA date (SAP Learning). Collections management gives collectors a worklist ranked by overdue amounts, with promises to pay. It can show a machine-learning "Risk of Late Payments" column once that feature is enabled (SAP Learning).
Returns run backwards through the same chain. In advanced returns management, the returns order is the central document. It leads to a returns delivery, an inspection, a follow-up activity (back to stock, scrap, or ship to the supplier) and a credit memo request that becomes the credit memo. SAP states the advanced returns business functions are always on in S/4HANA (SAP Learning).
flowchart LR
QT[Quotation<br/>20000001] --> SO[Sales order<br/>1000001]
SO --> DL[Delivery<br/>80000001]
DL --> GI[Goods issue<br/>4900000001]
GI --> J1[Journal<br/>COGS vs stock]
DL --> BD[Invoice<br/>90000001]
BD --> J2[Journal<br/>receivable vs revenue]
J2 --> PAY[Payment<br/>1400000001]
Step
Posts to finance
Why
Quotation, sales order
Nothing
A promise. Open order values can count toward credit exposure, but that is credit data, not a journal entry
Delivery, picking
Nothing
Logistics preparation
Goods issue
Cost of goods sold against finished goods inventory, at the stock's valuation
Stock left; the material's valuation sets the amount
Invoice
Customer receivable against revenue (and output tax)
The claim on the customer exists now
Payment
Bank against the receivable; the open item is cleared
The claim is settled
Short payment
Residual item on the customer, or partial payment
The rest is still owed, or disputed
Credit memo (return)
Revenue reduction against the receivable
The claim is reduced
At Calder (fictional), order 1000001 posts 19,600.00 USD of cost at goods issue and 29,100.00 USD of receivable and revenue at billing, then clears the receivable on 2026-10-20. The accounts and the rules that choose them are explained in Document flow and how logistics becomes finance, with its follow_the_money.py script.
As of 23 October 2026, these released interfaces carry each step. The CDS views come from SAP's Cloudification Repository (file read 23 October 2026), which also marks the classic tables VBAK, VBAP, VBEP, LIKP and VBRK as not to be released and names successors such as I_SalesDocument (Cloudification Repository). The OData API names and fields come from SAP's generated API models (SAP Cloud SDK model, 2022).
Step
OData API (A2X)
Released CDS views
What to read
Quotation
API_SALES_QUOTATION_SRV
I_SalesQuotation, I_SalesInquiry
Validity, rejection reasons
Sales order
API_SALES_ORDER_SRV; API_SALES_ORDER_SIMULATION_SRV to price without saving
Navigations to_PrecedingProcFlowDoc, to_SubsequentProcFlowDoc on the sales order
I_SDDocumentMultiLevelProcFlow
The chain from quotation to invoice
The functions are the API's own operations for picking and posting goods issue; use them only in a design where a person triggers the action. For events, SAP's integration training uses sap.s4.beh.salesorder.v1.SalesOrder.Created.v1 and ...Changed.v1 to replicate sales orders through SAP Event Mesh (SAP Learning). Event-driven agents with SAP events shows how to react to them.
You will write one script that does three things. It traces Calder's order 1000001 from quotation to payment and re-checks its prices with a small condition technique. It then runs the order-to-cash gates (pricing, availability, credit, completeness, blocks) on six more orders and an unpaid invoice. Finally, it prints the worklist a team lead would want each morning, and writes it to a file. An optional step counts blocked orders in SAP's sandbox.
flowchart LR
P[calder_pack/<br/>order 1000001] --> S[order_to_cash.py]
SC[Scenario in the script<br/>prices, stock, credit,<br/>orders 1000002-1000007] --> S
S --> T[Trace and<br/>cycle times]
S --> W[Worklist:<br/>what is stuck, who acts]
S --> M[calder_o2c_worklist.md]
K[SAP sandbox<br/>optional] --> S
In VS Code, create a new file, paste the script below, and save it as order_to_cash.py inside the unit1b folder.
Read the blocks PRICE_RECORDS, STOCK, CREDIT_LIMIT and ORDERS first. They are Calder's scenario written as Python data: price records, stock and receipts at plant CP11, credit limits and open orders. Every value is invented. Condition type names such as PRICE and block codes such as HC are the course's own labels, not SAP's.
"""Order to cash at Calder: trace one order end to end and triage the orders that are stuck.
Calder Pumpworks is a fictional company. The pack (unit1b/calder_pack/) and the
scenario below (prices, stock, credit limits, open orders) are made up for the
course. Field names follow SAP's released APIs where one exists; every code,
value and amount is illustrative, not SAP standard and not from any real system.
Run it from your course folder:
python unit1b/order_to_cash.py trace order 1000001, then triage Calder's open orders
python unit1b/order_to_cash.py --order 1000004 show every check for one order
python unit1b/order_to_cash.py --write write unit1b/calder_o2c_worklist.md
python unit1b/order_to_cash.py --sandbox count blocked orders in SAP's sandbox (needs a key)
Uses only Python's built-in modules. Only --sandbox needs a key and the network.
"""
import argparse
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_o2c_worklist.md"
SANDBOX = "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/"
AS_OF = date(2026, 10, 23) # fixed "today", so every run prints the same numbers
# --------------------------------------------------------------- pricing
# A tiny condition technique. Each condition type has an access sequence: a
# list of keys to try, most specific first. The first record found wins.
# Condition type names (PRICE, DISC) are course labels, not SAP's codes.
PRICE_RECORDS = {
("customer/material", "10000001", "CP-ES200"): 4200.00, # Brennock contract price
("customer/material", "10000001", "CP-ES300"): 6150.00,
("channel/material", "10", "CP-ES200"): 4400.00, # list price, direct
("channel/material", "10", "CP-ES300"): 6400.00,
("channel/material", "20", "CP-ES200"): 4400.00, # list price, distributors
("channel/material", "20", "CP-ES300"): 6400.00,
# No record for channel 30 (aftermarket): seal kits have no price there yet.
}
DISCOUNT_RECORDS = {("customer", "10000002"): 0.08} # Hallam: 8% distributor discount
def price_item(customer: str, channel: str, material: str) -> dict:
"""Run the access sequences for one item and say which record was used."""
price, found = None, "no record found"
for table, key in (("customer/material", customer), ("channel/material", channel)):
if (table, key, material) in PRICE_RECORDS:
price = PRICE_RECORDS[(table, key, material)]
found = f"PRICE from {table} {key}/{material}"
break
discount = DISCOUNT_RECORDS.get(("customer", customer), 0.0)
return {"price": price, "discount": discount, "found": found}
# ---------------------------------------------- availability (cumulative ATP)
# Plant CP11, storage location FG01, after order 1000001 has shipped.
STOCK = {"CP-ES200": 12, "CP-ES300": 3, "CP-SK200": 300}
RECEIPTS = {"CP-ES300": [(date(2026, 11, 6), 4)]} # a production order finishing
class Availability:
"""Cumulative ATP with look-ahead: a new order may not take stock that an
earlier confirmed order needs later. Orders are checked in creation order."""
def __init__(self):
self.confirmed = {m: [] for m in STOCK} # material -> [(date, qty)]
def _free(self, material: str, day: date) -> int:
events = sorted({day} | {d for d, _ in RECEIPTS.get(material, [])}
| {d for d, _ in self.confirmed[material]})
free = None
for t in (e for e in events if e >= day):
supply = STOCK[material] + sum(q for d, q in RECEIPTS.get(material, []) if d <= t)
demand = sum(q for d, q in self.confirmed[material] if d <= t)
free = supply - demand if free is None else min(free, supply - demand)
return max(free or 0, 0)
def check(self, material: str, qty: int, wanted: date) -> list:
"""Return schedule lines [(date, confirmed qty)]; date None means unconfirmed."""
lines, open_qty = [], qty
days = [wanted] + sorted(d for d, _ in RECEIPTS.get(material, []) if d > wanted)
for day in days:
take = min(open_qty, self._free(material, day))
if take:
lines.append((day, take))
self.confirmed[material].append((day, take))
open_qty -= take
if not open_qty:
break
if open_qty:
lines.append((None, open_qty))
return lines
# ---------------------------------------------------------- credit (simplified)
# One credit segment for Calder USA. Exposure here = open receivables plus open
# order value; SAP Credit Management has more categories and rules.
CREDIT_LIMIT = {"10000001": 250000.00, "10000002": 40000.00}
OPEN_RECEIVABLES = { # invoice, customer, amount still open (USD)
"90000002": ("10000002", 1150.00), # residual item after a short payment (disputed)
"90000003": ("10000002", 20240.00), # not yet due
}
# ------------------------------------------------------------ open orders
# Orders 1000002 onwards are scenario data for this topic, not in the pack.
# Each tuple: (order, sold-to, channel, division, payment terms, requested date,
# [(item, material, quantity)], delivery block, billing block, shipped)
ORDERS = [
("1000002", "10000002", "20", "PU", "NT45", date(2026, 10, 30), [("10", "CP-ES200", 6)], "", "", False),
("1000003", "10000001", "10", "PU", "", date(2026, 11, 2), [("10", "CP-ES300", 2)], "", "", False),
("1000004", "10000001", "10", "PU", "NT30", date(2026, 10, 27), [("10", "CP-ES300", 5)], "", "", False),
("1000005", "10000002", "30", "SP", "NT45", date(2026, 10, 28), [("10", "CP-SK200", 40)], "", "", False),
("1000006", "10000001", "10", "PU", "NT30", date(2026, 10, 20), [("10", "CP-ES200", 1)], "", "AC", True),
("1000007", "10000001", "10", "PU", "NT30", date(2026, 11, 9), [("10", "CP-ES200", 2)], "HC", "", False),
]
BLOCK_TEXT = {"HC": "customer asked to hold", "AC": "waiting for customer acceptance"}
OWNER = {
"credit block": "Credit analyst (credit decisions)",
"billing data incomplete": "Sales order team, with master data",
"partly confirmed": "Order fulfillment, with production planning",
"price missing": "Pricing team",
"billing block": "Billing clerk, with sales",
"delivery block": "Customer service",
"short payment": "Accounts receivable (dispute case)",
}
def load_pack() -> dict:
try:
return {name: json.loads((PACK / f"{name}.json").read_text(encoding="utf-8"))
for name in ("business_partners", "products", "order_1000001")}
except FileNotFoundError:
sys.exit(f"Cannot read {PACK}. Run python unit1b/make_calder_pack.py from your course folder first.")
def names(pack: dict) -> dict:
return {bp["BusinessPartner"]: bp["BusinessPartnerFullName"]
for bp in pack["business_partners"]["BusinessPartners"]}
def money(x: float) -> str:
return f"{x:,.2f}"
def risk_text(kind: str, amount: float) -> str:
return "unpriced" if kind == "price missing" else money(amount)
# --------------------------------------------- part 1: one order, end to end
def trace_1000001(pack: dict, show: bool = True) -> list:
"""Rebuild order 1000001's story from the pack and re-check its prices."""
docs = pack["order_1000001"]
so, dl, gi, bd = (docs["SalesOrder"], docs["OutboundDelivery"],
docs["MaterialDocument"], docs["BillingDocument"])
pay = next(j for j in docs["JournalEntries"] if j["AccountingDocument"] == "1400000001")
ordered = date.fromisoformat(so["SalesOrderDate"])
issued = date.fromisoformat(gi["PostingDate"])
billed = date.fromisoformat(bd["BillingDocumentDate"])
paid = date.fromisoformat(pay["PostingDate"])
due = billed + timedelta(days=int(so["CustomerPaymentTerms"][2:])) # NT30 -> 30 days, Calder's key
steps = [
("2026-09-08", "Quotation 20000001 (scenario)", "Sales", "Prices offered, valid to 2026-09-30"),
(so["SalesOrderDate"], f"Sales order {so['SalesOrder']}", "Sales",
f"Net {money(so['TotalNetAmount'])} {so['TransactionCurrency']}, terms {so['CustomerPaymentTerms']}"),
(dl["PlannedGoodsIssueDate"], f"Outbound delivery {dl['DeliveryDocument']}", "Shipping",
f"Picked at {dl['ShippingPoint']}, warehouse {dl['Warehouse']}"),
(gi["PostingDate"], f"Goods issue {gi['MaterialDocument']}", "Shipping", "Stock out, cost of goods sold posted"),
(bd["BillingDocumentDate"], f"Invoice {bd['BillingDocument']}", "Billing",
f"Receivable {money(bd['TotalNetAmount'])}, due {due.isoformat()}"),
(pay["PostingDate"], f"Payment {pay['AccountingDocument']}", "Accounts receivable", "Receivable cleared"),
]
if not show:
return steps
print("Order 1000001, end to end (fictional Calder data)\n")
for when, doc, team, what in steps:
print(f" {when} {doc:<32} {team:<20} {what}")
print("\nPricing re-check (condition records in this script vs. the order's net values):")
for item in so["to_Item"]:
p = price_item(so["SoldToParty"], so["DistributionChannel"], item["Material"])
net = round(item["RequestedQuantity"] * p["price"] * (1 - p["discount"]), 2)
flag = "OK" if abs(net - item["NetAmount"]) < 0.01 else "DIFFERS"
print(f" item {item['SalesOrderItem']} {item['Material']}: {p['found']} -> {money(net)} {flag}")
print("\nCycle times:")
print(f" order to goods issue {(issued - ordered).days:>3} days")
print(f" invoice to cash {(paid - billed).days:>3} days (due after {(due - billed).days})")
print(f" order to cash {(paid - ordered).days:>3} days")
return steps
# ----------------------------------------------- part 2: what is stuck, and why
def evaluate(pack: dict) -> list:
"""Run pricing, incompleteness, availability, credit and block checks on every open order."""
atp, rows = Availability(), []
exposure = {c: sum(a for cust, a in OPEN_RECEIVABLES.values() if cust == c) for c in CREDIT_LIMIT}
for order, cust, dc, div, terms, wanted, items, dblock, bblock, shipped in ORDERS:
checks, value, at_risk = [], 0.0, 0.0
for item, mat, qty in items:
p = price_item(cust, dc, mat)
if p["price"] is None:
checks.append(("price missing", f"item {item} {mat}: {p['found']}", 0.0))
continue
net = round(qty * p["price"] * (1 - p["discount"]), 2)
value += net
checks.append(("price", f"item {item} {mat}: {p['found']}, net {money(net)}", None))
if shipped:
continue
lines = atp.check(mat, qty, wanted)
text = ", ".join(f"{q} on {d.isoformat()}" if d else f"{q} unconfirmed" for d, q in lines)
short = sum(q for d, q in lines if d != wanted)
if short:
at_risk_item = round(short * net / qty, 2)
checks.append(("partly confirmed", f"item {item}: {text}", at_risk_item))
else:
checks.append(("availability", f"item {item}: {text}", None))
if not terms:
checks.append(("billing data incomplete", "payment terms missing: can deliver, cannot bill", value))
if value and not shipped:
after = exposure[cust] + value
if after > CREDIT_LIMIT[cust]:
checks.append(("credit block", f"exposure {money(after)} > limit {money(CREDIT_LIMIT[cust])}", value))
else:
checks.append(("credit", f"exposure {money(after)} within limit {money(CREDIT_LIMIT[cust])}", None))
exposure[cust] = after
if dblock:
checks.append(("delivery block", f"reason {dblock}: {BLOCK_TEXT[dblock]}", value))
if bblock:
checks.append(("billing block", f"reason {bblock}: {BLOCK_TEXT[bblock]}", value))
problems = [c for c in checks if c[2] is not None]
if problems:
at_risk = max(c[2] for c in problems)
rows.append({"order": order, "customer": cust, "wanted": wanted, "value": value,
"checks": checks, "problems": problems, "at_risk": at_risk})
# Receivables side: a short payment left a residual item that is now a dispute.
rows.append({"order": "invoice 90000002", "customer": "10000002", "wanted": date(2026, 10, 15),
"value": 1150.00, "checks": [], "at_risk": 1150.00,
"problems": [("short payment", "paid 1,150.00 less than invoiced; residual item open", 1150.00)]})
return rows
def worklist(rows: list) -> list:
"""Stuck items, most urgent first: past or nearest requested date, then most cash."""
stuck = [r for r in rows if r["problems"]]
return sorted(stuck, key=lambda r: (r["wanted"], -r["at_risk"]))
def print_worklist(pack: dict, rows: list) -> None:
who = names(pack)
items = worklist(rows)
print(f"\nOrder-to-cash worklist as of {AS_OF.isoformat()} (fictional Calder data)\n")
print(f" {'#':<2} {'Document':<17} {'Customer':<23} {'Wanted':<10} {'Problem':<24} {'At risk':>10} Owner")
for n, r in enumerate(items, start=1):
first = r["problems"][0]
late = " (past)" if r["wanted"] < AS_OF else ""
print(f" {n:<2} {r['order']:<17} {who[r['customer']][:23]:<23} {r['wanted'].isoformat():<10}"
f" {first[0]:<24} {risk_text(first[0], r['at_risk']):>10} {OWNER[first[0]]}{late}")
total = sum(r["at_risk"] for r in items)
print(f"\n {len(items)} items stuck, {money(total)} USD of priced value delayed. Order 1000001 is complete.")
def show_order(pack: dict, rows: list, number: str) -> int:
row = next((r for r in rows if r["order"] == number), None)
if not row:
print(f"Order {number} is not in the scenario. Try one of: "
+ ", ".join(r["order"] for r in rows if r["order"].isdigit()))
return 1
print(f"Order {number} for {names(pack)[row['customer']]}, wanted {row['wanted'].isoformat()},"
f" value {money(row['value'])} USD\n")
for kind, text, risk in row["checks"]:
mark = "STUCK" if risk is not None else "OK "
print(f" {mark} {kind:<24} {text}")
if row["problems"]:
print(f"\n Next: {OWNER[row['problems'][0][0]]}.")
return 0
# --------------------------------------------------------------- deliverable
def write_markdown(pack: dict, steps: list, rows: list) -> None:
who = names(pack)
fence = "`" * 3
out = ["# Calder order to cash: one order traced, open orders triaged", "",
"All data is fictional (Calder Pumpworks sample pack and the scenario in order_to_cash.py).", "",
fence + "mermaid", "flowchart LR",
" Q[Quotation] --> SO[Sales order<br/>price, ATP, credit] --> DL[Delivery<br/>pick, pack]",
" DL --> GI[Goods issue<br/>cost posted] --> BD[Invoice<br/>receivable posted] --> PAY[Payment<br/>cleared]",
" BD -.-> DSP[Dispute<br/>if short paid]", fence, "",
"## Order 1000001", "", "| Date | Document | Team | What happened |", "| --- | --- | --- | --- |"]
out += [f"| {w} | {d} | {t} | {x} |" for w, d, t, x in steps]
out += ["", f"## Worklist as of {AS_OF.isoformat()}", "",
"| # | Document | Customer | Wanted | Problem | Detail | At risk (USD) | Owner |",
"| --- | --- | --- | --- | --- | --- | ---: | --- |"]
for n, r in enumerate(worklist(rows), start=1):
kind, text, _ = r["problems"][0]
out.append(f"| {n} | {r['order']} | {who[r['customer']]} | {r['wanted'].isoformat()} | {kind} |"
f" {text} | {risk_text(kind, r['at_risk'])} | {OWNER[kind]} |")
out += ["", "Synthetic numbers: they show the method, not results from an SAP system.", ""]
OUT.write_text("\n".join(out), encoding="utf-8")
print(f"wrote unit1b/{OUT.name}")
# ------------------------------------------------------------------ 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 orders with blocks in SAP's demo data. Codes are printed as they come,
because their meanings are configuration: read them, don't assume them."""
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,SalesOrderType,TotalNetAmount,TransactionCurrency,OverallSDProcessStatus,"
"TotalCreditCheckStatus,DeliveryBlockReason,HeaderBillingBlockReason")
url = SANDBOX + f"API_SALES_ORDER_SRV/A_SalesOrder?$top=50&$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:
orders = 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 below.")
except urllib.error.URLError as err:
sys.exit(f"Cannot reach the sandbox: {err.reason}. Check your network or proxy, or use the pack.")
print(f"SAP sandbox: {len(orders)} sales orders read (SAP demo data, not Calder's)\n")
for field in ("OverallSDProcessStatus", "TotalCreditCheckStatus"):
counts = {}
for so in orders:
counts[so.get(field) or "(empty)"] = counts.get(so.get(field) or "(empty)", 0) + 1
print(f" {field}: " + ", ".join(f"'{k}' x{v}" for k, v in sorted(counts.items())))
for field in ("DeliveryBlockReason", "HeaderBillingBlockReason"):
blocked = [so["SalesOrder"] for so in orders if so.get(field)]
print(f" {field} set on {len(blocked)} orders" + (f", e.g. {', '.join(blocked[:3])}" if blocked else ""))
return 0
def main() -> int:
parser = argparse.ArgumentParser(description="Trace and triage Calder's order to cash.")
parser.add_argument("--order", help="show every check for one order, e.g. 1000004")
parser.add_argument("--write", action="store_true", help="write the worklist to unit1b/calder_o2c_worklist.md")
parser.add_argument("--sandbox", action="store_true", help="count blocked orders in SAP's sandbox")
args = parser.parse_args()
if args.sandbox:
return sandbox()
pack = load_pack()
rows = evaluate(pack)
if args.order:
return show_order(pack, rows, args.order)
if args.write:
write_markdown(pack, trace_1000001(pack, show=False), rows)
return 0
trace_1000001(pack)
print_worklist(pack, rows)
return 0
if __name__ == "__main__":
sys.exit(main())
You should see exactly this. The script has no random parts, so your numbers must match:
Order 1000001, end to end (fictional Calder data)
2026-09-08 Quotation 20000001 (scenario) Sales Prices offered, valid to 2026-09-30
2026-09-14 Sales order 1000001 Sales Net 29,100.00 USD, terms NT30
2026-09-25 Outbound delivery 80000001 Shipping Picked at RK01, warehouse RKDC
2026-09-25 Goods issue 4900000001 Shipping Stock out, cost of goods sold posted
2026-09-26 Invoice 90000001 Billing Receivable 29,100.00, due 2026-10-26
2026-10-20 Payment 1400000001 Accounts receivable Receivable cleared
Pricing re-check (condition records in this script vs. the order's net values):
item 10 CP-ES200: PRICE from customer/material 10000001/CP-ES200 -> 16,800.00 OK
item 20 CP-ES300: PRICE from customer/material 10000001/CP-ES300 -> 12,300.00 OK
Cycle times:
order to goods issue 11 days
invoice to cash 24 days (due after 30)
order to cash 36 days
Order-to-cash worklist as of 2026-10-23 (fictional Calder data)
# Document Customer Wanted Problem At risk Owner
1 invoice 90000002 Hallam Fluid Systems 2026-10-15 short payment 1,150.00 Accounts receivable (dispute case) (past)
2 1000006 Brennock Water Utility 2026-10-20 billing block 4,200.00 Billing clerk, with sales (past)
3 1000004 Brennock Water Utility 2026-10-27 partly confirmed 24,600.00 Order fulfillment, with production planning
4 1000005 Hallam Fluid Systems 2026-10-28 price missing unpriced Pricing team
5 1000002 Hallam Fluid Systems 2026-10-30 credit block 24,288.00 Credit analyst (credit decisions)
6 1000003 Brennock Water Utility 2026-11-02 billing data incomplete 12,300.00 Sales order team, with master data
7 1000007 Brennock Water Utility 2026-11-09 delivery block 8,400.00 Customer service
7 items stuck, 74,938.00 USD of priced value delayed. Order 1000001 is complete.
How to read it:
The trace rebuilds the chain from the pack and adds the scenario's quotation. The pricing re-check runs the script's access sequence for each item and finds Brennock's contract price first, matching the order's net values.
Cycle times are the process's own KPIs. Brennock paid 24 days after the invoice, 6 days before the due date, and the whole order took 36 days from order to cash.
The worklist is sorted by the date the customer wanted, then by value. Past dates are marked (past). Each line names one owner, because a blocked order nobody owns stays blocked.
Order 1000004 for Brennock Water Utility, wanted 2026-10-27, value 30,750.00 USD
OK price item 10 CP-ES300: PRICE from customer/material 10000001/CP-ES300, net 30,750.00
STUCK partly confirmed item 10: 1 on 2026-10-27, 4 on 2026-11-06
OK credit exposure 43,050.00 within limit 250,000.00
Next: Order fulfillment, with production planning.
Order 1000003 was entered first and needs two ES-300 pumps on 2026-11-02. Three are in stock, so only one is free on 2026-10-27 for 1000004: the check looks ahead and protects the earlier order's confirmation. The other four wait for the production receipt on 2026-11-06. That is cumulative ATP in miniature, and the reason a planner, not a sales clerk, owns this line.
Now try --order 1000002. The credit gate fails:
Order 1000002 for Hallam Fluid Systems, wanted 2026-10-30, value 24,288.00 USD
OK price item 10 CP-ES200: PRICE from channel/material 20/CP-ES200, net 24,288.00
OK availability item 10: 6 on 2026-10-30
STUCK credit block exposure 45,678.00 > limit 40,000.00
Next: Credit analyst (credit decisions).
Hallam's open receivables (21,390.00 USD, including the disputed residual item) plus the new order exceed its 40,000 USD limit. Availability is fine, so the analyst's decision is the only thing holding the pumps. Try --order 1000003, 1000005, 1000006 and 1000007 too.
You should see wrote unit1b/calder_o2c_worklist.md.
Open the file in VS Code and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS) to preview it. You should see a process diagram, the trace of order 1000001 and a seven-line worklist with an owner on every line.
#Step 7 (optional): Count blocked orders in SAP's sandbox
This step reads SAP's demo data, not Calder's. It needs your sandbox key and the network.
Make sure .env in the course folder has a line SAP_API_KEY="...".
Run:
Windows (PowerShell):
python unit1b\order_to_cash.py --sandbox
macOS / Linux:
python unit1b/order_to_cash.py --sandbox
The output has this shape. Values in angle brackets come from SAP's demo data and change over time:
SAP sandbox: <n> sales orders read (SAP demo data, not Calder's)
OverallSDProcessStatus: '<code>' x<count>, '<code>' x<count>
TotalCreditCheckStatus: '(empty)' x<count>, '<code>' x<count>
DeliveryBlockReason set on <count> orders, e.g. <number>, <number>
HeaderBillingBlockReason set on <count> orders
The script prints the status codes as they come and doesn't translate them, because their meanings are configuration. A valid "empty" result is set on 0 orders: the first 50 demo orders simply have no blocks. The sandbox is read-only, so you can't release or change anything there.
Where it sits. At Calder (fictional), order to cash for CP10 runs inside S/4HANA. Customer orders come in by EDI from equipment makers and distributors, and by PDF from utilities. The CRM holds opportunities and quotes for engineered pumps. Payments arrive as bank statements. Anything that triages or explains reads released interfaces and writes only to a worklist; decisions go back through SAP's own apps.
flowchart LR
EDI[EDI and PDF<br/>customer orders] --> S4[S/4HANA CP10<br/>orders, ATP, credit,<br/>billing, receivables]
CRM[CRM<br/>opportunities, quotes] --> S4
BANK[Bank statements] --> S4
S4 -->|released APIs, CDS, events| TRI[Triage service<br/>read-only]
TRI --> WL[Worklist per owner]
WL -->|person decides in SAP app| S4
ECC[ECC CP20] -.->|intercompany| S4
Non-functional requirements that decide the design
Requirement
Typical target
If missed
Order entry response
Pricing, ATP and credit check within seconds per order
Sales staff work around the system; EDI queues back up
Peak volume
Month-end and quarter-end billing runs finish in their batch window
Invoices slip into the next period; revenue and DSO suffer
Availability
Order entry available in all selling hours, EDI retried on failure
Lost or duplicate orders
Credit decision time
Blocked orders reach an analyst the same day
Pumps wait at the dock; customers escalate
Security
Readers see only the sales areas and company codes they are authorized for
Prices, margins and credit data exposed through a convenience tool
Auditability
Every release, rejection and credit change is logged with who and why
Audit findings; no basis for evaluating any AI recommendation
Integration and identity. Customer orders arrive by EDI as IDoc message types such as ORDERS, with ORDRSP for confirmations and INVOIC for invoices; SAP's Business Network training lists them from the buyer's side (SAP Learning). In cloud editions, released OData and SOAP APIs replace much of this. The triage service subscribes to sales order events and reads the order, credit and delivery views. It reads with the requesting user's identity where possible, so SAP's authorizations on sales area and company code apply (see grounding with authorizations). Integration patterns are covered in how SAP systems talk.
Context: Calder's credit analysts find blocked orders by running a report twice a day. Distributors complain that orders wait a day for release. CP20 runs ECC with the old credit management.
Options: (a) credit check at order entry only; (b) at order entry and again at goods issue; (c) at delivery creation only.
Decision: (b) for CP10. Order entry catches risk early, while the check at goods issue catches exposure that grew between order and shipment. Blocked orders feed a worklist the same hour.
Consequences: Some orders are checked twice, and the analysts need a clear rule for orders released at entry but blocked at goods issue. Group credit across CP10 and CP20 is manual until CP20 migrates.
What a design review will ask
Where are credit checks placed, and what does a customer experience when a check fails at goods issue?
Which ATP features are licensed in your edition, and what happens when a confirmed receipt slips?
Which channels send orders and invoices (EDI, email, portal), and who monitors failed messages?
Can any component release, reject, reprice or rebill without a person's decision?
How will you measure whether triage helped: block age, DSO, on-time delivery?
SAP lists AI-assisted sales order creation from unstructured purchase orders as GA in S/4HANA Cloud Public Edition, Q1 2026 (SAP News); see Document AI and SAP
EDI orders
Plain automation is enough
Structured already; monitor failures instead
Pricing
Plain automation is enough
Weak for AI
The condition technique is deterministic and auditable; AI can explain why a record was found
Gather exposure, payment history and order facts, recommend; a person releases. The FDE topic uses this example
Billing
Plain automation is enough
Exceptions (blocks, incompleteness) feed the same triage
Cash application
Match
Strong
SAP Cash Application matches bank items to open invoices (SAP Learning)
Disputes
Classify and retrieve
Strong
Classify the reason and gather the invoice, delivery and price evidence; SAP's Dispute Resolution Agent was a beta release for S/4HANA Cloud Public Edition in Q1 2026 (SAP News); as of October 2026, SAP lists it in the Accounts Receivable Assistant, with general availability rolling out in phases (SAP News)
Collections
Predict
Moderate
Payment-delay risk ranks the collector's worklist; Calder's data is too small to train on
The line is the same at every step: AI reads, explains, ranks and suggests; a person releases credit, changes prices, cancels invoices and writes off amounts. Human approval in AI workflows and agent permissions show how to enforce that in code.
Treating every block as a defect. Teams that measure "blocks per day" push to loosen controls. Measure block age and decision quality instead.
Reading statuses as universal codes. Credit, delivery and billing status values and block reasons are configured. Read them from the system.
Ignoring the look-ahead in ATP. "There are three in stock" doesn't mean three can be promised; earlier orders may already hold them.
One credit check, wrong place. Checking only at order entry misses exposure that grew before shipping; checking only at goods issue surprises the warehouse.
Testing the happy path. The costly cases are partial confirmations, short payments and returns. Put them in the test plan.
Triage without an owner. A worklist line with no owner is a report, not a process.
Assuming ECC and S/4HANA match. Credit management, business partners and output management differ, which matters for Calder's CP20.
Turn the worklist into a triage note a Calder team lead could use, and see how one fix exposes the next gate.
Run python unit1b/order_to_cash.py --write (Windows: python unit1b\order_to_cash.py --write). Copy unit1b/calder_o2c_worklist.md to unit1b/calder_o2c_triage.md.
In order_to_cash.py, add this line inside PRICE_RECORDS, just after the comment about channel 30:
("channel/material", "30", "CP-SK200"): 62.00, # aftermarket seal kit price
Run python unit1b/order_to_cash.py --order 1000005. The price gate now passes. Note which gate fails instead and why.
In the copy, add a column Next action to the worklist table. For each line write one sentence: what the owner should check or decide.
Add a column AI could with one of: extract, explain, predict, match or nothing. Use the table under Where AI fits to choose.
Under a heading One fix, next gate, write two lines on what you saw in Step 3.
Under a heading ECC note, write one line on what a credit analyst would need to see for a customer that buys from both CP10 and CP20.
Remove the line you added in Step 2, preview the file, and commit it with Git.
Done when:unit1b/calder_o2c_triage.md has the seven-line worklist with Next action and AI could filled in for every line, a One fix, next gate section that names the credit block on 1000005 and the exposure above Hallam's 40,000.00 USD limit, and an ECC note. Keep it: the capstone at the end of Unit 1B traces one exception across the whole enterprise, and this triage note is its order-to-cash part.
Pick one answer for each question. The explanation appears after you choose.
1In the script, order 1000004 asks for five ES-300 pumps on 2026-10-27 with three in stock. Why is only one confirmed for that date?
Answer: A. Cumulative ATP looks ahead. Two of the three pumps are promised to an earlier order on a later date, so only one is free now; the rest waits for the receipt on 2026-11-06.
2An order item has no price because no condition record matches. Where do you look first?
Answer: C. The condition technique tries the access sequence's tables in order and stops at the first valid record. A missing price means no table holds a record for this customer, channel and material key.
3You must list credit-blocked sales orders from custom code in an S/4HANA cloud edition. What do you read?
Answer: B. The Cloudification Repository lists I_CreditBlockedSalesDocument as released and marks VBAK as not to be released. Released views survive upgrades; table access in custom code doesn't fit clean core.
4The sandbox shows TotalCreditCheckStatus values you haven't seen before. What should your code do?
Answer: C. Status code values are SAP domain values and can depend on configuration. Hard-coded lists break silently; reading the system's own values keeps triage correct.
5Hallam pays an invoice 1,150 USD short and the difference is posted as a residual item. What is true?
Answer: A. With a residual item, the original invoice is cleared and the difference becomes a new open item, which a dispute case can track. A partial payment would instead leave the invoice open.
6Calder plans credit checks at order entry and again at goods issue. What consequence must the design handle?
Answer: C. Exposure can grow between order and shipment, so the second check can block an order that passed the first. Analysts and the warehouse need a rule for that case.
7What does S/4HANA change for credit management compared with Calder's ECC company CP20?
Answer: D. SAP's simplification guidance states FI-AR-CR is not available in S/4HANA and SAP Credit Management replaces it, with business partners and credit segments. CP20 on ECC still uses the old data.
8Your triage agent finds that order 1000002 is blocked only for credit and Hallam has paid on time for a year. What should it do?
Answer: B. Releasing credit and changing limits are decisions with an audit trail, made by a credit analyst. The agent's job is to put the evidence and a recommendation in front of that person.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
Orchestrate Lab is a free practice ERP in your browser, built around the fictional Calder Pumpworks. These are the places in it where you do what this topic teaches.
Stage 3 of 11Configure sellingThe whole stage practices this topic, on Pricing procedure steps, Pricing procedure assignment and Credit check settings.
Stage 8 of 11Take ordersThe whole stage practices this topic, on Sales orders and Price conditions.
Stage 9 of 11Deliver and shipThe whole stage practices this topic.
Stage 10 of 11Invoice and collectThe whole stage practices this topic, on Billing and Incoming payments.
Stage 1 of 11Found the companyPractice it on Sales organizations and Sales areas.
Stage 2 of 11Set up financePractice it on Payment terms and Credit segments.
ChapterQualityInspect what Calder buys before anyone uses it: an inspection setup and characteristics for seal kits, goods receipts that go to quality inspection and open inspection lots, results measured on a sample, and the usage decision that releases a good delivery to unrestricted stock or blocks a bad one, while planning counts only what can be used. Then the final inspection of a pump Calder builds: ES-300 tested at production receipt before anyone can sell it, and an order that confirms only the pumps that passed until a recheck after the decision. Then the complaint: a supplier's invoice blocked for quality, a quality notification, a return delivery and the credit memo that settles it. Then the supplier's replacement inspected and its invoice released by the decision, and the supplier's quality record read for what it can and can't say. Last, ES-300's final inspection fed by the test bench: one measurement per pump, recorded once against its serial number.
ChapterTransportationPlan Calder's freight from Rockford: carriers and their agreements, freight units from deliveries, freight orders, the carrier chosen by cost, loads with stops, truck capacity and a departure worked back from the dates, the final charges with stops and detention after free time, tendering each load to a carrier that has to accept it, following the truck by its execution events, and settling the freight: each carrier invoice checked against the agreed charges, the freight cost shared among the deliveries so each customer's margin shows what its freight really cost, and inbound freight on purchased motors planned on the purchase order and carried into the motors' cost.
ChapterWarehouseLay out Calder's Rockford distribution center: storage types, sections and bins, then put stock away, pick, pack and count it, put people on the work and watch it.
Stages build on each other: a stage link opens the core path at that stage. To start there without doing the earlier stages yourself, use Do this stage for me on the stage before it. A chapter starts inside your own company once the core path is done (or the lab finishes the core for you first); drills and exercises start from their own prepared copy. Everything is saved in this browser only.
Sources
Executing a Standard Sales from Stock Process (SAP Learning)— sell from stock (BD9) steps from sales order with pricing and ATP check through picking, goods issue, invoice and payment; apps Manage Sales Orders - Version 2, Sales Order Fulfillment Issues, Manage Billing Documents
Introducing the Condition Technique (SAP Learning)— pricing procedure determined from sales area, document and customer pricing procedures; condition types, access sequences, condition tables and records; search stops at the first valid record
Running an Available-to-Promise (ATP) Check in SAP S/4HANA Sales (SAP Learning)— check against cumulative ATP quantity in the delivering plant; confirmed, partly confirmed and unconfirmed schedule lines; only confirmed lines can be delivered; aATP always active in Public Edition with Basic (standard license) and Advanced (additional license); activation and additional license in Private Edition and on-premise
Working with SAP Credit Management (SAP Learning)— FI-AR-CR versus SAP Credit Management; only credit check type D or none for sales document types; BP role UKM000; credit segments; basic credit management without extra license, advanced functions licensed
Cloudification Repository (SAP on GitHub)— objectReleaseInfoLatest.json read 23 October 2026; I_SalesQuotation, I_SalesOrder, I_SalesOrderItem, I_SalesOrderScheduleLine, I_CreditBlockedSalesDocument, I_CreditManagementAccount, I_OutboundDelivery, I_BillingDocument, I_OperationalAcctgDocItem, I_CustomerReturn released; VBAK, VBAP, VBEP, LIKP, VBRK not to be released, with successors
SAP Cloud SDK virtual data model for the sales order service (npm, @sap/cloud-sdk-vdm-sales-order-service 2.1.0)— SAP's generated model of API_SALES_ORDER_SRV (2022), read 23 October 2026 with the matching quotation, outbound delivery, billing document, customer return and product availability packages; header fields TotalCreditCheckStatus, DeliveryBlockReason, HeaderBillingBlockReason; schedule line ConfdOrderQtyByMatlAvailCheck and DelivBlockReasonForSchedLine; delivery functions pickAllItems and postGoodsIssue