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.

Data migration: strategy, objects and reconciliation

Plan SAP data migration as a go-live workstream: objects in load order, cleansing, mock loads, reconciliation the business signs, and a cutover plan.

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

The 60-second version

When a company moves to a new SAP system, its data has to move too. The new system starts empty. Customers, suppliers, materials, open orders, unpaid invoices, stock and account balances must all be in it before the first user logs on.

Data migration is the workstream that does this. It sounds like a copy job. It isn't. Old data is incomplete, duplicated and shaped for the old system. Each record must be cleaned, mapped to the new structure, loaded in the right order and then proved: the business checks that what arrived matches what left, and signs.

The work runs as rehearsals. A team loads a copy of the data into a test system, finds what breaks, fixes it at the source and loads again. Only when a rehearsal passes agreed exit criteria does the real load happen, over a planned weekend called the cutover.

This topic follows Calder Pumpworks, the fictional pump maker this unit uses, as it moves its German company CP20 off ECC and into the S/4HANA system that already runs its US company.

Why it matters to the business

Go-live depends on it. Configuration can be finished and tested for months, yet go-live still fails if the opening balances don't reconcile or if half the open purchase orders didn't load. Migration is usually the last workstream to finish and the one that sets the go-live date.

Bad data stops processes on day one. A customer without payment terms can't be billed. A material without a unit conversion can't be received. A duplicate supplier gets paid twice. Each of these is cheap to fix in the old system before the move and expensive to fix in production after it.

Finance must sign. Opening balances in the new system become the starting point for the next close and the next audit. If receivables in the new system differ from the old system by one invoice, someone has to find it before the books open.

It is a one-way door. SAP's own product team notes that the SAP S/4HANA migration cockpit migrates each record once, does not keep systems in sync, and has no deletion function; test cycles use other clients or refreshed systems instead (SAP Community, SAP product management). Whatever loads into production stays there.

AI raises the bar. Every AI use case on SAP data inherits the migrated data. Duplicates and blank fields carried over at go-live become wrong answers later. The data readiness topic in Unit 7 starts from what migration leaves behind.

How SAP does it

As of October 2026, SAP's main tool for loading data into a new or extended SAP S/4HANA system is the SAP S/4HANA migration cockpit, opened through the Migrate Your Data app. SAP's training says it is available in on-premise, private cloud and public cloud deployments, with functions and object templates that vary by deployment, and that it is embedded at no additional cost (SAP Learning).

Need What SAP offers Note
Load from files Staging tables with a local schema: download an XML or CSV template per migration object, fill it, upload it The standard approach; can be labor-intensive for very large datasets (SAP Learning)
Load large volumes from any source Staging tables in a remote SAP HANA schema, filled by your own ETL code or SAP Data Services More complex setup; SAP's training calls it efficient for high volumes (SAP Learning)
Move data straight from an SAP ERP system Direct transfer over a remote function call (RFC), selected by company code A limited set of objects; the rest go through staging tables (SAP Learning)
Adjust objects Migration object modeler (transaction LTMOM): change selection and field mapping, add custom fields Custom fields are mapped by hand (SAP Community FAQ)
Find and merge duplicates SAP Master Data Governance consolidation: load, standardize, match, review, best record, activate A separate product; SAP's training names initial load as a use case (SAP Learning)

The cockpit is for the initial load. It creates records; it does not update records that already exist (SAP Community FAQ). For a company joining a system that is already live, like Calder, that matters: records that exist must be extended, not created again.

A whole-system conversion of ECC to S/4HANA is a different path. It keeps the data in place and transforms it, so most of this topic's loading work doesn't apply; the landscape topic compares the paths.

A migration plan on one page

Calder's migration of CP20 in one picture: what loads in which order, and how many rehearsals come before the real load.

flowchart LR
  ORG[Org units<br/>configured] --> SUP[Suppliers]
  ORG --> CUS[Customers]
  ORG --> PRD[Products]
  PRD --> INV[Stock]
  SUP --> POS[Open POs]
  PRD --> POS
  SUP --> AP[Open payables]
  CUS --> AR[Open receivables]
  AP --> GL[G/L balances]
  AR --> GL
  INV --> GL
Cycle When (course plan) Purpose Exit criteria
Mock load 1 February 2027 Find what breaks, measure load times Error list with an owner for each error
Mock load 2 April 2027 Prove the fixes; full volume No load errors; counts and amounts reconcile
Dress rehearsal June 2027 Run the cutover plan to the hour on a production-like system Mock 2 criteria, within the time window, business sign-off
Production load Weekend of 2-4 July 2027 The real move Signed reconciliation, go/no-go decision

The rule behind the order is simple: a record can only load once everything it points to exists. A purchase order points to a supplier and a product. An open invoice points to a customer. Account balances come last, because they must agree with everything loaded before them.

Questions to ask

  1. Which data moves (master data, open items, balances), and which history stays behind? Where will users read the history after go-live?
  2. Who owns each migration object on the business side, and who signs its reconciliation?
  3. How many mock loads are planned, on which systems, and what are the exit criteria for each?
  4. Where is data cleansed: in the old system before extraction, or in transformation rules during the load? Who approves each merge or exclusion?
  5. Which records already exist in the target system, and how will they be extended instead of duplicated?
  6. How long does the full load take at production volume, and does it fit the cutover window with time to spare for a fallback?
  7. If an AI tool suggests duplicates or mappings, who approves each suggestion, and how is that recorded?

Common misconceptions

  • "Migration is an IT task." IT moves the records. Only the business can say which customer is a duplicate, which payment terms are right, and whether the balances are correct.
  • "We'll clean the data in the new system." Cleansing after go-live means cleaning in production, under time pressure, while processes run on the bad data.
  • "Bring all the history, just in case." SAP's product team notes that the migration cockpit doesn't transfer historical transactions; the one exception is a historical balance object. History multiplies volume and reconciliation work.
  • "One test load is enough." The first mock finds the problems. The second proves the fixes. The dress rehearsal proves the timing. Each answers a different question.
  • "If the record counts match, we're done." Counts can match while amounts differ. Reconcile counts, amounts and a sample of records, and test that a process runs on them.
  • "AI can clean the data for us." AI can suggest duplicates and mappings quickly. A named owner still decides each one, because merging two customers changes who gets invoiced.

Key terms

All course terms, A to Z, in the glossary

  • Migration object: one kind of data the tool loads, such as customers, open purchase orders or receivable open items, with its own template and rules.
  • Load sequence: the order objects load in, so every record finds what it points to.
  • Extract, transform, load (ETL): take data out of the old system, reshape it, load it into the new one.
  • Cleansing: fixing, merging or excluding bad records, ideally in the source system.
  • Key mapping: the table that links each old number to its new number in the target.
  • Mock load: a full rehearsal of the load into a test system.
  • Exit criteria: the conditions a mock must meet before the next step.
  • Reconciliation: proving that counts and amounts in the target match the source.
  • Cutover: the planned window when the old system stops, the final load runs and the new system goes live.

Check yourself

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

1Why is data migration often the workstream that sets the go-live date?

Answer: C. Configuration can be tested for months, but nobody goes live until opening data is loaded and the business has signed the reconciliation. Migration usually finishes last, so it sets the date.

2Calder's first rehearsal failed 21 records. What is the right response?

Answer: A. Finding errors is the purpose of a first mock. Each error gets an owner and a fix, preferably in the old system, and the next mock proves the fix.

3Calder's German company joins a system that already holds some of its suppliers and materials. What does that change?

Answer: A. The migration cockpit creates records and doesn't update existing ones. Records already in the target are extended with new org levels, so the company doesn't end up with two versions of one supplier.

4Who should sign the reconciliation of open receivables?

Answer: C. Only the business can confirm that the receivables in the new system are the ones it is owed. A developer can prove that records loaded; the owner proves they are right.

5An AI tool flags two German customers as likely duplicates. What should happen next?

Answer: C. A merge changes who is invoiced and paid. A suggestion speeds up the review, but a named owner decides and the decision is logged so the next mock repeats it.

6Why does Calder plan a dress rehearsal after mock load 2 already passed?

Answer: A. Mock 2 proves the data is right. The dress rehearsal proves the whole cutover, with every step and hand-off, fits the weekend window with time left for a fallback.

7What is the main risk of migrating every year of history "just in case"?

Answer: A. Each year of history adds records to load and reconcile and stretches the cutover. Ask what the business actually reads, and keep the rest readable in the old system or an archive.
Deep layer · 40 min read

Mental model

A data migration is a factory you build, run many times, and then run once for real.

The factory has four stations: extract from the old system, transform (cleanse, map, renumber), load into the new system, and reconcile what arrived against what left. Each mock load runs the whole factory end to end on a recent copy of the data. Every defect found is fixed at the station that caused it, ideally in the source data, so the next run is cleaner. When a run passes the exit criteria at full volume and inside the time window, the factory runs once more, on production, over the cutover weekend.

Three consequences follow:

  1. Rules, not edits. A fix typed by hand into a test system disappears with the next run. Every fix must be a rule, a mapping entry or a decision in a log, so it repeats.
  2. Order is data. A record can only load once everything it references exists. The load sequence comes from the references, not from a project plan.
  3. Proof is the product. The business doesn't sign "records loaded". It signs "these counts and amounts match the old system, and these exceptions were approved by these people".

How it works

What moves, and what stays behind

