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.
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
Line Item table Dictionary entry
Mapping Table for SAP User -> Personnel Number mapping
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).
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
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.
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:
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
Draft entries show as per the screen shot below, with a ‘Draft’ identifier
Additional usability settings
I further enhanced the app as follows:
List Report auto load on opening (in manifest.json)
"dataLoadSettings": {
"loadDataOnAppLaunch": "always"
}
Remove editing status filter 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.
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.
Creation of line items is done ‘inline’. To facilitate this an adjustment is required to the default manifest.json config.
“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)
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.
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).
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:
@Consumption.filter.selectionType: #INTERVAL
along with an update to manifest.json
Arrangement of Fields in the app
(Excuse the rudimentary highlighting!)
The Header groupings below are controlled by Field Groups in the metadata extension.
Similarly for the Item page
Linking item to Field Group
This is done via qualifiers
Showing cumulative values of Hours/Minutes from line items in header and on list report
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.
and update the Metadata Extension for the Header to show the field in the Object Page
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.