Between order to cash, procure to pay, plan to produce and record to report sit three smaller processes. Each is small enough to be forgotten in a design, and big enough to stop the others.
Quality management decides whether goods can be used. A goods receipt creates an inspection lot; someone tests a sample; a usage decision releases the stock or blocks it. Problems become quality notifications: complaints against a supplier, complaints from a customer, or internal problems.
Projects carry work that is too big or too unique for one order. An engineered pump made for one customer gets a project with a work breakdown structure (WBS): engineering, purchased parts, manufacturing, commissioning. Costs collect on the WBS elements, revenue comes from milestone billing, and at period end the result goes to finance.
Customer service keeps installed products running. A service order sends a technician; a service confirmation records time, parts and travel; billing turns it into an invoice. In-house repair handles products the customer sends back.
Three ideas carry this topic:
A stock type is a decision. Goods in quality inspection are on the shelf but can't be sold, used or paid for until someone decides.
Projects make money only at the milestones. Work done but not recorded as a milestone is work not billed.
Service is billing. A field visit that is never confirmed, or confirmed but never released, is free work.
These processes rarely fail loudly. They fail as stock nobody can use, cash nobody invoiced and costs nobody saw coming.
Here is Monday morning, 26 October 2026, at Calder Pumpworks, the fictional pump maker this unit follows. The weekly review at the Rockford plant (CP11) brings the quality engineer, the project controller and the customer service manager together:
What the review shows
What it means
Who acts
200 seal kits have waited ten days in quality inspection
The kits can't be sold or fitted, and the supplier's invoice is blocked
Quality engineer
The customer approved the engineered pump's design four weeks ago
55,800.00 USD hasn't been invoiced, because nobody recorded the milestone
Project controller
Engineering on that project is 20% over plan
The project will make less than the quote promised
Project manager
A pump sent back for repair waits for an impeller that arrives after the promised return date
A distributor's customer will wait
Customer service manager, with the buyer
A field repair under warranty is marked billable
The customer would get an invoice they shouldn't
Customer service manager
The same supplier has sent hard seal lips three times this year
Purchasing keeps ordering from them
Quality engineer, with purchasing
The business decision is not "add inspections" or "buy a project tool". It is who decides, how fast, and whether that decision is recorded in SAP, because the other processes, finance, and any AI tool can only read what was recorded.
As of October 2026, all three run inside SAP S/4HANA, with optional cloud services around them.
Quality. In SAP S/4HANA Quality Management, an inspection lot is created by a goods receipt, an order release, a delivery or by hand, but only when an inspection type is active for the material (SAP Learning). The usage decision accepts or rejects the lot, posts the stock, and gives it a quality score from 1 to 100 that feeds supplier evaluation (SAP Learning). Quality notifications document, analyze and correct problems with suppliers, customers, production or storage (SAP Learning). A quality info record per supplier and material can block purchase orders for quality reasons (SAP Learning).
Projects. SAP S/4HANA Project System organizes work as a WBS and as networks of activities with dates, resources and materials (SAP Learning). For a customer project, the sales order item is assigned to a WBS billing element, and milestones release the billing plan (SAP Learning). For SAP S/4HANA Cloud Public Edition, an SAP Community blog on release 2302 described engineer-to-order as a new end-to-end process, with revenue recognized as the project posts costs (SAP Community, 2023).
Service. In SAP S/4HANA Service, the service order plans the work at the customer site, the service confirmation records it, and billing creates the invoice, with revenue posted against the service order items. SAP's Public Edition training calls this solution process 3D2 (SAP Learning, Public Edition). In-house repair is scope item 3XK there (SAP Learning, Public Edition). SAP Field Service Management adds dispatching and mobile apps for technicians, and needs its own licence (SAP Learning).
For AI, SAP's Q2 2025 release highlights listed a Field Service Dispatcher Agent in beta that proposes technicians and times, with a dispatcher verifying each proposal (SAP News, 2025). As of 4 November 2026, SAP's product page for the agent says it works with SAP Field Service Management and Joule but states neither its release status nor its licensing (SAP), so check both with SAP before you plan on it. Where AI creates value in S/4HANA places these in the wider picture.
flowchart LR
subgraph Q[Quality]
GR[Goods receipt] --> L[Inspection lot]
L --> UD[Usage decision]
L -.-> QN[Quality notification]
end
subgraph P[Project]
SO[Sales order item] --> W[WBS and network]
W --> M[Milestones]
M --> B[Milestone billing]
end
subgraph S[Service]
SV[Service order] --> C[Confirmation]
C --> SB[Billing]
end
UD --> ST[Stock usable<br/>or blocked]
B --> F[Period end:<br/>revenue, settlement]
SB --> F
Step
Team
Document created
Posts to finance?
Goods arrive or are produced
Warehouse, production
Goods receipt; inspection lot
Yes (the receipt); the lot itself, no
Sample is tested
Quality technician
Results, defects
No
Lot is decided
Quality engineer
Usage decision; stock posting
A move between stock types
Problem is handled
Quality engineer, supplier, customer
Quality notification; return delivery or returns order
The return, yes
Engineered pump is sold
Sales, project manager
Sales order item on a WBS billing element; project
No
Work is done
Engineering, purchasing, production
Confirmations, purchase orders, production orders on the project
Yes: costs to WBS elements
Milestone is reached
Project manager
Actual milestone date; invoice from the billing plan
Yes: revenue to the project
Period closes
Project controller
Results analysis or revenue recognition; settlement
"Inspection stock is available stock." It is on the shelf, but other processes can't use it until the usage decision.
"A usage decision can be undone." It can't. A wrong decision is corrected by reopening the data and reversing the stock postings by hand.
"Projects are only for large companies." A single engineered pump with its own design, parts and milestones is a project, whatever the company's size.
"Project revenue comes from invoices." Invoices follow milestones. Recognized revenue can run ahead of or behind billing, depending on the method and on whether the cost plan is current.
"Field service just needs a mobile app." The app records the work. Billing, warranty checks and costs live in the service order and its items.
"AI can decide whether to accept a lot." It can find similar past defects and suggest codes. The usage decision stays with the quality engineer.
Pick one answer for each question. The explanation appears after you choose.
1200 seal kits have waited ten days in quality inspection. What is the business cost?
Answer: B. Inspection stock is on the shelf but unavailable to other processes until the usage decision. At Calder the supplier's invoice is blocked for quality as well, so the delay costs sales, service and the supplier relationship.
2The same supplier has delivered hard seal lips three times this year. Which SAP record lets quality stop new orders?
Answer: B. The quality info record holds quality control data per supplier and material and can block purchase orders for quality reasons. Purchasing then gets a message when it tries to order.
3Brennock approved the engineered pump's design four weeks ago, but nothing was invoiced. What is the likely cause?
Answer: A. Milestone billing releases the billing plan item when the milestone gets its actual date. An approval sent by email changes nothing in SAP until someone records it.
4Engineering on the project is 20% over plan, but the cost plan was never updated. What risk does finance face?
Answer: D. Cost-based methods compare actual cost with planned cost. If the plan is too low, the project looks further along than it is and books margin it won't earn.
5A field repair under warranty is confirmed with all items marked billable. What should happen before billing?
Answer: B. What is billable is set on the order and confirmation items. Catching it before the release avoids an invoice, a dispute and a credit memo.
6A vendor offers an agent that accepts or rejects inspection lots from the results. What do you ask for?
Answer: A. A usage decision is final and moves stock. An assistant can summarize results and similar past defects; the engineer decides and the decision is logged.
7Calder wants technicians dispatched and confirming on phones, linked to S/4HANA service orders. Which SAP offering fits?
Answer: C. SAP Field Service Management turns released service orders into service calls, supports dispatching and mobile work, and sends confirmations back. In-house repair is for products sent to Calder.
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 the three processes as gates, envelopes and visits.
Quality is a gate. Material passes through it at goods receipt and after production. The inspection lot is the gate; the usage decision opens it to unrestricted stock or closes it to blocked stock. While the gate is shut, every process downstream sees the material as unavailable.
A project is an envelope. Everything for one engineered pump goes into it: engineering hours, purchased parts, production orders, the sales order item and its milestones. The envelope keeps the costs and revenue together, so finance can say at any period end how far along the job is and what it will earn.
Service is a visit. It starts with a promise (the service order), records what actually happened (the confirmation) and ends in an invoice or in warranty cost. Its unit of value is the item: an hour, a part, a trip.
Each has one moment that most failures trace back to: the gate decision nobody makes, the milestone nobody records, the confirmation nobody releases. The worklist you build in this topic looks for exactly those moments.
An inspection lot governs one quality inspection. It is created by a goods receipt for a purchase order, a goods receipt from a production or process order, a stock transfer, the release of an order, a customer delivery, a batch nearing its expiry, or by hand. It is created only when the quality planner has assigned and activated the matching inspection type in the material master (SAP Learning). For a goods receipt, SAP's configuration training names inspection lot origin 01, notes that the lot is stock-relevant, and recommends posting the goods to inspection stock (SAP Learning). Material in inspection stock can't be used by other processes (SAP Learning).
The lot holds:
Basic data: material, batch, quantity and the goods movement that created it.
Specifications copied from an inspection plan, routing or master recipe when the lot is created. Later changes to the plan don't reach existing lots.
Characteristics, each with a sample size from its sampling procedure. Quantitative characteristics take measurements; qualitative ones take pass or fail or a code.
Defects that the plan didn't foresee, as free text and optionally a code (SAP Learning).
A defect records type, severity, affected quantity, location and attachments. It can be tied to one characteristic or to the whole lot, can start immediate containment such as moving the defective quantity to blocked stock, and can lead to an 8D analysis (SAP Learning).
The usage decision reduces the inspection to accepted or rejected. The quality engineer picks a code from a catalog: usually one acceptance and one rejection code, sometimes "accepted with minor deviations". The code gives a quality score from 1 to 100, which feeds supplier evaluation and can influence automatic supplier selection. If the lot manages stock, acceptance usually posts it to unrestricted-use stock and rejection to blocked stock. The decision is the last step of the lot and can't be undone; correcting it means reopening the data and reversing any stock postings by hand (SAP Learning).
In SAP's model of the Inspection Lot API, A_InspectionLot carries InspectionLotOrigin, Material, Plant, InspectionLotQuantity, Supplier, PurchasingDocument, MaterialDocument, ManufacturingOrder, InspectionLotHasUsageDecision, and the quantities the decision posted, such as InspLotQtyToFree, InspLotQtyToBlocked and InspLotQtyReturnedToSupplier. A_InspLotUsageDecision carries InspectionLotUsageDecisionCode, InspectionLotQualityScore, InspLotUsgeDcsnFollowUpAction and who decided when (SAP Cloud SDK model, 2022).
At Calder (fictional), two lots matter on 26 October:
Lot
Origin
What
Status
010000000041
01, goods receipt for PO 4500000003
200 seal kits CP-SK200 from Ridgeline Bearing Supply (20000003), received 16 October
Three of 13 samples too hard; no decision after ten days
A quality notification documents, analyzes and corrects a problem. It is created with a description, images and links to the material, batch or delivery item; the responsible person records defects, activities (what was done at once) and tasks (what someone must do by a date, notified by email). Open tasks block closing the notification. Status networks and digital signatures can be configured for regulated industries (SAP Learning).
SAP's configuration training lists three origins, each with a manual type Qx and a type Fx created from defect recording (SAP Learning):
Origin
Types
Reference objects
Calder example (fictional)
Customer complaint
Q1, F1
Sales order, delivery
200000019: Hallam reports two seal kits leaking after two weeks
Complaint against supplier
Q2, F2
Purchasing or material document
200000017: hard seal lips in lot 010000000041
Internal problem
Q3, F3
Production order
200000015: ES-300 casing leaked at the pressure test on order 1100016
The same training lists the catalogs a notification is coded from: defect type (catalog type 9), defect location (E), cause (5), task (2) and activity (8). The codes themselves are configuration; the ones in this topic are the course's own.
The supplier complaint flow, in SAP's training, links the notification to the PO item and the goods receipt, moves the stock to blocked stock or to a new inspection lot, sets tasks such as asking the supplier for information, a return or scrapping, optionally runs an 8D or CAPA analysis, and records the root cause and the origin of the complaint, which feeds supplier evaluation. Replacement and return deliveries are booked as goods receipts and goods issues (SAP Learning). A customer complaint can be created from a sales order item or delivery, and its action box can create an advanced returns order, order type RE2 in SAP's standard (SAP Learning).
The quality info record for procurement holds quality control data for one supplier and material. It can carry a release date and quantity, and it can block a selected function, such as creating a purchase order or quotation, for quality reasons; a purchaser who tries then gets a message (SAP Learning). SAP's older documentation adds that the check covers quotations, purchase orders and goods receipts, depending on the control key in the material master's quality view (SAP Help, R/3). In SAP's API model, QualityInProcurement carries Material, Supplier, Plant, ProcurementBlock, BlockReason, QltyInProcmtReleaseValidTo and ReleasedQuantity (SAP Cloud SDK model, 2022).
A block stops new orders. It doesn't recall the purchase orders already open, such as Calder's 4500000009 for 100 more seal kits; that stays a buyer's decision.
Standard ES-200 and ES-300 pumps are made to stock. An engineered pump is designed for one customer, so Calder runs it as a customer project.
A project definition is the frame for the whole project: dates, organizational data, planning parameters and defaults; its controlling area is set once. The WBS is the project's hierarchy; each WBS element is a work package with company code, profit center and plant, and data rolls up the hierarchy (SAP Learning). Networks describe the flow of work. Their activities are internal processing, external processing, services or costs, linked by relationships; milestones mark important events and can drive billing. Confirmations, goods issues and invoices are account-assigned to activities, which aggregate to their WBS element (SAP Learning).
Revenue comes through the billing element, a WBS element marked for billing. The sales order item is assigned to it, and its billing plan takes dates from milestones. When the milestone gets its actual date, for example by confirming the activity, the billing block on the matching billing plan item is lifted; a billing run then updates actual project revenue (SAP Learning, SAP Learning).
Material made or bought for the project can sit in valuated project stock, valued per quantity like make-to-stock material; production orders for it are handled like normal production orders, and goods issued from it put actual costs on the receiving order (SAP Help). For SAP S/4HANA Cloud Public Edition 2302, an SAP Community blog on engineer-to-order (scope item 6GD) names special stock type Q for project stock (SAP Community, 2023).
In SAP's model of the Enterprise Project API, A_EnterpriseProject carries Project, ProjectDescription, ProcessingStatus, ProjectProfileCode, CompanyCode, ProfitCenter and IsBillingRelevant; A_EnterpriseProjectElement carries ProjectElement, WBSElementInternalID, ParentObjectUUID, PlannedStartDate, PlannedEndDate and WBSElementIsBillingElement (SAP Cloud SDK model, 2022).
At Calder (fictional), project CP-26-ENG01 is an engineered lift-station pump, EP-900, for Brennock Water Utility, sold on sales order 1000012 item 10 for 186,000.00 USD:
WBS element
Work
Plan (USD)
How progress is measured
CP-26-ENG01
Billing element; sales order item assigned
CP-26-ENG01.1
Engineering and design
22,000.00
Design approved
CP-26-ENG01.2
Purchased components: casing, 30 kW motor
48,000.00
Actual cost plus open purchase orders
CP-26-ENG01.3
Manufacturing and test, production order to project stock
54,000.00
Operations confirmed
CP-26-ENG01.4
Site commissioning by Calder's field service
12,000.00
Not started
The billing plan bills 30% at design approval, 50% at the factory acceptance test and 20% at delivery to site. Project and WBS numbers, the plan and the milestones are the course's own.
A customer project has costs long before it has invoices, so finance needs a method to recognize revenue and work in process each period.
Results analysis calculates work in process, reserves and cost of sales from a percentage of completion (POC). In the cost-based POC method, POC is actual cost over planned cost, and revenue is planned revenue times that ratio (SAP Learning).
Event-based revenue recognition posts revenue and cost accruals as the project posts. For engineer-to-order in Public Edition 2302, an SAP Community blog lists recognition keys EPMFC (cost-based POC, the default), EPMCC (completed contract) and EPMFR (revenue-based POC), and planned cost and revenue loaded per period drive the POC (SAP Community, 2023).
Settlement moves the results out of the project. For customer projects, only objects with results analysis are settled, using its valuated revenue, cost of sales and reserves; direct settlement sends each object to a receiver such as a profitability segment, and multi-level settlement first collects everything on the top WBS element (SAP Learning). For overhead projects, SAP's training describes a settlement rule in the project master, the Run Settlement-Actual app, and the status Complete once a project is fully settled (SAP Learning).
The lesson for an architect: every cost-based method is only as good as the cost plan. If the plan is too low, the project looks further along than it is, and margin is recognized early. You compute this for Calder in Step 6.
In SAP's Public Edition training for solution process 3D2, the service order is the central document for service at a customer site. Its line items are service products (materials of a service type), parts and expenses, optionally in bundles priced as fixed price or time and material. Planned costs and revenue are calculated on the order; releasing items creates purchase requisitions for what is bought. The service confirmation posts the technician's time and expenses, and procurement costs can be copied in. Releasing the confirmation for billing creates a billing document request; the invoice then posts revenue against the service order items, and because postings go straight to the universal journal, no settlement is needed (SAP Learning, Public Edition). In the private and on-premise editions, SAP's training describes the same idea as item-based accounting: service items are the account assignment objects for costs and revenues (SAP Learning).
With SAP Field Service Management, a released service order becomes a service call there; dispatched activities and their changes update the service order; approved time, effort and expenses come back as a completed service confirmation (SAP Learning).
In-house repair (scope item 3XK in Public Edition) starts with a goods receipt without reference into blocked, customer-specific, non-valuated stock. The repair is created and confirmed, which creates equipment records for serialized objects; prechecks decide Start Repair or Return to Customer; one repair order per object holds service products, parts and expenses; release creates purchase requisitions for non-stock parts; the final confirmation sets the object to Bill; and Prepare for Billing creates the billing document request (SAP Learning, Public Edition).
For long, complex service on assets, SAP S/4HANA 2023 added service with advanced execution, which pairs an execution item in the service order with a billable maintenance order, so service handles the commercial side and maintenance handles planning and execution (SAP Community).
In SAP's API models, A_ServiceOrder carries ServiceOrderType, SoldToParty, ServiceOrderIsReleased, ServiceOrderIsCompleted, ReferenceServiceContract, FSMServiceCall and ReferenceInHouseRepair; A_ServiceOrderItem carries Product, Quantity, ServiceOrderItemCategory, BillableControl, ExecutingServiceEmployee, SrvcDocItemSLADueByDateTime and FSMServiceActivity; A_ServiceOrderDefect carries ServiceDefectCodeGroup and ServiceDefectCode. A_ServiceConfirmationItem carries ActualServiceDuration, ServiceConfItemIsCompleted, BillableControl and ReferenceServiceOrder. The in-house repair API has InhRepairStatus on the repair and InhRepairReferenceEquipment, SerialNumber and InhRepairItemStatus on its items (SAP Cloud SDK models, 2022).
At Calder (fictional), the aftermarket business has these open on 26 October. Order types, rates and confirmation numbers are the course's own; field technicians cost 145.00 USD per hour and the repair shop 120.00:
Document
What
State
Service order 8000031
Brennock lift station 3: vibration on an ES-200, 4.5 h, one seal kit, travel
Confirmed 20 October, not released for billing
Service order 8000032
Seal replacement on an ES-300 at a Hallam customer, under warranty to 31 March 2027
Confirmed 23 October, items marked billable
Service order 8000034
Annual pump check at Brennock's treatment plant
Planned end 21 October, no confirmation
Service order 8000035
Goodwill: replace two leaking seal kits for complaint 200000019
Released, planned 29 October
In-house repair 5000012, repair order 8000033
ES-300 serial ES300-0188 from Hallam
Waiting for impeller CP-IMP300 on PO 4500000014, due 4 November; return promised 30 October
flowchart LR
GR[GR 4900000011<br/>seal kits] --> L[Lot 010000000041]
L --> UD{Usage decision}
UD -->|accept| U[Unrestricted]
UD -->|reject| BL[Blocked] --> RT[Return delivery]
SO[Sales order 1000012] --> W[WBS CP-26-ENG01]
W --> C1[Costs: hours,<br/>POs, production]
W --> MB[Milestone invoice]
C1 --> RR[Revenue<br/>recognition]
MB --> RR
RR --> SET[Settlement to<br/>margin analysis]
SV[Service order 8000031] --> CF[Confirmation] --> BDR[Billing request] --> INV[Invoice]
Step
Posts to finance
Why
Goods receipt into inspection stock
Inventory up, GR/IR credited
The goods belong to Calder, whatever their quality
Inspection results, defects, notification
Nothing
Facts and decisions, not value
Usage decision
A move between stock types; value unchanged
Only availability changes
Return delivery to the supplier
Inventory down; the supplier owes a credit
The goods leave
Project costs: confirmations, goods issues, invoices on the project
Costs to WBS elements or network activities
The work belongs to the project
Goods receipt into valuated project stock
Inventory of the project up, production order credited
The pump is built, not yet sold
Milestone invoice
Receivable, revenue on the billing element
The customer owes a part of the price
Period end: results analysis or event-based revenue recognition
Revenue, cost of sales, work in process or accruals
Cost and revenue matched to progress
Project settlement
To a profitability segment or margin analysis
Results reach the profit and loss view
Service confirmation
Time, parts and expenses as costs on the service order items
The work was done
Service invoice
Receivable, revenue on the service order items
No settlement needed
At Calder (fictional), the engineered pump has 74,300.00 USD of actual cost on 26 October and no invoice yet. On the approved plan of 136,000.00 USD it is 54.6% complete; on today's forecast of 143,900.00 USD it is 51.6% complete. 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 inspection lot table QALS as not to be released, with I_InspectionLot as its successor (Cloudification Repository). The OData API names, entities and fields come from SAP's generated API models, published in 2022 (SAP Cloud SDK models, 2022).
In-house repair API, OData V4 service api_inhouserepair (InHouseRepair, InHouseRepairItem)
Repair and item status, serial number, equipment
Usage decisions, notification tasks, milestone dates, releases for billing and confirmations 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 runs Calder's Monday review on 26 October 2026 for the three processes. It builds one ranked worklist across quality, the engineered-pump project and service; explains an inspection lot and what each usage decision would post; computes the project's forecast and its revenue on two cost bases; traces a service order to billing; and measures defect coding with a keyword baseline and a similar-case search. An optional step reads inspection lots from SAP's sandbox.
flowchart LR
P[calder_pack/<br/>plants, partners] --> S[quality_projects_service.py]
SC[Scenario in the script<br/>lots, notifications,<br/>project, service orders] --> S
S --> W[Ranked worklist]
S --> L[Lot and decision<br/>--lot, --ud]
S --> PR[Project figures<br/>--project]
S --> SV[Service trace<br/>--service]
S --> D[Defects and<br/>similar cases --defects]
S --> M[calder_qps_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 script reads plant CP11 and the names of four business partners from the pack: Brennock Water Utility (10000001), Hallam Fluid Systems (10000002), Tamsfield Foundry (20000002) and Ridgeline Bearing Supply (20000003).
In VS Code, create a new file, paste the script below, and save it as quality_projects_service.py inside the unit1b folder.
Read the blocks INSPECTION_LOTS, QUALITY_NOTIFICATIONS, QN_HISTORY, QUALITY_INFO_RECORDS, PROJECT, SERVICE_ORDERS and IN_HOUSE_REPAIRS first. They are Calder's scenario written as Python data. Every value is invented. PO 4500000003, lot 010000000041 and invoice 5105600002 come from the procure-to-pay topic, production order 1100016 and lot 040000000007 from the plan-to-produce topic, and PO 4500000009 from both. Usage decision codes, catalog codes, order types, project and WBS numbers, statuses and rates are the course's own, not SAP values.
"""Quality, projects and service at Calder: inspection lots, quality notifications,
an engineered-pump project and aftermarket service, with one worklist across all three.
Calder Pumpworks is a fictional company. The pack (unit1b/calder_pack/) and the
scenario below (inspection lots, notifications, the project, service orders,
repairs, rates, costs) are made up for the course. Field names follow SAP's released
APIs where the script mirrors one (API_INSPECTIONLOT_SRV, the quality info record
API, API_ENTERPRISE_PROJECT_SRV, API_SERVICE_ORDER_SRV, API_SERVICE_CONFIRMATION_SRV,
api_inhouserepair), but the logic is a teaching sketch, not SAP's. Usage decision
codes, catalog codes, order types, statuses and rates are course labels.
Run it from your course folder:
python unit1b/quality_projects_service.py the Monday worklist for 2026-10-26
python unit1b/quality_projects_service.py --lot 010000000041 one inspection lot: results, defects, what a decision posts
python unit1b/quality_projects_service.py --lot 010000000041 --ud R what a rejection would post (nothing is changed)
python unit1b/quality_projects_service.py --project CP-26-ENG01 costs, forecast, progress and billing of the project
python unit1b/quality_projects_service.py --service 8000031 one service order from order to billing
python unit1b/quality_projects_service.py --defects defect coding, a keyword baseline and similar past cases
python unit1b/quality_projects_service.py --write write unit1b/calder_qps_worklist.md
python unit1b/quality_projects_service.py --sandbox read inspection lots from 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 re
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import date
from pathlib import Path
HERE = Path(__file__).resolve().parent
PACK = HERE / "calder_pack"
AS_OF = date(2026, 10, 26) # Monday morning review
PLANT = "CP11"
UD_DAYS = 5 # an inspection lot without a usage decision this long is an exception
OVERRUN_PCT = 5.0 # forecast above plan by more than this is a cost signal
CONFIRM_DAYS = 2 # a service order this many days past its planned end without confirmation
BILL_DAYS = 3 # confirmed work not released for billing after this many days
CAUSE_DAYS = 7 # a quality notification this old should have a cause
# ================================================================ quality (fictional)
# Inspection lots, after API_INSPECTIONLOT_SRV (A_InspectionLot). Origin 01 = goods receipt
# for a purchase order, 04 = goods receipt from production (SAP's training).
# Characteristics: (id, text, unit, lower limit, upper limit, sample values).
INSPECTION_LOTS = {
"010000000041": {
"InspectionLotOrigin": "01", "Material": "CP-SK200", "Plant": "CP11", "InspectionLotQuantity": 200,
"Supplier": "20000003", "PurchasingDocument": "4500000003", "MaterialDocument": "4900000011",
"InspLotCreatedOnLocalDate": "2026-10-16", "PricePerUnit": 52.00, "Invoice": "5105600002",
"Characteristics": [
("0010", "Seal lip hardness", "Shore A", 70, 80,
[74, 76, 86, 75, 88, 73, 77, 86, 74, 76, 75, 78, 74]),
("0020", "Inner diameter", "mm", 31.90, 32.10,
[32.00, 31.98, 32.02, 32.01, 31.99, 32.00, 32.03, 31.97, 32.00, 32.01, 32.00, 31.99, 32.02]),
],
"Defects": ["3 of 13 seal lips too hard, fine cracks at the lip edge"],
"UsageDecision": None, "Notification": "200000017",
},
"040000000007": {
"InspectionLotOrigin": "04", "Material": "CP-ES300", "Plant": "CP11", "InspectionLotQuantity": 3,
"ManufacturingOrder": "1100016", "InspLotCreatedOnLocalDate": "2026-10-16", "PricePerUnit": 4100.00,
"Characteristics": [
("0010", "Pressure test, 1.5 x rated pressure, leak", "pass/fail", None, None, ["pass", "pass", "pass"]),
("0020", "Flow at rated head", "m3/h", 118.0, 126.0, [121.4, 122.0, 120.8]),
],
"Defects": [],
"UsageDecision": {"Code": "A", "Score": 100, "DecidedOn": "2026-10-19"},
"Notification": "",
},
}
# Usage decision codes (course labels after SAP's training: one acceptance, one rejection,
# optionally acceptance with minor deviations). Score 1-100, as in SAP's quality score.
UD_CODES = {
"A": ("Accepted", 100, "unrestricted-use stock"),
"A2": ("Accepted with minor deviations", 80, "unrestricted-use stock"),
"R": ("Rejected", 1, "blocked stock"),
}
# Quality notifications. Types Q1 customer complaint, Q2 complaint against supplier,
# Q3 internal problem (SAP's standard examples). Codes are course codes for defect type
# (catalog 9), defect location (E) and cause (5).
QUALITY_NOTIFICATIONS = [
{"QualityNotification": "200000015", "Type": "Q3", "Created": "2026-10-16", "Material": "CP-ES300",
"Reference": "production order 1100016", "Partner": "",
"Text": "ES-300 casing leaked at 1.5x test pressure, sweating through the wall near the outlet",
"DefectType": "LEAK", "Location": "CASING", "Cause": "", "Status": "in process", "Tasks": []},
{"QualityNotification": "200000017", "Type": "Q2", "Created": "2026-10-21", "Material": "CP-SK200",
"Reference": "inspection lot 010000000041, PO 4500000003", "Partner": "20000003",
"Text": "Seal lips too hard on receipt, fine cracks at the lip edge, 3 of 13 samples",
"DefectType": "HARD", "Location": "SEAL", "Cause": "", "Status": "in process",
"Tasks": [("Send 8D report on the seal compound", "Ridgeline Bearing Supply", "2026-10-23", "open"),
("Decide: return the lot or sort 100%", "Quality engineer", "2026-10-28", "open")]},
{"QualityNotification": "200000019", "Type": "Q1", "Created": "2026-10-22", "Material": "CP-SK200",
"Reference": "Hallam delivery of seal kits, September", "Partner": "10000002",
"Text": "Two seal kits leaking after two weeks in service, lip brittle",
"DefectType": "", "Location": "", "Cause": "", "Status": "outstanding",
"Tasks": [("Replace the two kits on goodwill (service order 8000035)", "Customer service manager",
"2026-10-29", "open")]},
]
# Closed notifications of the last twelve months, for coding and similar-case search.
# (notification, created, type, text, defect type, location, cause, what was found)
QN_HISTORY = [
("200000003", "2026-01-14", "Q3", "ES-300 casing sweating at test pressure, porous wall", "LEAK", "CASING", "POROS",
"casting porosity; casings from Tamsfield Foundry, lot sorted"),
("200000006", "2026-03-02", "Q2", "Seal lips cracked on receipt, material hard", "HARD", "SEAL", "MATL",
"Ridgeline changed the seal compound without notice"),
("200000008", "2026-04-17", "Q3", "Impeller rubbing the housing after assembly", "DIM", "IMPELLER", "MACH",
"machining offset at Augsburg (CP21)"),
("200000010", "2026-05-29", "Q1", "Customer reports pump noisy at startup", "NOISE", "BEARING", "ASSY",
"bearing preload set wrong at assembly"),
("200000011", "2026-06-23", "Q3", "Leak at casing outlet during pressure test, pores visible", "LEAK", "CASING",
"POROS", "casting porosity; Tamsfield Foundry"),
("200000012", "2026-07-30", "Q2", "Motor terminal box cracked on arrival", "DAMAGE", "MOTOR", "TRANS",
"transport damage; carrier claim"),
("200000013", "2026-08-19", "Q3", "Seal leaking at test, seal face scratched", "LEAK", "SEAL", "ASSY",
"seal face damaged by handling at assembly"),
("200000014", "2026-09-09", "Q1", "Seal kit leaking after a month, lip brittle and hard", "HARD", "SEAL", "MATL",
"Ridgeline seal compound, same as 200000006"),
]
CAUSES = {"POROS": "casting porosity", "MATL": "supplier material", "MACH": "machining error",
"ASSY": "assembly error", "TRANS": "transport damage"}
# Quality info record (quality in procurement), after the quality info record API.
QUALITY_INFO_RECORDS = [
{"Material": "CP-SK200", "Supplier": "20000003", "Plant": "CP11", "ProcurementBlock": "",
"QltyInProcmtReleaseValidTo": "2027-12-31", "OpenPO": "4500000009 (100 kits, due 2026-11-12)"},
]
# ================================================================ project (fictional)
# An engineered pump made to order, after API_ENTERPRISE_PROJECT_SRV. The top element is the
# billing element; the sales order item is account-assigned to it. Amounts in USD.
PROJECT = {
"Project": "CP-26-ENG01", "ProjectDescription": "Engineered lift-station pump EP-900 for Brennock",
"CompanyCode": "CP10", "Plant": "CP11", "Customer": "10000001", "SalesOrder": "1000012", "SalesOrderItem": "10",
"ContractPrice": 186000.00, "ProcessingStatus": "released",
# (element, description, plan, actual, open commitments, progress %, how progress is measured)
"Elements": [
("CP-26-ENG01.1", "Engineering and design", 22000.00, 26400.00, 0.00, 100, "design approved"),
("CP-26-ENG01.2", "Purchased components", 48000.00, 29000.00, 22500.00, None, "actual plus open POs"),
("CP-26-ENG01.3", "Manufacturing and test", 54000.00, 18900.00, 0.00, 35, "operations confirmed"),
("CP-26-ENG01.4", "Site commissioning", 12000.00, 0.00, 0.00, 0, "not started"),
],
# Billing plan on the sales order item, linked to project milestones.
# (milestone, text, percent, planned date, actual date entered in the project, billed on)
"Milestones": [
("M1", "Design approved by the customer", 30, "2026-09-30", "", ""),
("M2", "Factory acceptance test passed", 50, "2026-11-20", "", ""),
("M3", "Delivered to site", 20, "2026-12-04", "", ""),
],
"Facts": {"M1": "Brennock approved the design by email on 2026-09-30"},
}
# ================================================================ service (fictional)
# Service orders after API_SERVICE_ORDER_SRV; confirmations after API_SERVICE_CONFIRMATION_SRV.
# Order types and rates are course values. Items: (item, product, text, quantity, unit, price, billable).
RATES = {"SRV-FIELD-H": 145.00, "SRV-SHOP-H": 120.00} # USD per hour, course values
SERVICE_ORDERS = {
"8000031": {"Type": "SVF1", "Desc": "Vibration at lift station 3, ES-200 pump", "SoldToParty": "10000001",
"PlannedEnd": "2026-10-20", "FSMServiceCall": "FSM-4468", "Warranty": "",
"Items": [("10", "SRV-FIELD-H", "Field technician", 4.5, "h", 145.00, "billable"),
("20", "CP-SK200", "Seal kit", 1, "pc", 62.00, "billable"),
("30", "EXP-TRAVEL", "Travel", 1, "trip", 180.00, "billable")],
"Confirmation": {"ServiceConfirmation": "9100000044", "Date": "2026-10-20", "Final": True},
"ReleasedForBilling": False, "Billed": False},
"8000032": {"Type": "SVF1", "Desc": "Seal replacement on ES-300 at Hallam's customer", "SoldToParty": "10000002",
"PlannedEnd": "2026-10-23", "FSMServiceCall": "FSM-4472", "Warranty": "2027-03-31",
"Items": [("10", "SRV-FIELD-H", "Field technician", 2.0, "h", 145.00, "billable"),
("20", "CP-SK200", "Seal kit", 1, "pc", 62.00, "billable")],
"Confirmation": {"ServiceConfirmation": "9100000047", "Date": "2026-10-23", "Final": True},
"ReleasedForBilling": False, "Billed": False},
"8000034": {"Type": "SVF1", "Desc": "Annual pump check at Brennock's treatment plant", "SoldToParty": "10000001",
"PlannedEnd": "2026-10-21", "FSMServiceCall": "FSM-4471", "Warranty": "",
"Items": [("10", "SRV-FIELD-H", "Field technician", 6.0, "h", 145.00, "billable")],
"Confirmation": None, "ReleasedForBilling": False, "Billed": False},
"8000035": {"Type": "SVF1", "Desc": "Goodwill: replace two leaking seal kits (complaint 200000019)",
"SoldToParty": "10000002", "PlannedEnd": "2026-10-29", "FSMServiceCall": "FSM-4480",
"Warranty": "goodwill",
"Items": [("10", "SRV-FIELD-H", "Field technician", 1.5, "h", 145.00, "not billable"),
("20", "CP-SK200", "Seal kit", 2, "pc", 62.00, "not billable")],
"Confirmation": None, "ReleasedForBilling": False, "Billed": False},
}
# In-house repairs after the in-house repair API (api_inhouserepair); the repair order is a service order.
IN_HOUSE_REPAIRS = {
"5000012": {"SoldToParty": "10000002", "Product": "CP-ES300", "SerialNumber": "ES300-0188",
"Received": "2026-10-12", "Precheck": ("2026-10-14", "Start Repair"), "RepairOrder": "8000033",
"PromisedReturn": "2026-10-30", "InhRepairItemStatus": "Repair",
"Waiting": ("impeller CP-IMP300 (non-stock, intercompany from CP20)",
"purchase order 4500000014, due 2026-11-04"),
"Items": [("10", "SRV-SHOP-H", "Repair shop", 5.0, "h", 120.00, "billable"),
("20", "CP-IMP300", "Impeller ES-300", 1, "pc", 610.00, "billable")]},
}
OWNER = {
"UD": "Quality engineer (usage decision)",
"REPAIR": "Customer service manager, with the buyer",
"BILL": "Project controller, with billing",
"SBILL": "Customer service manager (release for billing)",
"WARRANTY": "Customer service manager",
"CONFIRM": "Service technician (confirm in the field app)",
"OVERRUN": "Project manager, with the project controller",
"TASK": "Quality engineer (chase the task owner)",
"CAUSE": "Quality engineer (find and code the cause)",
"SUPPLIER": "Quality engineer, with purchasing",
}
RANK = ["UD", "REPAIR", "BILL", "SBILL", "WARRANTY", "CONFIRM", "OVERRUN", "TASK", "CAUSE", "SUPPLIER"]
AREA = {"UD": "quality", "TASK": "quality", "CAUSE": "quality", "SUPPLIER": "quality",
"BILL": "project", "OVERRUN": "project",
"REPAIR": "service", "SBILL": "service", "WARRANTY": "service", "CONFIRM": "service"}
# --------------------------------------------------------------- helpers
def d(text: str) -> date:
return date.fromisoformat(text)
def money(x: float) -> str:
return f"{x:,.2f}"
def check_pack() -> dict:
"""Read the names of Calder's partners from the pack; stop if the pack is missing or edited."""
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.")
names = {bp["BusinessPartner"]: bp["BusinessPartnerFullName"] for bp in partners["BusinessPartners"]}
for needed in ("10000001", "10000002", "20000002", "20000003"):
if needed not in names:
sys.exit(f"Business partner {needed} is missing from the pack. Run make_calder_pack.py again.")
return names
def char_result(lower, upper, values) -> tuple:
"""(number outside the limits, verdict) for one characteristic."""
if lower is None:
bad = sum(1 for v in values if v != "pass")
else:
bad = sum(1 for v in values if not lower <= v <= upper)
return bad, ("accepted" if bad == 0 else "rejected")
def item_value(items: list, billable_only: bool = True) -> float:
return round(sum(q * p for _, _, _, q, _, p, b in items if b == "billable" or not billable_only), 2)
# --------------------------------------------------------------- project arithmetic
def project_figures(p: dict) -> dict:
"""Plan, actual, forecast (estimate at completion) and percentage of completion."""
rows = []
for elem, text, plan, actual, commit, progress, basis in p["Elements"]:
if progress is None: # purchased items: what is spent plus what is ordered
eac = actual + commit
elif progress == 0: # not started: assume the plan
eac = plan
else: # forecast from performance so far
eac = actual / (progress / 100)
rows.append({"elem": elem, "text": text, "plan": plan, "actual": actual, "commit": commit,
"progress": progress, "basis": basis, "eac": round(eac, 2),
"var_pct": round((eac - plan) / plan * 100, 1)})
plan = sum(r["plan"] for r in rows)
actual = sum(r["actual"] for r in rows)
eac = sum(r["eac"] for r in rows)
price = p["ContractPrice"]
poc_plan = actual / plan
poc_eac = actual / eac
billed = sum(price * pct / 100 for _, _, pct, _, _, billed_on in p["Milestones"] if billed_on)
return {"rows": rows, "plan": plan, "actual": actual, "eac": eac, "price": price,
"poc_plan": poc_plan, "poc_eac": poc_eac,
"rev_plan": round(price * poc_plan, 2), "rev_eac": round(price * poc_eac, 2),
"margin_plan": price - plan, "margin_eac": price - eac, "billed": billed}
# --------------------------------------------------------------- worklist
def worklist(names: dict) -> list:
items = []
# Quality: lots waiting for a usage decision
for lot, l in INSPECTION_LOTS.items():
if l["UsageDecision"] is None:
age = (AS_OF - d(l["InspLotCreatedOnLocalDate"])).days
if age > UD_DAYS:
value = l["InspectionLotQuantity"] * l["PricePerUnit"]
extra = f"; supplier invoice {l['Invoice']} blocked" if l.get("Invoice") else ""
items.append({"kind": "UD", "object": f"inspection lot {lot}",
"detail": f"{l['InspectionLotQuantity']} {l['Material']} in quality inspection for {age} days, "
f"{money(value)} USD{extra}"})
# Quality: overdue tasks and missing causes
for n in QUALITY_NOTIFICATIONS:
for text, who, due, status in n["Tasks"]:
if status == "open" and d(due) < AS_OF:
items.append({"kind": "TASK", "object": f"notification {n['QualityNotification']}",
"detail": f"task \"{text}\" ({who}) due {due}, {(AS_OF - d(due)).days} days late"})
# A cause takes time to find, so it is flagged only after CAUSE_DAYS; a defect type is needed at once.
missing = ["defect type"] if not n["DefectType"] else []
if not n["Cause"] and (AS_OF - d(n["Created"])).days > CAUSE_DAYS:
missing.append("cause")
if missing:
items.append({"kind": "CAUSE", "object": f"notification {n['QualityNotification']}",
"detail": f"{n['Type']} \"{n['Text']}\": no {', '.join(missing)}"})
# Quality: repeat supplier problems without a procurement decision
for q in QUALITY_INFO_RECORDS:
hits = [h for h in QN_HISTORY if h[6] == "MATL" and h[4] == "HARD"]
open_hits = [n for n in QUALITY_NOTIFICATIONS if n["Material"] == q["Material"] and n["Type"] == "Q2"]
if len(hits) + len(open_hits) >= 3 and not q["ProcurementBlock"]:
items.append({"kind": "SUPPLIER", "object": f"supplier {q['Supplier']}",
"detail": f"{names[q['Supplier']]}: {len(hits) + len(open_hits)} hard-seal cases on {q['Material']} "
f"in 12 months, no procurement block; open PO {q['OpenPO']}"})
# Project: milestones reached but not billed, and cost signals
f = project_figures(PROJECT)
for ms, text, pct, planned, actual, billed_on in PROJECT["Milestones"]:
fact = PROJECT["Facts"].get(ms, "")
if d(planned) < AS_OF and not billed_on:
amount = PROJECT["ContractPrice"] * pct / 100
why = "actual date not entered in the project, so the billing plan item stays blocked" if not actual else "not billed"
items.append({"kind": "BILL", "object": f"project {PROJECT['Project']}",
"detail": f"milestone {ms} \"{text}\" ({fact}): {money(amount)} USD not invoiced; {why}"})
for r in f["rows"]:
if r["var_pct"] > OVERRUN_PCT:
items.append({"kind": "OVERRUN", "object": f"WBS {r['elem']}",
"detail": f"{r['text']}: forecast {money(r['eac'])} vs plan {money(r['plan'])} (+{r['var_pct']}%), "
f"basis {r['basis']}"})
# Service: repairs late against the promise, confirmations, billing, warranty
for no, r in IN_HOUSE_REPAIRS.items():
part, source = r["Waiting"]
due = d(source.split("due ")[1])
if due > d(r["PromisedReturn"]):
items.append({"kind": "REPAIR", "object": f"in-house repair {no}",
"detail": f"{names[r['SoldToParty']]}, {r['Product']} {r['SerialNumber']}: return promised "
f"{r['PromisedReturn']}, waiting for {part}, {source}"})
for no, s in SERVICE_ORDERS.items():
c = s["Confirmation"]
if c is None and (AS_OF - d(s["PlannedEnd"])).days >= CONFIRM_DAYS:
items.append({"kind": "CONFIRM", "object": f"service order {no}",
"detail": f"{s['Desc']}: planned end {s['PlannedEnd']}, no confirmation from {s['FSMServiceCall']}"})
if c and c["Final"] and not s["ReleasedForBilling"]:
warranty_open = s["Warranty"] and s["Warranty"] != "goodwill" and d(s["Warranty"]) >= AS_OF
billable = item_value(s["Items"])
if warranty_open and billable:
items.append({"kind": "WARRANTY", "object": f"service order {no}",
"detail": f"equipment under warranty to {s['Warranty']}, but {money(billable)} USD "
f"marked billable on confirmation {c['ServiceConfirmation']}"})
elif (AS_OF - d(c["Date"])).days > BILL_DAYS:
items.append({"kind": "SBILL", "object": f"service order {no}",
"detail": f"confirmed {c['Date']} ({c['ServiceConfirmation']}), {money(billable)} USD "
f"not released for billing for {(AS_OF - d(c['Date'])).days} days"})
items.sort(key=lambda r: (RANK.index(r["kind"]), r["object"]))
return items
def print_worklist(items: list) -> None:
print(f"Quality, project and service worklist for plant {PLANT} as of {AS_OF} (fictional Calder data)\n")
print(f" {'#':>2} {'Kind':<9}{'Area':<9}{'Object':<29}Owner")
for i, it in enumerate(items, 1):
print(f" {i:>2} {it['kind']:<9}{AREA[it['kind']]:<9}{it['object']:<29}{OWNER[it['kind']]}")
print(f" {it['detail']}")
counts = {a: sum(1 for it in items if AREA[it["kind"]] == a) for a in ("quality", "project", "service")}
print(f"\n {len(items)} items: " + ", ".join(f"{n} {a}" for a, n in counts.items()) + ".")
print(" Synthetic data: the list shows the method, not results from an SAP system.")
# --------------------------------------------------------------- views
def explain_lot(lot: str, ud: str, names: dict) -> None:
if lot not in INSPECTION_LOTS:
sys.exit(f"Inspection lot {lot} is not in the scenario. Try one of: {', '.join(INSPECTION_LOTS)}")
l = INSPECTION_LOTS[lot]
origin = {"01": "goods receipt for a purchase order", "04": "goods receipt from production"}[l["InspectionLotOrigin"]]
print(f"Inspection lot {lot} (fictional): {l['InspectionLotQuantity']} x {l['Material']} at {l['Plant']}")
print(f" origin {l['InspectionLotOrigin']} ({origin}), created {l['InspLotCreatedOnLocalDate']}")
if l.get("Supplier"):
print(f" supplier {l['Supplier']} {names[l['Supplier']]}, PO {l['PurchasingDocument']}, "
f"material document {l['MaterialDocument']}")
if l.get("ManufacturingOrder"):
print(f" production order {l['ManufacturingOrder']}")
print("\nResults by characteristic:")
verdicts = []
for cid, text, unit, lo, hi, values in l["Characteristics"]:
bad, verdict = char_result(lo, hi, values)
verdicts.append(verdict)
limits = f"{lo}-{hi} {unit}" if lo is not None else unit
shown = ", ".join(str(v) for v in values)
print(f" {cid} {text:<42}limits {limits:<19}n={len(values):<3}outside {bad:<3}{verdict}")
print(f" values: {shown}")
for text in l["Defects"]:
print(f" defect recorded: {text}")
if l["Notification"]:
print(f" quality notification {l['Notification']} created from the defect")
proposal = "A" if all(v == "accepted" for v in verdicts) and not l["Defects"] else "R"
if l["UsageDecision"]:
code = l["UsageDecision"]["Code"]
print(f"\nUsage decision {code} ({UD_CODES[code][0]}) on {l['UsageDecision']['DecidedOn']}, "
f"quality score {l['UsageDecision']['Score']}; stock posted to {UD_CODES[code][2]}.")
return
age = (AS_OF - d(l["InspLotCreatedOnLocalDate"])).days
print(f"\nNo usage decision after {age} days. The results point to code {proposal} "
f"({UD_CODES[proposal][0]}); the quality engineer decides.")
code = ud or proposal
text, score, target = UD_CODES[code]
value = l["InspectionLotQuantity"] * l["PricePerUnit"]
print(f"\nIf the decision is {code} ({text}), quality score {score} (nothing is changed by this script):")
print(f" stock posting: {l['InspectionLotQuantity']} {l['Material']} from quality inspection to {target}")
print(f" value stays {money(value)} USD in inventory; only the stock type changes")
if code.startswith("A"):
if l.get("Invoice"):
print(f" invoice {l['Invoice']} can be released once the goods are usable; the payment run picks it up")
print(" the kits become available to sales orders and to service orders that need them")
else:
print(" follow-ups (decided in the quality notification, not automatic):")
print(" return delivery to the supplier: a goods issue from blocked stock, then a credit from the supplier")
print(" or sort 100% and accept the good part with a new inspection lot")
if l.get("Invoice"):
print(f" invoice {l['Invoice']} stays blocked until the return and the credit memo are settled with purchasing")
print(f" the score feeds supplier evaluation for {names.get(l.get('Supplier', ''), 'the supplier')}")
print("\n Synthetic data for a fictional lot, not from an SAP system.")
def explain_project(project: str, names: dict) -> None:
if project != PROJECT["Project"]:
sys.exit(f"Project {project} is not in the scenario. Try {PROJECT['Project']}")
p = PROJECT
f = project_figures(p)
print(f"Project {p['Project']}: {p['ProjectDescription']} (fictional)")
print(f" customer {p['Customer']} {names[p['Customer']]}, sales order {p['SalesOrder']} item {p['SalesOrderItem']} "
f"on billing element {p['Project']}, price {money(p['ContractPrice'])} USD\n")
print(f" {'WBS element':<15}{'Description':<25}{'Plan':>11}{'Actual':>11}{'Open PO':>10}{'Forecast':>11}{'Var':>8}")
for r in f["rows"]:
print(f" {r['elem']:<15}{r['text']:<25}{money(r['plan']):>11}{money(r['actual']):>11}"
f"{money(r['commit']):>10}{money(r['eac']):>11}{r['var_pct']:>7.1f}%")
print(f" {'total':<40}{money(f['plan']):>11}{money(f['actual']):>11}{'':>10}{money(f['eac']):>11}"
f"{(f['eac'] - f['plan']) / f['plan'] * 100:>7.1f}%")
print(f"\n Margin: planned {money(f['margin_plan'])} ({f['margin_plan'] / f['price']:.1%}), "
f"forecast {money(f['margin_eac'])} ({f['margin_eac'] / f['price']:.1%})")
print("\nRevenue recognition, cost-based percentage of completion (POC = actual cost / total cost):")
print(f" on the approved plan: POC {f['poc_plan']:.1%}, revenue {money(f['rev_plan'])}, "
f"margin recognized {money(f['rev_plan'] - f['actual'])}")
print(f" on the forecast: POC {f['poc_eac']:.1%}, revenue {money(f['rev_eac'])}, "
f"margin recognized {money(f['rev_eac'] - f['actual'])}")
print(f" difference: {money(f['rev_plan'] - f['rev_eac'])} USD of revenue recognized early if the plan isn't updated")
print(f" billed so far {money(f['billed'])}: recognized revenue exceeds billings by {money(f['rev_eac'] - f['billed'])}")
print("\nBilling plan (sales order item, linked to milestones):")
for ms, text, pct, planned, actual, billed_on in p["Milestones"]:
state = f"billed {billed_on}" if billed_on else ("unblocked, to bill" if actual else "blocked until the actual date is entered")
print(f" {ms} {text:<34}{pct:>3}% {money(p['ContractPrice'] * pct / 100):>10} planned {planned} {state}")
print("\n Synthetic numbers for a fictional project, not from an SAP system.")
def explain_service(no: str, names: dict) -> None:
if no in IN_HOUSE_REPAIRS:
r = IN_HOUSE_REPAIRS[no]
print(f"In-house repair {no} (fictional): {r['Product']} serial {r['SerialNumber']} from "
f"{r['SoldToParty']} {names[r['SoldToParty']]}")
print(f" {r['Received']} received into blocked, customer-owned stock (goods receipt without reference)")
print(f" {r['Precheck'][0]} precheck: {r['Precheck'][1]}; repair order {r['RepairOrder']} released")
print(f" waiting: {r['Waiting'][0]}, {r['Waiting'][1]}; return promised {r['PromisedReturn']}")
print(f" to bill after the final confirmation: {money(item_value(r['Items']))} USD")
print("\n Synthetic data for a fictional repair, not from an SAP system.")
return
if no not in SERVICE_ORDERS:
sys.exit(f"Service order {no} is not in the scenario. Try one of: {', '.join(list(SERVICE_ORDERS) + list(IN_HOUSE_REPAIRS))}")
s = SERVICE_ORDERS[no]
print(f"Service order {no} ({s['Type']}): {s['Desc']} (fictional)")
print(f" sold-to {s['SoldToParty']} {names[s['SoldToParty']]}, planned end {s['PlannedEnd']}, "
f"field service call {s['FSMServiceCall']}" + (f", warranty {s['Warranty']}" if s["Warranty"] else ""))
print(f"\n {'Item':<6}{'Product':<13}{'Text':<20}{'Qty':>6} {'Unit':<6}{'Price':>9}{'Value':>10} Billing")
for item, prod, text, qty, unit, price, bill in s["Items"]:
print(f" {item:<6}{prod:<13}{text:<20}{qty:>6} {unit:<6}{money(price):>9}{money(qty * price):>10} {bill}")
c = s["Confirmation"]
if not c:
print("\n No service confirmation yet: no actual costs on the order, nothing to bill.")
else:
print(f"\n Confirmation {c['ServiceConfirmation']} on {c['Date']} (final): the confirmed time, parts and "
"expenses post as actual costs on the order items")
billable = item_value(s["Items"])
if not billable:
print(" nothing billable: no billing document request; the costs stay as warranty or goodwill cost")
else:
state = "released for billing" if s["ReleasedForBilling"] else "not released for billing"
print(f" {state}: billing document request for {money(billable)} USD, then the customer invoice")
print(" the invoice posts revenue against the service order items, next to their costs")
print("\n Synthetic numbers for a fictional order, not from an SAP system.")
# --------------------------------------------------------------- defects: baseline and retrieval
STOP = {"a", "an", "the", "at", "on", "of", "in", "after", "and", "to", "from", "is", "with", "near", "two", "3", "13",
"x", "es-300", "1.5x", "samples", "service", "pump"}
DEFECT_WORDS = {"LEAK": ("leak", "leaking", "leaked", "sweating", "drip"), "HARD": ("hard", "brittle", "cracked", "cracks"),
"NOISE": ("noisy", "noise", "rattle"), "DIM": ("rubbing", "diameter", "offset"),
"DAMAGE": ("cracked", "broken", "dent")}
def words(text: str) -> set:
return {w for w in re.findall(r"[a-z0-9.\-]+", text.lower()) if w not in STOP}
def similar(text: str, k: int = 2) -> list:
"""Closed notifications ranked by word overlap (Jaccard): the simplest retrieval baseline."""
q = words(text)
scored = []
for h in QN_HISTORY:
w = words(h[3] + " " + h[7])
score = len(q & w) / len(q | w) if q | w else 0
scored.append((round(score, 2), h))
scored.sort(key=lambda s: (-s[0], s[1][0]))
return [s for s in scored[:k] if s[0] > 0]
def suggest_defect(text: str) -> str:
low = text.lower()
best = max(DEFECT_WORDS, key=lambda k: sum(w in low for w in DEFECT_WORDS[k]))
return best if any(w in low for w in DEFECT_WORDS[best]) else ""
def defects_report() -> None:
rows = QN_HISTORY
coded = [h for h in rows if h[4] and h[6]]
print(f"Quality notifications at {PLANT}, last 12 months (fictional)\n")
print(f" {len(rows)} closed; {len(coded)} with defect type and cause = {len(coded) / len(rows):.0%}")
causes = {}
for h in rows:
causes[h[6]] = causes.get(h[6], 0) + 1
print(" causes: " + ", ".join(f"{CAUSES[c]} {n}" for c, n in sorted(causes.items(), key=lambda x: (-x[1], x[0]))))
correct = sum(1 for h in rows if suggest_defect(h[3]) == h[4])
print(f" keyword baseline on the closed cases: defect type right {correct} of {len(rows)}")
print("\n Open notifications: suggested defect type and the most similar closed cases")
for n in QUALITY_NOTIFICATIONS:
print(f" {n['QualityNotification']} {n['Type']} \"{n['Text']}\"")
code = n["DefectType"] or suggest_defect(n["Text"])
how = "coded" if n["DefectType"] else "keyword suggestion"
print(f" defect type {code or '?'} ({how})")
for score, h in similar(n["Text"]):
print(f" similar {h[0]} ({score:.2f}): \"{h[3]}\" -> cause {CAUSES[h[6]]}: {h[7]}")
print("\n A person confirms every code and cause. Synthetic data: the rates show the method, not a benchmark.")
def write_markdown(items: list) -> Path:
out = HERE / "calder_qps_worklist.md"
lines = [f"# Quality, project and service worklist, {PLANT}, {AS_OF}", "",
"Fictional Calder Pumpworks data. Synthetic: it shows the method, not results from an SAP system.", "",
"```mermaid", "flowchart LR",
" GR[Goods receipt] --> L[Inspection lot] --> UD[Usage decision] --> ST[Stock usable or blocked]",
" L --> QN[Quality notification]", " SO[Sales order item] --> P[Project, WBS]",
" P --> B[Milestone billing]", " S[Service order] --> C[Confirmation] --> BR[Billing]", "```", "",
"| # | Kind | Area | Object | Owner | Detail |", "| --- | --- | --- | --- | --- | --- |"]
for i, it in enumerate(items, 1):
lines.append(f"| {i} | {it['kind']} | {AREA[it['kind']]} | {it['object']} | {OWNER[it['kind']]} | "
f"{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_INSPECTIONLOT_SRV/A_InspectionLot"
query = urllib.parse.urlencode({"$top": "50", "$format": "json",
"$select": "InspectionLot,InspectionLotOrigin,Material,Plant,"
"InspectionLotQuantity,InspectionLotHasUsageDecision"})
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.")
lots = data.get("d", {}).get("results", [])
origins, decided = {}, 0
for lot in lots:
origins[lot.get("InspectionLotOrigin", "")] = origins.get(lot.get("InspectionLotOrigin", ""), 0) + 1
decided += 1 if lot.get("InspectionLotHasUsageDecision") else 0
print(f"SAP sandbox: {len(lots)} inspection lots read (SAP demo data, not Calder's)\n")
print(" InspectionLotOrigin: " + ", ".join(f"'{o or '(empty)'}' x{n}" for o, n in sorted(origins.items())))
print(f" with a usage decision: {decided} of {len(lots)}")
for lot in lots[:3]:
print(f" e.g. {lot.get('InspectionLot')} {lot.get('Material')} plant {lot.get('Plant')} "
f"origin {lot.get('InspectionLotOrigin')}")
# --------------------------------------------------------------- main
def main() -> None:
parser = argparse.ArgumentParser(description="Quality, projects and service at Calder (fictional data).")
parser.add_argument("--lot", help="explain one inspection lot, e.g. 010000000041")
parser.add_argument("--ud", help="with --lot: show what usage decision A, A2 or R would post")
parser.add_argument("--project", help="explain the project, e.g. CP-26-ENG01")
parser.add_argument("--service", help="explain a service order or in-house repair, e.g. 8000031")
parser.add_argument("--defects", action="store_true", help="defect coding, keyword baseline, similar cases")
parser.add_argument("--write", action="store_true", help="write unit1b/calder_qps_worklist.md")
parser.add_argument("--sandbox", action="store_true", help="read inspection lots from SAP's sandbox")
args = parser.parse_args()
if args.sandbox:
sandbox()
return
if args.ud and args.ud.upper() not in UD_CODES:
sys.exit(f"Unknown usage decision code {args.ud}. Use one of: {', '.join(UD_CODES)}")
names = check_pack()
if args.lot:
explain_lot(args.lot, (args.ud or "").upper(), names)
elif args.project:
explain_project(args.project, names)
elif args.service:
explain_service(args.service, names)
elif args.defects:
defects_report()
else:
items = worklist(names)
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:
Quality, project and service worklist for plant CP11 as of 2026-10-26 (fictional Calder data)
# Kind Area Object Owner
1 UD quality inspection lot 010000000041 Quality engineer (usage decision)
200 CP-SK200 in quality inspection for 10 days, 10,400.00 USD; supplier invoice 5105600002 blocked
2 REPAIR service in-house repair 5000012 Customer service manager, with the buyer
Hallam Fluid Systems, CP-ES300 ES300-0188: return promised 2026-10-30, waiting for impeller CP-IMP300 (non-stock, intercompany from CP20), purchase order 4500000014, due 2026-11-04
3 BILL project project CP-26-ENG01 Project controller, with billing
milestone M1 "Design approved by the customer" (Brennock approved the design by email on 2026-09-30): 55,800.00 USD not invoiced; actual date not entered in the project, so the billing plan item stays blocked
4 SBILL service service order 8000031 Customer service manager (release for billing)
confirmed 2026-10-20 (9100000044), 894.50 USD not released for billing for 6 days
5 WARRANTY service service order 8000032 Customer service manager
equipment under warranty to 2027-03-31, but 352.00 USD marked billable on confirmation 9100000047
6 CONFIRM service service order 8000034 Service technician (confirm in the field app)
Annual pump check at Brennock's treatment plant: planned end 2026-10-21, no confirmation from FSM-4471
7 OVERRUN project WBS CP-26-ENG01.1 Project manager, with the project controller
Engineering and design: forecast 26,400.00 vs plan 22,000.00 (+20.0%), basis design approved
8 OVERRUN project WBS CP-26-ENG01.2 Project manager, with the project controller
Purchased components: forecast 51,500.00 vs plan 48,000.00 (+7.3%), basis actual plus open POs
9 TASK quality notification 200000017 Quality engineer (chase the task owner)
task "Send 8D report on the seal compound" (Ridgeline Bearing Supply) due 2026-10-23, 3 days late
10 CAUSE quality notification 200000015 Quality engineer (find and code the cause)
Q3 "ES-300 casing leaked at 1.5x test pressure, sweating through the wall near the outlet": no cause
11 CAUSE quality notification 200000019 Quality engineer (find and code the cause)
Q1 "Two seal kits leaking after two weeks in service, lip brittle": no defect type
12 SUPPLIER quality supplier 20000003 Quality engineer, with purchasing
Ridgeline Bearing Supply: 3 hard-seal cases on CP-SK200 in 12 months, no procurement block; open PO 4500000009 (100 kits, due 2026-11-12)
12 items: 5 quality, 3 project, 4 service.
Synthetic data: the list shows the method, not results from an SAP system.
How to read it:
Item 1 is the gate. Two hundred seal kits are on the shelf, but nobody can sell them, fit them on a service visit, or pay Ridgeline for them, until the quality engineer decides.
Items 2 to 6 are customers and cash. A promised repair date that the parts can't meet, a milestone that was reached but not recorded, a confirmed visit not released for billing, a warranty visit about to be billed, and a visit with no confirmation at all.
Items 7 and 8 are the project's forecast. Engineering is finished and cost 20% more than planned; purchased components will end 7.3% over once the open purchase orders are invoiced.
Items 9 to 12 are quality follow-up: an overdue 8D report, a leak without a cause, a complaint without a defect type, and a supplier with three hard-seal cases and no block.
The ranking is the script's own: stock and customer promises first, then cash, then cost signals, then follow-up. A plant chooses its own order. The thresholds (UD_DAYS, OVERRUN_PCT, CONFIRM_DAYS, BILL_DAYS, CAUSE_DAYS) are at the top of the script.
Inspection lot 010000000041 (fictional): 200 x CP-SK200 at CP11
origin 01 (goods receipt for a purchase order), created 2026-10-16
supplier 20000003 Ridgeline Bearing Supply, PO 4500000003, material document 4900000011
Results by characteristic:
0010 Seal lip hardness limits 70-80 Shore A n=13 outside 3 rejected
values: 74, 76, 86, 75, 88, 73, 77, 86, 74, 76, 75, 78, 74
0020 Inner diameter limits 31.9-32.1 mm n=13 outside 0 accepted
values: 32.0, 31.98, 32.02, 32.01, 31.99, 32.0, 32.03, 31.97, 32.0, 32.01, 32.0, 31.99, 32.02
defect recorded: 3 of 13 seal lips too hard, fine cracks at the lip edge
quality notification 200000017 created from the defect
No usage decision after 10 days. The results point to code R (Rejected); the quality engineer decides.
If the decision is R (Rejected), quality score 1 (nothing is changed by this script):
stock posting: 200 CP-SK200 from quality inspection to blocked stock
value stays 10,400.00 USD in inventory; only the stock type changes
follow-ups (decided in the quality notification, not automatic):
return delivery to the supplier: a goods issue from blocked stock, then a credit from the supplier
or sort 100% and accept the good part with a new inspection lot
invoice 5105600002 stays blocked until the return and the credit memo are settled with purchasing
the score feeds supplier evaluation for Ridgeline Bearing Supply
Synthetic data for a fictional lot, not from an SAP system.
Now see what an acceptance would post instead, with --lot 010000000041 --ud A. The part after "No usage decision" changes to:
If the decision is A (Accepted), quality score 100 (nothing is changed by this script):
stock posting: 200 CP-SK200 from quality inspection to unrestricted-use stock
value stays 10,400.00 USD in inventory; only the stock type changes
invoice 5105600002 can be released once the goods are usable; the payment run picks it up
the kits become available to sales orders and to service orders that need them
Synthetic data for a fictional lot, not from an SAP system.
How to read it:
The results point one way, the decision is still a person's. Three hardness values (86, 88, 86) are above the 80 Shore A limit. The script proposes rejection, but a quality engineer might also accept the lot with minor deviations after sorting, or return it. That is why the script prints what each choice would do and changes nothing.
The value doesn't move, availability does. Both decisions keep 10,400.00 USD in inventory; rejection moves it to blocked stock, and the return delivery and credit come after, through the supplier complaint.
The score matters later. A rejection gives score 1 for Ridgeline on this material, which feeds supplier evaluation.
Try --lot 040000000007 to see a lot that was decided: the three ES-300 pumps that passed the pressure test.
Project CP-26-ENG01: Engineered lift-station pump EP-900 for Brennock (fictional)
customer 10000001 Brennock Water Utility, sales order 1000012 item 10 on billing element CP-26-ENG01, price 186,000.00 USD
WBS element Description Plan Actual Open PO Forecast Var
CP-26-ENG01.1 Engineering and design 22,000.00 26,400.00 0.00 26,400.00 20.0%
CP-26-ENG01.2 Purchased components 48,000.00 29,000.00 22,500.00 51,500.00 7.3%
CP-26-ENG01.3 Manufacturing and test 54,000.00 18,900.00 0.00 54,000.00 0.0%
CP-26-ENG01.4 Site commissioning 12,000.00 0.00 0.00 12,000.00 0.0%
total 136,000.00 74,300.00 143,900.00 5.8%
Margin: planned 50,000.00 (26.9%), forecast 42,100.00 (22.6%)
Revenue recognition, cost-based percentage of completion (POC = actual cost / total cost):
on the approved plan: POC 54.6%, revenue 101,616.18, margin recognized 27,316.18
on the forecast: POC 51.6%, revenue 96,037.53, margin recognized 21,737.53
difference: 5,578.65 USD of revenue recognized early if the plan isn't updated
billed so far 0.00: recognized revenue exceeds billings by 96,037.53
Billing plan (sales order item, linked to milestones):
M1 Design approved by the customer 30% 55,800.00 planned 2026-09-30 blocked until the actual date is entered
M2 Factory acceptance test passed 50% 93,000.00 planned 2026-11-20 blocked until the actual date is entered
M3 Delivered to site 20% 37,200.00 planned 2026-12-04 blocked until the actual date is entered
Synthetic numbers for a fictional project, not from an SAP system.
How to read it:
The forecast uses three rules, one per kind of work. Engineering is done, so its forecast is its actual cost. Purchased components are what was spent plus what is on open purchase orders. Manufacturing is forecast from performance: 18,900.00 USD for 35% of the work points to 54,000.00 USD in total. Commissioning hasn't started, so it keeps its plan. Real projects use more careful methods; the point is that each element needs a stated basis.
The two percentages of completion are the lesson. On the approved plan, the project is 54.6% complete and has earned 27,316.18 USD of margin. On the forecast, it is 51.6% complete and has earned 21,737.53 USD. The difference, 5,578.65 USD of revenue, is margin recognized too early because nobody updated the plan. A cost-based method can only be as right as its cost plan.
Revenue runs ahead of billing. The project has recognized 96,037.53 USD on the forecast basis and billed nothing, because the design milestone was never recorded. That gap is what finance would explain as revenue not yet billed.
Service order 8000031 (SVF1): Vibration at lift station 3, ES-200 pump (fictional)
sold-to 10000001 Brennock Water Utility, planned end 2026-10-20, field service call FSM-4468
Item Product Text Qty Unit Price Value Billing
10 SRV-FIELD-H Field technician 4.5 h 145.00 652.50 billable
20 CP-SK200 Seal kit 1 pc 62.00 62.00 billable
30 EXP-TRAVEL Travel 1 trip 180.00 180.00 billable
Confirmation 9100000044 on 2026-10-20 (final): the confirmed time, parts and expenses post as actual costs on the order items
not released for billing: billing document request for 894.50 USD, then the customer invoice
the invoice posts revenue against the service order items, next to their costs
Synthetic numbers for a fictional order, not from an SAP system.
Now the repair, with --service 5000012:
In-house repair 5000012 (fictional): CP-ES300 serial ES300-0188 from 10000002 Hallam Fluid Systems
2026-10-12 received into blocked, customer-owned stock (goods receipt without reference)
2026-10-14 precheck: Start Repair; repair order 8000033 released
waiting: impeller CP-IMP300 (non-stock, intercompany from CP20), purchase order 4500000014, due 2026-11-04; return promised 2026-10-30
to bill after the final confirmation: 1,210.00 USD
Synthetic data for a fictional repair, not from an SAP system.
The field visit is done, confirmed and costed; only the release for billing is missing, which is the cheapest item on the list to fix. The repair is the opposite: the work can't be finished, because the impeller for an ES-300 is a non-stock part bought from Augsburg, and its purchase order is due five days after the date Calder promised. Try --service 8000032 to see the warranty order, and --service 8000034 for the order with no confirmation.
#Step 8: Look at defect coding and similar cases, and write the worklist
Quality notifications at CP11, last 12 months (fictional)
8 closed; 8 with defect type and cause = 100%
causes: assembly error 2, supplier material 2, casting porosity 2, machining error 1, transport damage 1
keyword baseline on the closed cases: defect type right 6 of 8
Open notifications: suggested defect type and the most similar closed cases
200000015 Q3 "ES-300 casing leaked at 1.5x test pressure, sweating through the wall near the outlet"
defect type LEAK (coded)
similar 200000003 (0.31): "ES-300 casing sweating at test pressure, porous wall" -> cause casting porosity: casting porosity; casings from Tamsfield Foundry, lot sorted
similar 200000011 (0.25): "Leak at casing outlet during pressure test, pores visible" -> cause casting porosity: casting porosity; Tamsfield Foundry
200000017 Q2 "Seal lips too hard on receipt, fine cracks at the lip edge, 3 of 13 samples"
defect type HARD (coded)
similar 200000006 (0.25): "Seal lips cracked on receipt, material hard" -> cause supplier material: Ridgeline changed the seal compound without notice
similar 200000014 (0.17): "Seal kit leaking after a month, lip brittle and hard" -> cause supplier material: Ridgeline seal compound, same as 200000006
200000019 Q1 "Two seal kits leaking after two weeks in service, lip brittle"
defect type LEAK (keyword suggestion)
similar 200000014 (0.29): "Seal kit leaking after a month, lip brittle and hard" -> cause supplier material: Ridgeline seal compound, same as 200000006
similar 200000013 (0.15): "Seal leaking at test, seal face scratched" -> cause assembly error: seal face damaged by handling at assembly
A person confirms every code and cause. Synthetic data: the rates show the method, not a benchmark.
How to read it:
The keyword baseline gets six of eight closed cases right. It misses two in instructive ways: "cracked" on a motor terminal box was transport damage, not a hard material, and "leaking ... brittle and hard" was a hard seal that leaked as a result. Keywords find the symptom; the defect is often one step behind it.
Complaint 200000019 shows the same trap. The keyword suggestion says LEAK. The most similar closed case, 200000014, says the cause was Ridgeline's seal compound, the same problem as lot 010000000041. Hallam's complaint and Calder's incoming inspection are probably one problem, which a person should confirm.
The casing leak 200000015 finds two earlier cases of casting porosity from Tamsfield Foundry. That is a lead for the quality engineer, not a cause to code without checking the casing.
Similar-case search here is word overlap (Jaccard similarity: shared words divided by all words). It is the bar a better method, such as embeddings or retrieval over SAP data, has to beat.
Write the worklist file:
Windows (PowerShell):
python unit1b\quality_projects_service.py --write
macOS / Linux:
python unit1b/quality_projects_service.py --write
The last line should read wrote unit1b/calder_qps_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 twelve items with owners.
#Step 9 (optional): Read inspection lots 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="...".
The output has this shape. Values in angle brackets come from SAP's demo data and change over time:
SAP sandbox: <n> inspection lots read (SAP demo data, not Calder's)
InspectionLotOrigin: '<origin>' x<count>
with a usage decision: <count> of <n>
e.g. <lot> <material> plant <plant> origin <origin>
The script reads up to 50 lots from API_INSPECTIONLOT_SRV with $select. A valid "empty" result is 0 inspection lots read: the sandbox simply has none at that moment. The sandbox is read-only.
Two lots after the inspection lot API, with characteristics, sample values and defects; the course's usage decision codes, scores and target stock
QUALITY_NOTIFICATIONS, QN_HISTORY, CAUSES
Open customer, supplier and internal notifications with tasks; closed cases with codes and findings
QUALITY_INFO_RECORDS
One supplier and material record with its procurement block
PROJECT, project_figures()
The engineered-pump project: WBS elements with plan, actual, commitments and progress; billing plan; forecast and percentage of completion on two bases
SERVICE_ORDERS, IN_HOUSE_REPAIRS, RATES
Field service orders with items, billing control, warranty and confirmations; one in-house repair waiting for a part
worklist()
Finds lots without decisions, overdue tasks, missing codes, repeat supplier cases, unbilled milestones, cost overruns, late repairs, missing confirmations, unreleased billing and warranty conflicts, and ranks them
explain_lot(), --lot, --ud
Results per characteristic and what a decision would post; nothing is changed
explain_project(), --project
Plan, actual, forecast, margin, revenue on two bases and the billing plan
explain_service(), --service
A service order's items, confirmation and billing state, or a repair's steps
defects_report(), suggest_defect(), similar()
Coding rate, a keyword baseline and word-overlap search for similar closed cases
--write, sandbox()
The worklist as Markdown; up to 50 sandbox inspection lots counted by origin and decision
Where it sits. At Calder (fictional), all three processes for Rockford (CP11) run in the CP10 S/4HANA system: quality management on goods receipts and production, Project System for engineered pumps, and S/4HANA Service for field service and repairs. Technicians would work in SAP Field Service Management, which exchanges service calls and confirmations with S/4HANA. Suppliers answer complaints and 8D tasks through the supplier portal. Augsburg's incoming inspections of machined impellers run on ECC. An assistant that ranks, codes or flags reads released interfaces and writes only drafts.
Non-functional requirements that decide the design
Requirement
Typical target
If missed
Decision latency
Usage decisions within a set number of days of the last result, owned by a named role
Stock and invoices wait; sales and service see shortages that aren't real
Traceability
Every lot, notification and decision linked to its PO, order, delivery or customer, with who decided
Recalls and audits can't be answered
Project data freshness
Cost plans updated when scope changes; actual costs posted by period end
Revenue recognized on stale plans
Field sync
Confirmations from the field reach S/4HANA within a day
Visits unbilled; costs late
Billing correctness
Warranty and contract coverage checked before release
Wrong invoices and credit memos
Security
Only quality roles make usage decisions; only project roles change plans and milestones; customer data in the field app limited to the job
Unauthorized decisions; data exposure on mobile devices
Integration and identity. SAP Field Service Management receives released service orders as service calls and returns approved time and expenses as confirmations, and needs its own licence (SAP Learning). Inside S/4HANA, S/4HANA Service creates follow-up documents such as reservations, purchase requisitions and SD sales orders through synchronous calls at save time, without the old CRM middleware (SAP Learning). An assistant that ranks the review or suggests codes should read with the requesting user's identity, so plant, project and sales area authorizations apply (grounding with authorizations). Integration patterns are covered in how SAP systems talk.
Context: Calder sells a few engineered pumps a year, each with its own design, purchased parts, production and site commissioning, priced at 100,000 to 300,000 USD with milestone payments. Today they run as ordinary sales orders, and their costs are known only after delivery.
Options: (a) make to order on the sales order item, with costs on the sales order and production orders; (b) a customer project with a WBS billing element, the sales order item assigned to it, milestone billing, valuated project stock and revenue recognized by percentage of completion; (c) a separate project tool outside SAP, with summary postings into S/4HANA.
Decision: (b) for engineered pumps; standard pumps stay make to stock.
Consequences: Project profiles, billing elements, settlement profiles and a recognition method to configure. Project managers keep cost plans current, because revenue depends on them. Milestones become accounting events that someone must record. Engineering hours must be confirmed on WBS elements, which touches time recording. Option (c) was rejected because costs and revenue would live in two systems and drift.
What a design review will ask
Which materials get which inspection types, and who owns each usage decision, with what deadline?
What happens to an invoice when its goods are rejected, and who talks to the supplier?
Which revenue recognition method do customer projects use, and who updates the cost plan when scope changes?
How are milestones confirmed, and can billing be released without one?
How do warranty and service contracts reach the billing control of service items, and what is billed when the field app is offline?
Suggest defect type, location and cause from the text; measure against the keyword baseline in Step 8; see classification metrics
Root cause
Retrieve
Strong
Find similar closed notifications, their causes and 8D findings, across plants and systems; the engineer confirms
Complaint intake
Extract
Strong
Turn a customer's email or a technician's note into a draft notification with material, batch and references; structured outputs keep it in SAP's fields
Usage decision
Summarize and explain
Weak for AI to decide
The decision is final and moves stock; AI can summarize results and history for the engineer
Supplier quality
Detect
Moderate
Flag repeat defects by supplier and material before the third case; the block is purchasing's and quality's call
Project cost overrun
Predict and detect
Strong
Flag elements whose forecast drifts from plan, and learn from past projects which signals came before overruns; see anomaly detection
Milestone and billing gaps
Plain automation is enough
Weak for AI
A rule finds reached milestones without an actual date; a person records it
Dispatching
Rank and propose
Moderate
SAP listed a Field Service Dispatcher Agent in beta in Q2 2025, with a dispatcher verifying each proposal (SAP News, 2025)
Service confirmation and billing release
Plain automation is enough
Weak for AI
Checks for missing confirmations and unreleased items are rules
Warranty check
Retrieve, then rule
Moderate
Retrieval finds the contract or warranty; a rule sets billable control; customer service approves
Technician briefing
Summarize
Strong
Summarize the equipment's history, open complaints and similar repairs before the visit, citing the documents
The line is the same throughout: AI suggests codes, finds similar cases, flags drift and drafts notifications; a quality engineer decides lots, a project manager owns the forecast, a project controller records milestones, and customer service releases billing. Human approval in AI workflows and agent permissions show how to enforce that in code. Explaining model predictions matters for overrun signals: a project manager acts on a flag only if it says why.
Turn the worklist into an action note and test two fixes yourself.
Run python unit1b/quality_projects_service.py --write (Windows: python unit1b\quality_projects_service.py --write). Copy unit1b/calder_qps_worklist.md to unit1b/calder_qps_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: classify, retrieve, extract, detect, summarize or nothing. Use the table under Where AI fits to choose.
In quality_projects_service.py, find milestone M1 in PROJECT. Change its last two values from "", "" to "2026-09-30", "2026-10-26". This is the project controller recording the approval and the billing run invoicing it.
In SERVICE_ORDERS, find order 8000032 and change "billable" to "not billable" on both of its items. This is customer service correcting the warranty visit.
Run the script without options, then with --project CP-26-ENG01 and with --service 8000032. Note how many items remain, what the project now says about billing, and what the service order says about billing.
Change both edits back.
In the action note, add a heading Fixes tested with three lines: what recording the milestone changed, what correcting the warranty visit changed, and which project figure did not change and why.
Add a heading ECC note with one line on what a group view of supplier quality needs from CP20. Preview the file and commit it with Git.
Done when:unit1b/calder_qps_actions.md has the twelve-line table with Action and AI could filled in for every line, a Fixes tested section that says the list drops to 10 items (5 quality, 2 project, 3 service), that the project shows 55,800.00 USD billed and recognized revenue exceeding billings by 40,237.53 USD, that order 8000032 now has nothing billable, and that the 5,578.65 USD of early revenue is unchanged because the cost plan wasn't updated, 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 quality, project and service part.
Pick one answer for each question. The explanation appears after you choose.
1Lot 010000000041 has three hardness values above the limit. What does the usage decision change in SAP?
Answer: C. A usage decision posts the lot's stock to unrestricted or blocked stock; the value stays in inventory. Returns and credits follow through the supplier complaint.
2A quality engineer saved the wrong usage decision code. What can they do?
Answer: C. The usage decision is final. SAP's training describes reopening the data set to correct it, and reversing any automatic stock postings manually.
3Calder wants new purchase orders for Ridgeline's seal kits stopped while the 8D is open. What do you set?
Answer: D. The quality info record holds quality control data per supplier and material and can block functions such as creating purchase orders. Open orders stay a buyer's decision.
4The engineered pump's sales order item must carry its revenue into the project. What does it need?
Answer: D. Revenue is planned and posted through billing elements. The sales order item is assigned to the billing element, and its billing plan takes dates from milestones.
5On the approved plan the project is 54.6% complete; on the forecast, 51.6%. Why does it matter which one finance uses?
Answer: A. Cost-based percentage of completion divides actual cost by total cost. A total that is too low inflates progress, so 5,578.65 USD of revenue is recognized before it is earned.
6Repair 5000012 waits for a non-stock impeller due five days after the promised return. What would have prevented it?
Answer: C. Non-stock parts are bought when the repair order is released, so their lead time sets the earliest finish. The promise to the customer should come after that check.
7An assistant suggests LEAK for Hallam's complaint, while the most similar past case says hard seal compound. What should the design do?
Answer: B. Keywords find the symptom; retrieval points to a likely cause. The engineer confirms the defect and the cause, and the link to the open supplier complaint may turn two problems into one.
8An agent proposes to release confirmed service orders for billing every night. What do you check first?
Answer: B. Releasing for billing creates a billing document request and then an invoice. A warranty item marked billable becomes a wrong invoice, so the check comes before any automated release, with a person approving exceptions.
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.
ChapterQualityInspect what Calder buys before anyone uses it: an inspection setup and characteristics for seal kits, goods receipts that go to quality inspection and open inspection lots, results measured on a sample, and the usage decision that releases a good delivery to unrestricted stock or blocks a bad one, while planning counts only what can be used. Then the final inspection of a pump Calder builds: ES-300 tested at production receipt before anyone can sell it, and an order that confirms only the pumps that passed until a recheck after the decision. Then the complaint: a supplier's invoice blocked for quality, a quality notification, a return delivery and the credit memo that settles it. Then the supplier's replacement inspected and its invoice released by the decision, and the supplier's quality record read for what it can and can't say. Last, ES-300's final inspection fed by the test bench: one measurement per pump, recorded once against its serial number.
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
Using the Inspection Lot (SAP Learning)— lots from goods receipts, order release, deliveries, batch deadlines or by hand, only with an active inspection type; plan copied into the lot; sample sizes; quantitative and qualitative results; defects
Executing a Usage Decision (SAP Learning)— accept or reject from a code catalog; quality score 1 to 100; automatic posting to unrestricted or blocked stock; the decision is final
Configuring Quality Notifications (SAP Learning)— origins and types Q1/F1 customer, Q2/F2 supplier, Q3/F3 internal; reference objects; catalog types 9, E, 5, 2, 8; action box, status profiles, digital signature
Executing a Settlement (SAP Learning)— customer projects settle results analysis data (revenue, cost of sales, reserves); direct and multi-level settlement; settlement profile; investment measures
Cloudification Repository (SAP on GitHub)— objectReleaseInfoLatest.json read 24 October 2026; I_InspectionLot, I_InspLotUsageDecision, I_Defect, I_QltyNotification, I_QualityInProcurement, I_EnterpriseProjectElement, I_EntProjSettlmtRule, I_ServiceOrderTP, I_ServiceConfirmationTP released; QALS not to be released