Hire-to-retire: where people data meets logistics and finance
See where employee data crosses into S/4HANA (cost centers, time, expenses, payroll), what breaks at each crossing, and why HR data stays out of AI grounding.
Hire to retire is everything a company does with its people: hiring, moving, paying, recording time, reimbursing travel, and handling someone's exit. Most of it runs in an HR system. This topic is not about HR. It is about the places where people data crosses into the ERP, because that is where logistics and finance depend on it.
There are five crossings to know:
Cost centers go out to HR. Finance owns cost centers. HR assigns each person to one.
People come back from HR. S/4HANA needs a record of each employee, so a technician can confirm hours on an order and a buyer can be reimbursed.
Time goes to orders. A technician's hours become cost on a maintenance order.
Expenses go to finance. A travel claim becomes a posting and a payment to the employee.
Payroll goes to the ledger. Each pay run sends a posting of wages and liabilities, usually summed by cost center.
Three ideas carry this topic:
One stale record breaks four processes. If HR assigns people to a cost center finance has closed, payroll, time, expenses and the employee record all fail at once.
Each crossing should carry the minimum. Finance needs wages by cost center, not each person's salary. A scheduler needs "available or not", not why someone is away.
People data is the most sensitive data in the landscape. That is why it is usually kept out of AI grounding, and why a leaver's access must end on their last day.
When a crossing fails, the symptom shows up somewhere else: an order that looks cheap, a close that is missing a month of wages, a new hire who can't be paid back for safety boots.
Here is Monday 2 November 2026, the first working day of the October close at Calder Pumpworks, the fictional pump maker this unit follows. Calder's US company (CP10) runs S/4HANA; in this course its cloud HR system is SAP SuccessFactors Employee Central. A check of the people-data crossings finds this:
What the check shows
What it means
Who acts
Cost center CP11-435 (rework area) closed on 30 September in S/4HANA, still open in HR
The cost center feed to HR stopped on 14 September
HR integration team
A maintenance technician hired on 19 October isn't in S/4HANA
HR gave him the old rework position, on the closed cost center; S/4HANA rejected the record
HR, with the cost accountant
October payroll posting is held: 7,350.00 USD on the closed cost center
In this scenario the whole document waits, so October wages are missing from CP10's books
Payroll, with the G/L accountant
14 hours of the new technician's work can't reach order 4000126
The crane brake rework looks 1,330.00 USD cheaper than it was
Maintenance supervisor
His expense report for safety boots can't post
No employee record to pay, and a closed cost center
Travel and expense admin
A technician who left on 16 October still has an active S/4HANA user
Access outlived the job
Security administrator
The business decision is who owns each crossing, and what it is allowed to carry. Five of the six findings above come from one feed nobody watched.
SAP's HR products and S/4HANA connect through standard integrations. As of October 2026, SAP's documentation describes these:
SAP SuccessFactors Employee Central with S/4HANA Cloud Public Edition. SAP's training lists scope items for workforce replication through SAP Master Data Integration, a service on SAP BTP, for financial master data such as cost centers going to SuccessFactors, and for payroll with Employee Central Payroll. HR data is then edited in Employee Central, not in S/4HANA's Manage Workforce app (SAP Learning). That page is from the public sector training; check the scope items for your edition.
Employee Central with SAP HCM in S/4HANA (on-premise or private cloud). Employee and organizational data replicate from Employee Central into the HR tables of S/4HANA, including the employee's cost center (SAP Learning).
On-premise HR in S/4HANA. SAP's compatibility packs for HCM ended with 2025. SAP says that from 2026, running HCM on-premise needs SAP HCM for SAP S/4HANA licensed and activated, while employee data used only by non-HR processes doesn't (SAP, 2024). Check your contract with SAP.
Travel and expenses with SAP Concur. SAP describes SAP Integration with Concur Solutions (SAP ICS) as its native framework linking SAP finance systems with Concur, for both S/4HANA Cloud and on-premise (SAP Learning).
Payroll from any provider. S/4HANA Cloud can send cost centers and company codes to a third-party payroll system and take its results as journal entries (SAP guide, 2019).
In every option, the HR system owns people and S/4HANA owns cost centers. Each side sends the other only what it needs.
"HR is a separate world." Payroll is often one of the largest postings of the month, and maintenance cost depends on hours booked by people.
"Finance needs each person's pay." It needs wages and liabilities by cost center and account. Detail by person stays in payroll unless a process requires it.
"Integration is set up once." Feeds stop, mappings drift and cost centers close. A replication monitor needs an owner, like any queue.
"A leaver is an HR event." It is also an access event. Their user, open time and open expense reports each need an action.
"HR data makes AI assistants smarter." Most use cases need a fact derived from HR, such as "available on Tuesday", not the HR record behind it.
Pick one answer for each question. The explanation appears after you choose.
1At Calder, five findings trace to one cause. What is it?
Answer: A. HR kept assigning people to a cost center finance had closed, because the feed stopped in September. Payroll, the employee record, time and expenses then all failed on the same cost center.
2What should payroll usually send to finance for each pay run?
Answer: C. Finance needs cost by cost center and the liabilities to pay. Individual pay stays in payroll, which keeps the most sensitive data in the fewest places.
3Order 4000126 looks 1,330.00 USD cheaper than the work it took. Why?
Answer: A. A confirmation needs a valid personnel number. Until the employee record replicates, his hours wait outside the order, so the order's cost is understated.
4A technician left on 16 October. Which follow-up is most urgent on the S/4HANA side?
Answer: B. An active user after the last day is a security risk. His record must stay for retention and his legitimate expenses must still be paid; deleting or blocking them would be wrong.
5A vendor's scheduling assistant asks to read the HR record of every technician. What do you offer instead?
Answer: C. Scheduling needs who can work when and on what, not why someone is away. Deriving that small fact keeps health, pay and family data out of the tool.
6Calder plans to keep HR on-premise in its S/4HANA system after 2026. What must it check with SAP?
Answer: B. SAP says the HCM compatibility packs ended with 2025, and on-premise HCM then needs SAP HCM for SAP S/4HANA licensed and activated. The other options misplace where data is owned.
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 hire to retire as two systems of record facing each other, with narrow pipes between them. Finance owns the cost structure; HR owns the people. Each pipe carries a small, defined slice:
Out of S/4HANA: cost centers with validity dates.
Into S/4HANA: a person's employment, with cost center and work center.
Into S/4HANA from people's work: hours on orders, expense claims.
Into the ledger: one payroll posting document per pay run.
Two rules follow. Every pipe needs an owner and a monitor, because a stopped pipe looks like a business problem far away. And every pipe should carry the minimum: the narrower the slice, the less personal data spreads, and the fewer places a breach or a wrong AI answer can come from. This topic's script checks both at Calder.
S/4HANA doesn't keep the HR file. It keeps what its own processes need about a person, as business partners:
On-premise, from S/4HANA 2020, each employee becomes a business partner with the employee role and an employment business partner (roles BUP003 and BUP010). A supplier role, needed to pay the person, is optional. Background jobs keep them in step with the HR tables. In SAP ERP, travel used separate employee vendors, created with transaction PRAA, which is no longer valid in S/4HANA (SAP Community, blogs by SAP).
In S/4HANA Cloud Public Edition, the person is a workforce person with work agreements. They arrive through one channel at a time: the Manage Workforce app, a SOAP API, the Employee Central integration or SAP Master Data Integration. The roles include BUP003 Employee, BUP010 Employment, BBP005 Contingent Worker and BUP011 External Employment. Duplicates appear when the person ID doesn't match across channels (SAP KBA 2813743).
When Employee Central sends payment information with a pay type for expenses, S/4HANA Cloud creates the business partner with the Employee and Supplier (FLVN00) roles, so expenses can be paid. The bank must already exist in S/4HANA, or replication fails (SAP Community, 2019).
With SAP HCM in S/4HANA, the HR record itself lives in the system as infotypes: numbered record types such as 0001 Organizational Assignment, which stores the employee's cost center (SAP Learning).
Cost centers are created in S/4HANA and copied to the HR and payroll systems, where people are assigned to them. SAP's public sector training describes cost centers going from S/4HANA Cloud to SuccessFactors through SAP Master Data Integration (SAP Learning). For a third-party payroll system, SAP's guide uses the Data Replication Framework under communication scenario SAP_COM_0179 (SAP guide, 2019). The validity dates matter: a cost center closed in S/4HANA must be closed in HR too.
Hours reach orders in two ways. A technician can confirm directly on the order: the maintenance confirmation carries PersonnelNumber, ActualWorkQuantity and the order (SAP Cloud SDK model, 2022). Or the person records time in a timesheet, which is transferred later. SAP's training on the Cross-Application Time Sheet (CATS) describes time recorded once and transferred, after release and any approval, to HR, project system, plant maintenance, customer service, controlling or purchasing. A confirmation creates an activity allocation: the order is debited and the work center's cost center credited. CATS can't confirm production orders; hours on one become an activity allocation without a confirmation (SAP Learning).
At Calder (fictional), technicians on work centers MECH1 (95.00 USD/h) and ELEC1 (105.00 USD/h) confirm hours on maintenance orders. The new technician's 14 hours on order 4000126 wait, because his personnel number isn't in S/4HANA.
An approved expense report becomes a posting: expense accounts with cost center or project, against an amount owed to the employee, who is paid like a supplier. A 2018 SAP Community post on the S/4HANA Cloud integration with SAP Concur (scope item 1M1) describes it sending cost centers and WBS elements to Concur and posting expense reports back as journal entries (SAP Community, 2018). The old BAPI for posting employee expenses, BAPI_ACC_EMPLOYEE_EXP_POST, is marked not to be released for the cloud, with the journal entry interface as its successor (Cloudification Repository).
A pay run ends with a posting run. SAP's training for HCM payroll in S/4HANA walks through it: an optional simulation, a live posting run for a payroll area and period, release of the posting documents, then posting with transaction PCP0 (SAP Learning). With Employee Central Payroll and S/4HANA Cloud, the posting document is transferred to the general ledger and posted as an expense and liability (SAP Learning). A third-party provider posts journal entries through the SOAP journal entry service under SAP_COM_0002 (SAP guide, 2019).
flowchart LR
CC[Cost center<br/>CP11-435 closed] -.->|feed stopped| HR[HR: 2 people<br/>on CP11-435]
HR -.->|rejected| BP[No employee<br/>00100231]
BP -.-> T[14.0 h waiting<br/>order 4000126]
BP -.-> E[ER-2026-0419<br/>can't post]
HR --> PY[October payroll<br/>236,010.00]
PY -.->|held| GL[CP10 ledger]
Step
Posts to finance
Why
Cost center or employee replication
Nothing
Master data only, but every later posting depends on it
Time confirmation on a maintenance order
Order debited, work center's cost center credited (activity allocation)
Labor becomes order cost
Expense report
Expense (cost center or project) debited, amount owed to the employee credited
The company owes the employee
Reimbursement
Amount owed to the employee cleared against the bank
The employee is paid like a supplier
Payroll posting
Wages and employer taxes debited by cost center; net pay and withholdings payable credited
The month's labor cost and what the company owes
Payroll payment
Net pay payable cleared against the bank
Employees are paid
At Calder (fictional), the October payroll posting has 236,010.00 USD on each side: 219,230.00 wages and 16,780.00 employer taxes against 158,640.00 net pay, 60,590.00 withholdings and 16,780.00 employer taxes payable. 7,350.00 of the debits sit on CP11-435. You compute this in Step 5.
As of 24 October 2026, these released interfaces carry each crossing in S/4HANA Cloud. The CDS views come from SAP's Cloudification Repository (file read 24 October 2026). The timesheet API and its fields come from SAP's generated API model, published in 2022 (SAP Cloud SDK model).
Crossing
API or service
Released CDS views
What to read
Workforce
SOAP Replicate Basic Master Data for Workforce from External HR Systems (named in SAP KBA 2813743)
Note what the timesheet model offers as receivers: cost center, WBS element, internal order, purchase order. Maintenance orders get hours through their own confirmation API, which carries PersonnelNumber.
You will write one script that checks Calder's people-data crossings on 2 November 2026. It compares cost centers between S/4HANA and HR, replicates employees and finds who is rejected, checks the October payroll posting, the technicians' time and the waiting expense reports, and groups every finding by root cause. Then it shows what a scheduling assistant may see about technicians, after minimization. An optional step reads timesheet records from SAP's sandbox.
flowchart LR
P[calder_pack/<br/>company codes,<br/>controlling area] --> S[hire_to_retire.py]
SC[Scenario in the script<br/>cost centers, people,<br/>payroll, time, expenses] --> S
S --> C[Crossings check]
S --> PY[--payroll]
S --> T[--time]
S --> E[--expenses]
S --> AI[--ai minimized view]
S --> R[calder_people_crossings.md]
K[SAP sandbox<br/>optional] --> S
In VS Code, create a new file, paste the script below, and save it as hire_to_retire.py inside the unit1b folder.
Read the blocks S4_COST_CENTERS, HR_EMPLOYEES, BUSINESS_USERS, PAYROLL_BY_CC, TIME_RECORDS, EXPENSE_REPORTS and AI_FIELD_POLICY first. They are Calder's people data written as Python data, and every value is invented. Order 4000126 and the work centers come from the maintain-to-settle topic. Personnel numbers, accounts, report IDs and skill tags are the course's own, not SAP values.
Notice what isn't there: no names, no addresses, no individual pay. The payroll data arrives already summed by cost center, the way it should cross to finance.
"""Hire to retire at Calder: where people data crosses into logistics and finance.
Calder Pumpworks is a fictional company. The pack (unit1b/calder_pack/) and the
scenario below (employees, cost centers, payroll totals, time records, expense
reports, absences) are made up for the course. Field names follow SAP's released
APIs where the script mirrors one (API_MANAGE_WORKFORCE_TIMESHEET, the workforce
and business partner models), but the logic is a teaching sketch, not SAP's.
Personnel numbers, G/L accounts, cost centers, report IDs and skill tags are
course labels, not SAP values. On purpose, the script holds no names, no
addresses and no individual pay: only what each crossing needs.
Run it from your course folder:
python unit1b/hire_to_retire.py the crossings check for CP10 on 2026-11-02
python unit1b/hire_to_retire.py --payroll October payroll posting document by cost center
python unit1b/hire_to_retire.py --time technician time waiting to reach maintenance orders
python unit1b/hire_to_retire.py --expenses expense reports waiting to post
python unit1b/hire_to_retire.py --ai what a scheduling assistant may see, minimized
python unit1b/hire_to_retire.py --write write unit1b/calder_people_crossings.md
python unit1b/hire_to_retire.py --sandbox read timesheet records 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 hashlib
import hmac
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"
AS_OF = "2026-11-02" # Monday, workday 1 of the October close
PERIOD = "2026-10"
# ------------------------------------------------------------ the fixes (the exercise changes these)
# Has cost center replication from S/4HANA to the HR system run again since CP11-435 closed?
CC_REPLICATION_RUN = False
# New cost centers HR enters for people still on a closed cost center: {personnel number: cost center}
HR_FIXES = {}
# ------------------------------------------------------------ cost centers (fictional, controlling area CP00)
# S/4HANA CP10 is the system of record for cost centers. (cost center, name, valid from, valid to)
S4_COST_CENTERS = [
("CP11-300", "Rockford purchasing", "2020-01-01", "9999-12-31"),
("CP11-410", "Assembly", "2020-01-01", "9999-12-31"),
("CP11-420", "Test", "2020-01-01", "9999-12-31"),
("CP11-435", "Rework area", "2023-01-01", "2026-09-30"), # closed: rework moved into assembly
("CP11-450", "Plant maintenance", "2020-01-01", "9999-12-31"),
("CP11-460", "Utilities", "2020-01-01", "9999-12-31"),
("CP11-900", "Rockford administration", "2020-01-01", "9999-12-31"),
]
# What the HR system last received. The replication job last succeeded on 2026-09-14.
HR_CC_LAST_REPLICATED = "2026-09-14"
def hr_cost_centers() -> dict:
if CC_REPLICATION_RUN:
return {cc: (vf, vt) for cc, _, vf, vt in S4_COST_CENTERS}
return {cc: (vf, "9999-12-31" if cc == "CP11-435" else vt) for cc, _, vf, vt in S4_COST_CENTERS}
def s4_valid(cc: str, on: str) -> bool:
return any(c == cc and vf <= on <= vt for c, _, vf, vt in S4_COST_CENTERS)
# ------------------------------------------------------------ people (fictional, CP10 Rockford operations, a subset)
# The HR system (Employee Central in this course) is the system of record for people.
# (personnel number, job, work center, cost center, employment start, employment end or "")
HR_EMPLOYEES = [
("00100094", "Maintenance technician", "MECH1", "CP11-450", "2019-03-04", "2026-10-16"),
("00100117", "Maintenance technician", "MECH1", "CP11-450", "2018-06-11", ""),
("00100142", "Maintenance electrician", "ELEC1", "CP11-450", "2021-02-01", ""),
("00100153", "Maintenance planner", "", "CP11-450", "2017-09-18", ""),
("00100188", "Assembler", "ASSY1", "CP11-410", "2022-04-25", ""),
("00100196", "Test technician", "TEST1", "CP11-420", "2020-08-03", ""),
("00100205", "Rework technician", "ASSY1", "CP11-435", "2023-01-09", ""),
("00100211", "Buyer", "", "CP11-300", "2024-05-13", ""),
("00100231", "Maintenance technician", "MECH1", "CP11-435", "2026-10-19", ""), # new hire, reused rework position
]
# S/4HANA business users linked to these people: (personnel number, user valid to)
BUSINESS_USERS = [("00100094", "9999-12-31"), ("00100117", "9999-12-31"), ("00100142", "9999-12-31"),
("00100153", "9999-12-31"), ("00100211", "9999-12-31")]
def hr_people() -> list:
return [(p, job, wc, HR_FIXES.get(p, cc), start, end) for p, job, wc, cc, start, end in HR_EMPLOYEES]
def replicate() -> tuple:
"""Employee replication HR -> S/4HANA: one employee and one employment business partner per person.
In this scenario S/4HANA rejects a record whose cost center isn't valid on the employment start."""
s4, rejected = {}, {}
for p, job, wc, cc, start, end in hr_people():
existed_before = start < "2026-10-01"
check_date = start if not existed_before else "2026-10-01"
if not existed_before and not s4_valid(cc, check_date):
rejected[p] = f"cost center {cc} not valid on {check_date}"
continue
s4[p] = {"WorkCenter": wc, "CostCenter": cc, "Start": start, "End": end,
"BPRoles": ["BUP003", "BUP010"]} # employee, employment
return s4, rejected
# ------------------------------------------------------------ payroll (fictional, summarized by cost center)
# October payroll posting document from the payroll system, posting date 2026-10-30, USD.
# Lines: (G/L account, name, cost center, debit, credit). Accounts are course labels.
GL_WAGES, GL_ER_TAX = "61100000", "61200000"
PAYROLL_BY_CC = { # cost center: (wages and salaries, employer payroll taxes)
"CP11-300": (18400.00, 1410.00), "CP11-410": (96300.00, 7370.00), "CP11-420": (31200.00, 2390.00),
"CP11-435": (6830.00, 520.00), "CP11-450": (38900.00, 2980.00), "CP11-900": (27600.00, 2110.00),
}
PAYROLL_CREDITS = [("21500000", "Net pay payable", 158640.00),
("21600000", "Employee withholdings payable", 60590.00),
("21700000", "Employer payroll taxes payable", 16780.00)]
# When HR moves the people off CP11-435, the payroll system recalculates the posting.
# The payroll system's re-run moves these totals (course values). Note what they reveal (see the topic).
REPOST_IF_FIXED = {"00100205": ("CP11-410", 4610.00, 350.00), "00100231": ("CP11-450", 2220.00, 170.00)}
def payroll_lines() -> list:
by_cc = {cc: list(v) for cc, v in PAYROLL_BY_CC.items()}
people = {p: cc for p, _, _, cc, _, _ in hr_people()}
for p, (new_cc, wages, tax) in REPOST_IF_FIXED.items():
if people[p] == new_cc:
by_cc["CP11-435"][0] -= wages
by_cc["CP11-435"][1] -= tax
by_cc[new_cc][0] += wages
by_cc[new_cc][1] += tax
lines = []
for cc in sorted(by_cc):
wages, tax = (round(x, 2) for x in by_cc[cc])
if wages or tax:
lines.append((GL_WAGES, "Wages and salaries", cc, wages, 0.0))
lines.append((GL_ER_TAX, "Employer payroll taxes", cc, tax, 0.0))
lines += [(gl, name, "", 0.0, amount) for gl, name, amount in PAYROLL_CREDITS]
return lines
def check_payroll() -> dict:
lines = payroll_lines()
debit = round(sum(l[3] for l in lines), 2)
credit = round(sum(l[4] for l in lines), 2)
bad = [l for l in lines if l[2] and not s4_valid(l[2], "2026-10-30")]
return {"lines": lines, "debit": debit, "credit": credit, "bad": bad,
"held": round(sum(l[3] for l in bad), 2), "posts": debit == credit and not bad}
# ------------------------------------------------------------ time (fictional)
# Time recorded by technicians, waiting to become confirmations on maintenance orders.
# (record, personnel number, date, order, work center, hours). MECH1 95.00 and ELEC1 105.00 USD/h.
RATES = {"MECH1": 95.00, "ELEC1": 105.00}
TIME_RECORDS = [
("T-5101", "00100117", "2026-10-26", "4000126", "MECH1", 6.0),
("T-5102", "00100142", "2026-10-26", "4000126", "ELEC1", 3.5),
("T-5103", "00100231", "2026-10-27", "4000126", "MECH1", 8.0),
("T-5104", "00100231", "2026-10-28", "4000126", "MECH1", 6.0),
("T-5105", "00100094", "2026-10-20", "4000126", "MECH1", 4.0),
]
def check_time(s4: dict) -> list:
out = []
for rec, p, day, order, wc, hours in TIME_RECORDS:
cost = hours * RATES[wc]
if p not in s4:
out.append((rec, p, day, order, hours, cost, "blocked", "personnel number not in S/4HANA"))
elif s4[p]["End"] and day > s4[p]["End"]:
out.append((rec, p, day, order, hours, cost, "check", f"date after employment end {s4[p]['End']}"))
else:
out.append((rec, p, day, order, hours, cost, "confirmed", "activity allocation to the order"))
return out
# ------------------------------------------------------------ travel and expenses (fictional)
# Expense reports approved in the expense system, waiting to post to CP10. (report, personnel number, purpose, cost center, USD)
EXPENSE_REPORTS = [
("ER-2026-0412", "00100117", "Crane inspection training, Chicago", "CP11-450", 642.18),
("ER-2026-0415", "00100094", "Seal supplier visit, 28 Sep - 2 Oct", "CP11-450", 2318.40),
("ER-2026-0419", "00100231", "Safety boots and induction travel", "CP11-435", 486.75),
]
def check_expenses(s4: dict) -> list:
out = []
people = {p: cc for p, _, _, cc, _, _ in hr_people()}
for rep, p, purpose, cc, amount in EXPENSE_REPORTS:
if p == "00100231" and p in HR_FIXES:
cc = people[p] # the expense system takes the default cost center from HR
problems = []
if p not in s4:
problems.append("no employee business partner or supplier role for reimbursement")
if not s4_valid(cc, AS_OF):
problems.append(f"cost center {cc} closed")
note = "; ".join(problems) if problems else (
"employment ended: keep the supplier role payable until reimbursed"
if p in s4 and s4[p]["End"] else "")
out.append((rep, p, purpose, cc, amount, "blocked" if problems else "posts", note))
return out
# ------------------------------------------------------------ the crossings check
def money(x: float) -> str:
return f"{x:,.2f}"
def check_pack() -> None:
try:
org = json.loads((PACK / "org_units.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.")
if not any(a["ControllingArea"] == "CP00" for a in org["ControllingAreas"]):
sys.exit("Controlling area CP00 is missing from the pack. Run make_calder_pack.py again.")
def findings() -> list:
"""Each finding: (id, crossing, what, owner, root cause)."""
s4, rejected = replicate()
hr_cc = hr_cost_centers()
found = []
for cc, (_, vt) in sorted(hr_cc.items()):
s4_vt = next(v for c, _, _, v in S4_COST_CENTERS if c == cc)
if vt != s4_vt:
users = [p for p, _, _, c, _, _ in hr_people() if c == cc]
found.append(("cost centers S/4HANA -> HR",
f"{cc} closed in S/4HANA on {s4_vt}, still open in HR (last replicated "
f"{HR_CC_LAST_REPLICATED}); {len(users)} people assigned", "HR integration team",
"cost center replication"))
for p, why in sorted(rejected.items()):
found.append(("employees HR -> S/4HANA", f"{p} hired, not in S/4HANA: {why}",
"HR integration team", "cost center replication"))
for p, rec in sorted(s4.items()):
if not s4_valid(rec["CostCenter"], AS_OF) and not rec["End"]:
found.append(("employees HR -> S/4HANA", f"{p} assigned to closed cost center {rec['CostCenter']}",
"HR, with the cost accountant", "cost center replication"))
for p, valid_to in BUSINESS_USERS:
if p in s4 and s4[p]["End"] and valid_to > s4[p]["End"]:
found.append(("leaver -> access", f"{p} left on {s4[p]['End']}; S/4HANA business user still valid",
"Security administrator", "leaver process"))
pay = check_payroll()
if not pay["posts"]:
ccs = sorted({l[2] for l in pay["bad"]})
found.append(("payroll -> finance", f"October posting held: {money(pay['held'])} USD on {', '.join(ccs)}",
"Payroll, with the G/L accountant", "cost center replication"))
for rec, p, day, order, hours, cost, status, why in check_time(s4):
if status != "confirmed":
root = "cost center replication" if status == "blocked" else "time entry"
found.append(("time -> maintenance orders", f"{rec}: {hours:.1f} h by {p} on order {order} ({why})",
"Maintenance supervisor", root))
for rep, p, purpose, cc, amount, status, why in check_expenses(s4):
if status == "blocked":
found.append(("expenses -> finance", f"{rep}: {money(amount)} USD for {p} ({why})",
"Travel and expense admin", "cost center replication"))
return [(f"H{i}", *f) for i, f in enumerate(found, 1)]
def print_check() -> None:
s4, _ = replicate()
found = findings()
print(f"Calder Pumpworks (fictional), CP10 people-data crossings on {AS_OF}")
print(f"HR records: {len(HR_EMPLOYEES)} in S/4HANA: {len(s4)} findings: {len(found)}\n")
for fid, crossing, what, owner, root in found:
print(f" {fid:<4}{crossing:<28}{what}")
print(f" owner: {owner:<34}root cause: {root}")
roots = {}
for f in found:
roots[f[4]] = roots.get(f[4], 0) + 1
print("\n By root cause: " + ", ".join(f"{r} {n}" for r, n in sorted(roots.items(), key=lambda x: -x[1])))
def print_payroll() -> None:
pay = check_payroll()
print(f"October payroll posting document for CP10 (fictional), posting date 2026-10-30\n")
print(f" {'G/L':<10}{'Account':<32}{'Cost center':<13}{'Debit':>12}{'Credit':>12} check")
for gl, name, cc, d, c in pay["lines"]:
flag = "" if not cc else ("ok" if s4_valid(cc, "2026-10-30") else "closed")
print(f" {gl:<10}{name:<32}{cc:<13}{money(d) if d else '':>12}{money(c) if c else '':>12} {flag}")
print(f"\n Debits {money(pay['debit'])} credits {money(pay['credit'])} "
f"{'balanced' if pay['debit'] == pay['credit'] else 'NOT balanced'}")
if pay["posts"]:
print(" Result: the document posts to CP10.")
else:
print(f" Result: held. {money(pay['held'])} USD sits on a cost center that is closed in S/4HANA;")
print(" in this scenario the whole document waits, so October payroll is missing from CP10's books.")
def print_time() -> None:
s4, _ = replicate()
rows = check_time(s4)
print("Technician time for maintenance orders (fictional), October 2026\n")
for rec, p, day, order, hours, cost, status, why in rows:
print(f" {rec:<8}{p:<10}{day:<12}{order:<9}{hours:>5.1f} h{money(cost):>10} {status:<10}{why}")
for status in ("confirmed", "blocked", "check"):
h = sum(r[4] for r in rows if r[6] == status)
c = sum(r[5] for r in rows if r[6] == status)
print(f"\n {status:<10}{h:>5.1f} h{money(c):>10} USD", end="")
print("\n\n Blocked hours are real work missing from the order's cost until the person exists in S/4HANA.")
def print_expenses() -> None:
s4, _ = replicate()
print("Expense reports waiting to post to CP10 (fictional)\n")
for rep, p, purpose, cc, amount, status, why in check_expenses(s4):
print(f" {rep:<14}{p:<10}{cc:<10}{money(amount):>10} {status:<8}{why}")
# ------------------------------------------------------------ AI grounding: what may cross
# Fields a planned maintenance scheduling assistant asked for, and the decision per field.
# allow: needed and low risk. pseudonymize: needed to join records, never shown as is. exclude: not needed or sensitive.
AI_FIELD_POLICY = [
("PersonnelNumber", "pseudonymize", "needed to join records; the assistant sees a token"),
("WorkCenter", "allow", "decides which orders a person can work on"),
("SkillTags", "allow", "job-related qualifications"),
("PlannedWorkingHours", "allow", "capacity per day"),
("AvailableOnDay", "allow", "yes or no per day, derived from absences"),
("AbsenceType", "exclude", "can reveal health or family matters; availability is enough"),
("CostCenter", "exclude", "not needed to schedule"),
("PayGrade", "exclude", "pay data; never needed to schedule"),
("HomeAddress", "exclude", "personal data; not needed"),
("DateOfBirth", "exclude", "personal data; not needed"),
("BankAccount", "exclude", "payment data; not needed"),
]
SKILLS = {"00100094": ["hydraulics"], "00100117": ["hydraulics", "crane inspection"],
"00100142": ["electrical", "PLC"], "00100231": ["hydraulics"]}
# Absences in the HR system for the overhaul week 9-13 Nov (the type stays in HR).
ABSENCES = {("00100117", "2026-11-10"): "sick leave",
("00100142", "2026-11-12"): "vacation", ("00100142", "2026-11-13"): "vacation"}
WEEK = ["2026-11-09", "2026-11-10", "2026-11-11", "2026-11-12", "2026-11-13"]
# The pseudonym key comes from the environment. The course default keeps your output
# identical to the topic; a real system reads it from a secret store and has no default.
PSEUDONYM_KEY = os.environ.get("CALDER_PSEUDONYM_KEY", "calder-course-only").encode()
def token(p: str) -> str:
return "T-" + hmac.new(PSEUDONYM_KEY, p.encode(), hashlib.sha256).hexdigest()[:8]
def ai_view() -> list:
s4, _ = replicate()
rows = []
for p, rec in sorted(s4.items()):
if rec["WorkCenter"] not in RATES or (rec["End"] and rec["End"] < WEEK[0]):
continue
rows.append({"Person": token(p), "WorkCenter": rec["WorkCenter"], "SkillTags": SKILLS.get(p, []),
"PlannedWorkingHours": 8.0,
"AvailableOnDay": {d[5:]: (p, d) not in ABSENCES for d in WEEK}})
return rows
def print_ai() -> None:
print("Field decisions for the maintenance scheduling assistant (course policy)\n")
for field, decision, why in AI_FIELD_POLICY:
print(f" {field:<22}{decision:<14}{why}")
rows = ai_view()
print(f"\nWhat the assistant receives for the overhaul week, {len(rows)} technicians:\n")
for r in rows:
days = " ".join(f"{d}:{'yes' if ok else 'no '}" for d, ok in r["AvailableOnDay"].items())
print(f" {r['Person']} {r['WorkCenter']:<7}{', '.join(r['SkillTags']):<30}{days}")
print("\n No names, no absence types, no pay. People not yet in S/4HANA aren't offered.")
# ------------------------------------------------------------ report
def write_report() -> Path:
found = findings()
pay = check_payroll()
lines = [f"# Calder people-data crossings, {AS_OF}", "",
"Fictional company and data, made up for the course. Not from an SAP system.", "",
"```mermaid", "flowchart LR",
" S4[S/4HANA CP10] -->|cost centers| HR[HR system]",
" HR -->|employees| S4",
" HR --> PY[Payroll]",
" PY -->|posting document| S4",
" TM[Time records] -->|confirmations| S4",
" EX[Expense system] -->|postings| S4",
"```", "",
"| ID | Crossing | Finding | Owner | Root cause |", "| --- | --- | --- | --- | --- |"]
lines += [f"| {f[0]} | {f[1]} | {f[2]} | {f[3]} | {f[4]} |" for f in found]
lines += ["", f"Payroll posting: debits {money(pay['debit'])}, credits {money(pay['credit'])}, "
f"{'posts' if pay['posts'] else 'held: ' + money(pay['held']) + ' USD'}.", ""]
out = HERE / "calder_people_crossings.md"
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
return out
# ------------------------------------------------------------ sandbox (SAP demo data, not Calder's)
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_MANAGE_WORKFORCE_TIMESHEET/TimeSheetEntryCollection")
# Minimization in practice: no PersonWorkAgreement, no notes. Only what a cost view needs.
query = urllib.parse.urlencode({"$top": "50", "$format": "json",
"$select": "TimeSheetRecord,CompanyCode,TimeSheetDate,"
"TimeSheetStatus,TimeSheetDataFields"})
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_receiver = {}
for it in items:
f = it.get("TimeSheetDataFields") or {}
receiver = f.get("ReceiverCostCenter") or f.get("WBSElement") or f.get("InternalOrder") or "(none)"
key2 = (f.get("ActivityType") or "-", receiver)
by_receiver[key2] = by_receiver.get(key2, 0.0) + float(f.get("RecordedHours") or 0)
print(f"SAP sandbox: {len(items)} timesheet records read (SAP demo data, not Calder's)\n")
for (activity, receiver), hours in sorted(by_receiver.items(), key=lambda x: -x[1])[:8]:
print(f" activity type {activity:<12} receiver {receiver:<26}{hours:>8.2f} h")
print("\n Person fields were not requested: a cost view doesn't need them.")
# ------------------------------------------------------------ main
def main() -> None:
parser = argparse.ArgumentParser(description="Hire to retire crossings at Calder (fictional data).")
parser.add_argument("--payroll", action="store_true", help="October payroll posting document")
parser.add_argument("--time", action="store_true", help="technician time for maintenance orders")
parser.add_argument("--expenses", action="store_true", help="expense reports waiting to post")
parser.add_argument("--ai", action="store_true", help="field decisions and the minimized view")
parser.add_argument("--write", action="store_true", help="write unit1b/calder_people_crossings.md")
parser.add_argument("--sandbox", action="store_true", help="read timesheet records from SAP's sandbox")
args = parser.parse_args()
if args.sandbox:
sandbox()
return
check_pack()
if args.payroll:
print_payroll()
elif args.time:
print_time()
elif args.expenses:
print_expenses()
elif args.ai:
print_ai()
else:
print_check()
if args.write:
print(f"\nwrote unit1b/{write_report().name}")
if __name__ == "__main__":
main()
Calder Pumpworks (fictional), CP10 people-data crossings on 2026-11-02
HR records: 9 in S/4HANA: 8 findings: 9
H1 cost centers S/4HANA -> HR CP11-435 closed in S/4HANA on 2026-09-30, still open in HR (last replicated 2026-09-14); 2 people assigned
owner: HR integration team root cause: cost center replication
H2 employees HR -> S/4HANA 00100231 hired, not in S/4HANA: cost center CP11-435 not valid on 2026-10-19
owner: HR integration team root cause: cost center replication
H3 employees HR -> S/4HANA 00100205 assigned to closed cost center CP11-435
owner: HR, with the cost accountant root cause: cost center replication
H4 leaver -> access 00100094 left on 2026-10-16; S/4HANA business user still valid
owner: Security administrator root cause: leaver process
H5 payroll -> finance October posting held: 7,350.00 USD on CP11-435
owner: Payroll, with the G/L accountant root cause: cost center replication
H6 time -> maintenance orders T-5103: 8.0 h by 00100231 on order 4000126 (personnel number not in S/4HANA)
owner: Maintenance supervisor root cause: cost center replication
H7 time -> maintenance orders T-5104: 6.0 h by 00100231 on order 4000126 (personnel number not in S/4HANA)
owner: Maintenance supervisor root cause: cost center replication
H8 time -> maintenance orders T-5105: 4.0 h by 00100094 on order 4000126 (date after employment end 2026-10-16)
owner: Maintenance supervisor root cause: time entry
H9 expenses -> finance ER-2026-0419: 486.75 USD for 00100231 (no employee business partner or supplier role for reimbursement; cost center CP11-435 closed)
owner: Travel and expense admin root cause: cost center replication
By root cause: cost center replication 7, leaver process 1, time entry 1
Read the last line first. Seven of nine findings share one root cause: HR still thinks CP11-435 is open because the cost center feed last ran on 14 September. The other two are separate problems: a leaver's user and hours keyed after a leaving date.
Run the script with --payroll (Windows: python unit1b\hire_to_retire.py --payroll; macOS/Linux: python unit1b/hire_to_retire.py --payroll).
You should see:
October payroll posting document for CP10 (fictional), posting date 2026-10-30
G/L Account Cost center Debit Credit check
61100000 Wages and salaries CP11-300 18,400.00 ok
61200000 Employer payroll taxes CP11-300 1,410.00 ok
61100000 Wages and salaries CP11-410 96,300.00 ok
61200000 Employer payroll taxes CP11-410 7,370.00 ok
61100000 Wages and salaries CP11-420 31,200.00 ok
61200000 Employer payroll taxes CP11-420 2,390.00 ok
61100000 Wages and salaries CP11-435 6,830.00 closed
61200000 Employer payroll taxes CP11-435 520.00 closed
61100000 Wages and salaries CP11-450 38,900.00 ok
61200000 Employer payroll taxes CP11-450 2,980.00 ok
61100000 Wages and salaries CP11-900 27,600.00 ok
61200000 Employer payroll taxes CP11-900 2,110.00 ok
21500000 Net pay payable 158,640.00
21600000 Employee withholdings payable 60,590.00
21700000 Employer payroll taxes payable 16,780.00
Debits 236,010.00 credits 236,010.00 balanced
Result: held. 7,350.00 USD sits on a cost center that is closed in S/4HANA;
in this scenario the whole document waits, so October payroll is missing from CP10's books.
The document balances; it is held only because two lines name a closed cost center. The script checks each cost center against its validity on the posting date, the same test the ledger applies. In a real system, the error comes back from the posting, and payroll must correct the assignment and post again.
Technician time for maintenance orders (fictional), October 2026
T-5101 00100117 2026-10-26 4000126 6.0 h 570.00 confirmed activity allocation to the order
T-5102 00100142 2026-10-26 4000126 3.5 h 367.50 confirmed activity allocation to the order
T-5103 00100231 2026-10-27 4000126 8.0 h 760.00 blocked personnel number not in S/4HANA
T-5104 00100231 2026-10-28 4000126 6.0 h 570.00 blocked personnel number not in S/4HANA
T-5105 00100094 2026-10-20 4000126 4.0 h 380.00 check date after employment end 2026-10-16
confirmed 9.5 h 937.50 USD
blocked 14.0 h 1,330.00 USD
check 4.0 h 380.00 USD
Blocked hours are real work missing from the order's cost until the person exists in S/4HANA.
Run with --expenses the same way. You should see:
Expense reports waiting to post to CP10 (fictional)
ER-2026-0412 00100117 CP11-450 642.18 posts
ER-2026-0415 00100094 CP11-450 2,318.40 posts employment ended: keep the supplier role payable until reimbursed
ER-2026-0419 00100231 CP11-435 486.75 blocked no employee business partner or supplier role for reimbursement; cost center CP11-435 closed
The leaver's expense report posts on purpose. He spent the money while employed, so Calder still owes it. Blocking a leaver's supplier role on the last day would stop a legitimate payment.
#Step 7: See what a scheduling assistant may receive
The overhaul of the test bench, order 4000123, runs 9 to 11 November. A planned assistant would propose which technicians to schedule. It asked for eleven HR fields.
Run with --ai (Windows: python unit1b\hire_to_retire.py --ai; macOS/Linux: python unit1b/hire_to_retire.py --ai).
You should see:
Field decisions for the maintenance scheduling assistant (course policy)
PersonnelNumber pseudonymize needed to join records; the assistant sees a token
WorkCenter allow decides which orders a person can work on
SkillTags allow job-related qualifications
PlannedWorkingHours allow capacity per day
AvailableOnDay allow yes or no per day, derived from absences
AbsenceType exclude can reveal health or family matters; availability is enough
CostCenter exclude not needed to schedule
PayGrade exclude pay data; never needed to schedule
HomeAddress exclude personal data; not needed
DateOfBirth exclude personal data; not needed
BankAccount exclude payment data; not needed
What the assistant receives for the overhaul week, 2 technicians:
T-dd79bd56 MECH1 hydraulics, crane inspection 11-09:yes 11-10:no 11-11:yes 11-12:yes 11-13:yes
T-c4b1170a ELEC1 electrical, PLC 11-09:yes 11-10:yes 11-11:yes 11-12:no 11-13:no
No names, no absence types, no pay. People not yet in S/4HANA aren't offered.
The assistant gets work center, skills and a yes or no per day. It never learns that one "no" is sick leave and two are vacation, and it sees tokens, not personnel numbers. The tokens come from a keyed hash (hmac); the same person always gets the same token, so the assistant's proposals can be mapped back in S/4HANA by someone allowed to. In the script the key comes from the environment variable CALDER_PSEUDONYM_KEY, with a course default so your output matches. A real system keeps the key in a secret store, with no default in the code: personnel numbers have only eight digits, so anyone who knows the key can hash every possible number and reverse every token in seconds.
The new technician isn't offered, because the assistant reads S/4HANA, where he doesn't exist yet. That is one more cost of the stopped feed.
Run with --write (Windows: python unit1b\hire_to_retire.py --write; macOS/Linux: python unit1b/hire_to_retire.py --write).
The last line should read wrote unit1b/calder_people_crossings.md. Open it in VS Code and press Ctrl+Shift+V (Windows, Linux) or Cmd+Shift+V (macOS) to preview it: a diagram of the crossings, the findings table and the payroll result.
#Step 9 (optional): Read timesheet records 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\hire_to_retire.py --sandbox
macOS / Linux:
python unit1b/hire_to_retire.py --sandbox
The output has this shape. Values in angle brackets come from SAP's demo data and change over time:
SAP sandbox: <n> timesheet records read (SAP demo data, not Calder's)
activity type <type> receiver <cost center, WBS or order> <hours> h
Person fields were not requested: a cost view doesn't need them.
The script reads up to 50 records from API_MANAGE_WORKFORCE_TIMESHEET and sums hours by activity type and receiver. Its $select leaves out PersonWorkAgreement and notes: minimization at the API call. A valid "empty" result is 0 timesheet records read. The sandbox is read-only.
Where it sits. At Calder (fictional), Employee Central holds people for CP10; CP20 keeps HR and payroll inside ECC until its move to S/4HANA. CP10's S/4HANA system owns cost centers and receives workforce records. Payroll and the expense system post into CP10. Any AI tool reads S/4HANA's derived facts, not the HR system.
Non-functional requirements that decide the design
Requirement
Typical target
If missed
Privacy and minimization
Each crossing carries a documented field list; no individual pay in finance unless required
Personal data spreads to systems and people with no need
Replication freshness
Hires and transfers in S/4HANA within a day; cost center changes in HR before the next pay run
New hires can't work or be paid; postings fail
Monitoring
Every feed has an owner and an alert on failure
A stopped feed is found at month end
Leaver timeliness
Access ends on the last day
Audit findings; security incidents
Retention and deletion
Personal data blocked after its purpose ends and deleted after its retention period
Legal exposure; data kept forever
Audit
Who changed a person's cost assignment and when
Payroll postings can't be explained
Integration and identity. The packaged Employee Central integrations use middleware: SAP Master Data Integration for S/4HANA Cloud Public Edition (SAP Learning), and web services configured in S/4HANA for SAP HCM targets (SAP Learning). The person ID must match across every channel (SAP KBA 2813743). The same person is also a business user who logs on; the leaver event must reach user administration as well as the business partner. Any assistant reading people data should use the user's own identity and authorizations (grounding with authorizations). Integration patterns between systems are covered in how SAP systems talk.
Context: Calder's payroll posts one document per pay run to CP10. Some cost centers have one or two people, so totals by cost center can reveal an individual's pay. Finance asks for detail by person to explain labor variances.
Options: (a) post one line per employee; (b) post totals by cost center and account, with detail kept in payroll; (c) totals by cost center, plus a restricted payroll report for the few people who explain variances.
Decision: (c). The ledger carries totals; detail stays in payroll behind payroll authorizations, and controlling reports hide cost centers with fewer than a set number of people.
Consequences: Variance explanations need a payroll analyst. A cost center with one person still shows that person's cost, so its access is restricted. Any AI that explains labor cost reads totals only.
What a design review will ask
Which system is the system of record for each field that crosses, and what is the field list per crossing?
Who is alerted when a feed stops, and how fast?
What happens in S/4HANA on a hire, a transfer and a leave, on the effective date?
Can anyone in finance see one person's pay, through a posting, a report or an AI tool?
When are a leaver's records blocked and deleted, and does that cover copies outside SAP?
Why HR data is usually kept out of AI grounding. A retrieval index or an agent's context is one more copy of the data, readable by anyone who can ask the assistant. HR records hold the data that does the most harm when exposed: health through absence types, pay, bank details, family and disciplinary matters. Most business questions need a derived fact instead. Data security and PII shows how to minimize, mask and erase personal data in an AI assistant, and agent permissions shows how to keep an agent within the user's own authorizations. The line here: AI reads derived facts and proposes; HR changes people records, payroll posts, and a person approves every change.
Closing a cost center without asking HR. Check who is assigned to it in the HR and payroll systems first, and plan the move.
No owner for the replication monitor. Errors pile up quietly until payroll or the close finds them.
Reusing positions across functions. A position carries a cost center; reusing an old one brings its old assignment.
Running two replication channels at once. Mismatched person IDs create duplicate employees.
Posting payroll by person by default. It spreads pay data into the ledger, reports and every tool that reads it.
Blocking everything on a leaving date. End access the same day, but keep paying what is owed and keep records for their retention period.
Grounding an assistant on HR tables "for context". Give it a derived fact through a narrow view, or nothing.
Assuming CP20 works like CP10. In ECC, HR and finance may share one system, with employee vendors instead of business partners; plan the move to S/4HANA around that.
Fix the stopped feed at Calder, prove what it repairs, and write the people part of the capstone note.
Run python unit1b/hire_to_retire.py --write (Windows: python unit1b\hire_to_retire.py --write). Copy unit1b/calder_people_crossings.md to unit1b/calder_people_actions.md.
In the copy, add a column Action to the findings table. For each finding write one sentence: what the owner does today.
In hire_to_retire.py, change CC_REPLICATION_RUN = False to CC_REPLICATION_RUN = True. This is the cost center feed running again.
Change HR_FIXES = {} to HR_FIXES = {"00100205": "CP11-410", "00100231": "CP11-450"}. This is HR moving the rework technician to assembly and the new technician to maintenance.
Run the script without options, then with --payroll, --time, --expenses and --ai. Note the number of findings, the payroll result, the hours now confirmed on order 4000126, the expense report and how many technicians the assistant now sees.
Change both edits back.
In the action note, add a heading Fixes tested with four lines: what is left in the findings, what payroll now posts to CP11-410 and CP11-450, what order 4000126 now carries, and the assistant's view.
Add a heading Privacy note with two lines: which payroll lines moved when the two people changed cost center, and why that shows one person's pay to anyone who can read the posting.
Preview the file and commit it with Git.
Done when:unit1b/calder_people_actions.md has the nine-line findings table with Action filled in, a Fixes tested section that says two findings remain (the leaver's user and the 4.0 hours after his leaving date), the payroll document posts with 100,910.00 USD wages on CP11-410 and 41,120.00 on CP11-450, order 4000126 carries 23.5 confirmed hours (2,267.50 USD), the expense report ER-2026-0419 posts to CP11-450, and the assistant sees three technicians, plus a Privacy note. Keep it: the capstone at the end of Unit 1B traces one exception across the whole enterprise, and the technicians' availability is its people part.
Pick one answer for each question. The explanation appears after you choose.
1Why did S/4HANA reject the new technician's employee record in Calder's scenario?
Answer: D. HR assigned him to CP11-435, which S/4HANA closed on 30 September. The HR system didn't know, because the cost center feed had stopped.
2In S/4HANA on-premise, what does an employee become so that logistics and finance processes can use them?
Answer: A. From S/4HANA 2020, each employee is synchronized to business partners with roles BUP003 and BUP010; a supplier role is optional. PRAA employee vendors belong to SAP ERP and are no longer valid.
3The payroll posting balances to the cent but is still held. What does the script check that explains this?
Answer: D. Two lines name CP11-435, closed on 30 September. A balanced document still fails if an account assignment isn't valid when it posts.
4Maintenance hours reach order 4000126 through which interface, given the timesheet API's receivers?
Answer: A. The timesheet model's receivers are cost center, WBS element, internal order and purchase order. Maintenance orders take hours as confirmations, which carry the personnel number and the hours.
5What would you do if finance asked to post payroll with one line per employee to explain labor variances?
Answer: B. Per-person lines put individual pay in the ledger and every report and tool that reads it. Totals by cost center serve the close; detail stays in payroll behind its own authorizations. Personnel numbers still identify people.
6The scheduling assistant gets "unavailable on 10 November" but not the absence type. Why?
Answer: A. Data minimization: the decision needs only whether someone can work. Sick leave is health data, so it stays in the HR system.
7A leaver's expense report for a trip while employed arrives after his last day. What should happen?
Answer: D. Calder owes the money for work done while employed. Access ends on the last day, but the payee record must stay payable until the debt is settled.
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.
Cloudification Repository (SAP on GitHub)— objectReleaseInfoLatest.json read 24 October 2026; I_WorkforcePerson_1, I_PersonWorkAgreement_1, I_PersonWorkAgrmtOrglDetails, I_TimeSheetRecord, I_BusinessUserBasic, I_BPEmployment, I_CostCenter released; BAPI_ACC_EMPLOYEE_EXP_POST not to be released
Workforce Timesheet service model (SAP Cloud SDK, npm)— API_MANAGE_WORKFORCE_TIMESHEET, TimeSheetEntryCollection with PersonWorkAgreement, CompanyCode, TimeSheetDate, TimeSheetStatus and TimeSheetDataFields; published 2022