A migration into S/4HANA moves three kinds of data:

Kind Examples at Calder Why it moves
Master data Customers, suppliers, products, banks, info records Every document needs it
Open items and open documents Unpaid invoices (receivables, payables), open purchase orders, open sales orders Business in flight must finish in the new system
Balances Stock quantities and values, G/L account balances, fixed asset values The books and the warehouse must open where the old ones closed

Closed history stays behind. SAP's product team states that the migration cockpit does not transfer historical transactions; the one exception is the historical balance object (FI - Historical balance) in the file or staging approach (SAP Community, SAP product management, SAP KBA 2853964). Calder's decision in the landscape topic follows this: master data and open items move, and ECC stays read-only for closed history until it is archived.

Org structure and configuration don't migrate either. Company code CP20, plant CP21, sales organization CPDE and purchasing organization PUDE are configured in the target and moved through the transport route, before any data loads.

Migration objects and the load sequence

The migration cockpit works in migration objects: one object per kind of data, each with its own template, fields and rules. SAP's training says predecessor objects must be migrated first (SAP Learning). Calder's objects, with names as SAP's support articles write them:

Object (cockpit name) References Source
Supplier, Supplier - extend existing record by new org levels Org units KBA 3011873
Customer, Customer - extend existing record by new org levels Org units same
Product (new products, and an extension for plant CP21) Org units check the object list for your release
Material inventory balance Product with plant data KBA 2989410
Purchase order (only open PO), shown as MM - Purchase Order (only open PO) in some releases Supplier, product KBA 3065870
FI - Accounts payable open item Supplier KBA 2853964
FI - Accounts receivable open item Customer same
FI - G/L account balance and open/line item Loaded last in Calder's plan same

Three details in that table cause most first-mock failures:

  • Extend, don't create. The extend objects add a new company code, purchasing organization or sales area to a business partner that already exists. SAP's support article adds a condition: the target business partner must already have the customer role (FLCU00, FLCU01) or supplier role (FLVN00, FLVN01) (KBA 3011873).
  • Stock is a goods movement. Material inventory balance posts the initial stock with movement type 561, so a zero quantity can't be posted (KBA 2989410). Products with no stock simply have no stock record.
  • Units must convert. An open purchase order in a unit the material master can't convert fails in simulation (KBA 3065870).

Calder loads G/L balances last. That is a course design choice, not an SAP rule: the trial balance check then runs on the complete picture, after the subledger objects (payables, receivables, stock) have posted their own amounts. Every open item and stock load also posts against an offset account so each journal entry balances; once everything is loaded, the offsets must net to zero. The account numbers are configuration and vary by system.

Three ways to get data in

flowchart LR
  ECC[(ECC CP20)] -->|RFC, company code CP20| DT[Direct transfer]
  ECC -->|export| X[XML or CSV<br/>templates]
  ECC -->|ETL tool| R[(Remote SAP HANA<br/>staging schema)]
  X --> L[(Local staging tables)]
  DT --> MC[Migration cockpit:<br/>mapping, simulate, migrate]
  L --> MC
  R --> MC
  MC --> S4[(S/4HANA<br/>CP10 + CP20)]
  1. Direct transfer. The cockpit reads an SAP ERP system over an RFC connection, selects data by company code, and converts it to the S/4HANA data model as it loads (SAP Learning). Only a set of predefined objects is supported; others need staging tables (SAP Learning). SAP's product team described it in February 2022 as available since release 1909 for S/4HANA on-premise and private edition (SAP Community); the community FAQ dates it for Public Edition to release 2402 (SAP Community FAQ).
  2. Staging tables, local schema. Download a template per object as XML or CSV, fill it, upload it. SAP's Public Edition training gives a 100 MB limit per file (SAP Learning).
  3. Staging tables, remote schema. A separate SAP HANA database holds the staging tables, filled by your own ETL code or by SAP Data Services. Its setup is more complex, and SAP's training calls it efficient for high-volume migrations.

Whichever way the data arrives, the cockpit then runs the same steps, as SAP's training lists them: prepare the staging tables, process mapping tasks (value mapping from old to new values, and fixed values for defaults), simulate (optional; errors can show here or in the migrate step), migrate, and create a correction file for records that failed (SAP Learning). Each object's documentation names an app to validate the result.

ETL or conversion. All three ways are forms of extract, transform, load: the data leaves one system and enters another. A system conversion is the alternative: the ECC system itself becomes S/4HANA, and its data is transformed in place. Calder's option A, a company code rollout, is ETL. That is why Calder needs everything below.

Cleansing and de-duplication

Cleansing is cheapest in the source. A blank payment term fixed in ECC is fixed for every mock and for production. A fix in a transformation rule works too, but it hides the bad data from the people who keep using ECC until cutover.

Calder's findings fall into five kinds, each with its own decision:

