How to create a Fiori Elements App for a Time-Dependent RAP BO

Introduction

In this blog post, you will learn how to create a Fiori Elements app for a Time-Dependent RAP Business Object.

Time dependency means that the underlying table has a date that represents the start or end of the record validity as a key field.

Our goal is to create a Fiori Elements app where

  • The user can filter the data based on record validity to display current, past, or future valid entries
  • The user can use a delimit action to split an existing time slice
  • Validations ensure that only one time slice is valid at any time and that there are no gaps between time slices

To follow this blog, you should be familiar with

  • ABAP RESTful Programming Model
  • Fiori Elements

This blog is relevant for:

  • SAP S/4HANA On-Premises 2023 or higher
  • SAP S/4HANA Cloud, Public Edition
  • SAP S/4HANA Cloud, Private Edition
  • SAP BTP, ABAP Environment

Generate RAP BO for time-dependent customizing table

The time-dependent table has the following definition:

@EndUserText.label : 'Time depend.'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #C
@AbapCatalog.dataMaintenance : #ALLOWED
define table ztimedep {
  key client              : abap.clnt not null;
  key numc1               : abap.numc(1) not null;
  key validity_begin_date : abap.dats not null;
  validity_end_date       : abap.dats;
  content                 : abap.char(30);
  last_changed_at         : abp_lastchange_tstmpl;
  local_last_changed_at   : abp_locinst_lastchange_tstmpl;
}

Time slice validation

Add the following validation to the table entity in the behavior definition. By using the validity date fields as trigger fields, you can ensure that all changes to the time slice are validated. Also add this validation to the preparation action because a draft-enabled BO has been generated and this validation should be executed for the draft instance.

validation ValidateTimeSlice on save { field ValidityBeginDate, ValidityEndDate; }

draft determine action Prepare {
  validation TimeDepend ~ ValidateTimeSlice;
}

Note the comments in the code for a detailed explanation:

METHOD ValidateTimeSlice.
    DATA check_date TYPE d.
    CONSTANTS c_state_area TYPE string VALUE `TimeValidity`.
    "Entities can only be read if the key is specified in full
    "Validation requires not only the modified entity, but all others with the same initial key
    "Therefore, we use the parent entity to retrieve all sibling entities and additionally read ValidityEndDate
    READ ENTITIES OF zi_timedepend_s IN LOCAL MODE
      ENTITY timedependall BY \_timedepend
      FROM VALUE #( ( %tky-singletonid = 1
                      %tky-%is_draft = keys[ 1 ]-%is_draft ) )
      RESULT FINAL(all_keys).
    READ ENTITY IN LOCAL MODE zi_timedepend
      FIELDS ( ValidityEndDate ) WITH CORRESPONDING #( all_keys )
      RESULT DATA(all_entities).
    "Sort by ValidityBeginDate
    SORT all_entities BY numc1 ValidityBeginDate ASCENDING.

    LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>).
      "We are using state messages: https://help.sap.com/docs/abap-cloud/abap-rap/state-messages?version=sap_btp
      "Therefore, invalidate existing messages first
      INSERT VALUE #( %tky        = <key>-%tky
                      %state_area = c_state_area ) INTO TABLE reported-timedepend.
      READ TABLE all_entities WITH KEY %tky = <key>-%tky BINARY SEARCH INTO DATA(entity).
      DATA(tabix) = sy-tabix.
      "Check1: If ValidityEndDate is set, it must be before ValidityBeginDate
      IF entity-ValidityEndDate IS NOT INITIAL AND entity-ValidityEndDate < entity-ValidityBeginDate.
        INSERT VALUE #( %tky = <key>-%tky ) INTO TABLE failed-timedepend.
        INSERT VALUE #( %tky = <key>-%tky
                        %state_area = c_state_area
                        %path-timedependall-singletonid = 1
                        %path-timedependall-%is_draft = <key>-%is_draft
                        %element-ValidityEndDate = if_abap_behv=>mk-on "to highlight the affected cell
                        %msg = new_message_with_text( text = `End date is before Begin date` ) ) INTO TABLE reported-timedepend.
        CONTINUE.
      ENDIF.
      "Check2: Since we sorted by ValidityBeginDate, we can read the preceding chronological entity and check for gaps or overlaps.
      READ TABLE all_entities INDEX tabix - 1 ASSIGNING FIELD-SYMBOL(<prev_entity>).
      IF sy-subrc = 0 AND <prev_entity>-numc1 = <key>-numc1.
        check_date = entity-ValidityBeginDate - 1.
        IF <prev_entity>-ValidityEndDate <> check_date.
          IF <prev_entity>-ValidityEndDate > check_date.
            DATA(text) = `Time slices overlap`.
          ELSE.
            text = `Gap between time slices`.
          ENDIF.
          INSERT VALUE #( %tky = <key>-%tky ) INTO TABLE failed-timedepend.
          INSERT VALUE #( %tky = <key>-%tky
                          %state_area = c_state_area
                          %path-timedependall-singletonid = 1
                          %path-timedependall-%is_draft = <key>-%is_draft
                          %element-ValidityBeginDate = if_abap_behv=>mk-on
                          %msg = new_message_with_text( text = text ) ) INTO TABLE reported-timedepend.
        ENDIF.
      ENDIF.
      "Check3: check the following chronological entity
      READ TABLE all_entities INDEX tabix + 1 ASSIGNING FIELD-SYMBOL(<next_entity>).
      IF sy-subrc = 0 AND <next_entity>-numc1 = <key>-numc1.
        check_date = entity-ValidityEndDate + 1.
        IF <next_entity>-ValidityBeginDate <> check_date.
          IF <next_entity>-ValidityBeginDate < check_date.
            text = `Time slices overlap`.
          ELSE.
            text = `Gap between time slices`.
          ENDIF.
          INSERT VALUE #( %tky = <key>-%tky ) INTO TABLE failed-timedepend.
          INSERT VALUE #( %tky = <key>-%tky
                          %state_area = c_state_area
                          %path-timedependall-singletonid = 1
                          %path-timedependall-%is_draft = <key>-%is_draft
                          %element-ValidityEndDate = if_abap_behv=>mk-on
                          %msg = new_message_with_text( text = text ) ) INTO TABLE reported-timedepend.
        ENDIF.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

