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.

Set up for Unit 1B: process tracing tools

Add the Calder Pumpworks sample pack, a way to draw Mermaid diagrams, and a check that your SAP sandbox key works, so you can trace processes across modules.

Updated Oct 23, 2026Foundational 6 minDeep 30 min
Foundational layer · 6 min read

The 60-second version

Unit 1B teaches how a whole SAP enterprise fits together: the organizational structure, the master data every module shares, and the main business processes from order to cash. The hands-on parts follow one company's documents from step to step.

That company is Calder Pumpworks, a fictional pump maker with a plant in Rockford, USA, and one in Augsburg, Germany. Learners use the same Calder orders, customers and plants in every Unit 1B topic, so each topic builds on the last.

Very little is new to install. Learners add three things to the laptop they set up in Unit 1:

  • The Calder sample pack: a small set of made-up data files, written by one script, that describe Calder's org units, master data and one sales order with every document that follows it.
  • A way to draw diagrams: process flows are written as Mermaid diagrams, plain text that tools turn into pictures.
  • A check that the SAP sandbox key still works, because some topics compare Calder's documents with SAP's own demo data.

It takes about 30 minutes and costs nothing.

Why it matters to the business

Most SAP problems that reach a leader cross module boundaries. A blocked sales order turns out to be a credit setting. A late invoice turns out to be a goods issue that never posted. People who can trace one order across sales, logistics and finance find those causes in hours instead of weeks.

Tracing needs data that shows the whole chain. A real system has it, but learners must not use real company data for training. The sample pack solves that: it is small enough to read in one sitting, complete enough to follow an order from the customer's purchase order to the payment, and entirely fictional.

The diagram habit matters too. Architects and consultants explain designs with pictures. Writing them as text means they live next to the code, can be reviewed like code, and show up correctly on GitHub, where learners keep their portfolio.

How SAP does it

On a real program, a team traces documents in the SAP system itself and reads data through SAP's released APIs. The course mirrors that in two ways:

  • SAP's sandbox. As of October 2026, the SAP Business Accelerator Hub offers sandbox APIs for SAP S/4HANA with demo data, authenticated with one free API key (SAP Learning). Learners got that key in Unit 1. The sandbox holds SAP's demo company, not Calder.
  • SAP-shaped sample data. The Calder pack uses the same field names as SAP's released APIs for sales orders, deliveries, billing documents, business partners and products. Code written against the pack therefore reads like code written against a real system. The codes and amounts are invented and are not SAP standard values.

Org units such as company codes and plants are configuration, set up once when a system is implemented. No single API returns them, so the pack keeps them in a file of their own.

What this unit adds

Item What it is Cost Used in
Calder sample pack Five data files: pack info, org units, business partners, products, one order's documents Free Every Unit 1B topic that runs code with --sample
Mermaid diagrams Text that VS Code, GitHub and mermaid.live draw as flowcharts Free Process flows and deliverables in every Unit 1B topic
SAP sandbox key The key from Unit 1, checked again Free Topics that compare Calder with SAP's demo data
check_unit1b.py A script that proves the three items above work Free This topic

No new Python libraries. Both scripts in this topic use only what comes with Python.

Time and money

  • Time: about 30 minutes, most of it reading the sample files.
  • Money: nothing. No new accounts, no trials.
  • Network: only the sandbox check needs the internet. The sample pack works offline.

Questions to ask IT

  • Does our network still allow sandbox.api.sap.com? (It was needed for Unit 1.)
  • May learners keep made-up sample data in a personal GitHub repository?
  • Is VS Code on company laptops kept up to date, or pinned to an older version?
  • If the browser is the only option, may learners use the public site mermaid.live to draw diagrams?

Common misconceptions

  • "The sample pack is a copy of SAP data." No. Every name, code and amount is invented for the course. Only the field names follow SAP's APIs.
  • "Calder's codes are SAP standard." No. Codes such as CP10 or NT30 are illustrative. Real systems define their own.
  • "We need a real SAP system to learn processes." Not to start. Reading a complete, consistent chain of documents teaches the flow. A system adds the screens and the configuration later.
  • "Diagrams need a drawing tool licence." Mermaid is free text. VS Code's preview, GitHub and mermaid.live all draw it.

Key terms

