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.

Document flow and how logistics becomes finance

Follow one order from sales order to cash, read its document flow, and see why goods movements and invoices post to finance on their own, and where that breaks.

Updated Nov 1, 2026Foundational 9 minDeep 40 min
Foundational layer · 9 min read

The 60-second version

In SAP, one business transaction leaves a trail of documents. A sales order leads to a delivery, the delivery to a goods issue, the goods issue and the delivery to an invoice, and the invoice to a payment. Each document points back to the one it came from. That chain is the document flow.

Some of those steps also change the company's books, and nobody types a journal entry for them. When stock leaves the warehouse, finance sees inventory go down and cost of goods sold go up. When the invoice is saved, finance sees a receivable and revenue. A set of rules, maintained once, decides which accounts each step hits. That is automatic account determination.

Three ideas carry this topic:

  • Not every step posts. A sales order or a purchase order is a promise; it changes nothing in the books. Goods movements, invoices and payments do.
  • The rules are the integration. Logistics teams never pick G/L accounts. The rules pick them from the plant, the material, the customer and the kind of movement. A wrong rule is wrong for every document that uses it.
  • The chain is the evidence. Any number in a finance report can be traced back through documents to the order, delivery or receipt that caused it. Auditors, controllers and AI agents all rely on that.

Why it matters to the business

Most "finance problems" at month-end start in logistics. Three examples from Calder Pumpworks, the fictional pump maker this unit follows. The numbers come from the topic's script and are synthetic.

What happened Where it started What finance sees
A goods issue for six pumps never reached finance A posting failed in the warehouse step Revenue of 29,100 USD with no cost: the order shows a 100% margin instead of about a third
Invoice revenue landed on the wrong account One rule in the revenue account setup Pump sales reported under the wrong line, every month, until someone notices
Motors received but not yet invoiced A supplier is slow to bill 9,500 USD sitting on the goods receipt / invoice receipt clearing account at month-end

The business value is speed and trust. When postings follow the documents automatically, the books are current every day and closing is mostly checking, not keying. When the chain breaks, people spend the close chasing differences between what logistics did and what finance shows. SAP's own description of the Universal Journal, its finance data model in S/4HANA, stresses removing reconciliation work between finance and controlling (SAP Learning). It cannot remove differences that start in logistics.

How SAP does it

