ABAP RESTful Application Programming Model(ABAP RAP) allows you to build a power report application whether in S/4 HANA or in Business Technology Platform. This model allows you to add features such as transactional behavior, visualization(bar and charts), user action based event, etc.

ABAP RAP comes with two flavors when it comes to data access. The more common way is to model CDS interface view on top of database table and let CDS consumption view and OData consume the data from database table directly. This way you don’t have to code an ABAP OPEN SQL to fetch the data.

The other way is to use ABAP RAP custom entity. This approach do not require you to build the CDS consumption view on top of CDS interface view and database table. Instead, a CDS custom entity view is built for OData consumption and this view will use ABAP class to fetch the data returned to OData.

The Setup

This blog will demonstrate how to build a ABAP RAP custom entity app and display multiple facets in object page. The list page will show all the sales documents and the drilled down object page will contain a facet with line item data and schedule line items.

1. Create a root custom entity

Go to ABAP Development Tool(ADT) and create a CDS data definition. This is a root CDS that contains the base of your RAP data models. In our case, we are going to display sales document data so the root custom entity will contain field from VBAK(Sales Order Header).

@EndUserText.label: 'Custom Entity Sales Order'
@ObjectModel: {
    query: {
implementedBy: 'ABAP:ZCL_SALESDOC'
    }
}

@UI: {
  headerInfo: {
    typeName: 'Sales Document',
    typeNamePlural: 'Sales Documents',
    title: { value: 'VBELN' }}
}
define root custom entity ZSALESDOC_C
{
      @UI.facet : [ { id: 'SalesDoc',
                      purpose: #STANDARD,
                      type  : #IDENTIFICATION_REFERENCE,
                      label : 'Sales Document',
                      position: 10 },
                    { id    :            'LineItem',
                      purpose  :       #STANDARD,
                      type  :          #LINEITEM_REFERENCE,
                      label :         'Sales Doc Item',
                      position :      20,
                      targetElement: '_LineItem'},
                    { id    :            'ScheLine',
                      purpose  :       #STANDARD,
                      type  :          #LINEITEM_REFERENCE,
                      label :         'Schedule Line Item',
                      position :      30,
                      targetElement: '_ScheLine'}                      
                      ]
      @UI.lineItem          : [{ position: 10 }]
      @UI.selectionField    : [{position: 10}]
      @UI.identification    : [{position: 10}]
  key VBELN     : vbeln_va;
      @UI.lineItem          : [{ position: 20 }]
      @UI.identification    : [{position: 20}]
      KUNNR     : kunag;
      @UI.lineItem          : [{ position: 30 }]
      @UI.identification    : [{position: 30}]
      AUART     : auart;
      @UI.lineItem          : [{ position: 40 }]
      @UI.identification    : [{position: 40}]
      ERDAT     : erdat;      

      //Associations
      _LineItem : composition [0..*] of ZSALESITEM_C;
      _ScheLine : composition [0..*] of ZSALESSCHEDULE_C;      
}

To break down the code, ZCL_SALESDOC is the ABAP class this CDS trigger to fetch data from database. As you can see, the custom entity is defined without specifying SELECT xx FROM statement because it depends on the ABAP class to do it.

@ObjectModel: {
    query: {
implementedBy: 'ABAP:ZCL_SALESDOC'
    }
}

Now to the facet annotation, this is definition of how the object page will look like. ‘SalesDoc’ that is placed at position 10 is the data of the root custom view itself, showing the data fetched from ZCL_SALESDOC. ‘LineItem’ and ‘ScheLine’ that are placed on 20 and 30 are data coming from other CDS views. You can see that they are using target element ‘_LineItem’ and ‘_ScheLine’, which we will associate with the root custom entity in the next few steps.

      @UI.facet : [ { id: 'SalesDoc',
                      purpose: #STANDARD,
                      type  : #IDENTIFICATION_REFERENCE,
                      label : 'Sales Document',
                      position: 10 },
                    { id    :            'LineItem',
                      purpose  :       #STANDARD,
                      type  :          #LINEITEM_REFERENCE,
                      label :         'Sales Doc Item',
                      position :      20,
                      targetElement: '_LineItem'},
                    { id    :            'ScheLine',
                      purpose  :       #STANDARD,
                      type  :          #LINEITEM_REFERENCE,
                      label :         'Schedule Line Item',
                      position :      30,
                      targetElement: '_ScheLine'}                      
                      ]

