Orchestrate

Report an issue

Something wrong, out of date or unclear? Tell us. Corrections are prioritized over new topics. You don't need to sign in.

0 / 2000

Don't include personal or confidential company details. Reports are reviewed with AI assistance.

Quality, projects and service: the processes between the big four

See how inspections, engineered-to-order projects and aftermarket service run in SAP, what they post, where they break, and where AI helps.

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

The 60-second version

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.

Why it matters to the business

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.

How SAP does it

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.

The process on one page

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 Yes
Service is planned and done Service manager, technician Service order; service confirmation Yes: costs to the order items
Service is billed Billing Billing document request; invoice Yes: revenue to the order items

How logistics steps become accounting entries is the subject of Document flow and how logistics becomes finance.

Questions to ask

  1. How long do lots wait for a usage decision, and how much stock and how many supplier invoices sit behind them today?
  2. When a supplier's goods are rejected for the third time, who decides on a procurement block, and where is that recorded?
  3. For each customer project, what is the forecast cost today, and when was the plan last updated?
  4. How many milestones were reached but not billed at last month end?
  5. What share of service orders are confirmed within two days of the visit, and released for billing within three?
  6. Who checks warranty and contract coverage before a confirmation is billed?
  7. If an AI tool codes defects or flags project overruns, who accepts each suggestion, and is that logged?

Common misconceptions

  • "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.

Key terms

All course terms, A to Z, in the glossary

  • Inspection lot: the record that controls one quality inspection of a quantity of material.
  • Usage decision: the accept or reject decision that closes an inspection lot and posts its stock.
  • Quality notification: the record of a quality problem, with defects, causes, tasks and activities.
  • Quality info record: quality control data for one supplier and material, including a procurement block.
  • Work breakdown structure (WBS): the hierarchy of a project; each WBS element can collect costs.
  • Billing element: a WBS element that receives the sales order item and its revenue.
  • Milestone billing: invoicing a percentage of the price when a project milestone is reached.
  • Service order: the document that plans service at a customer site and collects its costs and revenue.
  • Service confirmation: the record of the time, parts and expenses actually used.
  • In-house repair: the process for a product the customer sends in to be repaired.

Check yourself

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.
Deep layer · 40 min read

Mental model

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.

Business objects and documents

Before you start: this topic builds on The SAP data model for AI engineers, Master data that every module depends on, Document flow and how logistics becomes finance, Order to cash end to end, Procure to pay end to end (the seal kits in quality inspection), Plan to produce end to end (the scrapped ES-300) and Maintain to settle (orders, confirmations and settlement). It doesn't repeat them.

Quality: inspection lots and usage decisions

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
040000000007 Goods receipt from production order 1100016 3 ES-300 pumps that passed the pressure test Accepted 19 October, score 100

Quality notifications

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).

Quality info records

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.

Projects: an engineered pump made to order

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.

Period end for projects: revenue and settlement

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.

Service: field service and in-house repair

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

Document flow and postings

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.

Neighbors

Neighbor Gives Takes When the link fails
Procurement (procure to pay) Goods receipts, PO items, supplier invoices Usage decisions, quality info record blocks, complaints Invoices blocked for weeks; a supplier with repeat defects keeps getting orders
Production (plan to produce) Goods receipts from production, scrap, project production orders Inspection results, internal problem notifications Pumps wait in inspection; scrap without a cause
Inventory and warehouse Stock and bins Stock type changes from usage decisions Stock looks short while it sits in inspection
Sales (order to cash) Sales order items, customer complaints Available stock, returns orders, project and service invoices Orders can't confirm; complaints with no follow-up
Maintenance (maintain to settle) Technical objects, maintenance orders for advanced execution Equipment from in-house repair Two records for the same pump
People and time (hire to retire) Technicians, engineering hours Hours by WBS element and service order Hours on the wrong project or never confirmed
Finance (record to report) Periods, recognition methods Revenue, WIP, settlements Margin recognized early; work in process nobody can explain
Field Service Management Dispatch, mobile confirmations, service reports Released service orders Visits done but never confirmed in S/4HANA
Suppliers and customers 8D reports, approvals Complaints, milestone invoices Tasks overdue; approvals by email nobody records

Where it breaks

Symptom Usual cause Who resolves it What it costs Calder (scenario)
Lot 010000000041 has no decision after ten days Results are in, but nobody owns the decision Quality engineer 200 seal kits unavailable; invoice 5105600002 (10,400.00 USD) blocked
Ridgeline's 8D report is three days late Task set, never chased Quality engineer The cause of the hard seals stays unknown
Third hard-seal case in a year, no block No rule on repeat complaints Quality engineer, purchasing More kits from the same compound arrive on PO 4500000009
ES-300 casing leak without a cause Notification opened, analysis not done Quality engineer The foundry's porosity problem repeats
55,800.00 USD not invoiced Milestone approved by email, actual date never entered Project controller Cash late by a month
Engineering 20% over plan Design changes not re-estimated Project manager Margin falls from 26.9% to 22.6%
Repair 5000012 late Non-stock impeller ordered at release, lead time longer than the promise Customer service manager, buyer A distributor's customer waits; goodwill cost
Warranty work marked billable Item billing control not checked Customer service manager A wrong invoice, a dispute, a credit memo
Visit 8000034 without confirmation Technician didn't complete the work in the field app Technician Costs and revenue missing; nobody knows if the visit happened