As of October 2026, four pieces work together in SAP S/4HANA:

  • Document flow. Each sales document records what came before and after it. SAP's sell-from-stock training describes a process flow view on the sales order that shows related documents, including deliveries, billing documents and journal entries, with their status (SAP Learning). Purchasing keeps a similar purchase order history of receipts and invoices.
  • Postings at the right steps. The sales order posts nothing in finance. The goods issue creates a material document and an accounting document; billing creates the receivable and the revenue (SAP Learning).
  • Account determination rules. For inventory, the plant, the material's valuation class and the business transaction pick the accounts (SAP Learning). For revenue, a separate set of rules uses SAP's condition technique (SAP Learning).
  • One journal. In S/4HANA, general ledger, controlling, asset accounting, profitability analysis and the material ledger write to one table, the Universal Journal (SAP Learning). In SAP ERP (ECC), these lived in separate tables that had to be reconciled (SAPinsider, by SAP's Birgit Starmanns).

Follow one order: what each step posts

Calder's order 1000001 (fictional): Brennock Water Utility buys four ES-200 and two ES-300 pumps.

flowchart LR
  SO[Sales order<br/>1000001] --> DL[Delivery<br/>80000001]
  DL --> GI[Goods issue<br/>4900000001]
  GI --> J1[Journal entry<br/>cost of goods sold]
  DL --> BD[Invoice<br/>90000001]
  BD --> J2[Journal entry<br/>receivable, revenue]
  J2 --> J3[Payment<br/>clears receivable]
Step Document Posts to finance? Debit Credit Amount (USD)
Customer orders Sales order No
Warehouse prepares Outbound delivery No
Pumps leave the dock Goods issue (material document) Yes Cost of goods sold Finished goods inventory 19,600.00
Customer is billed Billing document Yes Receivable from the customer Revenue 29,100.00
Customer pays Payment Yes Bank Receivable from the customer 29,100.00

Purchasing works the same way in reverse. A purchase order posts nothing. The goods receipt raises stock and credits a GR/IR clearing account (goods receipt / invoice receipt). The supplier invoice debits that clearing account and credits the supplier. When both have happened for the same amount, the clearing account nets to zero. If only one has happened, the open balance is a signal someone must explain.

Questions to ask

  1. Who owns account determination: finance, logistics or both? Who signs off a change, and how is it tested?
  2. For each new process or movement type, which accounting document should it produce? Is that in the test script?
  3. How many items sit on the GR/IR clearing account at month-end, how old are they, and who clears them (SAP Learning)?
  4. Can every figure in the margin report be traced back to documents in a few clicks? Who has tried?
  5. Are any invoices or goods movements waiting to post to finance today, and who watches for them?
  6. In a mixed landscape (Calder's ECC and S/4HANA), where do the two sets of books meet, and who reconciles them?
  7. If an AI agent explains a variance, does it cite the document chain, and can a person check each link?

Common misconceptions

  • "Finance posts the logistics transactions." No one keys them. The rules post them when the logistics step is saved. Finance owns the rules and the review.
  • "The Universal Journal means no more reconciliation." It removes reconciliation between finance and controlling tables. It does not catch a goods issue that never posted or a supplier who never billed.
  • "The sales order is the first financial event." It is a commitment, not a posting. Cost appears at goods issue and revenue at billing.
  • "A balanced journal entry is a correct one." Every entry balances by design. It can still hit the wrong account or the wrong amount.
  • "Document flow is only for support desks." It is the audit trail. Controllers, auditors and AI explanations all depend on it.

Key terms

All course terms, A to Z, in the glossary

  • Document flow: the links between the documents of one transaction, each pointing to its predecessor and successors.
  • Material document: the logistics record of a goods movement, such as a goods issue or goods receipt.
  • Journal entry (accounting document): the finance record of a posting, with debit and credit lines that add up to zero.
  • Automatic account determination: rules that choose G/L accounts from organizational units, master data and the type of transaction.
  • Valuation class: a material setting that groups materials for account determination.
  • Transaction key: SAP's code for a kind of posting, such as BSX for stock or WRX for GR/IR clearing.
  • GR/IR clearing account: a temporary account between goods receipt and supplier invoice.
  • Universal Journal: the single S/4HANA table (ACDOCA) for finance and controlling line items.

Check yourself

Pick one answer for each question. The explanation appears after you choose.

1Calder saves sales order 1000001 for six pumps. What appears in finance at that moment?

Answer: D. The sales order posts nothing in finance. Cost appears when the goods issue is posted, and the receivable and revenue appear when the invoice is saved.

2An order shows revenue of 29,100 USD and no cost. What is the most likely cause?

Answer: C. Cost of goods sold comes from the goods issue. If that posting is missing, revenue stands alone and the margin looks perfect, which should raise a question, not a celebration.

3Who should approve a change to account determination?

Answer: C. The rules translate logistics events into finance. A change affects both sides, so both should sign off and test cases should check the accounting document.

4At month-end, Calder's GR/IR clearing account has a 9,500 USD balance for motors. What does it most likely mean?

Answer: D. Goods receipt credits the clearing account and the invoice debits it. A balance left over means one side is missing or the amounts differ, and someone must explain it.

5What does the Universal Journal in S/4HANA remove, compared with ECC?

Answer: D. One table for finance and controlling removes reconciliation between them. Missing goods issues or late supplier invoices still create differences someone has to explain.

6A vendor demo shows an AI agent explaining a margin drop. What should you ask first?

Answer: D. The document chain is the evidence. An explanation a person can check document by document is worth more than a fluent one, and posting corrections should stay with people.
Deep layer · 40 min read

Mental model

Hold two chains side by side.

The document chain records what happened: order, delivery, goods issue, invoice, payment. Each document stores references to the one before it, so you can walk forward or backward.

The value chain records what it was worth: journal entries with debits and credits. Only some documents create one.

Account determination is the translation table between the two. It takes facts the logistics document already has (plant, material, movement type, customer, sales organization) and returns G/L accounts. Nobody chooses an account at the dock or the billing desk. So when a number in finance looks wrong, there are only three places to look: the document (wrong quantity or price), the rules (wrong account), or the link (a posting that never happened).

How it works

Before you start: this topic builds on The SAP data model for AI engineers (header and item, released views, ACDOCA versus BSEG) and on Master data that every module depends on. It doesn't repeat them.

Document flow: references in both directions

Every follow-on document copies data from its predecessor and stores the predecessor's number. In the Calder pack, a delivery item carries ReferenceSDDocument and ReferenceSDDocumentItem pointing to the sales order item, and a billing item points to the delivery and to the original sales order. SAP also keeps the links in a separate document flow store, so you can go forward as well as back.

In S/4HANA, that store is table VBFA. SAP's Cloudification Repository marks VBFA as not to be released and names the released CDS view I_SDDocumentMultiLevelProcFlow as its successor (Cloudification Repository, file read 23 October 2026). Outside the system, SAP's model of the Sales Order API (API_SALES_ORDER_SRV) has navigations from an order to to_PrecedingProcFlowDoc and to_SubsequentProcFlowDoc, with fields such as SubsequentDocument, SubsequentDocumentCategory and ProcessFlowLevel (SAP Cloud SDK model, 2022).

Purchasing records the same chain differently. A purchase order item keeps its history: each goods receipt and invoice against it. The released view I_PurchaseOrderHistoryAPI01 exposes it. Material documents carry the purchase order and item they received against (PurchaseOrder, PurchaseOrderItem in the material document API model).

Which steps post

Process Step Logistics document Finance posting
Order to cash Sales order Sales order None
Delivery and picking Outbound delivery None
Goods issue Material document Cost of goods sold against stock
Billing Billing document Receivable against revenue (and tax)
Incoming payment None Bank against receivable
Procure to pay Purchase order Purchase order None
Goods receipt Material document Stock against GR/IR clearing
Invoice receipt Supplier invoice GR/IR clearing against supplier (and tax)
Payment None Supplier against bank

SAP's sales and finance training describes these steps: the sales order posts nothing in FI, the goods issue creates a material document plus an accounting document, and billing posts receivables and revenue (SAP Learning). The goods issue copies its data from the delivery and can't be edited itself; you change the delivery (SAP Learning).

sequenceDiagram
  participant L as Logistics step
  participant R as Account determination
  participant J as Journal
  L->>R: plant, material, movement type
  R->>R: valuation class + transaction key
  R-->>L: G/L accounts
  L->>J: journal entry, linked to the logistics document

Inventory account determination

SAP's materials management training lists four inputs (SAP Learning):

  1. Chart of accounts, from the company code. Rules are set per chart, because account numbers mean different things in each.
  2. Valuation grouping code, from the plant's valuation area. Plants that should post alike share a code.
  3. Valuation class, from the material. Materials that should post alike share a class.
  4. The business transaction: the movement type for goods movements, or the invoice verification step.

The transaction is broken into transaction keys, one per kind of line. SAP's training names, among others (SAP Learning):

Key Meaning Where Calder meets it
BSX Stock posting Every receipt into and issue from valuated stock
WRX GR/IR clearing Goods receipt and invoice for a purchase order
PRD Price differences Standard-price materials received or invoiced at another price
KDM Exchange rate differences CP10 buying from CP20 in EUR
GBB Offsetting entry for inventory postings The cost side of goods issues, scrapping, inventory differences

GBB is used by several unrelated events, so SAP adds an account grouping code (also called the general modification) taken from the movement type. It lets a goods issue for a delivery, a scrapping and an inventory difference post to different accounts (SAP Learning). In the Calder script, the course label SALE stands in for the code that a goods issue for a delivery carries; look up the real one in your system.

A value string (for example WE01 for goods receipts) lists which keys a transaction uses. It contains keys, not account numbers, which is what lets one rule set serve many charts of accounts. Customizing has a simulation in the activity Configure Automatic Postings that shows which accounts a material or valuation class would hit (SAP Learning).

Material valuation decides the amount

Account determination picks the account; valuation picks the amount. The material's price control, set per plant, decides (SAP Learning):

  • Standard price: receipts and issues all use one fixed price. Differences go to a price difference account.
  • Moving average price: receipts post at their actual value and the average is recalculated; issues use the current average.

SAP's worked example: 10 pieces ordered at EUR 12 for a material with a standard price of EUR 10. The goods receipt debits stock with EUR 100, credits GR/IR with EUR 120, and posts EUR 20 to price differences. The invoice for EUR 110 plus EUR 11 tax then debits GR/IR with EUR 120, credits the supplier with EUR 121, debits input tax with EUR 11 and credits price differences with EUR 10 (SAP Learning). Calder's script reproduces the same pattern for motors.

In S/4HANA the material ledger is built in and on by default; actual costing on top of it is optional (SAP Learning). It can carry inventory in up to three currencies and valuations.

Revenue account determination

Billing uses a different rule set. When billing data passes to accounting, revenue, sales deduction and other accounts are determined automatically, using the condition technique, the same mechanism pricing uses (SAP Learning). Two details from the same lesson matter in design:

  • The receivable goes to the reconciliation account in the payer's master record, unless a rule sets a different one.
  • Tax accounts are not set in sales. They are maintained in Financial Accounting Customizing.

The billing document carries the inputs. SAP's model of the Billing Document API shows CustomerAccountAssignmentGroup on the header and MatlAccountAssignmentGroup on the item. Many setups key revenue accounts on these groups plus the sales organization; Calder's script does. Check which fields your system's access sequence actually uses. The same API exposes AccountingDocument and AccountingTransferStatus, so you can see whether an invoice reached finance.

GR/IR clearing

The GR/IR account is where procure-to-pay proves itself. Items clear when receipt and invoice match. SAP's closing training lists why they don't: a missing invoice or receipt, different amounts, an outdated price on the purchase order, delivery costs on the wrong GR/IR account (SAP Learning). An invoice that differs from the order beyond tolerance is posted but blocked for payment, and tolerances are set per company code (SAP Learning).

The Universal Journal, and ECC

In S/4HANA, one table, ACDOCA, holds line items for the general ledger, asset accounting, controlling, profitability analysis and the material ledger. SAP's training states that no reconciliation between financial accounting and controlling is needed (SAP Learning). For reading it from code, the released successors are I_JournalEntryItem and the G/L line item views; for the entry view BSEG, it is I_OperationalAcctgDocItem (Cloudification Repository).

Where reconciliation breaks

Symptom Usual cause Who resolves it
Revenue without cost on an order Goods issue posted in logistics but not in finance, or posted in another period Logistics with finance
Invoice in sales, nothing in the general ledger Billing document not released to accounting, often a rule error Billing team with finance
Sales on an unexpected account Wrong account assignment group in master data, or a wrong rule Master data owner, finance
GR/IR balance that never clears Missing invoice, price difference, wrong freight account Accounts payable with purchasing
Inventory in finance differs from stock in logistics Manual postings to a stock account, or a period problem Finance with inventory management

Where AI fits

Document flow is the evidence chain an AI agent needs. "Why did pump margin drop in September?" has a checkable answer only if the agent can walk from the journal line back to the billing document, the delivery and the order, and cite each one. Three patterns work well:

  • Explain: retrieve the chain and summarize it, citing document numbers a person can open.
  • Detect: run checks like the ones in this topic's script (missing postings, rule mismatches, aged GR/IR items) and rank them.
  • Suggest: propose a likely cause or a matching invoice for a GR/IR item, for a person to confirm.

Posting corrections, changing account determination and clearing open items stay with people. The blocked-order example in the FDE topic and grounding with authorizations show how to keep an agent read-only and within the user's access.

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 walks the document flow of Calder's order 1000001 and explains every posting: which account it hit and which rule chose it. It then runs the checks a controller runs before trusting a margin, breaks the data on purpose to show what the checks catch, traces a purchase through the GR/IR account, and writes the "follow the money" trace, this topic's deliverable. An optional step reads real document flow from SAP's sandbox.

flowchart LR
  P[calder_pack/<br/>order 1000001] --> S[follow_the_money.py]
  R[Account determination<br/>rules in the script] --> S
  S --> T[Trace and<br/>explanations]
  S --> C[Checks:<br/>OK / FAIL]
  S --> M[calder_follow_<br/>the_money.md]
  K[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 7: no account, no key and no network. The script uses only Python's built-in modules, so requirements.txt doesn't change.
  • For Step 8 (optional): your free SAP Business Accelerator Hub key as SAP_API_KEY in .env, from the course setup.
  • Cost: free. Time: about 40 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

Step 3: Create the script

  1. In VS Code, create a new file, paste the script below, and save it as follow_the_money.py inside the unit1b folder.
  2. Read the block from VALUATION_GROUPING to BANK_ACCOUNT first. It is Calder's account determination written as Python lists. The transaction keys (BSX, WRX, GBB, PRD) are SAP's; every code and account number is Calder's own and invented.
"""Follow the money: trace a Calder order's document flow and explain every posting.

Calder Pumpworks is a fictional company. The pack (unit1b/calder_pack/) and the
account determination tables below are made up for the course. Field names
follow SAP's released APIs where one exists; accounts, keys and codes are
illustrative, not SAP standard values and not from any real system.

Run it from your course folder:
  python unit1b/follow_the_money.py                         trace order 1000001 and explain each posting
  python unit1b/follow_the_money.py --break missing-journal  drop the goods issue posting and watch the checks
  python unit1b/follow_the_money.py --break wrong-account    post revenue to the wrong account
  python unit1b/follow_the_money.py --p2p                    trace a purchase of motors through GR/IR
  python unit1b/follow_the_money.py --write                  write unit1b/calder_follow_the_money.md
  python unit1b/follow_the_money.py --sandbox                read one order's flow from SAP's sandbox
Uses only Python's built-in modules. Only --sandbox needs a key and the network.
"""
import argparse
import json
import os
import re
import sys
import urllib.error
import urllib.request
from pathlib import Path

HERE = Path(__file__).resolve().parent
PACK = HERE / "calder_pack"
OUT = HERE / "calder_follow_the_money.md"
SANDBOX = "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/"

# ------------------------------------------- account determination (fictional)
# Inventory postings: the plant gives the valuation grouping code, the product
# gives the valuation class, the business event gives the transaction key.
# BSX, WRX, GBB and PRD are SAP's transaction keys; every code and account
# number on the right-hand side is Calder's own, invented for the course.
VALUATION_GROUPING = {"CP11": "CP01"}            # plant -> valuation grouping code
VALUATION_CLASS = {"FERT": "VC-FG", "HAWA": "VC-TG", "ROH": "VC-RM", "HALB": "VC-SF"}
INVENTORY_RULES = [  # (grouping code, transaction key, account grouping, valuation class) -> G/L
    ("CP01", "BSX", "", "VC-FG", "13100"),       # stock: finished pumps
    ("CP01", "BSX", "", "VC-RM", "13000"),       # stock: raw materials and bought parts
    ("CP01", "GBB", "SALE", "VC-FG", "51000"),   # offsetting entry: goods issue for a delivery
    ("CP01", "WRX", "", "", "21500"),            # GR/IR clearing (no valuation class)
    ("CP01", "PRD", "", "VC-RM", "52000"),       # price differences on standard-price parts
]
# Revenue postings: condition technique keyed on the sales organization and the
# account assignment groups the billing document carries. Course key, not SAP's.
REVENUE_RULES = [  # (sales org, customer group, material group) -> G/L
    ("CPUS", "01", "10", "41000"),               # domestic customers, pumps
    ("CPUS", "01", "20", "41200"),               # domestic customers, spare parts
]
CUSTOMER_ACCT_GROUP = {"10000001": "01", "10000002": "01"}
MATERIAL_ACCT_GROUP = {"CP-ES200": "10", "CP-ES300": "10", "CP-SK200": "20"}
BANK_ACCOUNT = "11000"                           # house bank G/L account


def load_pack() -> dict:
    """Read the five pack files, or stop with a clear message."""
    try:
        return {p.stem: json.loads(p.read_text(encoding="utf-8")) for p in PACK.glob("*.json")}
    except (OSError, json.JSONDecodeError) as err:
        sys.exit(f"Cannot read {PACK}: {err}. Run: python unit1b/make_calder_pack.py")


def inventory_account(key: str, grouping: str, val_class: str, plant: str = "CP11") -> str:
    code = VALUATION_GROUPING[plant]
    for g, k, ag, vc, account in INVENTORY_RULES:
        if g == code and k == key and ag == grouping and vc in ("", val_class):
            return account
    return "?"


def revenue_account(sales_org: str, customer: str, material: str) -> str:
    cg, mg = CUSTOMER_ACCT_GROUP.get(customer), MATERIAL_ACCT_GROUP.get(material)
    for so, c, m, account in REVENUE_RULES:
        if (so, c, m) == (sales_org, cg, mg):
            return account
    return "?"


def money(x: float) -> str:
    return f"{x:>12,.2f}"


# ------------------------------------------------------------ order to cash
def documents(pack: dict) -> dict:
    """Index every document in the order file by 'Kind Number'."""
    order = pack["order_1000001"]
    docs = {
        f"SalesOrder {order['SalesOrder']['SalesOrder']}": order["SalesOrder"],
        f"OutboundDelivery {order['OutboundDelivery']['DeliveryDocument']}": order["OutboundDelivery"],
        f"MaterialDocument {order['MaterialDocument']['MaterialDocument']}": order["MaterialDocument"],
        f"BillingDocument {order['BillingDocument']['BillingDocument']}": order["BillingDocument"],
    }
    for je in order["JournalEntries"]:
        docs[f"JournalEntry {je['AccountingDocument']}"] = je
    return docs


def walk(flow: list, start: str) -> list:
    """Follow the document flow from the first document, depth first."""
    seen, out = set(), []

    def visit(doc: str, depth: int) -> None:
        if doc in seen:
            return
        seen.add(doc)
        out.append((depth, doc))
        for link in flow:
            if link["From"] == doc:
                visit(link["To"], depth + 1)

    visit(start, 0)
    return out


def expected_lines(pack: dict, je: dict) -> dict:
    """Re-derive, from the rules above, which account each kind of line should hit."""
    order = pack["order_1000001"]
    products = {p["Product"]: p for p in pack["products"]["Products"]}
    partners = {b["BusinessPartner"]: b for b in pack["business_partners"]["BusinessPartners"]}
    so = order["SalesOrder"]
    customer = so["SoldToParty"]
    recon = partners[customer]["to_Customer"]["to_CustomerCompany"][0]["ReconciliationAccount"]
    material = so["to_Item"][0]["Material"]
    vc = VALUATION_CLASS[products[material]["ProductType"]]
    src = je["Source"]
    if src.startswith("MaterialDocument"):
        return {"debit": (inventory_account("GBB", "SALE", vc), "GBB + SALE: offsetting entry for a goods issue"),
                "credit": (inventory_account("BSX", "", vc), "BSX: stock account of the valuation class")}
    if src.startswith("BillingDocument"):
        return {"debit": (recon, "reconciliation account in the payer's business partner record"),
                "credit": (revenue_account(so["SalesOrganization"], customer, material),
                           "revenue rule: sales org + customer and material account groups")}
    return {"debit": (BANK_ACCOUNT, "house bank account"),
            "credit": (recon, "clears the open item on the reconciliation account")}


def explain(pack: dict, docs: dict) -> list:
    """Print each journal entry, line by line, with why each account was chosen."""
    problems = []
    for name, je in docs.items():
        if not name.startswith("JournalEntry"):
            continue
        print(f"\n{name}  ({je['Source']}, posted {je['PostingDate']})")
        rules = expected_lines(pack, je)
        for line in je["Lines"]:
            side = "debit" if line["AmountInCompanyCodeCurrency"] > 0 else "credit"
            want, why = rules[side]
            mark = "ok" if line["GLAccount"] == want else f"MISMATCH, rule says {want}"
            print(f"  {side:<6} {line['GLAccount']}  {line['Text']:<28}"
                  f"{money(abs(line['AmountInCompanyCodeCurrency']))} USD  <- {why} [{mark}]")
            if line["GLAccount"] != want:
                problems.append(f"{name}: {side} posted to {line['GLAccount']}, rule says {want}")
    return problems


def reconcile(pack: dict, docs: dict) -> tuple:
    """The checks a controller runs before trusting the margin."""
    order = pack["order_1000001"]
    products = {p["Product"]: p for p in pack["products"]["Products"]}
    journals = [je for n, je in docs.items() if n.startswith("JournalEntry")]
    by_source = {je["Source"].split(" (")[0]: je for je in journals}
    checks = []
    for je in journals:  # 1. every journal entry balances
        total = round(sum(line["AmountInCompanyCodeCurrency"] for line in je["Lines"]), 2)
        checks.append((total == 0, f"journal entry {je['AccountingDocument']} balances (sum {total:.2f})"))
    md = order["MaterialDocument"]  # 2. the goods issue posted, at standard cost
    gi = by_source.get(f"MaterialDocument {md['MaterialDocument']}")
    cost = round(sum(i["QuantityInEntryUnit"] * products[i["Material"]]["to_Valuation"][0]["StandardPrice"]
                     for i in md["to_MaterialDocumentItem"]), 2)
    posted_cost = round(sum(l["AmountInCompanyCodeCurrency"] for l in gi["Lines"] if l["AmountInCompanyCodeCurrency"] > 0), 2) if gi else 0.0
    checks.append((gi is not None, f"goods issue {md['MaterialDocument']} has a journal entry"))
    checks.append((posted_cost == cost, f"cost posted {posted_cost:,.2f} = quantity x standard price {cost:,.2f}"))
    bd = order["BillingDocument"]  # 3. the invoice posted its full net value
    inv = next((je for je in journals if je["AccountingDocument"] == bd["AccountingDocument"]), None)
    revenue = round(-sum(l["AmountInCompanyCodeCurrency"] for l in inv["Lines"] if l["AmountInCompanyCodeCurrency"] < 0), 2) if inv else 0.0
    checks.append((revenue == bd["TotalNetAmount"], f"revenue posted {revenue:,.2f} = billing net {bd['TotalNetAmount']:,.2f}"))
    recon = "12100"  # 4. the receivable is cleared by the payment
    open_ar = round(sum(l["AmountInCompanyCodeCurrency"] for je in journals for l in je["Lines"]
                        if l["GLAccount"] == recon), 2)
    checks.append((open_ar == 0, f"receivable for {bd['PayerParty']} is cleared (open {open_ar:,.2f})"))
    return checks, revenue, posted_cost


def order_to_cash(break_what: str) -> int:
    pack = load_pack()
    order = pack["order_1000001"]
    if break_what == "missing-journal":
        order["JournalEntries"] = [je for je in order["JournalEntries"]
                                   if not je["Source"].startswith("MaterialDocument")]
        print("BROKEN ON PURPOSE: the goods issue's journal entry is missing.\n")
    elif break_what == "wrong-account":
        order["JournalEntries"][1]["Lines"][1]["GLAccount"] = "41900"
        print("BROKEN ON PURPOSE: the invoice's revenue line went to account 41900.\n")
    docs = documents(pack)
    print("Calder order 1000001: document flow (fictional data)")
    for depth, name in walk(order["DocumentFlow"], "SalesOrder 1000001"):
        exists = "" if name in docs else "   <- referenced, but not found"
        print("  " + "   " * depth + ("+- " if depth else "") + name + exists)
    problems = explain(pack, docs)
    checks, revenue, cost = reconcile(pack, docs)
    print("\nReconciliation checks")
    for ok, label in checks:
        print(f"  {'OK   ' if ok else 'FAIL '} {label}")
    margin = revenue - cost
    pct = 100 * margin / revenue if revenue else 0
    print(f"\nFollow the money: revenue {revenue:,.2f} - cost {cost:,.2f} = margin {margin:,.2f} USD ({pct:.1f}%)")
    print("Synthetic numbers from a fictional company; they show the method, not a real result.")
    failed = [label for ok, label in checks if not ok] + problems
    if failed:
        print(f"\n{len(failed)} problem(s): do not report this margin until they are explained.")
    return 1 if failed else 0


# ------------------------------------------------------------ procure to pay
P2P = {  # one purchase order for motors from Orrin Motor Works (all fictional)
    "PurchaseOrder": "4500000001", "Supplier": "20000001", "Material": "CP-MTR15",
    "Quantity": 10, "NetPriceAmount": 950.00,
    "GoodsReceipt": {"MaterialDocument": "4900000002", "PostingDate": "2026-10-05", "Quantity": 10},
    "SupplierInvoice": {"SupplierInvoice": "5105600001", "PostingDate": "2026-10-09",
                        "Quantity": 10, "UnitPrice": 940.00},
}


def procure_to_pay() -> int:
    pack = load_pack()
    products = {p["Product"]: p for p in pack["products"]["Products"]}
    partners = {b["BusinessPartner"]: b for b in pack["business_partners"]["BusinessPartners"]}
    po, gr, iv = P2P, P2P["GoodsReceipt"], P2P["SupplierInvoice"]
    std = products[po["Material"]]["to_Valuation"][0]["StandardPrice"]
    vc = VALUATION_CLASS[products[po["Material"]]["ProductType"]]
    supplier_recon = partners[po["Supplier"]]["to_Supplier"]["to_SupplierCompany"][0]["ReconciliationAccount"]
    stock, grir, prd = (inventory_account(k, "", vc) for k in ("BSX", "WRX", "PRD"))
    gr_po_value = gr["Quantity"] * po["NetPriceAmount"]
    gr_std_value = gr["Quantity"] * std
    inv_value = iv["Quantity"] * iv["UnitPrice"]
    print(f"Calder purchase order {po['PurchaseOrder']}: {po['Quantity']} x {po['Material']} at "
          f"{po['NetPriceAmount']:,.2f} USD; standard price {std:,.2f} USD (fictional data)\n")
    print(f"Goods receipt {gr['MaterialDocument']} ({gr['PostingDate']})")
    print(f"  debit  {stock}  stock, BSX            {money(gr_std_value)}   quantity x standard price")
    print(f"  debit  {prd}  price difference, PRD {money(gr_po_value - gr_std_value)}   PO price above standard")
    print(f"  credit {grir}  GR/IR clearing, WRX   {money(gr_po_value)}   quantity x PO price")
    print(f"  GR/IR open after goods receipt: {money(gr_po_value)} credit (received, not yet invoiced)\n")
    print(f"Supplier invoice {iv['SupplierInvoice']} ({iv['PostingDate']})")
    print(f"  debit  {grir}  GR/IR clearing, WRX   {money(gr_po_value)}   clears the goods receipt value")
    diff = round(inv_value - gr_po_value, 2)  # invoice above PO price -> debit, below -> credit
    side, why = ("debit ", "invoice above PO price") if diff > 0 else ("credit", "invoice below PO price")
    if diff:
        print(f"  {side} {prd}  price difference, PRD {money(abs(diff))}   {why}")
    print(f"  credit {supplier_recon}  supplier {po['Supplier']}      {money(inv_value)}   reconciliation account in the business partner")
    print(f"  GR/IR open after invoice:       {money(0.0)}  (cleared by the invoice)\n")
    print(f"Stock stays at standard: {gr_std_value:,.2f}. Net price difference: "
          f"{gr_po_value - gr_std_value + diff:,.2f} USD.")
    print("Synthetic numbers from a fictional company; they show the method, not a real result.")
    return 0


# ------------------------------------------------------------ deliverable
def write_trace() -> int:
    pack = load_pack()
    order = pack["order_1000001"]
    docs = documents(pack)
    checks, revenue, cost = reconcile(pack, docs)
    fence = "`" * 3
    ids = {name: f"D{n}" for n, (_, name) in enumerate(walk(order["DocumentFlow"], "SalesOrder 1000001"), 1)}
    lines = ["# Calder order 1000001: follow the money", "",
             "Fictional data from the Orchestrate course. Not SAP standard values, not from a real system.", "",
             fence + "mermaid", "flowchart LR"]
    for name, i in ids.items():
        kind, number = name.split(" ")
        lines.append(f'  {i}["{kind}<br/>{number}"]')
    for link in order["DocumentFlow"]:
        lines.append(f"  {ids[link['From']]} --> {ids[link['To']]}")
    lines += [fence, "", "| Journal entry | Posted | Triggered by | Debit | Credit | Amount (USD) |",
              "| --- | --- | --- | --- | --- | --- |"]
    for je in order["JournalEntries"]:
        dr = next(l for l in je["Lines"] if l["AmountInCompanyCodeCurrency"] > 0)
        cr = next(l for l in je["Lines"] if l["AmountInCompanyCodeCurrency"] < 0)
        lines.append(f"| {je['AccountingDocument']} | {je['PostingDate']} | {je['Source']} | "
                     f"{dr['GLAccount']} {dr['Text']} | {cr['GLAccount']} {cr['Text']} | "
                     f"{dr['AmountInCompanyCodeCurrency']:,.2f} |")
    lines += ["", f"Revenue {revenue:,.2f} - cost {cost:,.2f} = margin {revenue - cost:,.2f} USD (synthetic).", "",
              "## Checks", ""] + [f"- {'OK' if ok else 'FAIL'}: {label}" for ok, label in checks]
    OUT.write_text("\n".join(lines) + "\n", encoding="utf-8")
    print(f"wrote {OUT.relative_to(HERE.parent)}")
    return 0


# ------------------------------------------------------------ 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 get(path: str, key: str) -> dict:
    request = urllib.request.Request(SANDBOX + path, headers={"APIKey": key, "Accept": "application/json"})
    with urllib.request.urlopen(request, timeout=30) as response:
        return json.load(response)["d"]


def sandbox() -> int:
    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.")
    try:
        orders = get("API_SALES_ORDER_SRV/A_SalesOrder?$top=5&$select=SalesOrder,SoldToParty,TotalNetAmount,"
                     "TransactionCurrency&$expand=to_SubsequentProcFlowDoc&$format=json", key)["results"]
        print("SAP sandbox: sales orders and their subsequent documents (SAP demo data, not Calder's)")
        billing = []
        for so in orders:
            print(f"\nSalesOrder {so['SalesOrder']}  {so['TotalNetAmount']} {so['TransactionCurrency']}")
            flow = so.get("to_SubsequentProcFlowDoc", {}).get("results", [])
            if not flow:
                print("  (no subsequent documents: the order has not been delivered or billed)")
            for doc in flow:
                print(f"  +- {doc['SubsequentDocument']}  category {doc['SubsequentDocumentCategory']}"
                      f"  level {doc.get('ProcessFlowLevel', '')}")
                billing.append(doc["SubsequentDocument"])
        for number in billing[:5]:
            if not re.fullmatch(r"[0-9A-Za-z]{1,10}", number):
                print(f"  skipped unexpected document number {number!r}")
                continue  # never put an unchecked value into the URL's key
            try:
                bd = get(f"API_BILLING_DOCUMENT_SRV/A_BillingDocument('{number}')?$select=BillingDocument,"
                         "AccountingDocument,AccountingTransferStatus,TotalNetAmount&$format=json", key)
                print(f"\nBillingDocument {bd['BillingDocument']}: accounting document "
                      f"'{bd['AccountingDocument']}', transfer status '{bd['AccountingTransferStatus']}'")
            except urllib.error.HTTPError as err:
                if err.code in (401, 403):
                    raise
                # Any other 4xx: the number is not a billing document (a delivery, say). Skip it.
    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.")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="Follow the money through Calder's documents.")
    parser.add_argument("--break", dest="break_what", choices=["missing-journal", "wrong-account"], default="")
    parser.add_argument("--p2p", action="store_true", help="trace a purchase through GR/IR")
    parser.add_argument("--write", action="store_true", help="write the follow-the-money trace")
    parser.add_argument("--sandbox", action="store_true", help="read order flows from SAP's sandbox")
    args = parser.parse_args()
    if args.p2p:
        return procure_to_pay()
    if args.write:
        return write_trace()
    if args.sandbox:
        return sandbox()
    return order_to_cash(args.break_what)


if __name__ == "__main__":
    sys.exit(main())

Step 4: Trace the order and explain each posting

  1. Run the script:

    • Windows (PowerShell):

      python unit1b\follow_the_money.py
    • macOS / Linux:

      python unit1b/follow_the_money.py
  2. You should see the tree, three journal entries and seven checks:

Calder order 1000001: document flow (fictional data)
  SalesOrder 1000001
     +- OutboundDelivery 80000001
        +- MaterialDocument 4900000001
           +- JournalEntry 5000000001
        +- BillingDocument 90000001
           +- JournalEntry 9400000001
              +- JournalEntry 1400000001

JournalEntry 5000000001  (MaterialDocument 4900000001 (goods issue), posted 2026-09-25)
  debit  51000  Cost of goods sold             19,600.00 USD  <- GBB + SALE: offsetting entry for a goods issue [ok]
  credit 13100  Finished goods inventory       19,600.00 USD  <- BSX: stock account of the valuation class [ok]

JournalEntry 9400000001  (BillingDocument 90000001 (invoice), posted 2026-09-26)
  debit  12100  Receivable from 10000001       29,100.00 USD  <- reconciliation account in the payer's business partner record [ok]
  credit 41000  Revenue, pumps                 29,100.00 USD  <- revenue rule: sales org + customer and material account groups [ok]

JournalEntry 1400000001  (Incoming payment for invoice 90000001, posted 2026-10-20)
  debit  11000  Bank                           29,100.00 USD  <- house bank account [ok]
  credit 12100  Receivable from 10000001       29,100.00 USD  <- clears the open item on the reconciliation account [ok]

Reconciliation checks
  OK    journal entry 5000000001 balances (sum 0.00)
  OK    journal entry 9400000001 balances (sum 0.00)
  OK    journal entry 1400000001 balances (sum 0.00)
  OK    goods issue 4900000001 has a journal entry
  OK    cost posted 19,600.00 = quantity x standard price 19,600.00
  OK    revenue posted 29,100.00 = billing net 29,100.00
  OK    receivable for 10000001 is cleared (open 0.00)

Follow the money: revenue 29,100.00 - cost 19,600.00 = margin 9,500.00 USD (32.6%)
Synthetic numbers from a fictional company; they show the method, not a real result.

Read one line aloud: "Debit 51000 cost of goods sold, because a goods issue for a delivery uses key GBB with grouping SALE." That sentence is what a controller wants from an explanation: the amount, the account and the rule.

Step 5: Break the data and watch the checks

  1. Drop the goods issue's journal entry, as if the posting had failed:

    • Windows (PowerShell):

      python unit1b\follow_the_money.py --break missing-journal
    • macOS / Linux:

      python unit1b/follow_the_money.py --break missing-journal
  2. The end of the output should read:

Reconciliation checks
  OK    journal entry 9400000001 balances (sum 0.00)
  OK    journal entry 1400000001 balances (sum 0.00)
  FAIL  goods issue 4900000001 has a journal entry
  FAIL  cost posted 0.00 = quantity x standard price 19,600.00
  OK    revenue posted 29,100.00 = billing net 29,100.00
  OK    receivable for 10000001 is cleared (open 0.00)

Follow the money: revenue 29,100.00 - cost 0.00 = margin 29,100.00 USD (100.0%)
Synthetic numbers from a fictional company; they show the method, not a real result.

2 problem(s): do not report this margin until they are explained.

Every journal that exists still balances. The problem is the journal that doesn't exist, and only the document flow shows it: the tree marks JournalEntry 5000000001 as "referenced, but not found".

  1. Now post revenue to the wrong account:

    • Windows (PowerShell):

      python unit1b\follow_the_money.py --break wrong-account
    • macOS / Linux:

      python unit1b/follow_the_money.py --break wrong-account
  2. Look for this line, and the closing line 1 problem(s): ...:

  credit 41900  Revenue, pumps                 29,100.00 USD  <- revenue rule: sales org + customer and material account groups [MISMATCH, rule says 41000]

All seven reconciliation checks still pass. Amounts are right; the account is wrong. Only re-deriving the account from the rules catches it.

Step 6: Trace a purchase through GR/IR

  1. Run the procure-to-pay trace:

    • Windows (PowerShell):

      python unit1b\follow_the_money.py --p2p
    • macOS / Linux:

      python unit1b/follow_the_money.py --p2p
  2. You should see:

Calder purchase order 4500000001: 10 x CP-MTR15 at 950.00 USD; standard price 920.00 USD (fictional data)

Goods receipt 4900000002 (2026-10-05)
  debit  13000  stock, BSX                9,200.00   quantity x standard price
  debit  52000  price difference, PRD       300.00   PO price above standard
  credit 21500  GR/IR clearing, WRX       9,500.00   quantity x PO price
  GR/IR open after goods receipt:     9,500.00 credit (received, not yet invoiced)

Supplier invoice 5105600001 (2026-10-09)
  debit  21500  GR/IR clearing, WRX       9,500.00   clears the goods receipt value
  credit 52000  price difference, PRD       100.00   invoice below PO price
  credit 21100  supplier 20000001          9,400.00   reconciliation account in the business partner
  GR/IR open after invoice:               0.00  (cleared by the invoice)

Stock stays at standard: 9,200.00. Net price difference: 200.00 USD.
Synthetic numbers from a fictional company; they show the method, not a real result.

Between 5 and 9 October, the 9,500.00 USD credit on GR/IR is exactly the "goods received, not invoiced" item a month-end review would list.

Step 7: Write the follow-the-money trace

  1. Write the deliverable:

    • Windows (PowerShell):

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

      python unit1b/follow_the_money.py --write
  2. You should see wrote unit1b/calder_follow_the_money.md.

  3. Open the file in VS Code and press Ctrl+Shift+V (Windows) or Cmd+Shift+V (macOS) to preview it. You should see the flow diagram, a table of the three journal entries and the seven checks marked OK.

Step 8 (optional): Read real document flow from SAP's sandbox

This step reads SAP's demo data, not Calder's. It needs your sandbox key and the network.

  1. Make sure .env in the course folder has a line SAP_API_KEY="...".

  2. Run:

    • Windows (PowerShell):

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

      python unit1b/follow_the_money.py --sandbox
  3. The output has this shape. The values in angle brackets come from SAP's demo data and change over time, so they aren't shown here:

SAP sandbox: sales orders and their subsequent documents (SAP demo data, not Calder's)

SalesOrder <number>  <net amount> <currency>
  +- <document number>  category <code>  level <n>
  +- <document number>  category <code>  level <n>

BillingDocument <number>: accounting document '<number>', transfer status '<code>'

A valid "empty" result is an order line followed by (no subsequent documents: ...): that order simply hasn't been delivered or billed. If no billing document line appears, none of the first five subsequent documents was an invoice. The sandbox is read-only, so you can't post anything there.

Step 9: Save your work in Git

git add unit1b/follow_the_money.py unit1b/calder_follow_the_money.md
git commit -m "Unit 1B: follow the money for Calder order 1000001"

What each part of the code does

Part What it does
VALUATION_GROUPING, VALUATION_CLASS, INVENTORY_RULES Calder's inventory account determination: plant to grouping code, product type to valuation class, then key and class to account
REVENUE_RULES and the two ..._ACCT_GROUP maps Calder's revenue rules, keyed on sales organization and the customer and material account assignment groups
walk() Follows the pack's document flow depth first and returns the tree
expected_lines() Re-derives, from the rules, which account each side of a journal should hit
explain() Prints each journal line with the rule behind it and flags mismatches
reconcile() The controller's checks: entries balance, the goods issue posted at standard cost, revenue equals the billing net, the receivable is cleared
--break Changes the data in memory only; your pack files stay as they were
procure_to_pay() Values a goods receipt and invoice for a standard-price part and shows the GR/IR balance after each
write_trace() Writes the Mermaid diagram, journal table and checks to calder_follow_the_money.md
sandbox() Reads up to five orders with $expand=to_SubsequentProcFlowDoc, then asks the Billing Document API for each follow-on number's accounting document, after checking the number has only letters and digits so it can't change the URL

If something goes wrong

What you see What it means What to do
python is not recognized / command not found Python isn't on the path, or the terminal opened before installing Follow the Python step in Set up your computer, then open a new terminal
Cannot read ...calder_pack The pack wasn't written, or you ran from the wrong folder Run Step 2 from the orchestrate-course folder
KeyError: 'order_1000001' An older or edited pack Run Step 2 again to rewrite the pack
Your totals differ from the ones shown The pack or the rules were edited Rewrite the pack and copy the script again
SAP_API_KEY is not set No key in the environment or .env Add it as in the course setup, or skip Step 8
Sandbox answered HTTP 401 The key is wrong or expired Copy the key again from the Business Accelerator Hub
Cannot reach the sandbox No network, or a company proxy blocks it Try another network, or stay with Steps 1 to 7
ModuleNotFoundError You changed the script to use an outside library The script needs only built-in modules; copy it again

The SAP way

As of October 2026, SAP S/4HANA provides each piece of this topic as standard:

  • Seeing the flow. The sales order's process flow, in apps such as Manage Sales Orders - Version 2, shows related documents including deliveries, billing documents and journal entries, with status and navigation (SAP Learning). Purchase order history shows receipts and invoices per item.
  • Rules. Inventory account determination in Customizing, with a simulation under Configure Automatic Postings; revenue account determination with the condition technique; tax accounts in Financial Accounting Customizing (SAP Learning).
  • Watching GR/IR. Monitor GR/IR Account Reconciliation gives KPIs on open items; Reconcile GR/IR Accounts builds work lists; situation handling can alert on items that need attention (SAP Learning).
  • Reading it from code. Released CDS views (Cloudification Repository, read 23 October 2026): I_SDDocumentMultiLevelProcFlow for sales document flow, I_OutboundDelivery, I_BillingDocument, I_MaterialDocumentItem_2, I_PurchaseOrderHistoryAPI01, I_SupplierInvoiceAPI01, I_JournalEntry and I_JournalEntryItem. Released OData APIs used in this topic: Sales Order (API_SALES_ORDER_SRV) and Billing Document (API_BILLING_DOCUMENT_SRV) on the SAP Business Accelerator Hub.

Build vs. SAP

Need Use SAP standard Build (side by side)
See one order's documents Process flow in the sales order app Only if a non-SAP portal must show it, via released APIs
Pick G/L accounts Always the standard rules Never; a custom rule outside SAP breaks the audit trail
Monitor GR/IR GR/IR monitor and reconcile apps An agent that suggests causes, read-only, feeding the same work list
Explain a margin swing Standard margin reports with drill-down A retrieval layer that walks the chain and cites documents
Cross-system trace (ECC and S/4HANA) Each system's own flow A trace that joins both, keyed on document numbers and partner IDs

Architecture view

Where it sits. Document flow and account determination live inside each ERP. At Calder (fictional), Rockford's documents post in S/4HANA (CP10) and Augsburg's in ECC (CP20). Anything that explains or monitors postings reads from released interfaces and writes only to a work list.

flowchart LR
  S4[S/4HANA CP10<br/>flow + journal] -->|released APIs, CDS| TR[Trace and<br/>check service]
  ECC[ECC CP20<br/>flow + FI/CO] -->|existing interfaces| TR
  TR --> WL[Work list for<br/>finance and logistics]
  TR --> AI[AI explanation<br/>cites documents]
  AI --> WL

Non-functional requirements that decide the design

Requirement Typical target If missed
Traceability Every journal line reaches its source document in one hop Variances can't be explained; audit findings
Timeliness Logistics postings in finance the same day Margins and stock values lag; close becomes a chase
Completeness Zero unposted billing documents or goods issues at period end Revenue without cost, or cost without revenue
Data volume Traces bounded by document, not full-table reads Slow reports, API throttling, cost
Security Readers see only company codes they're authorized for Finance data exposed through a convenience tool

Integration and identity. Inside each ERP, the logistics step creates its own accounting document; no interface carries it. A trace service reads released APIs or CDS views with a technical user that is read-only, or better, with the requesting user's identity so SAP authorizations apply. Intercompany flows between CP10 and CP20 cross systems, and that interface is covered in intercompany and multi-company scenarios.

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

  • Context: Calder's controllers spend the first days of each close explaining margin differences by hand; CP20 is still on ECC.
  • Options: (a) standard apps and reports only; (b) a read-only trace and check service over released APIs, feeding a work list, with AI explanations that cite documents; (c) a data warehouse copy of all documents, checked there.
  • Decision: (b) for CP10 now, extended to CP20 after its migration; (c) re-evaluated when group reporting is designed.
  • Consequences: Checks run near real time and stay within SAP authorizations. Rules must be mirrored in the checks and kept in step with Customizing. ECC postings are explained manually until migration.

What a design review will ask

  1. Which identity does the trace service use, and can it read company codes the requester can't?
  2. How are the checks' copies of the rules kept in step with account determination in Customizing?
  3. What is the volume per day, and does the design read per document or per table?
  4. Who acts on the work list, and how fast?
  5. Can any component post, reverse or clear anything? (The answer should be no.)

Production concerns

  • Authorizations. Journal entries reveal margins, customer terms and supplier prices. Read with the user's identity where possible, filtered by company code; see Agent permissions and SAP authorizations.
  • Rules drift. A check that re-derives accounts must match Customizing. Treat the rule copy as configuration with an owner, and compare it with SAP's simulation after each change.
  • Periods. A goods issue in September and its invoice in October is normal. Checks must compare within the right period, or they will raise false alarms every month-end.
  • Volume and cost. Read by document or by small date ranges. Expanding the flow for thousands of orders at once is slow and may be throttled.
  • Clean core. Read through released APIs and CDS views, never through tables like VBFA or ACDOCA directly from custom code in a cloud edition.
  • Evaluation. For an AI explainer, keep a set of known cases (missing posting, wrong account, open GR/IR) and score whether each explanation names the right documents.

Pitfalls

  • Trusting balanced entries. Every journal balances; the wrong account and the missing posting do not show up as imbalance.
  • Hard-coding category codes and account numbers. They come from the system's configuration. Read them, don't assume them.
  • Testing only the logistics side. A new movement type that ships correctly but posts to a suspense account passes a logistics-only test.
  • Ignoring the document date versus the posting date. Period-end checks need the posting date.
  • Explaining without citing. An AI summary that doesn't name the documents can't be checked, and so can't be trusted.
  • Assuming ECC and S/4HANA read the same. The posting logic matches; the tables and released interfaces differ.

Exercise

Turn the generated trace into a review pack a controller could use at month-end.

  1. Run python unit1b/follow_the_money.py --write and copy unit1b/calder_follow_the_money.md to unit1b/calder_follow_the_money_review.md.
  2. In the copy, add a column Rule to the journal table. For each row, write the rule that chose the debit and the credit, using the explanations from Step 4 (for example "GBB + SALE / BSX").
  3. In follow_the_money.py, find "UnitPrice": 940.00 in the P2P block and change it to 990.00. Save, then run python unit1b/follow_the_money.py --p2p (Windows: python unit1b\follow_the_money.py --p2p).
  4. Under a heading Invoice above PO price, copy the three invoice lines and write one sentence on what changed and why. Add one question for Calder's finance team: which price tolerance would block this invoice for payment? Change the value back to 940.00 afterwards.
  5. Run both --break options. Under a heading Checks that caught it, write one line per option: which check or mismatch fired, and who at Calder would fix the cause.
  6. Add one line on the ECC difference: what a trace of an order on CP20 must read for finance and for controlling, which it doesn't need to on CP10.
  7. Preview the file to check the tables render, then commit it with Git.

Done when: unit1b/calder_follow_the_money_review.md has the flow diagram, a journal table with a Rule column for all three entries, an Invoice above PO price section showing a 400.00 USD price difference debit, a Checks that caught it section with two lines, and one ECC line. Keep it: Order to cash end to end and Procure to pay end to end build on this trace, and the record-to-report topic turns these checks into close tasks.

Check yourself

Pick one answer for each question. The explanation appears after you choose.

1In the script's default run, why does the goods issue post to account 51000?

Answer: C. Inventory postings are chosen by rules: the plant gives the grouping code, the product its valuation class, and the movement the key and account grouping. Nobody picks the account at the dock.

2With --break wrong-account, all seven reconciliation checks pass. Why does the script still report a problem?

Answer: D. The amounts are all correct, so balance and total checks pass. Only comparing the posted account with what the rules would choose catches it.

3Calder receives 10 motors at a PO price of 950 USD; the standard price is 920 USD. What does the goods receipt credit to GR/IR?

Answer: D. Stock is valued at standard (9,200) and GR/IR at the PO price (9,500); the 300 difference goes to the price difference account. The invoice later clears the 9,500.

4You must read sales document flow from custom code in an S/4HANA cloud edition. What do you use?

Answer: A. The Cloudification Repository marks VBFA as not to be released and names I_SDDocumentMultiLevelProcFlow as its successor. Released objects survive upgrades; table access in custom code doesn't fit clean core.

5Which field in SAP's Billing Document API tells you whether an invoice reached finance?

Answer: A. The billing header exposes AccountingDocument and AccountingTransferStatus. The account assignment group feeds revenue rules; it says nothing about whether the posting happened.

6A controller asks why September's margin on order 1000001 shows 100%. What would you do first?

Answer: B. Revenue without cost points to a missing goods issue posting. The document flow shows the gap; correcting it is a person's decision in SAP, not an agent's.

7What does the Universal Journal change for a trace from Calder's S/4HANA company CP10 compared with ECC company CP20?

Answer: B. The Universal Journal merges finance and controlling line items; posting logic and account determination stay. CP20 on ECC still has separate records to reconcile.

8Your design adds an AI agent that explains GR/IR balances. Which boundary belongs in the design?

Answer: D. Clearing items, changing prices or tolerances changes SAP data and controls. The agent should read within the user's authorizations and put suggestions on a work list that a person acts on.

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.

  • Stage 2 of 11Set up financeThe whole stage practices this topic, on Account determination.
  • Stage 6 of 11ProcurePractice it on Goods receipt.
  • ChapterQualityInspect what Calder buys before anyone uses it: an inspection setup and characteristics for seal kits, goods receipts that go to quality inspection and open inspection lots, results measured on a sample, and the usage decision that releases a good delivery to unrestricted stock or blocks a bad one, while planning counts only what can be used. Then the final inspection of a pump Calder builds: ES-300 tested at production receipt before anyone can sell it, and an order that confirms only the pumps that passed until a recheck after the decision. Then the complaint: a supplier's invoice blocked for quality, a quality notification, a return delivery and the credit memo that settles it. Then the supplier's replacement inspected and its invoice released by the decision, and the supplier's quality record read for what it can and can't say. Last, ES-300's final inspection fed by the test bench: one measurement per pump, recorded once against its serial number.
  • ChapterShop floorRun Rockford's floor from its own system: released production orders become dispatch lists per work center, in operation order with their scheduled dates, hours and the parts to issue; operators start and finish each operation by shift and confirm it with the good quantity, the scrap and its reason, and the hours; cheap parts are backflushed by the confirmation; the test bench's machine time is costed apart from the technician's; every pump gets a serial number and its own bench results; a pump that fails twice is repaired on its own rework order; every stop gets a reason, each work center's week reads as OEE, a breakdown goes to the maintenance planner, and in the end the floor runs its own system, fed by messages from the core, including cancellations the floor must agree to, and sending its confirmations back the same way, each of which can fail and must be fixed.
  • ChapterTransportationPlan Calder's freight from Rockford: carriers and their agreements, freight units from deliveries, freight orders, the carrier chosen by cost, loads with stops, truck capacity and a departure worked back from the dates, the final charges with stops and detention after free time, tendering each load to a carrier that has to accept it, following the truck by its execution events, and settling the freight: each carrier invoice checked against the agreed charges, the freight cost shared among the deliveries so each customer's margin shows what its freight really cost, and inbound freight on purchased motors planned on the purchase order and carried into the motors' cost.
  • ChapterWarehouseLay out Calder's Rockford distribution center: storage types, sections and bins, then put stock away, pick, pack and count it, put people on the work and watch it.

Stages build on each other: a stage link opens the core path at that stage. To start there without doing the earlier stages yourself, use Do this stage for me on the stage before it. A chapter starts inside your own company once the core path is done (or the lab finishes the core for you first); drills and exercises start from their own prepared copy. Everything is saved in this browser only.

Sources

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