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.

Procure to pay end to end

Follow a purchase from requisition to supplier payment in SAP, see why invoices get blocked and who frees them, and decide where AI helps.

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

The 60-second version

Procure to pay is everything between someone needing something and the supplier being paid for it. In SAP it runs as a chain of documents: purchase requisition, purchase order, supplier confirmation, goods receipt, supplier invoice and payment.

Two checks decide whether the money goes out:

  • Did we get it? The goods receipt records what actually arrived, and, for some materials, whether it passed inspection.
  • Is the bill right? The three-way match compares the invoice with the purchase order (what we agreed) and the goods receipt (what came). If the price or quantity is off by more than an allowed tolerance, SAP still posts the invoice but blocks it for payment.

A blocked invoice waits for a person: a buyer, a warehouse clerk or a quality engineer. Meanwhile the supplier isn't paid and calls to ask why.

Three ideas carry this topic:

  • The purchase order is a promise in the other direction. It commits the company to pay, but nothing is owed until goods or services arrive.
  • Matching is the control. Most procure-to-pay problems show up as a mismatch between three documents owned by three teams.
  • Blocked invoices cost twice. They take people's time, and late payments cost supplier goodwill and early-payment discounts.

Why it matters to the business

Procure to pay decides how much a company pays for what it buys and how well it treats its suppliers. Finance watches days payable outstanding (how long the company takes to pay) and the share of invoices that post without anyone touching them. Purchasing and production watch supplier on-time delivery.

Here is a snapshot from Calder Pumpworks, the fictional pump maker this unit follows. On 23 October 2026, Calder's procure-to-pay worklist shows six items stuck:

What is stuck Why Who frees it Value (USD)
Five motors received a month ago No invoice from the supplier yet Accounts payable 4,525.00
Twenty motors ordered, needed 5 November Supplier hasn't confirmed the order Buyer 18,100.00
Annual crane inspection, done and invoiced The service entry sheet isn't approved The requester 3,800.00
Forty impellers from Calder's German company Invoiced 40, only 36 received Goods receiving, with the buyer 13,400.00
Ten motors invoiced 6.6% above the order price Price outside tolerance Buyer, with accounts payable 9,650.00
Two hundred seal kits Still in quality inspection Quality engineer 10,400.00

The business decision is not "pay faster" or "block more". A block that stops a 600 USD overcharge is doing its job. The decision is how tight the tolerances are, and how fast each block reaches the person who can clear it.

How SAP does it

As of October 2026, SAP S/4HANA covers the whole chain as standard. Each step is documented in SAP's training:

  • Requisitions. Material requirements planning (MRP) can create purchase requisitions directly. People create them by hand or through self-service requisitioning (SAP Learning).
  • Sourcing. The system proposes a supplier from source lists, contracts and purchasing info records, in a fixed order of priority (SAP Learning). Requests for quotation can run in S/4HANA or go to SAP Ariba Sourcing (SAP Learning).
  • Confirmations. A confirmation control key says which confirmations the supplier must send, such as an order acknowledgment or a shipping notification. The Monitor Supplier Confirmations app shows what is missing (SAP Learning).
  • Receipt and inspection. A goods receipt can create an inspection lot in quality management and put the stock into quality inspection until someone records a decision (SAP Learning).
  • Invoice verification. Tolerances are set per company code. An invoice outside them posts but is blocked for payment (SAP Learning).
  • Payment. The automatic payment program proposes due items, a person reviews the proposal, and the run pays and clears them (SAP Learning).

Suppliers can collaborate through SAP Business Network. It carries purchase orders, order confirmations, advance ship notices and invoices between buyer and supplier. A standard supplier account is free; an enterprise account, needed for integration and reporting, has fees based on transactions (SAP Learning).

For AI, SAP lists AI-assisted multi-model data extraction in SAP Ariba Invoicing as generally available in its Q2 2026 release highlights (SAP News). Machine-learning proposals in GR/IR reconciliation need a separate subscription license (SAP Learning). Where AI creates value in S/4HANA places these in the wider picture.

The process on one page

flowchart LR
  PR[Requisition<br/>MRP or person] --> SRC[Source<br/>info record, contract]
  SRC --> PO[Purchase order]
  PO --> CF[Supplier<br/>confirmation]
  CF --> GR[Goods receipt<br/>inspection?]
  GR --> IV[Supplier invoice<br/>three-way match]
  IV --> PAY[Payment run]
  IV -.-> BLK[Blocked<br/>for payment]
  BLK -.-> IV
Step Team Document created Posts to finance?
Need is found Planning (MRP) or the requester Purchase requisition No
Supplier is chosen Purchasing Request for quotation and quotation (optional) No
Order is placed Buyer Purchase order No
Supplier answers Supplier, buyer Order acknowledgment, inbound delivery No
Goods arrive Goods receiving Goods receipt (material document) Yes: stock against GR/IR clearing
Goods are inspected Quality Inspection lot, usage decision Only the stock move
Service is done Requester Service entry sheet, then a goods receipt on approval Yes: at approval
Bill arrives Accounts payable Supplier invoice Yes: GR/IR against the supplier
Supplier is paid Accounts payable, treasury Payment, clearing Yes: supplier against bank

Which steps post, and to which accounts, is the subject of Document flow and how logistics becomes finance.

Questions to ask

  1. How many supplier invoices are blocked today, for which reasons, and how old is the oldest?
  2. What share of invoices post and get paid without anyone touching them?
  3. Who set the price and quantity tolerances, when were they last reviewed, and what did that review compare?
  4. How do invoices arrive: paper, PDF by email, EDI, SAP Business Network? How much is still keyed by hand?
  5. Which suppliers must confirm orders, and who chases the ones that don't?
  6. How large is the GR/IR balance, and how much of it is older than 90 days?
  7. If an AI tool proposes releasing a blocked invoice, who approves it, and is that logged?

Common misconceptions

  • "A blocked invoice wasn't posted." It was posted. It is on the books and owed, but held from payment until the block is cleared.
  • "Tighter tolerances are always safer." Very tight limits block invoices for cents, flood accounts payable and delay payments. The right limit is a trade-off, and it is a decision.
  • "Three-way match catches every overpayment." It only compares three documents. If the receipt itself is wrong, or the order price was wrong, the match passes.
  • "Services work like goods." There is no truck at the dock. Someone has to confirm the work was done, on a service entry sheet, before the invoice can match.
  • "An AI agent should release blocked invoices." It can gather the evidence and recommend. Releasing payment is a person's decision with an audit trail.

Key terms

All course terms, A to Z, in the glossary

  • Procure to pay (P2P): the end-to-end process from a need to a paid supplier.
  • Purchase requisition: an internal request to buy, created by MRP or a person.
  • Purchasing info record: a supplier's terms for one material, used as defaults in purchase orders.
  • Confirmation control key: the setting that says which confirmations a supplier must send.
  • Inspection lot: the quality management record of an inspection, created for example at goods receipt.
  • Three-way match: comparing purchase order, goods receipt and invoice before paying.
  • Tolerance limit: how far an invoice may differ from the order or receipt before it is blocked.
  • GR/IR clearing account: the account that holds goods received but not invoiced, and the reverse, until they match.
  • Service entry sheet: the record that a service was performed, which works like a goods receipt for services.
  • Payment run: the scheduled job that pays due supplier items and clears them.

Check yourself

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

1Calder sends a purchase order for twenty motors. What has changed in its books?

Answer: D. A purchase order posts nothing in finance. The goods receipt posts stock against GR/IR, and the invoice posts the payable to the supplier.

2An invoice is 6.6% above the purchase order price, beyond Calder's tolerance. What does SAP do?

Answer: C. SAP's invoice verification posts an invoice that exceeds the upper tolerance and blocks it for payment. The whole invoice waits until someone clears the cause.

3Accounts payable wants tolerances near zero so no overcharge ever gets through. What is the main cost of that choice?

Answer: A. A very tight limit blocks invoices for trivial differences. People spend time releasing them and suppliers wait; the limit is a trade-off between review effort and money at risk.

4Goods were received a month ago, but no invoice has arrived. Where does this show up?

Answer: B. The goods receipt credited GR/IR and nothing has debited it yet. Old GR/IR items like this one are what a month-end review chases.

5A crane inspection is done and invoiced, but the invoice can't match. What is usually missing?

Answer: A. For services, the approved service entry sheet confirms the work and, in S/4HANA lean services, posts the goods receipt. Until it is approved, there is nothing for the invoice to match.

6A vendor pitches an agent that releases blocked invoices on its own. What should you ask for?

Answer: B. Releasing a payment block is a financial decision with an audit trail. An agent can gather the order, receipt and invoice facts and recommend, while a person in accounts payable decides.

7Calder wants its smaller suppliers to confirm orders and send invoices electronically at no cost to them. Which option fits?

Answer: D. SAP describes the standard supplier account as free, with email-based exchange of basic documents. Enterprise accounts add integration and reporting, with fees based on transactions.
Deep layer · 40 min read

Mental model

Think of procure to pay as three documents that must agree before money leaves.

  • The purchase order says what was agreed: material, quantity, price, date.
  • The goods receipt (or the approved service entry sheet) says what actually arrived.
  • The supplier invoice says what the supplier wants paid.

Everything before the match prepares one of the three. Everything after it, the payment run, only pays what matched. The GR/IR clearing account is the scoreboard: the receipt credits it, the invoice debits it, and a balance means one side is missing or different. Each block, worklist and AI triage tool in this process reads which of the three disagree and who owns that document.

Business objects and documents

Before you start: this topic builds on The SAP data model for AI engineers, How an SAP enterprise is wired (purchasing organizations, plants), Master data that every module depends on (info records, source lists) and Document flow and how logistics becomes finance (the GR/IR postings). It doesn't repeat them. Order to cash end to end is the mirror image of this topic.