All course terms, A to Z, in the glossary

  • Sample pack: the set of made-up Calder data files the Unit 1B scripts read when run with --sample.
  • Org unit: an organizational element such as a company code (a legal entity's books), a plant (a site that makes or stores goods) or a sales organization (the unit that sells).
  • Master data: long-lived records that many documents use, such as customers, suppliers and products.
  • Document flow: the chain of documents one business transaction creates, each pointing to the one before.
  • Mermaid: a text format for diagrams, written in a code block and drawn by tools that support it.
  • Sandbox: SAP's public test system on the Business Accelerator Hub, with demo data and read access by API key.

Check yourself

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

1What does a learner need to add for Unit 1B?

Answer: B. Unit 1B adds the Calder sample pack, Mermaid diagrams and a check that the Unit 1 sandbox key still works. There are no new libraries, accounts or costs.

2Why does the course use a fictional company instead of real data?

Answer: C. Real company data must stay out of training. Using one fictional company across the unit also means each topic builds on the same orders, customers and plants.

3A consultant says the pack's company code CP10 is an SAP standard value. Is that right?

Answer: D. Only the field names follow SAP's APIs. Codes, names and amounts are invented for the course, and each real system defines its own codes.

4Why write process diagrams as Mermaid text?

Answer: B. Text diagrams sit beside the code, change with it and are reviewed the same way. VS Code, GitHub and mermaid.live draw them for free.

5What is the most useful thing to check with IT before Unit 1B?

Answer: A. Only the sandbox check needs the network, and company proxies often block new sites. The sample pack itself works offline.

6Where do org units such as company codes and plants come from in a real system?

Answer: A. Org units are configuration, set up once during implementation. No single API returns them, which is why the pack keeps them in their own file.
Deep layer · 30 min read

Mental model: one company, many files, one chain

Unit 1B is about following one business transaction through many modules. The sample pack is built for exactly that: a few small files, each one a kind of SAP data, linked by the codes they share.

flowchart LR
  O[org_units.json<br/>company codes, plants,<br/>sales and purchasing orgs] --> D
  B[business_partners.json<br/>customers and suppliers] --> D
  P[products.json<br/>pumps, parts, components] --> D
  D[order_1000001.json<br/>order, delivery, goods issue,<br/>invoice, journal entries, payment]

Every code in the order file must exist in another file. Sales organization CPUS must be an org unit. Customer 10000001 must be a business partner. Product CP-ES200 must have data for plant CP11. That is how a real SAP system behaves too: a document can only use org units and master data that already exist. The check script tests exactly this.

How it works

What is in the pack

File What it holds Shaped like
pack_info.json Pack name, version, a fictional flag and the file list Course format
org_units.json Client, company codes CP10 and CP20, controlling area, plants CP11 and CP21, storage locations, sales organizations, distribution channels, divisions, sales areas, purchasing organizations, shipping points and the Rockford warehouse SAP field names for each unit's key; course layout
business_partners.json Three customers and four suppliers, including the two Calder companies as intercompany partners API_BUSINESS_PARTNER: A_BusinessPartner with to_Customer and to_Supplier
products.json Two finished pumps, a seal kit, a machined impeller and a motor, with plant and valuation data API_PRODUCT_SRV: A_Product with to_Description, to_Plant, to_Valuation
order_1000001.json One sales order and everything after it: outbound delivery, goods issue, invoice, three journal entries and the document flow Sales order, outbound delivery, material document and billing document APIs; journal entries in a simplified course shape

Every field name in the business partner, product, sales order, delivery, material document and billing document records was checked against SAP Cloud SDK packages generated from those services' metadata (version 2.1.0, npm). Product types use SAP's standard material type keys, such as FERT for finished products and ROH for raw materials (SAP Help Portal). Your own system may define others.

One order, end to end

Order 1000001 is the thread the whole unit pulls on. Brennock Water Utility, a fictional customer, buys four ES-200 pumps and two ES-300 pumps from Calder's US sales organization. Here is what the pack records, in date order:

Date Document Number What it records
2026-09-14 Sales order 1000001 Customer, sales area, two items, prices and the plant that ships them (CP11)
2026-09-25 Outbound delivery 80000001 What leaves the Rockford dock (RK01), each item pointing back to the order item
2026-09-25 Material document 4900000001 The goods issue: stock leaves storage location FG01
2026-09-25 Journal entry 5000000001 The goods issue's cost moving from inventory to cost of goods sold
2026-09-26 Billing document 90000001 The invoice, pointing to the delivery and to its own journal entry
2026-09-26 Journal entry 9400000001 Revenue and the customer's receivable
2026-10-20 Journal entry 1400000001 The customer's payment clearing the receivable

Two things are worth noticing already. First, logistics documents (delivery, goods issue) and finance documents (journal entries) are separate, but linked. Second, in the pack two different events post to finance: the goods issue posts cost, and the invoice posts revenue. Why each one posts, and to which accounts, is the subject of Document flow and how logistics becomes finance.

Why a script writes the pack

The pack is written by a script rather than downloaded, for three reasons:

  • It is reproducible. The script contains no random numbers. Everyone who runs it gets byte-for-byte the same files, so your output can be compared with the topic's.
  • It is consistent by construction. Amounts are computed, not typed: each item's net value is quantity times price, the order total is the sum of the items, and the cost posted at goods issue comes from the products' standard prices. Change a price and every document that uses it changes with it.
  • It is readable. Opening the script shows how the data hangs together. A functional consultant can read it like a configuration table without running anything.

If you break a file while experimenting, run the script again. It rewrites all five files and the diagram.

Where the pack is simpler than SAP

The pack keeps only the fields the Unit 1B topics need. Three simplifications matter:

  • Dates are plain YYYY-MM-DD text. SAP's OData V2 APIs return dates as /Date(milliseconds)/, which you met in Calling your first SAP API. The pack uses readable dates so you can follow the timeline by eye.
  • Journal entries are a course shape. Each has a company code, fiscal year, document number and lines with a GL account and an amount. In the pack, debits are positive and credits negative, so every entry's lines add up to zero. The GL account numbers are made up.
  • The document flow is an explicit list. In SAP you read it from the documents' references or from process-flow entities. The pack lists the links once, so a script or a diagram can follow them directly.

Later Unit 1B topics add files to the pack when they need them, for example a purchase order for procure-to-pay. The pack_version field says which version you have.

How Mermaid diagrams get drawn

A Mermaid diagram is a fenced code block marked mermaid. Three free tools draw it:

  • VS Code. As of October 2026, the built-in Markdown preview renders mermaid code blocks, with pan and zoom (VS Code docs). On an older VS Code that shows the code as text, update VS Code or install the Markdown Preview Mermaid Support extension (GitHub).
  • GitHub. Mermaid blocks in Markdown files render as diagrams (GitHub blog), so your portfolio shows the pictures, not the code.
  • mermaid.live. The Mermaid project's free live editor in the browser (Mermaid). Use it if you can't install anything. Don't paste company data into it.

Build it yourself: add the sample pack and prove your tools work

You will write the Calder sample pack with one script, look at its files and its diagram, and run a check that proves everything Unit 1B needs is in place.

flowchart LR
  S[make_calder_pack.py] --> P[calder_pack/<br/>5 JSON files]
  S --> M[calder_order_flow.md<br/>Mermaid diagram]
  P --> C[check_unit1b.py]
  M --> C
  K[.env<br/>SAP_API_KEY] --> C
  C -->|one read-only call| X[SAP sandbox]

Before you start: complete Set up your computer for this course. It creates your orchestrate-course folder with its .venv, and stores SAP_API_KEY in .env. This topic adds nothing else to install.

What you need

  • Your course folder from Unit 1, with .venv and .env.
  • VS Code, ideally updated to the latest version.
  • About 30 minutes. Free. The sandbox check needs a network that reaches sandbox.api.sap.com; everything else works offline.

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

  1. Open VS Code, choose File > Open Folder and open orchestrate-course.
  2. Open a terminal with Terminal > New Terminal.
  3. Turn on the virtual environment if the prompt doesn't already start with (.venv):
    • Windows (PowerShell):

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

      source .venv/bin/activate

Step 2: Make sure VS Code can draw Mermaid

  1. Update VS Code: on Windows choose Help > Check for Updates; on macOS choose Code > Check for Updates; on Linux update it with your package manager. Restart it if it asks.
  2. You will test the preview in Step 4. If it shows the diagram as plain code there, click the Extensions icon on the left, search for Markdown Preview Mermaid Support, and install it.

Step 3: Write the sample pack

  1. Create the Unit 1B folder:

    mkdir unit1b
  2. In VS Code, create a new file, paste the script below, and save it as make_calder_pack.py inside unit1b.

  3. Run it from the course folder:

    • Windows (PowerShell):

      python unit1b\make_calder_pack.py
    • macOS / Linux:

      python unit1b/make_calder_pack.py
"""Write the Calder Pumpworks sample pack for Unit 1B.

Calder Pumpworks is a fictional company. Every code, name, number and amount
in this pack is made up for the course. Field names follow SAP's released
OData APIs where an API exists; values are illustrative, not SAP standard.

Run it from your course folder:  python unit1b/make_calder_pack.py
It writes the folder unit1b/calder_pack/ and changes nothing else.
Run it again at any time: it always writes exactly the same files.
"""
import json
from pathlib import Path

PACK_VERSION = "1.0"
HERE = Path(__file__).resolve().parent
OUT = HERE / "calder_pack"

# ---------------------------------------------------------------- org units
# Org units are configuration, not business data, so no single API returns
# them. This file uses SAP's field names for each unit's key.
ORG_UNITS = {
    "Client": "100",
    "CompanyCodes": [
        {"CompanyCode": "CP10", "CompanyCodeName": "Calder Pumpworks Inc.",
         "Country": "US", "Currency": "USD", "ControllingArea": "CP00", "System": "S/4HANA"},
        {"CompanyCode": "CP20", "CompanyCodeName": "Calder Pumpworks GmbH",
         "Country": "DE", "Currency": "EUR", "ControllingArea": "CP00", "System": "ECC"},
    ],
    "ControllingAreas": [
        {"ControllingArea": "CP00", "Name": "Calder group controlling", "Currency": "USD"},
    ],
    "Plants": [
        {"Plant": "CP11", "PlantName": "Rockford", "CompanyCode": "CP10",
         "Role": "Final assembly and test of pumps"},
        {"Plant": "CP21", "PlantName": "Augsburg", "CompanyCode": "CP20",
         "Role": "Machined components"},
    ],
    "StorageLocations": [
        {"Plant": "CP11", "StorageLocation": "FG01", "Name": "Rockford distribution center"},
        {"Plant": "CP11", "StorageLocation": "RM01", "Name": "Rockford plant stores"},
        {"Plant": "CP21", "StorageLocation": "RM01", "Name": "Augsburg plant stores"},
        {"Plant": "CP21", "StorageLocation": "FG01", "Name": "Augsburg shipping stock"},
    ],
    "SalesOrganizations": [
        {"SalesOrganization": "CPUS", "Name": "Calder sales USA", "CompanyCode": "CP10", "Currency": "USD"},
        {"SalesOrganization": "CPDE", "Name": "Calder sales Germany", "CompanyCode": "CP20", "Currency": "EUR"},
    ],
    "DistributionChannels": [
        {"DistributionChannel": "10", "Name": "Direct to equipment makers and utilities"},
        {"DistributionChannel": "20", "Name": "Distributors"},
        {"DistributionChannel": "30", "Name": "Aftermarket parts and service"},
    ],
    "Divisions": [
        {"Division": "PU", "Name": "Pumps"},
        {"Division": "SP", "Name": "Spare parts"},
    ],
    "SalesAreas": [
        {"SalesOrganization": "CPUS", "DistributionChannel": "10", "Division": "PU"},
        {"SalesOrganization": "CPUS", "DistributionChannel": "20", "Division": "PU"},
        {"SalesOrganization": "CPUS", "DistributionChannel": "30", "Division": "SP"},
        {"SalesOrganization": "CPDE", "DistributionChannel": "10", "Division": "PU"},
    ],
    "SalesOrgPlants": [
        {"SalesOrganization": "CPUS", "DistributionChannel": "10", "Plant": "CP11"},
        {"SalesOrganization": "CPUS", "DistributionChannel": "20", "Plant": "CP11"},
        {"SalesOrganization": "CPUS", "DistributionChannel": "30", "Plant": "CP11"},
        {"SalesOrganization": "CPDE", "DistributionChannel": "10", "Plant": "CP21"},
    ],
    "PurchasingOrganizations": [
        {"PurchasingOrganization": "PUUS", "Name": "Purchasing USA", "CompanyCode": "CP10", "Plants": ["CP11"]},
        {"PurchasingOrganization": "PUDE", "Name": "Purchasing Germany", "CompanyCode": "CP20", "Plants": ["CP21"]},
    ],
    "ShippingPoints": [
        {"ShippingPoint": "RK01", "Name": "Rockford dock", "Plant": "CP11"},
        {"ShippingPoint": "AU01", "Name": "Augsburg dock", "Plant": "CP21"},
    ],
    "Warehouses": [
        {"Warehouse": "RKDC", "Name": "Rockford distribution center",
         "Plant": "CP11", "StorageLocations": ["FG01"]},
    ],
}


# ------------------------------------------------------------ master data
def customer(bp, name, company_code, recon, sales_areas, terms="NT30"):
    """A business partner in the customer role, shaped like API_BUSINESS_PARTNER."""
    return {
        "BusinessPartner": bp, "BusinessPartnerFullName": name, "Customer": bp, "Supplier": "",
        "to_Customer": {
            "Customer": bp, "CustomerName": name,
            "to_CustomerCompany": [{"Customer": bp, "CompanyCode": company_code,
                                    "PaymentTerms": terms, "ReconciliationAccount": recon}],
            "to_CustomerSalesArea": [
                {"Customer": bp, "SalesOrganization": so, "DistributionChannel": dc, "Division": dv,
                 "Currency": cur, "CustomerPaymentTerms": terms}
                for so, dc, dv, cur in sales_areas],
        },
    }


def supplier(bp, name, company_code, purch_org, currency, recon="21100", terms="NT45"):
    """A business partner in the supplier role, shaped like API_BUSINESS_PARTNER."""
    return {
        "BusinessPartner": bp, "BusinessPartnerFullName": name, "Customer": "", "Supplier": bp,
        "to_Supplier": {
            "Supplier": bp, "SupplierName": name,
            "to_SupplierCompany": [{"Supplier": bp, "CompanyCode": company_code,
                                    "PaymentTerms": terms, "ReconciliationAccount": recon}],
            "to_SupplierPurchasingOrg": [{"Supplier": bp, "PurchasingOrganization": purch_org,
                                          "PurchaseOrderCurrency": currency, "PaymentTerms": terms,
                                          "InvoiceIsGoodsReceiptBased": True}],
        },
    }


BUSINESS_PARTNERS = {"BusinessPartners": [
    customer("10000001", "Brennock Water Utility", "CP10", "12100", [("CPUS", "10", "PU", "USD")]),
    customer("10000002", "Hallam Fluid Systems", "CP10", "12100",
             [("CPUS", "20", "PU", "USD"), ("CPUS", "30", "SP", "USD")]),
    customer("10000010", "Calder Pumpworks Inc. (intercompany)", "CP20", "12900",
             [("CPDE", "10", "PU", "EUR")]),
    supplier("20000001", "Orrin Motor Works", "CP10", "PUUS", "USD"),
    supplier("20000002", "Tamsfield Foundry", "CP20", "PUDE", "EUR"),
    supplier("20000003", "Ridgeline Bearing Supply", "CP10", "PUUS", "USD"),
    supplier("20000010", "Calder Pumpworks GmbH (intercompany)", "CP10", "PUUS", "USD", recon="21900"),
]}


def product(code, ptype, division, text, plants, std_price, currency):
    """A product shaped like API_PRODUCT_SRV, with plant and valuation data."""
    return {
        "Product": code, "ProductType": ptype, "BaseUnit": "EA", "Division": division,
        "to_Description": [{"Product": code, "Language": "EN", "ProductDescription": text}],
        "to_Plant": [{"Product": code, "Plant": p, "BaseUnit": "EA"} for p in plants],
        "to_Valuation": [{"Product": code, "ValuationArea": p, "StandardPrice": std_price,
                          "PriceUnitQty": 1, "Currency": currency} for p in plants],
    }


PRODUCTS = {"Products": [
    product("CP-ES200", "FERT", "PU", "End-suction pump ES-200", ["CP11"], 2850.00, "USD"),
    product("CP-ES300", "FERT", "PU", "End-suction pump ES-300", ["CP11"], 4100.00, "USD"),
    product("CP-SK200", "HAWA", "SP", "Seal kit for ES-200", ["CP11"], 38.00, "USD"),
    product("CP-IMP200", "HALB", "PU", "Machined impeller for ES-200", ["CP21"], 310.00, "EUR"),
    product("CP-MTR15", "ROH", "PU", "Electric motor 15 kW", ["CP11"], 920.00, "USD"),
]}


# ------------------------------------------------- one order, end to end
ITEMS = [  # item, product, quantity, price each (USD); costs come from PRODUCTS
    ("10", "CP-ES200", 4, 4200.00),
    ("20", "CP-ES300", 2, 6150.00),
]
STD_PRICE = {p["Product"]: p["to_Valuation"][0]["StandardPrice"] for p in PRODUCTS["Products"]}
NET = round(sum(q * price for _, _, q, price in ITEMS), 2)
COST = round(sum(q * STD_PRICE[m] for _, m, q, _ in ITEMS), 2)

ORDER_DOCUMENTS = {
    "SalesOrder": {
        "SalesOrder": "1000001", "SalesOrganization": "CPUS", "DistributionChannel": "10",
        "OrganizationDivision": "PU", "SoldToParty": "10000001",
        "PurchaseOrderByCustomer": "BWU-PO-5531", "SalesOrderDate": "2026-09-14",
        "RequestedDeliveryDate": "2026-09-28", "TransactionCurrency": "USD",
        "TotalNetAmount": NET, "CustomerPaymentTerms": "NT30",
        "to_Item": [{"SalesOrder": "1000001", "SalesOrderItem": i, "Material": m,
                     "RequestedQuantity": q, "RequestedQuantityUnit": "EA",
                     "NetAmount": round(q * price, 2), "ProductionPlant": "CP11",
                     "StorageLocation": "FG01", "ShippingPoint": "RK01"}
                    for i, m, q, price in ITEMS],
    },
    "OutboundDelivery": {
        "DeliveryDocument": "80000001", "ShippingPoint": "RK01", "SalesOrganization": "CPUS",
        "SoldToParty": "10000001", "ShipToParty": "10000001", "Warehouse": "RKDC",
        "PlannedGoodsIssueDate": "2026-09-25", "ActualGoodsMovementDate": "2026-09-25",
        "to_DeliveryDocumentItem": [{"DeliveryDocument": "80000001", "DeliveryDocumentItem": i,
                                     "Material": m, "ActualDeliveryQuantity": q,
                                     "DeliveryQuantityUnit": "EA", "Plant": "CP11",
                                     "StorageLocation": "FG01", "ReferenceSDDocument": "1000001",
                                     "ReferenceSDDocumentItem": i}
                                    for i, m, q, _ in ITEMS],
    },
    "MaterialDocument": {
        "MaterialDocument": "4900000001", "MaterialDocumentYear": "2026", "PostingDate": "2026-09-25",
        "ReferenceDocument": "80000001",
        "to_MaterialDocumentItem": [{"MaterialDocument": "4900000001", "MaterialDocumentYear": "2026",
                                     "MaterialDocumentItem": str(n), "Material": m, "Plant": "CP11",
                                     "StorageLocation": "FG01", "QuantityInEntryUnit": q,
                                     "EntryUnit": "EA", "Delivery": "80000001", "DeliveryItem": i,
                                     "Customer": "10000001"}
                                    for n, (i, m, q, _) in enumerate(ITEMS, start=1)],
    },
    "BillingDocument": {
        "BillingDocument": "90000001", "BillingDocumentDate": "2026-09-26",
        "SalesOrganization": "CPUS", "DistributionChannel": "10", "Division": "PU",
        "SoldToParty": "10000001", "PayerParty": "10000001", "TotalNetAmount": NET,
        "TaxAmount": 0.00, "TransactionCurrency": "USD", "CustomerPaymentTerms": "NT30",
        "CompanyCode": "CP10", "FiscalYear": "2026", "AccountingDocument": "9400000001",
        "to_Item": [{"BillingDocument": "90000001", "BillingDocumentItem": i, "Material": m,
                     "BillingQuantity": q, "BillingQuantityUnit": "EA",
                     "NetAmount": round(q * price, 2), "Plant": "CP11",
                     "ReferenceSDDocument": "80000001", "ReferenceSDDocumentItem": i,
                     "SalesDocument": "1000001", "SalesDocumentItem": i}
                    for i, m, q, price in ITEMS],
    },
    # Journal entries: debits are positive and credits negative in this pack,
    # so every entry's lines add up to zero. GL accounts are illustrative.
    "JournalEntries": [
        {"CompanyCode": "CP10", "FiscalYear": "2026", "AccountingDocument": "5000000001",
         "PostingDate": "2026-09-25", "Source": "MaterialDocument 4900000001 (goods issue)",
         "Lines": [{"GLAccount": "51000", "Text": "Cost of goods sold",
                    "AmountInCompanyCodeCurrency": COST, "CompanyCodeCurrency": "USD"},
                   {"GLAccount": "13100", "Text": "Finished goods inventory",
                    "AmountInCompanyCodeCurrency": -COST, "CompanyCodeCurrency": "USD"}]},
        {"CompanyCode": "CP10", "FiscalYear": "2026", "AccountingDocument": "9400000001",
         "PostingDate": "2026-09-26", "Source": "BillingDocument 90000001 (invoice)",
         "Lines": [{"GLAccount": "12100", "Text": "Receivable from 10000001", "Customer": "10000001",
                    "AmountInCompanyCodeCurrency": NET, "CompanyCodeCurrency": "USD"},
                   {"GLAccount": "41000", "Text": "Revenue, pumps",
                    "AmountInCompanyCodeCurrency": -NET, "CompanyCodeCurrency": "USD"}]},
        {"CompanyCode": "CP10", "FiscalYear": "2026", "AccountingDocument": "1400000001",
         "PostingDate": "2026-10-20", "Source": "Incoming payment for invoice 90000001",
         "Lines": [{"GLAccount": "11000", "Text": "Bank",
                    "AmountInCompanyCodeCurrency": NET, "CompanyCodeCurrency": "USD"},
                   {"GLAccount": "12100", "Text": "Receivable from 10000001", "Customer": "10000001",
                    "AmountInCompanyCodeCurrency": -NET, "CompanyCodeCurrency": "USD"}]},
    ],
    # The chain from first document to last. A course shape, not an SAP API.
    "DocumentFlow": [
        {"From": "SalesOrder 1000001", "To": "OutboundDelivery 80000001"},
        {"From": "OutboundDelivery 80000001", "To": "MaterialDocument 4900000001"},
        {"From": "MaterialDocument 4900000001", "To": "JournalEntry 5000000001"},
        {"From": "OutboundDelivery 80000001", "To": "BillingDocument 90000001"},
        {"From": "BillingDocument 90000001", "To": "JournalEntry 9400000001"},
        {"From": "JournalEntry 9400000001", "To": "JournalEntry 1400000001"},
    ],
}

FILES = {
    "org_units.json": ORG_UNITS,
    "business_partners.json": BUSINESS_PARTNERS,
    "products.json": PRODUCTS,
    "order_1000001.json": ORDER_DOCUMENTS,
}
PACK_INFO = {
    "pack": "Calder Pumpworks sample pack", "pack_version": PACK_VERSION,
    "fictional": True,
    "note": "Made-up data for the Orchestrate course. Not SAP standard values, not from any real system.",
    "files": sorted(FILES),
}


def flow_diagram() -> str:
    """A Mermaid flowchart of the order's document flow, built from the pack."""
    fence = "`" * 3  # three backticks open and close a Markdown code block
    ids = {}
    lines = [fence + "mermaid", "flowchart LR"]
    for link in ORDER_DOCUMENTS["DocumentFlow"]:
        for doc in (link["From"], link["To"]):
            if doc not in ids:
                ids[doc] = f"D{len(ids) + 1}"
                kind, number = doc.split(" ")
                lines.append(f'  {ids[doc]}["{kind}<br/>{number}"]')
        lines.append(f"  {ids[link['From']]} --> {ids[link['To']]}")
    lines.append(fence)
    return "# Calder order 1000001: document flow\n\n" + "\n".join(lines) + "\n"


def main() -> None:
    OUT.mkdir(exist_ok=True)
    for name, data in {**FILES, "pack_info.json": PACK_INFO}.items():
        (OUT / name).write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
        print(f"  wrote calder_pack/{name}")
    (HERE / "calder_order_flow.md").write_text(flow_diagram(), encoding="utf-8")
    print("  wrote calder_order_flow.md")
    print(f"\nCalder sample pack {PACK_VERSION}: order 1000001, net {NET:,.2f} USD, cost {COST:,.2f} USD.")
    print("All data is fictional. Next: python check_unit1b.py")


if __name__ == "__main__":
    main()

You should see:

  wrote calder_pack/org_units.json
  wrote calder_pack/business_partners.json
  wrote calder_pack/products.json
  wrote calder_pack/order_1000001.json
  wrote calder_pack/pack_info.json
  wrote calder_order_flow.md

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 contains no random numbers, so every run writes exactly the same files. Your output must match the totals above.

Step 4: Look inside the pack

  1. In VS Code's file list, open unit1b/calder_pack/order_1000001.json. Find "SoldToParty": "10000001" in the sales order.
  2. Open unit1b/calder_pack/business_partners.json and find business partner 10000001. It is Brennock Water Utility, a fictional water utility, with a sales area for sales organization CPUS.
  3. Back in the order file, find the delivery's "ReferenceSDDocument": "1000001". That is the delivery pointing back to the sales order: document flow in one field.
  4. Open unit1b/calder_order_flow.md and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS) to open the preview. You should see a flowchart from the sales order through the delivery to the invoice and the payment.
  5. If the preview shows code instead of a picture, finish Step 2.2. If you can't install anything, open mermaid.live in your browser and paste the lines between the two mermaid fences.

