Design the flows between a group's own companies, from stock transfer and billing to transfer prices, currencies and eliminations, across S/4HANA and ECC.
Most groups are several legal companies under one owner. Each company keeps its own books, pays its own taxes and signs its own financial statements. When one of them sells to another, both record the deal, and the group must later act as if it never happened.
Intercompany is the name for those deals between companies of the same group. At Calder Pumpworks, the fictional pump maker this unit follows, the German company (CP20, Augsburg) machines impellers and sells them to the US company (CP10, Rockford), which builds them into pumps.
Every intercompany deal raises the same design questions:
Which flow? A stock transfer between plants, a purchase order and sales order pair, or a direct shipment to a customer.
At what price? The transfer price is a business and tax decision. The system has to carry it the same way on both sides.
In which currency? The seller's, the buyer's, the invoice's and the group's currency can all differ.
How does it disappear? At group close, matching balances are eliminated. The ones that don't match have to be explained.
Three ideas carry this topic:
One event, two sets of books. Every intercompany transaction is two transactions that must tell the same story: same amount, same date, each naming the other company.
The design follows the landscape. The integrated SAP flows assume both companies are in one system. Calder's companies are not, which changes the documents, the interfaces and the close.
Most differences are explainable. Goods on a truck, exchange rates and a missing partner code cause more differences than real errors. A good design labels them early.
Intercompany problems are slow and expensive because two finance teams, often in two countries, have to agree before anyone can close the group.
Here is Calder on Saturday 31 October 2026, the last day of the October period:
What the checks show
What it means
Who acts
Augsburg shipped 40 impellers on 30 October; Rockford has no record yet
A timing difference of 13,400 USD, explainable but not yet labeled
Intercompany accountants, both sides
Augsburg booked Rockford's IT service charge without naming Rockford as partner
2,500 USD the group can't eliminate, so group receivables and payables are overstated
Augsburg finance
The impeller price earns Augsburg almost nothing over cost
The price breaks Calder's own cost-plus-8% transfer pricing policy; profit sits in the wrong country
Group tax, both controllers
One open order is priced 455.00 USD in Rockford's system and 480.00 in Augsburg's
The invoice due on 4 November will stop in Rockford's inbound queue
Rockford buyer, Augsburg sales
Augsburg expects payment in 30 days; Rockford pays in 45
Augsburg will chase invoices Rockford hasn't scheduled
Both controllers
None of these is a system error. Each is a design gap: a partner code that comes from memory, two price lists, two sets of payment terms. The business decision is which of these the design must prevent, which the close must catch, and who owns each one.
As of October 2026, SAP's training covers these building blocks for SAP S/4HANA:
Intercompany sales. A customer order in one company is delivered from another company's plant. The delivering company creates an internal invoice (billing type IV) to the selling company (SAP Learning). In SAP S/4HANA Cloud Public Edition, scope item 1HO sends that invoice by IDoc and posts it as a supplier invoice in the ordering company, for companies in the same system (SAP Learning).
Intercompany stock transfer. One company orders stock from another company's plant. The advanced version creates the supplying company's sales order and the buyer's supplier invoice automatically, and shows goods in transit in both companies (SAP Learning). SAP's training dates advanced intercompany sales to SAP S/4HANA 2022 (SAP Learning).
Transfer pricing. S/4HANA can keep up to three valuations side by side: legal (with the markup), group (markup removed) and profit center (SAP Learning).
Matching and elimination.Intercompany Matching and Reconciliation (ICMR) matches the two sides with rules, tolerances and reason codes (SAP Learning). SAP S/4HANA Finance for group reporting then eliminates intercompany revenue, balances and profit in inventory (SAP Learning).
SAP's site lists an Intercompany Matching and Reconciliation Agent among the agents of its Financial Closing Assistant, without availability dates on the page (SAP). Confirm what is live for your edition before you plan on it.
The catch for Calder: the integrated flows above work inside one system. Augsburg still runs SAP ECC and Rockford runs S/4HANA, so today they trade like two separate businesses, with a purchase order, a sales order and electronic messages between them.
"Intercompany differences mean someone made a mistake." Most are timing or currency. The work is to label them, not to hunt for errors.
"Transfer pricing is a finance setting." It is a tax policy that needs advisers and documentation. The system's job is to apply it consistently on both sides.
"Elimination happens automatically." Only when both sides name each other as partner and agree in group currency. Everything else lands on someone's desk at close.
"Moving to S/4HANA fixes intercompany." Only if the companies end up in one system and the flows are redesigned. Two S/4HANA systems still need interfaces.
"An intercompany sale and a stock transfer are the same thing." One serves an outside customer, the other refills a plant. They create different documents and different postings.
Pick one answer for each question. The explanation appears after you choose.
1Calder's German company sells impellers to its US company. Why must the group remove that sale at group close?
Answer: D. Each company records the sale in its own books, which is correct for its legal statements. The group, seen as one business, has only sold the impellers once they leave the group, so the internal sale is eliminated.
2Which question decides whether Calder can use SAP's integrated intercompany stock transfer between Augsburg and Rockford?
Answer: C. The integrated flows, classic and advanced, link documents inside one system. Augsburg on ECC and Rockford on S/4HANA must use a purchase order and a sales order joined by electronic messages until that changes.
3Augsburg shipped impellers on 30 October and Rockford has no record of them on 31 October. What should the intercompany accountant do?
Answer: D. The difference is real and explained: the goods are on their way. It gets a reason label, and the group close treats it as goods in transit. Reversing or posting early would create new problems.
4Your team proposes a new impeller price. Who should approve it before anyone changes it in the system?
Answer: D. A transfer price moves profit between countries, so it follows a tax policy and needs documentation. The system then applies it the same way on both sides, which is why one approved change should reach both price records together.
5Augsburg booked Rockford's IT charge without naming Rockford as the partner company. What is the business effect?
Answer: A. Elimination needs both sides to name each other. Without the partner, the payable looks like a debt to an outside supplier and stays in the group figures until Augsburg reposts it.
6A vendor promises its AI will fix Calder's intercompany differences. What is the most useful question to ask first?
Answer: B. Most differences are timing, currency or missing partners. A tool that labels them and proposes fixes for an accountant to approve helps; one that posts on its own, or works around missing master data, hides the problem.
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.
Every intercompany transaction is two transactions that must tell the same story, plus a third view that erases both.
When Augsburg (CP20) ships impellers to Rockford (CP10), Augsburg records a sale and a receivable; Rockford records a purchase, stock and a payable. Each is right for its own legal books. The group sees neither: for Calder as a whole, metal moved from one building to another, and any profit Augsburg made stays unrealized until the pump is sold to a customer.
So every design choice in this topic comes back to three tests:
Do both sides record the same event? Same quantity, same price, same currency, each naming the other as trading partner.
Do they record it at the same time? Shipment and receipt can fall in different periods; the design has to make that visible.
Can the group remove it? Elimination works on data in group currency, and only when both sides agree.
The integrated SAP flows pass tests 1 and 2 by construction, because one document chain serves both companies. They need both companies in one system. When the companies are in two systems, as at Calder, every test depends on master data, interfaces and the close.
The diagram shows Calder's intercompany flow today, with the S/4HANA side on the left and the ECC side on the right. Unit 1B's order to cash and procure to pay topics explain each document; here the point is how the two chains meet.
flowchart LR
subgraph S4[CP10 Rockford, S/4HANA]
PO[PO 4500000005] --> GR[Goods receipt<br/>36 of 40]
GR --> SI[Supplier invoice<br/>5105600004]
end
subgraph ECC[CP20 Augsburg, ECC]
SO[Sales order] --> DL[Delivery and<br/>goods issue]
DL --> BI[Invoice<br/>95000041]
end
PO -->|EDI order| SO
BI -->|EDI invoice| SI
SI --> M[Month end:<br/>match, eliminate]
BI --> M
#Stock transfer or sale: four designs for one need
Calder needs impellers from Augsburg's plant CP21 in Rockford's plant CP11. SAP offers several ways to model that, and they differ in which documents each company gets.
One system; selling sales org assigned to the supplying plant
Design D is for a different need: an outside customer of one company is served from another company's stock. If Hallam, a US distributor, ordered impellers from Calder's US sales organization CPUS and Augsburg shipped them directly, that would be an intercompany sale. The sample pack doesn't assign CPUS to plant CP21, so Calder hasn't set it up; the script flags this.
For design C, SAP's training on scope item 5HP describes the flow: the receiving company's intercompany purchase order creates the supplying company's sales order in the background. Goods issue moves stock into the supplying plant's stock in transit. On the transfer of ownership date in the outbound delivery, a job moves it to the receiving plant's stock in transit. The inbound delivery and, after billing, the supplier invoice are created automatically (SAP Learning). SAP's comparison of classic and advanced intercompany names what the classic flow lacks: no sales order or purchase order in the delivering company, no supplier invoice in the buying company, and no valuated stock in transit for the selling company (SAP Learning).
In an intercompany sale inside one system, two invoices leave the process. The selling company bills its customer as usual. The delivering company creates an internal invoice with billing type IV, which posts in its own company code. Pricing ties them together: condition PI01 on the customer invoice is statistical and carries the intercompany price for profitability analysis, and IV01 on the internal invoice references it and becomes the delivering company's revenue. VPRS carries the delivering company's cost (SAP Learning).
The buyer still needs a supplier invoice. In SAP S/4HANA Cloud Public Edition's scope item 1HO, the intercompany invoice is sent by IDoc and inbound processing posts an invoice receipt in the ordering company. The prerequisites include a customer-role partner for the ordering company in the supplier's company code, a supplier-role partner for the supplier in the ordering company, intercompany price condition records (PI02 in that scope item) and IDoc setup. The scope item covers companies sharing the same tenant, with IDoc processing in the same system and client (SAP Learning).
The mechanism is old. SAP's R/3 4.6C documentation describes the same idea: output type RD04 sends an INVOIC IDoc from the delivering company code, and inbound processing posts it to a vendor account in the selling company code (SAP Library, 4.6C). In ECC systems, that is still how many groups automate it.
#Transfer pricing: a business input the system must carry
The transfer price is set by the business with tax advisers, often as cost plus a markup or with reference to market prices. The system has two jobs: apply the price identically on both sides, and keep a view without it.
S/4HANA supports up to three parallel valuations. Legal valuation shows each company's view, markup included. Group valuation shows the group with markups removed. Profit center valuation treats profit centers as if they were independent, with negotiated prices. They can live in separate ledgers or as separate amount columns in one ledger, and are activated for the controlling area (SAP Learning).
At Calder, the policy says machined components sell at standard cost plus 8%. The impeller CP-IMP200 has a standard cost of 310.00 EUR in plant CP21, so the policy price is 334.80 EUR, or 361.58 USD at the course's closing rate. Both companies use 335.00 USD. Step 5 shows what that does: Augsburg earns almost no margin, and some of its profit is booked in Rockford. A group valuation across two systems isn't possible, so at Calder the group view comes from consolidation, which removes the profit still sitting in Rockford's stock.
Every intercompany amount exists in up to three currencies:
Currency
At Calder
Who uses it
Transaction (document) currency
USD on every impeller invoice
Both companies; matching compares it first
Local (company code) currency
EUR for CP20, USD for CP10
Each company's legal books
Group currency
USD
Consolidation, elimination
In S/4HANA, the universal journal stores the document currency (type 00) and the company code currency (type 10) by default, and you choose further currency types such as group currency (SAP Learning).
Because Augsburg bills in USD but keeps its books in EUR, its USD receivables change value every day in EUR. Month-end foreign currency valuation brings them to the closing rate (record to report covers the step). If Augsburg sends its numbers to the group before that valuation, its receivable translated back to USD no longer equals Rockford's payable. Revenue and expense are typically translated at an average rate, so they leave small differences even when everything is right. Group reporting's ICMR-integrated elimination can split a difference into transaction, translation and other parts (SAP Learning).
#Shared services: recharges, central purchasing and central payments
Not every intercompany flow is goods. Rockford runs IT for both companies and charges Augsburg 2,500 USD a month. Three patterns are common:
Recharges. One company bills another for a service, like Calder's IT charge. They need the same trading partner discipline as goods, because they are eliminated the same way.
Cross-company postings. Inside one system, one invoice can be split across company codes. S/4HANA creates a document per company code, adds clearing lines to receivable and payable clearing accounts for each pair of company codes, and links them with one cross-company code document number. SAP's training says to use it only when the transaction isn't tax-relevant or the companies form one taxable entity (SAP Learning).
Central payments and purchasing. The payment program can let one company code pay for another: the sending company code owns the items and the paying company code makes the payment (SAP Learning). Central purchasing lets one buying team work on requisitions and purchase orders from connected systems through an S/4HANA hub; SAP's training lists SAP S/4HANA, SAP S/4HANA Cloud and SAP ERP as connected systems (SAP Learning).
For Calder, cross-company postings and central payments need both companies in one system, so they wait for Augsburg's migration. A central purchasing hub could reach Augsburg's ECC system today; check the licensing with SAP.
ICMR compares the two sides. Its data sources include one that compares journal entries in the universal journal (ACDOCA) for pairs of company and trading partner, and one that compares ACDOCA with group reporting's consolidation journal (ACDOCU) for pairs of consolidation unit and partner unit. A matching method is a set of rules applied in order; items that don't match move to the next rule. Reconciliation cases sum the results with tolerances that set a status. Differences get reason codes, and adjustments can run through an approval workflow (SAP Learning).
Group reporting then eliminates. Integrated S/4HANA companies are released from ACDOCA into ACDOCU; companies on other systems are uploaded or entered through data collection; data is validated and translated into group currency; then eliminations run (SAP Learning). SAP's training lists separate elimination tasks for intercompany revenue and cost, other income and expense, dividends, payables and receivables (rule-based or ICMR-integrated) and profit in inventory. Elimination is two-sided and works on data already translated into group currency (SAP Learning). Payables and receivables are selected through the partner unit, so no dedicated intercompany accounts are needed, and interim eliminations reverse in the next period (SAP Learning).
The flow. Design A is the only option: a purchase order in S/4HANA, a sales order in ECC, two price records and two sets of partner master data that must agree. SAP's older documentation on stock transfers between two R/3 systems (written for transfers inside one company code) says purchase order and sales order can be kept in step only through IDocs, and that the issuing side can't post into the receiver's stock in transit by IDoc (SAP Library, 4.70). Rockford can't see goods on the truck.
The interfaces. SAP's purchasing EDI documentation lists ORDERS as the outbound purchase order and ORDRSP, DESADV and INVOIC as inbound confirmation, shipping notification and invoice (SAP Library). Each message is a place where the flow can stop. How SAP systems exchange these messages is covered in how SAP systems talk; how the integration lead specifies, tests and monitors them is covered in the integration lead's toolkit.
The close. Augsburg has no universal journal, so ICMR can't read its journal entries directly. Its figures reach group reporting by upload into ACDOCU, where the ACDOCA-against-ACDOCU data source can compare them with Rockford's postings, but only as current as the last package.
The future. SAP's training notes that advanced intercompany sales lists multi-system scenarios as planned, without details (SAP Learning). For Calder, whether Augsburg joins Rockford's system or gets its own decides whether designs B and C become possible; Landscape and deployment covers that choice.
You will write one script that looks at Calder's intercompany flow on 31 October. It runs pre-close checks on the pack's master data and the open items, compares four intercompany designs, shows the postings on each side for one impeller order, tests the transfer price against the policy, matches the two companies in transaction and group currency, works out what the group eliminates, and writes Calder's intercompany flow diagram with the documents and postings. An optional step reads billing documents from SAP's sandbox.
flowchart LR
P[calder_pack/<br/>company codes,<br/>partners, products] --> S[intercompany.py]
SC[Scenario in the script<br/>open items, rates,<br/>policy, prices] --> S
S --> C[Pre-close checks]
S --> F[--flows]
S --> PO[--postings]
S --> T[--transfer-price]
S --> M[--match]
S --> E[--eliminate]
S --> W[calder_intercompany_flow.md]
K[SAP sandbox<br/>optional] --> S
Rewrite the pack, so you start from the same data as this topic:
Windows (PowerShell):
python unit1b\make_calder_pack.py
macOS / Linux:
python unit1b/make_calder_pack.py
The last lines should read:
Calder sample pack 1.0: order 1000001, net 29,100.00 USD, cost 19,600.00 USD.
All data is fictional. Next: python check_unit1b.py
The script reads the company codes, the intercompany partners 10000010 (Rockford as a customer in CP20) and 20000010 (Augsburg as a supplier in CP10), the plant assignments of the sales organizations and the impeller's standard cost from the pack.
In VS Code, create a new file, paste the script below, and save it as intercompany.py inside the unit1b folder.
Read the blocks RATES, items(), PO_4500000014 and FLOWS first. They are Calder's intercompany situation on 31 October written as Python data. Every value is invented. The open items continue the earlier Unit 1B topics: purchase orders 4500000005, 4500000008 and 4500000014 from procure to pay and the quality topic, the invoices and IT charge from record to report. The exchange rates, the 8% policy, delivery 84000041 and sales order 12000417 are course values, not real rates or SAP numbers.
"""Intercompany at Calder: one impeller flow, both companies, the group view.
Calder Pumpworks is a fictional company. The pack (unit1b/calder_pack/) and the
scenario below (documents, prices, exchange rates, the transfer pricing policy,
amounts) are made up for the course. Field names follow SAP's released APIs
where the script mirrors one (API_BILLING_DOCUMENT_SRV, API_PURCHASEORDER_PROCESS_SRV,
API_BUSINESS_PARTNER), but the logic is a teaching sketch, not SAP's. Reason
codes, account names and the policy are course labels, not SAP values.
Run it from your course folder:
python unit1b/intercompany.py pre-close intercompany checks, 2026-10-31
python unit1b/intercompany.py --flows the same impeller order in four intercompany designs
python unit1b/intercompany.py --postings documents and postings on each side for PO 4500000005
python unit1b/intercompany.py --transfer-price the impeller price against Calder's policy
python unit1b/intercompany.py --match match CP10 against CP20 in transaction and group currency
python unit1b/intercompany.py --eliminate what the group eliminates for October
python unit1b/intercompany.py --write write unit1b/calder_intercompany_flow.md
python unit1b/intercompany.py --sandbox read billing documents from SAP's sandbox (needs SAP_API_KEY)
Steps without --sandbox use only Python's built-in modules and need no account and no network.
"""
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
HERE = Path(__file__).resolve().parent
PACK = HERE / "calder_pack"
PERIOD = "2026/010" # fiscal year 2026, period 10 (October)
KEY_DATE = "2026-10-31"
MATCH_TOLERANCE = 10.00 # USD; differences up to this are accepted
ROUNDING = 0.05 # USD; translation differences below this are rounding
# ------------------------------------------------------------ the three fixes (exercise)
# CP20 posted CP10's IT recharge without a trading partner. The exercise sets "CP10".
CP20_IT_RECHARGE_PARTNER = ""
# CP20 built its October package before running foreign currency valuation.
CP20_FCV_POSTED = False
# The impeller price both companies use, in USD. The exercise tries the policy price.
IMPELLER_PRICE_USD = 335.00
# ------------------------------------------------------------ rates and policy (fictional)
# USD per 1 EUR. Course values, not real market rates.
RATES = {"2026-10-07": 1.0850, "2026-10-30": 1.0790, "closing": 1.0800, "average": 1.0820}
MARKUP = 0.08 # Calder's policy: machined components at standard cost plus 8%
STOCK_AT_CP11 = 30 # CP-IMP200 bought from CP20 and still in Rockford stock on 2026-10-31
OCTOBER_VOLUME = 80 # impellers CP20 invoiced to CP10 in October
def money(x: float) -> str:
return f"{x:,.2f}"
def r2(x: float) -> float:
return round(x + 0.0, 2)
# ------------------------------------------------------------ the pack
def load_pack() -> dict:
pack = {}
for name in ("org_units.json", "business_partners.json", "products.json"):
try:
pack[name] = json.loads((PACK / name).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as err:
sys.exit(f"Cannot read {PACK / name} ({err}). Run: python unit1b/make_calder_pack.py")
codes = {c["CompanyCode"]: c for c in pack["org_units.json"]["CompanyCodes"]}
if not {"CP10", "CP20"} <= set(codes):
sys.exit("Company codes CP10 and CP20 are missing from the pack. Run make_calder_pack.py again.")
partners = {bp["BusinessPartner"]: bp for bp in pack["business_partners.json"]["BusinessPartners"]}
if not {"10000010", "20000010"} <= set(partners):
sys.exit("Intercompany partners 10000010 and 20000010 are missing. Run make_calder_pack.py again.")
return pack
def partner_facts(pack: dict) -> dict:
"""The intercompany customer in CP20 and supplier in CP10, read from the pack."""
bps = {bp["BusinessPartner"]: bp for bp in pack["business_partners.json"]["BusinessPartners"]}
cust, supp = bps["10000010"], bps["20000010"]
c_co = cust["to_Customer"]["to_CustomerCompany"][0]
c_sa = cust["to_Customer"]["to_CustomerSalesArea"][0]
s_co = supp["to_Supplier"]["to_SupplierCompany"][0]
s_po = supp["to_Supplier"]["to_SupplierPurchasingOrg"][0]
return {
"cust_company": c_co["CompanyCode"], "cust_terms": c_co["PaymentTerms"],
"cust_recon": c_co["ReconciliationAccount"], "cust_currency": c_sa["Currency"],
"cust_tp": cust.get("TradingPartner", ""),
"supp_company": s_co["CompanyCode"], "supp_terms": s_co["PaymentTerms"],
"supp_recon": s_co["ReconciliationAccount"], "supp_currency": s_po["PurchaseOrderCurrency"],
"supp_tp": supp.get("TradingPartner", ""),
}
def impeller_cost_eur(pack: dict) -> float:
for p in pack["products.json"]["Products"]:
if p["Product"] == "CP-IMP200":
for v in p["to_Valuation"]:
if v["ValuationArea"] == "CP21":
return float(v["StandardPrice"])
sys.exit("CP-IMP200 has no valuation in plant CP21 in the pack. Run make_calder_pack.py again.")
def cpus_can_ship_from_cp21(pack: dict) -> bool:
return any(a["SalesOrganization"] == "CPUS" and a["Plant"] == "CP21"
for a in pack["org_units.json"]["SalesOrgPlants"])
# ------------------------------------------------------------ open intercompany items (fictional)
# Shaped like journal entry items: company code, document, reference, partner company,
# amount in transaction currency (USD) and the rate it was booked at (CP20 only).
def items() -> list:
p = IMPELLER_PRICE_USD * 40
return [
{"CompanyCode": "CP10", "Document": "5105600004", "Reference": "95000041", "PartnerCompany": "CP20",
"Amount": -p, "Rate": None, "Kind": "goods", "Text": "supplier invoice, 40 x CP-IMP200, PO 4500000005"},
{"CompanyCode": "CP10", "Document": "90000051", "Reference": "90000051", "PartnerCompany": "CP20",
"Amount": 2500.00, "Rate": None, "Kind": "service", "Text": "IT service recharge October"},
{"CompanyCode": "CP20", "Document": "95000041", "Reference": "95000041", "PartnerCompany": "CP10",
"Amount": p, "Rate": RATES["2026-10-07"], "Kind": "goods", "Text": "invoice, 40 x CP-IMP200, shipped 2026-10-07"},
{"CompanyCode": "CP20", "Document": "95000047", "Reference": "95000047", "PartnerCompany": "CP10",
"Amount": p, "Rate": RATES["2026-10-30"], "Kind": "goods", "Text": "invoice, 40 x CP-IMP200, PO 4500000008, shipped 2026-10-30"},
{"CompanyCode": "CP20", "Document": "1900000388", "Reference": "90000051",
"PartnerCompany": CP20_IT_RECHARGE_PARTNER, "Amount": -2500.00, "Rate": RATES["2026-10-30"],
"Kind": "service", "Text": "IT service Calder Inc. October"},
]
def local_eur(it: dict) -> float:
"""CP20's local amount: booked at the document rate, revalued at closing if FCV ran."""
rate = RATES["closing"] if CP20_FCV_POSTED else it["Rate"]
return r2(it["Amount"] / rate)
def group_usd(it: dict) -> float:
"""Group currency is USD. CP10 is already USD; CP20's EUR balance is translated at closing."""
if it["CompanyCode"] == "CP10":
return it["Amount"]
return r2(local_eur(it) * RATES["closing"])
# ------------------------------------------------------------ the open price on PO 4500000014
# The same order lives in two systems: CP10's purchase order (S/4HANA) and CP20's sales
# order (ECC), each priced from its own copy of the intercompany price.
PO_4500000014 = {"Product": "CP-IMP300", "Quantity": 1, "CP10_PO_Price": 455.00,
"CP20_SalesOrder": "12000417", "CP20_SO_Price": 480.00, "Due": "2026-11-04"}
# ------------------------------------------------------------ default: pre-close checks
def checks(pack: dict) -> list:
f = partner_facts(pack)
cost = impeller_cost_eur(pack)
policy_usd = r2(r2(cost * (1 + MARKUP)) * RATES["closing"])
found = []
if not f["cust_tp"] or not f["supp_tp"]:
found.append(("Trading partner", "master data",
"10000010 (customer in CP20) and 20000010 (supplier in CP10) carry no TradingPartner "
"in the pack, so every posting depends on the person or interface adding it",
"Master data, both companies"))
if not CP20_IT_RECHARGE_PARTNER:
found.append(("Trading partner", "posting",
"CP20 document 1900000388 (IT recharge, -2,500.00 USD) has no partner company",
"Augsburg finance"))
if f["cust_terms"] != f["supp_terms"]:
found.append(("Payment terms", "master data",
f"CP20 expects {f['cust_terms']} from customer 10000010; CP10 pays supplier 20000010 "
f"on {f['supp_terms']}: CP20 will see invoices overdue that CP10 hasn't scheduled",
"Both controllers, then master data"))
if f["cust_currency"] != f["supp_currency"]:
found.append(("Currency", "master data",
f"CP10 orders in {f['supp_currency']}; CP20's sales area for 10000010 says "
f"{f['cust_currency']}, so each order needs its currency overwritten",
"Master data, CP20 sales"))
if PO_4500000014["CP10_PO_Price"] != PO_4500000014["CP20_SO_Price"]:
d = PO_4500000014["CP20_SO_Price"] - PO_4500000014["CP10_PO_Price"]
found.append(("Price", "open order",
f"PO 4500000014 (CP-IMP300) at {money(PO_4500000014['CP10_PO_Price'])} USD in CP10; "
f"CP20 sales order {PO_4500000014['CP20_SalesOrder']} at "
f"{money(PO_4500000014['CP20_SO_Price'])}: the invoice due {PO_4500000014['Due']} "
f"will be {money(d)} USD off and stop in CP10's inbound queue",
"CP10 buyer and CP20 sales"))
if abs(IMPELLER_PRICE_USD - policy_usd) > 0.01:
found.append(("Transfer price", "policy",
f"CP-IMP200 sells at {money(IMPELLER_PRICE_USD)} USD; the cost-plus-8% policy gives "
f"{money(policy_usd)} USD at the closing rate",
"Group tax, both controllers"))
if not CP20_FCV_POSTED:
found.append(("Currency", "close",
"CP20's package was built before foreign currency valuation: its USD items "
"sit at invoice-date rates", "Augsburg finance"))
found.append(("Timing", "in transit",
"CP20 invoice 95000047 (40 impellers) shipped 2026-10-30; CP10 has no goods receipt "
"or invoice yet", "Intercompany accountants, both sides"))
if not cpus_can_ship_from_cp21(pack):
found.append(("Design", "org structure",
"Sales org CPUS isn't assigned to plant CP21, so a US customer order can't be "
"delivered from Augsburg as an intercompany sale", "Solution architect (by design today)"))
return found
def print_checks(pack: dict) -> None:
print(f"Intercompany pre-close checks CP10 <-> CP20, key date {KEY_DATE} (fictional)\n")
for i, (area, kind, what, owner) in enumerate(checks(pack), 1):
print(f" {i}. [{area}, {kind}] {what}")
print(f" owner: {owner}")
print("\n Synthetic data for a fictional company, not from an SAP system.")
# ------------------------------------------------------------ --flows
FLOWS = [
("A. Two-system buy and sell (Calder today)", "CP10 on S/4HANA, CP20 on ECC: any landscape",
[("CP10", "purchase order 4500000005 to supplier 20000010"),
("link", "EDI order (ORDERS) to CP20"),
("CP20", "sales order for customer 10000010, delivery, goods issue"),
("link", "optional shipping notice (DESADV) to CP10"),
("CP20", "customer invoice 95000041"),
("link", "EDI invoice (INVOIC) to CP10"),
("CP10", "goods receipt, supplier invoice 5105600004 against the PO")],
"Two price copies, two sets of partner data, three interfaces; goods in transit invisible to CP10"),
("B. Intercompany stock transport order", "both company codes in one system and client",
[("CP10", "stock transport order, supplying plant CP21"),
("CP20", "outbound delivery, goods issue (stock in transit)"),
("CP10", "goods receipt"),
("CP20", "internal invoice, billing type IV"),
("CP10", "supplier invoice: manual, or posted automatically by IDoc in the same system")],
"One order, no EDI between companies; needs CP20 on the same system as CP10"),
("C. Advanced intercompany stock transfer (S/4HANA)", "one system; see the edition notes in the topic",
[("CP10", "intercompany purchase order"),
("CP20", "intercompany sales order created automatically, delivery, goods issue"),
("both", "transfer of ownership posted on its date: valuated stock in transit"),
("CP10", "inbound delivery created automatically, goods receipt"),
("CP20", "intercompany customer invoice"),
("CP10", "intercompany supplier invoice created automatically")],
"Ownership and in-transit stock visible in both companies; S/4HANA only"),
("D. Intercompany sale to a customer", "one system; sales org assigned to the other company's plant",
[("CP10", "sales order from Hallam in CPUS, delivering plant CP21"),
("CP20", "outbound delivery and goods issue from Augsburg"),
("CP10", "customer invoice to Hallam (intercompany price PI01 is statistical)"),
("CP20", "internal invoice IV to CP10 (condition IV01)"),
("CP10", "supplier invoice from CP20")],
"For direct shipments to customers, not for replenishing Rockford's stock"),
]
def print_flows(pack: dict) -> None:
print("The same need, four designs: impellers from Augsburg (CP21, company CP20) for Rockford\n")
for title, needs, steps, note in FLOWS:
print(f" {title}")
print(f" needs: {needs}")
for who, what in steps:
print(f" {who:<5} {what}")
print(f" trade-off: {note}\n")
codes = {c["CompanyCode"]: c["System"] for c in pack["org_units.json"]["CompanyCodes"]}
print(f" Pack: CP10 runs {codes['CP10']}, CP20 runs {codes['CP20']}: only design A works today.")
if not cpus_can_ship_from_cp21(pack):
print(" Pack: CPUS isn't assigned to plant CP21, so design D isn't set up either.")
# ------------------------------------------------------------ --postings
def postings(pack: dict) -> dict:
f = partner_facts(pack)
cost = impeller_cost_eur(pack)
rate = RATES["2026-10-07"]
rev_eur = r2(IMPELLER_PRICE_USD * 40 / rate)
cogs_eur = r2(cost * 40)
gr_usd = r2(IMPELLER_PRICE_USD * 36)
inv_usd = r2(IMPELLER_PRICE_USD * 40)
fcv = r2(IMPELLER_PRICE_USD * 40 / RATES["closing"] - rev_eur)
cp20 = [
("Goods issue, delivery 84000041 (2026-10-07)", [
("Cost of goods sold", cogs_eur, ""),
("Inventory, machined components", -cogs_eur, "")]),
("Invoice 95000041 to customer 10000010", [
(f"Receivable 10000010 (recon {f['cust_recon']})", rev_eur, "CP10"),
("Revenue, intercompany", -rev_eur, "CP10")]),
(f"Foreign currency valuation {KEY_DATE}" + ("" if CP20_FCV_POSTED else " (not yet run)"), [
("Receivable adjustment", fcv, "CP10"),
("Exchange rate gain" if fcv >= 0 else "Exchange rate loss", -fcv, "")]),
]
cp10 = [
("Goods receipt 4900000008, 36 of 40 (2026-10-08)", [
("Inventory, CP-IMP200 at the PO price", gr_usd, ""),
("GR/IR clearing", -gr_usd, "")]),
("Supplier invoice 5105600004, 40 invoiced (2026-10-10)", [
("GR/IR clearing", inv_usd, ""),
(f"Payable 20000010 (recon {f['supp_recon']})", -inv_usd, "CP20")]),
]
return {"cp20": cp20, "cp10": cp10, "rev_eur": rev_eur, "cogs_eur": cogs_eur,
"gr_usd": gr_usd, "inv_usd": inv_usd, "fcv": fcv}
def print_postings(pack: dict) -> None:
p = postings(pack)
print("PO 4500000005: 40 x CP-IMP200, documents and postings on each side (fictional)\n")
for name, cur, block in (("CP20 Augsburg, ECC, local currency", "EUR", p["cp20"]),
("CP10 Rockford, S/4HANA, local currency", "USD", p["cp10"])):
print(f" {name} {cur}")
for doc, lines in block:
print(f" {doc}")
for acct, amt, tp in lines:
side = "Dr" if amt >= 0 else "Cr"
print(f" {side} {acct:<42}{money(abs(amt)):>12} partner {tp or '-'}")
print()
margin = r2(p["rev_eur"] - p["cogs_eur"])
print(f" CP20 margin on the shipment: {money(p['rev_eur'])} - {money(p['cogs_eur'])} = {money(margin)} EUR")
print(f" CP10 GR/IR left open: {money(p['inv_usd'] - p['gr_usd'])} USD debit (4 impellers invoiced, not received)")
print(" Invoice in USD at 1.0850 USD per EUR (2026-10-07). Rates and accounts are course values.")
print("\n Synthetic numbers for a fictional company, not from an SAP system.")
# ------------------------------------------------------------ --transfer-price
def transfer_price(pack: dict) -> dict:
cost = impeller_cost_eur(pack)
policy_eur = r2(cost * (1 + MARKUP))
out = {"cost": cost, "policy_eur": policy_eur, "policy_usd": r2(policy_eur * RATES["closing"])}
out["actual_eur"] = r2(IMPELLER_PRICE_USD / RATES["closing"])
out["markup"] = (out["actual_eur"] - cost) / cost
out["gap"] = r2(out["policy_usd"] - IMPELLER_PRICE_USD)
out["volume_gap"] = r2(out["gap"] * OCTOBER_VOLUME)
out["profit_each"] = r2(IMPELLER_PRICE_USD - cost * RATES["closing"])
out["profit_policy_each"] = r2(out["policy_usd"] - cost * RATES["closing"])
return out
def print_transfer_price(pack: dict) -> None:
t = transfer_price(pack)
print("Transfer price for CP-IMP200, CP20 to CP10, at the closing rate 1.0800 USD per EUR (fictional)\n")
print(f" CP21 standard cost (pack) {money(t['cost']):>10} EUR")
print(f" Policy: cost plus {MARKUP:.0%} {money(t['policy_eur']):>10} EUR = {money(t['policy_usd'])} USD")
print(f" Price on PO and invoice {money(IMPELLER_PRICE_USD):>10} USD = {money(t['actual_eur'])} EUR")
print(f" Markup actually earned {t['markup']:>10.2%}")
print(f" Gap per impeller {money(t['gap']):>10} USD")
print(f" Gap on October's {OCTOBER_VOLUME} impellers {money(t['volume_gap']):>10} USD of profit in CP10 instead of CP20")
print()
print(f" Group view: profit in CP10's stock of {STOCK_AT_CP11} impellers to eliminate")
print(f" at today's price {STOCK_AT_CP11} x {money(t['profit_each'])} = {money(STOCK_AT_CP11 * t['profit_each'])} USD")
print(f" at the policy price {STOCK_AT_CP11} x {money(t['profit_policy_each'])} = "
f"{money(r2(STOCK_AT_CP11 * t['profit_policy_each']))} USD")
print("\n The policy and the rates are course values; transfer pricing rules come from tax advisers.")
print(" Synthetic numbers for a fictional company, not from an SAP system.")
# ------------------------------------------------------------ --match
def match() -> list:
"""Rules in order, like a matching method: what one rule doesn't match moves on."""
its = items()
cp10 = [i for i in its if i["CompanyCode"] == "CP10"]
cp20 = [i for i in its if i["CompanyCode"] == "CP20"]
used, results = set(), []
for a in cp10: # rule 1: partner pair, reference, amount
for b in cp20:
if id(b) in used or b["PartnerCompany"] != "CP10" or a["PartnerCompany"] != "CP20":
continue
if a["Reference"] == b["Reference"] and abs(a["Amount"] + b["Amount"]) <= MATCH_TOLERANCE:
used |= {id(a), id(b)}
results.append(("matched (rule 1: partner, reference, amount)", [a, b], ""))
for a in cp10: # rule 2: reference and amount, partner missing
if id(a) in used:
continue
for b in cp20:
if id(b) in used:
continue
if a["Reference"] == b["Reference"] and abs(a["Amount"] + b["Amount"]) <= MATCH_TOLERANCE:
used |= {id(a), id(b)}
results.append(("exception (rule 2: trading partner missing)", [a, b], "MISSING_TP"))
for x in its: # what is left is open on one side
if id(x) not in used:
results.append((f"open on {x['CompanyCode']} only", [x], "TIMING"))
return results
def print_match() -> None:
print(f"Intercompany reconciliation CP10 <-> CP20, period {PERIOD} (fictional)")
fcv = "after" if CP20_FCV_POSTED else "BEFORE"
print(f"Transaction currency USD; group currency USD; CP20's package built {fcv} currency valuation\n")
open_tc = translation = 0.0
for label, group, reason in match():
print(f" {label}")
for it in group:
lc = f"{money(local_eur(it)):>10} EUR" if it["CompanyCode"] == "CP20" else " " * 14
print(f" {it['CompanyCode']} {it['Document']:<11} partner {it['PartnerCompany'] or '(none)':<7}"
f"{money(it['Amount']):>11} USD {lc} {money(group_usd(it)):>11} grp {it['Text']}")
tc = r2(sum(i["Amount"] for i in group))
gc = r2(sum(group_usd(i) for i in group))
tr = r2(gc - tc) if len(group) == 2 else 0.0
if len(group) == 2 and abs(tr) > ROUNDING:
reason = (reason + " + " if reason else "") + "TRANSLATION"
translation += tr
if reason:
print(f" difference {money(tc)} USD in transaction currency, {money(gc)} in group currency; "
f"reason {reason}")
if len(group) == 1:
open_tc += abs(tc)
print()
print(f" Open in transaction currency: {money(open_tc)} USD (timing). Translation differences: "
f"{money(abs(translation))} USD.")
if not CP20_IT_RECHARGE_PARTNER:
print(" The IT recharge can't be eliminated until CP20 reposts it with partner CP10.")
print(" Reason codes are course labels. Synthetic numbers, not from an SAP system.")
# ------------------------------------------------------------ --eliminate
def eliminations(pack: dict) -> list:
t = transfer_price(pack)
its = {i["Document"]: i for i in items()}
rows = []
a, b = its["5105600004"], its["95000041"]
rows.append(("Receivable and payable, invoice 95000041", group_usd(b), r2(group_usd(a) + group_usd(b)),
"eliminated" if abs(group_usd(a) + group_usd(b)) <= MATCH_TOLERANCE else "translation difference to clear"))
transit = group_usd(its["95000047"])
rows.append(("Receivable 95000047, goods in transit", transit, r2(transit - IMPELLER_PRICE_USD * 40),
"group adds goods in transit on CP10's side, then eliminates"))
it_ok = bool(CP20_IT_RECHARGE_PARTNER)
rows.append(("IT recharge 90000051 / 1900000388", 2500.00 if it_ok else 0.0, 0.0 if it_ok else 2500.00,
"eliminated" if it_ok else "not eliminated: CP20 side has no partner"))
rev_eur = r2(sum(i["Amount"] / i["Rate"] for d, i in its.items() if d in ("95000041", "95000047")))
rev_grp = r2(rev_eur * RATES["average"])
buy = r2(IMPELLER_PRICE_USD * 80)
rows.append(("Revenue CP20 against purchases CP10 (80 impellers)", rev_grp, r2(rev_grp - buy),
f"CP20 revenue {money(rev_eur)} EUR at the average rate; difference is translation"))
rows.append((f"Profit in CP10 stock ({STOCK_AT_CP11} impellers)", r2(STOCK_AT_CP11 * t["profit_each"]), 0.0,
"removed from inventory and profit in the group view"))
return rows
def print_eliminate(pack: dict) -> None:
print("Group eliminations for October, group currency USD (fictional)\n")
print(f" {'What':<52}{'Eliminated':>12}{'Left':>11} Note")
for what, amt, left, note in eliminations(pack):
print(f" {what:<52}{money(amt):>12}{money(left):>11} {note}")
print("\n Group reporting eliminates from data in group currency, after translation and matching.")
print(" Synthetic numbers for a fictional company, not from an SAP system.")
# ------------------------------------------------------------ --write
def write_flow(pack: dict) -> Path:
p = postings(pack)
out = HERE / "calder_intercompany_flow.md"
lines = [
"# Calder intercompany flow: impellers from Augsburg to Rockford",
"",
f"Fictional company, course data. Period {PERIOD}, key date {KEY_DATE}. "
"CP20 (Augsburg) runs ECC; CP10 (Rockford) runs S/4HANA.",
"",
"```mermaid",
"sequenceDiagram",
" participant P as CP10 purchasing (S/4HANA)",
" participant A as CP10 finance",
" participant S as CP20 sales (ECC)",
" participant F as CP20 finance",
" P->>S: PO 4500000005 via EDI order",
" S->>S: Sales order, delivery 84000041",
" S->>F: Goods issue: COGS / inventory",
" S->>F: Invoice 95000041: receivable / revenue (partner CP10)",
" S->>P: EDI invoice",
" P->>A: Goods receipt 36: inventory / GR/IR",
" P->>A: Supplier invoice 5105600004: GR/IR / payable (partner CP20)",
" F-->>A: Month end: match, reason codes, group eliminations",
"```",
"",
"## Postings on each side",
"",
"| Company | Document | Debit | Credit | Amount | Partner |",
"| --- | --- | --- | --- | --- | --- |",
]
for comp, cur, block in (("CP20", "EUR", p["cp20"]), ("CP10", "USD", p["cp10"])):
for doc, ls in block:
dr = next(a for a, amt, _ in ls if amt >= 0)
cr = next(a for a, amt, _ in ls if amt < 0)
tp = next((t for _, _, t in ls if t), "-")
lines.append(f"| {comp} | {doc} | {dr} | {cr} | {money(abs(ls[0][1]))} {cur} | {tp} |")
lines += ["", "## Pre-close findings", "", "| Area | Kind | Finding | Owner |", "| --- | --- | --- | --- |"]
for area, kind, what, owner in checks(pack):
lines.append(f"| {area} | {kind} | {what} | {owner} |")
lines += ["", "## Group eliminations (USD)", "", "| What | Eliminated | Left | Note |", "| --- | --- | --- | --- |"]
for what, amt, left, note in eliminations(pack):
lines.append(f"| {what} | {money(amt)} | {money(left)} | {note} |")
lines += ["", "Synthetic numbers for a fictional company, not from an SAP system."]
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
return out
# --------------------------------------------------------------- sandbox (optional)
def read_env_key(name: str) -> str:
value = os.environ.get(name, "")
env = Path.cwd() / ".env"
if not value and env.exists():
for line in env.read_text(encoding="utf-8").splitlines():
if line.strip().startswith(f"{name}="):
value = line.split("=", 1)[1].strip().strip('"').strip("'")
return value
def sandbox() -> None:
key = read_env_key("SAP_API_KEY")
if not key:
sys.exit("SAP_API_KEY is not set. Add it to .env (course setup) or skip this step.")
base = ("https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/"
"API_BILLING_DOCUMENT_SRV/A_BillingDocument")
query = urllib.parse.urlencode({"$top": "50", "$format": "json",
"$select": "BillingDocument,BillingDocumentType,CompanyCode,"
"SoldToParty,PartnerCompany,TotalNetAmount,TransactionCurrency"})
req = urllib.request.Request(f"{base}?{query}", headers={"APIKey": key, "Accept": "application/json"})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as err:
sys.exit(f"Sandbox answered HTTP {err.code}. Check the key, or try again later.")
except (urllib.error.URLError, TimeoutError) as err:
sys.exit(f"Cannot reach the sandbox ({err}). Check the network or proxy.")
docs = data.get("d", {}).get("results", [])
print(f"SAP sandbox: {len(docs)} billing documents read (SAP demo data, not Calder's)\n")
by_type = {}
for d in docs:
key_t = (d.get("BillingDocumentType", ""), bool(d.get("PartnerCompany")))
by_type[key_t] = by_type.get(key_t, 0) + 1
for (btype, has_partner), n in sorted(by_type.items()):
print(f" type {btype:<6} partner company {'set' if has_partner else 'empty':<6} {n:>4} documents")
print("\n A billing document with a partner company was billed to another company of the group.")
# --------------------------------------------------------------- main
def main() -> None:
parser = argparse.ArgumentParser(description="Intercompany at Calder (fictional data).")
parser.add_argument("--flows", action="store_true", help="four intercompany designs for one need")
parser.add_argument("--postings", action="store_true", help="documents and postings on each side")
parser.add_argument("--transfer-price", action="store_true", help="the impeller price against the policy")
parser.add_argument("--match", action="store_true", help="match CP10 against CP20")
parser.add_argument("--eliminate", action="store_true", help="what the group eliminates")
parser.add_argument("--write", action="store_true", help="write unit1b/calder_intercompany_flow.md")
parser.add_argument("--sandbox", action="store_true", help="read billing documents from SAP's sandbox")
args = parser.parse_args()
if args.sandbox:
sandbox()
return
pack = load_pack()
if args.flows:
print_flows(pack)
elif args.postings:
print_postings(pack)
elif args.transfer_price:
print_transfer_price(pack)
elif args.match:
print_match()
elif args.eliminate:
print_eliminate(pack)
else:
print_checks(pack)
if args.write:
print(f"\nwrote unit1b/{write_flow(pack).name}")
if __name__ == "__main__":
main()
Intercompany pre-close checks CP10 <-> CP20, key date 2026-10-31 (fictional)
1. [Trading partner, master data] 10000010 (customer in CP20) and 20000010 (supplier in CP10) carry no TradingPartner in the pack, so every posting depends on the person or interface adding it
owner: Master data, both companies
2. [Trading partner, posting] CP20 document 1900000388 (IT recharge, -2,500.00 USD) has no partner company
owner: Augsburg finance
3. [Payment terms, master data] CP20 expects NT30 from customer 10000010; CP10 pays supplier 20000010 on NT45: CP20 will see invoices overdue that CP10 hasn't scheduled
owner: Both controllers, then master data
4. [Currency, master data] CP10 orders in USD; CP20's sales area for 10000010 says EUR, so each order needs its currency overwritten
owner: Master data, CP20 sales
5. [Price, open order] PO 4500000014 (CP-IMP300) at 455.00 USD in CP10; CP20 sales order 12000417 at 480.00: the invoice due 2026-11-04 will be 25.00 USD off and stop in CP10's inbound queue
owner: CP10 buyer and CP20 sales
6. [Transfer price, policy] CP-IMP200 sells at 335.00 USD; the cost-plus-8% policy gives 361.58 USD at the closing rate
owner: Group tax, both controllers
7. [Currency, close] CP20's package was built before foreign currency valuation: its USD items sit at invoice-date rates
owner: Augsburg finance
8. [Timing, in transit] CP20 invoice 95000047 (40 impellers) shipped 2026-10-30; CP10 has no goods receipt or invoice yet
owner: Intercompany accountants, both sides
9. [Design, org structure] Sales org CPUS isn't assigned to plant CP21, so a US customer order can't be delivered from Augsburg as an intercompany sale
owner: Solution architect (by design today)
Synthetic data for a fictional company, not from an SAP system.
How to read it:
Findings 1, 3 and 4 come from the pack itself. The script reads the intercompany customer and supplier and finds no TradingPartner field, payment terms NT30 on one side and NT45 on the other, and a sales-area currency of EUR for a partner that orders in USD. These are the master data gaps behind most intercompany differences.
Finding 5 is a price mismatch caught before it costs anything. The same order lives in two systems with two prices. Today it is a 25.00 USD difference on one impeller; on 4 November it becomes an invoice stuck in Rockford's inbound queue.
Finding 9 is a design fact, not a defect. Without CPUS assigned to plant CP21, a US customer can't be served directly from Augsburg as an intercompany sale. Calder hasn't needed it so far.
A valid "empty" result would be only the timing line and the design line: in-transit goods at month end are normal.
The output lists the four designs from How it works with the documents in each company, and ends with:
Pack: CP10 runs S/4HANA, CP20 runs ECC: only design A works today.
Pack: CPUS isn't assigned to plant CP21, so design D isn't set up either.
Now show the postings for one order:
Windows (PowerShell):
python unit1b\intercompany.py --postings
macOS / Linux:
python unit1b/intercompany.py --postings
You should see:
PO 4500000005: 40 x CP-IMP200, documents and postings on each side (fictional)
CP20 Augsburg, ECC, local currency EUR
Goods issue, delivery 84000041 (2026-10-07)
Dr Cost of goods sold 12,400.00 partner -
Cr Inventory, machined components 12,400.00 partner -
Invoice 95000041 to customer 10000010
Dr Receivable 10000010 (recon 12900) 12,350.23 partner CP10
Cr Revenue, intercompany 12,350.23 partner CP10
Foreign currency valuation 2026-10-31 (not yet run)
Dr Receivable adjustment 57.18 partner CP10
Cr Exchange rate gain 57.18 partner -
CP10 Rockford, S/4HANA, local currency USD
Goods receipt 4900000008, 36 of 40 (2026-10-08)
Dr Inventory, CP-IMP200 at the PO price 12,060.00 partner -
Cr GR/IR clearing 12,060.00 partner -
Supplier invoice 5105600004, 40 invoiced (2026-10-10)
Dr GR/IR clearing 13,400.00 partner -
Cr Payable 20000010 (recon 21900) 13,400.00 partner CP20
CP20 margin on the shipment: 12,350.23 - 12,400.00 = -49.77 EUR
CP10 GR/IR left open: 1,340.00 USD debit (4 impellers invoiced, not received)
Invoice in USD at 1.0850 USD per EUR (2026-10-07). Rates and accounts are course values.
Synthetic numbers for a fictional company, not from an SAP system.
How to read it:
Each side posts in its own currency. Augsburg books the USD invoice in EUR at the invoice-date rate; Rockford books in USD. Only the trading partner lines link them.
Augsburg loses 49.77 EUR on the shipment. At 1.0850 USD per EUR, 335.00 USD is less than the impeller's standard cost of 310.00 EUR. The transfer price was set in USD and hasn't followed the exchange rate.
Rockford's GR/IR account keeps 1,340.00 USD open. Augsburg invoiced 40 impellers; Rockford has received 36. That is a procure-to-pay question inside CP10 (the invoice is blocked for quantity), not an intercompany difference: both companies show 13,400.00 USD.
The valuation line is shown but not posted yet. Step 7 shows what that does to the group view.
Transfer price for CP-IMP200, CP20 to CP10, at the closing rate 1.0800 USD per EUR (fictional)
CP21 standard cost (pack) 310.00 EUR
Policy: cost plus 8% 334.80 EUR = 361.58 USD
Price on PO and invoice 335.00 USD = 310.19 EUR
Markup actually earned 0.06%
Gap per impeller 26.58 USD
Gap on October's 80 impellers 2,126.40 USD of profit in CP10 instead of CP20
Group view: profit in CP10's stock of 30 impellers to eliminate
at today's price 30 x 0.20 = 6.00 USD
at the policy price 30 x 26.78 = 803.40 USD
The policy and the rates are course values; transfer pricing rules come from tax advisers.
Synthetic numbers for a fictional company, not from an SAP system.
The policy price is 361.58 USD; Calder charges 335.00. On October's 80 impellers, 2,126.40 USD of profit is booked in the US instead of Germany. The second block is the group view: profit that is still in Rockford's stock hasn't been earned by the group and is eliminated. At today's price there is almost none; at the policy price it would be 803.40 USD.
Intercompany reconciliation CP10 <-> CP20, period 2026/010 (fictional)
Transaction currency USD; group currency USD; CP20's package built BEFORE currency valuation
matched (rule 1: partner, reference, amount)
CP10 5105600004 partner CP20 -13,400.00 USD -13,400.00 grp supplier invoice, 40 x CP-IMP200, PO 4500000005
CP20 95000041 partner CP10 13,400.00 USD 12,350.23 EUR 13,338.25 grp invoice, 40 x CP-IMP200, shipped 2026-10-07
difference 0.00 USD in transaction currency, -61.75 in group currency; reason TRANSLATION
exception (rule 2: trading partner missing)
CP10 90000051 partner CP20 2,500.00 USD 2,500.00 grp IT service recharge October
CP20 1900000388 partner (none) -2,500.00 USD -2,316.96 EUR -2,502.32 grp IT service Calder Inc. October
difference 0.00 USD in transaction currency, -2.32 in group currency; reason MISSING_TP + TRANSLATION
open on CP20 only
CP20 95000047 partner CP10 13,400.00 USD 12,418.91 EUR 13,412.42 grp invoice, 40 x CP-IMP200, PO 4500000008, shipped 2026-10-30
difference 13,400.00 USD in transaction currency, 13,412.42 in group currency; reason TIMING
Open in transaction currency: 13,400.00 USD (timing). Translation differences: 64.07 USD.
The IT recharge can't be eliminated until CP20 reposts it with partner CP10.
Reason codes are course labels. Synthetic numbers, not from an SAP system.
How to read it:
Rules run in order, like an ICMR matching method. Rule 1 needs the partner pair, the same reference and amounts that cancel within 10.00 USD. Rule 2 takes what has the same reference and amount but no partner. What is left is open on one side.
In transaction currency, the impeller invoice matches exactly. In group currency it is 61.75 USD off, because Augsburg's EUR amount still sits at the invoice-date rate. Strictly, that is a missing valuation in Augsburg's books that shows up as a translation difference.
The IT charge has both problems: no partner, and the same currency effect.
The in-transit invoice is a timing difference. It is real, explained and labeled, not chased as an error.
Group eliminations for October, group currency USD (fictional)
What Eliminated Left Note
Receivable and payable, invoice 95000041 13,338.25 -61.75 translation difference to clear
Receivable 95000047, goods in transit 13,412.42 12.42 group adds goods in transit on CP10's side, then eliminates
IT recharge 90000051 / 1900000388 0.00 2,500.00 not eliminated: CP20 side has no partner
Revenue CP20 against purchases CP10 (80 impellers) 26,800.21 0.21 CP20 revenue 24,769.14 EUR at the average rate; difference is translation
Profit in CP10 stock (30 impellers) 6.00 0.00 removed from inventory and profit in the group view
Group reporting eliminates from data in group currency, after translation and matching.
Synthetic numbers for a fictional company, not from an SAP system.
Read the Left column. The translation differences and the IT charge are what group accountants will have to explain or fix before sign-off. The 0.21 USD on revenue is expected: Augsburg's revenue is translated at the average rate, Rockford's purchases are in USD at invoice prices. Only the profit-in-stock line changes the group's profit.
Open unit1b/calder_intercompany_flow.md in VS Code and press Ctrl+Shift+V (Windows) or Cmd+Shift+V (macOS) for the preview. It has a sequence diagram of the flow between the two systems, a table of the postings on each side with the partner on each line, the pre-close findings and the eliminations. If the diagram shows as text, install the VS Code extension Markdown Preview Mermaid Support.
This file is the topic's deliverable: Calder's intercompany flow with the documents and postings on each side.
#Step 10 (optional): Read billing documents 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\intercompany.py --sandbox
macOS / Linux:
python unit1b/intercompany.py --sandbox
The output has this shape. Values in angle brackets come from SAP's demo data and change over time:
SAP sandbox: <n> billing documents read (SAP demo data, not Calder's)
type <type> partner company <set or empty> <count> documents
A billing document with a partner company was billed to another company of the group.
The script reads up to 50 billing documents from API_BILLING_DOCUMENT_SRV with $select, including BillingDocumentType and PartnerCompany, and counts them by type and whether a partner company is set. A valid "empty" result is 0 billing documents read. The sandbox is read-only.
1HO is a Public Edition scope item for companies in one tenant
Advanced intercompany sales and stock transfer
Automatic follow-on documents, valuated stock in transit (SAP Learning)
Advanced intercompany sales from S/4HANA 2022, process 5D2 for on-premise and private cloud, no additional licenses per SAP's training; stock transfer as scope item 5HP in Public Edition training
Transfer prices
Legal, group and profit center valuation (SAP Learning)
SAP S/4HANA Finance for group reporting (SAP Learning)
Non-integrated companies by upload or data collection
AI for matching
Intercompany Matching and Reconciliation Agent (SAP)
No availability or edition stated on the page
How it is exposed. The CDS views below are released in SAP's Cloudification Repository (file read 28 October 2026); ACDOCA itself is not to be released, with I_JournalEntryItem among its successors (Cloudification Repository). The OData fields come from SAP's generated API models for the SAP Cloud SDK, published in 2022 (billing document model).
Where it sits. Rockford's purchasing, inventory and finance run in the CP10 S/4HANA system; Augsburg's sales, inventory and finance run on ECC. EDI messages connect them, through an integration platform covered in Unit 5B. Group reporting in S/4HANA releases Rockford's data and receives Augsburg's by upload. A pre-close check service reads both sides through released interfaces and writes findings, not postings.
flowchart LR
S4[S/4HANA CP10<br/>PO, GR, invoice] <-->|EDI orders,<br/>invoices| IP[Integration<br/>platform]
IP <--> ECC[ECC CP20<br/>sales, billing]
S4 -->|release| GR[Group reporting<br/>ICMR, eliminations]
ECC -->|monthly<br/>upload| GR
S4 -->|released APIs| CK[Pre-close checks<br/>read-only]
ECC -->|extract| CK
CK --> FD[Findings for<br/>accountants]
Non-functional requirements that decide the design
Requirement
Typical target
If missed
Consistency
Same price, currency, terms and partner on both sides of every flow
Differences at every close; invoices stuck
Interface latency and monitoring
Orders and invoices processed within hours; failures alerted the same day
Shipments without orders; invoices missing at month end
Cut-off visibility
Goods in transit known on both sides at month end
Unexplained timing differences
Auditability
Every transfer price change approved and traceable
Tax exposure, audit findings
Data timeliness for the group
Augsburg's package complete and valued by a fixed workday
Translation differences; late group close
Volume
Monthly matching of all intercompany items in minutes
Matching squeezed into close days
Integration and identity. Business documents travel as EDI messages (ORDERS, DESADV, INVOIC) between the two ERPs, each posting under a technical user in the receiving system. That user's authorizations should allow only the intercompany partner, document types and company code it serves. Group reporting takes Rockford's journal by release and Augsburg's figures by upload. A check service or assistant reads with the accountant's own identity where it can, so company code authorizations apply (grounding with authorizations).
Context: Augsburg (CP20) stays on ECC for at least another year; Rockford (CP10) runs S/4HANA. Impellers flow monthly from Augsburg to Rockford. Group close finds unlabeled timing differences, missing partners and price mismatches every month.
Options: (a) keep design A as is and reconcile in a spreadsheet; (b) keep design A, but put trading partner, terms, currency and price under one owner, monitor the EDI messages, run pre-close checks in the last week of the month and match with ICMR after Augsburg's upload; (c) bring forward Augsburg's migration to run design C in one system.
Decision: (b) now, with design C as the target if Augsburg joins Rockford's system.
Consequences: Master data changes in two systems need one change process. The integration team owns message monitoring. Augsburg must deliver a valued package by a fixed workday. The check service is temporary and is retired after migration. The transfer price change waits for group tax.
What a design review will ask
For each intercompany flow, which design is used, and what changes after the migration?
Where is the transfer price maintained, who approves it, and how does it reach both systems?
How are trading partners guaranteed on every posting, including manual ones?
Which messages connect the companies, who monitors them, and what happens to a failed invoice at month end?
Which intercompany differences can remain at group close, and who signs them off?
Rules first; suggestions for what rules miss, such as different references for the same shipment. SAP names an agent for this (SAP)
Explaining differences
Retrieve and explain
Strong
Draft a reason from shipping dates, rates and documents, with citations; the accountant confirms
Unstructured intercompany invoices
Extract
Moderate
Read invoices from companies outside the SAP landscape into matching data
Adjustment postings
Act
Never alone
An accountant approves every adjustment, ideally through ICMR's workflow
The line is the same as in record to report: AI matches, labels and drafts; people approve postings and sign the group figures. Human approval in AI workflows shows how to enforce that in code.
Authorizations. An intercompany accountant needs read access to both company codes, and often to both systems. Keep posting rights per company code, so one person can't create and approve both sides of an adjustment.
Master data governance. Trading partner, payment terms, currency and intercompany prices need one owner and one change process across both systems (master data).
Tax. Transfer prices, cross-company postings and recharges have tax effects in both countries. Involve tax before changing any of them.
Operations. Monitor EDI messages daily and before every month end. A failed invoice found on workday 3 is a close delay.
Evaluation. If you pilot AI matching, measure it against labeled history: share of items matched correctly, share of wrong matches, and time to clear differences.
Clean core. Use standard flows, ICMR and group reporting. Keep custom checks side by side, reading released interfaces; never add custom matching tables or postings inside the ERP.
Turn the flow into a design note and test two fixes.
Run python unit1b/intercompany.py --write (Windows: python unit1b\intercompany.py --write). Copy unit1b/calder_intercompany_flow.md to unit1b/calder_intercompany_design.md.
In the copy, add a column Prevent or catch to the pre-close findings table. For each finding write prevent (the design should make it impossible) or catch (the close should find it), and one sentence on how.
In intercompany.py, change CP20_IT_RECHARGE_PARTNER = "" to CP20_IT_RECHARGE_PARTNER = "CP10". This is Augsburg reposting the IT charge with its partner.
Change CP20_FCV_POSTED = False to CP20_FCV_POSTED = True. This is Augsburg running foreign currency valuation before sending its package.
Run the script with --match and --eliminate. Note the translation differences and what is left.
Change IMPELLER_PRICE_USD = 335.00 to IMPELLER_PRICE_USD = 361.58, the policy price, and run --postings and --transfer-price. Note Augsburg's margin on the shipment and the profit in Rockford's stock.
Change all three settings back.
In the design note, add a heading Fixes tested with three lines: what the partner fix changed, what the valuation changed, and what the policy price changes for each company and for the group.
Add a heading Decision with the ADR from Architecture view, in your own words, and one line on which design Calder should use after Augsburg's migration and why. Preview the file and commit it with Git.
Done when:unit1b/calder_intercompany_design.md has the findings table with Prevent or catch filled in for all nine findings, a Fixes tested section that says both pairs now match by rule 1 with translation differences of 0.00 USD, the eliminations leave 0.00 on the invoice, the in-transit item and the IT charge, and at the policy price Augsburg earns 930.14 EUR on the shipment and the group eliminates 803.40 USD of profit in stock, and a Decision section. Keep it: the capstone at the end of Unit 1B traces one exception across the whole enterprise, and this note is its intercompany part.
Pick one answer for each question. The explanation appears after you choose.
1Why does Calder use a purchase order in S/4HANA and a sales order in ECC, instead of an intercompany stock transport order?
Answer: C. Designs B and C link the two companies' documents inside one system. With Augsburg on ECC and Rockford on S/4HANA, the only option is two separate document chains joined by EDI messages, which is design A.
2In an intercompany sale inside one system, what do conditions PI01 and IV01 do?
Answer: A. PI01 on the customer invoice is statistical and shows the selling company's intercompany cost. IV01 on the internal invoice references PI01 and is the delivering company's revenue. The customer's price is PR00 and the cost is VPRS.
3The impeller invoice matches in USD but is 61.75 USD off in group currency. What is the most likely cause?
Answer: A. In transaction currency both sides show 13,400.00 USD. Augsburg books in EUR at the invoice-date rate; without month-end valuation, its balance translated at the closing rate differs from Rockford's USD amount.
4Why does the group eliminate profit on impellers still in Rockford's stock but not on impellers already built into sold pumps?
Answer: C. The group earns profit only when goods leave it. Rockford's stock carries Augsburg's markup, which the group hasn't realized yet, so group reporting removes it. Once the pump is sold to a customer, the profit is real for the group.
5Augsburg's sales order for PO 4500000014 is priced at 480.00 USD and Rockford's PO at 455.00. What would you do first?
Answer: D. The mismatch is known before the invoice exists, so fixing the record now prevents a blocked invoice and a manual fix in two systems. Widening tolerances or adjusting later hides a master data problem that will recur.
6ICMR can't read Augsburg's journal entries directly. How can Calder still match the two companies in S/4HANA?
Answer: D. Non-integrated companies reach group reporting by upload or data collection into ACDOCU, and ICMR has a data source comparing ACDOCA with ACDOCU by unit and partner. The data is only as current as the last upload. Copying into another company's journal or double posting would corrupt the books.
7A team wants an agent to post intercompany adjustments by itself so the group closes a day earlier. What is the right design?
Answer: D. Adjustments change legal books and group figures. The agent can match, label and draft a reason, and ICMR can route adjustments through an approval workflow, but a person approves each posting.
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.
Understanding Transfer Pricing (SAP Learning)— legal, group and profit center valuation; up to three parallel valuations; single- or multi-valuation ledgers; activated per controlling area
Cloudification Repository (SAP on GitHub)— objectReleaseInfoLatest.json read 28 October 2026; I_BillingDocument, I_PurchaseOrderAPI01, I_SupplierInvoiceAPI01, I_PartnerCompany, I_JournalEntryItem, I_CnsldtnGroupJrnlEntryTP released; ACDOCA not to be released