Maintain to settle is everything between "this machine needs work" and "the cost of that work is booked where it belongs". In SAP it is the plant maintenance process, now called asset management: a need is reported, a planner turns it into an order, people and parts are lined up, the work is done and recorded, and the order's costs are settled.
Three objects carry it:
Technical objects.Functional locations are the places in a plant, arranged as a tree. Equipment is the physical thing installed there: a pump, a crane, a compressor. History and costs collect on them.
Notifications. A notification reports a problem or a request. When it is closed, it should say which part failed, how, and why, using standard codes.
Orders. A maintenance order plans the work: operations, hours, spare parts and services. It collects actual costs and then settles them, usually to the equipment's cost center, sometimes to a fixed asset when the work adds value.
Preventive work comes from maintenance plans: every twelve months, every 500 operating hours, or when a reading crosses a limit.
Three ideas carry this topic:
Parts decide the schedule. A job with no seal kit on the shelf doesn't happen on the planned day, however well it was planned.
History is only as good as the coding. A malfunction closed with "other" teaches nobody anything.
Every order must end in finance. An order that is finished but never settled leaves costs nowhere, and the asset's real cost of ownership stays unknown.
Maintenance decides whether machines are available when production needs them, how safe the plant is, and how long assets last. Operations watches downtime and the backlog; finance watches maintenance cost per asset and what gets capitalized.
Here is Monday morning, 26 October 2026, at Calder Pumpworks, the fictional pump maker this unit follows. The maintenance planner for the Rockford plant (CP11) sees this:
What the worklist shows
What it means
Who acts
The annual test bench overhaul on 9 November needs a seal set, and none is in stock
The outage that production already planned around may run long
Maintenance planner, with the buyer
A pressure transducer for the same job is still only a requisition
It can't arrive before the work starts
Buyer
The air compressor's vibration crossed its limit four days ago
A failure is coming, and nobody has looked at the request yet
Maintenance gatekeeper
The crane's brake slips again, eleven days after it was fixed
The first repair treated the symptom, not the cause
Maintenance supervisor
A finished September job still carries 1,240.00 USD
The compressor has no cost center, so the order can't settle
Cost accountant, with master data
A test bench leak was closed with damage code "other"
The bench has had five leaks this year; the codes show two
Maintenance supervisor
The business decision is not "do more preventive maintenance". It is whether parts, readings, codes and settlement are reliable enough that the plan can be trusted, because everything else, including any AI, reads them.
As of October 2026, SAP S/4HANA covers maintain to settle as standard, under the name SAP S/4HANA Asset Management. SAP's training describes the classic corrective flow: a notification on a technical object, an order with operations and materials, availability and capacity checks before release, goods withdrawals, time confirmations on the order and technical findings on the notification, technical completion, settlement by controlling and business completion (SAP Learning).
S/4HANA adds a phase model with nine phases, from initiation and screening by a maintenance gatekeeper through planning, approval, preparation, scheduling, execution and post execution to completion. SAP's Public Edition training ties it to the reactive, proactive, improvement, and operational and overhead maintenance processes (SAP Learning, Public Edition). The order type decides whether an order runs through the phases (SAP Learning, Public Edition).
Around the core, SAP offers:
SAP Service and Asset Manager, a mobile app for technicians that works offline and records time, readings, notifications and parts. It needs its own licence and runs on SAP BTP (SAP Learning).
SAP Asset Performance Management (APM) for asset health: it takes sensor data and readings, evaluates rules and posts maintenance notifications into S/4HANA (SAP Learning).
For AI, SAP's training describes order recommendations: when a planner creates an order from a notification, a Get Recommendations button suggests orders from similar past notifications. SAP lists it as of SAP S/4HANA 2025 FPS1, with a trained model in the Personalized Recommendation service on SAP BTP (SAP Learning). SAP's store also lists a Maintenance Planner Agent that suggests backlog and schedule changes and is consumed with AI Units; its page offers beta registration, so check its status and licensing before you plan on it (SAP). Where AI creates value in S/4HANA places these in the wider picture.
flowchart LR
P[Maintenance plan<br/>or reading] -.-> N
N[Notification<br/>request] --> S[Screening]
S --> O[Order<br/>planning, approval]
O --> R[Parts reserved<br/>or requested]
R --> E[Execution<br/>parts issued]
E --> C[Confirmations<br/>time, findings]
C --> T[Technical<br/>completion]
T --> ST[Settlement<br/>cost center or asset]
Step
Team
Document created
Posts to finance?
Need is reported
Operator, sensor rule, maintenance plan
Notification (maintenance request or malfunction report)
No
Request is screened
Maintenance gatekeeper
Accepted notification
No
Work is planned and approved
Maintenance planner, approver
Maintenance order with operations and components
No
Parts are lined up
Planner, stores, buyer
Reservations; purchase requisitions and orders for non-stock parts
No (commitments only)
Parts are issued or received
Stores
Goods issue or goods receipt (material document)
Yes: part cost to the order
Work is recorded
Technician
Time confirmation; damage and cause codes on the notification
"Preventive plans run themselves." A time-based plan does. A counter-based plan is only as current as its last reading.
"The failure code is paperwork." It is the only part of the history a report can count. Free text helps the next technician; codes help the reliability engineer.
"Technically complete means finished." The work is done, but the costs still sit on the order until settlement moves them.
"All maintenance is expense." Work that improves an asset can be settled to it and capitalized. That is an accounting decision, made with finance, not by the technician.
"An AI agent can create orders from sensor alerts." It can draft a notification and suggest codes and parts. Accepting the request and releasing work stays with people.
Pick one answer for each question. The explanation appears after you choose.
1Calder's test bench overhaul is planned in detail, but the seal set isn't in stock. What should the planner do first?
Answer: A. Parts decide the schedule. An order released without its parts stops on the day, so the planner either gets the part in time or moves the work, and tells production.
2The bench had five leaks this year, but reports show two. What went wrong?
Answer: D. Reports count codes, not free text. Leaks closed as "other" disappear from the history, so nobody sees that the bench has a seal problem.
3A finished job still carries 1,240.00 USD a month later. What is the usual cause?
Answer: A. Settlement needs a rule, usually proposed from the equipment's cost center. Without a receiver the order can't settle, and the costs sit on it, invisible to the department that caused them.
4Calder replaces the crane's hoist motor with a larger one. How can that cost end up on the crane's fixed asset?
Answer: C. Settlement decides where costs go. An improvement order can carry a settlement rule to a fixed asset, so the cost is capitalized and depreciated; finance decides whether the work qualifies.
5A counter-based plan should call the bench's 500-hour service, but the last reading is weeks old. What is the risk?
Answer: B. Counter-based plans compute dates from readings. Without recent readings the projected date drifts from reality, and the bench runs past its service interval.
6A vendor pitches an agent that turns sensor alerts straight into released maintenance orders. What should you ask for?
Answer: C. An agent can draft the notification, suggest codes and find parts. Accepting work, committing people and parts, and releasing orders stay with the gatekeeper and planner.
7Calder wants technicians to record time and readings offline at the machine. Which SAP offering fits?
Answer: A. SAP Service and Asset Manager is the mobile app for technicians and works offline. APM evaluates asset health, and planning buckets group work for planners.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Think of maintain to settle as a ledger kept for each machine: what happened to it, what was done, and what it cost.
The technical ledger lives on the equipment and its notifications. Each malfunction report adds a line: which part, what damage, what cause, what was done, how long the machine was down. Plans and readings add the expected lines: service due at 2,000 hours, inspection due in October.
The money ledger lives on the orders. Each order collects labor, parts and services, then settles them to the machine's cost center, to a project, or to the fixed asset itself when the work added value.
Every worklist and every AI idea in this topic reads one of those ledgers. A missing part puts the next entry at risk. A missing code leaves a blank line in the technical ledger. An unsettled order leaves the money ledger open. Most maintenance problems that look like planning problems are, underneath, gaps in one of the two.
#Technical objects: functional locations and equipment
A functional location is a node in a hierarchy that represents a technical system, a building or part of one, structured by space, technology or process. Its label follows a structure indicator, which sets the allowed characters and where each level ends; labels can be up to 40 characters (SAP Learning).
Equipment is an individual physical object maintained as its own unit, such as a pump or a motor. It is installed at a functional location if its equipment category and the location allow it, and the usage list records where it has been installed over time. Equipment can also sit under other equipment. Assemblies structure an object further as BOM items rather than as objects of their own, and a maintenance BOM lists the spare parts that matter for maintenance (SAP Learning).
The equipment record's organization data holds the account assignment, such as company code and cost center, and the maintenance planning plant (SAP Learning). In SAP's model of the Equipment API, that includes CostCenter, MasterFixedAsset and FixedAsset, MaintenancePlanningPlant, MainWorkCenter, ABCIndicator, CatalogProfile, FunctionalLocation and SuperordinateEquipment (SAP Cloud SDK model, 2022). The FixedAsset field is the link to asset accounting: the machine the technician sees and the asset finance depreciates are two records joined by that field.
At Calder (fictional), the Rockford plant has this structure. The labels are the course's own:
Functional location
Equipment
Cost center
Fixed asset
ABC
CP11-ASY-TB1 test bench position 1
10000101 pump test bench TB-1, with 10000102 flow meter under it
CP11-420 (the TEST1 work center's cost center)
30000017
A
CP11-ASY-CRN crane runway
10000103 overhead crane 10 t
CP11-410
30000009
A
CP11-UTL-AIR compressed air station
10000104 air compressor C-2
none
30000021
B
The compressor's empty cost center is deliberate. It is the root of one of the exceptions you will find.
Notifications are coded from catalogs. SAP's training names the standard catalog types: B object parts, C damage, 5 causes, A activities and 2 tasks, with R to Z free for your own. Each catalog is split into code groups, each holding codes with a short text. A catalog profile, set up per kind of object such as pumps or motors, picks the code groups a user sees. Profiles are assigned to technical objects and to notification types; if both have one, the technical object's wins (SAP Learning).
The profile is the main lever on coding quality. A technician facing 300 codes picks "other". One facing the eight codes that fit a test bench picks the right one.
A notification records a malfunction or a requirement against a technical object (SAP Learning). Its header carries the type, technical object, priority, text, breakdown flag and dates; its items carry the coded findings. In SAP's model of the Maintenance Notification API, the header has NotificationType, TechnicalObject, MaintPriority, NotificationText, MaintenanceObjectIsDown, MalfunctionStartDate and MaintenanceOrder; items have MaintNotifObjPrtCode and MaintNotificationDamageCode with their code groups; causes have MaintNotificationCauseCode (SAP Cloud SDK model, 2022).
With the phase model, a requester creates a maintenance request in the initiation phase, and a maintenance gatekeeper screens it: accept, reject or ask for more information. An accepted request becomes a notification for the planner (SAP Learning, Public Edition). Two shortcuts skip steps: emergency work accepts the request without screening and creates and approves the order in the background, and minor work uses only a notification, with no order (SAP Learning).
Notification types such as maintenance request and malfunction report are configuration; this topic uses M1 and M2 as course labels.
The order plans the work and collects its costs. SAP's training lists its parts (SAP Learning):
Part
Holds
Calder example (fictional)
Header
Order type, description, dates, priority, reference object, responsible people
4000123 annual test bench overhaul on 10000101, 9 to 11 November
Object list
Further technical objects or notifications the same work covers
None
Operations
Tasks, the work center doing them, planned hours
20 h at MECH1 (mechanics), 6 h at ELEC1 (electricians)
Components
Spare parts, stock or non-stock
1 seal set CP-SP-TBSEAL, 2 bearings CP-BRG6310, 1 pressure transducer (non-stock)
Production resources and tools
Things used but not consumed, such as tools or trucks
None in this scenario
Settlement rule
Who bears the cost; proposed from the reference object
100% to cost center CP11-420
Costs
Estimated, planned and actual costs by value category
Planned 3,768.00 USD
In the phase model, a new order starts in planning. Whether it needs approval is set per order type; approval sets the baseline cost to the planned cost. Approved orders go to preparation, where the planner groups work into planning buckets, levels the load and checks parts and services; then scheduling, where operations are dispatched; then execution (SAP Learning, Public Edition). Phase control codes can block an order from moving to a sub-phase, but they don't stop a technician from reporting time, and they never block goods receipts or service entry sheets.
Order types, their numbering and whether they run the phase model are configuration. This topic uses PM01 corrective, PM02 preventive and PM03 improvement as course labels.
This is where maintenance meets inventory and procurement (SAP Learning):
Stock parts are reserved in the warehouse. Customizing per order type decides whether the reservation counts for planning at once or only at release. Planned withdrawals reference the reservation; unplanned ones reference the order.
Non-stock parts are bought for the order. A purchase requisition is created when the order is saved or released, depending on the order type. The purchase order is account-assigned to the order, so its goods receipt charges the order, and invoice differences are posted to it automatically.
An availability check runs when components are assigned and automatically at release, but an order can be released without its parts. At release at the latest, reservations become relevant to MRP and requisitions are created.
S/4HANA's availability check for maintenance can hand stock to orders by final due date, priority, ABC indicator and creation time, and a planner can keep a commitment so it isn't reassigned (SAP Learning). In the Maintenance Order API model, each component carries Reservation, MaintOrdOpCompRequiredQuantity, RequirementDate, ReservationIsFinallyIssued, MaintOrdOpCompItemCategory, PurchaseRequisition and DeliveryTimeInDays (SAP Cloud SDK model, 2022).
Technicians record two kinds of facts (SAP Learning):
Time confirmations on the order's operations: actual work, start and end, final or partial. Each one charges the order with hours times the work center's activity rate. In the Confirmation API model these are ActualWorkQuantity, IsFinalConfirmation, RemainingWorkQuantity, ActivityType and PersonnelNumber (SAP Cloud SDK model, 2022).
Technical findings on the notification: damage and where, cause, activities performed, and the machine's downtime (SAP Learning).
Hours can also come from time sheets. SAP's older documentation describes transferring Cross-Application Time Sheet (CATS) records to plant maintenance as confirmations, with an error pool for records that fail (SAP Help, R/3). Mobile apps post confirmations directly.
Technical completion sets the order to TECO. Requisitions without a purchase order get a deletion flag, open reservations and capacities are closed, and, if no settlement rule exists, the system tries to create one (SAP Learning). With the phase model, the planner can first set Main Work Completed to move the order into post execution (SAP Learning, Public Edition).
A maintenance order can't be a permanent cost object, so its costs are moved to target objects, leaving a zero balance. The settlement rule says where. The usual receiver is the cost center of the reference object; a WBS element or a fixed asset is also possible, as the order type's settlement profile allows. Settlement covers stock issues, non-stock receipts, time confirmations, accepted service entry sheets and overhead. It runs per ledger, usually as a background job. For phase-based order types, an order must be fully settled before business completion; for others, closing first leaves costs that can no longer be settled (SAP Learning, Public Edition). The settlement rule entity in SAP's API model carries SettlementType, AccountAssignmentType, SettlementPercentageRate, CostCenter, WBSElementInternalID, MasterFixedAsset and FixedAsset (SAP Cloud SDK model, 2022).
That is how maintenance reaches asset accounting:
Repairs settle to a cost center and become expense in that period.
Improvements that finance agrees to capitalize can settle to the fixed asset, which increases its value and its depreciation.
Larger capital work usually runs as a project: costs collect on an investment WBS element and settle at month end to an asset under construction (AuC). An AuC isn't depreciated; when the asset is ready, full settlement credits the AuC and debits the final assets (SAP Learning).
Whether a given job is capital or expense is an accounting decision under the company's policy. The system only carries it out.
Preventive work comes from maintenance plans. Scheduling, usually a background job, starts plans, recalculates due dates and creates call objects: maintenance orders, notifications or service entry sheets. The call horizon says how early the call is made, as a percentage of the cycle or as days before the planned date; at 80% of a 90-day cycle, the call comes on day 72. Shift factors say how much an early or late completion moves the next date, and the completion requirement holds the next call until the previous one is complete (SAP Learning).
Plans can be time-based, performance-based or strategy plans with several cycles. A performance-based plan computes dates from counter readings and an estimated annual performance, and planned dates are recalculated with each new measurement document (SAP Learning). In the Maintenance Plan API model, a plan has MaintenanceStrategy, CallHorizonPercent, SchedulingDuration, LateCompletionShiftInPercent and PrdcssrCallObjCompltnIsRqd, and each maintenance item names the technical object, task list and order type (SAP Cloud SDK model, 2022).
Measuring points describe a condition at a technical object; counters track wear or use, such as operating hours. Each reading is a measurement document, entered by hand, from mobile apps, by barcode or from a process control system. Damage found during an inspection can trigger a malfunction report automatically (SAP Learning).
For condition-based work, SAP describes two routes. In SAP S/4HANA Cloud, release 2202 added limits and tolerances on measuring points that create a notification with a set type and priority when a reading crosses them (SAP Community, 2022). For richer rules and sensor data, SAP Asset Performance Management takes time-series data and readings, evaluates rules and posts maintenance notifications into S/4HANA; it also replicates failure modes and causes as catalog codes (SAP Learning).
flowchart LR
N[Notification<br/>10000452] --> O[Order 4000121]
O --> RS[Reservation<br/>seal set]
RS --> GI[Goods issue]
GI --> J1[Journal<br/>inventory to order]
O --> CF[Time confirmation<br/>6.5 h MECH1]
CF --> J2[Labor cost to order,<br/>maintenance cost<br/>center credited]
O --> T[TECO]
T --> ST[Settlement]
ST --> J3[Journal<br/>order to CP11-420]
Step
Posts to finance
Why
Notification, order, approval, release
Nothing
Plans and commitments
Purchase requisition and purchase order for a non-stock part
Nothing (a commitment on the order)
Money is promised, not spent
Goods issue of a stock part
Inventory leaves stock and is charged to the order
The part is used up
Goods receipt of a non-stock part
Charged straight to the order through the PO's account assignment
It was bought for this order
Service entry sheet accepted
External service cost to the order
The service was performed
Time confirmation
Hours times activity rate to the order; the maintenance work center's cost center is credited
Labor was spent
Technical completion
Nothing
A status
Settlement
Order credited; cost center, WBS element or fixed asset debited
The costs reach their final owner
Asset under construction settled
AuC credited, final asset debited
A capital project is finished
At Calder (fictional), order 4000121 fixed a test bench leak with one seal set from stock (420.00 USD) and 6.5 hours of a mechanic at 95.00 USD per hour (617.50 USD). At the end of October it settles 1,037.50 USD to cost center CP11-420, the bench's cost center. That is the same cost center that carries the TEST1 work center in Plan to produce end to end, so maintenance cost flows into the test activity rate that production orders pay. Order 4000108, which put a larger hoist motor on the crane in September, settled 9,360.00 USD to fixed asset 30000009 instead. You compute both in Step 6.
As of 24 October 2026, these released interfaces carry each step. The CDS views come from SAP's Cloudification Repository (file read 24 October 2026), which also marks the classic order header table AUFK and reservation table RESB as not to be released, with I_Order and I_ReservationDocumentItem as successors (Cloudification Repository). The OData API names, entities and fields come from SAP's generated API models, published in 2022 (SAP Cloud SDK models, 2022).
Measuring point and measurement document APIs (OData V4)
I_MeasuringPoint, I_MeasurementDocument
Counter readings, limits, valuation codes
Assets
I_FixedAsset
The asset the equipment points to
The functions that release, complete or change orders and notifications change SAP data. Use them only in a design where a person triggers the action. For events, check the event catalog on the SAP Business Accelerator Hub; Event-driven agents with SAP events covers how to react to them.
You will write one script that plays the Rockford maintenance planner's Monday on 26 October 2026. It prints the plant's technical structure, schedules three maintenance plans from cycles and counter readings, builds a ranked worklist of maintenance exceptions, traces two orders from notification to settlement, and measures how well failures are coded. An optional step reads maintenance notifications from SAP's sandbox.
flowchart LR
P[calder_pack/<br/>plants, suppliers] --> S[maintain_to_settle.py]
SC[Scenario in the script<br/>technical objects, plans,<br/>notifications, orders] --> S
S --> T[Structure<br/>--tree]
S --> PL[Plan dates<br/>--plans]
S --> W[Ranked worklist]
S --> O[Order trace<br/>--order]
S --> C[Coding report<br/>--coding]
S --> M[calder_maintenance_worklist.md]
K[SAP sandbox<br/>optional] --> S
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
The last lines should read:
Calder sample pack 1.0: order 1000001, net 29,100.00 USD, cost 19,600.00 USD.
All data is fictional. Next: python check_unit1b.py
The maintenance script checks that plant CP11 belongs to company code CP10 and that supplier 20000001 (Orrin Motor Works) is in the pack, because its scenario uses both.
In VS Code, create a new file, paste the script below, and save it as maintain_to_settle.py inside the unit1b folder.
Read the blocks FUNCTIONAL_LOCATIONS, EQUIPMENT, MEASURING_POINTS, READINGS, NOTIFICATIONS, HISTORY, ORDERS and PLANS first. They are Calder's maintenance scenario written as Python data. Every value is invented. Work centers TEST1 and ASSY1 and their cost centers come from the master data topic, maintenance order 4000123 and its dates from the plan-to-produce topic, and Corvane Lifting Services from the procure-to-pay topic. Notification types, order types, catalog codes, labels and activity rates are the course's own, not SAP values.
"""Maintain to settle at Calder: technical objects, plans, orders, settlement and the planner's worklist.
Calder Pumpworks is a fictional company. The pack (unit1b/calder_pack/) and the
scenario below (technical objects, notifications, orders, plans, readings, costs)
are made up for the course. Field names follow SAP's released APIs where the
script mirrors one (API_EQUIPMENT, API_MAINTNOTIFICATION, API_MAINTENANCEORDER,
API_MAINTENANCEPLAN, measuring point and measurement document APIs), but the
logic is a teaching sketch, not SAP's. Notification types, order types, catalog
codes, structure labels and activity rates are course labels, not SAP values.
Run it from your course folder:
python unit1b/maintain_to_settle.py the maintenance planner's worklist for 2026-10-26
python unit1b/maintain_to_settle.py --tree functional locations, equipment and their assets
python unit1b/maintain_to_settle.py --plans preventive and counter-based plans: what is due when
python unit1b/maintain_to_settle.py --order 4000121 trace one order from notification to settlement
python unit1b/maintain_to_settle.py --coding how well failures are coded, with a keyword baseline
python unit1b/maintain_to_settle.py --write write unit1b/calder_maintenance_worklist.md
python unit1b/maintain_to_settle.py --sandbox count maintenance notifications in SAP's sandbox (needs SAP_API_KEY)
Steps without --sandbox use only Python's built-in modules and need no account and no network.
"""
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import date, timedelta
from pathlib import Path
HERE = Path(__file__).resolve().parent
PACK = HERE / "calder_pack"
AS_OF = date(2026, 10, 26) # Monday morning: the maintenance planner's review
PLANT = "CP11"
PARTS_WINDOW_DAYS = 21 # check parts for orders that start within this many days
REWORK_WINDOW_DAYS = 30 # a new order on the same equipment this soon after completion is rework
STALE_READING_DAYS = 14 # a counter reading older than this makes the plan's dates a guess
# ------------------------------------------------------- technical objects (fictional)
# Functional locations: places in the plant where equipment is installed.
# The labels follow a course structure: plant - area - position.
FUNCTIONAL_LOCATIONS = [
{"FunctionalLocation": "CP11", "FunctionalLocationName": "Rockford plant", "SuperiorFunctionalLocation": ""},
{"FunctionalLocation": "CP11-ASY", "FunctionalLocationName": "Assembly and test hall", "SuperiorFunctionalLocation": "CP11"},
{"FunctionalLocation": "CP11-ASY-TB1", "FunctionalLocationName": "Test bench position 1", "SuperiorFunctionalLocation": "CP11-ASY"},
{"FunctionalLocation": "CP11-ASY-CRN", "FunctionalLocationName": "Overhead crane runway", "SuperiorFunctionalLocation": "CP11-ASY"},
{"FunctionalLocation": "CP11-UTL", "FunctionalLocationName": "Utilities", "SuperiorFunctionalLocation": "CP11"},
{"FunctionalLocation": "CP11-UTL-AIR", "FunctionalLocationName": "Compressed air station", "SuperiorFunctionalLocation": "CP11-UTL"},
]
# Equipment: the physical things that are maintained, after API_EQUIPMENT.
# CostCenter is the default settlement receiver; FixedAsset links to asset accounting.
EQUIPMENT = {
"10000101": {"EquipmentName": "Pump test bench TB-1", "FunctionalLocation": "CP11-ASY-TB1",
"SuperordinateEquipment": "", "CostCenter": "CP11-420", "FixedAsset": "30000017",
"ABCIndicator": "A", "CatalogProfile": "PUMPTB", "WorkCenter": "TEST1"},
"10000102": {"EquipmentName": "Flow meter FM-1", "FunctionalLocation": "CP11-ASY-TB1",
"SuperordinateEquipment": "10000101", "CostCenter": "CP11-420", "FixedAsset": "",
"ABCIndicator": "A", "CatalogProfile": "PUMPTB", "WorkCenter": "TEST1"},
"10000103": {"EquipmentName": "Overhead crane 10 t", "FunctionalLocation": "CP11-ASY-CRN",
"SuperordinateEquipment": "", "CostCenter": "CP11-410", "FixedAsset": "30000009",
"ABCIndicator": "A", "CatalogProfile": "CRANE", "WorkCenter": "ASSY1"},
"10000104": {"EquipmentName": "Air compressor C-2", "FunctionalLocation": "CP11-UTL-AIR",
"SuperordinateEquipment": "", "CostCenter": "", "FixedAsset": "30000021",
"ABCIndicator": "B", "CatalogProfile": "COMPR", "WorkCenter": ""},
}
# Measuring points, after the measuring point API. Limits are course values.
MEASURING_POINTS = {
"MP-101": {"Equipment": "10000101", "Description": "Operating hours", "IsCounter": True, "Unit": "h"},
"MP-104": {"Equipment": "10000104", "Description": "Vibration velocity", "IsCounter": False, "Unit": "mm/s",
"UpperLimit": 7.1},
}
# Measurement documents (readings), after the measurement document API.
READINGS = [
("MP-101", "2026-09-01", 1720.0), ("MP-101", "2026-09-15", 1776.0), ("MP-101", "2026-10-02", 1840.0),
("MP-104", "2026-10-08", 4.6), ("MP-104", "2026-10-15", 5.9), ("MP-104", "2026-10-22", 7.4),
]
# ------------------------------------------------------------ catalog (course codes)
# Catalog types after SAP's training: B object parts, C damage, 5 causes. Codes are invented.
CATALOG = {
"B": {"BRAKE": "Brake", "SEAL": "Seal", "BEARING": "Bearing", "SENSOR": "Sensor or meter", "MOTOR": "Motor"},
"C": {"WORN": "Worn", "LEAK": "Leak", "VIBR": "Vibration", "ELEC": "Electrical fault", "OTHER": "Other"},
"5": {"WEAR": "Normal wear", "CONTAM": "Contamination", "LOOSE": "Loose connection", "AGE": "Aged material"},
}
# ------------------------------------------------------------ notifications
# After API_MAINTNOTIFICATION: header plus the first item's object part, damage and cause codes.
# Types M1 (maintenance request) and M2 (malfunction report) are course labels.
NOTIFICATIONS = [
{"MaintenanceNotification": "10000451", "NotificationType": "M2", "TechnicalObject": "10000103",
"CreationDate": "2026-10-05", "NotificationText": "Hoist brake slipping under load, crane stopped",
"ObjPart": "BRAKE", "Damage": "WORN", "Cause": "WEAR", "MaintenanceOrder": "4000119", "Status": "completed",
"Origin": "crane operator"},
{"MaintenanceNotification": "10000452", "NotificationType": "M2", "TechnicalObject": "10000101",
"CreationDate": "2026-10-14", "NotificationText": "Leak on pressure side, test stopped twice, seals look hard",
"ObjPart": "", "Damage": "OTHER", "Cause": "", "MaintenanceOrder": "4000121", "Status": "completed",
"Origin": "test operator"},
{"MaintenanceNotification": "10000453", "NotificationType": "M2", "TechnicalObject": "10000103",
"CreationDate": "2026-10-20", "NotificationText": "Brake slipping again on the hoist",
"ObjPart": "BRAKE", "Damage": "WORN", "Cause": "", "MaintenanceOrder": "4000126", "Status": "in process",
"Origin": "crane operator"},
{"MaintenanceNotification": "10000454", "NotificationType": "M2", "TechnicalObject": "10000104",
"CreationDate": "2026-10-22", "NotificationText": "Vibration above limit at MP-104 (7.4 mm/s)",
"ObjPart": "", "Damage": "VIBR", "Cause": "", "MaintenanceOrder": "", "Status": "awaiting screening",
"Origin": "measurement document (condition)"},
{"MaintenanceNotification": "10000455", "NotificationType": "M1", "TechnicalObject": "10000101",
"CreationDate": "2026-10-23", "NotificationText": "Request: second test station for ES-300 pumps",
"ObjPart": "", "Damage": "", "Cause": "", "MaintenanceOrder": "4000131", "Status": "in process",
"Origin": "production engineer"},
{"MaintenanceNotification": "10000456", "NotificationType": "M2", "TechnicalObject": "10000102",
"CreationDate": "2026-10-26", "NotificationText": "Flow readings jump, display flickers",
"ObjPart": "", "Damage": "", "Cause": "", "MaintenanceOrder": "", "Status": "awaiting screening",
"Origin": "test operator"},
]
# Completed malfunction reports from the last twelve months, for the coding history (compact).
# (notification, equipment, date, text, object part, damage, cause)
HISTORY = [
("10000302", "10000101", "2025-11-18", "Seal leak at outlet flange", "SEAL", "LEAK", "AGE"),
("10000317", "10000101", "2026-01-12", "Water on floor under bench", "", "OTHER", ""),
("10000329", "10000103", "2026-02-03", "Hoist brake noisy", "BRAKE", "WORN", "WEAR"),
("10000344", "10000104", "2026-03-09", "Compressor trips, hot", "MOTOR", "ELEC", "LOOSE"),
("10000358", "10000101", "2026-04-21", "Pressure side dripping after test", "", "OTHER", ""),
("10000371", "10000102", "2026-05-14", "Flow meter shows zero", "SENSOR", "ELEC", "LOOSE"),
("10000386", "10000104", "2026-06-30", "Loud rattle, vibration", "", "OTHER", ""),
("10000402", "10000101", "2026-08-11", "Seal leak, pump side", "SEAL", "LEAK", "WEAR"),
("10000388", "10000104", "2026-09-21", "Compressor trips again", "MOTOR", "ELEC", "LOOSE"),
("10000417", "10000103", "2026-09-02", "Brake slips under load", "BRAKE", "WORN", ""),
]
# ------------------------------------------------------------ orders
# After API_MAINTENANCEORDER: header, operations (work center, planned and actual hours),
# components (stock items are reserved; non-stock items create a purchase requisition)
# and one settlement rule. Order types PM01 (corrective), PM02 (preventive), PM03
# (improvement) are course labels.
RATES = {"MECH1": 95.00, "ELEC1": 105.00} # USD per hour; maintenance cost center CP11-450 (course values)
STOCK = {"CP-SP-TBSEAL": 0, "CP-SP-BRK10": 3, "CP-BRG6310": 6}
ORDERS = {
"4000108": {"Type": "PM03", "Equipment": "10000103", "Desc": "Replace hoist motor with 15 kW unit",
"Notification": "", "Start": "2026-09-14", "Status": "closed", "TECO": "2026-09-17",
"Ops": [("MECH1", 10.0, 12.0), ("ELEC1", 4.0, 4.0)],
"Components": [("hoist motor 15 kW (non-stock)", 1, 7800.00, "PO 4500000011 from Orrin Motor Works")],
"Settlement": ("FXA", "30000009", "capitalized: the crane asset gains a better hoist"),
"Settled": "2026-09-30"},
"4000112": {"Type": "PM01", "Equipment": "10000104", "Desc": "Compressor trips: replace contactor",
"Notification": "10000388", "Start": "2026-09-22", "Status": "technically complete",
"TECO": "2026-09-24", "Ops": [("ELEC1", 3.0, 4.0)],
"Components": [("contactor (non-stock)", 1, 820.00, "PO 4500000012 from a local electrical wholesaler")],
"Settlement": None, "Settled": ""},
"4000119": {"Type": "PM01", "Equipment": "10000103", "Desc": "Hoist brake slipping: replace brake pads",
"Notification": "10000451", "Start": "2026-10-06", "Status": "technically complete",
"TECO": "2026-10-09", "Ops": [("MECH1", 3.0, 3.5)],
"Components": [("CP-SP-BRK10", 1, 180.00, "stock")],
"Settlement": ("CTR", "CP11-410", "cost center of the crane"), "Settled": ""},
"4000121": {"Type": "PM01", "Equipment": "10000101", "Desc": "Test bench leak: replace pressure-side seals",
"Notification": "10000452", "Start": "2026-10-15", "Status": "technically complete",
"TECO": "2026-10-16", "Ops": [("MECH1", 4.0, 6.5)],
"Components": [("CP-SP-TBSEAL", 1, 420.00, "stock")],
"Settlement": ("CTR", "CP11-420", "cost center of the test bench"), "Settled": ""},
"4000123": {"Type": "PM02", "Equipment": "10000101", "Desc": "Annual test bench overhaul (plan MP-TB-12M)",
"Notification": "", "Start": "2026-11-09", "Status": "released, in preparation",
"TECO": "", "Ops": [("MECH1", 20.0, 0.0), ("ELEC1", 6.0, 0.0)],
"Components": [("CP-SP-TBSEAL", 1, 420.00, "stock"), ("CP-BRG6310", 2, 64.00, "stock"),
("pressure transducer (non-stock)", 1, 690.00,
"purchase requisition 0010000014, no PO yet, delivery 15 days")],
"Settlement": ("CTR", "CP11-420", "cost center of the test bench"), "Settled": ""},
"4000126": {"Type": "PM01", "Equipment": "10000103", "Desc": "Hoist brake slipping again",
"Notification": "10000453", "Start": "2026-10-27", "Status": "released",
"TECO": "", "Ops": [("MECH1", 3.0, 0.0)],
"Components": [("CP-SP-BRK10", 1, 180.00, "stock")],
"Settlement": ("CTR", "CP11-410", "cost center of the crane"), "Settled": ""},
"4000131": {"Type": "PM03", "Equipment": "10000101", "Desc": "Install second test station",
"Notification": "10000455", "Start": "2026-12-07", "Status": "submitted for approval",
"TECO": "", "Ops": [("MECH1", 32.0, 0.0), ("ELEC1", 16.0, 0.0)],
"Components": [("flow meter and manifold (non-stock)", 1, 3880.00, "requisition after approval")],
"Settlement": ("FXA", "30000041", "asset under construction: second test station"),
"Settled": ""},
}
# Events for the order trace (date, step, who, what).
EVENTS = {
"4000121": [
("2026-10-14", "Notification 10000452", "Test operator", "malfunction report on 10000101; damage coded OTHER only"),
("2026-10-14", "Order 4000121 (PM01)", "Maintenance planner", "created from the notification; settlement rule proposed from the equipment"),
("2026-10-14", "Release", "Maintenance planner", "availability check: 1 seal set in stock, reserved"),
("2026-10-15", "Goods issue", "Storekeeper", "1 x CP-SP-TBSEAL against reservation; last one in stock"),
("2026-10-15", "Time confirmation", "Mechanic (MECH1)", "6.5 h actual vs 4.0 h planned; final confirmation"),
("2026-10-16", "Technical completion", "Maintenance planner", "notification completed with the order; no cause code"),
("2026-10-31", "Settlement (period end)", "Cost accountant", "order balance to cost center CP11-420"),
],
"4000108": [
("2026-09-08", "Order 4000108 (PM03)", "Maintenance planner", "improvement: larger hoist motor; settlement rule to asset 30000009"),
("2026-09-09", "Approval", "Plant manager", "approved as capital spend"),
("2026-09-09", "Purchase order 4500000011", "Buyer", "hoist motor from Orrin Motor Works, account assigned to the order"),
("2026-09-14", "Goods receipt", "Storekeeper", "motor received; cost goes straight to the order"),
("2026-09-16", "Time confirmations", "Mechanic, electrician", "12.0 h MECH1, 4.0 h ELEC1"),
("2026-09-17", "Technical completion", "Maintenance planner", "crane back in service"),
("2026-09-30", "Settlement (period end)", "Cost accountant", "order balance to fixed asset 30000009"),
("2026-09-30", "Business completion", "Maintenance planner", "balance zero; order closed"),
],
}
# ------------------------------------------------------------ maintenance plans
# After API_MAINTENANCEPLAN. Time-based plans have a cycle in months; the counter-based plan
# has a cycle in operating hours read from MP-101. CallHorizonPercent: when the order is called.
PLANS = [
{"MaintenancePlan": "MP-TB-12M", "Desc": "Annual test bench overhaul", "Equipment": "10000101",
"Kind": "time", "CycleMonths": 12, "LastDone": "2025-11-09", "CallHorizonPercent": 90, "Order": "4000123"},
{"MaintenancePlan": "MP-CRANE-12M", "Desc": "Annual crane inspection (external, Corvane Lifting Services)",
"Equipment": "10000103", "Kind": "time", "CycleMonths": 12, "LastDone": "2026-10-14",
"CallHorizonPercent": 80, "Order": ""},
{"MaintenancePlan": "MP-TB-500H", "Desc": "Test bench 500-hour service", "Equipment": "10000101",
"Kind": "counter", "MeasuringPoint": "MP-101", "CycleHours": 500.0, "LastDoneAt": 1500.0,
"AnnualEstimate": 1000.0, "CallHorizonPercent": 90, "Order": ""},
]
OWNER = {
"PARTS": "Maintenance planner, with the buyer",
"COND": "Maintenance gatekeeper (screen the request)",
"REWORK": "Maintenance supervisor",
"COUNTER": "Test bench operator (enter the reading)",
"SETTLE": "Cost accountant, with master data",
"CODING": "Maintenance supervisor (code it, or send it back)",
}
RANK = ["PARTS", "COND", "REWORK", "COUNTER", "SETTLE", "CODING"]
# --------------------------------------------------------------- helpers
def d(text: str) -> date:
return date.fromisoformat(text)
def money(x: float) -> str:
return f"{x:,.2f}"
def add_months(day: date, months: int) -> date:
month = day.month - 1 + months
year = day.year + month // 12
month = month % 12 + 1
last = [31, 29 if year % 4 == 0 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]
return date(year, month, min(day.day, last))
def check_pack() -> None:
try:
org = json.loads((PACK / "org_units.json").read_text(encoding="utf-8"))
partners = json.loads((PACK / "business_partners.json").read_text(encoding="utf-8"))
except FileNotFoundError:
sys.exit(f"Cannot read {PACK}. Run: python unit1b/make_calder_pack.py (Unit 1B setup).")
plants = {p["Plant"]: p["CompanyCode"] for p in org["Plants"]}
if plants.get(PLANT) != "CP10":
sys.exit("Plant CP11 of company code CP10 is missing from the pack. Run make_calder_pack.py again.")
ids = {bp["BusinessPartner"] for bp in partners["BusinessPartners"]}
if "20000001" not in ids:
sys.exit("Supplier 20000001 (Orrin Motor Works) is missing from the pack. Run make_calder_pack.py again.")
def order_costs(order: dict) -> list:
"""Actual cost lines: (value category, text, amount)."""
lines = []
for wc, _planned, actual in order["Ops"]:
if actual:
lines.append(("internal labor", f"{wc} {actual:.1f} h x {money(RATES[wc])}", round(actual * RATES[wc], 2)))
if order["TECO"] or order["Status"] == "closed":
for text, qty, price, source in order["Components"]:
category = "material from stock" if source == "stock" else "external purchase"
lines.append((category, f"{qty} x {text}", round(qty * price, 2)))
return lines
def settlement_rule(order: dict):
"""The order's own rule, or one proposed from the equipment's cost center, or None."""
if order["Settlement"]:
return order["Settlement"]
cost_center = EQUIPMENT[order["Equipment"]]["CostCenter"]
return ("CTR", cost_center, "proposed from the equipment's cost center") if cost_center else None
def balance(order: dict) -> float:
return round(sum(a for _, _, a in order_costs(order)), 2)
# --------------------------------------------------------------- plans
def plan_dates(plan: dict) -> dict:
"""Next due date and call date for one plan (teaching sketch of SAP's scheduling)."""
if plan["Kind"] == "time":
last = d(plan["LastDone"])
due = add_months(last, plan["CycleMonths"])
cycle_days = (due - last).days
call = last + timedelta(days=round(cycle_days * plan["CallHorizonPercent"] / 100))
return {"due": due, "call": call, "basis": f"every {plan['CycleMonths']} months from {last}"}
rows = sorted((d(day), val) for mp, day, val in READINGS if mp == plan["MeasuringPoint"])
(first_day, first_val), (last_day, last_val) = rows[0], rows[-1]
per_day = (last_val - first_val) / (last_day - first_day).days if last_day > first_day else plan["AnnualEstimate"] / 365
due_at = plan["LastDoneAt"] + plan["CycleHours"]
call_at = plan["LastDoneAt"] + plan["CycleHours"] * plan["CallHorizonPercent"] / 100
due = last_day + timedelta(days=round((due_at - last_val) / per_day))
call = last_day + timedelta(days=round((call_at - last_val) / per_day))
return {"due": due, "call": call, "per_day": per_day, "last_day": last_day, "last_val": last_val,
"due_at": due_at, "call_at": call_at,
"basis": f"{plan['CycleHours']:.0f} h after {plan['LastDoneAt']:.0f} h, {per_day:.2f} h/day from readings"}
# --------------------------------------------------------------- worklist
def worklist() -> list:
items = []
# 1. Parts for orders that start soon
for no, o in ORDERS.items():
if o["TECO"] or o["Status"] in ("closed", "submitted for approval"):
continue
start = d(o["Start"])
if (start - AS_OF).days > PARTS_WINDOW_DAYS:
continue
for text, qty, _price, source in o["Components"]:
if source == "stock":
if STOCK.get(text, 0) < qty:
items.append({"kind": "PARTS", "object": f"order {no}", "equipment": o["Equipment"],
"detail": f"{text}: need {qty}, stock {STOCK.get(text, 0)}, no open purchase order; work starts {start}"})
elif "no PO" in source:
days = int(source.split("delivery ")[1].split(" ")[0])
earliest = AS_OF + timedelta(days=days)
if earliest > start:
items.append({"kind": "PARTS", "object": f"order {no}", "equipment": o["Equipment"],
"detail": f"{text}: {source.split(',')[0]} not yet ordered; earliest delivery {earliest}, work starts {start}"})
# 2. Condition-based notifications waiting
for n in NOTIFICATIONS:
if n["Origin"].startswith("measurement") and not n["MaintenanceOrder"]:
age = (AS_OF - d(n["CreationDate"])).days
items.append({"kind": "COND", "object": f"notification {n['MaintenanceNotification']}",
"equipment": n["TechnicalObject"],
"detail": f"{n['NotificationText']}; {n['Status']} for {age} days"})
# 3. Rework: a new order on the same equipment soon after another was completed
for no, o in ORDERS.items():
if o["TECO"] or o["Type"] != "PM01":
continue
created = d(next(n["CreationDate"] for n in NOTIFICATIONS if n["MaintenanceNotification"] == o["Notification"]))
for no2, o2 in ORDERS.items():
if no2 != no and o2["Equipment"] == o["Equipment"] and o2["TECO"] and o2["Type"] == "PM01":
gap = (created - d(o2["TECO"])).days
if 0 <= gap <= REWORK_WINDOW_DAYS:
items.append({"kind": "REWORK", "object": f"order {no}", "equipment": o["Equipment"],
"detail": f"{gap} days after order {no2} ({o2['Desc']}) was completed on the same equipment"})
# 4. Stale counter readings behind a counter-based plan
for p in PLANS:
if p["Kind"] != "counter":
continue
info = plan_dates(p)
age = (AS_OF - info["last_day"]).days
if age > STALE_READING_DAYS:
items.append({"kind": "COUNTER", "object": f"plan {p['MaintenancePlan']}", "equipment": p["Equipment"],
"detail": f"last {p['MeasuringPoint']} reading {info['last_day']} ({age} days old); "
f"projected call {info['call']}, due {info['due']}"})
# 5. Completed orders that still carry costs
for no, o in ORDERS.items():
if o["TECO"] and not o["Settled"] and d(o["TECO"]) < date(AS_OF.year, AS_OF.month, 1):
reason = ("no settlement rule: equipment " + o["Equipment"] + " has no cost center"
if settlement_rule(o) is None else "rule in place, settlement not run")
items.append({"kind": "SETTLE", "object": f"order {no}", "equipment": o["Equipment"],
"detail": f"technically complete {o['TECO']}, {money(balance(o))} USD still on the order; {reason}"})
# 6. Malfunction reports without damage or cause codes
for n in NOTIFICATIONS:
if n["NotificationType"] != "M2" or n["Origin"].startswith("measurement"):
continue
# Causes are often found only during the repair, so open reports are checked for damage and part only.
keys = (("object part", "ObjPart"), ("cause", "Cause")) if n["Status"] == "completed" else (("object part", "ObjPart"),)
missing = [label for label, key in keys if not n[key]]
if n["Damage"] in ("", "OTHER"):
missing.insert(0, "damage")
if missing:
items.append({"kind": "CODING", "object": f"notification {n['MaintenanceNotification']}",
"equipment": n["TechnicalObject"],
"detail": f"\"{n['NotificationText']}\" ({n['Status']}): no {', '.join(missing)}"})
abc = {e: v["ABCIndicator"] for e, v in EQUIPMENT.items()}
items.sort(key=lambda r: (RANK.index(r["kind"]), abc.get(r["equipment"], "C"), r["object"]))
return items
def print_worklist(items: list) -> None:
print(f"Maintenance worklist for plant {PLANT} as of {AS_OF} (fictional Calder data)\n")
print(f" {'#':>2} {'Kind':<8}{'Object':<24}{'Equipment':<10} Owner")
for i, it in enumerate(items, 1):
print(f" {i:>2} {it['kind']:<8}{it['object']:<24}{it['equipment']:<10} {OWNER[it['kind']]}")
print(f" {'':<8}{it['detail']}")
open_cost = sum(balance(o) for o in ORDERS.values() if o["TECO"] and not o["Settled"]
and d(o["TECO"]) < date(AS_OF.year, AS_OF.month, 1))
print(f"\n {len(items)} items; {money(open_cost)} USD on orders completed before this month and not settled.")
print(" Synthetic data: the list shows the method, not results from an SAP system.")
# --------------------------------------------------------------- views
def print_tree() -> None:
print(f"Technical objects at {PLANT} (fictional; labels are course values)\n")
children = {}
for fl in FUNCTIONAL_LOCATIONS:
children.setdefault(fl["SuperiorFunctionalLocation"], []).append(fl)
def show_equipment(fl_id: str, superior: str, depth: int) -> None:
for no, e in EQUIPMENT.items():
if e["FunctionalLocation"] == fl_id and e["SuperordinateEquipment"] == superior:
asset = e["FixedAsset"] or "-"
cc = e["CostCenter"] or "MISSING"
mps = ", ".join(m for m, v in MEASURING_POINTS.items() if v["Equipment"] == no)
print(f"{' ' * depth}[{no}] {e['EquipmentName']} ABC {e['ABCIndicator']}, cost center {cc}, "
f"asset {asset}" + (f", points {mps}" if mps else ""))
show_equipment(fl_id, no, depth + 1)
def walk(parent: str, depth: int) -> None:
for fl in children.get(parent, []):
print(f"{' ' * depth}{fl['FunctionalLocation']} {fl['FunctionalLocationName']}")
show_equipment(fl["FunctionalLocation"], "", depth + 1)
walk(fl["FunctionalLocation"], depth + 1)
walk("", 1)
print("\n Equipment is installed at a functional location; costs settle to its cost center by default.")
def print_plans() -> None:
print(f"Maintenance plans at {PLANT} as of {AS_OF} (fictional)\n")
for p in PLANS:
info = plan_dates(p)
print(f" {p['MaintenancePlan']:<13}{p['Desc']}")
print(f" {'':<13}basis: {info['basis']}")
call_note = "called" if info["call"] <= AS_OF else "not yet called"
order = f", order {p['Order']}" if p["Order"] else ""
print(f" {'':<13}call {info['call']} ({call_note}{order}), due {info['due']}")
if p["Kind"] == "counter":
print(f" {'':<13}last reading {info['last_val']:.0f} h on {info['last_day']}; call at {info['call_at']:.0f} h, "
f"due at {info['due_at']:.0f} h")
print()
print(" Condition monitoring:")
for mp, v in MEASURING_POINTS.items():
if v["IsCounter"]:
continue
for point, day, val in READINGS:
if point == mp:
flag = " above limit -> notification" if val > v["UpperLimit"] else ""
print(f" {mp} {v['Description']} on {v['Equipment']}: {day} {val} {v['Unit']} "
f"(limit {v['UpperLimit']}){flag}")
overhaul = plan_dates(PLANS[0])["due"]
service = plan_dates(PLANS[2])
gap = (service["due"] - overhaul).days
print(f"\n The 500-hour service falls due {gap} days after the overhaul starts: do both in the same outage.")
def explain_order(no: str) -> None:
if no not in ORDERS:
sys.exit(f"Order {no} is not in the scenario. Try one of: {', '.join(ORDERS)}")
o = ORDERS[no]
e = EQUIPMENT[o["Equipment"]]
print(f"Maintenance order {no} ({o['Type']}): {o['Desc']} (fictional)")
print(f" equipment {o['Equipment']} {e['EquipmentName']}, status {o['Status']}\n")
for day, step, who, what in EVENTS.get(no, []):
print(f" {day} {step:<26}{who:<22}{what}")
if no not in EVENTS:
print(" (no event trace in the scenario for this order; costs below)")
lines = order_costs(o)
print("\nCosts on the order (USD):")
for cat, text, amount in lines:
print(f" {cat:<21}{text:<44}{money(amount):>10}")
total = balance(o)
print(f" {'balance':<21}{'':<44}{money(total):>10}")
planned = sum(p * RATES[wc] for wc, p, _ in o["Ops"]) + sum(q * pr for _, q, pr, _ in o["Components"])
print(f" {'planned':<21}{'':<44}{money(planned):>10}")
if settlement_rule(o) is None:
print("\nSettlement: no rule. The equipment has no cost center, so none was proposed.")
print(" The order can't be settled or closed until someone adds a receiver.")
else:
kind, receiver, why = settlement_rule(o)
label = {"CTR": "cost center", "FXA": "fixed asset"}[kind]
state = f"settled {o['Settled']}" if o["Settled"] else "to be settled at period end"
print(f"\nSettlement rule: 100% to {label} {receiver} ({why}); {state}")
if total:
print(f" order {no} credited {money(total)}; {label} {receiver} debited {money(total)}")
if kind == "FXA":
print(" The cost becomes part of the asset's value and is depreciated, not expensed this month.")
else:
print(" The cost becomes maintenance expense of that cost center in the period it is settled.")
print("\n Synthetic numbers for a fictional order, not from an SAP system.")
def coding_report() -> None:
rows = [(n["MaintenanceNotification"], n["TechnicalObject"], n["CreationDate"], n["NotificationText"],
n["ObjPart"], n["Damage"], n["Cause"]) for n in NOTIFICATIONS
if n["NotificationType"] == "M2" and n["Status"] == "completed"]
rows = HISTORY + rows
full = [r for r in rows if r[4] and r[5] not in ("", "OTHER") and r[6]]
print(f"Failure coding on completed malfunction reports at {PLANT}, last 12 months (fictional)\n")
print(f" {len(rows)} reports; {len(full)} fully coded (object part, damage, cause) = {len(full) / len(rows):.0%}")
other = [r for r in rows if r[5] in ("", "OTHER")]
print(f" {len(other)} have damage OTHER or none, so history can't count them\n")
print(" Damage per equipment as the codes tell it:")
for eq in EQUIPMENT:
counts = {}
for r in rows:
if r[1] == eq:
counts[r[5] or "none"] = counts.get(r[5] or "none", 0) + 1
if counts:
print(f" {eq} {EQUIPMENT[eq]['EquipmentName']:<22}" + ", ".join(f"{k} {v}" for k, v in sorted(counts.items())))
print("\n Keyword baseline for uncoded reports (the bar an AI suggestion has to beat):")
for r in rows + [(n["MaintenanceNotification"], n["TechnicalObject"], n["CreationDate"], n["NotificationText"],
n["ObjPart"], n["Damage"], n["Cause"]) for n in NOTIFICATIONS
if n["NotificationType"] == "M2" and n["Status"] != "completed"
and not n["Origin"].startswith("measurement")]:
if r[5] not in ("", "OTHER") and r[4]:
continue
part, damage, hits = suggest(r[3])
print(f" {r[0]} \"{r[3]}\"")
print(f" suggest part {part or '?'}, damage {damage or '?'} ({hits} keyword hits; cause: ask the technician)")
print("\n Synthetic data: the rates show the method, not results from an SAP system.")
KEYWORDS = {
"SEAL": ("seal", "leak", "drip", "water"), "BRAKE": ("brake",), "BEARING": ("bearing", "rattle"),
"SENSOR": ("meter", "reading", "display"), "MOTOR": ("motor", "trips", "hot"),
}
DAMAGE_WORDS = {
"LEAK": ("leak", "drip", "water"), "WORN": ("slip", "worn", "noisy"), "VIBR": ("vibration", "rattle"),
"ELEC": ("display", "flicker", "trips", "zero"),
}
def suggest(text: str) -> tuple:
low = text.lower()
best_part = max(KEYWORDS, key=lambda k: sum(w in low for w in KEYWORDS[k]))
best_damage = max(DAMAGE_WORDS, key=lambda k: sum(w in low for w in DAMAGE_WORDS[k]))
part_hits = sum(w in low for w in KEYWORDS[best_part])
damage_hits = sum(w in low for w in DAMAGE_WORDS[best_damage])
return (best_part if part_hits else "", best_damage if damage_hits else "", part_hits + damage_hits)
def write_markdown(items: list) -> Path:
out = HERE / "calder_maintenance_worklist.md"
lines = [f"# Maintenance worklist, {PLANT}, {AS_OF}", "",
"Fictional Calder Pumpworks data. Synthetic: it shows the method, not results from an SAP system.", "",
"```mermaid", "flowchart LR",
" N[Notification] --> O[Maintenance order]", " O --> R[Reservations,<br/>requisitions]",
" R --> X[Execution,<br/>confirmations]", " X --> T[Technical<br/>completion]",
" T --> S[Settlement]", " P[Maintenance plan] --> O", " M[Measurement<br/>document] --> N", "```", "",
"| # | Kind | Object | Equipment | Owner | Detail |", "| --- | --- | --- | --- | --- | --- |"]
for i, it in enumerate(items, 1):
lines.append(f"| {i} | {it['kind']} | {it['object']} | {it['equipment']} | {OWNER[it['kind']]} | {it['detail'].replace('|', '/')} |")
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
return out
# --------------------------------------------------------------- sandbox (optional)
def read_env_key(name: str) -> str:
value = os.environ.get(name, "")
env = Path.cwd() / ".env"
if not value and env.exists():
for line in env.read_text(encoding="utf-8").splitlines():
if line.strip().startswith(f"{name}="):
value = line.split("=", 1)[1].strip().strip('"').strip("'")
return value
def sandbox() -> None:
key = read_env_key("SAP_API_KEY")
if not key:
sys.exit("SAP_API_KEY is not set. Add it to .env (course setup) or skip this step.")
base = "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/API_MAINTNOTIFICATION/MaintenanceNotification"
query = urllib.parse.urlencode({"$top": "50", "$format": "json",
"$select": "MaintenanceNotification,NotificationType,MaintPriority,"
"TechnicalObject,NotificationText,MaintNotificationCodeGroup,"
"MaintNotificationCode,MaintenanceOrder"})
req = urllib.request.Request(f"{base}?{query}", headers={"APIKey": key, "Accept": "application/json"})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as err:
sys.exit(f"Sandbox answered HTTP {err.code}. Check the key, or try again later.")
except (urllib.error.URLError, TimeoutError) as err:
sys.exit(f"Cannot reach the sandbox ({err}). Check the network or proxy.")
items = data.get("d", {}).get("results", [])
types, coded, with_order = {}, 0, 0
for it in items:
types[it.get("NotificationType", "")] = types.get(it.get("NotificationType", ""), 0) + 1
coded += 1 if it.get("MaintNotificationCode") else 0
with_order += 1 if it.get("MaintenanceOrder") else 0
print(f"SAP sandbox: {len(items)} maintenance notifications read (SAP demo data, not Calder's)\n")
print(" NotificationType: " + ", ".join(f"'{t or '(empty)'}' x{n}" for t, n in sorted(types.items())))
print(f" with a header code: {coded} of {len(items)}; with an order: {with_order} of {len(items)}")
for it in items[:3]:
print(f" e.g. {it.get('MaintenanceNotification')} {it.get('TechnicalObject')} {it.get('NotificationText')}")
# --------------------------------------------------------------- main
def main() -> None:
parser = argparse.ArgumentParser(description="Maintain to settle at Calder (fictional data).")
parser.add_argument("--tree", action="store_true", help="functional locations, equipment and assets")
parser.add_argument("--plans", action="store_true", help="maintenance plans and condition readings")
parser.add_argument("--order", help="trace one maintenance order, e.g. 4000121")
parser.add_argument("--coding", action="store_true", help="failure coding quality and a keyword baseline")
parser.add_argument("--write", action="store_true", help="write unit1b/calder_maintenance_worklist.md")
parser.add_argument("--sandbox", action="store_true", help="count maintenance notifications in SAP's sandbox")
args = parser.parse_args()
if args.sandbox:
sandbox()
return
check_pack()
if args.tree:
print_tree()
elif args.plans:
print_plans()
elif args.order:
explain_order(args.order)
elif args.coding:
coding_report()
else:
items = worklist()
print_worklist(items)
if args.write:
print(f"\nwrote unit1b/{write_markdown(items).name}")
if __name__ == "__main__":
main()
You should see exactly this. The script has no random parts, so your output must match:
Technical objects at CP11 (fictional; labels are course values)
CP11 Rockford plant
CP11-ASY Assembly and test hall
CP11-ASY-TB1 Test bench position 1
[10000101] Pump test bench TB-1 ABC A, cost center CP11-420, asset 30000017, points MP-101
[10000102] Flow meter FM-1 ABC A, cost center CP11-420, asset -
CP11-ASY-CRN Overhead crane runway
[10000103] Overhead crane 10 t ABC A, cost center CP11-410, asset 30000009
CP11-UTL Utilities
CP11-UTL-AIR Compressed air station
[10000104] Air compressor C-2 ABC B, cost center MISSING, asset 30000021, points MP-104
Equipment is installed at a functional location; costs settle to its cost center by default.
Read it as a planner would. The flow meter sits under the test bench, so its history rolls up to the bench. Every machine except the compressor has a cost center, which will be the default receiver when its orders settle. Three machines point to fixed assets; the flow meter doesn't, because it was bought as part of the bench.
Maintenance plans at CP11 as of 2026-10-26 (fictional)
MP-TB-12M Annual test bench overhaul
basis: every 12 months from 2025-11-09
call 2026-10-03 (called, order 4000123), due 2026-11-09
MP-CRANE-12M Annual crane inspection (external, Corvane Lifting Services)
basis: every 12 months from 2026-10-14
call 2027-08-02 (not yet called), due 2027-10-14
MP-TB-500H Test bench 500-hour service
basis: 500 h after 1500 h, 3.87 h/day from readings
call 2026-10-30 (not yet called), due 2026-11-12
last reading 1840 h on 2026-10-02; call at 1950 h, due at 2000 h
Condition monitoring:
MP-104 Vibration velocity on 10000104: 2026-10-08 4.6 mm/s (limit 7.1)
MP-104 Vibration velocity on 10000104: 2026-10-15 5.9 mm/s (limit 7.1)
MP-104 Vibration velocity on 10000104: 2026-10-22 7.4 mm/s (limit 7.1) above limit -> notification
The 500-hour service falls due 3 days after the overhaul starts: do both in the same outage.
How to read it:
The annual overhaul was last done on 9 November 2025, so it falls due on 9 November 2026. At a 90% call horizon, the plan called order 4000123 on 3 October. That is the outage the production planner saw in Plan to produce end to end.
The 500-hour service is counter-based. The bench ran 120 hours between 1 September and 2 October, 3.87 hours a day. At that rate it reaches 2,000 hours on 12 November, three days after the overhaul starts. Doing both in the same outage saves a second stop of the bottleneck.
That projection rests on a reading from 2 October. If the bench ran harder in October, the service is due earlier. Step 6 flags it.
The compressor's vibration rose from 4.6 to 7.4 mm/s in two weeks and crossed its limit, which created notification 10000454. The script's limit logic is a sketch of the threshold idea, not SAP's configuration.
#Step 6: Read the planner's worklist and trace two orders
Run the script without options:
Windows (PowerShell):
python unit1b\maintain_to_settle.py
macOS / Linux:
python unit1b/maintain_to_settle.py
You should see:
Maintenance worklist for plant CP11 as of 2026-10-26 (fictional Calder data)
# Kind Object Equipment Owner
1 PARTS order 4000123 10000101 Maintenance planner, with the buyer
CP-SP-TBSEAL: need 1, stock 0, no open purchase order; work starts 2026-11-09
2 PARTS order 4000123 10000101 Maintenance planner, with the buyer
pressure transducer (non-stock): purchase requisition 0010000014 not yet ordered; earliest delivery 2026-11-10, work starts 2026-11-09
3 COND notification 10000454 10000104 Maintenance gatekeeper (screen the request)
Vibration above limit at MP-104 (7.4 mm/s); awaiting screening for 4 days
4 REWORK order 4000126 10000103 Maintenance supervisor
11 days after order 4000119 (Hoist brake slipping: replace brake pads) was completed on the same equipment
5 COUNTER plan MP-TB-500H 10000101 Test bench operator (enter the reading)
last MP-101 reading 2026-10-02 (24 days old); projected call 2026-10-30, due 2026-11-12
6 SETTLE order 4000112 10000104 Cost accountant, with master data
technically complete 2026-09-24, 1,240.00 USD still on the order; no settlement rule: equipment 10000104 has no cost center
7 CODING notification 10000452 10000101 Maintenance supervisor (code it, or send it back)
"Leak on pressure side, test stopped twice, seals look hard" (completed): no damage, object part, cause
8 CODING notification 10000456 10000102 Maintenance supervisor (code it, or send it back)
"Flow readings jump, display flickers" (awaiting screening): no damage, object part
8 items; 1,240.00 USD on orders completed before this month and not settled.
Synthetic data: the list shows the method, not results from an SAP system.
How to read it:
Items 1 and 2 are one problem: the overhaul on 9 November can't start with its parts. The last seal set went into the leak repair on 15 October, and the transducer's requisition was never turned into a purchase order. Production has already planned around a three-day outage; a fourth day costs pumps.
Item 3 is a condition alert waiting for a person. The machine told the system; the system is waiting for the gatekeeper.
Item 4 is rework. Eleven days after new brake pads, the brake slips again. The cause field on the first notification was left blank, which is why nobody asked why the pads wore.
Item 5 comes from Step 5: a plan running on an old reading.
Item 6 is money. The September contactor job can't settle, because the compressor has no cost center to propose a rule from.
Items 7 and 8 are coding gaps. Open notifications are checked for damage and object part only, because the cause is often found during the repair.
The ranking is the script's own: parts for work starting soon first, then condition alerts, rework, readings, settlement and coding, and within each the ABC indicator. A plant chooses its own order.
Maintenance order 4000121 (PM01): Test bench leak: replace pressure-side seals (fictional)
equipment 10000101 Pump test bench TB-1, status technically complete
2026-10-14 Notification 10000452 Test operator malfunction report on 10000101; damage coded OTHER only
2026-10-14 Order 4000121 (PM01) Maintenance planner created from the notification; settlement rule proposed from the equipment
2026-10-14 Release Maintenance planner availability check: 1 seal set in stock, reserved
2026-10-15 Goods issue Storekeeper 1 x CP-SP-TBSEAL against reservation; last one in stock
2026-10-15 Time confirmation Mechanic (MECH1) 6.5 h actual vs 4.0 h planned; final confirmation
2026-10-16 Technical completion Maintenance planner notification completed with the order; no cause code
2026-10-31 Settlement (period end) Cost accountant order balance to cost center CP11-420
Costs on the order (USD):
internal labor MECH1 6.5 h x 95.00 617.50
material from stock 1 x CP-SP-TBSEAL 420.00
balance 1,037.50
planned 800.00
Settlement rule: 100% to cost center CP11-420 (cost center of the test bench); to be settled at period end
order 4000121 credited 1,037.50; cost center CP11-420 debited 1,037.50
The cost becomes maintenance expense of that cost center in the period it is settled.
Synthetic numbers for a fictional order, not from an SAP system.
Now the capital job, --order 4000108:
Maintenance order 4000108 (PM03): Replace hoist motor with 15 kW unit (fictional)
equipment 10000103 Overhead crane 10 t, status closed
2026-09-08 Order 4000108 (PM03) Maintenance planner improvement: larger hoist motor; settlement rule to asset 30000009
2026-09-09 Approval Plant manager approved as capital spend
2026-09-09 Purchase order 4500000011 Buyer hoist motor from Orrin Motor Works, account assigned to the order
2026-09-14 Goods receipt Storekeeper motor received; cost goes straight to the order
2026-09-16 Time confirmations Mechanic, electrician 12.0 h MECH1, 4.0 h ELEC1
2026-09-17 Technical completion Maintenance planner crane back in service
2026-09-30 Settlement (period end) Cost accountant order balance to fixed asset 30000009
2026-09-30 Business completion Maintenance planner balance zero; order closed
Costs on the order (USD):
internal labor MECH1 12.0 h x 95.00 1,140.00
internal labor ELEC1 4.0 h x 105.00 420.00
external purchase 1 x hoist motor 15 kW (non-stock) 7,800.00
balance 9,360.00
planned 9,170.00
Settlement rule: 100% to fixed asset 30000009 (capitalized: the crane asset gains a better hoist); settled 2026-09-30
order 4000108 credited 9,360.00; fixed asset 30000009 debited 9,360.00
The cost becomes part of the asset's value and is depreciated, not expensed this month.
Synthetic numbers for a fictional order, not from an SAP system.
The two orders do the same kind of work: a mechanic, parts, a few hours. Settlement is what makes them different. The leak repair becomes expense on the test bench's cost center this period. The hoist motor becomes part of the crane asset and is spread over its remaining life as depreciation. Try --order 4000112 to see the order that can't settle.
#Step 7: Measure failure coding and write the worklist file
Run:
Windows (PowerShell):
python unit1b\maintain_to_settle.py --coding
macOS / Linux:
python unit1b/maintain_to_settle.py --coding
You should see:
Failure coding on completed malfunction reports at CP11, last 12 months (fictional)
12 reports; 7 fully coded (object part, damage, cause) = 58%
4 have damage OTHER or none, so history can't count them
Damage per equipment as the codes tell it:
10000101 Pump test bench TB-1 LEAK 2, OTHER 3
10000102 Flow meter FM-1 ELEC 1
10000103 Overhead crane 10 t WORN 3
10000104 Air compressor C-2 ELEC 2, OTHER 1
Keyword baseline for uncoded reports (the bar an AI suggestion has to beat):
10000317 "Water on floor under bench"
suggest part SEAL, damage LEAK (2 keyword hits; cause: ask the technician)
10000358 "Pressure side dripping after test"
suggest part SEAL, damage LEAK (2 keyword hits; cause: ask the technician)
10000386 "Loud rattle, vibration"
suggest part BEARING, damage VIBR (3 keyword hits; cause: ask the technician)
10000452 "Leak on pressure side, test stopped twice, seals look hard"
suggest part SEAL, damage LEAK (3 keyword hits; cause: ask the technician)
10000456 "Flow readings jump, display flickers"
suggest part SENSOR, damage ELEC (4 keyword hits; cause: ask the technician)
Synthetic data: the rates show the method, not results from an SAP system.
Only 58% of Calder's closed malfunction reports are fully coded, and the gap isn't random: the test bench's leaks are the ones coded "other". Its history reads "LEAK 2, OTHER 3", and every one of the three "other" texts mentions water, dripping or a leak. A reliability engineer reading only the codes would underestimate the bench's seal problem by more than half.
The keyword baseline guesses object part and damage from the text. It gets these five right because the scenario is small; on real notifications, with abbreviations, other languages and typos, it fails often. It never guesses the cause, which needs the technician. That is the bar an AI suggestion has to beat, and the reason to keep the baseline: classification metrics only mean something against it.
Write the worklist file:
Windows (PowerShell):
python unit1b\maintain_to_settle.py --write
macOS / Linux:
python unit1b/maintain_to_settle.py --write
The last line should read wrote unit1b/calder_maintenance_worklist.md. Open the file in VS Code and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS) to preview it: a process diagram and the eight items with owners.
#Step 8 (optional): Read maintenance notifications from SAP's sandbox
This step reads SAP's demo data, not Calder's. It needs your sandbox key and the network.
Make sure .env in the course folder has a line SAP_API_KEY="...".
Run:
Windows (PowerShell):
python unit1b\maintain_to_settle.py --sandbox
macOS / Linux:
python unit1b/maintain_to_settle.py --sandbox
The output has this shape. Values in angle brackets come from SAP's demo data and change over time:
SAP sandbox: <n> maintenance notifications read (SAP demo data, not Calder's)
NotificationType: '<type>' x<count>
with a header code: <count> of <n>; with an order: <count> of <n>
e.g. <notification> <technical object> <text>
The script reads up to 50 notifications from API_MAINTNOTIFICATION with $select. A valid "empty" result is 0 maintenance notifications read: the sandbox simply has none at that moment. The sandbox is read-only.
Where it sits. At Calder (fictional), maintenance for Rockford (CP11) runs in the CP10 S/4HANA system: technical objects, notifications, orders, plans and settlement. Technicians would use a mobile app against it. Sensor data from the test bench and compressor would go to an asset health service, which posts notifications back. Augsburg's machining centers are maintained on ECC. Anything that ranks, suggests or summarizes reads released interfaces and writes only drafts and worklists; people accept requests and release orders in SAP's own apps.
flowchart LR
SEN[Sensors<br/>bench, compressor] -->|time series| APM[Asset health<br/>rules]
APM -->|notifications| S4[S/4HANA CP10<br/>asset management,<br/>MM, CO, asset accounting]
MOB[Mobile app<br/>technicians] <-->|orders, confirmations,<br/>readings| S4
S4 <-->|outages, availability| PP[Production<br/>planning]
ECC[ECC CP20<br/>Augsburg machines] -.->|history| DW[Data warehouse]
S4 -->|released APIs, CDS| AI[Triage and coding<br/>assistant, read-only]
AI --> D[Drafts and<br/>worklists]
D -->|planner accepts in SAP| S4
Non-functional requirements that decide the design
Requirement
Typical target
If missed
Alert latency
A condition alert reaches a screening queue within minutes, with an owner on every shift
Failures happen while the request waits
Offline work
Technicians can confirm and read meters with no network, synced later
Paper notes, late or lost confirmations
Data volume
Readings filtered or aggregated before they become measurement documents
Millions of documents nobody reads; slow plans
Master data quality
Every equipment record has a cost center, a catalog profile and an ABC indicator
Orders can't settle; coding fails; priorities are guesses
Period-end close
All technically completed orders settled within the close calendar
Costs missing from cost centers and assets
Security and safety
Only authorized roles release orders and change plans; permits enforced
Unsafe work; audit findings
Integration and identity. SAP APM reads technical objects and posts notifications in batch over OData, and needs near-real-time events for measurement documents when rules depend on them, with time series coming through its IoT services (SAP Learning). SAP Service and Asset Manager runs on SAP BTP Mobile Services (SAP Learning). An assistant that suggests codes or ranks the backlog should read with the planner's identity, so plant and planner group authorizations apply (grounding with authorizations). Integration patterns are covered in how SAP systems talk.
Context: Calder wants to catch failing machines early on the test bench and compressor. Both are cheap to instrument. Sensors would produce a reading every second; SAP plans need a few readings a day. The bench is the plant's bottleneck.
Options: (a) enter readings by hand on rounds and use limits on measuring points in S/4HANA; (b) stream sensor data to an asset health service that evaluates rules and posts notifications, sending only daily summaries into S/4HANA as measurement documents; (c) write every sensor value into S/4HANA as a measurement document.
Decision: (b). Raw signals stay outside the ERP; S/4HANA gets what a planner or a plan needs: a counter value per day and a notification when a rule fires.
Consequences: A second system to run and license. A defined data boundary: which signals become readings, how often, and who owns the rules. Notifications from rules still go through screening, so a person accepts each one. Failure codes must be shared between the rules and S/4HANA catalogs.
What a design review will ask
Where is the boundary between sensor data and SAP measurement documents, and who owns the rules that cross it?
How is the functional location hierarchy governed, and who may change it?
How do maintenance outages reach production capacity, and how far ahead?
Can any automated component release an order, change a plan or post a confirmation without a person?
How will you measure success: unplanned downtime, schedule compliance, rework rate, coding rate, orders settled on time?
Turn a spoken or typed description into a draft notification with technical object, priority and text; structured outputs keep the result in SAP's fields
Failure coding
Predict (classify) and retrieve
Strong
Suggest object part, damage and cause codes from the technician's text and similar past cases; the technician confirms. Measure against the keyword baseline from Step 7
Screening
Retrieve and explain
Moderate
Find duplicates and similar open requests; the gatekeeper decides
Order planning
Retrieve
Strong
Suggest operations, parts and hours from similar past orders; SAP lists order recommendations as of S/4HANA 2025 FPS1 (SAP Learning)
Spare part demand
Predict
Strong
Forecast part usage from plans and failure history, as in time-series forecasting, so the seal set is reordered before the overhaul
Backlog and scheduling
Rank and explain
Moderate
Rank work by risk and production impact; SAP lists a Maintenance Planner Agent for this, consumed with AI Units (SAP)
Condition monitoring
Detect anomalies
Strong
Find unusual readings before a limit is crossed; see anomaly detection
Work order summaries
Summarize
Strong
Summarize a machine's history and open work for the technician before the job, citing the notifications
Time confirmation
Plain automation is enough
Weak for AI
Mobile apps and time sheets; no prediction needed
Settlement
Plain automation is enough
Weak for AI
Rules on master data; AI can flag orders that can't settle, not choose receivers
Capital or expense
Retrieve and explain
Weak for AI to decide
An accounting decision under policy; AI can gather the facts for finance
The line is the same at every step: AI drafts, suggests, ranks and summarizes; a technician confirms codes, a gatekeeper accepts requests, a planner releases orders, and a cost accountant decides what is capital. Human approval in AI workflows and agent permissions show how to enforce that in code. One of the portfolio projects, maintenance notification triage, builds the code, retrieve and rank steps end to end.
Checking parts on the day. Run the availability check when the order enters preparation, and again a week before work starts.
Equipment without a cost center. Orders can't settle, and maintenance cost disappears from budgets until someone finds it.
A catalog profile with hundreds of codes. Technicians pick "other". Keep profiles short and specific to the object type.
Counter-based plans without readings. The plan's dates drift. Make the reading part of a routine, or get it from a sensor.
Treating rework as a new failure. Look for a repeat within weeks on the same object, and ask why the first repair didn't hold.
Letting technicians decide capital or expense. It is finance's call, recorded in the settlement rule.
Streaming raw sensor data into the ERP. Decide the boundary first; S/4HANA needs readings and notifications, not every second of a signal.
Assuming ECC and S/4HANA work the same way. Screening, the phase model and many apps exist only in S/4HANA; a group view across Calder's two systems must allow for that.
Turn the worklist into a planner's action note and test two fixes yourself.
Run python unit1b/maintain_to_settle.py --write (Windows: python unit1b\maintain_to_settle.py --write). Copy unit1b/calder_maintenance_worklist.md to unit1b/calder_maintenance_actions.md.
In the copy, add a column Action to the table. For each line write one sentence: what the owner should do today.
Add a column AI could with one of: extract, suggest codes, predict, rank, summarize or nothing. Use the table under Where AI fits to choose.
In maintain_to_settle.py, find equipment 10000104. Change "CostCenter": "" to "CostCenter": "CP11-460". This is master data adding the compressor to a utilities cost center (a course value).
In READINGS, add ("MP-101", "2026-10-26", 1905.0), after the reading of 2 October. This is the test bench operator entering today's operating hours.
Run the script without options, then with --plans and with --order 4000112. Note how many items remain, what item 6 now says, and the new call and due dates of MP-TB-500H.
Change both edits back.
In the action note, add a heading Fixes tested with three lines: what the cost center fixed, what the reading changed, and whether the 500-hour service still fits the overhaul outage.
Add a heading ECC note with one line on what a group failure history needs from CP20. Preview the file and commit it with Git.
Done when:unit1b/calder_maintenance_actions.md has the eight-line table with Action and AI could filled in for every line, a Fixes tested section that says the list drops to 7 items, the settlement item now reads "rule in place, settlement not run" for cost center CP11-460, and the 500-hour service now calls on 2026-11-08 and falls due on 2026-11-23 (so doing it in the 9 to 11 November outage means doing it early), and an ECC note. Keep it: the capstone at the end of Unit 1B traces one exception across the whole enterprise, and this note is its maintenance part.
Pick one answer for each question. The explanation appears after you choose.
1Calder's test bench is installed at CP11-ASY-TB1, and its flow meter sits under it. Why model the flow meter as equipment under the bench?
Answer: C. Subordinate equipment keeps its own history and can be swapped, while reports on the bench include it. Functional locations are the places; equipment is what gets installed and replaced.
2A technician faces a catalog with three hundred damage codes and picks "other". What fixes it at the source?
Answer: A. Catalog profiles pick the code groups a user sees for a kind of object. A short, relevant list makes the right code easy; blocking or reminders don't make it findable.
3Order 4000123 needs a non-stock pressure transducer. What happens to it in SAP?
Answer: B. Non-stock parts are bought for the order. The requisition is created at save or release, depending on the order type, and the goods receipt on the account-assigned PO charges the order.
4Order 4000112 is technically complete, but its 1,240.00 USD won't settle. What do you check first?
Answer: D. The settlement rule is usually proposed from the reference object's cost center. With none on the equipment, there is no receiver, and the costs sit on the order.
5The 500-hour plan projects its due date from a reading 24 days old. A new reading shows the bench ran less than expected. What happens?
Answer: B. Performance-based plans recalculate planned dates with each measurement document. Fewer hours per day push the call and due dates later, which is what the exercise shows.
6Calder streams vibration data every second from the compressor. Where should those values live?
Answer: B. The ERP needs readings that plans use and notifications when a rule fires, not every value. Keeping raw signals outside and defining the boundary is the decision in the ADR.
7You build an assistant that suggests failure codes from a technician's text. How do you know it helps?
Answer: C. A suggestion is worth having only if it is more accurate than the simple rule, measured on cases with known codes. The technician still confirms, because the cause often needs someone who saw the machine.
8An agent proposes to release orders straight from sensor alerts to save the gatekeeper time. What do you design?
Answer: A. Accepting work commits people, parts and an outage. The agent drafts and explains; the gatekeeper and planner decide in SAP, and the decision is logged.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
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.
ChapterShop floorRun Rockford's floor from its own system: released production orders become dispatch lists per work center, in operation order with their scheduled dates, hours and the parts to issue; operators start and finish each operation by shift and confirm it with the good quantity, the scrap and its reason, and the hours; cheap parts are backflushed by the confirmation; the test bench's machine time is costed apart from the technician's; every pump gets a serial number and its own bench results; a pump that fails twice is repaired on its own rework order; every stop gets a reason, each work center's week reads as OEE, a breakdown goes to the maintenance planner, and in the end the floor runs its own system, fed by messages from the core, including cancellations the floor must agree to, and sending its confirmations back the same way, each of which can fail and must be fixed.
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
Introducing the Phase Model (SAP Learning, Public Edition)— nine phases from initiation to completion; apps and roles per phase (gatekeeper, planner, technician, financial controller); execution stages; phase control codes; scope items 4HH, 4HI, 4VT, 4WM
Modeling Maintenance Processing (SAP Learning)— corrective process BH1 (notification, order, check and release, withdrawal, time and technical confirmation, technical completion, CO settlement, business completion); emergency work and minor work; Report and Repair Malfunction; AI-assisted order recommendations (Private Edition)
Planning, Approval and Preparation (SAP Learning)— approval workflow per order type; material availability check by priority and due date; planning buckets; Rework Orders card; Get Recommendations as of SAP S/4HANA 2025 FPS1 with the Personalized Recommendation service on BTP
Processing Maintenance Orders (SAP Learning)— order header, object list, operations, components, PRTs, settlement rule proposed from the reference object; stock items reserved; non-stock items create purchase requisitions at save or release; PO account assigned to the order; release makes reservations MRP-relevant
Explaining Period-End Closing Maintenance Orders (SAP Learning, Public Edition)— the order is not a permanent cost object; settlement rule; cost center of the reference object as usual receiver, WBS element and fixed asset possible; settlement per ledger; phase-based orders must be fully settled before business completion
Technical Objects: Overview (SAP Learning)— functional locations as hierarchical structures; structure indicator; equipment as an individual physical object; installation at functional locations; maintenance BOMs; assemblies; serial numbers
Assigning Catalogs and Catalog Profiles (SAP Learning)— catalog types 2 tasks, 5 causes, A activities, B object parts, C damage; code groups and codes; catalog profiles per object type; technical object's profile takes priority over the notification type's
Scheduling a Single Cycle Plan and a Performance-Based Strategy Plan (SAP Learning)— scheduling creates call objects (orders, notifications, service entry sheets); call horizon in %, DAY or FKT; shift factors; completion requirement. Companion page on performance-based plans - planned dates from counter readings and estimated annual performance
Working with Measuring Points and Counters (SAP Learning)— measuring points and counters at technical objects; measurement documents from manual entry, mobile apps, barcode or a process control system; damage found can trigger a malfunction report
Cloudification Repository (SAP on GitHub)— objectReleaseInfoLatest.json read 24 October 2026; I_MaintenanceNotification, I_MaintenanceOrderTP, I_MaintOrderConfirmationTP, I_Equipment, I_FunctionalLocation, I_MaintenancePlanBasic, I_MeasuringPoint, I_MeasurementDocument, I_MaintOrderSettlmtDistrRuleTP released; AUFK and RESB not to be released