Purchase requisition: from MRP or from a person

MRP can create planned orders, purchase requisitions or schedule lines on a scheduling agreement. MRP Live creates schedule lines when a valid scheduling agreement exists and requisitions otherwise. In classic MRP, a planned order must first be converted to a requisition by the MRP controller; a requisition reaches purchasing at once (SAP Learning).

People create requisitions too. In SAP S/4HANA Cloud Public Edition, SAP's training uses Manage Purchase Requisitions - Professional and Create Purchase Requisition - Advanced (SAP Learning). Employees who rarely buy use self-service requisitioning in My Purchase Requisitions - New: they pick from a catalog or describe a free-text item, and ordering the cart creates the requisition (SAP Learning).

Approval comes in two styles. The classic release procedure uses release strategies and release codes (transactions ME54N and ME55); a blocked requisition item can't become a purchase order (SAP Learning). Flexible workflow, available since S/4HANA 1709, defines approval steps in apps such as Manage Workflows for Purchase Requisitions; it can replace or coexist with the classic procedure, chosen per document type (SAP Learning).

Sourcing: who supplies it

Source determination proposes the supplier. In the MRP run, SAP checks in this order: quota arrangement, source list entries flagged for MRP, scheduling agreements, contracts (plant-specific before cross-plant), then info records marked for automatic sourcing. For a requisition created by a person, the order is quota arrangement, source list (the fixed entry wins if there are several), outline agreements, then info records (SAP Learning). Source determination doesn't use the condition technique, so there is no determination analysis to read when it picks the "wrong" supplier: you check each source object.

When there is no source yet, purchasing runs a request for quotation. In S/4HANA, Manage RFQs sends it by SAP Business Network, email or print, or hands it to SAP Ariba Sourcing, whose awarded quotation comes back and creates a purchase order or contract (SAP Learning). Compare Supplier Quotations puts up to three side by side (SAP Learning).

Purchase order: header, item, schedule line, account assignment

Requisitions become purchase orders by hand in Process Purchase Requisitions, or in a background job "Automatic Creation of Purchase Orders from Requisitions", which needs the automatic-PO flags on both the material and the supplier (SAP Learning).

Level Holds Calder example (fictional)
Header Supplier, company code, purchasing organization and group, payment terms, currency PO 4500000001, Orrin Motor Works 20000001, CP10, PUUS
Item Material, plant, quantity, net price, info record, confirmation control key, GR and invoice flags Item 10: 10 × CP-MTR15 for CP11 at 950.00 USD
Schedule line Delivery date and quantity, quantity committed by the supplier 10 on 2026-10-05
Account assignment For consumption items: cost center, project, order or asset Crane inspection PO 4500000007 to cost center CP11-410

In SAP's model of the Purchase Order API, the item carries GoodsReceiptIsExpected, InvoiceIsExpected, InvoiceIsGoodsReceiptBased, SupplierConfirmationControlKey, PurchasingInfoRecord, IsCompletelyDelivered and IsFinallyInvoiced. The schedule line carries ScheduleLineDeliveryDate, ScheduleLineOrderQuantity and ScheduleLineCommittedQuantity (SAP Cloud SDK model, 2022). Account assignment categories such as K (cost center) send the cost of a consumable to that object at goods receipt instead of to stock (SAP Learning).

Supplier confirmation

The confirmation control key on the item says which confirmations are expected, in which sequence, and whether each counts for MRP and goods receipt. A shipping notification can create an inbound delivery, a separate document that a goods receipt can reference. Monitor Supplier Confirmations and transaction ME2A show the state; reminders go out with ME91F, or ME91FF with S/4HANA output management (SAP Learning). A confirmed date that is relevant for MRP replaces the order's own date in planning, which is why a missing or late confirmation matters to production, not only to the buyer.

Goods receipt and quality inspection

The goods receipt is posted with reference to the purchase order, for example in Post Goods Receipt for Purchasing Document. It updates the PO history, creates a material document and, for valuated stock, an accounting document; the movement type drives account determination (SAP Learning). Movement type 101 is the receipt, 102 its reversal, 122 a return delivery to the supplier (SAP Learning).

If quality management is active for the movement type and the material has an active inspection type for origin 01, the receipt creates an inspection lot. With "Post to Inspection Stock" set, the stock waits in quality inspection (SAP Learning). The usage decision moves it: a positive decision to unrestricted-use stock, a negative one to blocked stock (SAP Learning). Quality is covered in more depth in Quality, projects and service.

Service procurement

Services have no delivery to receive. In S/4HANA lean services, the PO item uses product type group "Service (2)" with an account assignment (SAP Learning). The work is recorded in Manage Service Entry Sheets - Lean Services. Saving a sheet has no effect in finance. Approval is automatic or under dual control, and on automatic approval a goods receipt document is posted (SAP Learning). Limit items cover services whose total cost isn't known up front (SAP Learning).

Invoice verification and the three-way match

SAP calls the three-way match the standard procedure: the purchase order posts nothing; the goods receipt debits stock (or consumption) and credits GR/IR; the invoice, posted through Logistics Invoice Verification, debits GR/IR and credits the supplier (SAP Learning). Invoices are entered in Create Supplier Invoice or Create Supplier Invoice - Advanced (MIRO), and managed in Manage Supplier Invoices (SAP Learning). With goods-receipt-based invoice verification, each partial receipt becomes its own invoice item and receipts and invoices are matched in the PO history (SAP Learning).

Tolerances are set per company code. SAP groups the tolerance keys by variance type: DQ and DW for quantity, KW, PP and PS for price, BR and BW for PO price quantity, ST for date, AN and AP for amounts, BD for small differences. Below the lower limit the system warns but doesn't block. Above the upper limit it warns, posts the invoice and blocks it for payment, and the block covers the whole invoice even if one item varies. For a standard-price material, a price variance goes to a price difference account; for a moving-average material, to stock if there is stock coverage (SAP Learning). Read each key's exact checks from your system's Customizing; this topic doesn't define them one by one.

Other blocks exist: a manual payment block in the invoice header, a manual item block, and a stochastic block that holds a random sample of invoices above a threshold (SAP Learning).

Releasing blocked invoices

Blocked invoices are released in Release Blocked Invoices (MRBR), also offered as a launchpad app. The blocking reasons are quantity, price, quality and date, and each becomes invalid on its own: quantity after a further receipt or credit memo, price after a PO price change or a subsequent credit, quality after a positive usage decision, date once the delivery date is reached. Automatic release works only for invoices blocked due to variances, can run in the background with RM08RELEASE, and never releases an invoice that also carries a manual header block (SAP Learning).

That last point shapes triage: the fastest fix for most blocks is to correct the cause (post the missing receipt, record the usage decision, get the credit memo) and let automatic release do the rest.

GR/IR reconciliation

Three apps support the GR/IR clean-up: Reconcile GR/IR Accounts (F3302) with worklists, statuses, root causes and write-offs; Monitor GR/IR Account Reconciliation (F3303); and GR/IR Process Insights (F5796). With machine learning, which needs a separate subscription license, the service learns from past decisions and proposes the next step, priority and root cause for each item (SAP Learning). Quantity differences that will never be resolved are cleared with Clear GR/IR Clearing Account or MR11, which creates an account maintenance document (SAP Learning).

Payment run

The automatic payment run has four steps: set the parameters, schedule a proposal, edit the proposal, and schedule the payment run, which posts payments, clears open items and creates payment media. Parameters include company code, payment method, supplier selection and the next payment date. Manage Automatic Payments runs it, Manage Payment Media downloads the bank file and Manage Payment Blocks blocks or unblocks suppliers or items. Open items are cleared only when the run completes (SAP Learning). Items without a valid payment method or bank details land on the proposal's exception list (SAP Learning).

Document flow and postings

flowchart LR
  PR[Requisition<br/>0010000001] --> PO[Purchase order<br/>4500000001]
  PO --> CF[Acknowledgment]
  PO --> GR[Goods receipt<br/>4900000002]
  GR --> J1[Journal<br/>stock vs GR/IR]
  GR --> IV[Invoice<br/>5105600001]
  IV --> J2[Journal<br/>GR/IR vs supplier]
  J2 --> PAY[Payment run]
Step Posts to finance Why
Requisition, RFQ, purchase order, confirmation Nothing Commitments and preparation
Goods receipt (stock material) Stock at standard price against GR/IR at PO price; the difference to price difference Goods arrived; the receipt is valued at the agreed price
Goods receipt (consumable, account assigned) Cost center, project or order against GR/IR No stock; the cost goes to the object
Usage decision A stock move (quality inspection to unrestricted or blocked) Same value, different stock type
Service entry sheet approved Cost object against GR/IR, through the goods receipt it posts The service counts as received
Supplier invoice GR/IR against the supplier's payable, with any price variance and input tax The supplier's claim exists now
Payment Supplier against bank; the open item is cleared The claim is settled

At Calder (fictional), PO 4500000001 posts 9,200.00 USD to stock at standard, 300.00 USD to price difference and 9,500.00 USD to GR/IR at goods receipt. The invoice for 9,400.00 USD clears the GR/IR and credits 100.00 USD back to price difference. The postings and the account determination behind them are traced in Document flow and how logistics becomes finance, Step 6.

Neighbors