Step 5: Run the Unit 1B check

  1. Create a new file in the course folder (not in unit1b), paste the script below and save it as check_unit1b.py.

  2. Run it:

    python check_unit1b.py
"""Check that your computer is ready for Unit 1B (SAP end to end).

Run it from your course folder:  python check_unit1b.py
Add --offline to skip the sandbox call (for example on a plane).
It reads the Calder sample pack and your .env, and makes one read-only
call to SAP's sandbox. It changes nothing and prints no keys.
"""
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path

PACK = Path("unit1b") / "calder_pack"
FILES = ["pack_info.json", "org_units.json", "business_partners.json",
         "products.json", "order_1000001.json"]
SANDBOX = ("https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/"
           "API_SALES_ORDER_SRV/A_SalesOrder?$top=1&$select=SalesOrder&$format=json")
OFFLINE = "--offline" in sys.argv
problems = 0


def report(ok: bool, label: str, fix: str = "", optional: bool = False) -> None:
    """Print one line: OK, MISSING (must fix) or LATER (optional for now)."""
    global problems
    if ok:
        print(f"  OK       {label}")
    elif optional:
        print(f"  LATER    {label}  ->  {fix}")
    else:
        problems += 1
        print(f"  MISSING  {label}  ->  {fix}")


def read_env_key(name: str) -> str:
    """Return a key from the environment or from .env, using built-in modules only."""
    if os.environ.get(name):
        return os.environ[name]
    env = Path(".env")
    if env.exists():
        for line in env.read_text(encoding="utf-8").splitlines():
            key, sep, value = line.partition("=")
            if sep and key.strip() == name:
                return value.strip().strip('"').strip("'")
    return ""