Finding Decision Where it is fixed
Mandatory field blank (payment terms, tax number) Fill it Source (ECC), by the data owner
Duplicate inside the source (two Tamsfield records, same tax number and bank) Merge into one; map the old number to the survivor Key mapping
Record already in the target (Ridgeline, CP10's supplier) Extend the existing business partner Extend object, key mapping
Number already used in the target (German customer 10000001 vs Brennock) Renumber Key mapping
Inactive or obsolete (no activity for 24 months, flagged for deletion) Exclude with a reason Selection, and closing stale documents in ECC

Matching finds the duplicates. Exact matches on a tax number or bank account are strong evidence. Similar names are weak evidence: "Ridgeline Bearing Supply" and "Ridgeline Bearing Supply Europe GmbH" look alike but have different tax numbers, and may be different legal entities that must stay apart. SAP Master Data Governance runs this as a process for business partners and products: load, check, standardize, match with SAP HANA fuzzy search or smart data quality, match review by a person, calculate a best record, validate and activate (SAP Learning). A small migration can follow the same steps with a script and a review sheet.

Key mapping ties it together. Every old number maps to exactly one new number (itself, a new number, a merge survivor or an extended record), and every open item, purchase order and stock record follows that map. The key map is also the reconciliation backbone: without it, nobody can prove that invoice 95001109 belongs to the customer it belonged to in ECC.

Mock loads and exit criteria

A mock load runs the factory end to end on a fresh extract into a non-production system. Because the cockpit can't delete migrated records and production must never be used for practice, each mock needs a clean target: another client or a refreshed system, as SAP's product team recommends (SAP Community).

Exit criteria turn "it looks fine" into a decision. Calder's, as course rules:

  1. No load errors in simulation and migration.
  2. Every count reconciles: records in scope = loaded or mapped + excluded with an approved reason.
  3. Every amount reconciles to 0.00: receivables, payables, stock value and open PO value equal the ECC control totals.
  4. Every profile finding has an approved decision, by a named owner.
  5. For the dress rehearsal only: the whole cutover fits the window with the fallback time intact.

Measure load times in every mock at full volume. The time a mock takes is the best predictor of the cutover timeline you will need.

Reconciliation and sign-off

Reconciliation works at three levels:

Level Question Calder's check
Technical Did every record arrive? Counts per object: in scope, loaded or mapped, excluded
Financial Do the amounts match? Receivables and payables totals vs ECC reconciliation accounts; stock value per product; trial balance; offsets net to zero
Functional Does a process run on the data? Post a goods receipt to a migrated PO; clear a migrated invoice with a test payment; run MRP for plant CP21

Control totals come from the source, produced by the business owner of each object from ECC reports, before the load. The migration team compares the target against them. The owner signs. Keep signed reconciliations with the program records; auditors will ask how the opening balances were proved.

The cutover plan

Cutover is a timetable, written to the hour and rehearsed. Calder's plan, as course dates:

When (course plan) Step Owner
Mon 28 June 2027 Freeze master data changes in ECC CP20; final cleansing checks Data owners
Wed 30 June, 18:00 CET Last postings in ECC CP20; stop interfaces and EDI to CP10 Augsburg finance, integration lead
Thu 1 to Fri 2 July June close in ECC; control totals produced and signed Augsburg finance
Fri 2 July, 18:00 Extract; load master data, stock, open POs, open items Migration team
Sat 3 July Load G/L balances; technical and financial reconciliation Migration team, group accounting
Sun 4 July, 12:00 Functional checks; go/no-go decision Program lead, business owners
Sun 4 July, 18:00 Fallback deadline: if no-go, reopen ECC CP20 for Monday Program lead
Mon 5 July 2027 Augsburg works in S/4HANA; hypercare starts Everyone

Two features matter most. A go/no-go point with a real fallback: until users post in the new system, the old one can reopen. And cutover at a period boundary, so the closing balances of June are the opening balances of July.

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 does a migration lead's four jobs on a small, made-up extract of Calder's German company. It plans the load sequence from the objects' references. It profiles the extract for missing fields, inactive records and broken units. It suggests duplicates and field mappings for a person to approve. And it runs two mock loads: one on the raw extract, one after the approved decisions, each with a reconciliation and the exit criteria.

flowchart LR
  P[calder_pack/<br/>target records] --> S[migration.py]
  E[CP20 extract<br/>in the script] --> S
  S --> PL[calder_migration_plan.md]
  S --> PR[Profile findings]
  S --> SG[Suggestions]
  S --> M[calder_mock_1.md<br/>calder_mock_2.md]

The script loads nothing into a real SAP system. It imitates the checks a migration cockpit simulation runs, so you can see the method on data small enough to read.

What you need

  • The course folder orchestrate-course with its .venv, and the Unit 1B sample pack.
  • No account, no key and no network. The script uses only Python's built-in modules, so requirements.txt doesn't change.
  • 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 business_partners.json and products.json from the pack to know which records the S/4HANA system already holds for CP10, such as Brennock Water Utility as customer 10000001 and Ridgeline Bearing Supply as supplier 20000003.

Step 3: Create the script

  1. In VS Code, create a new file, paste the script below, and save it as migration.py inside the unit1b folder.
"""Plan, profile and rehearse the migration of Calder Pumpworks GmbH (CP20)
from ECC into the S/4HANA system that already runs CP10.

Calder Pumpworks is a fictional company. Every record, number, amount, rule
and exit criterion below is a course label or workshop input, not an SAP
standard value or a real project figure. Migration object names follow SAP's
migration cockpit; check the object list for your own target release.
The script reads the Calder sample pack (unit1b/calder_pack/) and needs no
account, no key and no network.

Run it from your course folder:
  python unit1b/migration.py              object list and load sequence
  python unit1b/migration.py --profile    data quality profile of the CP20 extract
  python unit1b/migration.py --suggest    duplicate and field mapping suggestions
  python unit1b/migration.py --mock 1     mock load with the raw extract
  python unit1b/migration.py --mock 2     mock load after the approved decisions
It writes unit1b/calder_migration_plan.md or unit1b/calder_mock_<n>.md
and changes nothing else.
"""
import argparse
import copy
import json
import sys
from difflib import SequenceMatcher
from pathlib import Path

PACK = Path("unit1b") / "calder_pack"
OUT = Path("unit1b")
KEY_DATE = "2027-01-31"        # extract date of mock load 1 (course label)
INACTIVE_SINCE = "2025-01-31"  # course rule: no activity for 24 months = out of scope
SIMILAR = 0.80                 # course rule: name similarity that triggers a review

# --------------------------------------------------------- the object list
# One row per migration object, in no particular order. The script works out
# the load sequence from the predecessors.
OBJECTS = [
    {"id": "ORG", "name": "Org units for CP20 (configuration, transported, not migrated)",
     "pred": [], "owner": "Solution architect", "check": "CP20, CP21, CPDE, PUDE, AU01 exist in S4Q"},
    {"id": "SUP", "name": "Supplier / Supplier - extend existing record by new org levels",
     "pred": ["ORG"], "owner": "Purchasing lead CP20", "check": "count by action; no tax number twice"},
    {"id": "CUS", "name": "Customer / Customer - extend existing record by new org levels",
     "pred": ["ORG"], "owner": "AR lead CP20", "check": "count by action; no tax number twice"},
    {"id": "PRD", "name": "Product (new) and an extension for plant CP21 (existing)",
     "pred": ["ORG"], "owner": "Materials planner CP21", "check": "count by action; base unit and conversions"},
    {"id": "INV", "name": "Material inventory balance",
     "pred": ["PRD"], "owner": "Plant controller CP21", "check": "quantity and value per product = ECC stock"},
    {"id": "POS", "name": "Purchase order (only open PO)",
     "pred": ["SUP", "PRD"], "owner": "Purchasing lead CP20", "check": "count and open value = ECC open POs"},
    {"id": "APO", "name": "FI - Accounts payable open item",
     "pred": ["SUP"], "owner": "AP lead CP20", "check": "total = ECC payables reconciliation account"},
    {"id": "ARO", "name": "FI - Accounts receivable open item",
     "pred": ["CUS"], "owner": "AR lead CP20", "check": "total = ECC receivables reconciliation account"},
    {"id": "GLB", "name": "FI - G/L account balance and open/line item",
     "pred": ["APO", "ARO", "INV"], "owner": "Group accounting", "check": "trial balance = ECC; offsets net to zero"},
]

# ------------------------------------------- the CP20 extract (ECC, fictional)
LEGACY = {
    "customers": [
        {"key": "10000001", "name": "Lindqvar Wasserwerke AG", "country": "DE", "tax": "TX-DE-118204",
         "terms": "NT30", "last_activity": "2027-01-12"},
        {"key": "10000003", "name": "Lindqvar Wasserwerke", "country": "DE", "tax": "TX-DE-118204",
         "terms": "NT30", "last_activity": "2026-11-03"},
        {"key": "10000004", "name": "Brenzhof Pumpentechnik GmbH", "country": "DE", "tax": "TX-DE-230871",
         "terms": "", "last_activity": "2027-01-20"},
        {"key": "10000005", "name": "Kestrel Anlagenbau GmbH", "country": "AT", "tax": "",
         "terms": "NT45", "last_activity": "2026-12-18"},
        {"key": "10000007", "name": "Ammerholt Filter KG", "country": "DE", "tax": "TX-DE-402215",
         "terms": "NT30", "last_activity": "2023-08-30"},
        {"key": "10000008", "name": "Ostmark Filtertechnik GmbH", "country": "DE", "tax": "TX-DE-517730",
         "terms": "NT30", "last_activity": "2027-01-25"},
        {"key": "10000010", "name": "Calder Pumpworks Inc. (intercompany)", "country": "US", "tax": "TX-US-CP10",
         "terms": "NT30", "last_activity": "2027-01-29", "intercompany": True, "trading_partner": ""},
    ],
    "suppliers": [
        {"key": "20000002", "name": "Tamsfield Foundry Ltd", "country": "GB", "tax": "TX-GB-77310",
         "bank": "BK-7001", "terms": "NT45", "last_activity": "2027-01-27"},
        {"key": "20000004", "name": "Tamsfield Foundry Ltd.", "country": "GB", "tax": "TX-GB-77310",
         "bank": "BK-7001", "terms": "NT30", "last_activity": "2026-12-02"},
        {"key": "20000007", "name": "Ridgeline Bearing Supply", "country": "US", "tax": "TX-US-RBS01",
         "bank": "BK-3303", "terms": "NT45", "last_activity": "2027-01-14"},
        {"key": "20000008", "name": "Ridgeline Bearing Supply Europe GmbH", "country": "DE", "tax": "TX-DE-660912",
         "bank": "BK-8808", "terms": "NT30", "last_activity": "2027-01-21"},
        {"key": "20000009", "name": "Hollmer Werkzeugdienst GmbH", "country": "DE", "tax": "TX-DE-330145",
         "bank": "BK-9009", "terms": "", "last_activity": "2027-01-18"},
        {"key": "20000011", "name": "Petrak Stahlhandel GmbH", "country": "DE", "tax": "TX-DE-145566",
         "bank": "BK-1111", "terms": "NT30", "last_activity": "2027-01-26"},
        {"key": "20000012", "name": "Vennick Kuehlmittel GmbH", "country": "DE", "tax": "TX-DE-099812",
         "bank": "BK-1212", "terms": "NT30", "last_activity": "2024-05-15"},
    ],
    "products": [
        {"key": "CP-IMP200", "text": "Machined impeller for ES-200", "unit": "EA", "conv": {}, "deletion_flag": False},
        {"key": "CP-IMP300", "text": "Machined impeller for ES-300", "unit": "EA", "conv": {}, "deletion_flag": False},
        {"key": "CP-CAST200", "text": "Impeller casting ES-200", "unit": "EA", "conv": {}, "deletion_flag": False},
        {"key": "CP-CAST300", "text": "Impeller casting ES-300", "unit": "EA", "conv": {}, "deletion_flag": False},
        {"key": "CP-BAR42", "text": "Bar stock 42CrMo4 80 mm", "unit": "KG", "conv": {}, "deletion_flag": False},
        {"key": "CP-BAR42-OLD", "text": "Bar stock 42CrMo4 80 mm", "unit": "KG", "conv": {}, "deletion_flag": True},
        {"key": "CP-BRG6310", "text": "Bearing 6310", "unit": "EA", "conv": {}, "deletion_flag": False},
        {"key": "CP-CTOOL", "text": "Cutting coolant", "unit": "L", "conv": {}, "deletion_flag": False},
        {"key": "CP-FIX-07", "text": "Fixture plate, old impeller line", "unit": "EA", "conv": {}, "deletion_flag": False},
    ],
    "stock": [  # plant CP21, valuation at standard price in EUR
        {"product": "CP-IMP200", "qty": 140, "price": 310.00},
        {"product": "CP-IMP300", "qty": 22, "price": 402.00},
        {"product": "CP-CAST200", "qty": 260, "price": 41.50},
        {"product": "CP-CAST300", "qty": 85, "price": 58.00},
        {"product": "CP-BAR42", "qty": 3200, "price": 2.85},
        {"product": "CP-BRG6310", "qty": 75, "price": 18.40},
        {"product": "CP-CTOOL", "qty": 120, "price": 4.10},
        {"product": "CP-FIX-07", "qty": 0, "price": 95.00},
    ],
    "purchase_orders": [  # ordered and received in the PO unit; price per PO unit in EUR
        {"key": "4500001201", "supplier": "20000002", "product": "CP-CAST200", "unit": "EA",
         "ordered": 200, "received": 120, "price": 41.50, "due": "2027-02-15"},
        {"key": "4500001202", "supplier": "20000004", "product": "CP-CAST300", "unit": "EA",
         "ordered": 60, "received": 0, "price": 58.00, "due": "2027-02-22"},
        {"key": "4500001203", "supplier": "20000007", "product": "CP-BRG6310", "unit": "EA",
         "ordered": 40, "received": 0, "price": 18.40, "due": "2027-02-10"},
        {"key": "4500001204", "supplier": "20000011", "product": "CP-BAR42", "unit": "KG",
         "ordered": 1500, "received": 1500, "price": 2.85, "due": "2027-01-08"},
        {"key": "4500001205", "supplier": "20000011", "product": "CP-BAR42-OLD", "unit": "KG",
         "ordered": 500, "received": 0, "price": 2.85, "due": "2027-03-01"},
        {"key": "4500001206", "supplier": "20000009", "product": "CP-CTOOL", "unit": "KAN",
         "ordered": 20, "received": 0, "price": 82.00, "due": "2027-02-05"},
        {"key": "4500001207", "supplier": "20000012", "product": "CP-CTOOL", "unit": "L",
         "ordered": 200, "received": 0, "price": 4.10, "due": "2024-06-30"},
    ],
    "ar_items": [
        {"key": "95001102", "customer": "10000010", "amount": 12480.00},
        {"key": "95001107", "customer": "10000001", "amount": 8200.00},
        {"key": "95001109", "customer": "10000003", "amount": 1150.00},
        {"key": "95001111", "customer": "10000004", "amount": 4760.00},
        {"key": "95001114", "customer": "10000008", "amount": 3310.00},
    ],
    "ap_items": [
        {"key": "51000771", "supplier": "20000002", "amount": 4980.00},
        {"key": "51000774", "supplier": "20000004", "amount": 1740.00},
        {"key": "51000780", "supplier": "20000007", "amount": 2215.00},
        {"key": "51000783", "supplier": "20000009", "amount": 1090.00},
    ],
}

# Records the S/4HANA system already holds besides the pack's CP10 partners.
TARGET_EXTRA_PARTNERS = {"20000004": "Corvane Lifting Services", "20000005": "Larkspur Freight Lines",
                         "20000006": "Tallis Road Freight"}
TARGET_EXTRA_PRODUCTS = {"CP-IMP200", "CP-IMP300"}  # CP10 buys both from CP20 today (plant CP11 only)
TARGET_TAX = {"20000003": "TX-US-RBS01"}            # Ridgeline's tax number in CP10's records

# ------------------------------------------- decisions for mock load 2
# Each decision names who approved it. "where": source = fixed in ECC before
# extraction; map = handled in the mapping (key mapping or value mapping).
DECISIONS = [
    ("customers", "10000001", "renumber", {"to": "10000101"}, "map", "Master data lead"),
    ("customers", "10000003", "merge", {"into": "10000001"}, "map", "AR lead CP20"),
    ("customers", "10000004", "fix", {"terms": "NT30"}, "source", "AR lead CP20"),
    ("customers", "10000005", "fix", {"tax": "TX-AT-552019"}, "source", "AR lead CP20"),
    ("customers", "10000007", "exclude", {"reason": "no activity since 2023-08-30, no open items"}, "map", "AR lead CP20"),
    ("customers", "10000010", "fix", {"trading_partner": "CP10"}, "source", "Group accounting"),
    ("suppliers", "20000004", "merge", {"into": "20000002"}, "map", "Purchasing lead CP20"),
    ("suppliers", "20000007", "extend", {"into": "20000003"}, "map", "Master data lead"),
    ("suppliers", "20000008", "distinct", {"of": "20000003"}, "map", "Purchasing lead CP20"),
    ("suppliers", "20000009", "fix", {"terms": "NT45"}, "source", "AP lead CP20"),
    ("suppliers", "20000012", "exclude", {"reason": "no activity since 2024-05-15; stale PO closed in ECC"}, "map", "Purchasing lead CP20"),
    ("products", "CP-IMP200", "extend", {"into": "CP-IMP200"}, "map", "Materials planner CP21"),
    ("products", "CP-IMP300", "extend", {"into": "CP-IMP300"}, "map", "Materials planner CP21"),
    ("products", "CP-BAR42-OLD", "exclude", {"reason": "flagged for deletion; duplicate of CP-BAR42"}, "map", "Materials planner CP21"),
    ("products", "CP-FIX-07", "exclude", {"reason": "no stock, no movements, no open documents"}, "map", "Materials planner CP21"),
    ("products", "CP-CTOOL", "fix", {"conv": {"KAN": 20}}, "source", "Materials planner CP21"),
    ("stock", "CP-FIX-07", "exclude", {"reason": "zero quantity; product not migrated"}, "map", "Plant controller CP21"),
    ("purchase_orders", "4500001205", "fix", {"product": "CP-BAR42"}, "source", "Purchasing lead CP20"),
    ("purchase_orders", "4500001207", "close", {"reason": "due 2024-06-30, supplier inactive"}, "source", "Purchasing lead CP20"),
]
KEY_FIELD = {"stock": "product"}


def load_pack():
    """The partners and products the S/4HANA system already holds for CP10."""
    try:
        bps = json.loads((PACK / "business_partners.json").read_text(encoding="utf-8"))["BusinessPartners"]
        products = json.loads((PACK / "products.json").read_text(encoding="utf-8"))["Products"]
    except FileNotFoundError:
        sys.exit("Sample pack not found. Run: python unit1b/make_calder_pack.py")
    partners = dict(TARGET_EXTRA_PARTNERS)
    for bp in bps:
        role = bp.get("to_Customer") or bp.get("to_Supplier")
        companies = role.get("to_CustomerCompany") or role.get("to_SupplierCompany")
        if any(c["CompanyCode"] == "CP10" for c in companies):
            partners[bp["BusinessPartner"]] = bp["BusinessPartnerFullName"]
    target_products = {p["Product"] for p in products if any(pl["Plant"] == "CP11" for pl in p["to_Plant"])}
    return partners, target_products | TARGET_EXTRA_PRODUCTS


def load_sequence():
    """Order the objects so that every object comes after its predecessors."""
    done, order, todo = set(), [], list(OBJECTS)
    while todo:
        ready = [o for o in todo if all(p in done for p in o["pred"])]
        if not ready:
            sys.exit("The predecessors form a loop: " + ", ".join(o["id"] for o in todo))
        for o in ready:
            order.append(o)
            done.add(o["id"])
            todo.remove(o)
    return order


def plan():
    order = load_sequence()
    print(f"Calder CP20 migration: {len(order)} objects in load order (course labels)\n")
    print(f"{'#':>2}  {'ID':4} {'Object':64} After")
    for n, o in enumerate(order, 1):
        print(f"{n:>2}  {o['id']:4} {o['name'][:64]:64} {', '.join(o['pred']) or '-'}")
    fence = "`" * 3
    lines = ["# Calder CP20 migration plan", "", "Fictional course data. Object names follow the SAP migration cockpit.",
             "", "| # | Object | After | Owner | Reconciliation check |", "| --- | --- | --- | --- | --- |"]
    lines += [f"| {n} | {o['name']} | {', '.join(o['pred']) or '-'} | {o['owner']} | {o['check']} |"
              for n, o in enumerate(order, 1)]
    lines += ["", fence + "mermaid", "flowchart LR"]
    lines += [f"  {o['id']}[\"{o['id']}\"]" for o in order]
    lines += [f"  {p} --> {o['id']}" for o in order for p in o["pred"]]
    lines += [fence, ""]
    (OUT / "calder_migration_plan.md").write_text("\n".join(lines), encoding="utf-8")
    print("\nWrote unit1b/calder_migration_plan.md")


def norm(name):
    """Lower case, no punctuation, no legal form, for name comparison."""
    words = "".join(c if c.isalnum() else " " for c in name.lower()).split()
    return " ".join(w for w in words if w not in {"gmbh", "ag", "kg", "ltd", "inc"})


def duplicate_pairs(records, existing_tax=None):
    """Pairs that share a tax number or have similar names, with a score."""
    pairs = []
    for i, a in enumerate(records):
        for b in records[i + 1:]:
            score = SequenceMatcher(None, norm(a["name"]), norm(b["name"])).ratio()
            same_tax = bool(a["tax"]) and a["tax"] == b["tax"]
            if same_tax or score >= SIMILAR:
                pairs.append((a["key"], b["key"], round(score, 2), same_tax))
    for a in records:
        for key, tax in (existing_tax or {}).items():
            if a["tax"] == tax:
                pairs.append((a["key"], f"{key} (in S/4HANA)", 1.0, True))
    return pairs


def profile(quiet=False):
    """Print the findings and return the records they are about."""
    say = (lambda *a: None) if quiet else print
    say(f"CP20 extract, key date {KEY_DATE}: data quality profile (fictional data)\n")
    found = set()
    for kind, fields in (("customers", ["name", "country", "tax", "terms"]),
                         ("suppliers", ["name", "country", "tax", "bank", "terms"])):
        rows = LEGACY[kind]
        say(f"{kind}: {len(rows)} records")
        for f in fields:
            missing = [r["key"] for r in rows if not r.get(f)]
            filled = 100 * (len(rows) - len(missing)) / len(rows)
            found |= {(kind, k) for k in missing}
            say(f"  {'FINDING' if missing else 'OK':8} {f:8} {filled:5.1f}% filled"
                + (f"  missing: {', '.join(missing)}" if missing else ""))
        for r in rows:
            if r["last_activity"] < INACTIVE_SINCE:
                found.add((kind, r["key"]))
                say(f"  FINDING  inactive {r['key']} {r['name']}: last activity {r['last_activity']}")
            if r.get("intercompany") and not r.get("trading_partner"):
                found.add((kind, r["key"]))
                say(f"  FINDING  trading partner missing on intercompany partner {r['key']}")
    products = LEGACY["products"]
    say(f"products: {len(products)} records")
    stock = {s["product"]: s["qty"] for s in LEGACY["stock"]}
    for p in products:
        if p["deletion_flag"]:
            found.add(("products", p["key"]))
            say(f"  FINDING  {p['key']} is flagged for deletion")
        elif stock.get(p["key"], 0) == 0 and not any(po["product"] == p["key"] for po in LEGACY["purchase_orders"]):
            found.add(("products", p["key"]))
            say(f"  FINDING  {p['key']} has no stock and no purchase order")
    pos = LEGACY["purchase_orders"]
    say(f"purchase orders: {len(pos)} records")
    for po in pos:
        prod = next(p for p in products if p["key"] == po["product"])
        if po["unit"] != prod["unit"] and po["unit"] not in prod["conv"]:
            found.add(("products", prod["key"]))
            say(f"  FINDING  {po['key']} ordered in {po['unit']}, product {prod['key']} has no conversion to {prod['unit']}")
        if po["received"] < po["ordered"] and po["due"] < INACTIVE_SINCE:
            found.add(("purchase_orders", po["key"]))
            say(f"  FINDING  {po['key']} still open but due {po['due']}")
    say(f"\n{len(found)} record(s) with findings. Each needs an owner's decision before mock load 2.")
    return found


def suggest():
    print("Suggestions only. A named owner approves or rejects each one.\n")
    print("Possible duplicates (score = name similarity, 1.00 = identical after cleanup)")
    for kind, existing in (("customers", None), ("suppliers", TARGET_TAX)):
        for a, b, score, same_tax in duplicate_pairs(LEGACY[kind], existing):
            basis = "same tax number" if same_tax else "similar name only"
            print(f"  {kind[:-1]:8} {a} ~ {b}: {score:.2f}, {basis}")
    print("\nField mapping, Augsburg export header -> course target field")
    glossary = {"name 1": "BusinessPartnerFullName", "ust-idnr": "TaxNumber", "land": "Country",
                "zahlungsbedingung": "PaymentTerms", "abstimmkonto": "ReconciliationAccount",
                "basismengeneinheit": "BaseUnit", "materialkurztext": "ProductDescription"}
    targets = sorted(set(glossary.values()) | {"Currency", "SupplierName"})
    for header in ["Name 1", "USt-IdNr.", "Land", "Zahlungsbed.", "Abstimmkonto", "Basis-ME", "Waehrung"]:
        key = header.lower().rstrip(".")
        best = max(glossary, key=lambda g: SequenceMatcher(None, key, g).ratio())
        score = SequenceMatcher(None, key, best).ratio()
        if score >= 0.6:
            print(f"  {header:13} -> {glossary[best]:24} {score:.2f}")
        else:
            near = max(targets, key=lambda t: SequenceMatcher(None, key, t.lower()).ratio())
            print(f"  {header:13} -> ?{'':23} {score:.2f}  no confident match (nearest: {near}); ask the data owner")


def apply_decisions(data):
    """Return the extract after source fixes, and the key map for the load."""
    data = copy.deepcopy(data)
    keymap, excluded, log = {}, {}, []
    for kind, key, action, args, where, approver in DECISIONS:
        field = KEY_FIELD.get(kind, "key")
        rec = next(r for r in data[kind] if r[field] == key)
        if action == "fix":
            for f, v in args.items():
                rec[f] = {**rec[f], **v} if isinstance(v, dict) else v
        elif action == "close":
            rec["ordered"] = rec["received"]
        elif action in ("merge", "extend"):
            keymap[(kind, key)] = (action, args["into"])
        elif action == "renumber":
            keymap[(kind, key)] = ("renumber", args["to"])
        elif action == "exclude":
            excluded[(kind, key)] = args["reason"]
        log.append(f"{kind[:-1] if kind != 'stock' else 'stock'} {key}: {action} ({where}), approved by {approver}")
    return data, keymap, excluded, log


def control_totals(data):
    """What the ECC side reports for the extract: the numbers the business signs."""
    open_po = [p for p in data["purchase_orders"] if p["ordered"] > p["received"]]
    return {
        "AR open items (EUR)": round(sum(i["amount"] for i in data["ar_items"]), 2),
        "AP open items (EUR)": round(sum(i["amount"] for i in data["ap_items"]), 2),
        "Stock value CP21 (EUR)": round(sum(s["qty"] * s["price"] for s in data["stock"]), 2),
        "Open PO value (EUR)": round(sum((p["ordered"] - p["received"]) * p["price"] for p in open_po), 2),
    }


def mock(n):
    partners, target_products = load_pack()
    data, keymap, excluded, log = (copy.deepcopy(LEGACY), {}, {}, []) if n == 1 else apply_decisions(LEGACY)
    errors, loaded = [], {}
    created_tax, extended = {}, set()

    def resolve(kind, key):
        """Follow merges, extensions and new numbers to the record in S/4HANA."""
        while (kind, key) in keymap:
            key = keymap[(kind, key)][1]
        return key

    for kind in ("suppliers", "customers"):  # partners, in load order
        ok = 0
        for r in data[kind]:
            k = (kind, r["key"])
            if k in excluded:
                continue
            action = keymap.get(k, ("create",))[0]
            target = resolve(kind, r["key"])
            if action == "merge":
                ok += 1
                continue
            if action == "extend":
                if target in partners:
                    extended.add(target)
                    ok += 1
                else:
                    errors.append(f"{kind[:-1]} {r['key']}: record {target} to extend not found")
                continue
            problems = []
            if not r.get("terms"):
                problems.append("payment terms missing")
            if not r.get("tax"):
                problems.append("tax number missing")
            if r.get("intercompany") and not r.get("trading_partner"):
                problems.append("trading partner missing on intercompany partner")
            if target in partners:
                problems.append(f"number {target} already used by {partners[target]}")
            if r.get("tax") and (r["tax"] in created_tax.values() or r["tax"] in TARGET_TAX.values()):
                problems.append("tax number already exists (duplicate)")
            if problems:
                errors.append(f"{kind[:-1]} {r['key']}: " + "; ".join(problems))
            else:
                created_tax[target] = r["tax"]
                ok += 1
        loaded[kind] = ok
    good_partners = set(created_tax) | extended  # records this migration created or extended

    good_products, ok = set(), 0
    for p in data["products"]:
        k = ("products", p["key"])
        if k in excluded:
            continue
        action, _ = keymap.get(k, ("create", p["key"]))
        if action == "create" and p["key"] in target_products:
            errors.append(f"product {p['key']}: already exists; migration creates records, it doesn't update them")
            continue
        good_products.add(p["key"])
        ok += 1
    loaded["products"] = ok

    stock_value, ok = 0.0, 0
    for s in data["stock"]:
        if ("stock", s["product"]) in excluded:
            continue
        if s["qty"] <= 0:
            errors.append(f"stock {s['product']}: quantity 0 can't be posted as an initial stock entry")
        elif s["product"] not in good_products:
            errors.append(f"stock {s['product']}: product has no plant CP21 data in the target")
        else:
            stock_value += s["qty"] * s["price"]
            ok += 1
    loaded["stock"] = ok

    po_value, ok, in_scope = 0.0, 0, 0
    for po in data["purchase_orders"]:
        if po["ordered"] <= po["received"]:
            continue  # not open: out of scope for "Purchase order (only open PO)"
        in_scope += 1
        prod = next(p for p in data["products"] if p["key"] == po["product"])
        problems = []
        if resolve("suppliers", po["supplier"]) not in good_partners:
            problems.append(f"supplier {po['supplier']} not in target")
        if po["product"] not in good_products:
            problems.append(f"product {po['product']} not in target")
        if po["unit"] != prod["unit"] and po["unit"] not in prod["conv"]:
            problems.append(f"no conversion {po['unit']} -> {prod['unit']}")
        if problems:
            errors.append(f"purchase order {po['key']}: " + "; ".join(sorted(set(problems))))
        else:
            po_value += (po["ordered"] - po["received"]) * po["price"]
            ok += 1
    loaded["purchase_orders"] = ok

    sums = {}
    for kind, partner_kind, field in (("ar_items", "customers", "customer"), ("ap_items", "suppliers", "supplier")):
        total, ok = 0.0, 0
        for item in data[kind]:
            target = resolve(partner_kind, item[field])
            if target in good_partners:
                total += item["amount"]
                ok += 1
            else:
                errors.append(f"{kind[:2].upper()} item {item['key']}: {partner_kind[:-1]} {item[field]} not in target")
        sums[kind], loaded[kind] = total, ok

    target_totals = {"AR open items (EUR)": round(sums["ar_items"], 2),
                     "AP open items (EUR)": round(sums["ap_items"], 2),
                     "Stock value CP21 (EUR)": round(stock_value, 2),
                     "Open PO value (EUR)": round(po_value, 2)}
    control = control_totals(data)

    print(f"Mock load {n}, CP20 extract {KEY_DATE} (fictional data)\n")
    if log:
        print(f"Applied {len(log)} approved decisions")
        for line in log:
            print("  " + line)
        print()
    print("Counts: in scope, loaded or mapped, excluded with reason")
    counts_ok = True
    for kind in ("suppliers", "customers", "products", "stock", "purchase_orders", "ar_items", "ap_items"):
        field = KEY_FIELD.get(kind, "key")
        scope = in_scope if kind == "purchase_orders" else len(data[kind])
        exc = sum(1 for r in data[kind] if (kind, r[field]) in excluded)
        gap = scope - loaded[kind] - exc
        counts_ok &= gap == 0
        print(f"  {kind:16} {scope:3} {loaded[kind]:4} {exc:4}   " + ("OK" if gap == 0 else f"DIFF {gap}"))
    print("\nAmounts: ECC control total vs loaded")
    amounts_ok = True
    for name, value in control.items():
        diff = round(value - target_totals[name], 2)
        amounts_ok &= diff == 0
        print(f"  {name:24} {value:12,.2f} {target_totals[name]:12,.2f}   " + ("OK" if diff == 0 else f"DIFF {diff:,.2f}"))
    if errors:
        print(f"\nLoad errors ({len(errors)})")
        for e in errors:
            print("  " + e)
    decided = {(kind, key) for kind, key, *_rest, approver in (DECISIONS if n == 2 else []) if approver}
    undecided = sorted(profile(quiet=True) - decided)
    criteria = [("No load errors", not errors), ("Every count reconciles", counts_ok),
                ("Every amount reconciles to 0.00", amounts_ok),
                ("Every profile finding has an approved decision", not undecided)]
    print("\nExit criteria")
    for text, ok in criteria:
        print(f"  {'PASS' if ok else 'FAIL'}  {text}")
    passed = all(ok for _, ok in criteria)
    print("\nResult: " + ("ready for the dress rehearsal" if passed else "not ready; fix the errors and run the next mock"))
    report = [f"# Calder CP20 mock load {n}", "", f"Fictional course data, extract {KEY_DATE}.", ""]
    report += [f"- {'PASS' if ok else 'FAIL'}: {text}" for text, ok in criteria]
    report += ["", "## Load errors", ""] + ([f"- {e}" for e in errors] or ["- none"]) + [""]
    (OUT / f"calder_mock_{n}.md").write_text("\n".join(report), encoding="utf-8")
    print(f"Wrote unit1b/calder_mock_{n}.md")
    return 0 if passed else 1


def main():
    parser = argparse.ArgumentParser(description="Calder CP20 data migration (fictional course data)")
    parser.add_argument("--profile", action="store_true", help="data quality profile of the extract")
    parser.add_argument("--suggest", action="store_true", help="duplicate and field mapping suggestions")
    parser.add_argument("--mock", type=int, choices=[1, 2], help="run mock load 1 (raw) or 2 (after decisions)")
    args = parser.parse_args()
    if args.profile:
        profile()
    elif args.suggest:
        suggest()
    elif args.mock:
        sys.exit(mock(args.mock))
    else:
        load_pack()
        plan()


if __name__ == "__main__":
    main()
  1. Save the file (Ctrl+S on Windows and Linux, Cmd+S on macOS).

Step 4: Plan the load sequence

  1. Run the script with no options:

    • Windows (PowerShell):

      python unit1b\migration.py
    • macOS / Linux:

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

Calder CP20 migration: 9 objects in load order (course labels)

 #  ID   Object                                                           After
 1  ORG  Org units for CP20 (configuration, transported, not migrated)    -
 2  SUP  Supplier / Supplier - extend existing record by new org levels   ORG
 3  CUS  Customer / Customer - extend existing record by new org levels   ORG
 4  PRD  Product (new) and an extension for plant CP21 (existing)         ORG
 5  INV  Material inventory balance                                       PRD
 6  POS  Purchase order (only open PO)                                    SUP, PRD
 7  APO  FI - Accounts payable open item                                  SUP
 8  ARO  FI - Accounts receivable open item                               CUS
 9  GLB  FI - G/L account balance and open/line item                      APO, ARO, INV

Wrote unit1b/calder_migration_plan.md
  1. Open unit1b/calder_migration_plan.md and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS) for the preview. You should see the object table and a diagram of the dependencies.