Neighbor Gives procure to pay Takes from procure to pay When the link fails
Planning (MRP) Requisitions with dates and quantities Confirmed dates and open orders as supply Orders placed too late, or planning on dates nobody confirmed
Master data (business partner, material, info record, source list) Supplier, terms, prices, lead times, tolerances Nothing directly Wrong supplier proposed, wrong price defaulted, blocks that shouldn't fire or should
Inventory and warehouse Goods receipts, return deliveries Expected receipts, inbound deliveries Invoices for goods never booked in; GR/IR balances
Quality management Usage decisions Inspection lots from receipts Stock stuck in inspection; invoices blocked for quality
Payables and treasury Payment runs, bank files Posted, unblocked invoices Late payments, missed discounts, duplicate payments
Supplier channels (SAP Business Network, EDI, portal, email) Confirmations, ship notices, invoices Purchase orders, payment advices Orders not acknowledged, invoices keyed twice or lost
Controlling Cost centers and orders for account assignment Costs of consumables and services Costs on the wrong cost center
Intercompany (CP20 to CP10) Machined impellers bought between Calder companies Intercompany purchase orders and invoices Unmatched intercompany balances (see intercompany and multi-company scenarios)

Where it breaks

Symptom Usual cause Who resolves it What it costs Calder (scenario)
PO 4500000002 has no acknowledgment after 8 days Supplier didn't confirm; nobody chased Buyer MRP plans on an unconfirmed date for 18,100.00 USD of motors
Invoice 5105600003 blocked for price Orrin billed 965.00 against a PO price of 905.00 (+6.63%, 600.00 USD) Buyer, with accounts payable 9,650.00 USD held; a credit memo or a PO correction is needed
Invoice 5105600004 blocked for quantity 40 impellers invoiced, 36 received Goods receiving, with the buyer 13,400.00 USD held; GR/IR 1,340.00 USD debit
Invoice 5105600002 blocked for quality Seal kits still in inspection, no usage decision Quality engineer 10,400.00 USD held; seal kits can't be used
Invoice 5105600005 can't match Service entry sheet 1000000001 submitted, not approved Requester 3,800.00 USD held; the crane company waits
PO 4500000006 received, not invoiced Supplier hasn't billed five motors after 35 days Accounts payable GR/IR 4,525.00 USD credit open at month end

How it is exposed

As of 23 October 2026, these released interfaces carry each step. The CDS views come from SAP's Cloudification Repository (file read 23 October 2026), which also marks the classic tables EBAN, EKKO, EKPO, EKET, EKBE, RBKP, RSEG, MSEG, QALS and LFA1 as not to be released and names a successor for each (Cloudification Repository). The OData API names and fields come from SAP's generated API models, published in 2022 (SAP Cloud SDK model, 2022).

Step OData API (A2X) Released CDS views What to read
Requisition API_PURCHASEREQ_PROCESS_SRV I_PurchaseRequisitionAPI01, I_PurchaseRequisitionItemAPI01 Requisition items, source, release status
Sourcing API_RFQ_PROCESS_SRV, API_QTN_PROCESS_SRV, API_PURCHASECONTRACT_PROCESS_SRV, API_INFORECORD_PROCESS_SRV I_RequestForQuotation_Api01, I_SupplierQuotation_Api01, I_PurchaseContractAPI01, I_PurchasingInfoRecordApi01 Quotations, contracts, info record prices and lead times
Purchase order API_PURCHASEORDER_PROCESS_SRV I_PurchaseOrderAPI01, I_PurchaseOrderItemAPI01, I_PurOrdScheduleLineAPI01 InvoiceIsGoodsReceiptBased, SupplierConfirmationControlKey, IsCompletelyDelivered, IsFinallyInvoiced
Confirmation I_POSupplierConfirmationAPI01 Confirmed dates and quantities
PO history I_PurchaseOrderHistoryAPI01 Receipts and invoices per PO item, the basis of the match
Goods receipt API_MATERIAL_DOCUMENT_SRV I_MaterialDocumentHeader_2, I_MaterialDocumentItem_2 GoodsMovementType, PurchaseOrder, InventoryStockType
Inspection API_INSPECTIONLOT_SRV I_InspectionLot Lots from receipts and their usage decisions
Services API_SERVICE_ENTRY_SHEET_SRV (SubmitForApproval and related functions) I_ServiceEntrySheetAPI01, I_ServiceEntrySheetItemAPI01 Sheet status and approval
Invoice API_SUPPLIERINVOICE_PROCESS_SRV (functions Release, Cancel) I_SupplierInvoiceAPI01, I_SuplrInvcItemPurOrdRefAPI01, I_SupplierInvoiceBlockAPI01 PaymentBlockingReason, SupplierInvoiceStatus, InvoiceGrossAmount
Payables and payment I_OperationalAcctgDocItem, I_PaymentProposalItem, I_PaymentBlockingReason Open and cleared supplier items, the proposal, block reasons
Supplier API_BUSINESS_PARTNER I_Supplier, I_SupplierCompany, I_SupplierPurchasingOrg Terms, reconciliation account, purchasing data

The Release function on the supplier invoice API releases a blocked invoice. Use it only in a design where a person triggers the action. SAP also publishes business events for purchasing objects; check their exact names in the event catalog on the SAP Business Accelerator Hub, and see Event-driven agents with SAP events for 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 does four things. It traces Calder's purchase order 4500000001 from requisition to the planned payment, with the three-way match. It runs the same checks on six more purchases and prints the worklist an accounts payable lead would want each morning. It compares price tolerance limits on past invoices, so you can see the trade-off behind a tolerance design. And it writes the worklist to a file. An optional step counts blocked supplier invoices in SAP's sandbox.

