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.

How an SAP enterprise is wired: organizational structure

Learn the SAP org units from client to warehouse, how their assignments decide which modules share data, and why they decide what users and AI agents see.

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

The 60-second version

Before an SAP system can take a single order, someone has to describe the company to it. Which legal entities exist? Where are the factories and warehouses? Who sells, who buys, and from where do goods ship? The answers become the organizational structure: a set of codes, called org units, and the links between them.

Every business document carries some of these codes. A sales order names a sales organization and a plant. An invoice posts to a company code. A goods movement happens in a storage location. Because the codes are linked, SAP knows that a shipment from one plant belongs in one company's books.

Three things follow from that, and they are why this topic matters to anyone leading an SAP program:

  • The structure decides what can talk to what. If a plant isn't linked to a sales organization, that sales team can't ship from it.
  • The structure is hard to change later. Documents and master data are stamped with the codes for years.
  • The structure scopes access. Roles are restricted by these same codes, so they decide what a person, or an AI agent acting for them, can see.

Why it matters to the business

The org structure is a design decision with a long tail. A few examples from Calder Pumpworks, the fictional pump maker this unit follows:

  • Reporting. Calder has two legal entities, so it has two company codes: CP10 in the USA and CP20 in Germany. Each produces its own financial statements. Had Calder created a company code per product line, it would close the books, file taxes and reconcile intercompany balances for each one, every month.
  • Master data volume. Customers are maintained per sales area (a sales organization, a distribution channel and a division). Calder has four sales areas. A design with 36 would mean up to 36 sales records for a customer who buys everything, multiplied across every customer.
  • Intercompany. Calder's German plant makes components for the US plant. Because each plant belongs to exactly one company code, that flow is a sale between two legal entities, with invoices on both sides.
  • Access and AI. A blocked-order assistant for the US order desk should see US sales orders and nothing from Germany. That boundary is drawn with org units. Get the structure wrong and either the assistant sees too much, or roles become so complex that nobody can review them.

Changing a company code or merging plants after go-live usually means a project of its own, with data conversion and re-testing. The cheapest time to get it right is the design workshop.

How SAP does it

