Technical Articles
SAP Fiori Elements BOPF CRUD App using List Report/Object Page – step-by-step guide
There are lots of extremely useful guides out there (some written by SAP and some by the community) that outline how to go about implementing BOPF CRUD with SAP Fiori Elements. However, I couldn’t find one that detailed the full end to end steps that would allow a workable header and line item CRUD app using a List Report Object Page template in SAP Fiori Elements.
This step-by-step guide was created to show you what can be achieved with SAP Business Application Studio (BAS), SAP Fiori Elements, OData servcies, and CDS Views.
The example i’m going to show you is for a mocked-up basic timesheet application. It would require further work to make if fit for purpose in a productive environment, but you’ll get the basics from which you can build upon.
Pre-requisites
- A Business Technology Platform (trial) account https://www.sap.com/cmp/td/sap-hana-cloud-trial.html
- SAP BAS installed on your BTP account with Fiori tools installed in your Dev space.
- A S/4 on-premise system you can connect to via https://blogs.sap.com/2020/09/17/how-sap-cloud-connector-works/
- An installation of Eclipse (Java EE IDE) with the relevant SAP tools installed https://tools.hana.ondemand.com/
Basic Setup
Transparent Tables to store data
I created 3 tables to store data – a header table, a line item table and a table to map the logged in SAP user to an employee number.
To enable draft, create, update, delete functionality the key of each table needs to be of type SNWD_NODE_KEY. You’ll also see that the Header UUID is referenced in the Line Item table – this is the link between the Header and Line Item tables.
Header Table Dictionary entry
Header Table
Line Item table Dictionary entry
Line Item table
Mapping Table for SAP User -> Personnel Number mapping
User Mapping table
Draft Tables
If you plan on implementing draft capabilty you do not need to manually create these tables. The annotations in the CDS Views will do this automatically.
Number Ranges
So that my header and line items had a unique ID that wasn’t a UUID I also created two number ranges – one for Header and one for the Line Items. These were created using transaction SNRO.
CDS Views
The CDS Views are based on a 3 layer model – Basic, Composite, and Consumption. There is one of each for both Header and Line Items. I also added views that calculate the sum of all the line items in hours:minutes for each Header item.
Header views
Basic – selecting from the base transparent table
@AbapCatalog.sqlViewName: 'ZTIMEHDRBAS'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Time Header Basic view'
@VDM.viewType: #BASIC
define view ZCDS_I_TIME_HEADER_BAS
as select from zttime_hdr1
{
key uuid,
counter,
pernr,
workdate,
lchg_date_time,
lchg_uname,
crea_date_time,
crea_uname
}
Composite – selecting from the Basic views and adding the relevant Object Model annotations to enable create, update, delete, draft functionality. The draft table for the Header items is created automatically by the annotation:
@ObjectModel.writeDraftPersistence: 'ZTTIME_HDR1_D'
@AbapCatalog.sqlViewName: 'ZTIME_HDR1'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Time sheet Header'
@VDM.viewType: #COMPOSITE
//@VDM.viewType: #TRANSACTIONAL
@ObjectModel.modelCategory:#BUSINESS_OBJECT
@ObjectModel.compositionRoot:true
@ObjectModel.transactionalProcessingEnabled:true
@ObjectModel.writeActivePersistence:'ZTTIME_HDR1'
@ObjectModel.createEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.updateEnabled:true
@ObjectModel.draftEnabled: true
@ObjectModel.semanticKey:['counter']
@ObjectModel.entityChangeStateId: 'lchg_date_time'
@ObjectModel.writeDraftPersistence: 'ZTTIME_HDR1_D'
@Search.searchable: true
@OData.publish: false
define view ZCDS_I_TIME_HEADER1
as select from ZCDS_I_TIME_HEADER_BAS
association [1..*] to ZCDS_I_TIME_ITEM1 as _item on $projection.uuid = _item.uuid
association [1] to ZCDS_I_TIME_ITEM_SUM as _itemhrs on $projection.uuid = _itemhrs.uuid
{
@ObjectModel.readOnly: true
key uuid,
@EndUserText.label: 'Unique ID'
@Search.defaultSearchElement: true
counter,
@Search.defaultSearchElement: true
@EndUserText.label: 'Personnel Number'
pernr,
@EndUserText.label: 'Work Date'
workdate,
ZCDS_I_TIME_HEADER_BAS.lchg_date_time,
ZCDS_I_TIME_HEADER_BAS.lchg_uname,
crea_date_time,
crea_uname,
@ObjectModel.association.type: [#TO_COMPOSITION_CHILD]
_item,
_itemhrs.LongHrsMins,
_itemhrs
}
Consumption – exposed as the OData service
@AbapCatalog.sqlViewName: 'ZCTIME_HDR'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Time Sheet App'
@VDM.viewType: #CONSUMPTION
@Search.searchable: true
@ObjectModel.compositionRoot: true
@ObjectModel.transactionalProcessingDelegated: true
@ObjectModel.createEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.updateEnabled:true
@ObjectModel.draftEnabled: true
@ObjectModel.semanticKey:['counter']
@Metadata.allowExtensions: true
@UI.headerInfo.description.label: 'Nobia Time Sheet App'
@UI.headerInfo.description.value: 'counter'
@UI.headerInfo.typeName: 'Timesheet'
@UI.headerInfo.typeNamePlural: 'Timesheets'
@OData.publish: true
define view ZCDS_C_TIME_HEADER
as select from ZCDS_I_TIME_HEADER1
association [1..*] to ZCDS_C_TIME_ITEM as _item on $projection.uuid = _item.uuid
{
key uuid,
@EndUserText.label: 'Timesheet ID'
@Search.defaultSearchElement: true
@ObjectModel.readOnly: true
counter,
@EndUserText.label: 'Personnel Number'
@ObjectModel.readOnly: true
pernr,
@EndUserText.label: 'Work Date'
@ObjectModel.mandatory: true
@Consumption.filter.selectionType: #INTERVAL
workdate,
@Semantics.systemDateTime.lastChangedAt: true
@EndUserText.label: 'At'
@ObjectModel.readOnly: true
ZCDS_I_TIME_HEADER1.lchg_date_time,
@EndUserText.label: 'By'
@Semantics.user.lastChangedBy: true
@ObjectModel.readOnly: true
ZCDS_I_TIME_HEADER1.lchg_uname,
@EndUserText.label: 'At'
@Semantics.systemDateTime.createdAt: true
@ObjectModel.readOnly: true
ZCDS_I_TIME_HEADER1.crea_date_time,
@EndUserText.label: 'By'
@Semantics.user.createdBy: true
@ObjectModel.readOnly: true
ZCDS_I_TIME_HEADER1.crea_uname,
@EndUserText.label: 'Hours/Mins on work date'
@ObjectModel.readOnly: true
LongHrsMins,
@ObjectModel.association.type: [#TO_COMPOSITION_CHILD]
_item
}
Line Item views
Basic
@AbapCatalog.sqlViewName: 'ZTIMEITMBAS'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Time Item Basic view'
@VDM.viewType: #BASIC
define view ZCDS_I_TIME_ITEM_BAS
as select from zttime_item1
{
key itemuuid,
uuid,
linecounter,
timetype,
timestart,
timeend,
lchg_date_time,
lchg_uname,
crea_date_time,
crea_uname
}
Composite – the draft table for the Line Items is created automatically by the annotation:
@ObjectModel.writeDraftPersistence: 'ZTTIME_ITEM1_D'
@AbapCatalog.sqlViewName: 'ZTIME_ITM1'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Time sheet Item table'
@VDM.viewType: #COMPOSITE
@ObjectModel.modelCategory:#BUSINESS_OBJECT
@ObjectModel.writeActivePersistence:'ZTTIME_ITEM1'
@ObjectModel.createEnabled:true
@ObjectModel.updateEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.writeDraftPersistence: 'ZTTIME_ITEM1_D'
@ObjectModel.semanticKey:['linecounter']
@ObjectModel.entityChangeStateId: 'lchg_date_time'
@Search.searchable: true
define view ZCDS_I_TIME_ITEM1
as select from ZCDS_I_TIME_ITEM_BAS
association [1] to ZCDS_I_TIME_HEADER1 as _header on $projection.uuid = _header.uuid
association [1] to ZCDS_VH_TIMETYPES as _timetype on $projection.timetype = _timetype.DomainValue
{
@ObjectModel.readOnly: true
key itemuuid,
@ObjectModel.readOnly: true
uuid,
@Search.defaultSearchElement: true
@ObjectModel.readOnly: true
linecounter,
@ObjectModel.foreignKey.association: '_timetype'
@ObjectModel.mandatory: true
timetype,
@ObjectModel.mandatory: true
timestart,
@ObjectModel.mandatory: true
timeend,
lchg_date_time,
lchg_uname,
crea_date_time,
crea_uname,
@ObjectModel.association.type: [#TO_COMPOSITION_PARENT, #TO_COMPOSITION_ROOT]
_header,
_timetype
}
Consumption
@AbapCatalog.sqlViewName: 'ZCTIME_ITM'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Time sheet Item table'
@VDM.viewType: #CONSUMPTION
@ObjectModel.semanticKey:['linecounter']
@Metadata.allowExtensions: true
@ObjectModel.createEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.updateEnabled:true
@UI.headerInfo.description.label: 'Time sheet Item'
@UI.headerInfo.description.value: 'linecounter'
@Search.searchable: true
define view ZCDS_C_TIME_ITEM
as select from ZCDS_I_TIME_ITEM1
association [1] to ZCDS_C_TIME_HEADER as _header on $projection.uuid = _header.uuid
{
key itemuuid,
uuid,
@EndUserText.label: 'Time Entry ID'
@Search.defaultSearchElement: true
@ObjectModel.readOnly: true
linecounter,
@ObjectModel.mandatory: true
@EndUserText.label: 'Time Type'
timetype,
@ObjectModel.mandatory: true
timestart,
@ObjectModel.mandatory: true
timeend,
@Semantics.systemDateTime.lastChangedAt: true
@EndUserText.label: 'At'
@ObjectModel.readOnly: true
lchg_date_time,
@Semantics.user.lastChangedBy: true
@EndUserText.label: 'By'
@ObjectModel.readOnly: true
lchg_uname,
@Semantics.systemDateTime.createdAt: true
@EndUserText.label: 'At'
@ObjectModel.readOnly: true
crea_date_time,
@Semantics.user.createdBy: true
@EndUserText.label: 'By'
@ObjectModel.readOnly: true
crea_uname,
@ObjectModel.association.type: [#TO_COMPOSITION_ROOT, #TO_COMPOSITION_PARENT]
_header,
_timetype
}
Line Item calculation views
Composite
@AbapCatalog.sqlViewName: 'ZTIMEITMHRS'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Hours between times'
@VDM.viewType: #COMPOSITE
define view ZCDS_I_TIME_ITEM_HRS
as select from ZCDS_I_TIME_ITEM_BAS
association [1] to ZCDS_I_TIME_HEADER_BAS as _header on $projection.uuid = _header.uuid
{
key itemuuid,
uuid,
linecounter,
timetype,
timestart,
timeend,
_header.workdate,
_header.counter,
tstmp_seconds_between(dats_tims_to_tstmp(_header.workdate, timestart, abap_system_timezone($session.client,'NULL' ) ,
$session.client, 'INITIAL'), dats_tims_to_tstmp(_header.workdate,
timeend, abap_system_timezone($session.client,'NULL' ), $session.client, 'INITIAL'), 'INITIAL') as SecsBT
}
View to sum the SecsBT field and group it by Header item
@AbapCatalog.sqlViewName: 'ZTSITMCUMUL'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Time Item cumulative view'
define view ZCDS_I_TIME_ITEM_CUMUL
as select from ZCDS_I_TIME_ITEM_HRS
{
key uuid,
sum(SecsBT) as SumSecsBT
}
group by
uuid
View to create the field that is used in the Fiori Elements app
@AbapCatalog.sqlViewName: 'ZTSITMSUM'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Time Item Sum view'
@VDM.viewType: #COMPOSITE
define view ZCDS_I_TIME_ITEM_SUM
as select from ZCDS_I_TIME_ITEM_CUMUL
{
key uuid,
cast(floor(division(division(SumSecsBT,60,2),60,2)) as abap.char(21)) as WholeHours,
cast((div(SumSecsBT,60) - (floor(div(div(SumSecsBT,60),60)) * 60)) as abap.char(21)) as WholeMins,
concat(cast(floor(division(division(SumSecsBT,60,2),60,2)) as abap.char(21)),cast((div(SumSecsBT,60) - (floor(div(div(SumSecsBT,60),60)) * 60)) as abap.char(21))) as HrsMins,
@EndUserText.label: 'Hours/Mins for work date'
cast(concat_with_space(concat(cast(floor(division(division(SumSecsBT,60,2),60,2)) as abap.char(21)),'hrs'), concat(cast((div(SumSecsBT,60) - (floor(div(div(SumSecsBT,60),60)) * 60)) as abap.char(21)), 'mins'), 1) as abap.char(50) ) as LongHrsMins
}
Value Helps
To aid the end-user I created a value help using CDS Views – one for Time Types (for this i created a new Domain and added a list of fixed values).
Time Type Domain
I then referenced the Domain in my CDS View.
@AbapCatalog.sqlViewName: 'ZVHTIMETYPE'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Value Help for Time Types'
@ObjectModel.resultSet.sizeCategory: #XS
define view ZCDS_VH_TIMETYPES
as select from I_DomainFixedValue
{
key DomainValue
}
where
SAPDataDictionaryDomain = 'ZTIMETYPE'
Metadata Extensions
I had previously done all my Fiori Elements development in Web IDE which had a really nice Annotation Modeller. This isn’t currently available in BAS so i decided to add my annotations using Metadata Extensions.
Header
The Header extensions are enabled by a special annotation in the Header Consumption view
@Metadata.allowExtensions: true
@Metadata.layer: #CUSTOMER
annotate view ZCDS_C_TIME_HEADER with
{
@UI.facet: [
{
label: 'Date Entry',
id : 'GeneralInfo',
purpose: #STANDARD,
type : #COLLECTION,
position: 10
},
{ type: #FIELDGROUP_REFERENCE ,
label : 'Entry',
parentId: 'GeneralInfo',
id: 'idIdentification' ,
position: 10,
targetQualifier: 'dates' },
{ type: #FIELDGROUP_REFERENCE ,
label : 'Created',
parentId: 'GeneralInfo',
id: 'idIdentification2' ,
position: 20,
targetQualifier: 'audit' },
{ type: #FIELDGROUP_REFERENCE ,
label : 'Last Changed',
parentId: 'GeneralInfo',
id: 'idIdentification5' ,
position: 30,
targetQualifier: 'audit2' },
{
label: 'Time Entries',
id : 'TimeData',
type : #LINEITEM_REFERENCE,
targetElement: '_item' ,
position: 20
},
{ type: #IDENTIFICATION_REFERENCE ,
label : 'Times',
parentId: 'TimeData',
id: 'idIdentification1' ,
position: 20
}
]
@UI.identification: [{ position: 10, label:'TimeSheet ID',importance: #HIGH}]
@UI.lineItem: [{ importance: #HIGH, position: 10, label :'Timesheet ID'}]
@UI.hidden: true
counter;
@UI.fieldGroup: [{qualifier: 'dates', position: 20 }]
@UI.selectionField: [{position: 10}]
@UI.lineItem: [{position: 20, importance: #HIGH, label: 'Personnel Number' }]
@UI.identification: [{ position: 20, importance: #HIGH }]
pernr;
@UI.fieldGroup: [{qualifier: 'dates', position: 30 }]
@UI.selectionField: [{position: 20}]
@UI.lineItem:[{position: 30, importance: #HIGH, label: 'Work date'}]
@UI.identification: [{ position: 30, importance: #HIGH }]
workdate;
@UI.hidden: true
@UI.lineItem: [{position: 40, importance: #HIGH, type: #FOR_ACTION, dataAction: 'BOPF:COPY_HEADER', label: 'Copy'}]
uuid;
@UI.fieldGroup: [{qualifier: 'audit', position: 10 }]
@UI.identification: [{ position: 10, label:'By',importance: #HIGH}]
crea_uname;
@UI.fieldGroup: [{qualifier: 'audit', position: 20 }]
@UI.identification: [{ position: 20, label:'At',importance: #HIGH}]
crea_date_time;
@UI.fieldGroup: [{qualifier: 'audit2', position: 30 }]
@UI.identification: [{ position: 30, label:'By',importance: #HIGH}]
lchg_uname;
@UI.fieldGroup: [{qualifier: 'audit2', position: 40 }]
@UI.identification: [{ position: 40, label:'At',importance: #HIGH}]
lchg_date_time;
@UI.fieldGroup: [{qualifier: 'dates', position: 40 }]
@UI.identification: [{ position: 50, label:'Hours/Mins on work date',importance: #HIGH}]
@UI.lineItem:[{position: 50, importance: #HIGH}]
LongHrsMins;
}
Line Item
The Line Item extensions are similar enabled with the same metadata extension annotation in the Line Item Consumption view.
@Metadata.layer: #CUSTOMER
annotate view ZCDS_C_TIME_ITEM with
{
@UI.facet: [
{
label: 'Time Worked',
id : 'TimeInfo',
purpose: #STANDARD,
type : #COLLECTION,
position: 10
},
{ type: #FIELDGROUP_REFERENCE ,
label : 'Entry',
parentId: 'TimeInfo',
id: 'idIdentification' ,
position: 10,
targetQualifier: 'times' },
{ type: #FIELDGROUP_REFERENCE ,
label : 'Created',
parentId: 'TimeInfo',
id: 'idIdentification2' ,
position: 20,
targetQualifier: 'audit' },
{ type: #FIELDGROUP_REFERENCE ,
label : 'Last Changed',
parentId: 'TimeInfo',
id: 'idIdentification5' ,
position: 30,
targetQualifier: 'audit2' }
]
@UI.lineItem: [{ importance: #HIGH, label: 'Time Entry ID', position: 40 }]
@UI.identification: [{ position: 40, importance: #HIGH }]
@UI.fieldGroup: [{qualifier: 'times', position: 40 }]
linecounter;
@UI.lineItem: [{ importance: #HIGH, label: 'Time Type', position: 50 }]
@UI.identification: [{ position: 50, importance: #HIGH }]
@UI.fieldGroup: [{qualifier: 'times', position: 50 }]
timetype;
@UI.lineItem: [{ importance: #HIGH, label: 'Time Start', position: 60 }]
@UI.identification: [{ position: 60, importance: #HIGH }]
@UI.fieldGroup: [{qualifier: 'times', position: 60 }]
timestart;
@UI.lineItem: [{ importance: #HIGH, label: 'Time end', position: 70 }]
@UI.identification: [{ position: 70, importance: #HIGH }]
@UI.fieldGroup: [{qualifier: 'times', position: 70 }]
timeend;
@UI.hidden: true
uuid;
@UI.hidden: true
itemuuid;
@UI.fieldGroup: [{qualifier: 'audit', position: 10 }]
@UI.identification: [{ position: 10, label:'By',importance: #HIGH}]
crea_uname;
@UI.fieldGroup: [{qualifier: 'audit', position: 20 }]
@UI.identification: [{ position: 20, label:'At',importance: #HIGH}]
crea_date_time;
@UI.fieldGroup: [{qualifier: 'audit2', position: 30 }]
@UI.identification: [{ position: 30, label:' By',importance: #HIGH}]
lchg_uname;
@UI.fieldGroup: [{qualifier: 'audit2', position: 40 }]
@UI.identification: [{ position: 40, label:'At',importance: #HIGH}]
lchg_date_time;
}
Business Object
The Business Object is where all the logic is created to deal with the CRUD operations. It is only required on the Header – associations from the Header to the Line Items take care of the CRUD for the Line Items. The Business Object is created via annotations on the Header Composite view:
@ObjectModel.modelCategory:#BUSINESS_OBJECT
@ObjectModel.transactionalProcessingEnabled:true
Business Object showing Root and Child node
Determinations
In the Business Object i created Determinations for both the Header and Line Items. These are used to get the next number in range for both the Header and Line Item counter. When creating a determination the correspondng class/method is created automatically for you. The example below show how i’m getting the next number in the range for the Header, along with populating some other fields. I’m doing something almost identical for the Line Items.
CLASS zcl_cds_d_get_hdr_counter DEFINITION
PUBLIC
INHERITING FROM /bobf/cl_lib_d_supercl_simple
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS /bobf/if_frw_determination~execute
REDEFINITION .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_cds_d_get_hdr_counter IMPLEMENTATION.
METHOD /bobf/if_frw_determination~execute.
DATA lt_data TYPE ztcds_i_time_header13.
DATA: lt_item TYPE ztcds_i_time_item13,
wa_item TYPE zscds_i_time_item13.
io_read->retrieve(
EXPORTING
iv_node = is_ctx-node_key " uuid of node name
it_key = it_key " keys given to the determination
IMPORTING
eo_message = eo_message " pass message object
et_data = lt_data " itab with node data
et_failed_key = et_failed_key " pass failures
).
DATA lv_counter TYPE char12.
DATA lv_timestamp TYPE timestampl.
LOOP AT lt_data REFERENCE INTO DATA(lr_data).
IF lr_data->counter IS INITIAL.
CALL FUNCTION 'NUMBER_GET_NEXT'
EXPORTING
nr_range_nr = '01'
object = 'ZTIMEHDR'
IMPORTING
number = lv_counter
EXCEPTIONS
interval_not_found = 1
number_range_not_intern = 2
object_not_found = 3
quantity_is_0 = 4
quantity_is_not_1 = 5
interval_overflow = 6
buffer_overflow = 7
OTHERS = 8.
IF sy-subrc <> 0.
*
ENDIF.
lr_data->counter = lv_counter.
lr_data->counter = |{ lr_data->counter ALPHA = IN }|.
IF lr_data->pernr IS INITIAL.
SELECT zpernr FROM zusrpernr INTO @DATA(lv_pernr)
WHERE zuser = @sy-uname.
ENDSELECT.
lr_data->pernr = lv_pernr.
lr_data->crea_uname = sy-uname.
lr_data->lchg_uname = sy-uname.
GET TIME STAMP FIELD lv_timestamp.
lr_data->lchg_date_time = lv_timestamp.
lr_data->crea_date_time = lv_timestamp.
ENDIF.
io_modify->update(
EXPORTING
iv_node = is_ctx-node_key " uuid of node
iv_key = lr_data->key " key of line
is_data = lr_data " ref to modified data
it_changed_fields = VALUE #( ( zif_cds_i_time_header13_c=>sc_node_attribute-zcds_i_time_header1-counter )
( zif_cds_i_time_header13_c=>sc_node_attribute-zcds_i_time_header1-pernr )
( zif_cds_i_time_header13_c=>sc_node_attribute-zcds_i_time_header1-crea_uname )
( zif_cds_i_time_header13_c=>sc_node_attribute-zcds_i_time_header1-lchg_uname )
( zif_cds_i_time_header13_c=>sc_node_attribute-zcds_i_time_header1-lchg_date_time )
( zif_cds_i_time_header13_c=>sc_node_attribute-zcds_i_time_header1-crea_date_time )
)
).
* Create a default line item
wa_item-crea_uname = sy-uname.
wa_item-lchg_uname = sy-uname.
wa_item-lchg_date_time = lv_timestamp.
wa_item-crea_date_time = lv_timestamp.
APPEND wa_item TO lt_item.
LOOP AT lt_item REFERENCE INTO DATA(lr_item).
io_modify->create(
EXPORTING
iv_node = zif_cds_i_time_header13_c=>sc_node-zcds_i_time_item1 " Node to Create
* is_data = lr_item_copy " Data
is_data = lr_item " Data
iv_assoc_key = zif_cds_i_time_header13_c=>sc_association-zcds_i_time_header1-_item " Association
iv_source_node_key = zif_cds_i_time_header13_c=>sc_node-zcds_i_time_header1 " Parent Node
iv_source_key = lr_data->key " NodeID of Parent Instance
).
io_modify->end_modify( iv_process_immediately = abap_true ).
ENDLOOP.
ELSE.
lr_data->lchg_uname = sy-uname.
GET TIME STAMP FIELD lv_timestamp.
lr_data->lchg_date_time = lv_timestamp.
io_modify->update(
EXPORTING
iv_node = is_ctx-node_key " uuid of node
iv_key = lr_data->key " key of line
is_data = lr_data " ref to modified data
it_changed_fields = VALUE #(
( zif_cds_i_time_header13_c=>sc_node_attribute-zcds_i_time_header1-lchg_uname )
( zif_cds_i_time_header13_c=>sc_node_attribute-zcds_i_time_header1-lchg_date_time )
)
).
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
The screen fields and the data types are taken directly from the automatically generated structures and table types.
Generated Structures and Table Types
Actions
I also created an Action to allow a Header and its associated line items to be copied by clicking the Copy button in the Fiori Elements List Report. The code gets a reference to the Header, then gets references to all the associated line items and copies them to new Header and Line Items.
The action is linked to the front end via a metadata extension annotation. The action can be on any field.
@UI.hidden: true
@UI.lineItem: [{position: 40, importance: #HIGH, type: #FOR_ACTION, dataAction: 'BOPF:COPY_HEADER', label: 'Copy'}]
uuid;
CLASS zcl_cds_a_copy_header DEFINITION
PUBLIC
INHERITING FROM /bobf/cl_lib_a_supercl_simple
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS /bobf/if_frw_action~execute
REDEFINITION .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_cds_a_copy_header IMPLEMENTATION.
METHOD /bobf/if_frw_action~execute.
DATA: lr_head_copy TYPE ztcds_i_time_header13,
lr_item_copy TYPE ztcds_i_time_item13,
lv_timestamp TYPE timestampl.
" Internal tab for Header & Item Data
" Created using reference to Generated Table Type
DATA(lt_head) = VALUE ztcds_i_time_header13( ).
DATA(lt_item) = VALUE ztcds_i_time_item13( ).
" Get Dates Head Data
io_read->retrieve(
EXPORTING
iv_node = is_ctx-node_key " Node Name
it_key = it_key " Key Table
IMPORTING
et_data = lt_head " Data Return Structure
).
" Get Times Item Data
io_read->retrieve_by_association(
EXPORTING
iv_node = is_ctx-node_key " Node Name
it_key = it_key " Key Table
iv_association = zif_cds_i_time_header13_c=>sc_association-zcds_i_time_header1-_item " Name of Association
iv_fill_data = abap_true
IMPORTING
et_data = lt_item " Data Return Structure
).
GET TIME STAMP FIELD lv_timestamp.
" For Each Node Instance
LOOP AT lt_head REFERENCE INTO DATA(lr_head).
CLEAR: lr_head->counter,
lr_head->crea_date_time,
lr_head->crea_uname,
lr_head->lchg_date_time,
lr_head->lchg_uname.
lr_head->crea_uname = sy-uname.
lr_head->lchg_uname = sy-uname.
lr_head->lchg_date_time = lv_timestamp.
lr_head->crea_date_time = lv_timestamp.
" Create New date entry
io_modify->create(
EXPORTING
iv_node = is_ctx-node_key " Node to Create
* is_data = lr_head_copy " Data
is_data = lr_head " Data
IMPORTING
ev_key = DATA(lv_head_copy_key)
).
LOOP AT lt_item REFERENCE INTO DATA(lr_item) WHERE parent_key = lr_head->key.
CLEAR: lr_item->linecounter,
lr_item->crea_date_time,
lr_item->crea_uname,
lr_item->lchg_date_time,
lr_item->lchg_uname.
lr_item->crea_uname = sy-uname.
lr_item->lchg_uname = sy-uname.
lr_item->lchg_date_time = lv_timestamp.
lr_item->crea_date_time = lv_timestamp.
io_modify->create(
EXPORTING
iv_node = zif_cds_i_time_header13_c=>sc_node-zcds_i_time_item1 " Node to Create
* is_data = lr_item_copy " Data
is_data = lr_item " Data
iv_assoc_key = zif_cds_i_time_header13_c=>sc_association-zcds_i_time_header1-_item " Association
iv_source_node_key = zif_cds_i_time_header13_c=>sc_node-zcds_i_time_header1 " Parent Node
iv_source_key = lv_head_copy_key " NodeID of Parent Instance
).
ENDLOOP.
ENDLOOP.
io_modify->end_modify( iv_process_immediately = abap_true ).
ENDMETHOD.
ENDCLASS.
Validations
Just making a field mandatory in the CDS View doesn’t make it mandatory for input – it just puts a nice little red star next to the field. To enforce a mandatory field you need to check that it meets your criteria.
In this validation i’m checking that the workdate field is not blank and throwing an error if it is.
CLASS zcl_cds_v_check_header DEFINITION
PUBLIC
INHERITING FROM /bobf/cl_lib_v_supercl_simple
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS /bobf/if_frw_validation~execute
REDEFINITION .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_cds_v_check_header IMPLEMENTATION.
METHOD /bobf/if_frw_validation~execute.
DATA lt_head TYPE ztcds_i_time_header13.
" Retrieve the data of the requested node instance
io_read->retrieve(
EXPORTING
iv_node = is_ctx-node_key
it_key = it_key
IMPORTING
et_data = lt_head
eo_message = eo_message
et_failed_key = et_failed_key
).
LOOP AT lt_head ASSIGNING FIELD-SYMBOL(<fs_head>).
IF <fs_head>-workdate IS INITIAL.
IF <fs_head>-isactiveentity = abap_false.
DATA(lv_lifetime) = /bobf/cm_frw=>co_lifetime_state. "draft
ELSE.
lv_lifetime = /bobf/cm_frw=>co_lifetime_transition. "active
ENDIF.
eo_message = /bobf/cl_frw_factory=>get_message( ).
eo_message->add_message(
EXPORTING is_msg = VALUE #( msgid = 'TimeSheet' "
msgno = 1
msgv1 = 'Workdate cannot be blank: '
msgv2 = <fs_head>-workdate
msgty = /bobf/cm_frw=>co_severity_error
)
iv_node = is_ctx-node_key
iv_key = <fs_head>-key
iv_attribute = zif_cds_i_time_header13_c=>sc_node_attribute-zcds_i_time_header1-workdate
iv_lifetime = lv_lifetime
).
APPEND VALUE #( key = <fs_head>-key ) TO et_failed_key.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
OData Service
The Odata service was published from the Header Consumption view using annotation:
@OData.publish: true
I then used transaction /n/iwfnd/maint_service to activate the service and make it visible in BTP for me to use it as the base for my Fiori Elements app.
Fiori App Setup
In BAS I created a List Report Object Page directly from a template.
After choosing the Data Source and Service Selection from our on-premise system, I selected the entities:
Entity Selection
The Navigation Entity to_item comes from the association alias given in the CDS View exposed as an OData service (‘to’ is prefixed to the association alias automatically).
When the project is created you’ll be able to see the metadata extensions in the CDS VAN xml file.
Local annotations can be added in the annotations.xml file if required.
The application can be previewed from the newly create project via the right click menu -> Preview Application. Select the ‘start’ npm script and a new tab should open showing your app.
The screen shot below shows the List Report. The Timesheet ID column is showing that all entries have been persisted to the database table
List Report
Annotations that show how entry is persisted to Active table
Draft entries show as per the screen shot below, with a ‘Draft’ identifier
Draft entries in the List Report
Additional usability settings
I further enhanced the app as follows:
List Report auto load on opening (in manifest.json)
"dataLoadSettings": {
"loadDataOnAppLaunch": "always"
}
Auto Load in Manifest.json
Remove editing status filter field
Remove Editing Status field
Reflecting Changes when the metadata extensions are updated
When making changes in the metadata extensions you need to update the service definition by right clicking on manifest.json and navigating to ‘Open Service Manager’. Then refresh your data source.
Fiori App Usability
Create scenario – Header
The highlighted items below show data automatically populated by the GET_HDR_COUNTER Determination.
Header Create scenario
At this point the draft is saved to the Draft Table as per the CDS View.
Fill in the mandatory fields and click Create – draft table entry is then deleted and entry is added to persistent table as per CDS View.
Create scenario – Line Item
NB Item can only be created from within Header.
Item creation from Header
Creation of line items is done ‘inline’. To facilitate this an adjustment is required to the default manifest.json config.
Setting Create mode to inline
“TimeData” is the ID of the Line Items Facet in the metadata extension for the header view.
Fill in mandatory fields and click Apply – this confirms the line item associated to the header entry (Draft is saved in background as per same process as for header but using item draft table)
Creating a Line Item
Item is still draft at this point (and possibly header is also still draft). Click Create to confirm entry(ies).
Object is then confirmed as created. The Timesheet entry (object) then shows as persisted in list report.
Persisted Entry
Edit(Update) / Delete functionality
Once a record is persisted to the transparent table it can be edited and deleted within the app.
These actions are enabled by annotations:
@ObjectModel.createEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.updateEnabled:true
@ObjectModel.draftEnabled: true
When a record (header or item) is edited the audit info for last changed is updated – this is taken care of in the Determination class.
Multiple selections can be made for delete option
This is activated through a manifest.json entry (by default only 1 entry can be selected).
Selecting multiple items in a table
Selection Fields
Selection fields are generally added via the annotation:
@UI.selectionField: [{position: 10}]
In order to enhance a date field to allow range input the following is required:
Date field selection Range
@Consumption.filter.selectionType: #INTERVAL
along with an update to manifest.json
Date Range Manifest addition
Arrangement of Fields in the app
(Excuse the rudimentary highlighting!)
The Header groupings below are controlled by Field Groups in the metadata extension.
Field Groups highlighted
Field Groups linked to metadata extension
Similarly for the Item page
Item Field Groups highlighted
Item Field Groups link to metadata extension
Linking item to Field Group
This is done via qualifiers
Item to Field Group linking
Showing cumulative values of Hours/Minutes from line items in header and on list report
Showing Hours/Minutes in the Header List Report
Hours/Minutes in the Object Page
The value comes from LongHrsMins from the header Consumption view – calculated from Item Hours views.
Update annotation.xml to include the field in the List Report.
List Report Annotations for Hours/Minutes
and update the Metadata Extension for the Header to show the field in the Object Page
Metadata Extension update for Hours/Minutes
Final Thoughts
Combining BOPF with SAP Fiori Elements and CDS is a really powerful set of tools at your disposal and if you are willing to commit the time, the possibilities are endless. I decided against extending the app using extensions as it wasn’t required to achieve my simple example, but combining the templates with extensions should allow you to achieve (almost) anything you might have previously considered a freestlye UI5 app for.
Thanks for the wonderful guide, Mark.
Thank you so much for the detailed demo!
Thanks for this wonderful post. It helped me a lot to understand CDS BOPF.
Very Good Blog with end to end example.
Mark Castle I have no doubt got it to work.
I saw the elaborate comprehensiveness and tried and failed!
But on the way learnt something.
I hope someone really duplicates and publishes 1 repository by ABAPGIT a much unused tool, with me able to pull into S4/HANA server
A second standard git repository for BAS/VSCODE .
As I went deeper I am not convinced that one has to edit manifest.json or edmx files to make apps run. It should be simpler I hope.
Issues faced by me a beginner with access only to docker ABAP 1909
1. 2 Number Ranges by SNRO
In the Business Object i created Determinations for both the Header and Line Items. These are used to get the next number in range for both the Header and Line Item counter.
Only header code provided; need to create similar for ITEM which is not easy.
2. The CDS have cross dependency so one has to delete and re-insert to overcome cross dependency between esp header and item
3. ZCDS_VH_TIMETYPES has I_DomainFixedValue is not supported any more with SAP future proofing on RAPpers
Once has to drop domain as storage of values and use simple table.
Luckily searched and hit for a nice work around
https://www.sapdev.eu/cds-and-domain-fixed-values-as-value-help-in-ecc-but-not-s-4hana/
4. I hit this error
Service ZCDS_C_TIME_HEADER_CDS is not active [OData Exposure]
I needed to create this
https://help.sap.com/docs/SAP_NETWEAVER_750/cc0c305d2fab47bd808adcad3ca7ee9d/1b023c1cad774eeb8b85b25c86d94f87.html?locale=en-US&version=7.5.17
5. The BOPF generated did not have 13 suffix but only 1
ZSCDS_I_TIME_HEADER1
ZTCDS_I_TIME_HEADER1
ZIF_CDS_I_TIME_HEADER1_C
As I used Eclipse ADT decided to edit the classes changing 13 to 1
6. PERSNO is not available in developer trial editions; I used NUM08
7. I used VSCODE instead of BAS
8. There are 2 manifest.json one under dist and one under webide
Not knowing edited both. Nothing else.
9. Ended in failure. When I press Item create button F12 Chrome shows many errors as shared in link err.txt
First page OK

Second crash after clicking create!
Hi, can you help me? I found the same mistakes. Is there any sample that it's working? I'm begginer with SAP Fiori Elements.
Editing manifest.json or edmx files to make apps run is to me most inadvisable!
It is so disappointing that after a lot of effort you end up in Fear Uncerainty and Doubt
I wrote this blog -- please see "My only blog on SQL Console"
Was subject to lot of Editorial scrutiny which I welcome and appreciate
https://blogs.sap.com/2023/01/11/sapgui-sqlconsole-utility-hosted-on-github/
But how this blog cleared Editorial review is surprising!
It is a good blog but Mark needs to make sure that he corrects and reviews the whole thing.
____________________________________________________________
Renato Castro please try Amit Diwane and Uladzislau Pralat
SAPUI5 application Simple SAP Airlines Model from a very comprehensive example by Amit Diwane
https://blogs.sap.com/2020/06/26/sap-abap-programming-model-for-fiori-list-report-application-part-1/
https://blogs.sap.com/2020/06/26/sap-abap-programming-model-for-fiori-list-report-application-part-2/
https://blogs.sap.com/2020/06/26/sap-abap-programming-model-for-fiori-list-report-application-part-3/
Launchpad taken from this competent link
https://blogs.sap.com/2017/09/20/how-to-create-simplest-fiori-application-locally-step-by-step-from-cds-view-to-fiori-launchpad.-part-2./
I got deceived by many stale and wrong links in the blogs so arrived at these after a lot of pain
2 links I read recently seem from the Competent
https://blogs.sap.com/2020/01/09/fiori-elements-vs-fiori-freestyle-fare-comparison/
and the UI5 link he refers on Fiori FreeStyle
https://blogs.sap.com/2019/12/27/ui5-advanced-programming-model-overview-ui5con-2019/
Regards
Jayanta
Mark Castle
On the Edit (Update) functionality part, when we navigate from List Report to Object page and we want the EDIT mode to be dynamic based on the content - example based on Status field on the header page. How can this be achieved !