flowchart LR
  P[calder_pack/<br/>suppliers, products] --> S[procure_to_pay.py]
  SC[Scenario in the script<br/>info records, tolerances,<br/>POs 4500000001-07] --> S
  S --> T[Trace and<br/>three-way match]
  S --> W[Worklist:<br/>what is stuck, who acts]
  S --> TL[Tolerance<br/>trade-off]
  S --> M[calder_p2p_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 7: no account, no key and no network. The script uses only Python's built-in modules, so requirements.txt doesn't change.
  • For Step 8 (optional): your free SAP Business Accelerator Hub key as SAP_API_KEY in .env, from the course setup.
  • Cost: free. Time: about 40 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 the suppliers' payment terms from business_partners.json and the motor's standard price from products.json.

Step 3: Create the script

  1. In VS Code, create a new file, paste the script below, and save it as procure_to_pay.py inside the unit1b folder.
  2. Read the blocks INFO_RECORDS, the tolerance constants and PURCHASES first. They are Calder's scenario written as Python data. Every value is invented. The tolerance values and block labels such as price block are the course's own, not SAP's tolerance keys or block codes. The service supplier Corvane Lifting Services (20000004) lives only in this script.
"""Procure to pay at Calder: trace one purchase end to end and triage the ones that are stuck.

Calder Pumpworks is a fictional company. The pack (unit1b/calder_pack/) and the
scenario below (info records, tolerances, open purchase orders, receipts and
invoices) are made up for the course. Field names follow SAP's released APIs
where one exists; every code, value and amount is illustrative, not SAP
standard and not from any real system.

Run it from your course folder:
  python unit1b/procure_to_pay.py                     trace PO 4500000001, then triage Calder's open purchases
  python unit1b/procure_to_pay.py --po 4500000004     show the three-way match for one purchase order
  python unit1b/procure_to_pay.py --tolerance         compare price tolerance limits on past invoices
  python unit1b/procure_to_pay.py --write             write unit1b/calder_p2p_worklist.md
  python unit1b/procure_to_pay.py --sandbox           count blocked supplier invoices in SAP's sandbox (needs a key)
Uses only Python's built-in modules. Only --sandbox needs a key and the network.
"""
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from datetime import date, timedelta
from pathlib import Path

HERE = Path(__file__).resolve().parent
PACK = HERE / "calder_pack"
OUT = HERE / "calder_p2p_worklist.md"
SANDBOX = "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/"
AS_OF = date(2026, 10, 23)  # fixed "today", so every run prints the same numbers

# ------------------------------------------------------------ master data
# Info records and the source list, as in the master data topic (fictional).
INFO_RECORDS = {
    ("20000001", "CP-MTR15"): {"PurchasingInfoRecord": "5300000001", "NetPriceAmount": 905.00, "Days": 21},
    ("20000010", "CP-IMP200"): {"PurchasingInfoRecord": "5300000002", "NetPriceAmount": 335.00, "Days": 28},
    ("20000003", "CP-SK200"): {"PurchasingInfoRecord": "5300000003", "NetPriceAmount": 52.00, "Days": 10},
}
SOURCE_LIST = {"CP-MTR15": "20000001", "CP-IMP200": "20000010"}  # fixed sources in plant CP11
TERMS_DAYS = {"NT30": 30, "NT45": 45, "NT60": 60}
SERVICE_SUPPLIER = {"20000004": "Corvane Lifting Services"}  # not in the pack: services only

# Calder's tolerance rules for company code CP10. Course values, not SAP defaults.
# SAP sets tolerance limits per company code; these mimic a price and a quantity check.
PRICE_UPPER_PCT = 5.0      # block when the invoice price is more than 5% above the PO price ...
PRICE_UPPER_AMOUNT = 250.0  # ... or more than 250.00 USD above it for the item
PRICE_LOWER_PCT = -10.0    # below the PO price by more than 10%: a message, but no block
CONFIRM_WITHIN_DAYS = 3    # Calder expects an order acknowledgment within 3 days
GRIR_AGE_DAYS = 30         # received but not invoiced for 30 days: chase the invoice
PAYMENT_RUNS = [date(2026, 10, 27) + timedelta(weeks=w) for w in range(10)]  # Tuesdays

# ------------------------------------------------------------ the scenario
# One dict per purchase order item (each PO here has one item). Dates are ISO strings.
PURCHASES = [
    {"PurchaseOrder": "4500000001", "Requisition": "0010000001", "ReqBy": "MRP", "ReqDate": "2026-09-11",
     "Supplier": "20000001", "Material": "CP-MTR15", "Qty": 10, "Price": 950.00, "PODate": "2026-09-12",
     "Needed": "2026-10-05", "PriceNote": "buyer agreed an expedite price",
     "Confirmation": {"Date": "2026-09-14", "DeliveryDate": "2026-10-03"},
     "GoodsReceipts": [{"MaterialDocument": "4900000002", "Date": "2026-10-05", "Qty": 10, "Stock": "unrestricted"}],
     "Invoice": {"SupplierInvoice": "5105600001", "Date": "2026-10-09", "Qty": 10, "Price": 940.00}},
    {"PurchaseOrder": "4500000002", "Requisition": "0010000002", "ReqBy": "MRP", "ReqDate": "2026-10-14",
     "Supplier": "20000001", "Material": "CP-MTR15", "Qty": 20, "Price": 905.00, "PODate": "2026-10-15",
     "Needed": "2026-11-05", "Confirmation": None, "GoodsReceipts": [], "Invoice": None},
    {"PurchaseOrder": "4500000003", "Requisition": "0010000003", "ReqBy": "MRP", "ReqDate": "2026-09-30",
     "Supplier": "20000003", "Material": "CP-SK200", "Qty": 200, "Price": 52.00, "PODate": "2026-10-01",
     "Needed": "2026-10-15", "Confirmation": {"Date": "2026-10-02", "DeliveryDate": "2026-10-15"},
     "GoodsReceipts": [{"MaterialDocument": "4900000011", "Date": "2026-10-16", "Qty": 200,
                        "Stock": "quality inspection", "InspectionLot": "010000000041"}],
     "Invoice": {"SupplierInvoice": "5105600002", "Date": "2026-10-19", "Qty": 200, "Price": 52.00}},
    {"PurchaseOrder": "4500000004", "Requisition": "0010000004", "ReqBy": "MRP", "ReqDate": "2026-09-19",
     "Supplier": "20000001", "Material": "CP-MTR15", "Qty": 10, "Price": 905.00, "PODate": "2026-09-20",
     "Needed": "2026-10-12", "Confirmation": {"Date": "2026-09-21", "DeliveryDate": "2026-10-11"},
     "GoodsReceipts": [{"MaterialDocument": "4900000009", "Date": "2026-10-12", "Qty": 10, "Stock": "unrestricted"}],
     "Invoice": {"SupplierInvoice": "5105600003", "Date": "2026-10-14", "Qty": 10, "Price": 965.00}},
    {"PurchaseOrder": "4500000005", "Requisition": "0010000005", "ReqBy": "MRP", "ReqDate": "2026-09-07",
     "Supplier": "20000010", "Material": "CP-IMP200", "Qty": 40, "Price": 335.00, "PODate": "2026-09-08",
     "Needed": "2026-10-06", "Confirmation": {"Date": "2026-09-09", "DeliveryDate": "2026-10-06"},
     "GoodsReceipts": [{"MaterialDocument": "4900000008", "Date": "2026-10-08", "Qty": 36, "Stock": "unrestricted"}],
     "Invoice": {"SupplierInvoice": "5105600004", "Date": "2026-10-10", "Qty": 40, "Price": 335.00}},
    {"PurchaseOrder": "4500000006", "Requisition": "0010000006", "ReqBy": "manual", "ReqDate": "2026-08-24",
     "Supplier": "20000001", "Material": "CP-MTR15", "Qty": 5, "Price": 905.00, "PODate": "2026-08-25",
     "Needed": "2026-09-18", "Confirmation": {"Date": "2026-08-26", "DeliveryDate": "2026-09-16"},
     "GoodsReceipts": [{"MaterialDocument": "4900000004", "Date": "2026-09-18", "Qty": 5, "Stock": "unrestricted"}],
     "Invoice": None},
    {"PurchaseOrder": "4500000007", "Requisition": "0010000007", "ReqBy": "manual", "ReqDate": "2026-09-28",
     "Supplier": "20000004", "Material": "", "Service": "Annual inspection, overhead crane",
     "CostCenter": "CP11-410", "Qty": 1, "Price": 3800.00, "PODate": "2026-09-29", "Needed": "2026-10-13",
     "Confirmation": None, "GoodsReceipts": [],
     "ServiceEntrySheet": {"ServiceEntrySheet": "1000000001", "Submitted": "2026-10-14", "Approved": None},
     "Invoice": {"SupplierInvoice": "5105600005", "Date": "2026-10-20", "Qty": 1, "Price": 3800.00}},
]

# Price variances (percent of PO price) on Calder's last 24 PO-based invoices. Made up,
# fixed so that --tolerance prints the same table every time.
PAST_VARIANCES = [0.0, 0.0, 0.4, -1.2, 0.0, 2.1, 0.0, 3.4, 0.8, 0.0, -0.5, 6.2,
                  0.0, 1.5, 0.0, 4.8, 0.0, 0.0, 9.5, 0.3, 0.0, -2.0, 2.7, 12.0]
PAST_ITEM_VALUE = 4000.00  # USD per invoice item, the same for every past invoice in this sketch

OWNER = {
    "confirmation missing": "Buyer (purchasing group)",
    "quality block": "Quality engineer (usage decision)",
    "price block": "Buyer, with accounts payable",
    "quantity block": "Goods receiving, with the buyer",
    "received, not invoiced": "Accounts payable (ask the supplier)",
    "service not approved": "Requester (approve the service entry sheet)",
}


# --------------------------------------------------------------- helpers
def d(text: str) -> date:
    return date.fromisoformat(text)


def money(x: float) -> str:
    return f"{x:,.2f}"


def load_pack() -> dict:
    try:
        return {name: json.loads((PACK / f"{name}.json").read_text(encoding="utf-8"))
                for name in ("business_partners", "products")}
    except FileNotFoundError:
        sys.exit(f"Cannot read {PACK}. Run: python unit1b/make_calder_pack.py (Unit 1B setup).")


def suppliers(pack: dict) -> dict:
    """Supplier number -> (name, payment terms), from the pack's business partners."""
    out = {}
    for bp in pack["business_partners"]["BusinessPartners"]:
        if bp["Supplier"]:
            company = bp["to_Supplier"]["to_SupplierCompany"][0]
            out[bp["Supplier"]] = (bp["BusinessPartnerFullName"], company["PaymentTerms"])
    for number, name in SERVICE_SUPPLIER.items():
        out[number] = (name, "NT30")
    out["20000010"] = ("Calder GmbH (intercompany)", out["20000010"][1])  # shorter name for tables
    return out


def standard_prices(pack: dict) -> dict:
    return {p["Product"]: p["to_Valuation"][0]["StandardPrice"] for p in pack["products"]["Products"]}


def received(po: dict) -> int:
    return sum(gr["Qty"] for gr in po["GoodsReceipts"])


def due_date(po: dict, terms: dict) -> date:
    """Baseline date = invoice date in this sketch; due = baseline + the terms' days."""
    return d(po["Invoice"]["Date"]) + timedelta(days=TERMS_DAYS[terms[po["Supplier"]][1]])


def payment_run_for(due: date) -> date:
    """Calder pays in the last weekly run on or before the due date (the script's own rule)."""
    runs = [r for r in PAYMENT_RUNS if r <= due]
    return runs[-1] if runs else PAYMENT_RUNS[0]


# ------------------------------------------------------- three-way match
def match(po: dict) -> dict:
    """Compare PO, goods receipt and invoice the way a GR-based check would.
    Returns the checks, any block reasons and the GR/IR balance for the item."""
    checks, blocks = [], []
    inv = po["Invoice"]
    got = received(po)
    gr_value = round(got * po["Price"], 2)  # GR/IR is credited at the PO price
    if inv is None:
        return {"checks": [("invoice", "none received yet")], "blocks": [],
                "grir": 0.0 - gr_value, "variance": 0.0}
    if po.get("ServiceEntrySheet") and not po["ServiceEntrySheet"]["Approved"]:
        ses = po["ServiceEntrySheet"]
        checks.append(("service entry", f"sheet {ses['ServiceEntrySheet']} submitted {ses['Submitted']}, not approved"))
        blocks.append("service not approved")
        return {"checks": checks, "blocks": blocks, "grir": 0.0, "variance": 0.0}
    # Quantity: with GR-based checking, the invoice may not bill more than was received.
    if inv["Qty"] > got:
        checks.append(("quantity", f"invoiced {inv['Qty']}, received {got}, ordered {po['Qty']}"))
        blocks.append("quantity block")
    else:
        checks.append(("quantity", f"invoiced {inv['Qty']} = received {got}"))
    # Price: invoice price against the PO price, per Calder's tolerance rules.
    diff_each = inv["Price"] - po["Price"]
    pct = round(100 * diff_each / po["Price"], 2)
    diff_item = round(diff_each * inv["Qty"], 2)
    if pct > PRICE_UPPER_PCT or diff_item > PRICE_UPPER_AMOUNT:
        checks.append(("price", f"{money(inv['Price'])} vs PO {money(po['Price'])} ({pct:+.2f}%, "
                                f"{money(diff_item)} USD): above Calder's limit"))
        blocks.append("price block")
    elif pct < PRICE_LOWER_PCT:
        checks.append(("price", f"{pct:+.2f}% below the PO price: message, no block"))
    else:
        checks.append(("price", f"{money(inv['Price'])} vs PO {money(po['Price'])} ({pct:+.2f}%): within tolerance"))
    # Quality: stock still in quality inspection means no usage decision yet.
    lots = [gr["InspectionLot"] for gr in po["GoodsReceipts"] if gr["Stock"] == "quality inspection"]
    if lots:
        checks.append(("quality", f"inspection lot {lots[0]} has no usage decision; stock in quality inspection"))
        blocks.append("quality block")
    # GR/IR: credited at GR (qty x PO price), debited at invoice (qty x PO price).
    # A price difference goes elsewhere (here: a price difference account), not to GR/IR.
    grir = round(inv["Qty"] * po["Price"] - gr_value, 2) + 0.0  # + 0.0 turns -0.0 into 0.0
    return {"checks": checks, "blocks": blocks, "grir": grir, "variance": diff_item}


def evaluate(pack: dict) -> list:
    """Run the checks on every open purchase and record what is stuck, with value at risk."""
    terms = suppliers(pack)
    rows = []
    for po in PURCHASES:
        m = match(po)
        problems = list(m["blocks"])
        conf = po["Confirmation"]
        if conf is None and not po.get("Service") and not po["GoodsReceipts"] \
                and (AS_OF - d(po["PODate"])).days > CONFIRM_WITHIN_DAYS:
            problems.append("confirmation missing")
        if po["Invoice"] is None and po["GoodsReceipts"]:
            age = (AS_OF - d(po["GoodsReceipts"][-1]["Date"])).days
            if age >= GRIR_AGE_DAYS:
                problems.append("received, not invoiced")
        if po["Invoice"]:
            when = due_date(po, terms)
        else:
            when = d(po["Needed"])
        value = round(po["Invoice"]["Qty"] * po["Invoice"]["Price"], 2) if po["Invoice"] \
            else round(po["Qty"] * po["Price"], 2)
        rows.append({"po": po, "match": m, "problems": problems, "when": when, "value": value})
    return rows


def detail(row: dict) -> str:
    kind = row["problems"][0]
    po, m = row["po"], row["match"]
    if kind == "confirmation missing":
        return f"PO sent {po['PODate']}, no acknowledgment after {(AS_OF - d(po['PODate'])).days} days"
    if kind == "received, not invoiced":
        return f"GR {po['GoodsReceipts'][-1]['Date']}, GR/IR credit {money(-m['grir'])} open"
    return next(text for check, text in m["checks"]
                if check in kind or (kind == "service not approved" and check == "service entry"))


# --------------------------------------------------------------- trace
def trace_4500000001(pack: dict, show: bool = True) -> list:
    """Rebuild PO 4500000001 from requisition to payment and print each step."""
    po = PURCHASES[0]
    terms = suppliers(pack)
    std = standard_prices(pack)
    name = terms[po["Supplier"]][0]
    info = INFO_RECORDS[(po["Supplier"], po["Material"])]
    gr = po["GoodsReceipts"][0]
    inv = po["Invoice"]
    m = match(po)
    due = due_date(po, terms)
    run = payment_run_for(due)
    stock_value = po["Qty"] * std[po["Material"]]
    steps = [
        (po["ReqDate"], f"Requisition {po['Requisition']}", "MRP run / planner",
         f"{po['Qty']} x {po['Material']} needed {po['Needed']}"),
        (po["ReqDate"], f"Source: {name}", "Purchasing",
         f"fixed source list entry; info record {info['PurchasingInfoRecord']}, "
         f"{money(info['NetPriceAmount'])} USD, {info['Days']} days"),
        (po["PODate"], f"Purchase order {po['PurchaseOrder']}", "Buyer",
         f"{money(po['Price'])} each ({po['PriceNote']}), net {money(po['Qty'] * po['Price'])} USD"),
        (po["Confirmation"]["Date"], "Order acknowledgment", "Supplier",
         f"delivery confirmed for {po['Confirmation']['DeliveryDate']}"),
        (gr["Date"], f"Goods receipt {gr['MaterialDocument']}", "Goods receiving",
         f"stock {money(stock_value)} at standard, GR/IR credit {money(gr['Qty'] * po['Price'])}"),
        (inv["Date"], f"Supplier invoice {inv['SupplierInvoice']}", "Accounts payable",
         f"{money(inv['Qty'] * inv['Price'])} USD, matched, not blocked; due {due.isoformat()}"),
        (run.isoformat(), "Payment run (planned)", "Accounts payable",
         f"pays {money(inv['Qty'] * inv['Price'])} USD and clears the supplier item"),
    ]
    if not show:
        return steps
    print("Purchase order 4500000001, end to end (fictional Calder data)\n")
    for when, doc, team, text in steps:
        print(f"  {when}  {doc:<33} {team:<19} {text}")
    print("\nThree-way match for item 10 (PO, goods receipt, invoice):")
    for check, text in m["checks"]:
        print(f"  {check:<9} {text}")
    print(f"  GR/IR     balance after the invoice: {money(m['grir'])}")
    if po["Price"] != info["NetPriceAmount"]:
        pct = 100 * (po["Price"] - info["NetPriceAmount"]) / info["NetPriceAmount"]
        print(f"  note      the PO price is {pct:+.1f}% above info record {info['PurchasingInfoRecord']}; "
              "the match checks the invoice against the PO, not the info record")
    late = (d(gr["Date"]) - d(po["Confirmation"]["DeliveryDate"])).days
    print("\nCycle times:")
    print(f"  requisition to PO       {(d(po['PODate']) - d(po['ReqDate'])).days} day")
    print(f"  PO to goods receipt    {(d(gr['Date']) - d(po['PODate'])).days} days ({late} days after the confirmed date)")
    print(f"  receipt to invoice      {(d(inv['Date']) - d(gr['Date'])).days} days")
    print(f"  invoice to payment     {(run - d(inv['Date'])).days} days (planned; terms "
          f"{terms[po['Supplier']][1]}, due {due.isoformat()})")
    return steps


# ----------------------------------------------------------------- worklist
def worklist(rows: list) -> list:
    """Stuck items, most urgent first: earliest due or needed date, then most value."""
    return sorted([r for r in rows if r["problems"]], key=lambda r: (r["when"], -r["value"]))


def print_worklist(pack: dict, rows: list) -> None:
    who = suppliers(pack)
    items = worklist(rows)
    print(f"\nProcure-to-pay worklist as of {AS_OF.isoformat()} (fictional Calder data)\n")
    print(f"  {'#':<2} {'PO':<11} {'Supplier':<26} {'Due':<10} {'Problem':<23} {'Value':>10}  Owner")
    for n, r in enumerate(items, start=1):
        kind = r["problems"][0]
        past = " (past)" if r["when"] < AS_OF else ""
        print(f"  {n:<2} {r['po']['PurchaseOrder']:<11} {who[r['po']['Supplier']][0]:<26} "
              f"{r['when'].isoformat():<10} {kind:<23} {money(r['value']):>10}  {OWNER[kind]}{past}")
    total = sum(r["value"] for r in items)
    credit = -sum(r["match"]["grir"] for r in rows if r["match"]["grir"] < 0)
    debit = sum(r["match"]["grir"] for r in rows if r["match"]["grir"] > 0)
    print(f"\n  {len(items)} items stuck, {money(total)} USD held. "
          f"GR/IR open: {money(credit)} credit (received, not invoiced), "
          f"{money(debit)} debit (invoiced, not received).")
    print("\nPayment runs (Calder pays every Tuesday):")
    for r in rows:
        inv = r["po"]["Invoice"]
        if not inv:
            continue
        if r["match"]["blocks"]:
            print(f"  invoice {inv['SupplierInvoice']}  held: {', '.join(r['match']['blocks'])}")
        else:
            print(f"  invoice {inv['SupplierInvoice']}  due {r['when'].isoformat()}, "
                  f"paid in the run on {payment_run_for(r['when']).isoformat()}")


def show_po(pack: dict, rows: list, number: str) -> int:
    row = next((r for r in rows if r["po"]["PurchaseOrder"] == number), None)
    if not row:
        print(f"PO {number} is not in the scenario. Try one of: "
              + ", ".join(r["po"]["PurchaseOrder"] for r in rows))
        return 1
    po = row["po"]
    what = po.get("Service") or f"{po['Qty']} x {po['Material']}"
    print(f"PO {number} from {suppliers(pack)[po['Supplier']][0]}: {what} at {money(po['Price'])} USD, "
          f"needed {po['Needed']}\n")
    for check, text in row["match"]["checks"]:
        mark = "STUCK" if any(check in b for b in row["match"]["blocks"]) or \
            (check == "service entry" and "service not approved" in row["match"]["blocks"]) else \
            ("INFO " if check == "invoice" else "OK   ")
        print(f"  {mark} {check:<22} {text}")
    for kind in row["problems"]:
        if kind not in row["match"]["blocks"]:
            print(f"  STUCK {kind:<22} {detail({**row, 'problems': [kind]})}")
    print(f"\n  GR/IR balance: {money(row['match']['grir'])}")
    if row["problems"]:
        print(f"  Next: {OWNER[row['problems'][0]]}.")
    return 0


# --------------------------------------------------------- tolerance design
def tolerance_table() -> int:
    """How many past invoices would block, and how much variance passes unreviewed, per limit."""
    print(f"Price tolerance on Calder's last {len(PAST_VARIANCES)} PO-based invoices "
          f"({money(PAST_ITEM_VALUE)} USD per item; fictional)\n")
    print("  Percentage limit only; Calder's amount limit is left out here.\n")
    print(f"  {'Upper limit':<12} {'Blocked':>8} {'Share':>7} {'Overcharge passed unreviewed (USD)':>36}")
    for limit in (1.0, 2.0, 5.0, 10.0):
        blocked = [v for v in PAST_VARIANCES if v > limit]
        passed = sum(v for v in PAST_VARIANCES if 0 < v <= limit) * PAST_ITEM_VALUE / 100
        share = 100 * len(blocked) / len(PAST_VARIANCES)
        print(f"  {limit:>5.1f}%       {len(blocked):>8} {share:>6.0f}% {money(passed):>36}")
    print("\nA tighter limit blocks more invoices (more work in accounts payable);"
          "\na looser one lets more overcharge through. Synthetic data: it shows the trade-off, not a benchmark.")
    return 0


# --------------------------------------------------------------- deliverable
def write_markdown(pack: dict, steps: list, rows: list) -> None:
    who = suppliers(pack)
    fence = "`" * 3
    out = ["# Calder procure to pay: one purchase traced, open purchases triaged", "",
           "All data is fictional (Calder Pumpworks sample pack and the scenario in procure_to_pay.py).", "",
           fence + "mermaid", "flowchart LR",
           "  PR[Requisition<br/>from MRP] --> PO[Purchase order] --> CF[Confirmation]",
           "  CF --> GR[Goods receipt<br/>GR/IR credit] --> IV[Invoice<br/>three-way match] --> PAY[Payment run]",
           "  IV -.-> BLK[Blocked<br/>for payment]", fence, "",
           "## Purchase order 4500000001", "", "| Date | Document | Team | What happened |",
           "| --- | --- | --- | --- |"]
    out += [f"| {w} | {doc} | {t} | {x} |" for w, doc, t, x in steps]
    out += ["", f"## Worklist as of {AS_OF.isoformat()}", "",
            "| # | PO | Supplier | Due | Problem | Detail | Value (USD) | Owner |",
            "| --- | --- | --- | --- | --- | --- | ---: | --- |"]
    for n, r in enumerate(worklist(rows), start=1):
        kind = r["problems"][0]
        out.append(f"| {n} | {r['po']['PurchaseOrder']} | {who[r['po']['Supplier']][0]} | {r['when'].isoformat()} |"
                   f" {kind} | {detail(r)} | {money(r['value'])} | {OWNER[kind]} |")
    out += ["", "Synthetic numbers: they show the method, not results from an SAP system.", ""]
    OUT.write_text("\n".join(out), encoding="utf-8")
    print(f"wrote unit1b/{OUT.name}")


# ------------------------------------------------------------------ sandbox
def read_env_key(name: str) -> str:
    if os.environ.get(name):
        return os.environ[name]
    env = Path(".env")
    if env.exists():
        for line in env.read_text(encoding="utf-8").splitlines():
            key, sep, value = line.partition("=")
            if sep and key.strip() == name:
                return value.strip().strip('"').strip("'")
    return ""


def sandbox() -> int:
    """Count supplier invoices with a payment block in SAP's demo data. Codes are printed
    as they come, because their meanings are configuration: read them, don't assume them."""
    key = read_env_key("SAP_API_KEY")
    if not key:
        sys.exit("SAP_API_KEY is not set. Add it to .env (course setup topic), or run without --sandbox.")
    fields = ("SupplierInvoice,FiscalYear,CompanyCode,InvoicingParty,InvoiceGrossAmount,"
              "DocumentCurrency,PaymentBlockingReason,SupplierInvoiceStatus")
    url = SANDBOX + f"API_SUPPLIERINVOICE_PROCESS_SRV/A_SupplierInvoice?$top=50&$select={fields}&$format=json"
    request = urllib.request.Request(url, headers={"APIKey": key, "Accept": "application/json"})
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            invoices = json.load(response)["d"]["results"]
    except urllib.error.HTTPError as err:
        sys.exit(f"Sandbox answered HTTP {err.code}. 401 means the key is wrong; see the table below.")
    except (urllib.error.URLError, TimeoutError) as err:
        sys.exit(f"Cannot reach the sandbox: {getattr(err, 'reason', err)}. Check your network or proxy, or use the scenario.")
    print(f"SAP sandbox: {len(invoices)} supplier invoices read (SAP demo data, not Calder's)\n")
    for field in ("SupplierInvoiceStatus", "PaymentBlockingReason"):
        counts = {}
        for inv in invoices:
            value = inv.get(field) or "(empty)"
            counts[value] = counts.get(value, 0) + 1
        print(f"  {field}: " + ", ".join(f"'{k}' x{v}" for k, v in sorted(counts.items())))
    blocked = [inv["SupplierInvoice"] for inv in invoices if inv.get("PaymentBlockingReason")]
    print(f"  PaymentBlockingReason set on {len(blocked)} invoices"
          + (f", e.g. {', '.join(blocked[:3])}" if blocked else ""))
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="Trace and triage Calder's procure to pay.")
    parser.add_argument("--po", help="show the three-way match for one purchase order, e.g. 4500000004")
    parser.add_argument("--tolerance", action="store_true", help="compare price tolerance limits")
    parser.add_argument("--write", action="store_true", help="write the worklist to unit1b/calder_p2p_worklist.md")
    parser.add_argument("--sandbox", action="store_true", help="count blocked supplier invoices in SAP's sandbox")
    args = parser.parse_args()
    if args.sandbox:
        return sandbox()
    if args.tolerance:
        return tolerance_table()
    pack = load_pack()
    rows = evaluate(pack)
    if args.po:
        return show_po(pack, rows, args.po)
    if args.write:
        write_markdown(pack, trace_4500000001(pack, show=False), rows)
        return 0
    trace_4500000001(pack)
    print_worklist(pack, rows)
    return 0


