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.
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.
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.
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).
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.
"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.
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.
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.
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).
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).
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
SAP's materials management training lists four inputs (SAP Learning):
Chart of accounts, from the company code. Rules are set per chart, because account numbers mean different things in each.
Valuation grouping code, from the plant's valuation area. Plants that should post alike share a code.
Valuation class, from the material. Materials that should post alike share a class.
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).
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.
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.
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).
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).
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.
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
In VS Code, create a new file, paste the script below, and save it as follow_the_money.py inside the unit1b folder.
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())
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.
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".
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.
You should see wrote unit1b/calder_follow_the_money.md.
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.
Make sure .env in the course folder has a line SAP_API_KEY="...".
Run:
Windows (PowerShell):
python unit1b\follow_the_money.py --sandbox
macOS / Linux:
python unit1b/follow_the_money.py --sandbox
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.
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
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.
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.
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
Which identity does the trace service use, and can it read company codes the requester can't?
How are the checks' copies of the rules kept in step with account determination in Customizing?
What is the volume per day, and does the design read per document or per table?
Who acts on the work list, and how fast?
Can any component post, reverse or clear anything? (The answer should be no.)
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.
Turn the generated trace into a review pack a controller could use at month-end.
Run python unit1b/follow_the_money.py --write and copy unit1b/calder_follow_the_money.md to unit1b/calder_follow_the_money_review.md.
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").
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).
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.
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.
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.
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.
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.
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.
Procure to pay end to endUnit 1B · SAP end to endLinked from this topic · Shares: GR/IR clearing account
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.
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
Understanding Sales and Distribution Integration (SAP Learning)— the sales order posts nothing in FI; goods issue creates a material document and an accounting document (debit the account for sold goods, credit stock); billing posts receivables and revenue; document flow shows the documents of a transaction
Describing Automatic Account Determination (SAP Learning)— determination factors (chart of accounts, plant and valuation grouping code, valuation class, movement type); BSX and WRX at goods receipt; worked GR and invoice postings with price differences; supplier reconciliation account from the business partner
Setting Up Account Determination (SAP Learning, billing)— revenue, sales deduction and other G/L accounts determined automatically when billing data passes to accounting; revenue account determination uses the condition technique; tax accounts are set in Financial Accounting Customizing
Discussing the Universal Journal (SAP Learning)— ACDOCA as one table for G/L, asset accounting, controlling, profitability analysis and material ledger; no reconciliation between FI and CO needed
Cloudification Repository (SAP on GitHub)— objectReleaseInfoLatest.json read 23 October 2026; VBFA, ACDOCA, BSEG, MSEG not to be released, with successors I_SDDocumentMultiLevelProcFlow, I_JournalEntryItem, I_OperationalAcctgDocItem, I_MaterialDocumentItem_2; I_BillingDocument, I_OutboundDelivery, I_PurchaseOrderHistoryAPI01, I_SupplierInvoiceAPI01 released
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 (July 2022); A_SalesOrder navigations to_PrecedingProcFlowDoc and to_SubsequentProcFlowDoc with SubsequentDocument and SubsequentDocumentCategory; the matching billing document package (2.1.0) shows AccountingDocument, AccountingTransferStatus, CustomerAccountAssignmentGroup and MatlAccountAssignmentGroup; the material document package shows PurchaseOrder and PurchaseOrderItem on A_MaterialDocumentItem