SAP S/4HANA, SAP Fiori, SAP Fiori Elements

SAP Fiori Elements BOPF CRUD App using List Report/Object Page – step-by-step guide

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

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.

Leave a Reply

Your email address will not be published. Required fields are marked *