This part is just defining what fields should to be displayed in the list page of RAP application. They are the fields ABAP class ZCL_SALESDOC needs to fetch.

      @UI.lineItem          : [{ position: 10 }]
      @UI.selectionField    : [{position: 10}]
      @UI.identification    : [{position: 10}]
  key VBELN     : vbeln_va;
      @UI.lineItem          : [{ position: 20 }]
      @UI.identification    : [{position: 20}]
      KUNNR     : kunag;
      @UI.lineItem          : [{ position: 30 }]
      @UI.identification    : [{position: 30}]
      AUART     : auart;
      @UI.lineItem          : [{ position: 40 }]
      @UI.identification    : [{position: 40}]
      ERDAT     : erdat;    

Finanlly the associations with Line Item data and Schedule line data. To fetch these data, it’s associating ZSALESITEM_C and ZSALESSCHEDULE_C which we need to create in the next steps.

      //Associations
      _LineItem : composition [0..*] of ZSALESITEM_C;
      _ScheLine : composition [0..*] of ZSALESSCHEDULE_C;  

Save but do not activate as ZSALESITEM_C and ZSALESSCHEDULE_C have not been created yet.

2. Create custom entities for Sales Item and Schedule Line

Create below CDS custom entity for Sales Line Item. It’s got the same structure as the root custom entity but a few things to note:

  • A seperate ABAP class ZCL_SALESITEMS is used
  • ‘association to parent’ is used to create parent-child relation between ZSALESDOC_C and ZSALESITEM_C
@EndUserText.label: 'Custom Entity Sales Order Items'
@ObjectModel: {
    query: {
implementedBy: 'ABAP:ZCL_SALESITEMS'
    }
}

@UI: {
  headerInfo: {
    typeName: 'Item',
    typeNamePlural: 'Items',
    title: { value: 'POSNR' }}
}
define custom entity ZSALESITEM_C
{
      @UI.facet : [ { id:            'LineItem',
                     purpose   :       #STANDARD,
                     type      :          #IDENTIFICATION_REFERENCE,
                     label     :         'Sales Items',
                     position  :      10 } ]
      @UI.hidden: true
  key vbeln     : vbeln_va;
      @UI.lineItem          : [{ position: 10 }]
      @UI.identification    : [{position: 10}]
  key posnr     : posnr_va;
      @UI.lineItem          : [{ position: 20 }]
      @UI.identification    : [{position: 20}]
      matnr     : matnr;
      @UI.lineItem          : [{ position: 30 }]
      @UI.identification    : [{position: 30}]
      @Semantics: { amount : {currencyCode: 'waerk'} }
      netwr     : netwr_ap;
      @UI.lineItem          : [{ position: 40 }]
      @UI.identification    : [{position: 40}]
      waerk     : waerk;

      _SalesDoc : association to parent ZSALESDOC_C on  $projection.vbeln = _SalesDoc.VBELN;
}

Create below CDS custom entity for Schedule Line and is following the same principle of ZSALESITEM_C.

@EndUserText.label: 'Custom Entity Sales Order Schedule'
@ObjectModel: {
    query: {
implementedBy: 'ABAP:ZCL_SALESSCHEDULE'
    }
}

@UI: {
  headerInfo: {
    typeName: 'Schedule Line Item',
    typeNamePlural: 'Schedule Line Items',
    title: { value: 'etenr' }}
}
define custom entity ZSALESSCHEDULE_C
{
      @UI.facet : [ { id:            'LineItem',
                     purpose   :       #STANDARD,
                     type      :          #IDENTIFICATION_REFERENCE,
                     label     :         'Schedule Line Items',
                     position  :      10 } ]
      @UI.hidden: true
  key vbeln     : vbeln_va;
      @UI.lineItem          : [{ position: 10 }]
      @UI.identification    : [{position: 10}]
  key posnr     : posnr_va;
      @UI.lineItem          : [{ position: 20 }]
      @UI.identification    : [{position: 20}]
  key etenr     : etenr;
      @UI.lineItem          : [{ position: 30 }]
      @UI.identification    : [{position: 30}]
      edatu     : edatu;
      @UI.lineItem          : [{ position: 40 }]
      @UI.identification    : [{position: 40}]
      @Semantics: { quantity : {unitOfMeasure: 'vrkme' } }
      wmeng     : wmeng;
      @UI.lineItem          : [{ position: 50 }]
      @UI.identification    : [{position: 50}]
      @Semantics: { quantity : {unitOfMeasure: 'vrkme' } }
      bmeng     : bmeng;
      @UI.lineItem          : [{ position: 60 }]
      @UI.identification    : [{position: 60}]
      vrkme     : vrkme;

      _SalesDoc : association to parent ZSALESDOC_C on  $projection.vbeln = _SalesDoc.VBELN;
}