In the following example, a new record is added, but its time slice overlaps with the existing records. Note that the validation is executed when you save.

Validation example

Delimit action

A typical task is to split or delimit an existing time-dependent record. We provide the user with an action where a record is selected and a new ValididyEndDate is provided by the user. The existing data record is adjusted with the specified date and a new data record is also created.

Create a new abstract entity for parameterizing the action:

@EndUserText.label: 'Delimit'
define abstract entity ZD_DELIMITTP
{
  @EndUserText.label: 'New Validity End Date'
  ValidityEndDate : abap.dats;
}

Add the following action to the table entity in the behavior definition and, if applicable, the behavior projection. Also, add a side effect so that the table is refreshed when the action is applied to multiple rows.

"behavior definition
factory action ( features: instance ) Delimit parameter ZD_DELIMITTP [1];
side effects
  { action Delimit affects entity _TimeDependAll; }
"behavior projection
projection;
strict;
use draft;
use side effects;

use action Delimit;

Add the action to the metadata extension:

@UI.identification: [ {
    position: 1 , 
    label: 'Numc1'
  } ]
  @UI.lineItem: [ {
    position: 1 , 
    label: 'Numc1'
  },
  {
    type: #FOR_ACTION, 
    dataAction: 'Delimit', 
    label: 'Delimit Selected Entry'
  } ]
  @UI.facet: [ {
    id: 'ZI_TimeDepend', 
    purpose: #STANDARD, 
    type: #IDENTIFICATION_REFERENCE, 
    label: 'Time depend.', 
    position: 1 
  } ]
  Numc1;

Note the comments in the code for a detailed explanation:

