See the period-end close as a chain of tasks fed by every module, find what blocks it at Calder, and where AI helps with accruals, matching and commentary.
Record to report is how a company turns a month of business into numbers it can sign. Every other process records as it goes: a goods issue posts cost, an invoice posts revenue, a confirmation posts labor. Record to report finishes the job at period end, in a set order:
Fill the gaps. Book costs that are real but not yet invoiced, such as a service that was done in October and will be billed in November. These are accruals.
Move costs to where they belong. Settle production and maintenance orders, and allocate shared costs, such as utilities, to the departments that used them.
Prove the balances. Reconcile the accounts that should net out: goods received against invoices, one Calder company against the other.
Report. Look at margin by product and customer, explain what moved, close the period, and hand the figures to the group.
Three ideas carry this topic:
The close is a chain, not a list. You can't allocate a cost center before the orders that charge it have settled. One late task early in the chain moves the last one.
Most close problems start outside finance. An equipment record without a cost center, an unapproved service entry sheet or a billing block shows up at period end as a finance problem.
Explaining matters as much as balancing. A margin that drops 14 points needs a reason, backed by documents, before anyone signs.
A slow or shaky close costs twice. Leaders decide later, on figures they don't fully trust, and the finance team spends its first week of every month chasing other departments.
Here is Tuesday morning, 3 November 2026, the second working day of the October close at Calder Pumpworks, the fictional pump maker this unit follows. The controller of the US company (CP10) sees this:
What the close board shows
What it means
Who acts
Maintenance order settlement is blocked
A September repair on the air compressor can't settle, because the compressor has no cost center
Master data, with the cost accountant
Overhead allocation and the margin review wait on it
Utilities can't be shared out until the repair cost has arrived
Cost accountant, controller
CP10's local close slips from day 3 to day 4
One missing field in a maintenance record moves the whole calendar
Controller
The crane inspection was done in October but isn't approved
Without an accrual, October's maintenance cost looks 3,800 USD too low
The requester, then the G/L accountant
The German company (CP20) shows 13,400 USD more owed by CP10 than CP10 has booked
Impellers shipped on 30 October are still on the truck
Intercompany accountant, both sides
ES-300 margin fell from 29% to 15%
One pump was scrapped at the pressure test
Controller, with production
The business decision is not "close faster". It is which upstream steps must be clean before the close starts, and who owns each one, because finance can't fix a cost center, approve a service or ship an impeller.
In SAP S/4HANA, most postings land in one table, the Universal Journal, so finance and controlling read the same numbers (Document flow and how logistics becomes finance explains how they get there). The close runs as a set of standard jobs and apps on top of it. As of October 2026, SAP's training covers these for S/4HANA:
Accruals Management proposes accruals for purchase orders from planned minus actual cost, with an optional review by the purchase order owner, and posts manual accruals such as an annual audit fee month by month (SAP Learning).
GR/IR reconciliation apps give accountants a worklist of goods receipts and invoices that don't match. Machine-learning proposals for next step, priority and root cause need a separate subscription (SAP Learning).
Intercompany Matching and Reconciliation (ICMR) matches the two sides of intercompany transactions with rules, tolerances and reason codes (SAP Learning).
Margin analysis is S/4HANA's profitability analysis, stored in the Universal Journal. The older costing-based variant, with its own tables, is not available in the cloud editions (SAP Learning).
SAP S/4HANA Finance for group reporting consolidates the companies: it releases the S/4HANA data, takes uploads from companies on other systems, translates currencies and eliminates intercompany balances (SAP Learning).
To run the close as a plan, SAP offers SAP Advanced Financial Closing, an application on SAP BTP. It holds task lists with owners, dependencies and approvals, and starts jobs in connected SAP S/4HANA Cloud, SAP S/4HANA and SAP ERP systems (SAP Learning). It is a separate product; check licensing with SAP.
For AI, SAP's site describes a Financial Closing Assistant that orchestrates close agents, among them an Accounting Accruals Agent and an Intercompany Matching and Reconciliation Agent (SAP). At SAP Sapphire in May 2026, SAP said general availability of the assistant was planned for Q2 2026 (SAP). That date has passed, and as of 4 November 2026 the use-case page gives no release date, edition or region, so treat the Sapphire plan as unconfirmed. As of 4 November 2026, the Accounting Accruals Agent's page lists SAP S/4HANA Cloud Public Edition, requires AI Units and offers beta registration without stating general availability (SAP). Confirm what is live for your edition and region before you plan on it.
flowchart LR
L[Logistics<br/>cut-off] --> B[Billing<br/>and freight]
L --> A[Accruals,<br/>GR/IR review]
L --> S[Order<br/>settlement]
A --> AL[Allocations]
S --> AL
AL --> M[Margin review,<br/>commentary]
B --> M
A --> R[Reconciliations,<br/>intercompany]
M --> C[Close period]
R --> C
C --> G[Group<br/>reporting]
Step
Team
Document created
Posts to finance?
Logistics cut-off
Warehouse, purchasing, production
Last goods movements and confirmations of the month
Yes, as usual
Billing and freight
Billing, freight settlement
Customer invoices, freight settlement documents
Yes: revenue, freight cost
Accruals
General ledger accountants
Accrual postings (reversed or replaced later)
Yes: cost and liability for the period
GR/IR review
Accounts payable
Clarified items, write-offs where allowed
Only for write-offs
Order settlement
Cost accounting
Settlement documents for production and maintenance orders
"The close is finance's job." Finance runs it, but most of its inputs come from logistics, maintenance and sales. The fix for a blocked settlement is usually a master data change.
"The Universal Journal makes reconciliation unnecessary." It removes the reconciliation between finance and controlling inside S/4HANA. It doesn't match goods to invoices, one company to another, or SAP to other systems.
"An accrual is a guess." It is an estimate with a basis: a purchase order, a contract, last months' bills. Each should be traceable and reviewed.
"Intercompany differences mean someone made a mistake." Many are timing: goods shipped by one company and not yet received by the other. The reason matters as much as the amount.
"An agent can close the books." It can propose accruals, match items and draft commentary. A person approves postings and signs the figures.
Pick one answer for each question. The explanation appears after you choose.
1Calder's local close slips a day because a maintenance order can't settle. Where does the fix belong?
Answer: B. The order can't settle because the equipment has no cost center to receive the cost. Fixing the record lets the order settle normally; a manual entry hides the cause and repeats next month.
2Why does overhead allocation wait for maintenance order settlement?
Answer: C. The close is a chain. If the cycle runs before the repair cost reaches the utilities cost center, that cost stays behind and never reaches the products this month.
3The crane inspection was done in October, but its service entry sheet isn't approved. What should October's books show?
Answer: C. The cost belongs to October because the work was done then. An accrual books it now, based on the purchase order, so October's maintenance cost isn't understated.
4CP20 shows 13,400 USD more owed by CP10 than CP10 has booked. The goods left Augsburg on 30 October. What is the likely reason?
Answer: A. CP20 billed when it shipped; CP10 books when the goods and invoice arrive. The difference is real but explainable, and the reconciliation should record it with a timing reason, not chase an error.
5ES-300 margin fell 14 points. What should the controller have before signing the commentary?
Answer: D. A swing needs an explanation that traces to documents, here a scrapped pump on one production order. The numbers come from the postings, so anyone can check them.
6A vendor offers an agent that proposes accruals from past bills and posts them at month end. What do you insist on?
Answer: B. Proposals save time; postings change the books. A person accepts, adjusts or rejects each one, with a record, which is also how SAP describes its own accruals agent.
7Calder wants one plan for the close across CP10 on S/4HANA and CP20 on ECC, with owners and dependencies. Which SAP offering fits?
Answer: D. Advanced Financial Closing holds task lists with owners, dependencies and approvals and connects to S/4HANA and SAP ERP systems. The others are tasks inside the close, not the plan for it.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Think of the close as a build that compiles every module's work into one set of figures. Each close task is a build step with inputs and outputs:
Settlement takes orders full of cost and outputs cost centers, products and assets that carry it.
Allocation takes cost centers and outputs other cost centers, or margin, that carry a share.
Accruals take purchase orders, contracts and history and output cost that belongs to the period.
Reconciliation takes two sets of postings that should agree and outputs a match, a reason or an adjustment.
Reporting takes all of it and outputs margin, statements and commentary.
A build fails when an input is missing, and it fails late when the missing input sits early in the chain. That is why a cost center missing from one equipment record in maintenance can move the date the group reports. Every worklist and AI idea in this topic does one of three things: finds a missing input before the step that needs it, matches outputs that should agree, or explains an output that moved.
Every posting is a journal entry: a header with company code, fiscal year, document type, posting date and period, and items with G/L account, amount and the account assignments that controlling needs, such as cost center, order, profit center or profitability segment. In S/4HANA, the items sit in the Universal Journal; SAP's released CDS views are I_JournalEntry and I_JournalEntryItem, and the Cloudification Repository marks the tables underneath, ACDOCA and BKPF, as not to be released (Cloudification Repository).
Two fields on each item matter most for the close:
The posting period. The close is the moment a period stops taking postings. Companies usually open the next period before the month ends and keep the old one open for finance only for a few days.
The trading partner (PartnerCompany in the API model). It names the group company on the other side of an intercompany posting. Without it, the posting looks like business with a stranger (SAP Cloud SDK model, 2022).
A close plan is a task list built from a template. SAP's training describes both tools this way:
In the Financial Closing cockpit in S/4HANA, the template follows a closing hierarchy of organizational units, such as controlling areas and their company codes. A periodic task list is created for a key date, fiscal year and period, and the system fills in time-related program parameters. Tasks can be notes, transactions, programs, jobs or workflows. Predecessor relationships on the Dependencies tab make the system run tasks in the right order (SAP Learning).
In SAP Advanced Financial Closing, templates "depict the closing structure" of the organization with folders, task list models and dependencies. Tasks can come from models, from spreadsheets, from external jobs or from workflows, and tasks with approval types need a responsible user or group, which a consistency check enforces (SAP Learning).
Both describe the same idea: a close is a directed graph of tasks, each with an owner, a day and predecessors. The script in this topic builds that graph for Calder.
S/4HANA's Accruals Management uses an Accrual Engine with two parts that matter here (SAP Learning):
Purchase order accruals. The engine computes planned costs, by default from the PO item's delivery schedule, and actual costs already posted by goods receipts or invoices up to the key date. The accrual is planned minus actual, kept only if positive. A proposal run creates the amounts, usually after the last receipts and invoices of the period; a posting run posts them. Review by the PO owner or a cost accountant is optional, and if nobody reviews, the proposed amount posts. Approval is stricter: the amount posts only after approval. Postings carry reference procedure ACES4. The apps include Schedule Accruals Jobs and Display Purchase Order Accruals.
Manual accruals. Each recurring accrual, such as an annual fee, is an accrual object with an item type, a validity period, a total and an accrual method that spreads it over periods. At period end the posting run checks what the Universal Journal already holds and posts only the remaining amount (SAP Learning).
At Calder (fictional), the crane inspection on PO 4500000007 was done in October and its service entry sheet waits for approval, so October shows no cost for it. The PO accrual run proposes 3,800.00 USD to cost center CP11-410. Two manual accrual objects post too: the audit fee at 3,000.00 USD a month, and an estimate of October's Rockford utilities, 8,400.00 USD, based on the last three bills.
The GR/IR clearing account holds two kinds of open items: goods received but not invoiced (a credit) and invoiced but not received (a debit). Items clear themselves when quantities and amounts match; the rest need clarification. SAP's apps for this are Reconcile GR/IR Accounts (F3302), Monitor GR/IR Account Reconciliation (F3303) and GR/IR Process Insights (F5796). Processors set a status, a priority and a root cause such as "Missing Goods Receipt", and only write-off statuses allow a write-off (SAP Learning).
At Calder, the review finds the two items from the procure-to-pay topic: 4,525.00 USD credit for five motors received in September and never invoiced, and 1,340.00 USD debit for four impellers invoiced by CP20 and not received.
Production and maintenance orders settle at period end (plan to produce and maintain to settle cover how). Then allocations share out cost centers that serve others. SAP's training on S/4HANA's universal allocation describes two cycle types: distribution keeps the original primary cost elements on the receiver, and overhead allocation (assessment) posts with an assessment cost element and can also send to profit centers and profitability segments. Cycles are built in the Manage Allocations app and run at period end (SAP Learning).
At Calder, cycle CP11-OH (a course label) sends the utilities cost center CP11-460 to the assembly and test cost centers by October energy use. The compressor's September repair, order 4000112, should settle to CP11-460, but the compressor has no cost center. Run the cycle before that is fixed, and the repair cost misses October's product costs.
At period end, open items and balances in foreign currency are revalued at the closing rate with report FAGL_FCV (Perform Foreign Currency Valuation). The difference posts to an adjustment account, not the reconciliation account, and by default is reversed on the first day of the next period (SAP Learning). For Calder, CP20 keeps its books in EUR but bills CP10 in USD, so its USD receivables from CP10 are revalued every month.
Intercompany balances are created by ordinary documents. When a plant of one company delivers for a sales order of another, SAP creates an internal invoice (billing type IV) in the delivering company code, priced with condition IV01, which references the statistical intercompany price PI01 on the customer's invoice (SAP Learning). When one company buys from another on a purchase order, as CP10 buys impellers from CP20, the seller bills and the buyer posts a supplier invoice. Either way, each side posts with the other as trading partner, and the two must agree.
Intercompany Matching and Reconciliation (ICMR) compares them. A matching method is a set of rules; each rule splits the data into two sides and applies a matching expression, and items that don't match move on to the next rule. Rules can express exact, suggested or exceptional matches. Reconciliation cases group results with tolerances that set the status. Items with differences get reason codes, and adjustments can post automatically or through an approval workflow (SAP Learning).
Margin analysis records revenue and cost by profitability segment: the combination of product, customer, sales organization and other characteristics that a sale or cost belongs to. In S/4HANA it is stored in the Universal Journal; costing-based profitability analysis keeps value fields in its own tables and is not available in cloud deployments (SAP Learning). I_ProfitabilitySegment is a released CDS view (Cloudification Repository).
One timing fact explains many margin swings: the goods issue posts cost of goods sold when the goods leave, and billing posts revenue when the invoice is created. A delivery with a billing block at month end puts cost in one month and revenue in the next.
SAP's training on group reporting lists the steps: release the Universal Journal from integrated S/4HANA companies into the consolidation journal (from ACDOCA to ACDOCU), collect external data for companies on other systems by upload, manual entry or Group Reporting Data Collection, validate, translate currency, match intercompany items with ICMR, and eliminate intercompany balances (SAP Learning). Group journal entries change only the consolidation journal; I_CnsldtnGroupJrnlEntryTP is released (Cloudification Repository).
flowchart LR
PO[PO 4500000007<br/>crane inspection] --> AC[Accrual<br/>3,800.00]
MO[Order 4000112<br/>compressor] -.->|blocked| ST[Settlement]
ST -.-> CC[CP11-460<br/>utilities]
UT[Utilities<br/>accrual] --> CC
CC --> AL[Cycle CP11-OH]
AL --> PC[CP11-410,<br/>CP11-420]
GI[Goods issue<br/>order 1000006] --> MA[Margin<br/>ES-200]
IC[CP20 invoice<br/>95000047] -.->|in transit| R[ICMR<br/>timing]
Step
Posts to finance
Why
Purchase order accrual
Cost (cost center or order) debited, accrued liability credited
The service was done; the invoice hasn't posted
Manual accrual
Cost debited, accrual account credited
The cost belongs to the period
Reversal or clearing of an accrual
The opposite, when the real cost arrives
So the cost isn't counted twice
Order settlement
Order credited; cost center, product or asset debited
Costs reach their owner
Allocation
Sender credited, receivers debited (assessment or original cost elements)
Shared costs reach their users
Foreign currency valuation
Adjustment account against exchange gain or loss
Balances at the closing rate
GR/IR write-off (rare)
GR/IR against an expense or price difference account
Clearing an item that will never match
ICMR adjustment (with approval)
Correction in one company
A real difference, fixed at the source
Group elimination
Consolidation journal only
The group can't sell to itself
At Calder (fictional), the October accruals post 15,200.00 USD: 3,800.00 for the crane inspection, 3,000.00 for the audit fee and 8,400.00 for utilities. The allocation cycle sends 9,000.00 USD from CP11-460, which becomes 10,240.00 once order 4000112 can settle there. You compute both in Steps 5 and 6.
As of 24 October 2026, these released interfaces carry each step. The CDS views come from SAP's Cloudification Repository (file read 24 October 2026). The OData API and its fields come from SAP's generated API model, published in 2022 (SAP Cloud SDK model, 2022).
I_CnsldtnGroupJrnlEntryTP and many I_Cnsldtn... master data views
Group journal entries, consolidation master data
An SAP Knowledge Base article notes that the accounting document read API covers only the entry view, and points to A_JournalEntryItemBasic for journal entry items (SAP KBA 2821106). The journal entry posting APIs are SOAP services under communication scenario SAP_COM_0002; SAP's guideline post lists what they don't do, such as clearing open items or calculating tax (SAP Community, 2019).
You will write one script that plays Calder's October close on the morning of workday 2. It builds the close as a graph of tasks, pushes the upstream issues from the earlier topics through it to find the projected close day and the critical path, proposes accruals, runs the utilities allocation with and without the stuck maintenance order, matches CP10 against CP20, and explains the margin swings with a draft commentary computed from the numbers. An optional step reads journal entry items from SAP's sandbox.
flowchart LR
P[calder_pack/<br/>company codes,<br/>partners, products] --> S[record_to_report.py]
SC[Scenario in the script<br/>tasks, issues, accruals,<br/>intercompany, margin] --> S
S --> B[Close board]
S --> A[Accruals<br/>--accruals]
S --> AL[Allocation<br/>--allocation]
S --> I[Matching<br/>--intercompany]
S --> M[Margin and<br/>commentary --margin]
S --> R[calder_close_report.md]
K[SAP sandbox<br/>optional] --> S
Rewrite the pack, so you start from the same data as this topic:
Windows (PowerShell):
python unit1b\make_calder_pack.py
macOS / Linux:
python unit1b/make_calder_pack.py
The last lines should read:
Calder sample pack 1.0: order 1000001, net 29,100.00 USD, cost 19,600.00 USD.
All data is fictional. Next: python check_unit1b.py
The close script checks that company codes CP10 (S/4HANA) and CP20 (ECC) and the intercompany partners 10000010 and 20000010 are in the pack, and it takes the pumps' standard costs from products.json.
In VS Code, create a new file, paste the script below, and save it as record_to_report.py inside the unit1b folder.
Read the blocks TASKS, issues(), PO_ACCRUAL_ITEMS, MANUAL_ACCRUALS, GRIR_OPEN, CP10_ITEMS, CP20_ITEMS and MARGIN first. They are Calder's October close written as Python data. Every value is invented. The open items come from the earlier Unit 1B topics: order 4000112 from maintain to settle, PO 4500000007 and the GR/IR items from procure to pay, order 1000006 from order to cash, the carrier invoices from warehouse and transportation, and production orders 1100015 and 1100016 from plan to produce. Task IDs, workdays, cycle names, reason codes and late counts are the course's own, not SAP values.
"""Record to report at Calder: the October close as a dependency chain.
Calder Pumpworks is a fictional company. The pack (unit1b/calder_pack/) and the
scenario below (close tasks, accruals, intercompany items, margins, amounts)
are made up for the course. Field names follow SAP's released APIs where the
script mirrors one (API_JOURNALENTRYITEMBASIC_SRV, I_JournalEntryItem), but the
logic is a teaching sketch, not SAP's. Task IDs, workdays, cycle names, reason
codes and late counts are course labels, not SAP values.
Run it from your course folder:
python unit1b/record_to_report.py the close board for CP10, October 2026, on 2026-11-03
python unit1b/record_to_report.py --accruals purchase order and manual accruals, and the GR/IR review
python unit1b/record_to_report.py --allocation the utilities allocation, with and without order 4000112
python unit1b/record_to_report.py --intercompany match CP10 against CP20 by trading partner
python unit1b/record_to_report.py --margin margin by pump model, October vs September, with drivers
python unit1b/record_to_report.py --write write unit1b/calder_close_report.md
python unit1b/record_to_report.py --sandbox sum journal entry items 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 sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
HERE = Path(__file__).resolve().parent
PACK = HERE / "calder_pack"
PERIOD = "2026/010" # fiscal year 2026, period 10 (October)
AS_OF_WD = 2 # Tuesday 2026-11-03 is workday 2 of the close
WORKDAYS = {-1: "2026-10-30", 1: "2026-11-02", 2: "2026-11-03", 3: "2026-11-04",
4: "2026-11-05", 5: "2026-11-06"}
LOCAL_CLOSE_TARGET = 3 # CP10's books for October are closed on workday 3
GROUP_CLOSE_TARGET = 5 # group figures are final on workday 5
MATCH_TOLERANCE = 10.00 # USD; intercompany differences up to this are accepted
# ------------------------------------------------------------ the master data fix
# Air compressor 10000104 has no cost center (see the maintain-to-settle topic), so
# maintenance order 4000112 can't settle. The exercise sets this to "CP11-460".
COMPRESSOR_COST_CENTER = ""
# ------------------------------------------------------------ close task list (fictional)
# (id, task, owner, planned workday, predecessors, status)
TASKS = [
("C01", "Open November; restrict October to finance", "G/L accountant", -1, [], "done"),
("C02", "Logistics cut-off: last October goods movements", "Warehouse and purchasing", -1, [], "done"),
("C03", "Billing due list for October deliveries", "Billing clerk", 1, ["C02"], "done"),
("C04", "Freight settlement and carrier invoices", "Freight settlement clerk", 1, ["C02"], "done"),
("C05", "GR/IR review", "Accounts payable", 1, ["C02"], "done"),
("C06", "Purchase order accruals: propose, review, post", "G/L accountant", 2, ["C05"], "in process"),
("C07", "Manual accruals: audit fee, utilities", "G/L accountant", 2, ["C01"], "open"),
("C08", "Production orders: WIP and variances", "Cost accountant", 2, ["C02"], "done"),
("C09", "Production order settlement", "Cost accountant", 2, ["C08"], "open"),
("C10", "Maintenance order settlement", "Cost accountant", 2, ["C02"], "open"),
("C11", "Overhead allocation cycle CP11-OH", "Cost accountant", 2, ["C07", "C10"], "open"),
("C12", "Foreign currency valuation", "G/L accountant", 2, ["C05"], "open"),
("X01", "CP20 (ECC) local close and reporting package", "Augsburg finance", 3, [], "open"),
("C13", "Intercompany reconciliation CP10-CP20", "Intercompany accountant", 3, ["C03", "C06", "X01"], "open"),
("C14", "Margin analysis review and commentary", "Controller", 3, ["C03", "C04", "C09", "C11"], "open"),
("C15", "Balance sheet reconciliations and sign-off", "Accounting manager", 3, ["C06", "C07", "C12", "C13"], "open"),
("C16", "Close October for all postings", "G/L accountant", 3, ["C14", "C15"], "open"),
("G01", "Group reporting: release, upload CP20, translate, eliminate", "Group accountant", 4, ["C16", "X01"], "open"),
]
# How often each task finished late in the last twelve closes (made up).
LATE_HISTORY = {"C06": 3, "C09": 2, "C10": 4, "C11": 3, "C13": 7, "C14": 5, "C15": 2, "X01": 4}
# ------------------------------------------------------------ open issues from upstream
# (id, task it hits, source process, what, owner, workday it is expected to be fixed, blocks?)
def issues() -> list:
found = []
if not COMPRESSOR_COST_CENTER:
found.append(("I1", "C10", "maintain to settle",
"order 4000112 (1,240.00 USD) can't settle: equipment 10000104 has no cost center",
"Master data, with the cost accountant", 4, True))
found += [
("I2", "C06", "procure to pay",
"service entry sheet 1000000001 (crane inspection) not approved; the accrual proposal awaits review",
"Requester (PO owner)", 2, False),
("I3", "C14", "order to cash",
"order 1000006 delivered on 2026-10-20 but billing block AC: cost in October, revenue later",
"Billing clerk, with sales", 4, False),
("I4", "C04", "warehouse and transport",
"carrier invoice LFL-77203 duplicates LFL-77188; LFL-77120 billed fuel at 22%",
"Freight settlement clerk", 3, False),
]
if not intercompany_partner_fixed():
found.append(("I5", "C13", "intercompany (CP20 on ECC)",
"CP20 booked CP10's IT service recharge without a trading partner",
"Augsburg finance", 3, True))
return found
# ------------------------------------------------------------ accruals (fictional)
# Purchase order accruals: planned cost from the delivery schedule up to the key date,
# minus actual cost posted (goods receipts, approved service entry sheets, invoices).
PO_ACCRUAL_ITEMS = [
# (PO, item text, supplier, account assignment, planned by 2026-10-31, actual posted, reviewer)
("4500000007", "Annual inspection, overhead crane", "20000004 Corvane Lifting Services",
"cost center CP11-410", 3800.00, 0.00, "PO owner (requester)"),
("4500000002", "20 x CP-MTR15 motors, due 2026-11-05", "20000001 Orrin Motor Works",
"stock (GR-based)", 0.00, 0.00, "none needed"),
("4500000006", "5 x CP-MTR15 motors, received 2026-09-18", "20000001 Orrin Motor Works",
"stock (GR-based)", 4525.00, 4525.00, "none needed"),
]
# Manual accruals: accrual objects with a total, a method and what is already posted.
MANUAL_ACCRUALS = [
# (accrual object, text, cost center, total for the year, method, posted through September)
("ACC-AUD-2026", "Annual audit fee 2026", "CP10-900", 36000.00, "straight line Jan-Dec", 27000.00),
("ACC-UTL-2610", "Rockford electricity and gas, October (bill due mid-November)", "CP11-460",
8400.00, "one period, estimate", 0.00),
]
UTILITY_BILLS = {"2026-07": 8150.00, "2026-08": 8620.00, "2026-09": 8430.00} # basis of the estimate
# GR/IR open items after the procure-to-pay topic (USD; credit negative).
GRIR_OPEN = [
("4500000006", "20000001 Orrin Motor Works", "received 2026-09-18, not invoiced", -4525.00),
("4500000005", "20000010 Calder GmbH (intercompany)", "40 invoiced, 36 received", 1340.00),
]
# ------------------------------------------------------------ allocation (fictional)
# Cycle CP11-OH sends the utilities cost center CP11-460 to the production cost centers
# by October kWh (course values). Order 4000112 would settle to CP11-460 once the
# compressor has that cost center.
ALLOC_SENDER = "CP11-460"
ALLOC_RECEIVERS = {"CP11-410 (ASSY1)": 0.60, "CP11-420 (TEST1)": 0.40}
SENDER_COSTS = [("depreciation, compressor asset 30000021", 600.00)] # posted in October
# ------------------------------------------------------------ intercompany items (fictional)
# CP10 runs S/4HANA; CP20 runs ECC and reports through an upload. Amounts in transaction
# currency USD (the currency of the intercompany invoices), so exchange rates don't
# create differences at this level. PartnerCompany is the trading partner.
CP10_ITEMS = [
{"CompanyCode": "CP10", "PartnerCompany": "CP20", "Account": "payable to 20000010",
"Reference": "95000041", "Document": "5105600004", "AmountInTransactionCurrency": -13400.00,
"Text": "40 x CP-IMP200 for PO 4500000005"},
{"CompanyCode": "CP10", "PartnerCompany": "CP20", "Account": "receivable from CP20",
"Reference": "90000051", "Document": "90000051", "AmountInTransactionCurrency": 2500.00,
"Text": "IT service recharge October"},
]
CP20_ITEMS = [
{"CompanyCode": "CP20", "PartnerCompany": "CP10", "Account": "receivable from 10000010",
"Reference": "95000041", "Document": "95000041", "AmountInTransactionCurrency": 13400.00,
"Text": "40 x CP-IMP200, shipped 2026-10-07"},
{"CompanyCode": "CP20", "PartnerCompany": "CP10", "Account": "receivable from 10000010",
"Reference": "95000047", "Document": "95000047", "AmountInTransactionCurrency": 13400.00,
"Text": "40 x CP-IMP200 for PO 4500000008, shipped 2026-10-30"},
{"CompanyCode": "CP20", "PartnerCompany": "", "Account": "payable to an IT supplier account",
"Reference": "90000051", "Document": "1900000388", "AmountInTransactionCurrency": -2500.00,
"Text": "IT service Calder Inc. October"},
]
def intercompany_partner_fixed() -> bool:
return all(i["PartnerCompany"] for i in CP20_ITEMS)
# ------------------------------------------------------------ margin (fictional)
# Margin by product in CP10: billed revenue, cost of goods sold at standard from goods
# issues, production variances settled in the month, and freight. Units and amounts are
# course values; prices and standard costs come from the sample pack.
MARGIN = {
"2026-09": {"CP-ES200": {"billed": 14, "issued": 14, "variance": 0.00, "freight": 1800.00},
"CP-ES300": {"billed": 4, "issued": 4, "variance": 0.00, "freight": 1000.00}},
"2026-10": {"CP-ES200": {"billed": 16, "issued": 17, "variance": 388.00, "freight": 2100.00},
"CP-ES300": {"billed": 5, "issued": 5, "variance": 4280.00, "freight": 1320.00}},
}
PRICE = {"CP-ES200": 4200.00, "CP-ES300": 6150.00} # net price per pump, as in order 1000001
EVIDENCE = {
("CP-ES200", "issued"): "order 1000006: 1 pump delivered 2026-10-20, billing block AC",
("CP-ES200", "variance"): "production order 1100015 settled, 388.00 USD to price differences",
("CP-ES300", "variance"): "production order 1100016 settled, 1 pump scrapped at the pressure test",
}
# --------------------------------------------------------------- helpers
def money(x: float) -> str:
return f"{x:,.2f}"
def wd(n: int) -> str:
return f"WD{n:+d}" if n < 0 else f"WD{n}"
def check_pack() -> dict:
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"))
products = json.loads((PACK / "products.json").read_text(encoding="utf-8"))
except FileNotFoundError:
sys.exit(f"Cannot read {PACK}. Run: python unit1b/make_calder_pack.py (Unit 1B setup).")
codes = {c["CompanyCode"]: c for c in org["CompanyCodes"]}
if set(codes) != {"CP10", "CP20"} or codes["CP20"]["System"] != "ECC":
sys.exit("Company codes CP10 (S/4HANA) and CP20 (ECC) are missing from the pack. Run make_calder_pack.py again.")
ids = {bp["BusinessPartner"] for bp in partners["BusinessPartners"]}
if not {"10000010", "20000010"} <= ids:
sys.exit("Intercompany partners 10000010 and 20000010 are missing from the pack. Run make_calder_pack.py again.")
return {p["Product"]: p["to_Valuation"][0]["StandardPrice"] for p in products["Products"]}
# --------------------------------------------------------------- the close board
def schedule() -> dict:
"""Projected finish workday for each task: its planned day, pushed by open blocking
issues and by its predecessors. Tasks are listed so predecessors come first."""
blocking = {}
for iid, task, _src, _what, _owner, fix_wd, blocks in issues():
if blocks and fix_wd > blocking.get(task, (0, ""))[0]:
blocking[task] = (fix_wd, iid)
plan = {}
for tid, _name, _owner, planned, preds, status in TASKS:
if status == "done":
plan[tid] = {"finish": planned, "driver": ""}
continue
finish, driver = planned, ""
for p in preds:
if plan[p]["finish"] > finish:
finish, driver = plan[p]["finish"], plan[p]["driver"]
fix_wd, iid = blocking.get(tid, (0, ""))
if fix_wd > finish:
finish, driver = fix_wd, iid
plan[tid] = {"finish": finish, "driver": driver}
return plan
def critical_path(plan: dict) -> list:
"""Walk back from the group close along the predecessor that finishes last
(on a tie, the one with the longer chain of tasks behind it)."""
preds = {t[0]: t[4] for t in TASKS}
depth = {}
for tid, *_rest in TASKS: # predecessors come first in TASKS
depth[tid] = 1 + max((depth[p] for p in preds[tid]), default=0)
path, node = [], "G01"
while node:
path.append(node)
node = max(preds[node], key=lambda p: (plan[p]["finish"], depth[p]), default=None)
return list(reversed(path))
def risk(tid: str, plan: dict) -> str:
planned = next(t[3] for t in TASKS if t[0] == tid)
status = next(t[5] for t in TASKS if t[0] == tid)
if status == "done":
return "-"
late_rate = LATE_HISTORY.get(tid, 0) / 12
if plan[tid]["finish"] > planned:
return "LATE"
return "high" if late_rate >= 0.4 else "medium" if late_rate >= 0.2 else "low"
def print_board() -> None:
plan = schedule()
path = set(critical_path(plan))
print(f"Close board, company code CP10, period {PERIOD}, as of {wd(AS_OF_WD)} {WORKDAYS[AS_OF_WD]}"
" (fictional Calder data)\n")
print(f" {'Task':<5}{'':<2}{'Plan':<5}{'Proj':<5}{'Status':<11}{'Risk':<7}What")
for tid, name, owner, planned, _preds, status in TASKS:
mark = "*" if tid in path else " "
print(f" {tid:<5}{mark:<2}{wd(planned):<5}{wd(plan[tid]['finish']):<5}{status:<11}{risk(tid, plan):<7}{name}")
if plan[tid]["finish"] > planned:
print(f" {'':<30}late because of issue {plan[tid]['driver']}; task owner: {owner}")
print("\n * critical path: " + " -> ".join(critical_path(plan)))
local, group = plan["C16"]["finish"], plan["G01"]["finish"]
print(f" CP10 local close: target {wd(LOCAL_CLOSE_TARGET)}, projected {wd(local)}")
print(f" Group reporting: target {wd(GROUP_CLOSE_TARGET)}, projected {wd(group)}")
print("\n Open issues from upstream processes:")
for iid, task, src, what, owner, fix, blocks in issues():
kind = "BLOCKS" if blocks else "risk"
print(f" {iid} {kind:<7}{task} ({src}) {what}")
print(f" {'':<10}owner: {owner}; expected fix {wd(fix)}")
print("\n Risk: LATE = projected after plan; otherwise from the task's late rate in the last 12 closes.")
print(" Synthetic data: the board shows the method, not results from an SAP system.")
# --------------------------------------------------------------- accruals
def print_accruals() -> None:
print(f"Accruals for CP10, period {PERIOD}, key date 2026-10-31 (fictional)\n")
print(" Purchase order accruals: planned cost to the key date minus actual cost posted")
total = 0.0
for po, text, supplier, assign, planned, actual, reviewer in PO_ACCRUAL_ITEMS:
proposal = max(planned - actual, 0.0)
total += proposal
print(f" PO {po} {text}")
print(f" {supplier}; {assign}; planned {money(planned)}, actual {money(actual)}, "
f"proposal {money(proposal)}; review: {reviewer}")
print(f" proposed total {money(total)} USD. If nobody reviews it, the proposal posts as it is.\n")
print(" Manual accruals: accrual objects")
for obj, text, cc, year_total, method, posted in MANUAL_ACCRUALS:
if method.startswith("straight"):
due = round(year_total * 10 / 12, 2)
else:
due = year_total
print(f" {obj} {text}")
print(f" cost center {cc}; {method}; total {money(year_total)}, posted {money(posted)}, "
f"October posting {money(due - posted)}")
avg = sum(UTILITY_BILLS.values()) / len(UTILITY_BILLS)
print(f" utilities estimate: average of the last three bills {money(avg)}, rounded to 8,400.00")
print("\n GR/IR open items at period end (debit +, credit -)")
for po, supplier, why, amount in GRIR_OPEN:
print(f" PO {po} {supplier:<38}{why:<36}{money(amount):>10}")
net = sum(a for *_, a in GRIR_OPEN)
print(f" net GR/IR balance {money(net)}: a liability without an invoice, and an invoice without goods")
print("\n Synthetic numbers for a fictional company, not from an SAP system.")
# --------------------------------------------------------------- allocation
def print_allocation() -> None:
utilities = next(a for a in MANUAL_ACCRUALS if a[0] == "ACC-UTL-2610")[3]
lines = list(SENDER_COSTS) + [("utilities accrual ACC-UTL-2610", utilities)]
if COMPRESSOR_COST_CENTER == ALLOC_SENDER:
lines.append(("maintenance order 4000112 settled", 1240.00))
print(f"Allocation cycle CP11-OH for period {PERIOD}: sender {ALLOC_SENDER} (fictional)\n")
for text, amount in lines:
print(f" {text:<44}{money(amount):>10}")
total = round(sum(a for _, a in lines), 2)
print(f" {'sender total':<44}{money(total):>10}\n")
for receiver, share in ALLOC_RECEIVERS.items():
print(f" to {receiver:<41}{money(total * share):>10} ({share:.0%} of October kWh)")
if COMPRESSOR_COST_CENTER != ALLOC_SENDER:
print("\n Order 4000112's 1,240.00 USD is not in the cycle: the order can't settle yet.")
print(" Run the cycle now and that cost stays behind; run it after task C10 and it reaches")
print(" the production cost centers, and through their activity rates, the pumps.")
print("\n Synthetic numbers for a fictional company, not from an SAP system.")
# --------------------------------------------------------------- intercompany
def match_intercompany() -> list:
"""Three rules, applied in order, like a matching method: exact, suggested, exception."""
left = [dict(i) for i in CP10_ITEMS]
right = [dict(i) for i in CP20_ITEMS]
results = []
# Rule 1: partner pair, same reference, amounts cancel within the tolerance
for a in left:
for b in right:
if b.get("used") or a.get("used"):
continue
pair = a["PartnerCompany"] == b["CompanyCode"] and b["PartnerCompany"] == a["CompanyCode"]
diff = round(a["AmountInTransactionCurrency"] + b["AmountInTransactionCurrency"], 2)
if pair and a["Reference"] == b["Reference"] and abs(diff) <= MATCH_TOLERANCE:
a["used"] = b["used"] = True
results.append(("matched (rule 1: partner, reference, amount)", a, b, diff, ""))
# Rule 3 before leaving items open: same reference and opposite amount, but a partner missing
for a in left:
for b in right:
if a.get("used") or b.get("used"):
continue
diff = round(a["AmountInTransactionCurrency"] + b["AmountInTransactionCurrency"], 2)
if a["Reference"] == b["Reference"] and abs(diff) <= MATCH_TOLERANCE and not (
a["PartnerCompany"] and b["PartnerCompany"]):
a["used"] = b["used"] = True
results.append(("exception (rule 3: trading partner missing)", a, b, diff, "MISSING_TP"))
for a in left:
if not a.get("used"):
results.append(("open on CP10 only", a, None, a["AmountInTransactionCurrency"], "CHECK"))
for b in right:
if not b.get("used"):
reason = "TIMING" if "shipped" in b["Text"] else "CHECK"
results.append(("open on CP20 only", None, b, b["AmountInTransactionCurrency"], reason))
return results
def print_intercompany() -> None:
print(f"Intercompany reconciliation CP10 <-> CP20, period {PERIOD}, transaction currency USD (fictional)\n")
open_total = 0.0
for label, a, b, diff, reason in match_intercompany():
print(f" {label}")
for side in (a, b):
if side:
tp = side["PartnerCompany"] or "(none)"
print(f" {side['CompanyCode']} {side['Document']:<11}ref {side['Reference']:<9}partner {tp:<7}"
f"{money(side['AmountInTransactionCurrency']):>11} {side['Text']}")
if reason:
print(f" difference {money(diff)}; reason code {reason}")
open_total += abs(diff) if reason != "MISSING_TP" else 0
print()
print(f" Unexplained or open: {money(open_total)} USD. Reason codes are course labels.")
print(" CP20 runs ECC: its items arrive with the reporting package, so they are only as")
print(" current as the last upload.")
print("\n Synthetic numbers for a fictional company, not from an SAP system.")
# --------------------------------------------------------------- margin
def margin_rows(std: dict) -> dict:
rows = {}
for month, products in MARGIN.items():
for product, m in products.items():
revenue = m["billed"] * PRICE[product]
cogs = m["issued"] * std[product]
margin = revenue - cogs - m["variance"] - m["freight"]
rows[(month, product)] = {"revenue": revenue, "cogs": cogs, "variance": m["variance"],
"freight": m["freight"], "margin": margin,
"pct": margin / revenue if revenue else 0.0,
"billed": m["billed"], "issued": m["issued"]}
return rows
def drivers(product: str, std: dict, rows: dict) -> list:
"""Percentage-point effects on October's margin, each computed from the data."""
oct_ = rows[("2026-10", product)]
sep = rows[("2026-09", product)]
out = []
unbilled = oct_["issued"] - oct_["billed"]
if unbilled:
noun = "pump" if unbilled == 1 else "pumps"
out.append((f"{unbilled} {noun} issued, not billed: cost without revenue",
-unbilled * std[product] / oct_["revenue"], EVIDENCE.get((product, "issued"), "")))
if oct_["variance"]:
out.append(("production variance settled", -oct_["variance"] / oct_["revenue"],
EVIDENCE.get((product, "variance"), "")))
freight_change = oct_["freight"] / oct_["revenue"] - sep["freight"] / sep["revenue"]
if abs(freight_change) >= 0.0005:
out.append(("freight per revenue dollar", -freight_change, "freight orders settled in October"))
explained = sum(e for _, e, _ in out)
residual = (oct_["pct"] - sep["pct"]) - explained
out.append(("not explained by the rules above", residual, ""))
return out
def print_margin(std: dict) -> None:
rows = margin_rows(std)
print("Margin by pump model, company code CP10 (USD, fictional)\n")
print(f" {'Product':<10}{'Month':<9}{'Billed':>7}{'Revenue':>12}{'COGS':>12}{'Variance':>10}"
f"{'Freight':>10}{'Margin':>11}{'%':>7}")
for product in PRICE:
for month in MARGIN:
r = rows[(month, product)]
print(f" {product:<10}{month:<9}{r['billed']:>7}{money(r['revenue']):>12}{money(r['cogs']):>12}"
f"{money(r['variance']):>10}{money(r['freight']):>10}{money(r['margin']):>11}{r['pct']:>7.1%}")
print()
for product in PRICE:
swing = rows[("2026-10", product)]["pct"] - rows[("2026-09", product)]["pct"]
print(f" {product}: margin {swing * 100:+.1f} points vs September")
for text, effect, evidence in drivers(product, std, rows):
print(f" {effect * 100:+6.1f} {text}" + (f" [{evidence}]" if evidence else ""))
print("\n Draft commentary (built from the numbers; a controller reviews and signs it):")
for line in commentary(std, rows):
print(f" {line}")
print("\n Synthetic numbers for a fictional company, not from an SAP system.")
def commentary(std: dict, rows: dict) -> list:
lines = []
for product in PRICE:
o, s = rows[("2026-10", product)], rows[("2026-09", product)]
main = max(drivers(product, std, rows)[:-1], key=lambda d: abs(d[1]))
lines.append(f"{product} margin {o['pct']:.1%} in October vs {s['pct']:.1%} in September; "
f"largest driver: {main[0]} ({main[1] * 100:+.1f} pts, {main[2]}).")
return lines
# --------------------------------------------------------------- report
def write_report(std: dict) -> Path:
plan = schedule()
out = HERE / "calder_close_report.md"
lines = [f"# Close report, CP10, period {PERIOD}, {wd(AS_OF_WD)}", "",
"Fictional Calder Pumpworks data. Synthetic: it shows the method, not results from an SAP system.", "",
"```mermaid", "flowchart LR"]
for tid, _name, _owner, _planned, preds, _status in TASKS:
for p in preds:
lines.append(f" {p} --> {tid}")
lines += ["```", "", "| Task | Name | Plan | Projected | Status | Risk |", "| --- | --- | --- | --- | --- | --- |"]
for tid, name, _owner, planned, _preds, status in TASKS:
lines.append(f"| {tid} | {name} | {wd(planned)} | {wd(plan[tid]['finish'])} | {status} | {risk(tid, plan)} |")
lines += ["", "## Open issues", "", "| Issue | Task | Source | What | Owner | Fix |", "| --- | --- | --- | --- | --- | --- |"]
for iid, task, src, what, owner, fix, _b in issues():
lines.append(f"| {iid} | {task} | {src} | {what} | {owner} | {wd(fix)} |")
lines += ["", "## Draft margin commentary", ""] + [f"- {c}" for c in commentary(std, margin_rows(std))]
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_JOURNALENTRYITEMBASIC_SRV/A_JournalEntryItemBasic")
query = urllib.parse.urlencode({"$top": "100", "$format": "json",
"$select": "CompanyCode,FiscalYearPeriod,GLAccount,GLAccountName,"
"PartnerCompany,AmountInCompanyCodeCurrency,CompanyCodeCurrency"})
req = urllib.request.Request(f"{base}?{query}", headers={"APIKey": key, "Accept": "application/json"})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as err:
sys.exit(f"Sandbox answered HTTP {err.code}. Check the key, or try again later.")
except (urllib.error.URLError, TimeoutError) as err:
sys.exit(f"Cannot reach the sandbox ({err}). Check the network or proxy.")
items = data.get("d", {}).get("results", [])
by_account, partner_lines = {}, 0
for it in items:
acct = (it.get("GLAccount", ""), it.get("GLAccountName", ""), it.get("CompanyCodeCurrency", ""))
by_account[acct] = by_account.get(acct, 0.0) + float(it.get("AmountInCompanyCodeCurrency") or 0)
partner_lines += 1 if it.get("PartnerCompany") else 0
print(f"SAP sandbox: {len(items)} journal entry items read (SAP demo data, not Calder's)\n")
for (acct, name, cur), amount in sorted(by_account.items(), key=lambda x: -abs(x[1]))[:8]:
print(f" {acct:<12}{name[:30]:<32}{money(amount):>16} {cur}")
print(f"\n lines with a trading partner: {partner_lines} of {len(items)}")
print(" The sample is the first 100 items, not a balance: don't read it as a trial balance.")
# --------------------------------------------------------------- main
def main() -> None:
parser = argparse.ArgumentParser(description="Record to report at Calder (fictional data).")
parser.add_argument("--accruals", action="store_true", help="purchase order and manual accruals, GR/IR review")
parser.add_argument("--allocation", action="store_true", help="the utilities allocation cycle")
parser.add_argument("--intercompany", action="store_true", help="match CP10 against CP20")
parser.add_argument("--margin", action="store_true", help="margin by pump model with drivers")
parser.add_argument("--write", action="store_true", help="write unit1b/calder_close_report.md")
parser.add_argument("--sandbox", action="store_true", help="sum journal entry items from SAP's sandbox")
args = parser.parse_args()
if args.sandbox:
sandbox()
return
std = check_pack()
if args.accruals:
print_accruals()
elif args.allocation:
print_allocation()
elif args.intercompany:
print_intercompany()
elif args.margin:
print_margin(std)
else:
print_board()
if args.write:
print(f"\nwrote unit1b/{write_report(std).name}")
if __name__ == "__main__":
main()
You should see exactly this. The script has no random parts, so your output must match:
Close board, company code CP10, period 2026/010, as of WD2 2026-11-03 (fictional Calder data)
Task Plan Proj Status Risk What
C01 WD-1 WD-1 done - Open November; restrict October to finance
C02 * WD-1 WD-1 done - Logistics cut-off: last October goods movements
C03 WD1 WD1 done - Billing due list for October deliveries
C04 WD1 WD1 done - Freight settlement and carrier invoices
C05 WD1 WD1 done - GR/IR review
C06 WD2 WD2 in process medium Purchase order accruals: propose, review, post
C07 WD2 WD2 open low Manual accruals: audit fee, utilities
C08 WD2 WD2 done - Production orders: WIP and variances
C09 WD2 WD2 open low Production order settlement
C10 * WD2 WD4 open LATE Maintenance order settlement
late because of issue I1; task owner: Cost accountant
C11 * WD2 WD4 open LATE Overhead allocation cycle CP11-OH
late because of issue I1; task owner: Cost accountant
C12 WD2 WD2 open low Foreign currency valuation
X01 WD3 WD3 open medium CP20 (ECC) local close and reporting package
C13 WD3 WD3 open high Intercompany reconciliation CP10-CP20
C14 * WD3 WD4 open LATE Margin analysis review and commentary
late because of issue I1; task owner: Controller
C15 WD3 WD3 open low Balance sheet reconciliations and sign-off
C16 * WD3 WD4 open LATE Close October for all postings
late because of issue I1; task owner: G/L accountant
G01 * WD4 WD4 open low Group reporting: release, upload CP20, translate, eliminate
* critical path: C02 -> C10 -> C11 -> C14 -> C16 -> G01
CP10 local close: target WD3, projected WD4
Group reporting: target WD5, projected WD4
Open issues from upstream processes:
I1 BLOCKS C10 (maintain to settle) order 4000112 (1,240.00 USD) can't settle: equipment 10000104 has no cost center
owner: Master data, with the cost accountant; expected fix WD4
I2 risk C06 (procure to pay) service entry sheet 1000000001 (crane inspection) not approved; the accrual proposal awaits review
owner: Requester (PO owner); expected fix WD2
I3 risk C14 (order to cash) order 1000006 delivered on 2026-10-20 but billing block AC: cost in October, revenue later
owner: Billing clerk, with sales; expected fix WD4
I4 risk C04 (warehouse and transport) carrier invoice LFL-77203 duplicates LFL-77188; LFL-77120 billed fuel at 22%
owner: Freight settlement clerk; expected fix WD3
I5 BLOCKS C13 (intercompany (CP20 on ECC)) CP20 booked CP10's IT service recharge without a trading partner
owner: Augsburg finance; expected fix WD3
Risk: LATE = projected after plan; otherwise from the task's late rate in the last 12 closes.
Synthetic data: the board shows the method, not results from an SAP system.
How to read it:
Each task has a planned and a projected workday. The projection starts from the plan and is pushed later by a blocking issue on the task, or by a predecessor that finishes late.
Issue I1 moves the close. Order 4000112 can't settle until master data gives the compressor a cost center, expected on WD4. Settlement (C10) moves to WD4, so the allocation (C11) that needs its cost moves too, then the margin review (C14) that needs the allocation, then the close itself (C16). One field in a maintenance record decides CP10's close day.
Issue I5 blocks but doesn't delay. CP20 can repost its IT charge with a trading partner on WD3, the day the intercompany reconciliation is planned anyway.
The other issues are risks, not blockers. The accrual for the crane inspection posts as proposed if nobody reviews it (Step 5 explains why). The billing block on order 1000006 and the carrier invoices don't stop any task, but they change what the margin review has to explain.
The critical path walks back from group reporting along the predecessor that finishes last: C02 -> C10 -> C11 -> C14 -> C16 -> G01. That is where a controller spends Tuesday morning.
Risk for tasks that aren't late comes from how often each one finished late in the last twelve closes: 40% or more is high, 20% or more medium. Intercompany reconciliation was late seven times in twelve. This is the simplest possible risk model; Step 8 explains what a better one would need.
Accruals for CP10, period 2026/010, key date 2026-10-31 (fictional)
Purchase order accruals: planned cost to the key date minus actual cost posted
PO 4500000007 Annual inspection, overhead crane
20000004 Corvane Lifting Services; cost center CP11-410; planned 3,800.00, actual 0.00, proposal 3,800.00; review: PO owner (requester)
PO 4500000002 20 x CP-MTR15 motors, due 2026-11-05
20000001 Orrin Motor Works; stock (GR-based); planned 0.00, actual 0.00, proposal 0.00; review: none needed
PO 4500000006 5 x CP-MTR15 motors, received 2026-09-18
20000001 Orrin Motor Works; stock (GR-based); planned 4,525.00, actual 4,525.00, proposal 0.00; review: none needed
proposed total 3,800.00 USD. If nobody reviews it, the proposal posts as it is.
Manual accruals: accrual objects
ACC-AUD-2026 Annual audit fee 2026
cost center CP10-900; straight line Jan-Dec; total 36,000.00, posted 27,000.00, October posting 3,000.00
ACC-UTL-2610 Rockford electricity and gas, October (bill due mid-November)
cost center CP11-460; one period, estimate; total 8,400.00, posted 0.00, October posting 8,400.00
utilities estimate: average of the last three bills 8,400.00, rounded to 8,400.00
GR/IR open items at period end (debit +, credit -)
PO 4500000006 20000001 Orrin Motor Works received 2026-09-18, not invoiced -4,525.00
PO 4500000005 20000010 Calder GmbH (intercompany) 40 invoiced, 36 received 1,340.00
net GR/IR balance -3,185.00: a liability without an invoice, and an invoice without goods
Synthetic numbers for a fictional company, not from an SAP system.
How to read it:
Purchase order accruals follow SAP's rule: planned cost to the key date minus actual cost posted. The crane inspection was planned for 13 October and nothing has posted, so 3,800.00 USD is proposed. The September motors are fully received, so their GR/IR credit already carries the liability and no accrual is needed. The November motors aren't due yet.
Review is optional. In SAP's process, a proposal that nobody reviews posts as proposed; only an approval step holds it back. That is why issue I2 is a risk, not a blocker. It is also why the PO owner should look: if the inspection was cancelled, the accrual is wrong.
Manual accruals post what is due minus what is posted. The audit fee is straight-line over twelve months, so 3,000.00 USD falls in October. The utilities estimate is the average of the last three bills, which is exactly the kind of estimate an accruals agent would propose from history, with its basis shown.
GR/IR has two open items that point in opposite directions. The net balance, 3,185.00 USD credit, hides both: always review GR/IR by item, never by balance.
#Step 6: Run the allocation with and without the stuck order
Run:
Windows (PowerShell):
python unit1b\record_to_report.py --allocation
macOS / Linux:
python unit1b/record_to_report.py --allocation
You should see:
Allocation cycle CP11-OH for period 2026/010: sender CP11-460 (fictional)
depreciation, compressor asset 30000021 600.00
utilities accrual ACC-UTL-2610 8,400.00
sender total 9,000.00
to CP11-410 (ASSY1) 5,400.00 (60% of October kWh)
to CP11-420 (TEST1) 3,600.00 (40% of October kWh)
Order 4000112's 1,240.00 USD is not in the cycle: the order can't settle yet.
Run the cycle now and that cost stays behind; run it after task C10 and it reaches
the production cost centers, and through their activity rates, the pumps.
Synthetic numbers for a fictional company, not from an SAP system.
The cycle shares the utilities cost center by energy use: 60% to assembly, 40% to test. Without the compressor repair, it sends 9,000.00 USD. With it, it would send 10,240.00 USD. If a cost accountant runs the cycle on WD2 to stay on plan, the 1,240.00 USD stays on the maintenance order, then on the utilities cost center after a late settlement, and reaches the products a month late. Waiting for task C10 costs a day; not waiting costs accuracy. The exercise shows the numbers after the fix.
Intercompany reconciliation CP10 <-> CP20, period 2026/010, transaction currency USD (fictional)
matched (rule 1: partner, reference, amount)
CP10 5105600004 ref 95000041 partner CP20 -13,400.00 40 x CP-IMP200 for PO 4500000005
CP20 95000041 ref 95000041 partner CP10 13,400.00 40 x CP-IMP200, shipped 2026-10-07
exception (rule 3: trading partner missing)
CP10 90000051 ref 90000051 partner CP20 2,500.00 IT service recharge October
CP20 1900000388 ref 90000051 partner (none) -2,500.00 IT service Calder Inc. October
difference 0.00; reason code MISSING_TP
open on CP20 only
CP20 95000047 ref 95000047 partner CP10 13,400.00 40 x CP-IMP200 for PO 4500000008, shipped 2026-10-30
difference 13,400.00; reason code TIMING
Unexplained or open: 13,400.00 USD. Reason codes are course labels.
CP20 runs ECC: its items arrive with the reporting package, so they are only as
current as the last upload.
Synthetic numbers for a fictional company, not from an SAP system.
How to read it:
The script applies rules in order, like an ICMR matching method. Rule 1 needs the trading partner pair, the same reference and amounts that cancel within 10.00 USD. What doesn't match moves on.
The impeller invoice for PO 4500000005 matches, even though CP10 has only received 36 of the 40 impellers. Matching compares the two companies' books; the missing four are a GR/IR question inside CP10 (Step 5), not an intercompany difference.
The IT recharge matches by reference and amount, but CP20 has no trading partner on it. The script flags it as an exception rather than a match: until CP20 reposts it, group reporting can't eliminate it, because it looks like a payable to an outside supplier.
The second impeller invoice is on CP20's side only. It was shipped on 30 October and CP10 hasn't received goods or invoice. That is a timing difference: real, explainable and recorded with a reason code, not chased as an error.
Matching in transaction currency (USD, the invoice currency) keeps exchange rates out of the comparison. In group currency, CP20's EUR books translated at period-end rates would add small differences that need their own rule.
#Step 8: Explain the margin swings and write the close report
Run:
Windows (PowerShell):
python unit1b\record_to_report.py --margin
macOS / Linux:
python unit1b/record_to_report.py --margin
You should see:
Margin by pump model, company code CP10 (USD, fictional)
Product Month Billed Revenue COGS Variance Freight Margin %
CP-ES200 2026-09 14 58,800.00 39,900.00 0.00 1,800.00 17,100.00 29.1%
CP-ES200 2026-10 16 67,200.00 48,450.00 388.00 2,100.00 16,262.00 24.2%
CP-ES300 2026-09 4 24,600.00 16,400.00 0.00 1,000.00 7,200.00 29.3%
CP-ES300 2026-10 5 30,750.00 20,500.00 4,280.00 1,320.00 4,650.00 15.1%
CP-ES200: margin -4.9 points vs September
-4.2 1 pump issued, not billed: cost without revenue [order 1000006: 1 pump delivered 2026-10-20, billing block AC]
-0.6 production variance settled [production order 1100015 settled, 388.00 USD to price differences]
-0.1 freight per revenue dollar [freight orders settled in October]
+0.0 not explained by the rules above
CP-ES300: margin -14.1 points vs September
-13.9 production variance settled [production order 1100016 settled, 1 pump scrapped at the pressure test]
-0.2 freight per revenue dollar [freight orders settled in October]
+0.0 not explained by the rules above
Draft commentary (built from the numbers; a controller reviews and signs it):
CP-ES200 margin 24.2% in October vs 29.1% in September; largest driver: 1 pump issued, not billed: cost without revenue (-4.2 pts, order 1000006: 1 pump delivered 2026-10-20, billing block AC).
CP-ES300 margin 15.1% in October vs 29.3% in September; largest driver: production variance settled (-13.9 pts, production order 1100016 settled, 1 pump scrapped at the pressure test).
Synthetic numbers for a fictional company, not from an SAP system.
How to read it:
Every driver is computed, not guessed. One ES-200 was issued and not billed, so October carries its 2,850.00 USD cost without its revenue: 4.2 points of margin. The scrapped ES-300 on order 1100016 costs 13.9 points. Freight per revenue dollar barely moved.
The residual line is what the rules don't explain. Here it is close to zero, so the commentary can be signed. A large residual is the signal to dig, not to write around it.
The draft commentary is one sentence per product, built from the driver table, with the evidence attached. A language model can make it read better; it must not change the numbers or invent a cause. That is the pattern of the portfolio project finance variance assistant: compute in code, cite the postings, let the model write words.
A better risk model for the close board would learn from task history, upstream open items and volumes, as in classification and the metrics that matter. The late-count rule is the baseline it has to beat.
Write the close report:
Windows (PowerShell):
python unit1b\record_to_report.py --write
macOS / Linux:
python unit1b/record_to_report.py --write
The last line should read wrote unit1b/calder_close_report.md. Open the file in VS Code and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS) to preview it: the task graph as a diagram, the task table, the open issues and the draft commentary.
#Step 9 (optional): Read journal entry items from SAP's sandbox
This step reads SAP's demo data, not Calder's. It needs your sandbox key and the network.
Make sure .env in the course folder has a line SAP_API_KEY="...".
Run:
Windows (PowerShell):
python unit1b\record_to_report.py --sandbox
macOS / Linux:
python unit1b/record_to_report.py --sandbox
The output has this shape. Values in angle brackets come from SAP's demo data and change over time:
SAP sandbox: <n> journal entry items read (SAP demo data, not Calder's)
<G/L account> <account name> <amount> <currency>
lines with a trading partner: <count> of <n>
The sample is the first 100 items, not a balance: don't read it as a trial balance.
The script reads up to 100 items from API_JOURNALENTRYITEMBASIC_SRV with $select and sums them by G/L account. A valid "empty" result is 0 journal entry items read. The sandbox is read-only. The sum of 100 items is not a balance: a real trial balance reads a whole period, filtered by ledger and company code.
Where it sits. At Calder (fictional), CP10's ledger, controlling, accruals, allocations and margin analysis run in the CP10 S/4HANA system. CP20 closes its own books on ECC and sends a reporting package. Group reporting consolidates both. A close orchestration tool holds the plan and starts jobs in both systems. Anything that predicts, matches or drafts reads released interfaces and writes proposals; accountants post and sign in SAP's own apps.
flowchart LR
S4[S/4HANA CP10<br/>ledger, CO, margin,<br/>accruals] -->|release| GR[Group reporting]
ECC[ECC CP20<br/>local close] -->|upload<br/>package| GR
OR[Close orchestration<br/>task lists] -->|start jobs,<br/>status| S4
OR -->|start jobs,<br/>status| ECC
S4 -->|released APIs,<br/>CDS| AI[Close assistant<br/>read-only]
AI --> P[Proposals:<br/>accruals, matches,<br/>commentary]
P -->|accountant posts| S4
GR --> DW[Analytics and<br/>data warehouse]
Non-functional requirements that decide the design
Requirement
Typical target
If missed
Close duration
Local close by a fixed workday, group close a day or two later
Decisions on old numbers; overtime every month
Cut-off discipline
Every upstream task due before the close starts has an owner and a deadline
Settlement and allocation run on incomplete data
Auditability
Every accrual, adjustment and approval traceable to its basis and approver
Audit findings; restatements
Segregation of duties
The person who proposes an accrual or adjustment isn't the one who approves it
Control failures
Data volume and latency
Close reports read a period's journal in minutes, not overnight
Reviews squeezed into the last hours
Availability at peak
Systems and jobs sized for close days, when everyone runs reports at once
Jobs fail on WD2 and the plan slips
Integration and identity. SAP Advanced Financial Closing runs in an SAP BTP subaccount, uses OData services to reach S/4HANA systems and a REST service for SAP ERP, doesn't replicate financial data, and needs an identity provider (SAP Learning). Group reporting takes integrated S/4HANA data by release and other companies' data by upload or data collection (SAP Learning). A close assistant should read journal entries with the accountant's own identity, so company code and authorization limits apply (grounding with authorizations). Integration patterns between systems are covered in how SAP systems talk.
Context: Calder's close spans an S/4HANA company and an ECC company. Today it runs on a spreadsheet of tasks, and the local close slips most months because of upstream problems found on WD2. CP20 will stay on ECC for at least another year.
Options: (a) keep the spreadsheet and add a weekly pre-close review; (b) use the Financial Closing cockpit in each system separately; (c) one close plan in a BTP-based orchestration tool connected to both systems, with pre-close checks for the known upstream issues.
Decision: (c), with the pre-close checks from (a). One plan shows the cross-company dependencies, such as CP20's package feeding the intercompany reconciliation, and the pre-close checks catch missing cost centers and unapproved services before WD1.
Consequences: A separate subscription and a BTP subaccount to run. Task owners in two companies maintain one template. Pre-close checks need read access to logistics data, owned by finance but agreed with each process owner. Any AI that predicts task risk or proposes accruals reads from the same plan and never posts by itself.
What a design review will ask
Which upstream conditions are checked before the close starts, and who owns each one?
Where does each company's data enter group reporting, and how current is it on group close day?
Who can post accruals and adjustments, who approves them, and is the split enforced in SAP roles?
Which reports read the Universal Journal directly and which read a data warehouse, and how are they reconciled?
Can any automated component post, adjust or close a period without a person?
Predict which tasks will be late from history, upstream open items and volumes; beat the late-count baseline from Step 4 first
Pre-close checks
Plain automation is enough
Weak for AI
Missing cost centers, unapproved services and billing blocks are queries, not predictions
Accruals
Predict and extract
Strong
Propose amounts from history, contracts and policy documents, with the basis shown; SAP lists an Accounting Accruals Agent for this (SAP). The accountant confirms
GR/IR clarification
Predict (classify) and retrieve
Strong
Suggest root cause and next step per item; SAP offers machine-learning proposals with a separate subscription (SAP Learning)
Settlement and allocation
Plain automation is enough
Weak for AI
Rules on master data; AI can flag what will fail, not choose receivers
Intercompany matching
Match and explain
Strong
Rules first, then suggestions for what rules miss, with reason codes; SAP names an Intercompany Matching and Reconciliation Agent (SAP)
A person approves every posting and closes the period
The line is the same at every step: AI predicts, proposes, matches and drafts; accountants approve postings, controllers sign commentary and the period is closed by a person. Human approval in AI workflows and agent permissions show how to enforce that in code.
Treating the close as a checklist. Write down the dependencies. A list hides that allocation needs settlement first.
Finding upstream problems on WD2. Run pre-close checks in the last week of the month: settlement receivers, unapproved services, billing blocks, open goods movements.
Reviewing GR/IR by balance. Opposite items cancel out. Review by item and age.
Accruals without a basis. Every estimate needs a source and a reviewer, or it becomes a plug.
Intercompany postings without trading partners. They can't be matched or eliminated. Make the trading partner come from master data on intercompany customers and suppliers, not from memory.
Explaining margin from memory. Compute drivers from postings and keep a residual line; a commentary that explains more than the change is wrong.
Letting an agent post at period end. Proposals are cheap to review and expensive to reverse once figures are reported.
Assuming CP20 works like CP10. ECC has no Universal Journal, and its data reaches group reporting by upload; plan the group calendar around that.
Turn the close board into a controller's action note and test two fixes yourself.
Run python unit1b/record_to_report.py --write (Windows: python unit1b\record_to_report.py --write). Copy unit1b/calder_close_report.md to unit1b/calder_close_actions.md.
In the copy, add a column Action to the open issues table. For each issue write one sentence: what the owner should do today.
Add a column AI could with one of: predict, propose, match, explain or nothing. Use the table under Where AI fits to choose.
In record_to_report.py, change COMPRESSOR_COST_CENTER = "" to COMPRESSOR_COST_CENTER = "CP11-460". This is master data giving the compressor the utilities cost center (a course value).
In CP20_ITEMS, find the item with reference 90000051 and change its "PartnerCompany": "" to "PartnerCompany": "CP10". This is CP20 reposting the IT charge with its trading partner.
Run the script without options, then with --allocation and --intercompany. Note the projected local close, the critical path, the allocation total and how the IT charge is now matched.
Change both edits back.
In the action note, add a heading Fixes tested with three lines: what the cost center changed, what the trading partner changed, and which task now paces the close.
Add a heading ECC note with one line on what CP20 must deliver, and by when, for the group close on WD4. Preview the file and commit it with Git.
Done when:unit1b/calder_close_actions.md has the five-line issue table with Action and AI could filled in for every line, a Fixes tested section that says the local close is projected on WD3 again, the critical path becomes X01 -> C13 -> C15 -> C16 -> G01 (CP20's package now paces the close), the allocation sends 10,240.00 USD (6,144.00 to CP11-410, 4,096.00 to CP11-420) and the IT charge is matched by rule 1, 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 finance part.
Pick one answer for each question. The explanation appears after you choose.
1The close board projects settlement on WD4 because order 4000112 can't settle. Why does the margin review move too?
Answer: A. Delays travel along predecessors. The allocation needs the repair cost on the utilities cost center, and the margin review needs the allocation, so one blocked settlement moves both.
2In SAP's purchase order accruals, nobody reviews the 3,800.00 USD proposal for the crane inspection. What happens?
Answer: D. Review is optional, and an unreviewed proposal posts as proposed. Only an approval step holds an amount back, which is why the PO owner should still check it.
3Calder's GR/IR net balance is 3,185.00 USD credit. Why is that number alone not enough?
Answer: C. Five motors received and not invoiced and four impellers invoiced and not received point opposite ways. Each needs its own owner; the net hides both.
4An intercompany pair matches on reference and amount, but CP20's side has no trading partner. Why flag it instead of accepting it?
Answer: D. Elimination relies on the trading partner to know that both sides belong to the group. Until CP20 reposts it, the payable sits in group figures as a debt to a stranger.
5Why does the script match intercompany items in transaction currency rather than in group currency?
Answer: C. Both invoices are in USD, so they agree to the cent there. In group currency, CP20's EUR books translated at different rates would add differences that need their own rule.
6ES-200 margin fell 4.2 points because one pump was delivered with a billing block. What is happening in the books?
Answer: B. Cost of goods sold posts at goods issue and revenue at billing. A delivery that can't be billed at month end puts cost in one month and revenue in the next.
7You add a language model to write the variance commentary. Which design keeps it trustworthy?
Answer: C. The numbers come from postings and code; the model only phrases them, and a check confirms every number in the text matches the driver table.
8A team wants an agent to post accruals, reconcile and close the period overnight on WD1. What do you design instead?
Answer: A. Postings and closing the period change reported figures. The agent prepares and explains; people approve each posting and close the period, with a record.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
Procure to pay end to endUnit 1B · SAP end to endLinked from this topic · Shares: GR/IR clearing account
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 11 of 11Pay and close the monthThe whole stage practices this topic, on Posting periods, Journal entry and Reports.
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.
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.
Managing the Financial Closing Cockpit (SAP Learning)— templates from the closing hierarchy; periodic task lists with key date, fiscal year and period; task types; predecessor relationships on the Dependencies tab
AI Assistant for Financial Closing (SAP)— Financial Closing Assistant orchestrates close agents; named agents include Accounting Accruals and Intercompany Matching and Reconciliation; no dates on the page
Accounting Accruals Agent (SAP)— proposes accrual entries from history and policy documents for accountants to confirm; SAP S/4HANA Cloud Public Edition; AI Units required; beta registration offered
Cloudification Repository (SAP on GitHub)— objectReleaseInfoLatest.json read 24 October 2026; I_JournalEntry, I_JournalEntryItem, I_GLAccountLineItem, I_ProfitabilitySegment, I_PartnerCompany, I_CnsldtnGroupJrnlEntryTP, I_MnlAccrsAccrualSubobject released; ACDOCA, BSEG, BKPF not to be released
Journal Entry Item Basic service model (SAP Cloud SDK, npm)— API_JOURNALENTRYITEMBASIC_SRV, entity A_JournalEntryItemBasic with Ledger, CompanyCode, GLAccount, PartnerCompany, FiscalYearPeriod, SoldMaterial and amount fields; published 2022