The order is computed, not typed. The script repeatedly takes every object whose predecessors are already placed. If you add an object whose predecessors form a loop, it stops and names them.

Step 5: Profile the extract

  1. Run:
    • Windows (PowerShell): python unit1b\migration.py --profile
    • macOS / Linux: python unit1b/migration.py --profile
  2. You should see:
CP20 extract, key date 2027-01-31: data quality profile (fictional data)

customers: 7 records
  OK       name     100.0% filled
  OK       country  100.0% filled
  FINDING  tax       85.7% filled  missing: 10000005
  FINDING  terms     85.7% filled  missing: 10000004
  FINDING  inactive 10000007 Ammerholt Filter KG: last activity 2023-08-30
  FINDING  trading partner missing on intercompany partner 10000010
suppliers: 7 records
  OK       name     100.0% filled
  OK       country  100.0% filled
  OK       tax      100.0% filled
  OK       bank     100.0% filled
  FINDING  terms     85.7% filled  missing: 20000009
  FINDING  inactive 20000012 Vennick Kuehlmittel GmbH: last activity 2024-05-15
products: 9 records
  FINDING  CP-BAR42-OLD is flagged for deletion
  FINDING  CP-FIX-07 has no stock and no purchase order
purchase orders: 7 records
  FINDING  4500001206 ordered in KAN, product CP-CTOOL has no conversion to L
  FINDING  4500001207 still open but due 2024-06-30

