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.

Reporting and analytics architecture: what lives where in an SAP landscape

Decide where each report belongs (transactional system, data platform, group reporting or API) and design cross-system margin reporting that reconciles.

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

The 60-second version

Every SAP program promises "reporting". The hard part is deciding where each report should run.

Some reports belong inside the transactional system. A buyer's list of late purchase orders or a planner's list of shortages must show what is true right now, and it acts on single records. S/4HANA has its own analytics for that, built on the same data the processes write.

Other reports need data from several systems, years of history, or a plan to compare against. Those belong on a separate data platform that copies, cleans and combines data from the sources. SAP's offering for that today is SAP Business Data Cloud, with SAP Datasphere for modeling and SAP Analytics Cloud for dashboards and planning. Many companies also run a non-SAP data warehouse.

Two more homes are easy to miss. Legal consolidation has its own product and rules. And a "report" that an outside party checks one order at a time is really an app or API, not analytics.

This topic follows Calder Pumpworks, the fictional pump maker this unit uses. Its CFO wants margin by pump model across its US company on S/4HANA and its German company on ECC.

Why it matters to the business

The wrong home makes reports slow, stale or wrong. A shortage list built in a data warehouse that loads overnight shows yesterday's shortages. A five-year margin trend run on the transactional system competes with order entry for the same database.

Cross-system numbers rarely agree by themselves. Calder's two companies keep different currencies, different profitability models and different customer numbers for each other. Someone has to define how they combine. If nobody does, two spreadsheets give two margins, and meetings argue about the numbers instead of the business.

Copies cost money and need governing. Every copy of SAP data has to be loaded, secured, reconciled and paid for. A data platform is worth it when a question truly needs one. It is waste when the transactional system can already answer.

Contracts and SAP's rules shape the choices. How data may leave an SAP system is not only technical. As of mid-2026, SAP restricts customer and third-party use of one common extraction interface (the RFC modules of its ODP data replication API) and points customers to SAP Business Data Cloud and ODP over OData instead (SAPinsider on SAP Note 3255746). A feed into a non-SAP warehouse needs that checked before it is built.

AI depends on it. An AI assistant that answers "what was our margin on ES-300 pumps?" is only as good as the governed figure it reads. The analytics layer is often the safest place to ground it, because the numbers there are already defined, reconciled and filtered by role.

How SAP does it

As of October 2026, SAP's analytics offerings split along the same line as this topic.