def pack_problems(pack: dict) -> list:
    """Cross-check the pack: every code a document uses must exist in the org units or master data."""
    org, bps, prods, order = (pack[f] for f in FILES[1:])
    company_codes = {c["CompanyCode"] for c in org["CompanyCodes"]}
    plants = {p["Plant"]: p["CompanyCode"] for p in org["Plants"]}
    sales_orgs = {s["SalesOrganization"] for s in org["SalesOrganizations"]}
    customers = {b["Customer"] for b in bps["BusinessPartners"] if b["Customer"]}
    products = {p["Product"]: {pl["Plant"] for pl in p["to_Plant"]} for p in prods["Products"]}
    found = []
    found += [f"plant {p} points to unknown company code {c}" for p, c in plants.items() if c not in company_codes]
    so = order["SalesOrder"]
    if so["SalesOrganization"] not in sales_orgs:
        found.append(f"sales order uses unknown sales organization {so['SalesOrganization']}")
    if so["SoldToParty"] not in customers:
        found.append(f"sales order uses unknown customer {so['SoldToParty']}")
    for item in so["to_Item"]:
        if item["ProductionPlant"] not in products.get(item["Material"], set()):
            found.append(f"item {item['SalesOrderItem']}: {item['Material']} has no plant data in {item['ProductionPlant']}")
    for item in order["OutboundDelivery"]["to_DeliveryDocumentItem"]:
        if item["ReferenceSDDocument"] != so["SalesOrder"]:
            found.append(f"delivery item {item['DeliveryDocumentItem']} does not point to the sales order")
    billing = order["BillingDocument"]
    accounting = {j["AccountingDocument"] for j in order["JournalEntries"]}
    if billing["AccountingDocument"] not in accounting:
        found.append("billing document points to a journal entry that is not in the pack")
    for entry in order["JournalEntries"]:
        total = round(sum(line["AmountInCompanyCodeCurrency"] for line in entry["Lines"]), 2)
        if total != 0:
            found.append(f"journal entry {entry['AccountingDocument']} does not balance ({total})")
    return found