if __name__ == "__main__":
    sys.exit(main())

Step 4: Trace PO 4500000001 and see the worklist

  1. Run the script:

    • Windows (PowerShell):

      python unit1b\procure_to_pay.py
    • macOS / Linux:

      python unit1b/procure_to_pay.py
  2. You should see exactly this. The script has no random parts, so your numbers must match:

Purchase order 4500000001, end to end (fictional Calder data)

  2026-09-11  Requisition 0010000001            MRP run / planner   10 x CP-MTR15 needed 2026-10-05
  2026-09-11  Source: Orrin Motor Works         Purchasing          fixed source list entry; info record 5300000001, 905.00 USD, 21 days
  2026-09-12  Purchase order 4500000001         Buyer               950.00 each (buyer agreed an expedite price), net 9,500.00 USD
  2026-09-14  Order acknowledgment              Supplier            delivery confirmed for 2026-10-03
  2026-10-05  Goods receipt 4900000002          Goods receiving     stock 9,200.00 at standard, GR/IR credit 9,500.00
  2026-10-09  Supplier invoice 5105600001       Accounts payable    9,400.00 USD, matched, not blocked; due 2026-11-23
  2026-11-17  Payment run (planned)             Accounts payable    pays 9,400.00 USD and clears the supplier item

Three-way match for item 10 (PO, goods receipt, invoice):
  quantity  invoiced 10 = received 10
  price     940.00 vs PO 950.00 (-1.05%): within tolerance
  GR/IR     balance after the invoice: 0.00
  note      the PO price is +5.0% above info record 5300000001; the match checks the invoice against the PO, not the info record