10 record(s) with findings. Each needs an owner's decision before mock load 2.

Each finding is a question for a person, not a fix for the script. The trading partner finding is the one the intercompany topic already raised: intercompany partners without a trading partner break the group's elimination.

Step 6: Ask for suggestions

  1. Run:
    • Windows (PowerShell): python unit1b\migration.py --suggest
    • macOS / Linux: python unit1b/migration.py --suggest
  2. You should see:
Suggestions only. A named owner approves or rejects each one.

Possible duplicates (score = name similarity, 1.00 = identical after cleanup)
  customer 10000001 ~ 10000003: 1.00, same tax number
  supplier 20000002 ~ 20000004: 1.00, same tax number
  supplier 20000007 ~ 20000008: 0.87, similar name only
  supplier 20000007 ~ 20000003 (in S/4HANA): 1.00, same tax number

Field mapping, Augsburg export header -> course target field
  Name 1        -> BusinessPartnerFullName  1.00
  USt-IdNr.     -> TaxNumber                1.00
  Land          -> Country                  1.00
  Zahlungsbed.  -> PaymentTerms             0.79
  Abstimmkonto  -> ReconciliationAccount    1.00
  Basis-ME      -> ?                        0.54  no confident match (nearest: BaseUnit); ask the data owner
  Waehrung      -> ?                        0.40  no confident match (nearest: BaseUnit); ask the data owner