Save and activate all three CDS views(ZSALESDOC_C, ZSALESITEM_C, ZSALESSCHEDULE_C) together.

3. Service Definition, Service Biding and preview data

Create Service definition as below and make sure to expose all three CDS custom entities.

@EndUserText.label: 'Service definition for ZSALESDOC_C'
define service ZSALESDOC_SD {
  expose ZSALESDOC_C      as SalesDoc;
  expose ZSALESITEM_C     as SalesItem;
  expose ZSALESSCHEDULE_C as SalesScheduleItem;
}

Create a service biding from the service definition with ODataV4. Publish the service. Click on the alesDoc entity set and you should be able to preview the app.

At this point, there is no data displayed on the app preview, as we have not implemented the logic to fetch the data. In the next step, we will add that logic to ABAP class in respective custom entities.

4. Implement logic to fetch data

Let’s implement the ABAP class to fetch data. Starting with ZCL_SALESDOC, copy paste below code:

CLASS zcl_salesdoc DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
    INTERFACES if_rap_query_provider.
    TYPES:
      BEGIN OF ty_range_option,
        sign   TYPE c LENGTH 1,
        option TYPE c LENGTH 2,
        low    TYPE string,
        high   TYPE string,
      END OF ty_range_option,
      tt_range_option TYPE STANDARD TABLE OF ty_range_option WITH EMPTY KEY.
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS zcl_salesdoc IMPLEMENTATION.
  METHOD if_rap_query_provider~select.

    DATA: lt_r_vbeln TYPE tt_range_option,
          lv_count   TYPE int8.

    IF io_request->is_data_requested( ).

      DATA(lv_parameter)     = io_request->get_parameters( ).

      DATA(lv_top)     = io_request->get_paging( )->get_page_size( ).
      IF lv_top < 0.
        lv_top = 1.
      ENDIF.

      DATA(lv_skip)    = io_request->get_paging( )->get_offset( ).

      DATA(lt_sort)    = io_request->get_sort_elements( ).

      DATA : lv_orderby TYPE string.
      LOOP AT lt_sort INTO DATA(ls_sort).
        IF ls_sort-descending = abap_true.
          lv_orderby = |'{ lv_orderby } { ls_sort-element_name } DESCENDING '|.
        ELSE.
          lv_orderby = |'{ lv_orderby } { ls_sort-element_name } ASCENDING '|.
        ENDIF.
      ENDLOOP.
      IF lv_orderby IS INITIAL.
        lv_orderby = 'vbak~vbeln'.
      ENDIF.
*
*          DATA(lv_conditions) =  io_request->get_filter( )->get_as_sql_string( ).
      DATA(lt_conditions_range) =  io_request->get_filter( )->get_as_ranges( ).

      IF line_exists( lt_conditions_range[ name = 'VBELN' ] ).
        lt_r_vbeln  = lt_conditions_range[ name = 'VBELN' ]-range.
      ENDIF.

      SELECT DISTINCT
             vbak~vbeln,
             vbak~erdat,
             vbak~auart,
             vbak~kunnr
        FROM vbak
       INNER JOIN kna1
          ON vbak~kunnr = kna1~kunnr
       INNER JOIN vbap
          ON vbap~vbeln = vbak~vbeln
       WHERE vbak~vbeln IN @lt_r_vbeln
       ORDER BY (lv_orderby)
       INTO TABLE @DATA(lt_vbak)
       UP TO @lv_top ROWS OFFSET @lv_skip.

      "Get the total number of records
      SELECT DISTINCT
             vbak~vbeln,
             vbak~erdat,
             vbak~auart,
             vbak~kunnr
        FROM vbak
       INNER JOIN kna1
          ON vbak~kunnr = kna1~kunnr
       INNER JOIN vbap
          ON vbap~vbeln = vbak~vbeln
       WHERE vbak~vbeln IN @lt_r_vbeln
       ORDER BY (lv_orderby)
       INTO TABLE @DATA(lt_vbak_count).

      lv_count = lines( lt_vbak_count ).

      io_response->set_total_number_of_records( lv_count ). "Set the max total count of rows expected in the list report.
      io_response->set_data( lt_vbak ).

    ENDIF.
  ENDMETHOD.