print("\n1. Python")
v = sys.version_info
report(v >= (3, 10), f"Python {v.major}.{v.minor}.{v.micro}", "install Python 3.10 or newer (course setup topic)")
report(sys.prefix != sys.base_prefix, "virtual environment is active", "activate .venv (course setup topic, Step 4)")

print("\n2. Calder sample pack")
pack = {}
for name in FILES:
    try:
        pack[name] = json.loads((PACK / name).read_text(encoding="utf-8"))
        report(True, f"calder_pack/{name}")
    except FileNotFoundError:
        report(False, f"calder_pack/{name}", "run python unit1b/make_calder_pack.py (Step 3)")
    except json.JSONDecodeError as err:
        report(False, f"calder_pack/{name}", f"not valid JSON (line {err.lineno}); run the pack script again")
if len(pack) == len(FILES):
    info = pack["pack_info.json"]
    report(info.get("fictional") is True, f"pack version {info.get('pack_version')}, marked fictional",
           "run python unit1b/make_calder_pack.py again")
    issues = pack_problems(pack)
    report(not issues, "documents point to org units and master data that exist",
           "; ".join(issues[:3]) + " (run the pack script again)")
    order = pack["order_1000001.json"]
    print(f"           order {order['SalesOrder']['SalesOrder']}: {len(order['DocumentFlow'])} links "
          f"in its document flow, {len(order['JournalEntries'])} journal entries")