The org units themselves are the same across SAP's ERP family, with differences in how they are set up:

  • SAP S/4HANA Cloud Public Edition. As of October 2026, the structure is built in the Organizational Structure app of SAP Central Business Configuration, usually in a dedicated fit-to-standard workshop. Units sit in a staging area until confirmed; once confirmed they can't be deleted. Some finance settings, such as the fiscal year variant, can't be changed after saving (SAP Learning).
  • SAP S/4HANA (private cloud and on-premise) and SAP ECC. The structure is set up in Customizing (the system's configuration tool), including which plants each sales organization may deliver from (SAP Learning).
  • Warehousing changed most. In SAP S/4HANA, the classic ECC warehouse management (LE-WM) is not the target. SAP positions Extended Warehouse Management (EWM) and Stock Room Management instead, and LE-WM's limited use right under the compatibility package ended on 31 December 2025 (SAP Learning).

The org units on one page

Org unit What it represents Belongs to Calder (fictional)
Client The whole system's top level; settings here apply to all company codes — 100
Company code A legal entity that keeps its own books Client CP10 USA, CP20 Germany
Controlling area The boundary for internal cost accounting Holds one or more company codes CP00
Plant A site where goods are made, stored or planned Exactly one company code CP11 Rockford, CP21 Augsburg
Storage location A place where stock is kept inside a plant One plant FG01, RM01
Sales organization The unit that sells and owns the terms of sale Exactly one company code CPUS, CPDE
Distribution channel How goods reach the customer Assigned to sales orgs 10 direct, 20 distributors, 30 aftermarket
Division A product line Assigned to sales orgs PU pumps, SP spare parts
Sales area One sales org + one channel + one division — 4 combinations
Purchasing organization The unit that buys and negotiates with suppliers Optionally one company code; serves plants PUUS, PUDE
Shipping point Where outbound deliveries are processed Assigned to plants RK01, AU01
Warehouse number A warehouse managed bin by bin Adds bin-level management to a plant's stock RKDC

The two links that cause the most trouble are sales organization to plant (which plants a sales team may ship from) and purchasing organization to plant (which plants a buying team may buy for).

Questions to ask

  1. How many legal entities do we have, and is there a reason for any company code that isn't a legal entity?
  2. Which plants may each sales organization ship from, and which of those cross company codes?
  3. Is each warehouse a plant, a storage location, or a warehouse number, and why?
  4. How many sales areas will customers need, and can we reach the same reporting with fewer?
  5. Which org units will roles be restricted by, and does every AI assistant follow the user's roles?
  6. If part of the group stays on SAP ECC for a while, how do codes stay consistent across both systems?
  7. What would it cost to change this structure two years after go-live?

Common misconceptions

  • "More org units give us more detailed reporting." Most reporting needs are better met by reporting dimensions, such as profit centers or customer groups, than by new org units that every document and role must carry.
  • "A warehouse is a plant." Often it is a storage location in a plant, perhaps managed bin by bin with a warehouse number. A separate plant adds its own planning, transfers and settings.
  • "We can fix the structure after go-live." Codes are stamped on years of documents. Changes are possible but are projects in their own right.
  • "The AI assistant will see what it needs." It sees what its identity's roles allow. If those roles aren't scoped by org unit, it may see far more than its users.
  • "Org units are an IT topic." They encode legal entities, sales channels and sites. Finance, sales and operations leaders own the decisions.

Key terms

All course terms, A to Z, in the glossary

  • Org unit (organizational unit): a configured code, such as a company code or plant, that business documents refer to.
  • Assignment: a configured link between two org units, such as a plant to a company code.
  • Company code: a legal entity with its own set of books.
  • Plant: a site where goods are produced, stored or planned.
  • Sales area: a sales organization, a distribution channel and a division together. Customer sales data is kept per sales area.
  • Intercompany: a business flow between two company codes of the same group, such as a sale from one to the other.
  • Organizational level: a field in a role that takes an org unit value, such as a company code, to limit what the role can do.

Check yourself

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

1Why does the org structure matter beyond configuration?

Answer: D. Every document is stamped with org units, customer data is kept per sales area, and roles are restricted by these codes. That is why the structure is hard to change and why it shapes access.

2Calder wants a company code for each product line to get product-line profit figures. What is the best response?

Answer: C. A company code represents a legal entity with its own books. Product-line views are better met by reporting dimensions, without extra closes and intercompany reconciliation.

3Why does a design with many sales areas worry the master data team?

Answer: D. A customer who buys through several sales areas needs a sales record in each one. More sales areas means more records to create, maintain and keep consistent.

4Calder's German plant supplies the US plant. What does the org structure make this flow?

Answer: C. Each plant belongs to exactly one company code, and the plants sit in different company codes. So goods moving between them cross legal entities and need intercompany billing.

5A vendor says its AI assistant for the US order desk "sees what it needs". What should you ask?

Answer: C. Roles are restricted by org units such as sales organization. An assistant that follows the user's roles inherits that scope; one with a broad technical user can see other countries' data.

6What is a common mistake when modeling a distribution center at the same site as a plant?

Answer: C. A distribution center at an existing site is usually a storage location, possibly with a warehouse number. A separate plant adds planning, transfers and settings, so it needs a real reason.

7Which statement about SAP S/4HANA Cloud Public Edition org structure is accurate as of October 2026?

Answer: B. SAP Learning describes the Organizational Structure app in Central Business Configuration. Once units are confirmed they can't be deleted, which makes the design workshop the moment to get it right.
Deep layer · 40 min read

Mental model

Think of the org structure as the company's address system. Every document has an address made of org units: a sales order lives at sales organization CPUS, channel 10, division PU, plant CP11. The assignments between org units are the roads between addresses. A process can only travel along roads that exist.

Three consequences make the rest of this topic click:

  1. Integration follows assignments. Sales can ship from a plant only if that plant is assigned to the sales org and channel. Finance can post a delivery's cost only because the plant belongs to a company code. No assignment, no flow.
  2. Master data is stored at the addresses. A material has plant data, a customer has company code and sales area data, a supplier has company code and purchasing org data. Every extra address means more records.
  3. Authorizations are written in addresses. Roles say "may change sales orders in sales org CPUS". So the structure is also the map of who can see what, for people and for the AI agents that act for them.

If you know the address of a document, you know which module owns it, which books it lands in and who may read it.

How it works

The structure as a picture

flowchart TB
  CL[Client] --> CC[Company code]
  CC -.-> CA[Controlling area]
  CC --> PL[Plant]
  PL --> SL[Storage location]
  SL -.-> WH[Warehouse number]
  PL --> SP[Shipping point]
  CC --> SO[Sales organization]
  SO -.->|per channel| PL
  CC -.-> PO[Purchasing organization]
  PO -.-> PL

Solid lines mean "belongs to". Dotted lines mean "is assigned to" or "serves". The rules behind each line come next.

Finance: client, company code, controlling area

  • Client. The top level, with a three-digit key. Settings made at client level apply to all company codes in it (SAP Learning). Calder's US company runs on SAP S/4HANA and its German company on SAP ECC, so in practice Calder has two systems, each with its own client and its own configuration.
  • Company code. The smallest unit of external accounting, with a four-character key unique in the client. Several company codes can share a client (same source). Every financial posting names one.
  • Controlling area. The boundary for management accounting. One controlling area holds one or more company codes. Those company codes must use the same operational chart of accounts and fiscal year variant, but they may use different currencies (SAP Learning). Putting company codes in one controlling area is what lets costs be allocated across them.

Logistics: plant and storage location

  • Plant. A logistics unit seen from production, procurement, plant maintenance and materials planning. Four-character key, unique in the client (SAP Learning). Each plant belongs to exactly one company code (SAP Learning). That single link is how every goods movement finds its books.
  • Storage location. Separates stock within a plant; inventory is managed at this level. Its four-character key is unique within a plant (SAP Learning), so FG01 in Rockford and FG01 in Augsburg are two different places.

Sales: sales organization, channel, division, sales area

As of October 2026, SAP Learning states these rules for SAP S/4HANA Sales (SAP Learning):

  • Each sales organization is assigned to exactly one company code. A company code can have several.
  • A distribution channel is assigned to one or more sales organizations.
  • Several divisions can be assigned to a sales organization.
  • A sales area is one sales organization, one channel and one division. Sales employees are authorized for one or more sales areas.
  • The pair of sales organization and channel is called a distribution chain.

The link from sales to logistics is the delivering plant: plants are maintained in Customizing for each sales organization and channel. A plant can serve several of them, and the sales org and plant don't have to share a company code (SAP Learning). When they don't, the sale is intercompany: the selling company bills the customer, and the delivering company bills the seller. Intercompany and multi-company scenarios covers the postings.

Purchasing: purchasing organization and group

A purchasing organization must be assigned to at least one plant; a company code assignment is optional (SAP Learning). Three shapes follow:

Shape Company code Plants
Plant-specific One One
Cross-plant One Several, all of that company code
Cross-company-code None Plants of several company codes

A purchasing group (a buyer or team of buyers) is not assigned to any org unit. It is a label on documents, not a branch of the tree.

Shipping and warehousing

  • Shipping point. The top unit of shipping. Shipping points and plants are many-to-many, set in Customizing; one shipping point serving several plants only makes sense when they are close together. The shipping point is found per sales order item, and each outbound delivery has exactly one (SAP Learning).
  • Warehouse number. For a warehouse managed bin by bin. In SAP EWM, the warehouse number is the top of a hierarchy of storage types, sections and bins; SAP recommends one warehouse number for storage areas or buildings in the same area, and a separate one for a distant complex (same source). Plant and storage location stay the inventory view; the warehouse number adds the bin-level view on top.

How assignments decide what modules can exchange

If this assignment is missing This breaks
Plant to company code Goods movements can't post value; the plant can't be used
Sales org and channel to delivering plant Orders can't be delivered from that plant
Shipping point to plant Deliveries for that plant find no shipping point
Purchasing org to plant That team can't buy for the plant
Company code to controlling area Costs can't be allocated to or from that company

This is why org design is integration design. Every arrow in Calder's processes, from order to invoice, rides on one of these assignments.

Three design mistakes and what they cost

1. Too many company codes. A company code is a self-contained set of books. Each one adds a period close, tax reporting, and master data: customers, suppliers and G/L accounts all have company-code-level data. Flows between company codes become intercompany, with invoices on both sides and balances to reconcile. Product lines, regions or business units are usually better served by reporting dimensions.

2. Plant versus storage location confusion. A plant is where SAP plans materials and production; a storage location only separates stock. Making a dock or a cage into a plant forces stock transfers, separate planning and extra master data for every material stored there. Making a distant site into a storage location hides it from planning. Ask: does this place plan on its own, sit in a different company, or need its own settings? If not, it's a storage location.

3. Sales areas that multiply master data. Customer sales data is kept per sales area: the business partner API has a to_CustomerSalesArea record for each one, as the Unit 1B setup pack shows. A division for every product family and a channel for every route to market can turn two sales organizations into dozens of sales areas, each needing customer records, pricing and tests. Divisions and channels should reflect real differences in terms, pricing or responsibility.

Org structure scopes authorizations, and therefore AI

SAP checks authorizations with authorization objects, and many of them have org-unit fields. In role maintenance, these appear as organizational levels, such as plant, company code or business area, each given a value for the role (SAP Help Portal). In SAP S/4HANA Cloud Public Edition, the same idea appears as restrictions on business roles, covered in Agent permissions and SAP authorizations. Released authorization objects include F_BKPF_BUK, the company code check for accounting documents, and V_VBAK_VKO in sales (Cloudification Repository).

For AI, the chain is direct. An agent that reads SAP as the user, through principal propagation, sees what the user's org-level values allow. An agent that reads through a broad technical user sees everything that user can, regardless of who asked. Agent permissions and SAP authorizations and Grounding on SAP data with authorizations cover the mechanics. The point here is upstream: a clean org structure makes clean roles possible. If the US order desk and the German order desk share a sales organization, no role can separate them, and neither can an agent.

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 a script that reads Calder's org units from the sample pack and does three things. It checks the assignment rules from this topic. It draws the structure as a Mermaid diagram. And it scopes the structure: given an org unit value, it shows which of order 1000001's documents a role limited to that value would reach. That last part is the authorization idea in miniature.

flowchart LR
  A[calder_pack/<br/>org_units.json] --> S[org_structure.py]
  B[business_partners.json] --> S
  C[order_1000001.json] --> S
  S --> R[Rule check<br/>in the terminal]
  S --> D[calder_org_structure.md<br/>diagram]
  S --> V[Scope report<br/>SEES / BLOCKED]

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 30 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

If your exercise from the setup topic changed the script, this run uses your changed version. That's fine as long as python check_unit1b.py --offline still reports the pack as OK.

Step 3: Create the script

  1. In VS Code, create a new file, paste the script below, and save it as org_structure.py inside the unit1b folder.
"""Check, draw and scope Calder Pumpworks' organizational structure.

Calder Pumpworks is a fictional company. This script reads the Calder sample
pack (unit1b/calder_pack/, written by make_calder_pack.py) and needs no account,
no key and no network.

Run it from your course folder:
  python unit1b/org_structure.py                      check the rules, print a summary, draw the diagram
  python unit1b/org_structure.py --scope plant=CP11   what a role limited to plant CP11 would see
  python unit1b/org_structure.py --what-if 2 3 6      sales areas for 2 sales orgs x 3 channels x 6 divisions
It writes unit1b/calder_org_structure.md and changes nothing else.
"""
import argparse
import json
import sys
from collections import Counter
from pathlib import Path

PACK = Path("unit1b") / "calder_pack"
DIAGRAM = Path("unit1b") / "calder_org_structure.md"


def load(name: str) -> dict:
    """Read one pack file, or stop with a clear message."""
    path = PACK / name
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        sys.exit(f"Can't find {path}. Run: python unit1b/make_calder_pack.py")
    except json.JSONDecodeError as err:
        sys.exit(f"{path} is not valid JSON (line {err.lineno}). Run make_calder_pack.py again.")


def check_rules(org: dict) -> tuple:
    """Test the assignment rules. Returns (errors, notes): errors break processes, notes are design facts."""
    errors, notes = [], []
    company_codes = {c["CompanyCode"]: c for c in org["CompanyCodes"]}
    plant_cc = {p["Plant"]: p["CompanyCode"] for p in org["Plants"]}
    so_cc = {s["SalesOrganization"]: s["CompanyCode"] for s in org["SalesOrganizations"]}
    channels = {d["DistributionChannel"] for d in org["DistributionChannels"]}
    divisions = {d["Division"] for d in org["Divisions"]}

    # Rule 1: every plant belongs to exactly one company code that exists.
    for plant, cc in plant_cc.items():
        if cc not in company_codes:
            errors.append(f"plant {plant} points to unknown company code {cc}")

    # Rule 2: a storage location exists only inside a plant, and its key is unique within that plant.
    seen = Counter((s["Plant"], s["StorageLocation"]) for s in org["StorageLocations"])
    for (plant, sloc), n in seen.items():
        if plant not in plant_cc:
            errors.append(f"storage location {sloc} sits in unknown plant {plant}")
        if n > 1:
            errors.append(f"storage location {sloc} is defined {n} times in plant {plant}")
    reused = Counter(sloc for _, sloc in seen)
    for sloc, n in sorted(reused.items()):
        if n > 1:
            notes.append(f"storage location key {sloc} is used in {n} plants: fine, keys are unique per plant only")

    # Rule 3: every sales organization belongs to exactly one company code that exists.
    for so, cc in so_cc.items():
        if cc not in company_codes:
            errors.append(f"sales organization {so} points to unknown company code {cc}")

    # Rule 4: a sales area is one sales org + one channel + one division, all defined.
    for area in org["SalesAreas"]:
        so, dc, dv = area["SalesOrganization"], area["DistributionChannel"], area["Division"]
        if so not in so_cc or dc not in channels or dv not in divisions:
            errors.append(f"sales area {so}/{dc}/{dv} uses an undefined unit")

    # Rule 5: each distribution chain (sales org + channel) that sells needs at least one plant.
    chains = {(a["SalesOrganization"], a["DistributionChannel"]) for a in org["SalesAreas"]}
    chain_plants = {}
    for link in org["SalesOrgPlants"]:
        chain_plants.setdefault((link["SalesOrganization"], link["DistributionChannel"]), []).append(link["Plant"])
    for so, dc in sorted(chains):
        if not chain_plants.get((so, dc)):
            errors.append(f"distribution chain {so}/{dc} has no delivering plant")

    # Rule 6: a sales org may deliver from another company code's plant: that is intercompany.
    for (so, dc), plants in sorted(chain_plants.items()):
        for plant in plants:
            if plant not in plant_cc:
                errors.append(f"distribution chain {so}/{dc} delivers from unknown plant {plant}")
            elif plant_cc[plant] != so_cc.get(so):
                notes.append(f"{so}/{dc} delivers from plant {plant} of company code {plant_cc[plant]}: "
                             "intercompany sales, needs intercompany billing set up")

    # Rule 7: a purchasing org assigned to a company code may only serve that company code's plants.
    for po in org["PurchasingOrganizations"]:
        cc = po.get("CompanyCode")
        for plant in po["Plants"]:
            if plant not in plant_cc:
                errors.append(f"purchasing org {po['PurchasingOrganization']} serves unknown plant {plant}")
            elif cc and plant_cc[plant] != cc:
                errors.append(f"purchasing org {po['PurchasingOrganization']} belongs to {cc} "
                              f"but serves plant {plant} of {plant_cc[plant]}")
        if not cc:
            notes.append(f"purchasing org {po['PurchasingOrganization']} has no company code: cross-company-code")

    # Rule 8: shipping points and warehouses hang off plants and storage locations that exist.
    for sp in org["ShippingPoints"]:
        if sp["Plant"] not in plant_cc:
            errors.append(f"shipping point {sp['ShippingPoint']} points to unknown plant {sp['Plant']}")
    for wh in org["Warehouses"]:
        for sloc in wh["StorageLocations"]:
            if (wh["Plant"], sloc) not in seen:
                errors.append(f"warehouse {wh['Warehouse']} uses unknown storage location {wh['Plant']}/{sloc}")

    # Rule 9: a controlling area is configuration inside one system, so its company codes must share one.
    by_area = {}
    for cc in company_codes.values():
        by_area.setdefault(cc["ControllingArea"], set()).add(cc.get("System", "?"))
    for area, systems in sorted(by_area.items()):
        if len(systems) > 1:
            notes.append(f"controlling area {area} spans systems {sorted(systems)}: "
                         "a target design only; today each system needs its own")
    return errors, notes


def summary(org: dict, bps: dict) -> None:
    """Print each company code with the units under it, then the sales-area master data count."""
    for cc in org["CompanyCodes"]:
        code = cc["CompanyCode"]
        plants = [p["Plant"] for p in org["Plants"] if p["CompanyCode"] == code]
        slocs = [f"{s['Plant']}/{s['StorageLocation']}" for s in org["StorageLocations"] if s["Plant"] in plants]
        sales = [s["SalesOrganization"] for s in org["SalesOrganizations"] if s["CompanyCode"] == code]
        purch = [p["PurchasingOrganization"] for p in org["PurchasingOrganizations"] if p.get("CompanyCode") == code]
        print(f"  {code} {cc['CompanyCodeName']} ({cc['Currency']}, {cc['System']}, "
              f"controlling area {cc['ControllingArea']})")
        print(f"      plants {plants}  storage locations {slocs}")
        print(f"      sales orgs {sales}  purchasing orgs {purch}")
    rows = sum(len(b["to_Customer"]["to_CustomerSalesArea"])
               for b in bps["BusinessPartners"] if b.get("to_Customer"))
    customers = sum(1 for b in bps["BusinessPartners"] if b.get("to_Customer"))
    print(f"\n  {len(org['SalesAreas'])} sales areas defined; {customers} customers hold "
          f"{rows} sales-area records between them")


def scope(org: dict, order: dict, field: str, value: str) -> None:
    """Show which org units and order documents a role limited to one org value would reach."""
    plant_cc = {p["Plant"]: p["CompanyCode"] for p in org["Plants"]}
    if field == "company_code":
        plants = {p for p, cc in plant_cc.items() if cc == value}
        sales_orgs = {s["SalesOrganization"] for s in org["SalesOrganizations"] if s["CompanyCode"] == value}
    elif field == "plant":
        plants = {value} if value in plant_cc else set()
        sales_orgs = set()
    elif field == "sales_org":
        plants = set()
        sales_orgs = {value} if any(s["SalesOrganization"] == value for s in org["SalesOrganizations"]) else set()
    else:
        sys.exit("Use --scope company_code=..., plant=... or sales_org=...")
    if not plants and not sales_orgs:
        sys.exit(f"{value} is not a {field} in the pack.")
    print(f"\nA role whose org-level values cover {field} {value} and the units under it reaches:")
    print(f"  plants {sorted(plants) or '-'}  sales orgs {sorted(sales_orgs) or '-'}")
    so = order["SalesOrder"]
    checks = [
        ("SalesOrder " + so["SalesOrder"], "sales org", so["SalesOrganization"] in sales_orgs),
        ("OutboundDelivery " + order["OutboundDelivery"]["DeliveryDocument"], "plant",
         any(i["Plant"] in plants for i in order["OutboundDelivery"]["to_DeliveryDocumentItem"])),
        ("MaterialDocument " + order["MaterialDocument"]["MaterialDocument"], "plant",
         any(i["Plant"] in plants for i in order["MaterialDocument"]["to_MaterialDocumentItem"])),
        ("BillingDocument " + order["BillingDocument"]["BillingDocument"], "sales org",
         order["BillingDocument"]["SalesOrganization"] in sales_orgs),
    ]
    company_codes = {cc for cc in [value] if field == "company_code"}
    checks += [(f"JournalEntry {j['AccountingDocument']}", "company code", j["CompanyCode"] in company_codes)
               for j in order["JournalEntries"]]
    for doc, checked_by, visible in checks:
        print(f"  {'SEES   ' if visible else 'BLOCKED'}  {doc:<28} (checked by {checked_by})")


def what_if(sales_orgs: int, channels: int, divisions: int, customers: int) -> None:
    """How many sales areas, and customer sales-area records, a design could create."""
    areas = sales_orgs * channels * divisions
    print(f"\nWhat if: {sales_orgs} sales orgs x {channels} channels x {divisions} divisions "
          f"= up to {areas} sales areas.")
    print(f"  A customer buying in every one of them needs {areas} sales-area records;")
    print(f"  {customers} such customers would need {areas * customers:,}.")


def diagram(org: dict) -> str:
    """A Mermaid flowchart: company codes, plants, storage locations, sales and purchasing orgs."""
    fence = "`" * 3
    out = [fence + "mermaid", "flowchart TB", f'  CL["Client {org["Client"]}"]']
    for area in org["ControllingAreas"]:
        out.append(f'  CA_{area["ControllingArea"]}["Controlling area {area["ControllingArea"]}"]')
    for cc in org["CompanyCodes"]:
        c = cc["CompanyCode"]
        out.append(f'  CC_{c}["Company code {c}<br/>{cc["Currency"]}, {cc["System"]}"]')
        out.append(f"  CL --> CC_{c}")
        out.append(f"  CC_{c} -.-> CA_{cc['ControllingArea']}")
    for p in org["Plants"]:
        out.append(f'  PL_{p["Plant"]}["Plant {p["Plant"]}<br/>{p["PlantName"]}"]')
        out.append(f"  CC_{p['CompanyCode']} --> PL_{p['Plant']}")
    for s in org["StorageLocations"]:
        node = f"SL_{s['Plant']}_{s['StorageLocation']}"
        out.append(f'  {node}["Storage location {s["StorageLocation"]}"]')
        out.append(f"  PL_{s['Plant']} --> {node}")
    for w in org["Warehouses"]:
        out.append(f'  WH_{w["Warehouse"]}["Warehouse {w["Warehouse"]}"]')
        for sloc in w["StorageLocations"]:
            out.append(f"  SL_{w['Plant']}_{sloc} -.-> WH_{w['Warehouse']}")
    for sp in org["ShippingPoints"]:
        out.append(f'  SP_{sp["ShippingPoint"]}["Shipping point {sp["ShippingPoint"]}"]')
        out.append(f"  PL_{sp['Plant']} --> SP_{sp['ShippingPoint']}")
    for s in org["SalesOrganizations"]:
        out.append(f'  SO_{s["SalesOrganization"]}["Sales org {s["SalesOrganization"]}"]')
        out.append(f"  CC_{s['CompanyCode']} --> SO_{s['SalesOrganization']}")
    for link in org["SalesOrgPlants"]:
        out.append(f"  SO_{link['SalesOrganization']} -.->|ch {link['DistributionChannel']}| PL_{link['Plant']}")
    for po in org["PurchasingOrganizations"]:
        out.append(f'  PO_{po["PurchasingOrganization"]}["Purchasing org {po["PurchasingOrganization"]}"]')
        if po.get("CompanyCode"):
            out.append(f"  CC_{po['CompanyCode']} --> PO_{po['PurchasingOrganization']}")
        for plant in po["Plants"]:
            out.append(f"  PO_{po['PurchasingOrganization']} -.-> PL_{plant}")
    out.append(fence)
    return ("# Calder Pumpworks (fictional): organizational structure\n\n"
            "Solid lines: belongs to. Dotted lines: is assigned to or serves.\n\n" + "\n".join(out) + "\n")


def main() -> None:
    parser = argparse.ArgumentParser(description="Check, draw and scope Calder's org structure.")
    parser.add_argument("--scope", help="company_code=CP10, plant=CP11 or sales_org=CPUS")
    parser.add_argument("--what-if", nargs=3, type=int, metavar=("ORGS", "CHANNELS", "DIVISIONS"))
    parser.add_argument("--customers", type=int, default=500, help="customers for --what-if (default 500)")
    args = parser.parse_args()

    org, bps, order = load("org_units.json"), load("business_partners.json"), load("order_1000001.json")
    print("Calder Pumpworks (fictional) org structure\n")
    summary(org, bps)
    errors, notes = check_rules(org)
    print(f"\nRules: {len(errors)} error(s), {len(notes)} design note(s)")
    for e in errors:
        print(f"  ERROR  {e}")
    for n in notes:
        print(f"  NOTE   {n}")
    DIAGRAM.write_text(diagram(org), encoding="utf-8")
    print(f"\nWrote {DIAGRAM.as_posix()}")
    if args.scope:
        field, _, value = args.scope.partition("=")
        scope(org, order, field.strip(), value.strip())
    if args.what_if:
        what_if(*args.what_if, args.customers)
    sys.exit(1 if errors else 0)


if __name__ == "__main__":
    main()

Step 4: Check the rules and draw the diagram

  1. Run the script with no options:

    • Windows (PowerShell):

      python unit1b\org_structure.py
    • macOS / Linux:

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

Calder Pumpworks (fictional) org structure

  CP10 Calder Pumpworks Inc. (USD, S/4HANA, controlling area CP00)
      plants ['CP11']  storage locations ['CP11/FG01', 'CP11/RM01']
      sales orgs ['CPUS']  purchasing orgs ['PUUS']
  CP20 Calder Pumpworks GmbH (EUR, ECC, controlling area CP00)
      plants ['CP21']  storage locations ['CP21/RM01', 'CP21/FG01']
      sales orgs ['CPDE']  purchasing orgs ['PUDE']

  4 sales areas defined; 3 customers hold 4 sales-area records between them

Rules: 0 error(s), 3 design note(s)
  NOTE   storage location key FG01 is used in 2 plants: fine, keys are unique per plant only
  NOTE   storage location key RM01 is used in 2 plants: fine, keys are unique per plant only
  NOTE   controlling area CP00 spans systems ['ECC', 'S/4HANA']: a target design only; today each system needs its own

Wrote unit1b/calder_org_structure.md

"0 error(s)" is the result you want. The notes are design facts, not faults. The third one matters: the pack puts both companies in controlling area CP00, but CP20 still runs on ECC. A controlling area is configuration inside one system, so CP00 spanning both is the target after Germany moves to S/4HANA. Until then, each system has its own, and group cost reporting happens outside both.

  1. In VS Code's file list, open unit1b/calder_org_structure.md and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS). You should see Calder's structure as a tree, from client to storage locations, with sales and purchasing orgs beside it.

Step 5: Scope the structure the way a role would

The commands from here on are the same in PowerShell and in macOS or Linux terminals; on Windows you may also write unit1b\org_structure.py.

  1. Ask what a role limited to the Rockford plant would reach:

    python unit1b/org_structure.py --scope plant=CP11
  2. After the summary, you should see:

A role whose org-level values cover plant CP11 and the units under it reaches:
  plants ['CP11']  sales orgs -
  BLOCKED  SalesOrder 1000001           (checked by sales org)
  SEES     OutboundDelivery 80000001    (checked by plant)
  SEES     MaterialDocument 4900000001  (checked by plant)
  BLOCKED  BillingDocument 90000001     (checked by sales org)
  BLOCKED  JournalEntry 5000000001      (checked by company code)
  BLOCKED  JournalEntry 9400000001      (checked by company code)
  BLOCKED  JournalEntry 1400000001      (checked by company code)

A warehouse clerk at Rockford sees the delivery and the goods issue, but not the order's prices, the invoice or the books. Now try the German sales org:

python unit1b/org_structure.py --scope sales_org=CPDE

Every line of the scope report shows BLOCKED. That is a valid result, not an error: the US order is outside the German sales org. An AI assistant running with that user's roles would find nothing either. Try --scope company_code=CP10 to see a role that reaches everything in the US company.

Step 6: Size a sales-area design

  1. Ask how many sales areas a richer design would create, for example 2 sales organizations, 3 channels and 6 divisions:

    python unit1b/org_structure.py --what-if 2 3 6
  2. At the end you should see:

What if: 2 sales orgs x 3 channels x 6 divisions = up to 36 sales areas.
  A customer buying in every one of them needs 36 sales-area records;
  500 such customers would need 18,000.

Step 7: Save your work in Git

git add unit1b/org_structure.py unit1b/calder_org_structure.md
git commit -m "Add Calder org structure check and diagram"

What each part of the code does

Part What it does
load() Reads one pack file and stops with a clear message if it's missing or broken
check_rules() Tests the assignment rules from this topic; errors break processes, notes record design facts such as intercompany links
summary() Lists each company code with its plants, storage locations, sales and purchasing orgs, and counts customer sales-area records
scope() Turns an org unit value into the plants and sales orgs it covers, then marks each document of order 1000001 as SEES or BLOCKED
what_if() Multiplies sales orgs, channels and divisions to show how sales areas, and customer records, grow
diagram() Writes the structure as a Mermaid flowchart
sys.exit(1 if errors else 0) Ends with an error code when a rule fails, so a later script or a test can rely on it

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 find unit1b/calder_pack/org_units.json The pack hasn't been written, or you're in the wrong folder Run from orchestrate-course, then run Step 2
is not valid JSON A pack file was edited and broken Run make_calder_pack.py again; it rewrites the pack
KeyError: 'SalesOrgPlants' or a similar name Your pack is older or was changed by hand Run Step 2 to rewrite it at version 1.0
ERROR ... lines and exit code 1 A rule failed, usually after you edited org_units.json Read the message; it names the unit and the missing link
XX is not a plant in the pack The --scope value doesn't exist Use a code from the summary, such as CP11 or CPUS
error: argument --what-if: expected 3 arguments The option needs three whole numbers Write it as --what-if 2 3 6
The preview shows code, not a diagram VS Code lacks Mermaid support See Step 2 of Set up for Unit 1B

This script needs no network, so proxy or firewall problems don't apply here.

The SAP way

On a real system you don't read org units from a JSON file. You read them from the system, or design them in its configuration tools.

Reading the structure. As of the Cloudification Repository file of 21 October 2026, SAP has released CDS views for the org units in this topic: I_CompanyCode, I_ControllingArea, I_Plant, I_StorageLocation, I_SalesOrganization, I_DistributionChannel, I_Division, I_SalesArea, I_PurchasingOrganization and I_ShippingPoint. The underlying tables, such as T001 (company codes) and TVKO (sales organizations), are marked not to be released, with those views as successors (Cloudification Repository). The same file lists the old function module BAPI_COMPANYCODE_GETLIST as not to be released, with I_CompanyCode as its successor. That is the clean-core rule from The SAP data model for AI engineers applied to configuration data.

For remote reads, SAP publishes OData services for some org units. The SAP Cloud SDK packages generated from them (version 2.1.0) show API_COMPANYCODE_SRV, whose A_CompanyCode entity carries ControllingArea, ChartOfAccounts, FiscalYearVariant and CreditControlArea, and API_SALESORGANIZATION_SRV, whose A_SalesOrganization entity carries CompanyCode and IntercompanyBillingCustomer (npm). Check each service on the SAP Business Accelerator Hub for your edition before relying on it.

Designing the structure.

  • SAP S/4HANA Cloud Public Edition: the Organizational Structure app in SAP Central Business Configuration. It derives the units needed from the activated scope and countries, checks mandatory links such as a distribution chain having at least one plant, and keeps units in staging until confirmed (SAP Learning).
  • SAP S/4HANA private cloud and on-premise, and SAP ECC: Customizing, for example the delivering plants per sales org and channel (SAP Learning).

Licensing. Org units aren't licensed separately; they come with the ERP. Warehouse options differ: SAP Learning states that Stock Room Management's license is included in the SAP S/4HANA Enterprise Management component (SAP Learning). Check EWM licensing with SAP for your contract.

Build vs. SAP

Need Build your own Use SAP Choose
Define the structure Spreadsheets and diagrams Central Business Configuration or Customizing SAP, always; your diagrams document it
Check a proposed design A script like this topic's Central Business Configuration's own checks (Public Edition) Both: your checks catch design rules SAP can't know, such as "no company code without a legal entity"
Read the structure into an app Copies in your own database Released views and APIs (I_Plant, API_COMPANYCODE_SRV) SAP sources; cache only with a refresh rule
Scope an agent's data Filters in your code The user's SAP roles via principal propagation SAP roles; code filters only as an extra layer

Architecture view

Where it sits. Org units are configuration inside each ERP system. At Calder, CP10 lives in the S/4HANA system and CP20 in the ECC system, so the "one" org structure is really two, kept consistent by agreement. Everything else reads them: BTP apps, the data warehouse and any AI assistant.

flowchart LR
  S4[S/4HANA<br/>CP10, CP11, CPUS] --> W[Data warehouse<br/>group view]
  ECC[ECC<br/>CP20, CP21, CPDE] --> W
  S4 -->|released views, APIs| APP[BTP app or agent]
  U[User roles<br/>org-level values] -.-> S4

Non-functional requirements that decide the design

Requirement Typical target If missed
Access separation Each country's sales and finance data reachable only by its roles Agents and users see other entities' data; audit findings
Master data volume Customer sales-area records sized before go-live Data migration and maintenance costs grow with every sales area
Changeability No org unit change needed for normal growth (new customer, new product) A new product line forces a new division and new records everywhere
Consistency across systems Same codes and meanings in S/4HANA, ECC and the warehouse Group reports double-count or miss entities
Close effort One period close per legal entity Extra company codes add closes and intercompany reconciliation

Integration and identity. The org units travel on every document: the sales order's SalesOrganization, the delivery item's Plant, the journal entry's CompanyCode. Integration maps them, for example when ECC's CP20 sends components to S/4HANA's CP10. The user's identity reaches SAP through principal propagation, and SAP applies the org-level values in the user's roles. An app that needs the structure itself reads the released views or APIs above, never the tables.

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

  • Context: Calder needs cross-company cost allocation between the US and Germany. CP20 stays on ECC for at least a year.
  • Options: (a) one controlling area CP00 for both, after CP20 moves to S/4HANA; (b) one controlling area per company code, permanently; (c) a group view built only in the data warehouse.
  • Decision: (a) as the target, with (c) until the move. Both company codes already share the chart of accounts and fiscal year variant, which (a) requires.
  • Consequences: Cross-company allocation waits for the migration. Until then, group cost views come from the warehouse, and its mapping of org codes must be maintained.

What a design review will ask

  1. Is every company code a legal entity, and if not, why does it exist?
  2. Which sales org and plant combinations cross company codes, and is intercompany billing set up for each?
  3. How many customer sales-area records will migration create?
  4. Which org-level fields will roles use, and does every agent run with the user's roles?
  5. How are codes kept consistent between S/4HANA, ECC and the warehouse during the hybrid period?

Production concerns

  • Security and authorizations. Org units are the coarse filter in almost every role. Design roles alongside the structure, test them with users from each company code, and treat any technical user without org restrictions as high risk. For agents, see Agent permissions and SAP authorizations.
  • Evaluation. For an AI assistant, add test cases that ask for another entity's data. The right answer is "I can't see that", and your eval set should check for it.
  • Cost. Every org unit adds master data, test cases and role variants. Ask for the business reason before adding one.
  • Operations. Changes to org units are transports with wide impact. Treat them like data model changes: impact analysis, regression tests, and sign-off from finance and the authorization team.
  • Clean core. Read org units through released views and APIs. Don't copy them into custom tables that drift from the system.

Pitfalls

  • Designing the structure around today's organization chart. Teams reorganize more often than legal entities change. Use org units for legal and logistics facts; use reporting dimensions and roles for teams.
  • Forgetting the delivering plant assignment. Orders fail at delivery, not at entry, so the gap shows up late in testing.
  • Same code, different meaning across systems. FG01 may be finished goods in one plant and a quality hold in another. Document meanings, not just codes.
  • Testing only the main path. Test every allowed sales org and plant combination, especially intercompany ones.
  • Letting an agent use a technical user. It erases the org-level boundaries this topic is about.

Exercise

Write Calder's org structure rationale, the deliverable a solution architect brings to a design review.

  1. Run python unit1b/org_structure.py and confirm it prints 0 error(s).
  2. Create unit1b/calder_org_rationale.md in VS Code.
  3. Copy the Mermaid block from unit1b/calder_org_structure.md into it, under a heading Structure.
  4. Under a heading Decisions, add a table with columns Decision, Choice, Why, Alternative rejected. Fill in one row each for: number of company codes, controlling area, Rockford distribution center (plant or storage location), divisions, distribution channels, purchasing organizations.
  5. Run python unit1b/org_structure.py --what-if 2 3 6 and add one sentence on why Calder's design keeps sales areas few. Label the number as synthetic.
  6. Run python unit1b/org_structure.py --scope sales_org=CPUS and add a short Access section: which documents of order 1000001 a US order-desk role would reach, and what that means for an AI assistant on that desk.
  7. Add one line on the ECC difference: what changes for CP20 when it moves to S/4HANA.
  8. Preview the file to check the diagram renders, then commit it with Git.

Done when: unit1b/calder_org_rationale.md shows the rendered diagram, a decisions table with six rows, a labeled synthetic sales-area figure, an access section based on your scope run, and one ECC line, and fits on one printed page. Keep it: the process topics in Unit 1B refer to these units, and the intercompany topic builds on your decisions.

Check yourself

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

1A new Calder sales org CPMX takes orders, but deliveries from plant CP11 fail. What is the most likely cause?

Answer: A. Delivering plants are maintained per sales org and distribution channel. Without that assignment, orders can't be delivered from the plant, and the gap often shows only at delivery.

2Why can storage location FG01 exist in both Rockford and Augsburg?

Answer: B. SAP Learning states the storage location key is unique within a plant. CP11/FG01 and CP21/FG01 are two different places, which is why the script reports it as a note, not an error.

3What must company codes share to sit in one controlling area?

Answer: D. SAP Learning lists the shared chart of accounts and fiscal year variant; currencies may differ. That is why Calder's USD and EUR companies can share CP00 once both are in one system.

4The script's scope run for plant CP11 blocks the sales order but shows the delivery. Why?

Answer: C. Each document carries its own org units, and access is checked against those. The model shows how one role can see logistics documents while prices and invoices stay hidden.

5Why does the topic recommend reading org units through I_CompanyCode rather than table T001?

Answer: A. The Cloudification Repository lists T001 as not to be released, with I_CompanyCode as its successor. Released objects are the stable, clean-core way to read SAP data.

6A partner proposes 4 sales orgs, 4 channels and 5 divisions. What should you check first?

Answer: D. Up to 80 sales areas means customer sales data, pricing and tests for each combination customers use. The --what-if option sizes that before anyone builds it.

7What would you do if an AI agent for Calder's US order desk could read German sales orders?

Answer: D. A prompt instruction is not an access control. Running with the user's identity applies the org-level values in their roles, so German orders are blocked by SAP itself.

8Calder's German company stays on ECC for a year. What is true of controlling area CP00 in the pack?

Answer: B. A controlling area is configuration inside one system, so it can't span S/4HANA and ECC. The decision record makes CP00 the target and uses the data warehouse for group cost views meanwhile.

Practice this in the lab

Orchestrate Lab is a free practice ERP in your browser, built around the fictional Calder Pumpworks. These are the places in it where you do what this topic teaches.

Stages build on each other: a stage link opens the core path at that stage. To start there without doing the earlier stages yourself, use Do this stage for me on the stage before it. A chapter starts inside your own company once the core path is done (or the lab finishes the core for you first); drills and exercises start from their own prepared copy. Everything is saved in this browser only.

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