METHOD get_global_authorizations.
    AUTHORITY-CHECK OBJECT 'S_TABU_NAM' ID 'TABLE' FIELD 'ZI_TIMEDEPEND' ID 'ACTVT' FIELD '02'.
    DATA(is_authorized) = COND #( WHEN sy-subrc = 0 THEN if_abap_behv=>auth-allowed
                                  ELSE if_abap_behv=>auth-unauthorized ).
    result-%action-delimit = is_authorized.
  ENDMETHOD.

  METHOD get_instance_features.
    "The delimitation action shall only be possible for draft entities as the transport selection logic requires a draft entity
    result = VALUE #( FOR <key> IN keys (
               %tky = <key>-%tky
               %action-delimit = COND #( WHEN <key>-%is_draft = if_abap_behv=>mk-on
                                         THEN if_abap_behv=>fc-o-enabled
                                         ELSE if_abap_behv=>fc-o-disabled ) ) ).
  ENDMETHOD.

  METHOD delimit.
    DATA new_timedepend TYPE TABLE FOR CREATE zi_timedepend_s\_timedepend.
    DATA modify_timedepend TYPE TABLE FOR UPDATE zi_timedepend.

    CHECK lines( keys ) > 0.
    READ ENTITIES OF zi_timedepend_s IN LOCAL MODE
      ENTITY timedepend
        ALL FIELDS WITH CORRESPONDING #( keys )
        RESULT FINAL(ref_timedepends).
    APPEND VALUE #( %is_draft = keys[ 1 ]-%is_draft
                    singletonid = 1 )
      TO new_timedepend ASSIGNING FIELD-SYMBOL(<new_timedepend>).

    LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>).
      READ TABLE ref_timedepends WITH TABLE KEY draft COMPONENTS %tky = <key>-%tky INTO DATA(ref_timedepend).
      "The new ValidityEndDate must be between the current ValidityBeginDate and ValidityEndDate.
      IF <key>-%param-ValidityEndDate < ref_timedepend-ValidityBeginDate OR <key>-%param-ValidityEndDate >= ref_timedepend-ValidityEndDate.
        INSERT VALUE #( %tky = <key>-%tky ) INTO TABLE failed-timedepend.
        INSERT VALUE #( %tky = <key>-%tky
                        %path-timedependall-singletonid = 1
                        %path-timedependall-%is_draft = <key>-%is_draft
                        %msg = new_message_with_text( text = |{ <key>-%param-ValidityEndDate DATE = USER } is not a valid date for delimit action| ) ) INTO TABLE reported-timedepend.
        CONTINUE.
      ENDIF.
      "new record-ValidityBeginDate = user selected date + 1 day
      "new record-ValidityEndDate = reference record-ValidityEndDate
      ref_timedepend-ValidityBeginDate = <key>-%param-ValidityEndDate + 1.
      INSERT VALUE #( %cid = <key>-%cid
                      %is_draft = <key>-%is_draft
                      %data = CORRESPONDING #( ref_timedepend EXCEPT lastChangedAt localLastChangedAt singletonid ) "don't copy technical fields
       ) INTO TABLE <new_timedepend>-%target.
      "reference record-ValidityEndDate = user selected date
      INSERT VALUE #( %tky = <key>-%tky
                      ValidityEndDate = <key>-%param-ValidityEndDate
                      %control-ValidityEndDate = if_abap_behv=>mk-on ) INTO TABLE modify_timedepend.
    ENDLOOP.
    IF new_timedepend[ 1 ]-%target IS NOT INITIAL.
      MODIFY ENTITIES OF zi_timedepend_s IN LOCAL MODE
        ENTITY timedependall CREATE BY \_timedepend
        FIELDS ( numc1
                 ValidityBeginDate
                 ValidityEndDate
                 content ) WITH new_timedepend
        ENTITY  timedepend  UPDATE FIELDS  ( ValidityEndDate ) WITH modify_timedepend
          MAPPED FINAL(mapped_create).
      mapped-timedepend = mapped_create-timedepend.
    ENDIF.
  ENDMETHOD.

In the following example, the record valid for October is delimited:

Delimit Selected Entry
Delimit Action Result

Validity View Variants

The user should be able to filter the entries by their validity regarding a key date:

  • Currently valid
  • Valid in the past
  • Valid in the future

There are two alternative solutions:

  • View settings variants
  • Calculated or Virtual validity field

View settings variants

The user can use the table view settings to create the required filter conditions.

Filter Conditions for Effective Date Today

For this approach, you must ensure that both ValidityBeginDate and ValidityEndDate are within a reasonable period of time so that Today +/- can be applied. You can change the time slice validation by changing the first check:

"Check1: ValidityBeginDate and ValidityEndDate must be within a reasonable timeframe
      DATA(max_date) = CONV d( cl_abap_context_info=>get_system_date( ) + 99999 ).
      DATA(min_date) = CONV d( cl_abap_context_info=>get_system_date( ) - 99999 ).
      IF entity-ValidityEndDate < entity-ValidityBeginDate
        OR entity-ValidityEndDate > max_date.
        IF entity-ValidityEndDate < entity-ValidityBeginDate.
          DATA(err_text) = `End date must be greather than Begin date`.
        ELSE.
          err_text = |End date must not be greather than {  max_date DATE = USER }|.
        ENDIF.
        INSERT VALUE #( %tky = <key>-%tky ) INTO TABLE failed-timedepend.
        INSERT VALUE #( %tky = <key>-%tky
                        %state_area = c_state_area
                        %path-timedependall-singletonid = 1
                        %path-timedependall-%is_draft = <key>-%is_draft
                        %element-ValidityEndDate = if_abap_behv=>mk-on "to highlight the affected cell
                        %msg = new_message_with_text( text = err_text ) ) INTO TABLE reported-timedepend.
        CONTINUE.
      ENDIF.
      IF entity-ValidityBeginDate < min_date.
        INSERT VALUE #( %tky = <key>-%tky ) INTO TABLE failed-timedepend.
        INSERT VALUE #( %tky = <key>-%tky
                        %state_area = c_state_area
                        %path-timedependall-singletonid = 1
                        %path-timedependall-%is_draft = <key>-%is_draft
                        %element-ValidityBeginDate = if_abap_behv=>mk-on
                        %msg = new_message_with_text( text = |Begin date must be greather than { min_date DATE = USER }| ) ) INTO TABLE reported-timedepend.
        CONTINUE.
      ENDIF.

The user now has the option to set this view as the default view for all and create other view variants, for example, for Valid in the future.

Default view

Calculated or virtual validity field

A field “Validity” is added to the data model that calculates the validity of each record with the system date as the key date. The user can use this field in the filter conditions.

Create a custom domain that represents the different validity values:

Validity domain

Create a custom value help entity for the domain:

"for use in SAP BTP, ABAP Environment or S/4HANA Cloud Public Edition
@ObjectModel.dataCategory: #VALUE_HELP
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Validity'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.resultSet.sizeCategory: #XS
define view entity ZI_ValidityVH
  as select from DDCDS_CUSTOMER_DOMAIN_VALUE( p_domain_name : 'ZVALIDITY' ) as Id
  association [0..1] to DDCDS_CUSTOMER_DOMAIN_VALUE_T as _Text on  _Text.value_low   = $projection.Validity
                                                               and _Text.language    = $session.system_language
                                                               and _Text.domain_name = Id.domain_name
                                                               and _Text.value_position = Id.value_position
{
         @ObjectModel.text.element: ['Description']
  key    value_low                                 as Validity,
         @Semantics.text: true
         _Text( p_domain_name : 'ZVALIDITY' ).text as Description
}

"for use in S/4HANA Cloud Private Edition and S/4HANA On-Premises
@ObjectModel.dataCategory: #VALUE_HELP
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Validity'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.resultSet.sizeCategory: #XS
define view entity ZI_ValidityVH
  as select from dd07l as id
  association [0..1] to dd07t as _text on  _text.domname    = id.domname
                                       and _text.ddlanguage = $session.system_language
                                       and _text.as4local   = id.as4local
                                       and _text.valpos     = id.valpos
                                       and _text.as4vers    = id.as4vers
{
      @ObjectModel.text.element: ['Description']
  key domvalue_l   as Validity,
      @Semantics.text: true
      _text.ddtext as Description
}
where
      id.domname  = 'ZVALIDITY'
  and id.as4local = 'A'
  and id.as4vers  = '0000'

Add a calculated field to the base CDS Entity of the table. It calculates the validity value based on the system date. Use the custom value help entity for value help definition. If you want to add an additional visual indicator for the validity, you can use the criticality annotation.

If you have a projection layer, also add the new field to the projection CDS entity.