print("\n3. Diagrams")
flow = Path("unit1b") / "calder_order_flow.md"
report(flow.exists() and "`" * 3 + "mermaid" in flow.read_text(encoding="utf-8"),
       "unit1b/calder_order_flow.md holds a Mermaid diagram", "run python unit1b/make_calder_pack.py (Step 3)")

print("\n4. SAP sandbox")
key = read_env_key("SAP_API_KEY")
report(bool(key), "SAP_API_KEY", "add it to .env (course setup topic, Step 6)")
if OFFLINE:
    report(False, "sandbox call", "skipped (--offline); run again without --offline", optional=True)
elif key:
    request = urllib.request.Request(SANDBOX, headers={"APIKey": key, "Accept": "application/json"})
    try:
        with urllib.request.urlopen(request, timeout=30) as resp:
            report(resp.status == 200, "sandbox accepted your key (Sales Order API)", f"HTTP {resp.status}")
    except urllib.error.HTTPError as err:
        hint = "key rejected: copy it again from api.sap.com" if err.code in (401, 403) else f"HTTP {err.code}"
        report(False, "sandbox call", hint)
    except (urllib.error.URLError, TimeoutError, OSError) as err:
        report(False, "sandbox call", f"network problem ({type(err).__name__}): try another network or ask IT")