Read the duplicate list like a steward would. Three pairs share a tax number and are strong candidates. The pair 20000007 and 20000008 shares only a similar name; in the decisions, Calder's purchasing lead keeps them apart. The mapping list shows the limit of string similarity: Basis-ME (base unit) and Waehrung (currency) are obvious to a German speaker and invisible to the matcher. That gap is where a language model helps, and where its answer still goes to the data owner.

Step 7: Run mock load 1 on the raw extract

  1. Run:
    • Windows (PowerShell): python unit1b\migration.py --mock 1
    • macOS / Linux: python unit1b/migration.py --mock 1
  2. You should see:
Mock load 1, CP20 extract 2027-01-31 (fictional data)

Counts: in scope, loaded or mapped, excluded with reason
  suppliers          7    4    0   DIFF 3
  customers          7    3    0   DIFF 4
  products           9    7    0   DIFF 2
  stock              8    5    0   DIFF 3
  purchase_orders    6    3    0   DIFF 3
  ar_items           5    2    0   DIFF 3
  ap_items           4    1    0   DIFF 3

Amounts: ECC control total vs loaded
  AR open items (EUR)         29,900.00     4,460.00   DIFF 25,440.00
  AP open items (EUR)         10,025.00     4,980.00   DIFF 5,045.00
  Stock value CP21 (EUR)      78,956.00    26,712.00   DIFF 52,244.00
  Open PO value (EUR)         11,421.00     5,565.00   DIFF 5,856.00

Load errors (21)
  supplier 20000004: number 20000004 already used by Corvane Lifting Services; tax number already exists (duplicate)
  supplier 20000007: tax number already exists (duplicate)
  supplier 20000009: payment terms missing
  customer 10000001: number 10000001 already used by Brennock Water Utility
  customer 10000004: payment terms missing
  customer 10000005: tax number missing
  customer 10000010: trading partner missing on intercompany partner
  product CP-IMP200: already exists; migration creates records, it doesn't update them
  product CP-IMP300: already exists; migration creates records, it doesn't update them
  stock CP-IMP200: product has no plant CP21 data in the target
  stock CP-IMP300: product has no plant CP21 data in the target
  stock CP-FIX-07: quantity 0 can't be posted as an initial stock entry
  purchase order 4500001202: supplier 20000004 not in target
  purchase order 4500001203: supplier 20000007 not in target
  purchase order 4500001206: no conversion KAN -> L; supplier 20000009 not in target
  AR item 95001102: customer 10000010 not in target
  AR item 95001107: customer 10000001 not in target
  AR item 95001111: customer 10000004 not in target
  AP item 51000774: supplier 20000004 not in target
  AP item 51000780: supplier 20000007 not in target
  AP item 51000783: supplier 20000009 not in target

