Orchestrate

Report an issue

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

0 / 2000

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

Intercompany and multi-company scenarios

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.

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

The 60-second version

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.

Why it matters to the business

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.

How SAP does it

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.

The decisions on one page

Decision Options What decides it Calder today
Flow for replenishing a plant Intercompany stock transfer, or a purchase order and sales order pair between the companies Are both companies in one system? PO and sales order, with electronic messages between ECC and S/4HANA
Flow for a customer order shipped by the other company Intercompany sale (internal invoice), or two separate sales Same system, plant assigned to the selling sales organization Not set up
Transfer price Cost plus a markup, a market-based price, or another method set with tax advisers Tax rules in both countries, documented policy Cost plus 8% on paper; the real price lags
Invoice currency Seller's currency, buyer's currency, or a fixed group currency Who carries the exchange risk USD, so Augsburg carries it
Partner identification Trading partner set in master data, or typed per posting Whether elimination can be automatic Not in master data
Shared services Recharges between companies, central purchasing, central payments One system or several, tax treatment Rockford charges Augsburg for IT monthly
Reconciliation Spreadsheet, ICMR in S/4HANA, or a tool across systems Where both companies' data can meet Spreadsheet at group close

Questions to ask

  1. Which flows cross company codes, and for each one, are both companies in the same system?
  2. Who owns the transfer price, where is it maintained, and how does a change reach both sides on the same day?
  3. Which currency does each intercompany invoice use, and who carries the exchange difference?
  4. Does every intercompany customer and supplier carry its trading partner in master data, or does someone type it?
  5. How much intercompany difference is left at group close, and how much of it is timing, currency or a missing partner?
  6. Where both companies run different systems, which messages connect them, who monitors failures, and how fast?
  7. Will the planned migration put the companies in one system, and which intercompany flows change when it does?

Common misconceptions

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

Key terms

All course terms, A to Z, in the glossary

  • Intercompany: a transaction between two companies of the same group.
  • Company code: SAP's unit for a legal entity with its own books.
  • Trading partner: the group company on the other side of an intercompany posting; matching and elimination depend on it.
  • Transfer price: the price one group company charges another; set by policy, checked by tax authorities.
  • Internal invoice: the invoice the delivering company sends the selling company in an intercompany sale (billing type IV).
  • Stock in transit: goods that have left one plant and not reached the other.
  • Group currency: the currency the group reports in; Calder's is USD.
  • Translation: converting a company's figures into group currency.
  • Elimination: removing intercompany revenue, cost, balances and profit so the group reports only deals with outsiders.
  • EDI: electronic data interchange, structured business messages such as orders and invoices sent between systems.

Check yourself

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

Mental model

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:

  1. Do both sides record the same event? Same quantity, same price, same currency, each naming the other as trading partner.
  2. Do they record it at the same time? Shipment and receipt can fall in different periods; the design has to make that visible.
  3. 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.

How it works

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.

Design Documents in the supplying company Documents in the receiving company Precondition
A. Two-system buy and sell (Calder today) Sales order, delivery, goods issue, customer invoice Purchase order, goods receipt, supplier invoice None: works across any systems, joined by EDI
B. Intercompany stock transport order Outbound delivery, goods issue, internal invoice IV Stock transport order, goods receipt, supplier invoice Both company codes in one system
C. Advanced intercompany stock transfer Intercompany sales order (automatic), delivery, goods issue, intercompany customer invoice Intercompany purchase order, inbound delivery (automatic), goods receipt, supplier invoice (automatic) One S/4HANA system
D. Intercompany sale to a customer Delivery, goods issue, internal invoice IV Customer's sales order, customer invoice, supplier invoice 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).

Billing and the purchase-side posting

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.

Currencies: transaction, local and group

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.

Reconciliation and elimination

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 hybrid case: CP20 on ECC, CP10 on S/4HANA

Calder's landscape changes every step above:

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

Where it breaks

Symptom Usual cause Who resolves it What it costs Calder (scenario)
Augsburg shows 13,400.00 USD Rockford hasn't booked Impellers shipped 30 October, still in transit Intercompany accountants A timing difference to label; group adds goods in transit
IT charge can't be eliminated Posted in CP20 without a trading partner Augsburg finance 2,500.00 USD left in group receivables and payables
Balances match in USD but not in group currency Augsburg's package built before foreign currency valuation Augsburg finance 61.75 USD difference on one invoice
Invoice stops in Rockford's inbound queue PO 4500000014 at 455.00 USD; Augsburg's sales order at 480.00 Rockford buyer, Augsburg sales Late payment, a manual fix in two systems
Augsburg loses money on impellers Transfer price 335.00 USD hasn't followed cost or the policy Group tax, controllers −49.77 EUR margin on one shipment; tax exposure
Augsburg chases invoices Rockford hasn't scheduled Payment terms NT30 in CP20, NT45 in CP10 Both controllers, master data Disputes, reminders between colleagues
No sales order in Augsburg EDI order failed on the ECC side Integration support Late shipment, emergency phone order
Every order needs its currency changed CP20's customer master says EUR; Rockford orders in USD Augsburg master data Manual work and pricing errors