print()
if problems:
    print(f"{problems} item(s) to fix. Fix them in order, then run this again.")
    sys.exit(1)
print("All set. You are ready for Unit 1B.")

When everything is ready you see:

1. Python
  OK       Python 3.14.4
  OK       virtual environment is active

2. Calder sample pack
  OK       calder_pack/pack_info.json
  OK       calder_pack/org_units.json
  OK       calder_pack/business_partners.json
  OK       calder_pack/products.json
  OK       calder_pack/order_1000001.json
  OK       pack version 1.0, marked fictional
  OK       documents point to org units and master data that exist
           order 1000001: 6 links in its document flow, 3 journal entries

3. Diagrams
  OK       unit1b/calder_order_flow.md holds a Mermaid diagram

4. SAP sandbox
  OK       SAP_API_KEY
  OK       sandbox accepted your key (Sales Order API)

All set. You are ready for Unit 1B.

Your Python version may differ. If you are offline, run python check_unit1b.py --offline: the sandbox line then shows LATER, which is fine for now, because every Unit 1B script also runs with --sample.

Step 6: Save your work in Git

git add unit1b check_unit1b.py
git commit -m "Add Unit 1B sample pack and check"

The pack is made-up data, so it is safe in your portfolio. .env stays out, as set up in Unit 1.

What each part of the code does