ENDCLASS.

To break down the code:

  • if_rap_query_provider is the interface the RAP uses and the method Select is triggered the when OData service access the CDS custom entity. So when the Go button is pressed in the app preview, Select method is called therefore all the logic to fetch data must be implemented in the Select method.
  • The RAP list report cannot display all the data into one page, as there is limit to the browser screen. So it will set a certain limit to how many records to be shown. Lets say that limit is 30 records per page. Now how the RAP list report framework works is that when user scrolls down, the Odata service will be triggered again and OData service must return the next 30 records and the app will display these so user can see the next batch of records as user scroll down(and the next next 30 records if user scroll twice). So to achieve this, we must fetch the records in such a way. To archive this, we must get that paging limit and how many scrolls user has done. io_request->get_paging( )->get_page_size( ) in the Select method returns that paging limit on how many records the app will display in one page. io_request->get_paging( )->get_offset( ) returns the offset number, which means how many records user has skipped by scrolling down the list. With these information, we can perform ABAP Open SQL and get the records that we need, by using ‘UP TO @lv_top ROWS OFFSET @lv_skip’.
  • The filter information is fetched with io_request->get_filter( ). In our above example, we have set Sale Document Number(VBELN) as the selection filter. So with io_request->get_filter( ), the method can know what the user input is for that filter.
  • Open SQL is done twice. One for selecting only the rows that the page needs by using ‘UP TO @lv_top ROWS OFFSET @lv_skip’, and other one without this restriction. This is so that app knows the total number of rows the list should display(so that the user will have the last page after scrolling the list). The data for that page is returned with io_response->set_data( ) and the total count is returned with io_response->set_total_number_of_records( ).

Having understood how the ABAP class works, copy paste the code for ZCL_SALESITEMS and ZCL_SALESSCHEDULE.

CLASS zcl_salesitems DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
    INTERFACES if_rap_query_provider.
    TYPES:
      BEGIN OF ty_range_option,
        sign   TYPE c LENGTH 1,
        option TYPE c LENGTH 2,
        low    TYPE string,
        high   TYPE string,
      END OF ty_range_option,
      tt_range_option TYPE STANDARD TABLE OF ty_range_option WITH EMPTY KEY.
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.



CLASS zcl_salesitems IMPLEMENTATION.
  METHOD if_rap_query_provider~select.

    DATA: lt_r_vbeln TYPE tt_range_option,
          lv_count   TYPE int8.

    IF io_request->is_data_requested( ).

      DATA(lv_parameter)     = io_request->get_parameters( ).

      DATA(lv_top)     = io_request->get_paging( )->get_page_size( ).
      IF lv_top < 0.
        lv_top = 1.
      ENDIF.

      DATA(lv_skip)    = io_request->get_paging( )->get_offset( ).

      DATA(lt_sort)    = io_request->get_sort_elements( ).

      DATA : lv_orderby TYPE string.
      LOOP AT lt_sort INTO DATA(ls_sort).
        IF ls_sort-descending = abap_true.
          lv_orderby = |'{ lv_orderby } { ls_sort-element_name } DESCENDING '|.
        ELSE.
          lv_orderby = |'{ lv_orderby } { ls_sort-element_name } ASCENDING '|.
        ENDIF.
      ENDLOOP.
      IF lv_orderby IS INITIAL.
        lv_orderby = 'vbap~vbeln, vbap~posnr'.
      ENDIF.
*
*          DATA(lv_conditions) =  io_request->get_filter( )->get_as_sql_string( ).
      DATA(lt_conditions_range) =  io_request->get_filter( )->get_as_ranges( ).

      IF line_exists( lt_conditions_range[ name = 'VBELN' ] ).
        lt_r_vbeln  = lt_conditions_range[ name = 'VBELN' ]-range.
      ENDIF.

      SELECT
        FROM vbap
       INNER JOIN vbak
          ON vbap~vbeln = vbak~vbeln
      FIELDS vbap~vbeln,
             vbap~posnr,
             vbak~kunnr,
             vbap~matnr,
             vbap~netwr,
             vbap~waerk
       WHERE vbap~vbeln IN @lt_r_vbeln
       ORDER BY (lv_orderby)
       INTO TABLE @DATA(lt_vbap)
       UP TO @lv_top ROWS OFFSET @lv_skip.

      "Get the total number of records
      SELECT
        FROM vbap
       INNER JOIN vbak
          ON vbap~vbeln = vbak~vbeln
      FIELDS vbap~vbeln,
             vbap~posnr,
             vbak~kunnr,
             vbap~matnr,
             vbap~netwr,
             vbap~waerk
       WHERE vbap~vbeln IN @lt_r_vbeln
       ORDER BY (lv_orderby)
       INTO TABLE @DATA(lt_vbap_count).

      IF io_request->is_data_requested( ).
        io_response->set_data( lt_vbap ).
      ENDIF.

      IF io_request->is_total_numb_of_rec_requested(  ).
        io_response->set_total_number_of_records( lines( lt_vbap_count ) ).
      ENDIF.

    ENDIF.
  ENDMETHOD.