Build it yourself

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

You will write one script that 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

What you need

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

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

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

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

      source .venv/bin/activate

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

Step 2: Make sure the sample pack is current

  1. Rewrite the pack, so you start from the same data as this topic:

    • Windows (PowerShell):

      python unit1b\make_calder_pack.py
    • macOS / Linux:

      python unit1b/make_calder_pack.py
  2. The last lines should read:

Calder sample pack 1.0: order 1000001, net 29,100.00 USD, cost 19,600.00 USD.
All data is fictional. Next: python check_unit1b.py

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.

Step 3: Create the script

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

Step 4: Run the pre-close checks

  1. Run:

    • Windows (PowerShell):

      python unit1b\intercompany.py
    • macOS / Linux:

      python unit1b/intercompany.py
  2. You should see:

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.

Step 5: Compare the designs and see the postings

  1. Run the design comparison:

    • Windows (PowerShell):

      python unit1b\intercompany.py --flows
    • macOS / Linux:

      python unit1b/intercompany.py --flows
  2. 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.
  1. Now show the postings for one order:

    • Windows (PowerShell):

      python unit1b\intercompany.py --postings
    • macOS / Linux:

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

Step 6: Test the transfer price

  1. Run:

    • Windows (PowerShell):

      python unit1b\intercompany.py --transfer-price
    • macOS / Linux:

      python unit1b/intercompany.py --transfer-price
  2. You should see:

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.

Step 7: Match the two companies

  1. Run:

    • Windows (PowerShell):

      python unit1b\intercompany.py --match
    • macOS / Linux:

      python unit1b/intercompany.py --match
  2. You should see:

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.

Step 8: Work out the eliminations

  1. Run:

    • Windows (PowerShell):

      python unit1b\intercompany.py --eliminate
    • macOS / Linux:

      python unit1b/intercompany.py --eliminate
  2. You should see:

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.

Step 9: Write Calder's intercompany flow

  1. Run:

    • Windows (PowerShell):

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

      python unit1b/intercompany.py --write
  2. After the checks, the last line reads:

wrote unit1b/calder_intercompany_flow.md
  1. 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.

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

  2. Run:

    • Windows (PowerShell):

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

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

Step 11: Save your work in Git

git add unit1b/intercompany.py unit1b/calder_intercompany_flow.md
git commit -m "Unit 1B: Calder intercompany flow, checks, matching and eliminations"

What each part of the code does

Part What it does
CP20_IT_RECHARGE_PARTNER, CP20_FCV_POSTED, IMPELLER_PRICE_USD The three settings the exercise changes: a reposted partner, the valuation run, the price
RATES, MARKUP, STOCK_AT_CP11 Course exchange rates, the cost-plus policy and the impellers still in Rockford's stock
load_pack(), partner_facts() Reads the pack and the intercompany customer and supplier: terms, currency, reconciliation account, trading partner
items(), local_eur(), group_usd() The open intercompany items in transaction, local and group currency
checks() The pre-close checks: partner, terms, currency, price, policy, valuation, timing, design
FLOWS, print_flows() The four intercompany designs with the documents in each company
postings() Documents and postings on each side for PO 4500000005
transfer_price() Policy price, actual markup, the gap and the profit in stock
match() Matching rules applied in order, with reason codes and translation differences
eliminations() What the group removes and what is left
write_flow() Writes the flow diagram, postings, findings and eliminations as Markdown
sandbox() Reads up to 50 sandbox billing documents with $select

If something goes wrong

What you see What it means What to do
python is not recognized / command not found Python isn't on the path, or the terminal opened before installing Follow the Python step in Set up your computer, then open a new terminal
Cannot read ...calder_pack The pack wasn't written, or you ran from the wrong folder Run Step 2 from the orchestrate-course folder
Company codes CP10 and CP20 are missing or Intercompany partners ... are missing The pack was edited Run Step 2 again
CP-IMP200 has no valuation in plant CP21 The pack's products were edited Run Step 2 again
Your output differs from the one shown Values in the script were changed Copy the script again, or compare your edits
ModuleNotFoundError You changed the script to use an outside library The script needs only built-in modules; copy it again
SAP_API_KEY is not set No key in the environment or .env Add it as in the course setup, or skip Step 10
Sandbox answered HTTP 401 The key is wrong or expired Copy the key again from the Business Accelerator Hub
Sandbox answered HTTP 404 The API or entity isn't in the sandbox under that path Check the API's sandbox URL on the Business Accelerator Hub, or skip Step 10
Cannot reach the sandbox No network, or a company proxy blocks it Try another network, or stay with Steps 1 to 9

The SAP way

As of October 2026, these are the SAP pieces for each part of the design. Names and scope change; check each against your edition.