Need What SAP offers Note
Operational reports on one S/4HANA system SAP S/4HANA embedded analytics: a virtual data model of core data services (CDS) views, analytical queries on top, and Fiori analytical apps SAP Analytics Cloud is the engine behind embedded stories, through a live connection (SAP Learning)
Key users building their own reports Custom CDS Views and Custom Analytical Queries apps; Manage KPIs and Reports No coding; built in development and transported (SAP Learning)
Data from many systems, history, data science SAP Business Data Cloud: foundation services that build data products, SAP Datasphere for models and access control, SAP Databricks, SAP BW private cloud edition Managed SaaS (SAP Learning)
Dashboards, planning, natural-language questions SAP Analytics Cloud: stories, planning, and Just Ask The key front end of Business Data Cloud (SAP Learning)
An existing SAP BW SAP BW/4HANA, with maintenance planned to 2040; BW 7.5 maintenance to 2030 (SAP's training page says mainstream; SAP's 2020 announcement splits it into mainstream to end 2027 and optional extended maintenance to 2030, so confirm on SAP's release strategy page) Can feed Business Data Cloud through a Data Product Generator (SAP Learning)
Legal consolidation SAP S/4HANA Finance for group reporting Eliminations and translation; see Intercompany and multi-company scenarios

A non-SAP platform is also a valid home. The Business Data Cloud topic in Unit 7 explains data products and how BDC Connect shares them with partner platforms such as Databricks and Snowflake without another copy.

Calder's reporting needs, placed

Calder's design workshop listed eight reporting needs. Each is placed by a few questions: how many systems, how fresh, how much history, and who reads it.

flowchart LR
  S4[(S/4HANA CP10)] --> EA[Embedded analytics]
  ECC[(ECC CP20)] --> ER[ECC reports]
  S4 --> EL[(Enterprise<br/>data layer)]
  ECC --> EL
  MES[(Shop-floor<br/>system)] --> EL
  EL --> BI[Dashboards<br/>and planning]
  S4 --> GR[Group reporting]
  ECC -->|upload| GR
  S4 --> API[API for<br/>distributors]
ID Need Where it lives Why
RP01 Blocked sales orders worklist Embedded analytics, CP10 One system, must be live, users act on each order
RP02 MRP exceptions for plant CP11 Embedded analytics, CP10 Same
RP03 Augsburg's open purchase orders ECC report, until CP20 joins S/4HANA One system; rebuilding it elsewhere is wasted after July 2027
RP04 Group margin by pump model Enterprise data layer Two systems, two currencies, three years of history
RP05 Plan vs actual, with next year's plan Enterprise data layer and planning Needs plan entry next to actuals from both companies
RP06 Test bench first-pass yield Enterprise data layer Combines the shop-floor system with S/4HANA
RP07 Order status for distributors API or app (not reporting) One record at a time, for outside users
RP08 Consolidated statements Group reporting Legal rules, eliminations, audit trail

RP04 shows why history matters. Under Calder's migration plan, only open items move from ECC; closed history stays behind (Data migration). After July 2027, a three-year margin trend needs a layer that holds both the old ECC figures and the new S/4HANA ones.

Questions to ask

  1. For each report: which systems feed it, how fresh must it be, and how much history does it need?
  2. Which reports are worklists that users act on? Those usually belong in the transactional system.
  3. Who defines each shared measure, such as "margin", and where is the definition written?
  4. How does each source's data reach the data platform, and is that interface allowed under our SAP contract today?
  5. How is each report reconciled to the source, and who signs it off?
  6. Who may see which company's or region's figures, and is that enforced in every layer?
  7. If an AI assistant answers questions on this data, which governed figures does it read, and what does it refuse?

Common misconceptions

  • "Put everything in the data warehouse." Worklists need live data and links back to the transaction. A nightly copy shows yesterday's problems.
  • "Embedded analytics can do group reporting." It reports on one system. Combining companies on two systems, with currency translation and eliminations, needs another layer.
  • "Business Data Cloud is just Datasphere renamed." Datasphere is one component. Business Data Cloud also includes SAP Analytics Cloud, SAP Databricks, SAP BW private cloud edition and SAP-managed data products.
  • "Any tool can pull SAP data the same way." Some extraction interfaces are now restricted for non-SAP use. Check before you build.
  • "If the totals look right, the report is right." Reconcile to control totals from each source, and check that every source line landed in a bucket.
  • "AI makes the semantic layer unnecessary." AI that reads raw tables guesses what "margin" means. A governed model tells it.

Key terms

All course terms, A to Z, in the glossary

  • Operational reporting: reports on current business, read and acted on in the transactional system.
  • Embedded analytics: S/4HANA's built-in analytics on its own live data, through CDS views and analytical queries.
  • CDS view: a core data services view, SAP's way to define a data model on top of database tables.
  • Analytical query: a CDS view that defines a report's measures, filters and layout.
  • Data product: a packaged, described dataset, such as S/4HANA sales orders, ready for analytics and AI.
  • Enterprise data layer: a separate platform that combines data from many systems with history.
  • Control total: a count or amount from the source that a report must match.
  • Semantic layer: the agreed definitions of measures and dimensions that every report and AI tool uses.

Check yourself

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

1Rockford's planners need a list of today's MRP exceptions to act on. Where should it live?

Answer: D. Planners act on each exception now, in the system that holds it. A nightly copy shows yesterday's exceptions, and group reporting is for legal consolidation.

2Why does Calder's "group margin by pump model" need a separate data layer?

Answer: A. The report joins Rockford's S/4HANA margin with Augsburg's ECC figures, translates currency, and keeps history from before the move. Embedded analytics reports on one system's live data.

3A distributor wants to check the status of its orders online. What is the right design?

Answer: A. Looking up one order at a time, for an outside party, is an operational access need, not analytics. An app or API can return live status and enforce that each distributor sees only its own orders.

4Your team plans to feed a non-SAP data warehouse straight from ECC. What should you check first?

Answer: A. As of 2026, SAP restricts customer and third-party use of the RFC modules of its ODP replication API and points to Business Data Cloud and ODP over OData. Checking that early avoids building a feed that later stops.

5October's margin file counted Augsburg's spare-parts sales as pump margin. Which control would have caught it?

Answer: D. Reconciliation checks that each source line lands in a defined bucket and that totals match the source. A wrong mapping shows up there; a reader looking at a dashboard rarely spots it.

6Why is the analytics layer often a safe place to ground an AI assistant on SAP data?

Answer: A. An assistant that reads governed measures answers with an agreed definition and the reader's own access rights. Reading raw tables, it has to guess what "margin" means. The layer is usually not live, which is a limit to state.
Deep layer · 40 min read

Mental model

A reporting landscape is a set of homes, and every report needs exactly one.

Each home trades something. The transactional system gives live data and one click back to the document, but it sees only itself and is busy running the business. A data platform sees every system and keeps history, but it holds copies that are minutes to a day old and must be reconciled. Group reporting applies legal consolidation rules that no dashboard should reinvent. An API serves one record at a time to whoever needs it.

So the design work is not choosing a tool. It is writing down, for every report, four facts (systems, freshness, history, readers) and letting those facts pick the home. Then, for the reports that combine systems, define the measures once, in one place, and prove every figure back to its source.

Three consequences follow:

  1. Placement is a rule, not a taste. Write the rule down so the next report is placed the same way.
  2. A cross-system measure is a design object. "Group margin" needs a definition, a currency rule, a mapping of every source line and a reconciliation. Without them, two teams compute two margins.
  3. Copies are liabilities. Each one needs a load, a security model, a reconciliation and an owner. Make one only when a question needs it.

How it works

The four homes

Home Data Freshness Strength Limit
Transactional system (S/4HANA embedded analytics) One system's own tables, through CDS views Live Act on the record; no copy to govern One system; heavy history queries compete with transactions
Enterprise data layer (Business Data Cloud, Datasphere, BW, or a non-SAP warehouse) Copies from many systems, with history Minutes to daily Cross-system, history, planning, data science Copies to load, secure, reconcile and pay for
Group reporting Company financials, uploaded or released Period close Legal consolidation, eliminations, audit Financial statements only, not operational detail
API or app One system, one record Live Outside users, one record at a time Not for analysis across many records

Embedded analytics in S/4HANA

SAP's training describes three layers: the SAP HANA database holds the data, a virtual data model (VDM) of CDS views sits in the middle, and Fiori analytical apps sit on top. CDS views are the VDM's building blocks, and customers can copy or extend the delivered ones or build their own (SAP Learning). The SAP data model topic in Unit 1 covers CDS views and the VDM in more depth.

An analytical query is a special kind of CDS view. It selects fields, sets filters, and adds calculations and hierarchies, and it can be consumed by SAP Analytics Cloud or SAP BusinessObjects as well as Fiori apps (SAP Learning). On top of the queries, SAP's training lists the report types a key user can create with Manage KPIs and Reports: KPIs, generic drilldown reports, analytical list pages, Data Analyzer reports, multidimensional reports, review booklets and SAP Analytics Cloud stories (SAP Learning).

flowchart BT
  DB[(SAP HANA tables)] --> V[CDS views<br/>virtual data model]
  V --> Q[Analytical queries]
  Q --> A[Fiori analytical apps,<br/>KPIs, stories]
  A -->|navigate| T[The business document]

Two tool tracks build these objects. Key users use View Browser, Custom CDS Views, Query Browser and Custom Analytical Queries (SAP Learning). Developers on on-premise systems use ABAP Development Tools in Eclipse (SAP Learning). Either way, custom objects are built in the development system and transported, like any other change (SAP Learning).

What embedded analytics is good at: live worklists and KPIs on one system, where the user acts on what they see. What it is not built for: joining another company's ECC data, keeping history from a retired system, or entering a plan.

Getting SAP data out for a data platform

A data platform needs a supported way to copy data out. For S/4HANA, the strategic route is CDS-based extraction. A CDS view annotated @Analytics.dataExtraction.enabled becomes available to the operational data provisioning (ODP) framework, in the context the post names ODP_CDS (SAP's BW training writes ABAP_CDS), with delta based on a timestamp or on change data capture (SAP Community). The same post lists SAP Analytics Cloud, SAP BW/4HANA, SAP BW, SAP Data Intelligence and SAP Datasphere as consumers, and draws an edition line: in S/4HANA Cloud Public Edition only views SAP has enabled for extraction can be used, while on-premise systems allow custom extraction views and should restrict the connection to C1-released views.

For ECC, the older route is the classic extractors: function-module-based DataSources on the service API (S-API), which CDS extraction replaces in S/4HANA (SAP Learning). Table replication is the other route: an SAP blog shows a Datasphere replication flow reading ECC tables through SAP Landscape Transformation Replication Server (SLT), with an on-premise Cloud Connector and matching DMIS add-on versions on both sides (SAP Community). Raw tables carry no business meaning, so the model has to rebuild it.

Which interface a non-SAP tool may use is a contract question too. SAP Note 3255746 restricts the use of the RFC modules of the ODP data replication API by customer or third-party applications. Per SAPinsider's July 2026 summary, version 11 of the note appeared on 21 April 2026, a technical block was planned for June 2026 with a temporary opt-out to the end of 2026, and SAP points to SAP Business Data Cloud and ODP over OData for non-SAP targets (SAPinsider). Read the note's current version on the SAP Support Portal before you design a feed; the details have changed several times.

SAP Business Data Cloud

SAP's training describes Business Data Cloud as a set of components (SAP Learning):

Component Role, as SAP describes it
Foundation services Replicate data from SAP applications, harmonize it and turn it into data products
SAP Datasphere Models on top of data products; analytical roles and data access control
SAP Databricks Data engineering, AI and machine learning on SAP data
SAP BW, private cloud edition Brings existing BW data in, so it can generate data products
SAP Analytics Cloud Stories, planning, ad-hoc analysis, and AI-driven chat and suggestions
BDC cockpit Browse and install data products and intelligent applications

SAP's BW/4HANA training calls Datasphere SAP's enterprise data fabric for public cloud and the central modeling component of Business Data Cloud. It says Business Data Cloud can technically run without Datasphere, but advises against it (SAP Learning). For customers with BW, the same lesson describes a Data Product Generator that turns BW InfoProviders into data products, and gives BW 7.5 maintenance to 2030 and BW/4HANA planned to 2040. Check the terms (mainstream or extended, and the dates) on SAP's release strategy page before you plan a BW retirement.

Defining a cross-system measure

Calder's group margin by pump model shows what "combine two systems" really involves. The pieces:

Question Calder's answer (course design)
What is margin in CP10? Billed revenue minus cost of goods sold at standard, production variances and freight, from margin analysis in S/4HANA
What does CP20 contribute to a pump? The impeller. Rockford's pump cost includes it at the transfer price; Augsburg's own margin on that impeller is the group's extra margin
How is CP20's figure attributed to a model? Through the bill of materials: one CP-IMP200 per ES-200, one CP-IMP300 per ES-300, times the pumps issued
Which currency and rate? USD, at the October average rate 1.0820, the same rate consolidation uses for income
What about lines that aren't pump components? Every CP20 line maps to exactly one bucket; spare parts sold outside the group are reported separately; an unmapped line fails the check
What is left to consolidation? Profit still in Rockford's stock (impellers not yet in a sold pump), which group reporting eliminates

Two system differences sit under that table. S/4HANA's margin analysis, which SAP's training says replaces the account-based profitability analysis of SAP ERP, groups revenue and cost by G/L account in universal journal storage. Costing-based profitability analysis keeps value fields in its own storage and isn't available in the cloud editions (SAP Learning). Calder's ECC uses costing-based (a course assumption), so CP20's figures arrive in a different shape and must be mapped, not just appended.

The attribution uses this month's impeller margin for the impellers in this month's pumps. Some of those impellers were bought last month at last month's rate. That is a simplification the controllers accepted and wrote down. A definition that nobody writes down is the one that causes the next argument.

Reconciliation

A cross-system figure is trusted only if it traces back. Calder's checks, as course rules:

  1. CP10 revenue in the model equals the S/4HANA control total for the period.
  2. CP20 revenue in the extract equals the ECC control total, in EUR, before translation.
  3. Every CP20 line maps to exactly one bucket. An unmapped line is a failure, never a silent drop.
  4. Every pump model's component has CP20 data for the period, or the gap is stated.
  5. The rate is the one the group uses for consolidation, so the two reports can be compared.

The record-to-report topic explains CP10's October margin swings; this model starts from the same figures.

Where AI fits

The analytics layer is often the safest place to ground an AI assistant on SAP data, for three reasons:

  • Defined measures. "Group margin" has one definition, so the answer doesn't depend on how the model guesses a join.
  • Reconciled figures. The assistant can say whether the data passed its checks, and warn when it didn't.
  • Access already designed. In Business Data Cloud, Datasphere carries analytical roles and data access control (SAP Learning). SAP Analytics Cloud's Just Ask answers natural-language questions on models (SAP Learning).

Its limits matter as much:

  • Not live. A data layer loaded nightly can't answer "is order 1000002 still blocked?" That question belongs to the transactional system or an API.
  • Only what is modeled. If margin by customer isn't in the model, the right answer is "not available", not a guess from raw tables.
  • Not an action channel. Reading the analytics layer is safe; changing SAP data goes through released APIs with approval, as Grounding on SAP data with authorizations explains.

The natural-language queries topic in Unit 7 builds the text-to-query step; this topic's script shows the guardrail around it: answer from governed metrics, or decline with a reason.

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 reporting architect's four jobs for Calder. It places each of Calder's eight reporting needs in a home, by rule. It builds group margin by pump model from two made-up extracts, one from S/4HANA and one from ECC. It reconciles that model to the sources. And it answers questions only from the governed figures, declining the rest.

flowchart LR
  P[calder_pack/<br/>company codes,<br/>products] --> S[reporting.py]
  X[CP10 and CP20<br/>extracts in the script] --> S
  S --> L[Placement]
  S --> M[Group margin]
  S --> C[Reconciliation]
  S --> A[Governed answers]
  S --> W[calder_reporting.md]

The script connects to no SAP system. It imitates the design decisions and checks 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 35 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 org_units.json to confirm that CP10 runs on S/4HANA and CP20 on ECC, and products.json for the pumps' standard costs (2,850.00 USD for the ES-200, 4,100.00 USD for the ES-300).

Step 3: Create the script

  1. In VS Code, create a new file, paste the script below, and save it as reporting.py inside the unit1b folder.
"""Place Calder Pumpworks' reporting needs in the right layer, build group margin
by pump model across CP10 (S/4HANA) and CP20 (ECC), reconcile it, and answer
questions only from the governed figures.

Calder Pumpworks is a fictional company. Every requirement, record, amount, rate
and rule below is a course label or workshop input, not an SAP standard value or
a real project figure. Column names in the two extracts are course labels, not
SAP field names. 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/reporting.py                 where each reporting need should live
  python unit1b/reporting.py --margin        group margin by pump model, October 2026
  python unit1b/reporting.py --check         reconcile the margin model to the sources
  python unit1b/reporting.py --ask "..."     answer a question from governed metrics only
  python unit1b/reporting.py --write         write unit1b/calder_reporting.md
"""
import argparse
import json
import sys
from pathlib import Path

PACK = Path("unit1b") / "calder_pack"
OUT = Path("unit1b") / "calder_reporting.md"

# ------------------------------------------------------- reporting needs (fictional)
# systems: where the data comes from. latency: how fresh it must be.
# history: months of history the users need. Values are workshop inputs.
NEEDS = [
    {"id": "RP01", "name": "Blocked sales orders worklist", "users": "Order-to-cash team, Rockford",
     "systems": {"S4"}, "latency": "seconds", "history": 0},
    {"id": "RP02", "name": "MRP exceptions for plant CP11", "users": "Planners, Rockford",
     "systems": {"S4"}, "latency": "seconds", "history": 0},
    {"id": "RP03", "name": "Open purchase orders by supplier, Augsburg", "users": "Buyers, Augsburg",
     "systems": {"ECC"}, "latency": "seconds", "history": 0},
    {"id": "RP04", "name": "Group margin by pump model", "users": "CFO, controllers, product managers",
     "systems": {"S4", "ECC"}, "latency": "monthly", "history": 36},
    {"id": "RP05", "name": "Plan vs actual by pump model, with next year's plan", "users": "Controllers, sales leads",
     "systems": {"S4", "ECC"}, "latency": "monthly", "history": 24, "planning": True},
    {"id": "RP06", "name": "Test bench first-pass yield", "users": "Plant manager, quality",
     "systems": {"S4", "MES"}, "latency": "daily", "history": 12},
    {"id": "RP07", "name": "Order status for distributors such as Hallam", "users": "Distributors (outside Calder)",
     "systems": {"S4"}, "latency": "seconds", "history": 0, "single_record": True},
    {"id": "RP08", "name": "Consolidated financial statements", "users": "Group accounting, auditors",
     "systems": {"S4", "ECC"}, "latency": "monthly", "history": 24, "statutory": True},
]

LAYERS = {
    "group": "Group reporting (consolidation)",
    "api": "Not reporting: an API or app",
    "enterprise": "Enterprise layer (data platform)",
    "embedded": "Embedded analytics in S/4HANA (CP10)",
    "ecc": "Report in ECC until CP20 joins S/4HANA",
}


def place(need: dict) -> tuple:
    """Return (layer key, reason). Rules are checked in order; the first match wins."""
    if need.get("statutory"):
        return "group", "legal consolidation with eliminations and audit trail"
    if need.get("single_record"):
        return "api", "one record at a time for an outside user, not analysis"
    if len(need["systems"]) > 1:
        return "enterprise", "combines " + " + ".join(sorted(need["systems"]))
    if need.get("planning"):
        return "enterprise", "needs plan data entry next to actuals"
    if need["history"] > 24:
        return "enterprise", f"needs {need['history']} months of history"
    if need["systems"] == {"S4"}:
        return "embedded", f"one system, {need['latency']} fresh, on live data"
    if need["systems"] == {"ECC"}:
        return "ecc", "one system, on live data; ECC has no S/4HANA analytics model"
    return "enterprise", "source outside SAP"


# --------------------------------------------- margin model inputs (fictional)
RATE_AVERAGE = 1.0820   # USD per EUR, October 2026 average (course rate)

# CP10, from S/4HANA margin analysis, October 2026, USD. Same values as the
# record-to-report topic: units billed and issued, variances and freight.
CP10_MARGIN = {
    "CP-ES200": {"billed": 16, "issued": 17, "variance": 388.00, "freight": 2100.00},
    "CP-ES300": {"billed": 5, "issued": 5, "variance": 4280.00, "freight": 1320.00},
}
PRICE = {"CP-ES200": 4200.00, "CP-ES300": 6150.00}      # net price per pump, USD
CP10_CONTROL_REVENUE = 97950.00                         # S/4HANA control total, pumps, USD

# The impeller each pump uses, and the price CP10 pays CP20 for it (in the
# pump's standard cost). Course values.
BOM_IMPELLER = {"CP-ES200": ("CP-IMP200", 335.00), "CP-ES300": ("CP-IMP300", 455.00)}

# CP20, from ECC profitability analysis, October 2026, EUR. Revenue is booked at
# each invoice's own rate. Column names are course labels.
CP20_LINES = [
    {"customer": "10000010", "material": "CP-IMP200", "qty": 80, "revenue_eur": 24769.14, "cogs_eur": 24800.00},
    {"customer": "10000010", "material": "CP-IMP300", "qty": 6, "revenue_eur": 2523.10, "cogs_eur": 2370.00},
    {"customer": "10000004", "material": "CP-IMP200", "qty": 12, "revenue_eur": 4920.00, "cogs_eur": 3720.00},
    {"customer": "10000008", "material": "CP-CAST300", "qty": 2, "revenue_eur": 1180.00, "cogs_eur": 960.00},
]
CP20_CONTROL_REVENUE = 33392.24                         # ECC control total, all lines, EUR
INTERCOMPANY_CUSTOMER = "10000010"                      # CP10 as a customer of CP20

# Which bucket each CP20 line belongs to in the group view. A line with no
# mapping is an error the check reports, never a silent drop.
MAPPING = {
    ("10000010", "CP-IMP200"): "component for ES-200",
    ("10000010", "CP-IMP300"): "component for ES-300",
    ("10000004", "CP-IMP200"): "CP20 spare parts",
}

METRICS = {
    "group margin": "CP10 margin plus CP20's margin on the impellers in the pumps issued, in USD at the October average rate",
    "cp10 margin": "Billed revenue minus cost of goods sold at standard, production variances and freight, CP10 only",
}


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


def load_pack() -> dict:
    try:
        org = json.loads((PACK / "org_units.json").read_text(encoding="utf-8"))
        products = json.loads((PACK / "products.json").read_text(encoding="utf-8"))
    except FileNotFoundError:
        sys.exit(f"Cannot read {PACK}. Run: python unit1b/make_calder_pack.py (Unit 1B setup).")
    systems = {c["CompanyCode"]: c["System"] for c in org["CompanyCodes"]}
    if systems != {"CP10": "S/4HANA", "CP20": "ECC"}:
        sys.exit("The pack must hold CP10 on S/4HANA and CP20 on ECC. Run make_calder_pack.py again.")
    std = {p["Product"]: p["to_Valuation"][0]["StandardPrice"] for p in products["Products"]}
    for code in ("CP-ES200", "CP-ES300"):
        if code not in std:
            sys.exit(f"{code} is missing from products.json. Run make_calder_pack.py again.")
    return std


def cp10_rows(std: dict) -> dict:
    rows = {}
    for model, m in CP10_MARGIN.items():
        revenue = m["billed"] * PRICE[model]
        cogs = m["issued"] * std[model]
        margin = revenue - cogs - m["variance"] - m["freight"]
        rows[model] = {"revenue": revenue, "cogs": cogs, "variance": m["variance"],
                       "freight": m["freight"], "margin": margin}
    return rows


def cp20_unit_margin_usd(material: str) -> float:
    """CP20's margin per impeller sold to CP10 this month, translated to USD."""
    lines = [l for l in CP20_LINES if l["customer"] == INTERCOMPANY_CUSTOMER and l["material"] == material]
    qty = sum(l["qty"] for l in lines)
    if qty == 0:
        return 0.0
    margin_eur = sum(l["revenue_eur"] - l["cogs_eur"] for l in lines) / qty
    return margin_eur * RATE_AVERAGE


def group_rows(std: dict) -> dict:
    rows = cp10_rows(std)
    for model, row in rows.items():
        material, _ = BOM_IMPELLER[model]
        unit = cp20_unit_margin_usd(material)
        row["impellers"] = CP10_MARGIN[model]["issued"]
        row["cp20_unit"] = unit
        row["cp20_margin"] = round(unit * row["impellers"], 2)
        row["group_margin"] = row["margin"] + row["cp20_margin"]
    return rows


def checks(std: dict) -> list:
    """Return a list of (ok, text). Every figure in the report must trace to a source."""
    results = []
    rev = sum(r["revenue"] for r in cp10_rows(std).values())
    results.append((abs(rev - CP10_CONTROL_REVENUE) < 0.005,
                    f"CP10 revenue in the model {money(rev)} USD = S/4HANA control total {money(CP10_CONTROL_REVENUE)}"))
    rev20 = sum(l["revenue_eur"] for l in CP20_LINES)
    results.append((abs(rev20 - CP20_CONTROL_REVENUE) < 0.005,
                    f"CP20 revenue in the extract {money(rev20)} EUR = ECC control total {money(CP20_CONTROL_REVENUE)}"))
    for l in CP20_LINES:
        if (l["customer"], l["material"]) not in MAPPING:
            results.append((False, f"CP20 line customer {l['customer']}, {l['material']}, "
                                   f"{money(l['revenue_eur'])} EUR has no mapping: add it to MAPPING"))
    for model, (material, _) in BOM_IMPELLER.items():
        sold = any(l["customer"] == INTERCOMPANY_CUSTOMER and l["material"] == material for l in CP20_LINES)
        results.append((sold, f"{model}: impeller {material} has CP20 sales to CP10 this month"))
    results.append((True, f"Rate: October average {RATE_AVERAGE:.4f} USD per EUR, the same rate consolidation uses"))
    return results


# ---------------------------------------------------------------- commands
def print_layers() -> None:
    print("Where each reporting need should live (Calder Pumpworks, fictional)\n")
    for n in NEEDS:
        layer, reason = place(n)
        print(f"  {n['id']}  {n['name']}")
        print(f"        -> {LAYERS[layer]}: {reason}")
    counts = {}
    for n in NEEDS:
        key = place(n)[0]
        counts[key] = counts.get(key, 0) + 1
    print("\n  " + ", ".join(f"{LAYERS[k]}: {v}" for k, v in counts.items()))


def print_margin(std: dict) -> None:
    rows = group_rows(std)
    print("Margin by pump model, October 2026, USD (fictional)\n")
    print(f"  {'Model':<9}{'Revenue':>11}{'CP10 margin':>13}{'%':>7}{'CP20 on impellers':>19}{'Group margin':>14}{'%':>7}")
    for model, r in rows.items():
        print(f"  {model:<9}{money(r['revenue']):>11}{money(r['margin']):>13}{r['margin'] / r['revenue']:>7.1%}"
              f"{money(r['cp20_margin']):>19}{money(r['group_margin']):>14}{r['group_margin'] / r['revenue']:>7.1%}")
    print()
    for model, r in rows.items():
        material, price = BOM_IMPELLER[model]
        print(f"  {model}: {r['impellers']} impellers {material}, CP20 margin {money(r['cp20_unit'])} USD each "
              f"(CP10 pays {money(price)} USD)")
    spare = [l for l in CP20_LINES if MAPPING.get((l["customer"], l["material"])) == "CP20 spare parts"]
    if spare:
        print(f"  CP20 spare parts sold outside the group: {money(sum(l['revenue_eur'] for l in spare))} EUR revenue, "
              "reported separately, not in pump margin")


def print_check(std: dict) -> int:
    print("Reconciliation of the margin model to its sources\n")
    results = checks(std)
    for ok, text in results:
        print(f"  {'OK  ' if ok else 'FAIL'}  {text}")
    failed = sum(1 for ok, _ in results if not ok)
    print(f"\n  {failed} issue(s). " + ("The report may be published." if not failed
                                        else "Fix them before anyone reads the figures."))
    return 1 if failed else 0


def answer(question: str, std: dict) -> str:
    """Answer only from the governed metrics. Anything else is declined, with a reason."""
    q = question.lower()
    squeezed = q.replace("-", "").replace(" ", "")
    models = [m for m in CP10_MARGIN if m.lower().replace("cp-", "") in squeezed]
    out_of_scope = [("customer", "margin by customer isn't in the governed model yet"),
                    ("forecast", "the model holds actuals only; forecasts belong to planning (RP05)"),
                    ("next month", "the model holds actuals only; forecasts belong to planning (RP05)"),
                    ("2025", "only October 2026 is loaded in this course model")]
    for word, reason in out_of_scope:
        if word in q:
            return f"Not answered: {reason}."
    metric = "cp10 margin" if ("cp10" in q or "rockford" in q) else "group margin" if "margin" in q else None
    if metric is None or not models:
        return ("Not answered: ask for 'group margin' or 'CP10 margin' and name a pump model "
                "(ES-200 or ES-300). Those are the governed metrics.")
    failed = [t for ok, t in checks(std) if not ok]
    rows = group_rows(std)
    lines = []
    for m in models:
        r = rows[m]
        value = r["group_margin"] if metric == "group margin" else r["margin"]
        lines.append(f"{m} {metric}, October 2026: {money(value)} USD ({value / r['revenue']:.1%} of revenue).")
    lines.append(f"Definition: {METRICS[metric]}.")
    lines.append("Source: CP10 margin analysis (S/4HANA) and CP20 profitability extract (ECC), course data.")
    if failed:
        lines.append(f"Warning: the model has {len(failed)} open reconciliation issue(s); run --check.")
    return "\n".join(lines)


def write_report(std: dict) -> None:
    rows = group_rows(std)
    failed = [t for ok, t in checks(std) if not ok]
    md = ["# Calder Pumpworks reporting landscape (fictional)", "",
          "```mermaid", "flowchart LR",
          "  S4[(S/4HANA CP10)] --> EA[Embedded analytics<br/>RP01 RP02]",
          "  ECC[(ECC CP20)] --> ER[ECC reports<br/>RP03]",
          "  S4 --> EL[(Enterprise layer)]",
          "  ECC --> EL",
          "  MES[(MES)] --> EL",
          "  EL --> BI[Dashboards and planning<br/>RP04 RP05 RP06]",
          "  S4 --> GR[Group reporting<br/>RP08]",
          "  ECC -->|upload| GR",
          "  S4 --> API[API or app<br/>RP07]",
          "```", "", "## Placement", "",
          "| ID | Need | Layer | Reason |", "| --- | --- | --- | --- |"]
    for n in NEEDS:
        layer, reason = place(n)
        md.append(f"| {n['id']} | {n['name']} | {LAYERS[layer]} | {reason} |")
    md += ["", "## Group margin by pump model, October 2026, USD (synthetic)", "",
           "| Model | Revenue | CP10 margin | CP20 on impellers | Group margin |", "| --- | --- | --- | --- | --- |"]
    for m, r in rows.items():
        md.append(f"| {m} | {money(r['revenue'])} | {money(r['margin'])} | {money(r['cp20_margin'])} | {money(r['group_margin'])} |")
    md += ["", f"Reconciliation: {len(failed)} open issue(s).", "",
           "## Decision record", "",
           "- **Context:** fill in", "- **Options:** fill in", "- **Decision:** fill in",
           "- **Consequences:** fill in", "", "All data is fictional."]
    OUT.write_text("\n".join(md) + "\n", encoding="utf-8")
    print(f"Wrote {OUT}. Open it and fill in the decision record.")


def main() -> None:
    parser = argparse.ArgumentParser(description="Calder reporting landscape (fictional data)")
    parser.add_argument("--margin", action="store_true", help="group margin by pump model")
    parser.add_argument("--check", action="store_true", help="reconcile the margin model to its sources")
    parser.add_argument("--ask", metavar="QUESTION", help="answer from governed metrics only")
    parser.add_argument("--write", action="store_true", help="write unit1b/calder_reporting.md")
    args = parser.parse_args()
    std = load_pack()
    if args.margin:
        print_margin(std)
    elif args.check:
        sys.exit(print_check(std))
    elif args.ask:
        print(answer(args.ask, std))
    elif args.write:
        write_report(std)
    else:
        print_layers()


if __name__ == "__main__":
    main()
  1. Read the blocks NEEDS, CP10_MARGIN, BOM_IMPELLER, CP20_LINES and MAPPING first. They are Calder's reporting workshop and October data written as Python. Every value is invented. CP10's October figures are the same as in the record-to-report topic; the impeller price 335.00 USD and the 80 impellers invoiced in October come from the intercompany topic. The CP-IMP300 price and cost, the CP20 spare-parts lines, the control totals and the needs RP01 to RP08 are this topic's course values.
Part of the script What it does
NEEDS Calder's eight reporting needs with systems, freshness and history
place() The placement rule: checked in order, the first match wins
CP10_MARGIN, PRICE CP10's October margin inputs from S/4HANA, in USD
BOM_IMPELLER Which impeller each pump uses, and what CP10 pays for it
CP20_LINES CP20's October profitability lines from ECC, in EUR
MAPPING Which bucket each CP20 line belongs to
group_rows() CP10 margin plus CP20's margin on the impellers in the pumps issued
checks() The five reconciliation checks
answer() Answers from the governed metrics, or declines with a reason
write_report() Writes the landscape diagram, placement, margin and a decision record template

Step 4: Place the reporting needs

  1. Run the script with no option:

    • Windows (PowerShell):

      python unit1b\reporting.py
    • macOS / Linux:

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

Where each reporting need should live (Calder Pumpworks, fictional)

  RP01  Blocked sales orders worklist
        -> Embedded analytics in S/4HANA (CP10): one system, seconds fresh, on live data
  RP02  MRP exceptions for plant CP11
        -> Embedded analytics in S/4HANA (CP10): one system, seconds fresh, on live data
  RP03  Open purchase orders by supplier, Augsburg
        -> Report in ECC until CP20 joins S/4HANA: one system, on live data; ECC has no S/4HANA analytics model
  RP04  Group margin by pump model
        -> Enterprise layer (data platform): combines ECC + S4
  RP05  Plan vs actual by pump model, with next year's plan
        -> Enterprise layer (data platform): combines ECC + S4
  RP06  Test bench first-pass yield
        -> Enterprise layer (data platform): combines MES + S4
  RP07  Order status for distributors such as Hallam
        -> Not reporting: an API or app: one record at a time for an outside user, not analysis
  RP08  Consolidated financial statements
        -> Group reporting (consolidation): legal consolidation with eliminations and audit trail

  Embedded analytics in S/4HANA (CP10): 2, Report in ECC until CP20 joins S/4HANA: 1, Enterprise layer (data platform): 3, Not reporting: an API or app: 1, Group reporting (consolidation): 1

Read the rule in place() from the top. Legal consolidation and one-record lookups are caught first, because they are never "analytics" whatever their data looks like. Next, anything combining systems goes to the enterprise layer. Only then does a single-system need stay home.

Step 5: Build group margin by pump model

  1. Run:

    • Windows (PowerShell):

      python unit1b\reporting.py --margin
    • macOS / Linux:

      python unit1b/reporting.py --margin
  2. You should see:

Margin by pump model, October 2026, USD (fictional)

  Model        Revenue  CP10 margin      %  CP20 on impellers  Group margin      %
  CP-ES200   67,200.00    16,262.00  24.2%              -7.10     16,254.90  24.2%
  CP-ES300   30,750.00     4,650.00  15.1%             138.05      4,788.05  15.6%

  CP-ES200: 17 impellers CP-IMP200, CP20 margin -0.42 USD each (CP10 pays 335.00 USD)
  CP-ES300: 5 impellers CP-IMP300, CP20 margin 27.61 USD each (CP10 pays 455.00 USD)
  CP20 spare parts sold outside the group: 4,920.00 EUR revenue, reported separately, not in pump margin

The ES-200 line shows the group losing 0.42 USD on each impeller. That is Augsburg selling at 335.00 USD an impeller that costs it 310.00 EUR: at 1.0820, it earns nothing. The intercompany topic traced the same transfer price problem from the finance side. The ES-300 line shows the opposite: Augsburg earns 27.61 USD on each CP-IMP300, so the group margin is higher than Rockford's own. Neither number is visible in either system alone.

Step 6: Reconcile the model to its sources

  1. Run:

    • Windows (PowerShell):

      python unit1b\reporting.py --check
    • macOS / Linux:

      python unit1b/reporting.py --check
  2. You should see:

Reconciliation of the margin model to its sources

  OK    CP10 revenue in the model 97,950.00 USD = S/4HANA control total 97,950.00
  OK    CP20 revenue in the extract 33,392.24 EUR = ECC control total 33,392.24
  FAIL  CP20 line customer 10000008, CP-CAST300, 1,180.00 EUR has no mapping: add it to MAPPING
  OK    CP-ES200: impeller CP-IMP200 has CP20 sales to CP10 this month
  OK    CP-ES300: impeller CP-IMP300 has CP20 sales to CP10 this month
  OK    Rate: October average 1.0820 USD per EUR, the same rate consolidation uses

  1 issue(s). Fix them before anyone reads the figures.

The exit code is 1. One CP20 line, two castings CP-CAST300 sold to a German customer, has no bucket. The control totals still match, which is the point: totals alone wouldn't have caught it. You fix it in the exercise.

Step 7: Ask questions of the governed figures

  1. Ask for a governed metric:

    • Windows (PowerShell):

      python unit1b\reporting.py --ask "What is the group margin of the ES-200 in October?"
    • macOS / Linux:

      python unit1b/reporting.py --ask "What is the group margin of the ES-200 in October?"
  2. You should see:

CP-ES200 group margin, October 2026: 16,254.90 USD (24.2% of revenue).
Definition: CP10 margin plus CP20's margin on the impellers in the pumps issued, in USD at the October average rate.
Source: CP10 margin analysis (S/4HANA) and CP20 profitability extract (ECC), course data.
Warning: the model has 1 open reconciliation issue(s); run --check.
  1. Now ask for something the model doesn't hold:

    • Windows (PowerShell):

      python unit1b\reporting.py --ask "Which customer buys the most ES-300 pumps?"
    • macOS / Linux:

      python unit1b/reporting.py --ask "Which customer buys the most ES-300 pumps?"
  2. You should see:

Not answered: margin by customer isn't in the governed model yet.

The answer carries its definition, its source and the reconciliation status. The decline names why. answer() uses keyword rules so the script runs without an account; in a real assistant a language model turns the question into a request, and code like this decides whether the request is inside the governed model. The script has one local user, so it doesn't check who is asking. A real assistant must also check the asker's own authorizations (in Business Data Cloud, Datasphere's analytical roles and data access control) before it reads a figure, and run the question under that user, never a broad technical user.

Step 8: Write the landscape file and save your work

  1. Run:

    • Windows (PowerShell):

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

      python unit1b/reporting.py --write
  2. You should see Wrote unit1b\calder_reporting.md. Open it and fill in the decision record. on Windows, or the same with / on macOS and Linux.

  3. Open unit1b/calder_reporting.md in VS Code and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS) to see the diagram and tables.

  4. Save your work with Git:

git add unit1b/reporting.py unit1b/calder_reporting.md
git commit -m "Unit 1B: Calder reporting landscape, group margin and reconciliation"

If something goes wrong

What you see What it means What to do
python: command not found or 'python' is not recognized Python isn't on the path, or the virtual environment is off Turn on .venv (Step 1); on macOS or Linux try python3
Cannot read unit1b/calder_pack The sample pack isn't there, or you're in the wrong folder Run Step 2 from the orchestrate-course folder
The pack must hold CP10 on S/4HANA and CP20 on ECC The pack is from an older version or was edited Run make_calder_pack.py again
ModuleNotFoundError A library name was mistyped in the script The script only imports built-in modules; compare the import lines with the listing
--check ends with 1 issue(s) and exit code 1 Expected before the exercise: one CP20 line has no mapping Add the mapping in the exercise
A network or proxy error Not from this script; it uses no network Check that you ran reporting.py, not another script

The SAP way

As of October 2026, the same design maps onto SAP's products like this:

  • Operational reporting, CP10: analytical queries on released CDS views, surfaced through Fiori apps and KPIs. Key users build them with Custom CDS Views and Custom Analytical Queries, developers with ABAP Development Tools; both are transported (SAP Learning). Stand-alone SAP Analytics Cloud stories that combine data from other SAP cloud solutions need an enterprise SAC license and an integration on SAP BTP, per the same page.
  • Enterprise layer: SAP Business Data Cloud, with Datasphere modeling the group margin on data products from CP10 and on custom loads from CP20 (SAP Learning). CP10 data leaves through extraction-enabled CDS views (SAP Community); CP20 through ECC extractors or SLT table replication (SAP Community).
  • Dashboards, planning and questions: SAP Analytics Cloud stories and planning on Datasphere models; Just Ask for natural-language questions (SAP Learning).
  • Existing BW: keep it and generate data products from it, or move to Datasphere; SAP's training describes both paths (SAP Learning).
  • Consolidation: SAP S/4HANA Finance for group reporting, with CP20's figures uploaded while it is on ECC (Intercompany and multi-company scenarios).

Build vs. SAP

Situation Choose Why
A worklist or KPI on one S/4HANA system Embedded analytics Live, transported, no copy
A report on ECC only, and ECC retires soon Keep the ECC report Rebuilding it elsewhere is wasted work
Cross-system finance and planning, mostly SAP data, no warehouse yet Business Data Cloud with Datasphere and SAC SAP's data products, models, access control and planning in one service
A non-SAP lakehouse already holds most enterprise data That platform, with data products shared through BDC Connect or a permitted extraction One home for enterprise data; check SAP's interface rules
A large existing BW with trusted models Keep BW and generate data products Protects years of modeling
Legal consolidated statements Group reporting Legal rules and audit trail
A small prototype or a one-off analysis A script on an extract, like this topic's Fast; never the production home for a governed figure

Architecture view

Where it sits. The diagram from the foundational layer, with one design choice made explicit: CP20 reaches the enterprise layer by replication until July 2027, and then from CP10's S/4HANA system after the rollout. Its pre-rollout history stays in the data layer.

Non-functional requirements that decide the design

Requirement Typical target (course values) If missed
Freshness Worklists live; margin daily; plan monthly Users act on stale data, or wait for data they don't need sooner
History 36 months of margin, including ECC years Trend breaks at go-live
Accuracy 0.00 difference to control totals; every line mapped Two margins in one meeting
Data protection Company-level and customer-level access in every layer A US sales lead sees German margins
Load on the source Extraction outside the busiest close days; delta, not full loads Order entry slows down at month end
Cost Only reports that need a copy get one A platform paying to copy data nobody reads

Integration and identity. CP10 data leaves through released, extraction-enabled CDS views, with the connection user restricted to them. CP20 data leaves through SLT or ECC extractors under the same restriction; a direct feed from ECC to the non-SAP warehouse is checked against SAP Note 3255746 first (SAPinsider). Users reach embedded analytics with their S/4HANA roles (Roles and authorization design). In the data layer, their access is designed again in Datasphere's data access controls, keyed on company code and sales organization, because a copied row doesn't carry S/4HANA's authorization checks with it.

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

  • Context: The CFO needs group margin by pump model across CP10 (S/4HANA) and CP20 (ECC), with 36 months of history. CP20 joins CP10's system in July 2027 and its closed history stays in ECC. Calder already runs a non-SAP data warehouse for CRM and shop-floor data.
  • Options: (a) keep the spreadsheet; (b) build the model in the existing warehouse, fed from both SAP systems; (c) build it in Datasphere within Business Data Cloud, fed by CDS extraction from CP10 and SLT from CP20, and share it to the warehouse if needed.
  • Decision: (c), as a course choice. The SAP data stays on SAP's supported routes, the access model lives with the data, and the CP20 feed is temporary.
  • Consequences: A Business Data Cloud subscription to size and fund. Two feeds to run until July 2027, then one. A margin definition with a named owner in the controlling team. Shop-floor data still needs a route into the model for RP06.

What a design review will ask

  1. Which reporting needs stay in S/4HANA, and what rule placed each of the others elsewhere?
  2. How does each source's data leave, and is that interface permitted under SAP's current rules?
  3. Who owns the definition of group margin, and where is it written?
  4. How is the model reconciled to each source every period, and what happens when a check fails?
  5. How are company-level access rules kept the same in S/4HANA and in the data layer?

Production concerns

  • Security and authorizations. In S/4HANA, CDS access controls filter rows by the user's authorizations when the data is read (Grounding on SAP data with authorizations). A copy loses that. Rebuild the rules in the data layer and test them with the same test users.
  • Released objects only. Build on released CDS views and extraction-enabled views. Unreleased tables change with upgrades and break feeds, which is the clean-core point from Fit-to-standard and extension choices.
  • Evaluation. Run the reconciliation every load, not once at go-live. Store the results so an auditor or an AI answer can show them.
  • Cost. Count the copies. Each one costs storage, compute, licensing and someone's time to reconcile. Decommission reports nobody opens.
  • Operations. Feeds fail quietly. Alert on late loads and failed checks, and show "data as of" on every dashboard.
  • Clean core. Analytical objects are built in development and transported (SAP Learning). Treat them like code: reviewed, tested, versioned.

Pitfalls

  • Rebuilding every ECC report before the move. The fit-to-standard topic declined CP20's custom report ZMM_OPEN_PO_AGING because nobody ran it in 14 months. Ask who reads a report before migrating it.
  • Silent drops. A join that loses unmapped lines produces totals that look plausible. Fail on any unmapped line.
  • Mixed rates. Translating income at closing rates in one report and average rates in another produces two margins. Use the group's rate rules.
  • Treating a worklist as a dashboard. Moving the blocked-orders list to a nightly copy breaks the link to the order and shows yesterday's blocks.
  • Forgetting history at go-live. If closed ECC history isn't kept in the data layer, every trend starts again in July 2027.
  • Grounding AI on raw tables. An assistant that writes its own joins over raw SAP tables will define margin differently from the controllers. Point it at the governed model.
  • Assuming any extractor is allowed. Interface rules change; check SAP's current notes for every non-SAP feed.

Exercise

Fix Calder's reconciliation issue, add a new reporting need, and record the decision.

  1. Open unit1b/reporting.py. In MAPPING, add a line for the castings: ("10000008", "CP-CAST300"): "CP20 spare parts",.
  2. Run python unit1b/reporting.py --check. It should report 0 issue(s) and end with exit code 0.
  3. Run python unit1b/reporting.py --margin. The spare-parts line should now read 6,100.00 EUR revenue, and the pump margins should not change.
  4. In NEEDS, add RP09: "Margin by customer across both companies", users "Sales leads", systems {"S4", "ECC"}, latency "monthly", history 24. Run python unit1b/reporting.py and confirm where it lands and why.
  5. Run python unit1b/reporting.py --ask "What is the group margin of the ES-300?" and confirm the warning line is gone.
  6. Run python unit1b/reporting.py --write, open unit1b/calder_reporting.md, and fill in the Decision record with your own context, options, decision and consequences for RP04. You may disagree with the course choice; say why.
  7. Commit the script and the file.

Done when: --check reports 0 issue(s), --margin shows ES-200 group margin 16,254.90 USD and ES-300 4,788.05 USD with 6,100.00 EUR of CP20 spare parts, RP09 is placed in the enterprise data layer because it combines ECC and S/4HANA, and unit1b/calder_reporting.md has a filled-in decision record. Keep it: the capstone at the end of Unit 1B traces one exception across the enterprise, and this landscape is its reporting part.

Check yourself

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

1In the script, why does place() test for legal consolidation and one-record lookups before anything else?

Answer: D. Consolidation has legal rules and its own product, and a one-record lookup for an outside user is an API. Testing them first stops a cross-system rule from sending them to the data layer.

2The group margin for the ES-200 is 7.10 USD lower than Rockford's own margin. What causes it?

Answer: D. At 335.00 USD and a cost of 310.00 EUR, Augsburg's margin per impeller translates to minus 0.42 USD at 1.0820. Seventeen impellers in the pumps issued make minus 7.10 USD. The scrap variance belongs to the ES-300.

3The control totals match, but --check still fails. What does that tell you?

Answer: B. Totals prove that everything arrived; the mapping check proves that everything landed somewhere defined. The castings line was in the extract and the total, but no bucket claimed it.

4You need CP10 sales data in a data platform. Which route is SAP's strategic one for S/4HANA?

Answer: D. CDS views annotated for extraction are exposed to the ODP framework, with delta by timestamp or change data capture. Raw tables carry no released contract, and per-record API calls don't scale to analytics loads.

5Calder's sales leads want margin by customer through the AI assistant, but the model doesn't hold it. What should the assistant do?

Answer: D. A governed assistant answers from defined measures or declines. Writing its own joins would invent a margin definition nobody agreed. The right fix is to add the measure to the model.

6Why must company-level access be designed again in the data layer?

Answer: B. In S/4HANA, CDS access controls filter rows when the data is read. Once copied, the row is just data. Datasphere carries its own analytical roles and data access controls, which must be designed and tested to match.

7A design review asks why CP20's history is kept in the data layer. What is the strongest answer?

Answer: B. Calder's migration moves master data and open items, not closed history. A three-year margin trend needs the old ECC figures next to the new S/4HANA ones, and the data layer is the one place that holds both.

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