Technology Blog Posts by Members
cancel
Showing results for 
Search instead for 
Did you mean: 
sharathtm
Explorer
873

Introduction

In the ABAP RESTful Application Programming Model (RAP), virtual elements are essential for displaying computed data that doesn't exist in the database—such as deriving a month or year from a timestamp. However, because these elements are calculated at runtime, they lack standard OData capabilities like server-side sorting and filtering. In this article, we will explore how to overcome these limitations by implementing a SADL exit. We’ll walk through how to enable full sorting and calculation capabilities for virtual elements, ensuring a seamless experience for the end user.

What are Virtual Elements in RAP?

In the ABAP RESTful Application Programming Model (RAP), a virtual element is a field defined in a CDS (Core Data Services) projection view that does not have a corresponding column in the underlying database table.

Real-Time Business Scenario: Sales Period Reporting

In many SAP implementations, the business requires reports segmented by Month and Year (e.g., for Monthly Sales Performance). While the underlying database table stores a precise (Created On) date, it does not store the Month or Year as separate columns.

The Problem

If we create a Fiori Elements app and want to show "Sales Month" and "Sales Year" as separate columns:

  1. Simple Virtual Elements can calculate these strings (e.g., "January", "2024").

  2. The Failure: When a user tries to sort the "Sales Month" column, the UI will throw an error or do nothing because the OData service doesn't know how to sort a field that doesn't exist in the database.

The Solution

By using a SADL Exit, we "map" the virtual Month/Year fields back to the physical Sales date field. This allows the database to perform the sort on the actual date, while the user sees the sorted Month or Year on their screen.

Implementation Steps

Step 1: Create a database table

Define the physical storage. This is where the raw data lives before any calculations happen.

@EndUserText.label : 'Sales table'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zsales_table {

  key client      : abap.clnt not null;
  key sales_uuid  : sysuuid_x16 not null;
  sales_id        : abap.char(10);
  sales_date      : abap.dats;
  @Semantics.amount.currencyCode : 'zsales_table.currency'
  total_amount    : abap.curr(15,2);
  currency        : abap.cuky;
  last_changed_at : timestampl;

}

Step 2: Create a interface view on top of database table.

This layer projects the database fields into a clean, business-oriented structure.

@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Interface view for sales table'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
    serviceQuality: #X,
    sizeCategory: #S,
    dataClass: #MIXED
}
define root view entity zi_sales_table
  as select from zsales_table
{
  key sales_uuid      as SalesUuid,
      sales_id        as SalesId,
      sales_date      as SalesDate,
      @Semantics.amount. currencyCode: 'Currency'
      total_amount    as TotalAmount,
      currency        as Currency,
      last_changed_at as LastChangedAt
}

 Step 3: Create a Consumption view

This is where the Virtual Elements are defined. We use annotations to link the UI to the specific ABAP logic for calculation and sorting.

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Projection view for sales table'
@Metadata.ignorePropagatedAnnotations: true

define root view entity ZP_SALESPROJ
  provider contract transactional_query
  as projection on zi_sales_table
{
      .facet: [{ type: #IDENTIFICATION_REFERENCE,
                    position: 10,
                    label: 'Salestable',
                    targetQualifier: 'GENERAL',
                    purpose: #STANDARD }]
                    
      .lineItem: [{ position: 10, label: 'Sales Uuid' }]
  key SalesUuid,

      .lineItem: [{ position: 20, label: 'Sales Id' }]
      .identification: [{ position: 20, label: 'Sales Id' }]
      SalesId,

      .lineItem: [{ position: 30, label: 'Sales order date' }]
      .identification: [{ position: 30, label: 'Sales order date', qualifier: 'GENERAL' }]
      SalesDate,

      .lineItem: [{ position: 40, label: 'Sales order year' }]
      .identification: [{ position: 40, label: 'Sales order year', qualifier: 'GENERAL' }]
      @ObjectModel.virtualElement: true
      @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_SALES_VE_HANDLER'
      @ObjectModel.sort.transformedBy: 'ABAP:ZCL_SALES_VE_HANDLER'
  virtual SalesYear  : abap.char(4),

      .lineItem: [{ position: 50, label: 'Sales order month' }]
      .identification: [{ position: 50, label: 'Sales order month' , qualifier: 'GENERAL' }]
      @ObjectModel.virtualElement: true
      @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_SALES_VE_HANDLER'
      @ObjectModel.sort.transformedBy: 'ABAP:ZCL_SALES_VE_HANDLER'
  virtual SalesMonth : abap.char(2),

      .lineItem: [{ position: 60, label: 'Total amount' }]
      .identification: [{ position: 60, label: 'Total amount' }]
      @Semantics.amount.currencyCode: 'Currency'
      TotalAmount,

      Currency
}

Step 5: The Handler Class Logic

This class implements two critical interfaces: if_sadl_exit_calc_element_read for the calculation and if_sadl_exit_sort_transform  to enable sorting.

CLASS zcl_sales_ve_handler DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    INTERFACES if_sadl_exit_calc_element_read.
    INTERFACES if_sadl_exit_sort_transform.
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.



CLASS zcl_sales_ve_handler IMPLEMENTATION.
  METHOD if_sadl_exit_calc_element_read~get_calculation_info.
    IF line_exists( it_requested_calc_elements[ table_line = 'SALESYEAR' ] ) OR
           line_exists( it_requested_calc_elements[ table_line = 'SALESMONTH' ] ).
      APPEND 'SALESDATE' TO et_requested_orig_elements.

    ENDIF.
  ENDMETHOD.

  METHOD if_sadl_exit_calc_element_read~calculate.
    DATA lt_data TYPE TABLE OF zp_salesproj WITH DEFAULT KEY.
    lt_data = CORRESPONDING #( it_original_data ).


    LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<fs_row>).
      <fs_row>-SALESYEAR  = <fs_row>-salesdate(4).
      <fs_row>-SALESMONTH = <fs_row>-salesdate+4(2).
    ENDLOOP.
    ct_calculated_data = CORRESPONDING #( lt_data ).
  ENDMETHOD.

  METHOD if_sadl_exit_sort_transform~map_element.
    CASE iv_element.
      WHEN 'SALESYEAR' OR 'SALESMONTH'.
        " Map the UI sort request to the database field 'SALESDATE'
        APPEND VALUE #( name = 'SALESDATE' ) TO et_sort_elements.
    ENDCASE.
  ENDMETHOD.

ENDCLASS.

Result:


With the handler class implemented, the Sales order month and Sales order year ( Virtual Elements) now behaves like a standard database fields.

sharathtm_0-1775541139507.pngsharathtm_1-1775541190320.png

sharathtm_0-1775714874405.png

 

3 Comments