ENDCLASS.

CLASS zcl_salesschedule DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
    INTERFACES if_rap_query_provider.
    TYPES:
      BEGIN OF ty_range_option,
        sign   TYPE c LENGTH 1,
        option TYPE c LENGTH 2,
        low    TYPE string,
        high   TYPE string,
      END OF ty_range_option,
      tt_range_option TYPE STANDARD TABLE OF ty_range_option WITH EMPTY KEY.
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.



CLASS zcl_salesschedule IMPLEMENTATION.
  METHOD if_rap_query_provider~select.

    DATA: lt_r_vbeln TYPE tt_range_option,
          lv_count   TYPE int8.

    IF io_request->is_data_requested( ).

      DATA(lv_parameter)     = io_request->get_parameters( ).

      DATA(lv_top)     = io_request->get_paging( )->get_page_size( ).
      IF lv_top < 0.
        lv_top = 1.
      ENDIF.

      DATA(lv_skip)    = io_request->get_paging( )->get_offset( ).

      DATA(lt_sort)    = io_request->get_sort_elements( ).

      DATA : lv_orderby TYPE string.
      LOOP AT lt_sort INTO DATA(ls_sort).
        IF ls_sort-descending = abap_true.
          lv_orderby = |'{ lv_orderby } { ls_sort-element_name } DESCENDING '|.
        ELSE.
          lv_orderby = |'{ lv_orderby } { ls_sort-element_name } ASCENDING '|.
        ENDIF.
      ENDLOOP.
      IF lv_orderby IS INITIAL.
        lv_orderby = 'vbap~vbeln, vbap~posnr, vbep~etenr'.
      ENDIF.
*
*          DATA(lv_conditions) =  io_request->get_filter( )->get_as_sql_string( ).
      DATA(lt_conditions_range) =  io_request->get_filter( )->get_as_ranges( ).

      IF line_exists( lt_conditions_range[ name = 'VBELN' ] ).
        lt_r_vbeln  = lt_conditions_range[ name = 'VBELN' ]-range.
      ENDIF.

      SELECT
        FROM vbap
       INNER JOIN vbep
          ON vbap~vbeln = vbep~vbeln
         AND vbap~posnr = vbep~posnr
      FIELDS vbap~vbeln,
             vbap~posnr,
             vbep~etenr,
             vbep~edatu,
             vbep~wmeng,
             vbep~bmeng,
             vbep~vrkme
       WHERE vbap~vbeln IN @lt_r_vbeln
       ORDER BY (lv_orderby)
       INTO TABLE @DATA(lt_vbep)
       UP TO @lv_top ROWS OFFSET @lv_skip.

      "Get the total number of records
      SELECT
        FROM vbap
       INNER JOIN vbep
          ON vbap~vbeln = vbep~vbeln
         AND vbap~posnr = vbep~posnr
      FIELDS vbap~vbeln,
             vbap~posnr,
             vbep~etenr,
             vbep~edatu,
             vbep~wmeng,
             vbep~bmeng,
             vbep~vrkme
       WHERE vbap~vbeln IN @lt_r_vbeln
       ORDER BY (lv_orderby)
       INTO TABLE @DATA(lt_vbep_count).

      IF io_request->is_data_requested( ).
        io_response->set_data( lt_vbep ).
      ENDIF.

      IF io_request->is_total_numb_of_rec_requested(  ).
        io_response->set_total_number_of_records( lines( lt_vbep_count ) ).
      ENDIF.

    ENDIF.
  ENDMETHOD.

ENDCLASS.

Activate all three ABAP classes and preview the app again. You should be able to see the data on the list page.

Go into any rows and the app navigate to the object page. There you should be able to see the Line item data and Schedule line data in tables.

What’s next?

Make sure to check out the other blogs related to ABAP RAP application.

Passing filter to object page table – ABAP RAP Custom Entity – SAP Extensibility 101