Need SAP function Edition notes
Intercompany sale with internal invoice Billing type IV, conditions PI01/IV01 (SAP Learning) S/4HANA; the same mechanism exists in ECC
Automatic supplier invoice in the buyer IDoc inbound posting; scope item 1HO (SAP Learning) 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) S/4HANA; activated per controlling area
Recharges and split invoices Cross-company code postings (SAP Learning) Company codes in one system; tax caveat
Matching ICMR (SAP Learning) S/4HANA; reads ACDOCA and ACDOCU
Elimination 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).

Step API Released CDS views Fields that matter for intercompany
Buyer's purchase order API_PURCHASEORDER_PROCESS_SRV (A_PurchaseOrder) I_PurchaseOrderAPI01, I_PurchaseOrderItemAPI01 CompanyCode, Supplier, SupplyingPlant, DocumentCurrency
Seller's sales order and delivery I_SalesOrder, I_DeliveryDocument, I_InboundDelivery Sales organization, delivering plant
Seller's invoice API_BILLING_DOCUMENT_SRV (A_BillingDocument) I_BillingDocument, I_BillingDocumentItem BillingDocumentType, CompanyCode, PartnerCompany, TransactionCurrency
Buyer's supplier invoice I_SupplierInvoiceAPI01, I_SuplrInvcItemPurOrdRefAPI01 Purchase order reference, amounts
Journal entries API_JOURNALENTRYITEMBASIC_SRV I_JournalEntryItem, I_GLAccountLineItem, I_OperationalAcctgDocItem PartnerCompany, currencies and amounts
Partners and group API_BUSINESS_PARTNER (A_BusinessPartner) I_PartnerCompany, I_CompanyCode, I_CnsldtnGroupJrnlEntryTP TradingPartner on the business partner

Build vs. SAP

Need Build it yourself Use SAP's function Calder's choice
Pre-close checks across both systems A small read-only service like this topic's script, on released interfaces Not one standard function across ECC and S/4HANA Build, until the migration
Intercompany flow Never: custom order and invoice logic between companies Standard flows and EDI SAP design A now; B or C after migration
Transfer price maintenance A price table outside SAP that both sides read Condition records and parallel valuation SAP, with one approval process for both records
Matching Spreadsheet or a custom matcher ICMR on ACDOCA and ACDOCU ICMR once Augsburg's data is uploaded each month
Explaining differences An assistant that drafts reasons from documents The Intercompany Matching and Reconciliation Agent, when available for your edition Pilot only after the master data fixes

Architecture view

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

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

  • 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

  1. For each intercompany flow, which design is used, and what changes after the migration?
  2. Where is the transfer price maintained, who approves it, and how does it reach both systems?
  3. How are trading partners guaranteed on every posting, including manual ones?
  4. Which messages connect the companies, who monitors them, and what happens to a failed invoice at month end?
  5. Which intercompany differences can remain at group close, and who signs them off?

Where AI fits, and where it doesn't

Step Pattern Fit Note
Partner, terms and currency checks Plain automation is enough Weak for AI Rules on master data, as in Step 4
Price consistency across systems Plain automation is enough Weak for AI Compare two price records; AI adds nothing
Transfer price setting None Never A tax decision with documentation
Matching Match, then suggest Strong 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.

Production concerns

  • 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.
  • Cost. Each extra company code adds a close, tax filings and intercompany work (how an SAP enterprise is wired).
  • 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.

Pitfalls

  • Partners typed by hand. Put the trading partner on the intercompany customer and supplier, so postings inherit it.
  • Two price lists. Two systems with two copies of the price will drift. One owner, one change, both records the same day.
  • Ignoring the exchange rate in a fixed-currency price. Calder's USD price drifted below Augsburg's EUR cost. Review transfer prices when rates move.
  • Matching in group currency first. Match in transaction currency, then explain the translation difference separately.
  • Sending the package before valuation. The group sees differences that are really a missing local close step.
  • Treating in-transit goods as errors. Label them with a reason and an expected receipt date.
  • Designing for one system when you have two. Integrated flows assume one system. Check the landscape before choosing a design.
  • Letting an agent post adjustments. Proposals are cheap to review; wrong eliminations are expensive to unwind.

Exercise

Turn the flow into a design note and test two fixes.

  1. Run python unit1b/intercompany.py --write (Windows: python unit1b\intercompany.py --write). Copy unit1b/calder_intercompany_flow.md to unit1b/calder_intercompany_design.md.
  2. 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.
  3. In intercompany.py, change CP20_IT_RECHARGE_PARTNER = "" to CP20_IT_RECHARGE_PARTNER = "CP10". This is Augsburg reposting the IT charge with its partner.
  4. Change CP20_FCV_POSTED = False to CP20_FCV_POSTED = True. This is Augsburg running foreign currency valuation before sending its package.
  5. Run the script with --match and --eliminate. Note the translation differences and what is left.
  6. 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.
  7. Change all three settings back.
  8. 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.
  9. 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.

Check yourself

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.

Sources

Sign in to track your progress

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

or

Tell us a little about you

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

SAP areas you work in