How it is exposed

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).

Step OData API Released CDS views What to read
Inspection lot and results API_INSPECTIONLOT_SRV (A_InspectionLot, A_InspectionCharacteristic, A_InspectionResult) I_InspectionLot, I_InspectionCharacteristic, I_InspectionResult, I_InspectionLotOrigin Origin, quantities, PO and order references, results
Usage decision API_INSPECTIONLOT_SRV (A_InspLotUsageDecision) I_InspLotUsageDecision Code, score, follow-up action, date
Defects I_Defect, I_DefectCode, I_DefectClass Defect codes, classes, texts
Quality notification Check the Hub for your edition I_QltyNotification, I_QualityNotificationItem, I_QltyNotificationCause, I_QltyNotificationTask Type, references, defects, causes, open tasks
Quality info record Quality info record API (QualityInProcurement) I_QualityInProcurement ProcurementBlock, BlockReason, release dates
Project and WBS API_ENTERPRISE_PROJECT_SRV (A_EnterpriseProject, A_EnterpriseProjectElement) I_EnterpriseProject, I_EnterpriseProjectElement, I_EntProjSettlmtRule Status, hierarchy, billing element, settlement rule
Customer project sales order I_CustProjSlsOrd, I_CustProjSlsOrdItem, I_CustProjSlsOrdItmBillgPln Sales order items and billing plans of customer projects
Service order API_SERVICE_ORDER_SRV (A_ServiceOrder, A_ServiceOrderItem, A_ServiceOrderDefect) I_ServiceOrderTP, I_ServiceDocument, I_ServiceDocumentItem Released and completed flags, billable control, SLA dates, FSM links
Service confirmation API_SERVICE_CONFIRMATION_SRV (A_ServiceConfirmation, A_ServiceConfirmationItem) I_ServiceConfirmationTP, I_ServiceConfirmationItemTP Actual duration, completion, billable control
In-house repair 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.

Build it yourself

Before you start: finish Set up your computer for this course and Set up for Unit 1B. This walkthrough uses the Calder sample pack from the Unit 1B setup.

You will write one script that 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

What you need

  • The course folder orchestrate-course with its .venv, and the Unit 1B sample pack.
  • For Steps 1 to 8: no account, no key and no network. The script uses only Python's built-in modules, so requirements.txt doesn't change.
  • For Step 9 (optional): your free SAP Business Accelerator Hub key as SAP_API_KEY in .env, from the course setup.
  • Cost: free. Time: about 45 minutes.

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

  1. Open VS Code, then File > Open Folder and choose orchestrate-course.
  2. Open a terminal with Terminal > New Terminal.
  3. Turn on the virtual environment:
    • Windows (PowerShell):

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

      source .venv/bin/activate

You should see (.venv) at the start of the prompt.

Step 2: Make sure the sample pack is current

  1. Rewrite the pack, so you start from the same data as this topic:

    • Windows (PowerShell):

      python unit1b\make_calder_pack.py
    • macOS / Linux:

      python unit1b/make_calder_pack.py
  2. The last lines should read:

Calder sample pack 1.0: order 1000001, net 29,100.00 USD, cost 19,600.00 USD.
All data is fictional. Next: python check_unit1b.py

The script reads 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).

Step 3: Create the script

  1. In VS Code, create a new file, paste the script below, and save it as quality_projects_service.py inside the unit1b folder.
  2. 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()

Step 4: Read the Monday worklist

  1. Run the script without options:

    • Windows (PowerShell):

      python unit1b\quality_projects_service.py
    • macOS / Linux:

      python unit1b/quality_projects_service.py
  2. 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.

Step 5: Decide the seal kit lot

  1. Look at the lot:

    • Windows (PowerShell):

      python unit1b\quality_projects_service.py --lot 010000000041
    • macOS / Linux:

      python unit1b/quality_projects_service.py --lot 010000000041
  2. You should see:

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.
  1. 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.

Step 6: Read the project's figures

  1. Run:

    • Windows (PowerShell):

      python unit1b\quality_projects_service.py --project CP-26-ENG01
    • macOS / Linux:

      python unit1b/quality_projects_service.py --project CP-26-ENG01
  2. You should see:

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.