Cycle times:
  requisition to PO       1 day
  PO to goods receipt    23 days (2 days after the confirmed date)
  receipt to invoice      4 days
  invoice to payment     39 days (planned; terms NT45, due 2026-11-23)

Procure-to-pay worklist as of 2026-10-23 (fictional Calder data)

  #  PO          Supplier                   Due        Problem                      Value  Owner
  1  4500000006  Orrin Motor Works          2026-09-18 received, not invoiced    4,525.00  Accounts payable (ask the supplier) (past)
  2  4500000002  Orrin Motor Works          2026-11-05 confirmation missing     18,100.00  Buyer (purchasing group)
  3  4500000007  Corvane Lifting Services   2026-11-19 service not approved      3,800.00  Requester (approve the service entry sheet)
  4  4500000005  Calder GmbH (intercompany) 2026-11-24 quantity block           13,400.00  Goods receiving, with the buyer
  5  4500000004  Orrin Motor Works          2026-11-28 price block               9,650.00  Buyer, with accounts payable
  6  4500000003  Ridgeline Bearing Supply   2026-12-03 quality block            10,400.00  Quality engineer (usage decision)

  6 items stuck, 59,875.00 USD held. GR/IR open: 4,525.00 credit (received, not invoiced), 1,340.00 debit (invoiced, not received).

Payment runs (Calder pays every Tuesday):
  invoice 5105600001  due 2026-11-23, paid in the run on 2026-11-17
  invoice 5105600002  held: quality block
  invoice 5105600003  held: price block
  invoice 5105600004  held: quantity block
  invoice 5105600005  held: service not approved

How to read it:

  • The trace rebuilds the purchase from the MRP requisition to the planned payment. The source list names Orrin as the fixed source, and the info record proposes 905.00 USD and 21 days.
  • The match compares the invoice with the PO price of 950.00, not the info record. The buyer agreed an expedite price, so the PO is 5% above the info record, and the invoice at 940.00 is within tolerance. A wrong PO price passes the match: the three-way match only checks that the three documents agree with each other.
  • Cycle times show the supplier delivered 2 days after its own confirmed date, and the invoice will be paid 39 days after it arrived, in the last weekly run before the due date.
  • The worklist is sorted by the invoice due date (or, without an invoice, the date the goods are needed), then by value. Past dates are marked (past). Each line names one owner.
  • GR/IR shows both kinds of imbalance: 4,525.00 credit for motors received but not invoiced, and 1,340.00 debit for four impellers invoiced but not received.

Step 5: Look at one purchase order's match

  1. Show the match for PO 4500000004:

    • Windows (PowerShell):

      python unit1b\procure_to_pay.py --po 4500000004
    • macOS / Linux:

      python unit1b/procure_to_pay.py --po 4500000004
  2. You should see:

PO 4500000004 from Orrin Motor Works: 10 x CP-MTR15 at 905.00 USD, needed 2026-10-12

  OK    quantity               invoiced 10 = received 10
  STUCK price                  965.00 vs PO 905.00 (+6.63%, 600.00 USD): above Calder's limit

  GR/IR balance: 0.00
  Next: Buyer, with accounts payable.

The quantity matches and GR/IR is balanced: the receipt credited 9,050.00 and the invoice debited 9,050.00, because GR/IR always moves at the PO price. The 600.00 difference sits outside GR/IR, which is why a balanced GR/IR account doesn't prove an invoice was right. The buyer either accepts the new price (and fixes the PO) or asks Orrin for a credit memo; either removes the cause, and automatic release can do the rest.

  1. Now try --po 4500000005:
PO 4500000005 from Calder GmbH (intercompany): 40 x CP-IMP200 at 335.00 USD, needed 2026-10-06

  STUCK quantity               invoiced 40, received 36, ordered 40
  OK    price                  335.00 vs PO 335.00 (+0.00%): within tolerance

  GR/IR balance: 1,340.00
  Next: Goods receiving, with the buyer.

Here the price is right and the quantity isn't. The first question is for the dock: did four impellers arrive without a receipt, or are they still on the way? Try --po 4500000003, 4500000006 and 4500000007 too.

Step 6: Compare tolerance limits

  1. Run:

    • Windows (PowerShell):

      python unit1b\procure_to_pay.py --tolerance
    • macOS / Linux:

      python unit1b/procure_to_pay.py --tolerance
  2. You should see:

Price tolerance on Calder's last 24 PO-based invoices (4,000.00 USD per item; fictional)

  Percentage limit only; Calder's amount limit is left out here.

  Upper limit   Blocked   Share   Overcharge passed unreviewed (USD)
    1.0%              8     33%                                60.00
    2.0%              7     29%                               120.00
    5.0%              3     12%                               640.00
   10.0%              1      4%                             1,268.00

A tighter limit blocks more invoices (more work in accounts payable);
a looser one lets more overcharge through. Synthetic data: it shows the trade-off, not a benchmark.

At 1%, a third of invoices wait for a person, to protect 60 USD. At 10%, almost nothing blocks, and 1,268 USD of overcharge passes without review. Calder's 5% limit sits between the two, and its 250 USD amount limit catches large items that a percentage alone would let through. That is the tolerance design decision in miniature: review effort against money at risk.

Step 7: Write the worklist file

  1. Run:

    • Windows (PowerShell):

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

      python unit1b/procure_to_pay.py --write
  2. You should see wrote unit1b/calder_p2p_worklist.md.

  3. Open the file in VS Code and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS) to preview it. You should see a process diagram, the trace of PO 4500000001 and a six-line worklist with an owner on every line.

Step 8 (optional): Count blocked supplier invoices in 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\procure_to_pay.py --sandbox
    • macOS / Linux:

      python unit1b/procure_to_pay.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> supplier invoices read (SAP demo data, not Calder's)

  SupplierInvoiceStatus: '<code>' x<count>, '<code>' x<count>
  PaymentBlockingReason: '(empty)' x<count>, '<code>' x<count>
  PaymentBlockingReason set on <count> invoices, e.g. <number>, <number>

The script prints the codes as they come and doesn't translate them, because their meanings are configuration. A valid "empty" result is set on 0 invoices: the first 50 demo invoices simply have no payment block. The sandbox is read-only, so you can't release anything there.

Step 9: Save your work in Git

git add unit1b/procure_to_pay.py unit1b/calder_p2p_worklist.md
git commit -m "Unit 1B: procure-to-pay trace and worklist for Calder"

What each part of the code does

Part What it does
INFO_RECORDS, SOURCE_LIST The master data topic's info records and fixed sources, used for the trace's sourcing step
Tolerance constants Calder's course rules: block above +5% or +250.00 USD per item, message below -10%, confirmations due in 3 days, GR/IR chased after 30 days
PURCHASES Seven purchase orders, each with its confirmation, goods receipts, invoice and, for the service, a service entry sheet
match() The three-way match: quantity against receipts, price against the PO, quality against open inspection lots, and the GR/IR balance at PO price
evaluate(), detail() Adds the checks that aren't about the invoice (missing confirmation, received but not invoiced) and the value and date for each item
trace_4500000001() Rebuilds the pack's purchase from requisition to payment, shows the match and computes cycle times
worklist(), print_worklist() Sorts stuck items by due date, then value, names one owner, sums GR/IR, and shows which run pays each invoice
tolerance_table() Counts blocked invoices and unreviewed overcharge at four percentage limits
--po, --tolerance, --write Show one PO's match; compare tolerance limits; write the worklist file
sandbox() Reads up to 50 sandbox supplier invoices with $select and counts statuses and payment block reasons, without interpreting codes

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
KeyError naming a supplier or material The pack or the scenario was edited Run Step 2 again and copy the script again
Your numbers differ from the ones shown Prices, quantities or tolerances in the script were changed Copy the script again, or compare your edits
PO ... is not in the scenario You asked for a PO the script doesn't have Use one of the numbers the message lists
SAP_API_KEY is not set No key in the environment or .env Add it as in the course setup, or skip Step 8
Sandbox answered HTTP 401 The key is wrong or expired Copy the key again from the Business Accelerator Hub
Cannot reach the sandbox No network, or a company proxy blocks it Try another network, or stay with Steps 1 to 7
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), procure to pay for CP10 runs inside S/4HANA. Purchase orders go to large suppliers through SAP Business Network and to small ones by email. Invoices arrive as network documents, as PDFs by email and on paper. The bank receives payment files and returns statements. CP20 on ECC buys its castings separately and sells impellers to CP10. Anything that triages or explains reads released interfaces and writes only to a worklist; releases happen in SAP's own apps.

flowchart LR
  BN[SAP Business Network<br/>POs, confirmations,<br/>ship notices, invoices] <--> S4[S/4HANA CP10<br/>purchasing, receipts,<br/>invoices, payments]
  MAIL[PDF invoices<br/>by email] --> CAP[Invoice capture<br/>extraction]
  CAP -->|draft invoice| S4
  S4 -->|payment file| BANK[Bank]
  S4 -->|released APIs, CDS| TRI[Triage service<br/>read-only]
  TRI --> WL[Worklist per owner]
  WL -->|person releases in SAP app| S4
  ECC[ECC CP20] -.->|intercompany| S4

Non-functional requirements that decide the design

Requirement Typical target If missed
Invoice volume and peaks Capture and matching keep up with month-end peaks Invoices pile up, discounts and due dates are missed
Touchless rate A target share of invoices posts and pays without review, set with finance Accounts payable staffing grows with volume
Block resolution time Each block reaches its owner the same day Suppliers chase payments; late fees and lost goodwill
Payment run window Proposal reviewed and run completed before the bank's cut-off Payments a day late across all suppliers
Security Reviewers see only their company codes; supplier bank details changed under dual control Fraudulent bank changes, exposed prices
Auditability Every release, tolerance change and write-off logged with who and why Audit findings; no basis to evaluate any AI recommendation

Integration and identity. With SAP Business Network, a ship notice becomes an inbound delivery in S/4HANA through the managed gateway, and cancelling a ship notice deletes the inbound delivery unless a goods receipt exists (SAP Learning). Network invoices arrive in S/4HANA as drafts for accounts payable to complete (SAP Learning). For PDF invoices, SAP's reference architecture shows SAP Document AI reading an inbox, extracting fields and passing them on through SAP Integration Suite or a CAP application (SAP Architecture Center). The triage service reads with the user's identity where possible, so authorizations on company code and purchasing organization 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 receives about a third of its invoices as PDFs by email, keyed by hand. The large suppliers already use SAP Business Network. Accounts payable wants fewer touches without losing the three-way match.
  • Options: (a) keep keying PDFs; (b) move every supplier to SAP Business Network; (c) network for suppliers who can, plus extraction for the remaining PDFs, with every extracted invoice landing as a draft for a person to post.
  • Decision: (c). Network documents are structured from the start, and the free standard account lowers the bar for small suppliers. Extraction covers the rest without asking suppliers to change.
  • Consequences: Two intake channels to monitor. Extraction errors are possible, so drafts stay drafts until a person posts them, and extraction accuracy is measured per field. Tolerances and the match stay in S/4HANA, unchanged.

What a design review will ask

  1. Which channel does each supplier segment use for orders, confirmations and invoices, and who monitors failed messages?
  2. What are the tolerance limits per company code, who owns them, and how were they chosen?
  3. Can any component post an invoice, release a block or change supplier bank details without a person's decision?
  4. How is a duplicate invoice caught when the same bill arrives by email and by the network?
  5. How will you measure success: touchless rate, block age, GR/IR age, payments on time?

Where AI fits, and where it doesn't

Step Pattern Fit Note
Requisition text to material group Predict (classify) Moderate SAP Ariba Central Procurement offers material group recommendations from free text as an AI feature (SAP)
Sourcing Retrieve and explain Moderate Summarize quotations and past performance; the award stays with the buyer
Confirmations and delivery dates Predict Strong SAP offers Supplier Delivery Date Prediction for PO items (SAP); see predicting late deliveries
Goods receipt Plain automation is enough Scanning and ship notices already structure it
Invoice capture Extract Strong SAP Ariba Invoicing's AI-assisted multi-model extraction was GA in Q2 2026 (SAP News); see Document AI and SAP
Three-way match Plain automation is enough Weak for AI Deterministic, auditable rules; AI shouldn't replace them
Blocked-invoice resolution Retrieve and explain Strong Gather PO, receipt, inspection and price history, propose the fix and the owner; a person releases. The invoice exception copilot project builds this
GR/IR clean-up Predict root cause Strong SAP's GR/IR reconciliation proposes next step, priority and root cause with a separate license (SAP Learning)
Unusual invoices Detect anomalies Moderate Catch what tolerances miss; see anomaly detection in finance and procurement
Supplier risk Retrieve Moderate SAP Ariba Supplier Risk monitors financial, operational and other risk signals (SAP)
Spend classification Predict (classify) Strong SAP announced SAP Ariba Spend Analysis and Insights, mapping spend to UNSPSC or a customer taxonomy, on 16 September 2026 (SAP News)
Payment run Plain automation is enough The risk is fraud in bank details; control the master data change, not the run

