Vor einer Weile musste ich mich recht kurzfristig in das oben genannte Thema einarbeiten. Ich hatte bisher nie konkret damit zu tun und habe daher erstmal die entsprechende SAP Help Seite überflogen. Relativ schnell habe ich zum Glück den für mich relevanten Teil gefunden.
I was working with the Fiori App My Travel and Expenses for Business Traveler and implemented a new approval workflow. Unlike in the SAP standard workflow, the first approver was not the boss of the employee. The workflow was already working fine, but when sending the travel expense from the Fiori app, it still displayed the boss as approver.
Naturally, the Fiori app does not know anything from the changes on the workflow. So I had to figure out where this approver value comes from and how I could adjust it to match the workflow logic.
I first went to the official documentation to check if there is any BAdI to change/overwrite the approver.
Spoiler: Actually there is a BAdI called FIN_TRV_APPROVER_NAME, but it is not listed hereor anywhere else in the documentation. As many times before, incomplete SAP documentation wasted hours of my life…..
Because I could not find a BAdI, I went to the RAP service, to check where the approver is set and if it’s possible to overwrite it in someway.
When clicking the Create and send button in Fiori, a POST request to the endpoint CheckTripAmounts is fired. The response of type cds_ui_travelexpensemanage.TravelExpenseType contains the required value in field TrvlExpnApproverName.
I already knew the corresponding package for RAP-Service.
In package ODATA_TRV_MTE_UI_MANAGE I found the root view entity C_TravelExpense which contained the property TrvlExpnApproverName. Its projection I_TravelExpense was located in package VDM_TRV_MTE_UI_MANAGE. I was already afraid, that the approver is somehow read via some CDS association, but it turned out to be added as empty field and properly therefore is manually filled in some behavior implementation.
cast ( ' ' as vdm_trvlexpnapprovername ) as TrvlExpnApproverName,
The related behavior implementation was in class CL_BP_I_TRAVELEXPENSE and there I found the CheckTripAmounts method, which matched the POST request I saw in the Dev console. And here I found the following logic.
Möchte man Führungskräften die Möglichkeit geben, in der MyForms App die Formulare ihrer Mitarbeiter einzusehen, kann dies einfach über den HCMFAB_B_COMMON BAdI in der Methode GET_CONFIGURATION realisiert werden.
Im Standard werden der Zeitnachweis und Entgeltnachweis angezeigt. Möchte man nun für die Führungskraft den Entgeltnachweis ausblenden, kann dies über den BAdI HCMFAB_BI_MYFORMS gemacht werden.
In der Methode GET_HIDE_REM_TIME_STATEMENT kann dies z.B. so realisiert werden.
Die Variable ev_hide_rem verhindert das Hinzufügen des Entgeltnachweises im folgenden, wie ab Zeile 25 zu sehen.
Jedoch ist nach meiner Beobachtung das noch nicht ausreichend, da in der Methode FILL_FORM_TYPES der Entgeltnachweis dann doch wieder hinzugefügt wird.
Daher habe ich den Entgeltnachweis zusätzlich noch in der Methode FILL_FORM_TYPES entfernt.
DATA(ownpernr) = go_employee_api->get_employeenumber_from_user( ).
IF iv_pernr <> ownpernr. " Manager dürfen nur Zeitnachweise ihrer Mitarbeiter sehen
DELETE ct_form_types WHERE form_type <> 'SAP_INT_TIM_STM'.
RETURN.
ENDIF.
Die Führungskraft kann nun ausschließlich den Zeitnachweis des Mitarbeiters sehen. Er selbst kann alle Formulare wie gewohnt einsehen.
In der Meine Formulare App können über den BAdI HCMFAB_B_MYFORMS ganz einfach Spalten ausgeblendet werden. Dafür eine BAdI Implementierung anlegen und die Methode FILL_FORM_SPECIFIC_FIELDS ausprogrammieren. Zum Entfernen einer Spalte muss diese einfach aus dem CHANGING Parameter ct_form_specific_fields gelöscht werden.
Dabei einfach über den FORM_TYPE auf das entsprechende Formular filtern und die gewünschte Spalte entfernen:
DELETE ct_form_specific_fields WHERE form_type = 'SAP_INT_TIM_STM' AND field_id = 'DEVIATION'. "Abweichung entfernen
Für einen direkten Absprung von z.B. einer Stellenanzeige einer Karriereseite zu dem zugehörigen Jobangebot im Recruiting, muss ein Link folgendermaßen aufgebaut sein:
Dies kann hilfreich sein, wenn man den Karriere-Feed in einem Drittsystem eingebunden hat, aber dennoch einen direkten Absprung für den Bewerber haben möchte.
Kürzlich habe ich die My Reporting App in der S/4HANA Version auf einem System aktiviert. Nach der Einrichtung bin ich aber in die folgende generische Fehlermeldung gelaufen:
App kann nicht geöffnet werden, weil die SAP-UI5-Komponente der Anwendung nicht geladen werden konnte. UI5-Komponente für Navigationsabsicht “#Manager-launchReports?scenarioId=SAP_EXAMPLE” wurde nicht geladen.
Ein Blick in die Konsole zeigte, dass versucht wurde, eine weitere Komponente namens hcm_reuselibs1 zu laden. Diese wird in der Fiori Reference Library zu der App nicht erwähnt, aber nach der Aktivierung, ließ sich die App erfolgreich laden.
In der Transaktion /UI2/FLP_SYS_CONF bzw. /UI2/FLP_CUS_CONF kann man über die folgenden Parameter eine Info-Bar einblenden um z.B. das aktuelle System oder andere Informationen dem Anwender anzuzeigen.
This was pretty easy and after enabling the Apps, I activated the default Workflows WS20000050 (Approve Travel Request) and WS20000040 (Approve Trip) as well. I also checked SWE2 for BU2089 and everything looked good.
But when testing, only the Travel Request Workflow started successfully. When creating a Trip Expense, no Workflow started, instead the Trip Expense directly went to status “Trip Approved” instead of “Trip Completed“. Here you can find an overview of the possible statuses: Trip Status Directory
Initially I thought, this is somehow a Workflow issue or the BUS2089 connection does throw the right event. I also found a note related to this: 2792991. But it turned out to be a customizing problem and the answer was only one click away from the Trip Status Directory in the chapter Trip Status Assignment. Here the Feature TRVPA is mentioned. And when looking into the system and reading the documentation via PE03 I found the following
Turned out, for Feature TRVPA and Entry WRP the value 4 was set. After changing it to 3, the right trip status got set and the workflow got triggered.
As always, the solution is quite simple once you know it, but it took quite a while to figure it out.
Recently I was working on a Fiori Elements application, where users should be able to select large amount of rows. With the default ResponsiveTable this cannot be achieved, but Fiori Elements with Grid Table allows multi-selection of thousands of rows via Multi-selection plug-in. This is what I’ve added to the manifest.json.
Honestly, not 100% sure if every property was necessary, but I couldn’t find a clear documentation what property is valid for a GridTable…
The result looked good and selecting large amounts of rows was possible in the UI! However, actions like “Go” or table refresh after a custom action generated a massive OData $filter:, because each selected row got appended to the filter like this:
$filter=ID eq 'uuid1' or ID eq 'uuid2' or ... (4000+ ORs)
When sending such a large request, it exceeds that standard Header Limit: “Request Header Fields Too Large“
Now the request went through and reached the backend. But CAP parses the request to CQN, which becomes a deep SQL WHERE tree. SQLite rejects this with “Expression tree is too large (maximum depth 1000)“.
Haven’t tested on HANA, but I wanted to make sure it also works in my local development environment. So I needed to find a way to reduce the Expression tree size.
CAP Solution: Rewrite OR Chain to IN
Intercept READ requests, parse the CQN SELECT.where array, extract IDs, rebuild as ID IN (...) using cds.parse.xpr.
srv/service.js:
const cds = require('@sap/cds');
this.before('READ', 'YourEntity', req => {
const whereArray = req.query.SELECT?.where;
if (!Array.isArray(whereArray) || whereArray.length < 50) return;
const ids = [];
let i = 0;
while (i < whereArray.length) {
// Skip 'or' connectors
if (typeof whereArray[i] === 'string' && whereArray[i] === 'or') {
i++;
continue;
}
const refObj = whereArray[i]; // {ref: ['ID']}
const op = whereArray[i + 1]; // '='
const valObj = whereArray[i + 2]; // {val: 'uuid'}
if (refObj?.ref && op === '=' && valObj?.val) {
ids.push(`'${valObj.val}'`);
i += 3;
} else {
console.log('Pattern mismatch at', i);
return;
}
}
if (ids.length < 20) return; // Skip small filters
// Build IN clause
const condition = `ID in (${ids.join(',')})`;
req.query.SELECT.where = cds.parse.xpr(condition);[web:180][web:187]
console.log(`Rewrote ${whereArray.length/3} OR triplets → IN (${ids.length} IDs)`);
});
The handler rebuilds this to ID in ('uuid1','uuid2',...) → compact SQL without deep tree.
This worked, and now the SQLite could process the query! So to summarize what needed to be done:
Grid Table (not Responsive Table) for large datasets.
Multi-selection plug-in (limit: 0 or high value).
Increase max_batch_header_size in your reverse proxy/Gateway – otherwise $filter gets truncated before reaching CAP.
Custom Handler to transform the where condition
Quite complex – at least with the classic ALV, that wouldn’t have been an issue at all…
Update 27.03.2026: Here is a blog tackling a very similar situation, but with RAP instead of CAP: Fiori List Report: The “Process All” Dilemma — Parsing OData $filter in ABAP But with the provided solution it is not possible to select/unselect certain lines, since only the filter criteria are respected and no manual selection/deselection of specific rows is possible.