Step 7: Trace a service order and a repair

  1. Run:

    • Windows (PowerShell):

      python unit1b\quality_projects_service.py --service 8000031
    • macOS / Linux:

      python unit1b/quality_projects_service.py --service 8000031
  2. You should see:

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.
  1. 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

  1. Run:

    • Windows (PowerShell):

      python unit1b\quality_projects_service.py --defects
    • macOS / Linux:

      python unit1b/quality_projects_service.py --defects
  2. You should see:

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.
  1. Write the worklist file:

    • Windows (PowerShell):

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

      python unit1b/quality_projects_service.py --write
  2. 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.

  1. Make sure .env in the course folder has a line SAP_API_KEY="...".

  2. Run:

    • Windows (PowerShell):

      python unit1b\quality_projects_service.py --sandbox
    • macOS / Linux:

      python unit1b/quality_projects_service.py --sandbox
  3. 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.

Step 10: Save your work in Git

git add unit1b/quality_projects_service.py unit1b/calder_qps_worklist.md
git commit -m "Unit 1B: quality, project and service worklist for Calder"

What each part of the code does

Part What it does
INSPECTION_LOTS, UD_CODES 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

If something goes wrong

What you see What it means What to do
python is not recognized / command not found Python isn't on the path, or the terminal opened before installing Follow the Python step in Set up your computer, then open a new terminal
Cannot read ...calder_pack The pack wasn't written, or you ran from the wrong folder Run Step 2 from the orchestrate-course folder
Plant CP11 ... is missing or Business partner ... is missing The pack was edited Run Step 2 again
Your output differs from the one shown Dates, values or amounts in the script were changed Copy the script again, or compare your edits
... is not in the scenario You asked for a lot, project or order the script doesn't have Use one of the numbers the message lists
Unknown usage decision code --ud got something other than A, A2 or R Use one of those three
SAP_API_KEY is not set No key in the environment or .env Add it as in the course setup, or skip Step 9
Sandbox answered HTTP 401 The key is wrong or expired Copy the key again from the Business Accelerator Hub
Sandbox answered HTTP 404 The API or entity isn't in the sandbox under that path Check the API's sandbox URL on the Business Accelerator Hub, or skip Step 9
Cannot reach the sandbox No network, or a company proxy blocks it Try another network, or stay with Steps 1 to 8
ModuleNotFoundError You changed the script to use an outside library The script needs only built-in modules; copy it again

Architecture view

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.

flowchart LR
  SUP[Supplier portal<br/>8D answers] <-->|complaints, tasks| S4
  S4[S/4HANA CP10<br/>QM, PS, Service,<br/>MM, SD, FI] <-->|service calls,<br/>confirmations| FSM[Field Service<br/>Management]
  CRM[CRM<br/>customer cases] -->|complaints| S4
  ECC[ECC CP20<br/>Augsburg QM] -.->|lots, notifications| DW[Data warehouse]
  S4 -->|released APIs, CDS| AI[Review assistant<br/>read-only]
  AI --> D[Drafts and<br/>worklists]
  D -->|people decide in SAP| S4

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.

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

  • 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

  1. Which materials get which inspection types, and who owns each usage decision, with what deadline?
  2. What happens to an invoice when its goods are rejected, and who talks to the supplier?
  3. Which revenue recognition method do customer projects use, and who updates the cost plan when scope changes?
  4. How are milestones confirmed, and can billing be released without one?
  5. How do warranty and service contracts reach the billing control of service items, and what is billed when the field app is offline?

Where AI fits, and where it doesn't

Step Pattern Fit Note
Defect and complaint texts Predict (classify) Strong 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.

Pitfalls

  • No owner for usage decisions. Lots wait, stock looks short, invoices block. Give every inspection type an owner and a deadline.
  • Treating inspection stock as a planning problem. It is a decision problem; buying more doesn't help if the last delivery is still waiting.
  • Complaints without a link. A notification without its PO, delivery or order can't feed supplier evaluation or returns.
  • Coding the symptom. "Leak" is what the customer saw; the defect may be a hard seal. Ask for the defect, not the symptom.
  • Stale project plans. Cost-based recognition on an old plan books margin early. Update the plan when scope changes, not at the end.
  • Milestones by email. Approval that isn't recorded as a milestone date isn't billed.
  • Non-stock parts on short promises. Check the lead time of parts before promising a repair date.
  • Billable by default. Warranty, contract and goodwill must set billable control before the confirmation is released.
  • Assuming ECC and S/4HANA work the same way. Service and revenue recognition differ most; a group view across Calder's two systems must allow for that.

Exercise

Turn the worklist into an action note and test two fixes yourself.

  1. 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.
  2. In the copy, add a column Action to the table. For each line write one sentence: what the owner should do today.
  3. Add a column AI could with one of: classify, retrieve, extract, detect, summarize or nothing. Use the table under Where AI fits to choose.
  4. 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.
  5. 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.
  6. 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.
  7. Change both edits back.
  8. 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.
  9. 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.

Check yourself

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.

Practice this in the lab

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

  • 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

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in