Background
One of the more nuanced integration challenges in an SAP Employee Central Payroll (ECP) landscape is keeping SuccessFactors Employee Central (EC) in sync with what actually happened inside the payroll engine. This is especially true for loan advance management.
In EC, when a loan is approved for an employee, the system creates a structured repayment schedule — a set of installment records, each with a due date, an amount, and a status (NOT_PAID, PAID, PARTIALLY_PAID). EC holds the plan.
ECP, on the other hand, holds the reality. The actual salary deduction for the EMI happens inside the payroll run via specific wage types. EC has no automatic visibility into whether ECP deducted the right amount in a given month, or whether it deducted anything at all.
For this engagement, the client had multiple active loan types — for example, a car loan scheme, a marriage loan scheme, and an employee loan scheme. Each loan type had its own set of wage types covering issuance, regular recovery, off-cycle recovery, and outstanding balance. Someone on the payroll team was manually updating EC installment statuses every month after payroll closed. This PoC was built to eliminate that manual step entirely.
What We Built
We developed a custom ABAP report running inside ECP that does the following end to end:
- Reads each employee's installment schedule from EC via the AdvancesInstallments OData v2 API
- Reads the employee's complete payroll run history from the ECP payroll cluster
- Reconciles the two — did ECP actually deduct the EMI for each installment?
- Produces a comma-delimited CSV file formatted for EC's batch import job, updating each installment's status
- Displays results in an ALV grid for review before the file is uploaded
- Logs every step to SLG1 for audit purposes
No middleware. No integration platform. A single ABAP program running directly inside ECP, calling SuccessFactors over HTTP.
Solution Architecture
┌─────────────────────────────────────────────────────┐
│ ECP (ABAP Stack) │
│ │
│ Custom Report │
│ ┌──────────────┐ OData GET ┌─────────────────┐ │
│ │ OData │ ────────────► │ SuccessFactors │ │
│ │ Reader │ ◄──────────── │ EC │ │
│ │ (per PERNR) │ JSON resp. │ AdvancesInst. │ │
│ └──────────────┘ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Payroll │ CU_READ_RGDIR + PYXX_READ │
│ │ Cluster │ PCL2 — complete run history │
│ │ Reader │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Reconcilia- │ FIFO matching │
│ │ tion Engine │ PAID / NOTPAID / EXCEPTION │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ CSV Writer │ AL11 output → EC Batch Import │
│ └──────────────┘ │
└─────────────────────────────────────────────────────┘The program is structured as an OO ABAP report — a local class hierarchy with clearly separated responsibilities:
Class Responsibility
lcl_logger | SLG1 application log wrapper |
lcl_odata_reader | HTTP call and JSON parsing for EC OData |
lcl_cluster_reader | PCL2 cluster read and off-cycle detection |
lcl_wt_config | Wage type mapping (from custom config table) |
lcl_reconciler | FIFO EMI matching logic |
lcl_csv_writer | EC upload file formatting and write |
lcl_alv_display | ALV result grid |
The OData Integration — Key Technical Points
This is the part that had the most surprises, so it deserves the most detail.
Connection Setup
The HTTP connection to SuccessFactors is configured as an SM59 HTTP destination. Authentication is handled via OAuth 2.0 — the SM59 destination manages token acquisition automatically. The ABAP code uses
cl_http_client=>create_by_destination to obtain an HTTP client object and issues a standard GET request. No custom token management code is needed inside the report itself.The Entity and URL Structure
The OData entity being called is
AdvancesInstallments. A typical request URL looks like this:/odata/v2/AdvancesInstallments
?$filter=startswith(Advance_externalCode,'<PERNR>_')
&$select=externalCode,Advance_externalCode,installmentAmount,
installmentStatus,balanceRemaining,paymentDate,
currencyGO,lastModifiedDateTime
&$format=jsonThe
Advance_externalCode field is the foreign key linking each installment back to its parent loan advance record. Its format typically includes the employee PERNR as a prefix, so filtering with startswith(Advance_externalCode,'<PERNR>_') returns all installments across all loan types for that employee in a single call.The Critical SF OData v2 Limitation — One Call Per Employee
The most important gotcha in this integration: you cannot reliably combine multiple employees in a single OData call using OR filters on
startswith.The intuitive approach would be to build a combined filter like:
startswith(Advance_externalCode,'EMP001_') or
startswith(Advance_externalCode,'EMP002_') or
startswith(Advance_externalCode,'EMP003_')In practice, SF OData v2 silently returns results for only the first condition and ignores the rest. No HTTP error is raised — you simply receive incomplete data with no indication that anything went wrong. That is far more dangerous than an outright error.
The solution is to make one dedicated HTTP GET call per employee PERNR. For a typical payroll run this is perfectly acceptable in terms of performance. For large employee populations, a pagination loop using
$skip and $top should be added in a production implementation.JSON Parsing
SF returns the response as a JSON structure where the
d.results array contains one object per installment. Rather than relying on a full JSON library — which can have version dependencies across ECP landscapes — the report uses lightweight string parsing: locating known field name anchors and extracting the value between surrounding quotes or up to the next delimiter.Dates come back in Microsoft OData
/Date(milliseconds)/ format and are converted to ABAP DATS format by dividing the millisecond value by 86,400,000 and applying the standard Unix epoch offset.Reading the ECP Payroll Cluster
The payroll cluster read has an important design decision worth explaining: there is no payroll period on the selection screen.
Instead of reading just one month's results, the report calls
CU_READ_RGDIR to fetch the complete payroll run directory for each employee — every finalised run from the beginning of employment to the current date. It then loops through every directory entry and reads the RT (Results Table) for each run using PYXX_READ_PAYROLL_RESULT, specifying the country-specific cluster area via the MOLGA parameter.The reason for reading the entire history rather than a single period is reconciliation completeness. To correctly determine whether an installment is PAID or NOTPAID, you need the full deduction history. An installment that was partially recovered in one month and topped up in the next requires both runs to be assessed correctly.
Off-Cycle Detection
Regular monthly runs and off-cycle runs (loan issuances, bonus runs, corrections) are handled differently. Off-cycle runs are identified by two checks combined with OR logic:
- The SRTZA flag in the run directory entry is
‘B' - The run's for-period begin date equals its for-period end date — a single-day period, which is characteristic of loan issuance off-cycle runs
Off-cycle deductions are logged but excluded from the installment matching, since they represent loan events rather than regular EMI recoveries.
The Reconciliation Logic
With the OData installments and the payroll RT entries both loaded for an employee, the reconciler works as follows:
- Groups the RT wage type entries by loan type using a custom configuration table
- For each loan type, sorts the EC installments by due date ascending — oldest first
- Applies a FIFO match — the oldest unreconciled installment is matched first against the available deduction amount
The outcome per installment:
Condition Status Written to CSV
| Deduction ≥ 95% of installment amount | PAID |
| Deduction > 0 but < 95% | NOTPAID |
| No deduction found | NOTPAID |
| ECP has deduction but EC has no matching installment | EXCEPTION — separate file |
The 95% threshold rather than 100% accounts for minor rounding differences that can arise in payroll calculations.
The EC Upload File — 12-Column Format
The CSV output follows the exact template that EC's Advance Installments batch import job accepts:
- Row 1 — technical API field names as EC expects them
- Row 2 — human-readable column labels
- Row 3 onwards — one data row per installment
Two fields use the special EC token
&&NO_OVERWRITE&&:paymentDate— the report does not override whatever payment date EC already holdsamortization— EC retains its existing value
The
amortizationTotal column is intentionally left blank — EC calculates this value itself.EXCEPTION rows are written to a separate file so they can be reviewed manually before any action is taken in EC.
The Wage Type Mapping Table
Rather than hardcoding wage types inside the ABAP logic, the mapping is externalised into a custom SE11 transparent table. This makes the solution maintainable — adding a new loan type in future only requires a new table entry, not a code change and transport.
The table holds the country grouping (MOLGA), wage type, a description, and an active flag. The report reads this table dynamically at runtime, so the ABAP code itself contains no client-specific wage type values.
Key Learnings and Gotchas
Having built and iterated through this PoC, here are the things worth knowing before attempting something similar:
1. SF OData v2 OR filters on
startswith are unreliable for multiple values. Always make one call per employee. Do not attempt to batch multiple employees in a single filter expression — you will get silently incomplete results.2.
/Date(ms)/ is the date format you will receive — plan to parse it. There is no automatic conversion. You need to handle the millisecond-to-ABAP-date arithmetic yourself.3.
CU_READ_RGDIR combined with PYXX_READ_PAYROLL_RESULT is the correct payroll cluster read path for ECP. Avoid direct PCL2 reads unless PYXX fails with a type conflict (return code 8), in which case consider the country-specific variant of the payroll result read function module.4. Off-cycle detection using the SRTZA flag alone is not always sufficient. Add the FPBEG = FPEND single-day check as a fallback — some off-cycle run types in certain country versions do not set SRTZA correctly.
5.
&&NO_OVERWRITE&& is essential in EC batch uploads. Use it for any field you do not intend to update. Without it, an empty CSV column will blank out the field in EC — which for a date or amount field can cause data corruption.6. SLG1 logging is non-negotiable for payroll integrations. Every HTTP call, every employee processed, every exception — log it. Payroll disputes are always easier to resolve when you have a timestamped audit trail of exactly what the program read and decided.
Conclusion
The integration pattern described here — an ABAP OData consumer inside ECP reconciling against payroll cluster results — is broadly applicable beyond loan advances. Any scenario where EC holds a plan and ECP holds the execution reality follows the same structural pattern: arrears management, advance salary recoveries, third-party remittance reconciliation.
The key insight from this PoC is that ECP already has everything it needs to drive this reconciliation. The payroll cluster contains the ground truth on what was actually processed. SF OData provides structured, filterable access to the EC-side plan. Connecting the two in ABAP, with a proper class structure and audit log, produces a reliable and auditable automation with no dependency on a middleware platform.
Questions and comments welcome below.
Source link