Part What it does
ORG_UNITS Calder's org structure: which plant belongs to which company code, which sales organization sells from which plant
customer() and supplier() Build business partners in the shape of API_BUSINESS_PARTNER, with company code and sales area or purchasing organization data
product() Builds products in the shape of API_PRODUCT_SRV, with plant and standard price
ORDER_DOCUMENTS One order and its follow-on documents; amounts come from the item prices and product costs, so they always agree
flow_diagram() Turns the DocumentFlow list into a Mermaid flowchart
pack_problems() in the check Tests that every code the order uses exists, that the delivery points to the order, and that each journal entry balances
Sandbox section in the check Reads SAP_API_KEY from the environment or .env, then asks the Sales Order API for one order number with the key in the APIKey header (SAP tutorial)

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found The terminal can't find Python Repeat Step 1 of the course setup; on macOS and Linux use python3 until .venv is active
can't open file ... make_calder_pack.py You ran it from the wrong folder, or saved it elsewhere Run it from orchestrate-course, and check the file is inside unit1b
MISSING calder_pack/... The pack hasn't been written yet Run Step 3
not valid JSON A pack file was edited and broken Run make_calder_pack.py again; it rewrites all files
documents point to ... -> followed by a reason You changed a code in one file but not the others Run the pack script again, or fix the code the message names
MISSING SAP_API_KEY .env is missing, misnamed or in another folder Put SAP_API_KEY="..." on its own line in .env in the course folder
key rejected The sandbox didn't accept the key Log on at api.sap.com, open Sales Order (A2X), click Show API Key and copy it again
network problem A proxy, VPN or firewall blocks the sandbox Try another network or ask IT to allow sandbox.api.sap.com; use --offline meanwhile
SSL: CERTIFICATE_VERIFY_FAILED on macOS Python's certificates aren't installed Open Applications > Python 3.x and run Install Certificates.command
The Markdown preview shows code, not a diagram Your VS Code is older than the built-in Mermaid support Update VS Code or install the extension (Step 2)

Where this shows up in SAP

On a real program, the same three habits appear with SAP's tools. Consultants trace a document's flow in the system itself, from the order to the invoice to the accounting document. Architects keep landscape and process diagrams in the project repository next to the specifications. Test teams build consistent test data sets in which every document uses org units and master data that exist, because a missing customer or plant makes a test fail for the wrong reason.

The pack's cross-check is a tiny version of that last habit. When you add a document to the pack in a later topic, add its master data first, just as a real system requires.

Pitfalls

  • Treating Calder's codes as SAP defaults. Only the field names follow SAP's APIs. A real system has its own codes and its own configuration.
  • Editing one file and not the others. The pack only works because the codes agree. Run the check after every change.
  • Mixing sandbox data with Calder data. The sandbox holds SAP's demo company. Never present sandbox output as Calder's, or the reverse.
  • Pasting company data into mermaid.live. Diagrams of a real customer's landscape stay on approved tools.
  • Skipping --offline when the network is down. The sample pack doesn't need the network; don't let a blocked sandbox stop the unit.

Exercise: add a second order to the pack

  1. Open unit1b/make_calder_pack.py in VS Code.
  2. In PRODUCTS, look up the product code of the seal kit (CP-SK200) and its division (SP).
  3. In org_units.json, find which sales area sells division SP. (It is sales organization CPUS, distribution channel 30.)
  4. In business_partners.json, find which customer has that sales area. (It is 10000002, Hallam Fluid Systems, a fictional distributor.)
  5. On paper or in a new file unit1b/order_1000002_plan.md, write down a second order: Hallam orders 20 seal kits from the aftermarket channel. List its sales organization, distribution channel, division, customer, plant and shipping point, and say which file each value comes from.
  6. Add a Mermaid flowchart to the same file showing the documents this order would create, using the first order's flow as a model, and check it in the preview.
  7. Commit the file with Git.

Done when: unit1b/order_1000002_plan.md lists every code for the new order with the file it comes from, its diagram renders in the VS Code preview, and python check_unit1b.py --offline still prints All set. Keep it: later Unit 1B topics add more orders to the pack, and your plan is a head start.

Check yourself

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

1The check reports that the sales order uses an unknown customer. What happened?

Answer: C. The check compares every code the order uses with the org units and master data files. A real system enforces the same rule: a document can only use a customer that exists. Run the pack script again or fix the code it names.

2Which field in the pack shows that the delivery belongs to sales order 1000001?

Answer: C. Each delivery item's ReferenceSDDocument and ReferenceSDDocumentItem point back to the sales order and its item. SoldToParty names the customer, not the order.

3Why does every journal entry in the pack add up to zero?

Answer: C. In the pack, debits are positive and credits negative, so a balanced entry sums to zero. The check tests this for every entry, which catches an edited amount at once.

4You run code against the sandbox and see dates like /Date(1789344000000)/, but the pack shows 2026-09-14. Which is right?

Answer: D. SAP's OData V2 APIs return dates as /Date(milliseconds)/. The pack uses plain dates so you can read the timeline by eye. Convert when you compare the two.

5Your Markdown preview shows the Mermaid block as code, not a picture. What do you do?

Answer: B. As of October 2026, VS Code's built-in preview draws Mermaid. An older version needs an update or the Markdown Preview Mermaid Support extension. mermaid.live works in the browser if you can't install anything.

6The sandbox check fails with a network problem on a company laptop. Can you continue with Unit 1B?

Answer: C. The pack is written and read locally, so --offline turns the sandbox line into LATER and the unit's scripts run with --sample. Ask IT about sandbox.api.sap.com meanwhile.

7A colleague wants to show sandbox output as an example of Calder's orders. What do you tell them?

Answer: D. The sandbox holds SAP's own demo data and Calder exists only in the pack. Label sandbox output as sandbox output so readers never mix the two.

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