In many SAP S/4HANA implementations, particularly those involving Fiori List Reports, business users often require a unified experience that combines transactional capabilities ,such as executing actions on records ,with analytical insights like grouping, subtotals, and totals. This is especially relevant in operational reporting scenarios where decisions are made directly from the data view.
These hybrid use cases are common in operational reporting, where users need to analyze data and take immediate action within the same interface.However, when using analytical CDS views (annotated with @analytics.query: true), SAP RAP imposes architectural constraints that prevent the direct definition of actions within the same view. This limitation can be a bottleneck when designing intelligent, user-centric applications.
To overcome this challenge, the following approach is recommended:
Separate OData Services for Analytics and Transactions
@analytics.query: true, enabling features like grouping, subtotals, and totals.UI5 Extension Logic for Action Integration
Workaround for Filtering on Aggregated Fields
This approach allows developers to deliver intelligent Fiori applications that combine the best of both worlds: real-time analytical insights and direct transactional capabilities. It supports business scenarios such as:
By decoupling analytics and actions at the service level and integrating them at the UI layer, you can build scalable, maintainable, and user-friendly applications that align with SAP’s clean core principles.
You’ve correctly defined analytical views such as:
Z_I_APPRREQ_TOTAL: Aggregates planned costs using sum(bpja.wtjhr)Z_I_APPRREQ_COSTS: Joins totals with individual cost recordsZ_I_APPROPRIATEREQUEST: Combines investment request data with ledger and object info@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Appropriate Request Ledger Details.'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
serviceQuality: #X,
sizeCategory: #S,
dataClass: #MIXED
}
define view entity Z_I_APPRREQ_LEDGER as select from tka01
inner join tbp0l on tbp0l.waers = tka01.waers
and tbp0l.periv = tka01.lmona
{
key tka01.kokrs as Kokrs,
tbp0l.periv as Periv,
tbp0l.waers as Waers,
tbp0l.lednr as Lednr
}@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Appropriate Request Costs Details.'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
serviceQuality: #X,
sizeCategory: #S,
dataClass: #MIXED
}
define view entity Z_I_APPRREQ_COSTS
as select from bpja
inner join Z_I_APPRREQ_TOTAL on Z_I_APPRREQ_TOTAL.Ledger = bpja.lednr
and Z_I_APPRREQ_TOTAL.ObjectNumber = bpja.objnr
{
key bpja.lednr as Ledger,
key bpja.objnr as ObjectNumber,
key bpja.gjahr as FiscalYear,
// @semantics.currencycode
bpja.twaer as Currency,
@Semantics.amount.currencyCode : 'Currency'
bpja.wtjhr as PlancostsYear,
Z_I_APPRREQ_TOTAL.Currency as Currency_Code,
@Semantics.amount.currencyCode : 'Currency_Code'
Z_I_APPRREQ_TOTAL.PlancostsYear as PlancostsTotal
}
where
bpja.wrttp = '40';@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Appropriate Request Costs Details.'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
serviceQuality: #X,
sizeCategory: #S,
dataClass: #MIXED
}
define view entity Z_I_APPRREQ_TOTAL as select from bpja
{
key bpja.lednr as Ledger,
key bpja.objnr as ObjectNumber,
bpja.twaer as Currency ,
@Semantics.amount.currencyCode : 'Currency'
sum(bpja.wtjhr) as PlancostsYear
}
where bpja.wrttp = '40'
group by lednr,objnr,bpja.twaer;@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'IMAK Details'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
serviceQuality: #X,
sizeCategory: #S,
dataClass: #MIXED
}
@Analytics.dataCategory: #CUBE
define view entity Z_I_APPROPRIATEREQUEST
as select from imak
association [0..1] to imakpi on imakpi.posnr = imak.posnr
association [0..1] to imakt on imakt.posnr = imak.posnr
association [0..1] to imav on imav.posnr = imak.posnr
association [0..1] to Z_I_APPRREQ_LEDGER on Z_I_APPRREQ_LEDGER.Kokrs = imak.vkokrs
{
key
cast(imak.posnr as abap.char( 12 )) as Appr_Request,
imak.abukrs as CompanyCode,
imak.gjahr as FiscalYear,
imakpi.izwek as Izwek,
imakt.txt50 as Txt50,
imav.objnr as ObjectNumber,
Z_I_APPRREQ_LEDGER.Lednr as Ledger,
concat('IQ',imak.posnr) as IQOBJNR,
erdat as Erdat
}This consumption view is designed to support a Fiori List Report that displays investment request data with:
@EndUserText.label: 'Bericht zur Freigabe der Maßnahmenanforderungen'
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
serviceQuality: #X,
sizeCategory: #S,
dataClass: #MIXED
}
@UI.headerInfo.typeName: 'Freigabeliste'
@UI.headerInfo.typeNamePlural: 'Freigabeliste'
define root view entity ZAPPR_APPRLLIST_MAIL
as select from Z_I_APPROPRIATEREQUEST
association [0..1] to Z_I_APPRREQ_COSTS as Investcosts on Investcosts.Ledger = Z_I_APPROPRIATEREQUEST.Ledger
and Investcosts.ObjectNumber = Z_I_APPROPRIATEREQUEST.ObjectNumber
and Investcosts.FiscalYear = Z_I_APPROPRIATEREQUEST.FiscalYear
{
@UI.facet : [
{
id : 'AppropRequest',
purpose : #STANDARD,
type : #IDENTIFICATION_REFERENCE,
label : 'Maßnahmenanforderung',
position : 10 }
]
@UI : {
lineItem : [{position: 10} ],
identification : [{position: 10 }],
selectionField : [{position: 10}]
}
@EndUserText: { label: 'Investitionsantragsnummer'}
key Z_I_APPROPRIATEREQUEST.Appr_Request,
@UI : {
lineItem : [{position: 20}],
identification : [{ position: 20 }],
selectionField : [{position: 20}]
}
@EndUserText: { label: 'Buchungskreis'}
Z_I_APPROPRIATEREQUEST.CompanyCode,
@UI : {
lineItem : [{position: 50}],
identification : [{position: 50}],
selectionField : [{position: 50}]
}
@EndUserText: { label: 'Bezeichnung'}
Z_I_APPROPRIATEREQUEST.Txt50,
Investcosts.Currency,
@UI : {
lineItem : [{position: 60}],
identification : [{position: 60}],
selectionField : [{position: 60}] }
@EndUserText: { label: 'Geplante Kosten [aktuelles GJ] in EUR'}
@Semantics.amount.currencyCode : 'Currency'
@Aggregation.default : #SUM
Investcosts.PlancostsYear as TotalPlancostsYear,
@UI : {
// lineItem : [{position: 70}],
identification : [{position: 70}],
selectionField : [{position: 70}] }
@EndUserText: { label: 'Geplante Kosten [aktuelles GJ] in EUR '}
@Semantics.amount.currencyCode : 'Currency'
Investcosts.PlancostsYear as TotalPlancostsYear1,
Investcosts.Currency_Code,
@UI : {
lineItem : [{position: 80}],
identification : [{position: 80}],
selectionField : [{position: 80}] }
@Semantics.amount.currencyCode : 'Currency_Code'
@EndUserText: { label: 'Geplante Kosten [GESAMT] in EUR'}
@Aggregation.default : #SUM
Investcosts.PlancostsTotal as PlancostsTotalSum,
@UI : {
identification : [{position: 90}],
selectionField : [{position: 90}] }
@Semantics.amount.currencyCode : 'Currency_Code'
@EndUserText: { label: 'Geplante Kosten [GESAMT] in EUR '}
Investcosts.PlancostsTotal as PlancostsTotalSum1,
@UI : {
selectionField : [{position: 100}] }
@Consumption.filter.selectionType: #INTERVAL
Z_I_APPROPRIATEREQUEST.Erdat
}Once done create a Service Behaviour and Definition for the above CDS Entity .
managed implementation in class zbp_appr_appremail unique;
strict ( 1 );
define behavior for ZAPPR_APPREMAIL //alias <alias_name>
persistent table ZAPPR_APPREMAIL
lock master
authorization master ( instance )
{
action Email parameter Z_APPROVER3 result [1] $self;
}Abstract entity for the comments
@EndUserText.label: 'Approval 3 Input Details'
define abstract entity Z_APPROVER3
// with parameters parameter_name : parameter_type
{
comments : char255;
}Behavior Definition code:
CLASS lhc_zappr_appremail DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
IMPORTING keys REQUEST requested_authorizations FOR zappr_appremail RESULT result.
METHODS email FOR MODIFY
IMPORTING keys FOR ACTION zappr_appremail~email RESULT result.
ENDCLASS.
CLASS lhc_zappr_appremail IMPLEMENTATION.
METHOD get_instance_authorizations.
ENDMETHOD.
METHOD email.
READ ENTITIES OF zappr_appremail IN LOCAL MODE
ENTITY zappr_appremail
FIELDS ( appr_request )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_app_data).
DATA: lv_recipient TYPE adr6-smtp_addr.
DATA: lv_subject TYPE string.
DATA: lv_content TYPE string.
DATA : email_template_name TYPE smtg_tmpl_id VALUE 'ZEMAIL_APPROVAL'.
DATA(email_api) =
cl_smtg_email_api=>get_instance(
iv_template_id = email_template_name ).
DATA(lv_html_text) = VALUE string( ).
DATA(i_cds_key) = VALUE if_smtg_email_template=>ty_gt_data_key( ).
" Get the RawHTML Content and Replace the palceholder with the INV Details in the email
email_api->render( EXPORTING
iv_language = sy-langu
it_data_key = i_cds_key
IMPORTING
ev_subject = DATA(lv_subject_tmp)
ev_body_html = DATA(lv_body_html) ).
LOOP AT keys INTO DATA(appdetails) .
lv_content = |{ appdetails-%param-comments }|.
exit.
endloop.
REPLACE 'ZCOMMENTS' WITH lv_content INTO lv_body_html.
lv_subject = lv_subject_tmp.
DATA(lv_body_html_soli) = cl_bcs_convert=>string_to_soli( lv_body_html ). " Build HTML for Sending
DATA(lo_multipart_ref) = NEW cl_gbt_multirelated_service( ).
lo_multipart_ref->set_main_html(
EXPORTING
content = lv_body_html_soli
description = '' ).
TRY.
DATA(o_document) = cl_document_bcs=>create_document( i_type = 'HTM'
i_text = lv_body_html_soli
i_subject = CONV so_obj_des( lv_subject ) ).
* Sendrequest erzeugen
DATA(o_send_request) = cl_bcs=>create_persistent( ).
* Email-Subject festlegen, ip_subject ist vom Typ String
o_send_request->set_message_subject( ip_subject = lv_subject ).
* Die Mail an den Sendrequest hängen
o_send_request->set_document( o_document ).
o_send_request->set_sender( o_sender ).
DATA o_recipient TYPE REF TO cl_cam_address_bcs .
LOOP AT recipients ASSIGNING FIELD-SYMBOL(<fs_email_recipient>).
o_send_request->add_recipient( i_recipient = cl_cam_address_bcs=>create_internet_address( <fs_email_recipient>-email ) i_express = abap_true ).
ENDLOOP.
* Sofort senden
o_send_request->set_send_immediately( abap_true ).
* Dokument senden
IF o_send_request->send( i_with_error_screen = abap_true ) = abap_true.
ELSE.
ENDIF.
CATCH cx_root INTO DATA(e_text).
ENDTRY.
ENDMETHOD.
ENDCLASS.Now create CDS Entity with Annotation for action to Trigger transactional actions (e.g., sending approval emails) directly from the report
@EndUserText.label: 'Bericht zur Freigabe der Maßnahmenanforderungen'
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
serviceQuality: #X,
sizeCategory: #S,
dataClass: #MIXED
}
@UI.headerInfo.typeName: 'Freigabeliste'
@UI.headerInfo.typeNamePlural: 'Freigabeliste'
define root view entity ZAPPR_APPREMAIL
as select from Z_I_APPROPRIATEREQUEST
association [0..1] to Z_I_APPRREQ_COSTS as Investcosts on Investcosts.Ledger = Z_I_APPROPRIATEREQUEST.Ledger
and Investcosts.ObjectNumber = Z_I_APPROPRIATEREQUEST.ObjectNumber
and Investcosts.FiscalYear = Z_I_APPROPRIATEREQUEST.FiscalYear
association [0..1] to Z_I_APPRREQ_STATUS as ApprStatus on ApprStatus.Objnr = Z_I_APPROPRIATEREQUEST.IQOBJNR
and ApprStatus.spras = $session.system_language
{
@UI.facet : [
{
id : 'AppropRequest',
purpose : #STANDARD,
type : #IDENTIFICATION_REFERENCE,
label : 'Maßnahmenanforderung',
position : 10 }
]
@UI : {
lineItem : [{position: 10} ,{ type: #FOR_ACTION , dataAction: 'Email' ,label: 'FraigabeListe Email Senden' , invocationGrouping: #CHANGE_SET
}],
identification : [{position: 10 }],
selectionField : [{position: 10}]
}
@EndUserText: { label: 'Investitionsantragsnummer'}
key Z_I_APPROPRIATEREQUEST.Appr_Request,
@UI : {
lineItem : [{position: 20}],
identification : [{ position: 20 }],
selectionField : [{position: 20}]
}
@EndUserText: { label: 'Buchungskreis'}
Z_I_APPROPRIATEREQUEST.CompanyCode,
@UI : {
lineItem : [{position: 30}],
identification : [{position: 50}],
selectionField : [{position: 50}]
}
@EndUserText: { label: 'Bezeichnung'}
Z_I_APPROPRIATEREQUEST.Txt50,
@UI : {
lineItem : [{position: 40}],
identification : [{position: 40}],
selectionField : [{position: 40}] }
@EndUserText: { label: 'Status'}
ApprStatus.txt30 as Statustext,
Investcosts.Currency,
@UI : {
lineItem : [{position: 50}],
identification : [{position: 50}],
selectionField : [{position: 50}] }
@EndUserText: { label: 'Geplante Kosten [aktuelles GJ] in EUR'}
@Semantics.amount.currencyCode : 'Currency'
// @DefaultAggregation: #SUM
Investcosts.PlancostsYear as TotalPlancostsYear,
Investcosts.Currency_Code,
@UI : {
lineItem : [{position: 60}],
identification : [{position: 60}],
selectionField : [{position: 60}] }
@Semantics.amount.currencyCode : 'Currency_Code'
@EndUserText: { label: 'Geplante Kosten [GESAMT] in EUR'}
// @Aggregation.default : #SUM
Investcosts.PlancostsTotal as PlancostsTotalSum,
@UI : {
selectionField : [{position: 70}] }
Z_I_APPROPRIATEREQUEST.Erdat
}Now genrate the Service definition and service
Using the guided development add a action as shwon below .
<core:FragmentDefinition xmlns:core='sap.ui.core'
xmlns='sap.m'>
<Dialog class="sapUiSizeCompact"
xmlns="sap.m"
xmlns:ca="sap.ca.ui"
xmlns:layout="sap.ui.layout"
xmlns:form="sap.ui.layout.form"
xmlns:core="sap.ui.core"
xmlns:unified="sap.ui.unified" title="{i18n>remarks}" contentWidth="200px" id="idremarks">
<content >
<form:SimpleForm columnsL="1" columnsM="1" maxContainerCols="1" layout="ResponsiveGridLayout" id="formexlstat" editable="true">
<form:content >
<Label id="remarkslabel" text="{i18n>remarks}"/>
<Input id="idremarksinput" type="Text"/>
</form:content>
</form:SimpleForm>
</content>
<beginButton >
<Button text="{i18n>idEmail}" press="._onSend" id="idEmail"/>
</beginButton>
<endButton>
<Button text="{i18n>Cancel}" press="._onCancel" id="idcancelbtn"/>
</endButton>
</Dialog>
</core:FragmentDefinition>Handle the action using the below controller code
sap.ui.define([
"sap/m/MessageToast"
], function(MessageToast) {
'use strict';
return {
onInit: function(){
this.loadDialogFragment();
this.AnalyticalTable = this.getView().byId('apprsendemail::sap.suite.ui.generic.template.ListReport.view.ListReport::apprListEmail--analyticalTable')
//this.getView(this.getView().getContent()[0].getContent().getItems()[0].getId())
},
_onCancel: function(oEvent){
var oModel = this.getView().getModel();
this._oDialog.close();
if (this._oDialog) {
this._oDialog.destroy();
this._oDialog = null; // Important to nullify the reference
}
},
_onSend: function (oEvent) {
//var oInput = this.byId("inputParam");
// var sValue = oInput.getValue();
// Get selected items from the Analytical List Table
var oTable = this.getView().byId("apprsendemail::sap.suite.ui.generic.template.ListReport.view.ListReport::apprListEmail--analyticalTable"); // Replace with your table ID
var aSelectedIndices = oTable._getSelectionPlugin().getSelectedIndices();
var aSelectedItems = [];
aSelectedIndices.forEach(function (iIndex) {
var oContext = oTable.getContextByIndex(iIndex);
aSelectedItems.push(oContext.getObject());
});
// Call the backend action with the selected items and parameter
this._callBackendAction(aSelectedItems);
oTable.getModel().refresh(true);
// Close the dialog
this._oDialog.close();
if (this._oDialog) {
this._oDialog.destroy();
this._oDialog = null; // Important to nullify the reference
}
},
sendEmailList: function(oEvent) {
// MessageToast.show("Custom handler invoked.");
if (!this._oDialog) {
this._oDialog = sap.ui.xmlfragment("apprsendemail.ext.fragment.EmailListDialog", this);
this.getView().addDependent(this._oDialog);
}
this._oDialog.open();
},
loadDialogFragment: function(){
if (!this.ReleaseListDialog) {
this.oDialog = this.loadFragment({
name: "apprsendemail.ext.fragment.EmailListDialog",
controller: this
});
}
},
_callBackendAction: async function (aSelectedItems) {
var oModel = this.getOwnerComponent().getModel("emailAction");
var sPath = "/Email"; // Adjust the path as needed
var mParameters = {
batchGroupId: "myChangeset",
changeSetId: "myChangeset"
};
var comments = sap.ui.getCore().byId("idremarksinput").getValue();
// Example of calling the backend for each selected item
aSelectedItems.forEach(function (oItem) {
oModel.callFunction(sPath, {
method: "POST",
mParameters: mParameters,
urlParameters: {
"Appr_Request": oItem.Appr_Request,
"comments" : comments,
},
});
});
//Submitting the function import batch call
oModel.submitChanges({
batchGroupId: "myChangeset", //Same as the batch group id used previously
success: function (oData) {
// var oTable = this.getView().byId("releaseapprreq::sap.suite.ui.generic.template.ListReport.view.ListReport::appropriatelist--analyticalTable");
// oTable._getSelectionPlugin().getModel().refresh(true);
// Handle success
MessageToast.show("E-Mail erfolgreich gesendet");
}.bind(this),
error: function (oError) {
// Handle error
MessageToast.show("E-Mail wurde nicht erfolgreich gesendet");
}
});
},
};
});
Once done deploy the app and this will be available in the Workzone and you see the app showing totals and also actions as shown below :
The approach outlined in this guide demonstrates how to:
This design not only enhances user productivity but also aligns with SAP’s clean core principles, ensuring scalability, maintainability, and future readiness of your S/4HANA applications.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.