Exit criteria
  FAIL  No load errors
  FAIL  Every count reconciles
  FAIL  Every amount reconciles to 0.00
  FAIL  Every profile finding has an approved decision

Result: not ready; fix the errors and run the next mock
Wrote unit1b/calder_mock_1.md

The exit code is 1. Look at three things in this result:

  • Errors cascade. Seven partner errors cause ten more downstream: purchase orders and open items can't load for partners that didn't load. Fix the master data and most of the rest disappears.
  • Some damage doesn't show as an error. Invoice 95001109 loaded, because customer 10000003 loaded. But 10000003 is the duplicate of 10000001, so the target now holds a duplicate customer with a real receivable on it. Only the profile and the suggestions caught that. In a real system you can't delete it; you start the next mock in a fresh client.
  • Counts and amounts disagree in size. Stock is short by 52,244.00 EUR from only three records, because the two impellers are the most valuable stock. Reconcile amounts, not just counts.

Step 8: Run mock load 2 after the decisions

The list DECISIONS in the script holds 19 decisions, each with the person who approved it and whether it is fixed in the source (ECC) or in the mapping. That list is the decision log a real program keeps.

  1. Run:
    • Windows (PowerShell): python unit1b\migration.py --mock 2
    • macOS / Linux: python unit1b/migration.py --mock 2
  2. You should see:
Mock load 2, CP20 extract 2027-01-31 (fictional data)

Applied 19 approved decisions
  customer 10000001: renumber (map), approved by Master data lead
  customer 10000003: merge (map), approved by AR lead CP20
  customer 10000004: fix (source), approved by AR lead CP20
  customer 10000005: fix (source), approved by AR lead CP20
  customer 10000007: exclude (map), approved by AR lead CP20
  customer 10000010: fix (source), approved by Group accounting
  supplier 20000004: merge (map), approved by Purchasing lead CP20
  supplier 20000007: extend (map), approved by Master data lead
  supplier 20000008: distinct (map), approved by Purchasing lead CP20
  supplier 20000009: fix (source), approved by AP lead CP20
  supplier 20000012: exclude (map), approved by Purchasing lead CP20
  product CP-IMP200: extend (map), approved by Materials planner CP21
  product CP-IMP300: extend (map), approved by Materials planner CP21
  product CP-BAR42-OLD: exclude (map), approved by Materials planner CP21
  product CP-FIX-07: exclude (map), approved by Materials planner CP21
  product CP-CTOOL: fix (source), approved by Materials planner CP21
  stock CP-FIX-07: exclude (map), approved by Plant controller CP21
  purchase_order 4500001205: fix (source), approved by Purchasing lead CP20
  purchase_order 4500001207: close (source), approved by Purchasing lead CP20

Counts: in scope, loaded or mapped, excluded with reason
  suppliers          7    6    1   OK
  customers          7    6    1   OK
  products           9    7    2   OK
  stock              8    7    1   OK
  purchase_orders    5    5    0   OK
  ar_items           5    5    0   OK
  ap_items           4    4    0   OK

Amounts: ECC control total vs loaded
  AR open items (EUR)         29,900.00    29,900.00   OK
  AP open items (EUR)         10,025.00    10,025.00   OK
  Stock value CP21 (EUR)      78,956.00    78,956.00   OK
  Open PO value (EUR)         10,601.00    10,601.00   OK

Exit criteria
  PASS  No load errors
  PASS  Every count reconciles
  PASS  Every amount reconciles to 0.00
  PASS  Every profile finding has an approved decision

Result: ready for the dress rehearsal
Wrote unit1b/calder_mock_2.md

The exit code is 0. Notice the open PO control total fell from 11,421.00 to 10,601.00 EUR. That isn't a loss: purchasing closed the stale PO 4500001207 in ECC, so the source total changed with it. A control total always comes from the source as it stands after source-side cleansing.

Step 9: Save your work in Git

  1. Run:
git add unit1b/migration.py unit1b/calder_migration_plan.md unit1b/calder_mock_1.md unit1b/calder_mock_2.md
git commit -m "Unit 1B: Calder CP20 migration plan and two mock loads"
  1. You should see a line such as 4 files changed.

What each part of the code does

Part What it does
OBJECTS The migration object list: names, predecessors, business owner and the reconciliation check
LEGACY The made-up CP20 extract: customers, suppliers, products, stock, open POs, open items
TARGET_EXTRA_PARTNERS, TARGET_TAX Records the S/4HANA system holds besides the pack's CP10 partners
DECISIONS The decision log: action, where it is fixed (source or mapping), approver
load_pack() Reads the pack to find the partners and products already in the target
load_sequence() Orders the objects so that each comes after its predecessors; stops on a loop
profile() Counts filled fields and flags inactive, obsolete and unconvertible records
duplicate_pairs(), suggest() Scores name similarity and exact tax-number matches; maps export headers with a glossary
apply_decisions() Applies source fixes to a copy of the extract and builds the key map and exclusions
mock() Imitates the simulation checks per object in load order, then reconciles counts and amounts and tests the exit criteria

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found Python isn't on the path, or the virtual environment is off Turn on .venv (Step 1); on macOS or Linux try python3
Sample pack not found. Run: python unit1b/make_calder_pack.py The pack folder is missing, or you are in the wrong folder Run the command from the course folder; repeat Step 2
can't open file ... migration.py The file was saved elsewhere or under another name Save it as migration.py inside unit1b
ModuleNotFoundError A line was mistyped in the imports The script needs only built-in modules; copy it again
SyntaxError or IndentationError A paste lost or added spaces Paste the whole script again into an empty file
StopIteration after you edit DECISIONS A decision names a record that isn't in LEGACY Check the key and the kind (customers, products) of the new decision
The predecessors form a loop Two objects in OBJECTS wait for each other Remove one of the two predecessor entries
Network or proxy errors None expected: the script never uses the network If you see one, you are running a different file

The SAP way

As of October 2026, here is what SAP documents for each part of a migration into S/4HANA. Check each against the release you are moving to.

Part SAP offering Status and notes
Load tool SAP S/4HANA migration cockpit, Migrate Your Data app In on-premise, private and public cloud deployments; functions and templates vary by deployment; embedded at no additional cost (SAP Learning)
Load from an SAP ERP system Direct transfer over RFC, selected by company code A set of predefined objects; others by staging tables (SAP Learning); Public Edition since 2402 (SAP Community FAQ)
Load from files or other systems Staging tables, local (XML or CSV templates) or remote SAP HANA schema (your ETL or SAP Data Services) Remote schema for large volumes (SAP Learning)
Change an object Migration object modeler (LTMOM) To adjust source selection and field mapping; custom fields mapped by hand (SAP Community FAQ)
Which objects exist Object list on the SAP Help Portal per release Linked from the community FAQ; names change between releases
Duplicates and best records SAP Master Data Governance, consolidation Separate product; matching for business partner and product (SAP Learning)

What the cockpit doesn't do matters as much for the plan. According to SAP's product team, it migrates each record once, offers no delta loads, isn't built to keep two systems in sync, and has no deletion function (SAP Community). The community FAQ adds that updating data already in the target is out of scope (SAP Community FAQ). For Calder that settles three design points: one final load at cutover rather than a trickle, fresh clients for each mock, and extend objects for records that already exist.

For Calder's source, ECC 6.0 with enhancement package 8, and target, S/4HANA Cloud Private Edition 2023, direct transfer by company code CP20 is the natural route for the objects it supports. The plan still needs the staging route for objects outside that set and for the records the team transforms outside the source. Which objects fall where is a check against the object list, not something to assume.

Build vs. SAP

Situation Use Why
Loading master data, open items and balances into S/4HANA Migration cockpit SAP's objects run the application's own checks; included in the system
Moving a company code from an SAP ERP system Direct transfer where the object is supported, staging tables for the rest No file handling for supported objects; selection by company code
Very large volumes or many non-SAP sources Remote staging schema filled by an ETL tool Efficient at high volume; transformation logic in one tool
Profiling, duplicate screening and control totals during mocks A script like this topic's, or a data quality tool Fast to change between mocks; output goes to people for decisions
Ongoing duplicate prevention and governance after go-live SAP Master Data Governance Matching and approval become part of the process, not a one-off
Writing records directly into SAP tables Never Bypasses the application's checks and breaks clean core

Architecture view

Where it sits. The migration runs between Calder's two production systems, with the mocks in the quality landscape.

flowchart LR
  ECP[(ECC ECP<br/>CP20)] -->|RFC, direct transfer| MC[Migration cockpit<br/>in S4Q / S4P]
  ECP -->|extract| ST[(Staging tables)]
  ST --> MC
  PR[Profiling and<br/>reconciliation scripts] -.read.-> ECP
  PR -.read.-> S4P
  MC --> S4P[(S/4HANA S4P<br/>CP10 + CP20)]
  PR --> RC[Signed reconciliation]

Mocks load into S4Q clients refreshed for each cycle; the production load goes into S4P. The scripts read both sides and write only reports.

Non-functional requirements that decide the design