In this case, you also have the option to define a virtual element in the projection view instead of the calculated field in the base view. The advantage is that you don’t need to define a determination with the same logic as in the CDS view, because the calculation is executed for both the active instance and the draft instance via the annotated ABAP class.

@EndUserText.label: 'Time depend.'
@AccessControl.authorizationCheck: #CHECK
define view entity ZI_TimeDepend
  as select from ztimedep
  association to parent ZI_TimeDepend_S as _TimeDependAll on $projection.SingletonID = _TimeDependAll.SingletonID
{
  key numc1                 as Numc1,
  key validity_begin_date   as ValidityBeginDate,
      content               as Content,
      validity_end_date     as ValidityEndDate,
      @Semantics.systemDateTime.lastChangedAt: true
      last_changed_at       as LastChangedAt,
      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      local_last_changed_at as LocalLastChangedAt,
      1                     as SingletonID,
      @Consumption.valueHelpDefinition: [{  entity:
      {name: 'ZI_ValidityVH' , element: 'Validity' }
      }]
      case
         when validity_begin_date <= $session.system_date and ( validity_end_date >= $session.system_date or validity_end_date is initial ) then 'C'
         when validity_begin_date > $session.system_date then 'F'
      else 'P'
      end                   as Validity,
      _TimeDependAll
}

Adapt the draft table by adding the calculated field:

@EndUserText.label : 'ZI_TimeDepend - Draft'
@AbapCatalog.enhancement.category : #EXTENSIBLE_ANY
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table ztimedep_d {
  key mandt             : mandt not null;
  key numc1             : abap.numc(1) not null;
  key validitybegindate : abap.dats not null;
  content               : abap.char(30);
  validityenddate       : abap.dats;
  lastchangedat         : abp_lastchange_tstmpl;
  locallastchangedat    : abp_locinst_lastchange_tstmpl;
  singletonid           : abap.int1;
  validity              : abap.char(1);
  "%admin"              : include sych_bdl_draft_admin_inc;
}

Add the following determination to the table entity in the behavior definition to update the validity value when the validity dates are changed. A side effect is used so that the validity change is reflected on the UI. If you have a projection layer, reuse the side effect in the behavior projection. If you already have a side effect for the delimit action, simply extend the side effect list.

"behavior definition  
  determination setValidity on modify { field ValidityEndDate; }

  side effects
  { field ValidityEndDate affects field Validity; }

"behavior projection
  use side effects;

Implement the determination:

METHOD setvalidity.
    DATA modify_timedepend TYPE TABLE FOR UPDATE zi_timedepend.
    CHECK lines( keys ) > 0.
    READ ENTITIES OF zi_timedepend_s IN LOCAL MODE
      ENTITY timedepend
        ALL FIELDS WITH CORRESPONDING #( keys )
        RESULT FINAL(ref_timedepends).
    LOOP AT ref_timedepends ASSIGNING FIELD-SYMBOL(<timedepends>).
      INSERT VALUE #( %tky = <timedepends>-%tky
                      validity = COND #( WHEN <timedepends>-ValidityBeginDate <= cl_abap_context_info=>get_system_date( )
                                              AND ( <timedepends>-ValidityEndDate >= cl_abap_context_info=>get_system_date( ) OR <timedepends>-ValidityEndDate IS INITIAL ) THEN 'C'
                                         WHEN <timedepends>-ValidityBeginDate > cl_abap_context_info=>get_system_date( ) THEN 'F'
                                         ELSE 'P' )
                      %control-validity = if_abap_behv=>mk-on ) INTO TABLE modify_timedepend.
    ENDLOOP.
    MODIFY ENTITIES OF zi_timedepend_s IN LOCAL MODE
      ENTITY timedepend
      UPDATE FIELDS  ( validity )
      WITH modify_timedepend.
  ENDMETHOD.

Set the new field Validity as readonly in the behavior definition:

field ( readonly )
   Validity,
   SingletonID,
   LastChangedAt,
   LocalLastChangedAt;

The user can now filter the records in the view settings based on the validity value:

Validity Filter

The user now has the option to set this view as the default view for all:

Default view

If the user expands the ValidityEndDate for an outdated entry, the validity column value is updated once the focus is moved from the ValidityEndDate cell.

Rating: 0 / 5 (0 votes)