SAP has also announced agents for this process. Its Sapphire 2026 innovation guide listed an Invoicing Assistant in Joule as planned for general availability in June 2026 (SAP); check its status and license before you plan on it.

The line is the same at every step: AI reads, extracts, explains and ranks; a person posts drafts, releases blocks, changes prices and tolerances, and approves bank details. Human approval in AI workflows and agent permissions show how to enforce that in code. Supplier invoices are third-party text, so an agent that reads them is exposed to prompt injection.

Pitfalls

  • Measuring blocked invoices per day. Teams push to loosen tolerances. Measure block age, touchless rate and the overcharge the blocks caught.
  • Trusting a balanced GR/IR. GR/IR moves at the PO price. A wrong PO price or a price variance doesn't show there.
  • Releasing instead of fixing. A manual release clears the symptom. Fix the cause (receipt, usage decision, credit memo) and let automatic release work; manual header blocks still need a person.
  • Forgetting services. Service invoices wait on approvals by people who aren't in purchasing. Put approvers on the worklist.
  • Unconfirmed dates in planning. If confirmations aren't required or chased, MRP plans on dates nobody promised.
  • Reading block reasons as universal codes. Payment block keys and tolerance settings are configuration. Read them from the system.
  • Assuming ECC and S/4HANA match. Business partners, flexible workflow and lean services differ, which matters for Calder's CP20.

Exercise

Turn the worklist into a triage note a Calder accounts payable lead could use, and test the tolerance rules yourself.

  1. Run python unit1b/procure_to_pay.py --write (Windows: python unit1b\procure_to_pay.py --write). Copy unit1b/calder_p2p_worklist.md to unit1b/calder_p2p_triage.md.
  2. In the copy, add a column Next action to the worklist table. For each line write one sentence: what the owner should check or do.
  3. Add a column AI could with one of: extract, explain, predict, detect or nothing. Use the table under Where AI fits to choose.
  4. In procure_to_pay.py, find "Price": 965.00 (invoice 5105600003) and change it to 930.00. Run python unit1b/procure_to_pay.py --po 4500000004. Note the result.
  5. Change it to 931.00 and run the same command again. Note what blocks it now, and why the percentage alone wouldn't have.
  6. Change it back to 965.00. Then, in PO 4500000003, change "Stock": "quality inspection" to "Stock": "unrestricted" (a positive usage decision). Run python unit1b/procure_to_pay.py and find which payment run now pays invoice 5105600002.
  7. Change the stock back to "quality inspection".
  8. In the triage file, add a heading Tolerance decision with three lines: the limit you would set for Calder, the run of Step 6 that supports it, and what you saw in Steps 4 and 5.
  9. Add a heading ECC note with one line on what a group view of blocked invoices needs from CP20. Preview the file and commit it with Git.

Done when: unit1b/calder_p2p_triage.md has the six-line worklist with Next action and AI could filled in for every line, a Tolerance decision section that says 930.00 passes and 931.00 is blocked by the 250.00 USD amount limit (260.00 USD over), a line stating that invoice 5105600002 is paid in the run on 2026-12-01 after the usage decision, and an ECC note. Keep it: the capstone at the end of Unit 1B traces one late supplier confirmation across the whole enterprise, and this triage note is its procure-to-pay part.

Check yourself

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

1The trace shows the PO at 950.00 and the info record at 905.00. The invoice says 940.00. Why does the match pass?

Answer: B. The three-way match compares invoice, PO and receipt. The PO price was 950.00, and 940.00 is within Calder's tolerance; the info record only supplied a default when the PO was created.

2Invoice 5105600003 is blocked for price, yet GR/IR for PO 4500000004 is balanced. How can that be?

Answer: B. Both the receipt and the invoice move GR/IR at the PO price, so it balances. The 600.00 variance goes to a price difference account here, which is why a clean GR/IR doesn't prove the invoice was right.

3An invoice is blocked for quantity and also carries a manual payment block in the header. The missing goods receipt is then posted. What happens?

Answer: B. Automatic release removes blocking reasons that are no longer valid, but SAP never auto-releases an invoice with a manual header block. That one is released separately by a person.

4You need supplier invoices with their payment block reason in custom code on an S/4HANA cloud edition. What do you read?

Answer: B. The Cloudification Repository lists I_SupplierInvoiceAPI01 as released and RBKP as not to be released, with that view as its successor. Released views survive upgrades; table access in custom code doesn't fit clean core.

5Seal kits for PO 4500000003 sit in quality inspection and the invoice is blocked. What is the right next step?

Answer: D. The block's cause is the open inspection lot. A positive usage decision moves the stock to unrestricted use and makes the quality reason invalid, so automatic release can do the rest.

6The tolerance table shows 33% of invoices blocked at 1% and 4% at 10%. What is the design question?

Answer: D. A tighter limit catches more overcharge but blocks more invoices for people to review. The limit is a decision about effort against money at risk, which finance and audit should own.

7Calder plans PDF invoice extraction. Where should the extracted data land?

Answer: B. Extraction can misread fields, so the design keeps extracted invoices as drafts until a person posts them. The match and tolerances then run in S/4HANA as before.

8A triage agent finds invoice 5105600004 blocked because 36 of 40 impellers were received. What should it do?

Answer: B. The agent can't know whether four impellers sit unbooked at the dock or never shipped. It routes the question to goods receiving with the facts; posting receipts and releasing blocks stay with people.

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.

  • Stage 6 of 11ProcureThe whole stage practices this topic, on Purchase requisitions, Purchase orders and Supplier invoices.
  • Stage 1 of 11Found the companyPractice it on Purchasing organizations.
  • Stage 2 of 11Set up financePractice it on Invoice tolerances.
  • Stage 4 of 11Create master dataPractice it on Purchasing info records.
  • Stage 11 of 11Pay and close the monthPractice it on Payment run.
  • ExercisePlanner's morningOne planning run, five exception messages, one of every kind: rank them, fix each where it is fixed, run planning again and explain what is left.
  • ChapterProduct lifecycleChange how a product is built without stopping the line: an engineering change record with a reason and a date, a redesigned ES-200 impeller with its plant data, price and supplier, and a bill of material with two dated lives, the old impeller ending and the new one starting on the change's date, so every order reads the bill valid on the day it starts; then the planner runs MRP across the date and reads the old impeller's run-out and the new one's first order.
  • 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.
  • ChapterTransportationPlan Calder's freight from Rockford: carriers and their agreements, freight units from deliveries, freight orders, the carrier chosen by cost, loads with stops, truck capacity and a departure worked back from the dates, the final charges with stops and detention after free time, tendering each load to a carrier that has to accept it, following the truck by its execution events, and settling the freight: each carrier invoice checked against the agreed charges, the freight cost shared among the deliveries so each customer's margin shows what its freight really cost, and inbound freight on purchased motors planned on the purchase order and carried into the motors' cost.
  • ChapterWarehouseLay out Calder's Rockford distribution center: storage types, sections and bins, then put stock away, pick, pack and count it, put people on the work and watch it.
  • DrillThe invoice that won't payProcurement, about 6 minutes

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

  • Running Materials Requirements Planning (SAP Learning) — MRP creates planned orders, purchase requisitions or delivery schedule lines; MRP Live creates schedule lines when a valid scheduling agreement exists, otherwise requisitions; a requisition reaches purchasing at once
  • Identifying Additional Aspects of Source Determination (SAP Learning) — source determination order in the MRP run (quota arrangement, source list with MRP indicator, scheduling agreement, contract, info record with automatic sourcing) and for manual requisitions
  • Monitoring Purchase Orders (SAP Learning) — confirmation control key sets expected confirmations and their sequence and relevance for MRP and goods receipt; inbound delivery; Monitor Supplier Confirmations app; ME2A; reminders ME91F and ME91FF
  • Describing Quality Management at Goods Receipt (SAP Learning) — inspection lot created at goods receipt for a purchase order, origin 01, stock-relevant; QM active for the movement type; inspection type in the material's QM view; post to inspection stock
  • Entering Invoices with Variances (SAP Learning) — tolerance limits per company code; tolerance keys grouped by variance type; above the upper limit the invoice posts and is blocked for payment, whole invoice; price variance postings for standard and moving average price
  • Releasing Blocked Invoices (SAP Learning) — MRBR; blocking reasons quantity, price, quality, date and when each becomes invalid; automatic release only for invoices blocked due to variances; RM08RELEASE; manual header block released separately
  • Using Fiori Apps for GR/IR Reconciliation (SAP Learning) — Reconcile GR/IR Accounts (F3302), Monitor GR/IR Account Reconciliation (F3303), GR/IR Process Insights (F5796); machine learning proposes next step, priority and root cause; separate subscription license
  • Processing Automatic Payments (SAP Learning) — parameters, proposal, edit, payment run; Manage Automatic Payments, Revise Payment Proposals (Public Edition), Manage Payment Media, Manage Payment Blocks; open items cleared when the run completes
  • Processing a Service Entry Sheet (SAP Learning) — Manage Service Entry Sheets - Lean Services; saving has no financial effect; automatic or manual approval; on approval a goods receipt document is posted
  • Cloudification Repository (SAP on GitHub) — objectReleaseInfoLatest.json read 23 October 2026; I_PurchaseRequisitionItemAPI01, I_PurchaseOrderAPI01, I_PurchaseOrderItemAPI01, I_PurOrdScheduleLineAPI01, I_PurchaseOrderHistoryAPI01, I_POSupplierConfirmationAPI01, I_SupplierInvoiceAPI01, I_MaterialDocumentItem_2, I_InspectionLot, I_ServiceEntrySheetAPI01 released; EBAN, EKKO, EKPO, EKET, EKBE, RBKP, RSEG, MSEG, QALS, LFA1 not to be released, with successors

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