Requirement Typical target If missed
Cutover window Full load and reconciliation in under half the window, leaving the rest for checks and fallback No time to reconcile; go/no-go made blind
Data volume Every object loaded at full production volume in at least one mock Load times in production are a surprise
Accuracy 0.00 difference on financial objects; every exclusion approved Opening balances can't be signed
Repeatability Every fix is a source change, rule or logged decision Mocks pass, production fails
Data protection Extracts and staging tables treated like production data Customer and bank data leak from test systems
Auditability Control totals, key map and signed reconciliations kept Auditors can't trace opening balances

Integration and identity. Direct transfer reads ECC over an RFC connection with a technical user; restrict that user to read access in the source and to the migration roles in the target, and remove it after go-live. Business partner roles, company code and plant values come from the configuration transported before the load. After cutover, the EDI link between CP10 and CP20 is retired, and users in Augsburg need roles restricted to CP20 and CP21 (Roles and authorization design). Anyone who can run migration objects in production can create suppliers and post open items, so that access is emergency-style: granted for the cutover, logged and removed.

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

  • Context: CP20 moves from ECC into CP10's S/4HANA system (option A). Some of its suppliers and products already exist there, some numbers collide, and the migration cockpit can't update or delete records.
  • Options: (a) direct transfer for every supported object, cleansing in ECC beforehand; (b) extract everything to staging tables and transform outside the source; (c) a mix: cleanse in ECC, direct transfer for supported objects, staging tables for extend objects and anything transformed.
  • Decision: (c). Cleanse in the source wherever possible, load by company code CP20, and keep merges, renumbering and extensions in one key map used by every object.
  • Consequences: Two load routes to test in every mock. The key map becomes a controlled artifact with an owner. Source data must be frozen before cutover so the last cleansing holds.

What a design review will ask

  1. Which objects load by direct transfer and which by staging tables, and was that checked against the object list for the target release?
  2. Who owns the key map, and how do purchase orders and open items follow merged and renumbered partners?
  3. What are the exit criteria for each mock, and who decides when they are met?
  4. How long does the load take at full volume, and how much of the window is left for reconciliation and fallback?
  5. Where do users read CP20's closed history after go-live, and for how long?

Where AI fits, and where it doesn't

Step Pattern What it does The control that keeps a person accountable
Profiling Plain automation is enough Counts, blank fields, inactive records Findings go to named owners
Duplicate detection Predict Scores likely duplicates, including fuzzy names across languages Owner approves each merge or "distinct"; decisions logged and replayed
Field and value mapping Retrieve and extract Suggests target fields for legacy headers and values (Basis-ME to base unit) Data owner confirms; mapping stored as a rule, not re-asked each mock
Explaining load errors Retrieve Groups error messages and points to the likely root cause, such as a partner that didn't load Migration lead assigns the fix
Loading data Plain automation (the cockpit) Runs the load No AI writes to the target
Reconciliation sign-off None Counts and amounts are arithmetic The business owner signs

Two rules apply throughout. Suggestions, never actions: an AI tool reads the extract and writes suggestions to a review list; it never merges, excludes or loads. And evaluate before trusting: build a labeled set of known duplicate and distinct pairs from steward decisions, and measure how many flags were right and how many real duplicates were missed, as the master data topic describes. The data readiness topic goes further on profiling SAP data for AI, and AI in S/4HANA migration projects covers AI that supports migration projects as a whole.

Production concerns

  • Security and authorizations. Extracts contain customer, supplier and bank data. Store them encrypted, delete them after go-live, and never copy them to personal drives. Give migration roles in production only for the cutover window. Bank details deserve a second check after loading, since a changed digit in a mock can reach production through a mapping rule.
  • Evaluation. Treat each mock as a test run with a report: errors by object and root cause, load times, reconciliation results. Track the error count across mocks; it should fall sharply between mock 1 and mock 2.
  • Cost. Mocks need systems or clients refreshed for each cycle, people from the business for each reconciliation, and weekends for the dress rehearsal. Budget them at the start; cutting a mock to save time usually costs the go-live date.
  • Operations. Freeze master data changes in the source before cutover, or every late change must be repeated in the target. Keep a hypercare list for the first weeks: migrated records that fail in processes go to the owner who approved them.
  • Clean core. Load through SAP's migration objects and released interfaces, never through direct table writes. Custom fields added with the object modeler should map to fields created with the system's own extensibility tools.

Pitfalls

  • Starting late. Cleansing takes months of business time. Profiling should start when the program does, not when configuration is finished.
  • Fixing in the target. A fix made by hand in a mock client is lost with the next refresh. Make it a source change, a rule or a logged decision.
  • Ignoring records that already exist. In a rollout into a live system, collisions and duplicates against the target are the first-mock surprise.
  • Matching on names alone. Similar names find candidates; tax numbers and bank accounts decide. Different legal entities with similar names must stay apart.
  • Reconciling counts only. Three missing records can carry most of the value, as Calder's stock showed.
  • No fallback. A go/no-go point without a tested way back to the old system is a formality.
  • Migrating stale documents. A purchase order due two years ago will confuse MRP in the new system. Close it in the source.

Exercise

Write Calder's migration design, the document a steering committee signs before the dress rehearsal.

  1. Run python unit1b/migration.py --mock 2 and confirm it ends with ready for the dress rehearsal.

  2. The dress rehearsal extract brings a new customer. In LEGACY["customers"], add this line after customer 10000010:

    {"key": "10000012", "name": "Fennhart Messtechnik GmbH", "country": "DE", "tax": "TX-DE-771204",
     "terms": "", "last_activity": "2027-01-28"},
  3. In LEGACY["ar_items"], add an open invoice for it:

    {"key": "95001120", "customer": "10000012", "amount": 1980.00},
  4. Run python unit1b/migration.py --mock 2 again. Confirm it fails with 2 load errors, a count difference and an AR difference of 1,980.00, and that the exit code is 1.

  5. Add a decision to DECISIONS that fills the payment terms in the source, approved by the AR lead:

    ("customers", "10000012", "fix", {"terms": "NT30"}, "source", "AR lead CP20"),
  6. Run --mock 2 once more. Confirm all four criteria pass and AR open items reconcile at 31,880.00 EUR.

  7. Create unit1b/calder_migration_design.md. Under Object list and load sequence, copy the table and diagram from unit1b/calder_migration_plan.md, and add a column Route (direct transfer or staging tables) with a note that it must be checked against the object list for the target release.

  8. Under Reconciliation checks, write one line per object: the control total, who produces it, who signs.

  9. Under Decision log, list the decisions by kind (fix, merge, extend, renumber, exclude, close) with their counts and approvers.

  10. Under Cutover, copy Calder's cutover timeline from the deep layer and mark the go/no-go and fallback deadline.

  11. Add one line on AI controls (what a duplicate or mapping suggestion may and may not do) and one line on the ECC difference (customers and vendors becoming business partners).

  12. Preview the file to check the table and diagram render, then commit it with Git.

Done when: unit1b/calder_migration_design.md has the object list with a Route column, a reconciliation line for each of the 9 objects, a decision log with 20 decisions, the cutover timeline with go/no-go and fallback marked, an AI controls line and an ECC line, and --mock 2 passes with 31,880.00 EUR receivables. Keep it: the Unit 1B capstone, One exception, traced across the enterprise, reuses it in its solution design summary.

Check yourself

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

1Why does the factory model insist that every fix become a rule or a logged decision?

Answer: C. Each mock runs on a fresh extract into a fresh client, so only fixes in the source, in rules or in the decision log repeat. A hand fix in the target vanishes, and production would fail where the mock passed.

2In mock 1, ten of the 21 errors are on purchase orders and open items. What is the most likely root cause?

Answer: B. Each failed purchase order and open item names a supplier or customer that didn't load. Fixing the seven partner errors removes most downstream errors, which is why the load sequence follows the references.

3Supplier 20000007 in CP20 has the same tax number as Ridgeline, supplier 20000003 in CP10's system. What should the migration do?

Answer: C. The cockpit creates records and doesn't update existing ones, and production records can't be deleted. Extending the existing business partner with CP20's company code and purchasing organization keeps one supplier, and the key map sends 20000007's POs and invoices to it.

4Mock 1 loaded invoice 95001109 for customer 10000003 without an error. Why is that still a problem?

Answer: A. 10000003 loaded only because its twin failed on a number collision. No load check flagged it; the duplicate suggestion did. In a real system the record can't be deleted, so the next mock starts in a fresh client.

5The open PO control total dropped from 11,421.00 to 10,601.00 EUR between mocks. What explains it?

Answer: D. Control totals come from the source as it stands after source-side cleansing. Closing the PO due in 2024 removed 820.00 EUR of open value in ECC, and the target matches the new total.

6Why does Calder load G/L balances after payables, receivables and stock?

Answer: D. It's a course design choice: once payables, receivables and stock have posted, the G/L load completes the picture, and the trial balance and the netting of offsets can be checked on everything.

7What would you do if an AI tool rated Ridgeline Bearing Supply Europe GmbH a 0.87 match with Ridgeline Bearing Supply?

Answer: D. A similar name is weak evidence; the tax numbers differ, so they may be separate legal entities. The tool suggests and a named owner decides, and the decision is logged so the next mock repeats it.

8Which control matters most for the user that runs migration objects in production?

Answer: B. Whoever runs migration objects in production can create suppliers and post open items. That is emergency-level access: time-boxed, logged and removed, as with firefighter access in the roles topic.

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