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
To follow this blog, you should be familiar with
This blog is relevant for:
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;
}
With the BC Maintenance Object ADT Wizard, you generate the RAP BO for this table. This way, you can also use the Custom Business Configurations app, so you don’t need to create a custom Fiori Elements app. You can also create your own custom Fiori app based on the generated service binding if you don’t want to use this app. For more information, see this blog. The following modifications can also be made to any existing RAP BO and can be implemented independently of each other.
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 = `Valid-to date is before valid-from 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
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
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
The user should be able to filter the entries by their validity regarding a key date:
There are two alternative solutions:
The user can use the table view settings to create the required filter conditions.
Filter Conditions for Effective Date Today
The advantage of this approach is that no backend implementation is required.
However, filter expressions such as “Today” are immediately converted to constants if the expression is used in combination with operands such as “greater than” or “less than”. This means that you cannot save the filter in a view variant with the dynamic expression “Today”, but only with constants.
A virtual field 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 virtual field to the 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.
@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 virtual 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.