ABAP Blog Posts
cancel
Showing results for 
Search instead for 
Did you mean: 
VijayCR
Active Contributor
2,132

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

    • Analytical Service: Built using a CDS view with @analytics.query: true, enabling features like grouping, subtotals, and totals.
    • Transactional Service: Developed using RAP with behavior definitions to support actions, determinations, and validations. This view excludes analytical annotations to maintain RAP compatibility.
  • UI5 Extension Logic for Action Integration

    • Use SAPUI5 extensions to wire up action buttons in the Fiori List Report.
    • These buttons trigger transactional logic from the RAP-based service, even though the data is displayed via the analytical service.
  • Workaround for Filtering on Aggregated Fields

    • Since analytical views often aggregate fields (e.g., total amounts), filtering directly on these fields may not work as expected.
    • Introduce dummy fields or helper logic to enable meaningful filtering in the UI.

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:

  • Mass approvals with KPI-based filtering
  • Sales performance analysis with direct follow-up actions
  • Inventory grouping with replenishment triggers

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.

  • Analytical CDS View (OData V2 or V4)

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 records
  • Z_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

}

 Consumption CDS View

  • This consumption view is designed to support a Fiori List Report that displays investment request data with:

    • Aggregated planned costs per fiscal year and overall
    • Currency semantics for proper formatting
    • Additional Filters for amounts as the original ones does not support in analytical views.
@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 .

  • CDS entity with Action Service
    RAP-Based CDS View with Behavior Definition with Action
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 

Create the Fiori List Report App

In SAP Business Application Studio:

    • Create a new project from template 
    • Select List Report Object Page  with analytical table and select the Odata entity generated from the aggregated Service
    • In the Manifest.json add additional Odata Service or use guided development  

VijayCR_0-1760567958925.png

 

 

 

 

 

 

Using the guided development  add a action as shwon below .

VijayCR_1-1760568663616.png

<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 :

VijayCR_2-1760569240360.png

VijayCR_3-1760569271440.png

The approach outlined in this guide demonstrates how to:

  • Leverage CDS view annotations for grouping, subtotals, and totals
  • Integrate status tracking and multilingual support
  • Enable transactional actions like sending approval emails via RAP behavior
  • Maintain a clean architecture by decoupling analytics and actions at the service level and integrating them at the UI layer

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.

 

 

 

3 Comments
junwu
SAP Champion
SAP Champion
0 Kudos

that is not embedded analytics

VijayCR
Active Contributor
0 Kudos

I have update it 

 

junwu
SAP Champion
SAP Champion

that can be barely called analytical.....

Top kudoed authors