SAP Fiori for SAP S/4HANA, SAPUI5

Multiple Level/Multiple Child Draft Enabled FIORI Elements List Report

Overview

This blog post is to show how to create a 4 level deep draft enabled List report. This will be a lengthy post as many objects need to be created to show how to structure the business object with more than 3 levels and multiple children.

The other objective here is to show how to enable a Flexible Column Layout based List Report to go “deeper” than 3 levels and how to add multiple children to an object page via annotations and edits to manifest.json.

Read More: SAP Fiori System Administration Certification Preparation Guide

This example will use a simple sports hierarchy. The top level will be a “Sport” (i.e. Basketball) table as the root of the BOPF Business Object. The sport table will have a “League” table as a child (i.e. NCAA or NBA). The league table will have a “Team” table under it (i.e Atlanta, Phoenix etc..). And finally the team table will have 2 child tables “Coach” and “Player”. See below.

Table Definitions

Create the tables and define the key associations. Note: Each table should have a UUID as the primary key. There is a workaround for using natural keys instead of UUIDs but it’s not recommended unless there is no other option. There will also be a human readable ID associated with each table that will be assigned via a determination. Also, the tables need the direct parent and the root (zsport) as foreign keys in all child tables. This will become more clear once @ObjectModel annotations are created in the CDS Views.

ZSPORT

@EndUserText.label : 'The Sport Table'
@AbapCatalog.enhancementCategory : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #LIMITED
define table zsport {
  key client        : abap.clnt not null;
  key contract_uuid : snwd_node_key not null;
  sport_no          : vbeln_va not null;
  name              : abap.sstring(120);
  include /bobf/s_lib_admin_data;

}

ZLEAGUE

@EndUserText.label : 'The League Table'
@AbapCatalog.enhancementCategory : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #LIMITED
define table zleague {
  key client    : abap.clnt not null;
  key leauge_uuid : snwd_node_key not null;
  @AbapCatalog.foreignKey.screenCheck : false
  sport_uuid : snwd_node_key not null
    with foreign key [0..*,1] zsport
      where client = zleague.client
        and sport_uuid = zleague.sport_uuid;
  league_no    : posnr_va not null;
  name   : abap.sstring(120);
  include /bobf/s_lib_admin_data;

}

ZTEAM

@EndUserText.label : 'The Team Table'
@AbapCatalog.enhancementCategory : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #LIMITED
define table zteam {
  key client    : abap.clnt not null;
  key team_uuid : snwd_node_key not null;
  @AbapCatalog.foreignKey.screenCheck : false
  league_uuid   : snwd_node_key not null
    with foreign key [0..*,1] zleague
      where client = zteam.client
        and league_uuid = zteam.league_uuid;
  @AbapCatalog.foreignKey.screenCheck : false
  sport_uuid    : snwd_node_key not null
    with foreign key [0..*,1] zsport
      where client = zteam.client
        and sport_uuid = zteam.sport_uuid;
  team_no       : posnr_va not null;
  name          : abap.sstring(120);
  include /bobf/s_lib_admin_data;

}

ZPLAYER

@EndUserText.label : 'The Player Table'
@AbapCatalog.enhancementCategory : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #LIMITED
define table zplayer {
  key client      : abap.clnt not null;
  key player_uuid : snwd_node_key not null;
  @AbapCatalog.foreignKey.screenCheck : false
  team_uuid       : snwd_node_key not null
    with foreign key [0..*,1] zteam
      where client = zplayer.client
        and team_uuid = zplayer.team_uuid;
  @AbapCatalog.foreignKey.screenCheck : false
  sport_uuid      : snwd_node_key not null
    with foreign key [0..*,1] zsport
      where client = zplayer.client
        and sport_uuid = zplayer.sport_uuid;
  player_no       : posnr_va not null;
  name            : abap.sstring(120);
  include /bobf/s_lib_admin_data;

}

ZCOACH

@EndUserText.label : 'The Coach Table'
@AbapCatalog.enhancementCategory : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #LIMITED
define table zcoach {

  key client      : abap.clnt not null;
  key coach_uuid : snwd_node_key not null;
  @AbapCatalog.foreignKey.screenCheck : false
  team_uuid       : snwd_node_key not null
    with foreign key [0..*,1] zteam
      where client = zcoach.client
        and team_uuid = zcoach.team_uuid;
  @AbapCatalog.foreignKey.screenCheck : false
  sport_uuid      : snwd_node_key not null
    with foreign key [0..*,1] zsport
      where client = zcoach.client
        and sport_uuid = zcoach.sport_uuid;
  player_no       : posnr_va not null;
  name            : abap.sstring(120);
  include /bobf/s_lib_admin_data;

}

CDS View Definitions

There are 3 levels of CDS that will be defined. Basic, Composite and Consumption.

Create the Basic Views. Note: Do not alias any column names here. It is necessary to include an association to all direct child views in the parent views. In the child views it is necessary to create an association to the direct parent and the root view (in this case the SPORT view).

As the views are being created, you will need to go back to the parent view to add the child associations once the child is created.

Z_I_SPORT

@AbapCatalog.sqlViewName: 'ZISPORT'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Sport Basic View'
@VDM.viewType: #BASIC

define view Z_I_SPORT as select from zsport 
{
//zsport
key sport_uuid,
sport_no,
name,
crea_date_time,
crea_uname,
lchg_date_time,
lchg_unamee
    
}

Z_I_LEAGUE

@AbapCatalog.sqlViewName: 'ZILEAGUE'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The League Basic View'
@VDM.viewType: #BASIC
define view Z_I_LEAGUE as select from zleague 
    association[1..1] to Z_I_SPORT as _zsport on $projection.sport_uuid = _zsport.sport_uuid
{
    //zleague
    key league_uuid,
    @ObjectModel.foreignKey.association:'_zsport'
    sport_uuid,
    @ObjectModel.text.element:['name']
    league_no,
    @Semantics.text:true
    name,
    crea_date_time,
    crea_uname,
    lchg_date_time,
    lchg_uname, 
    
    //associations
    _zsport
    
}

Now that Z_I_LEAGUE is created, we need to go back to Z_I_SPORT and add an association to it.

Z_I_SPORT(EDIT).

...
define view Z_I_SPORT as select from zsport 
    association[0..*] to z_i_league as _zleague on $projection.sport_uuid = _zleague.sport_uuid
{
...
//Assocations
_zleague
    
}

Z_I_TEAM

@AbapCatalog.sqlViewName: 'ZITEAM'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Team Basic View'
@VDM.viewType: #BASIC
define view Z_I_TEAM as select from zteam 
    association[1..1] to Z_I_SPORT as _zsport on $projection.sport_uuid = _zsport.sport_uuid
    association[1..1] to Z_I_LEAGUE as _zleague on $projection.league_uuid = _zleague.league_uuid
{
    //zteam
    key team_uuid,
    @ObjectModel.foreignKey.association:'_zleague'
    league_uuid,
    @ObjectModel.foreignKey.association:'_zsport'
    sport_uuid,
    @ObjectModel.text.element:['name']
    team_no,
    @Semantics.text:true
    name,
    crea_date_time,
    crea_uname,
    lchg_date_time,
    lchg_uname, 
    
    //Associations 
    _zsport,
    _zleague
}

Now that Z_I_TEAM is created, go back to Z_I_LEAGUE and add the association to it.

Z_I_LEAGUE(EDIT)

...
define view Z_I_LEAGUE as select from zleague 
    association[1..1] to Z_I_SPORT as _zsport on $projection.sport_uuid = _zsport.sport_uuid
    association[0..*] to Z_I_TEAM as _zteam on $projection.league_uuid = _zteam.league_uuid
{
...
    _zteam
    
}

Z_I_PLAYER

@AbapCatalog.sqlViewName: 'ZIPLAYER'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Player Basic View'
@VDM.viewType: #BASIC

define view Z_I_PLAYER
  as select from zplayer
  association [1..1] to Z_I_SPORT as _zsport on $projection.sport_uuid = _zsport.sport_uuid
  association [1..1] to Z_I_TEAM  as _zteam  on $projection.team_uuid = _zteam.team_uuid

{
      //zplayer
  key player_uuid,
      @ObjectModel.foreignKey.association:'_zteam'
      team_uuid,
      @ObjectModel.foreignKey.association:'_zsport'
      sport_uuid,
      @ObjectModel.text.element:['name']
      player_no,
      @Semantics.text: true
      name,
      crea_date_time,
      crea_uname,
      lchg_date_time,
      lchg_uname,

      //Assocations
      _zsport,
      _zteam

}

Z_I_COACH

@AbapCatalog.sqlViewName: 'ZICOACH'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Coach Basic View'
@VDM.viewType: #BASIC

define view Z_I_COACH
  as select from zcoach
  association [1..1] to Z_I_SPORT as _zsport on $projection.sport_uuid = _zsport.sport_uuid
  association [1..1] to Z_I_TEAM  as _zteam  on $projection.team_uuid = _zteam.team_uuid

{
      //zplayer
  key coach_uuid,
      @ObjectModel.foreignKey.association:'_zteam'
      team_uuid,
      @ObjectModel.foreignKey.association:'_zsport'
      sport_uuid,
      @ObjectModel.text.element:['name']
      coach_no,
      @Semantics.text: true
      name,
      crea_date_time,
      crea_uname,
      lchg_date_time,
      lchg_uname,

      //Assocations
      _zsport,
      _zteam

}

Now that Z_I_COACH and Z_I_PLAYER are created, go back to Z_I_TEAM and add the relevant associations

Z_I_TEAM(EDIT)

...
select from zteam 
    ...
    association[0..*] to Z_I_PLAYER as _zplayer on $projection.team_uuid = _zplayer.team_uuid
    association[0..*] to Z_I_COACH as _zcoach on $projection.team_uuid = _zcoach.team_uuid
{
...
    _zplayer,
    _zcoach
}

Create the Composite Views.

This is the level where the transactional processing will be defined. A BOPF Business Object will be generated after creating the composite views. Also, do not alias columns at this level either. Similar to the BASIC views, we will need to come back and add the child associations to the parent views after they are created.This will cause BOPF generation errors. You may need to go back through and activate each view after adding the child associations back to the parent.

The @ObjectModel.writeDraftPersistence will automatically create the draft table, there is no need to manually create it. The @ObjectModel.semanticKey is the most specific non UUID field.

Z_I_SPORT_TP

@AbapCatalog.sqlViewName: 'ZISPORTTP'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Sport Composite View'
@VDM.viewType: #COMPOSITE

@ObjectModel.modelCategory:#BUSINESS_OBJECT
@ObjectModel.compositionRoot:true
@ObjectModel.transactionalProcessingEnabled:true
@ObjectModel.writeActivePersistence:'ZSPORT'
@ObjectModel.createEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.updateEnabled:true
@ObjectModel.draftEnabled: true
@ObjectModel.semanticKey:['sport_no']

@ObjectModel.writeDraftPersistence: 'ZSPORT_D'

define view Z_I_SPORT_TP as select from Z_I_SPORT {
    //Z_I_SPORT
    key sport_uuid,
    @ObjectModel.text.element:['name']
    sport_no,
    @Semantics.text:true
    name,
    crea_date_time,
    crea_uname,
    lchg_date_time,
    lchg_uname,
    /* Associations */
    //Z_I_SPORT
    _zleague
}

Z_I_LEAGUE_TP

@AbapCatalog.sqlViewName: 'ZILEAGUETP'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The League Composite View.'
@VDM.viewType: #COMPOSITE

@ObjectModel.modelCategory:#BUSINESS_OBJECT
@ObjectModel.writeActivePersistence:'ZLEAGUE'
@ObjectModel.createEnabled:true
@ObjectModel.updateEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.writeDraftPersistence: 'ZLEAGUE_D'
@ObjectModel.semanticKey:['league_no']

define view Z_I_LEAGUE_TP as select from Z_I_LEAGUE 
    association[1..1] to Z_I_SPORT_TP as _zsport on $projection.sport_uuid = _zsport.sport_uuid
{
    //Z_I_LEAGUE
    key league_uuid,
    sport_uuid,
    league_no,
    name,
    crea_date_time,
    crea_uname,
    lchg_date_time,
    lchg_uname,
    /* Associations */
    //Z_I_LEAGUE
    @ObjectModel.association.type:[ #TO_COMPOSITION_PARENT, #TO_COMPOSITION_ROOT ]
    _zsport,
    _zteam
}

Now go back to Z_I_SPORT_TP and add the association to Z_I_LEAGUE_TP

Z_I_SPORT_TP(EDIT)

...
define view Z_I_SPORT_TP as select from Z_I_SPORT 
    association[0..*] to Z_I_LEAGUE_TP as _zleague on $projection.sport_uuid = _zleague.sport_uuid
{
    ...
    @ObjectModel.association.type: [ #TO_COMPOSITION_CHILD ]
    _zleague
}

Z_I_TEAM_TP

@AbapCatalog.sqlViewName: 'ZITEAMTP'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Team Composite View'
@VDM.viewType: #COMPOSITE

@ObjectModel.modelCategory:#BUSINESS_OBJECT
@ObjectModel.writeActivePersistence:'ZTEAM'
@ObjectModel.createEnabled:true
@ObjectModel.updateEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.writeDraftPersistence: 'ZTEAM_D'
@ObjectModel.semanticKey:['team_no']


define view Z_I_TEAM_TP as select from Z_I_TEAM 
association[1..1] to Z_I_SPORT_TP as _zsport on $projection.sport_uuid = _zsport.sport_uuid
association[1..1] to Z_I_LEAGUE_TP as _zleague on $projection.league_uuid = _zleague.league_uuid
{
    //Z_I_TEAM
    key team_uuid,
    league_uuid,
    sport_uuid,
    team_no,
    name,
    crea_date_time,
    crea_uname,
    lchg_date_time,
    lchg_uname,
    /* Associations */
    //Z_I_TEAM
    _zcoach,
    @ObjectModel.association.type:[ #TO_COMPOSITION_PARENT]
    _zleague,
    _zplayer,
    @ObjectModel.association.type:[ #TO_COMPOSITION_ROOT ]
    _zsport
}

Now that Z_I_TEAM_TP is created, go back into Z_I_LEAGUE_TP and add it as an association.

Z_I_LEAGUE_TP(EDIT)

...
define view Z_I_LEAGUE_TP as select from Z_I_LEAGUE 
    ...
    association[0..*] to Z_I_TEAM_TP as _zteam on $projection.league_uuid = _zteam.league_uuid
{
...
    @ObjectModel.association.type:[ #TO_COMPOSITION_CHILD ]
    _zteam
}

Z_I_COACH_TP

@AbapCatalog.sqlViewName: 'ZICOACHTP'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Coach Composite View'
@VDM.viewType: #COMPOSITE

@ObjectModel.modelCategory:#BUSINESS_OBJECT
@ObjectModel.writeActivePersistence:'ZCOACH'
@ObjectModel.createEnabled:true
@ObjectModel.updateEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.writeDraftPersistence: 'ZCOACH_D'
@ObjectModel.semanticKey:['coach_no']

define view Z_I_COACH_TP as select from Z_I_COACH 
    association[1..1] to Z_I_SPORT_TP as _zsport on $projection.sport_uuid = _zsport.sport_uuid
    association[1..1] to Z_I_TEAM_TP as _zteam on $projection.team_uuid = _zteam.team_uuid
{
    //Z_I_COACH
    key coach_uuid,
    team_uuid,
    sport_uuid,
    coach_no,
    name,
    crea_date_time,
    crea_uname,
    lchg_date_time,
    lchg_uname,
    /* Associations */
    //Z_I_COACH
    @ObjectModel.association.type:[ #TO_COMPOSITION_ROOT]
    _zsport,
    @ObjectModel.association.type:[ #TO_COMPOSITION_PARENT]
    _zteam
}

Z_I_PLAYER_TP

@AbapCatalog.sqlViewName: 'ZIPLAYERTP'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Player Composite View'
@VDM.viewType: #COMPOSITE 

@ObjectModel.modelCategory:#BUSINESS_OBJECT
@ObjectModel.writeActivePersistence:'ZPLAYER'
@ObjectModel.createEnabled:true
@ObjectModel.updateEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.writeDraftPersistence: 'ZPLAYER_D'
@ObjectModel.semanticKey:['player_no']

define view Z_I_PLAYER_TP as select from Z_I_PLAYER 
    association[1..1] to Z_I_SPORT_TP as _zsport on $projection.sport_uuid = _zsport.sport_uuid
    association[1..1] to Z_I_TEAM_TP as _zteam on $projection.team_uuid = _zteam.team_uuid
{
    //Z_I_PLAYER
    key player_uuid,
    team_uuid,
    sport_uuid,
    player_no,
    name,
    crea_date_time,
    crea_uname,
    lchg_date_time,
    lchg_uname,
    /* Associations */
    //Z_I_PLAYER
    @ObjectModel.association.type:[ #TO_COMPOSITION_ROOT]
    _zsport,
    @ObjectModel.association.type:[ #TO_COMPOSITION_PARENT]
    _zteam   
}

Now that Z_I_PLAYER_TP and Z_I_COACH_TP are created, go back to Z_I_TEAM_TP and add the relevant associations.

Z_I_TEAM_TP(EDIT)

...
define view Z_I_TEAM_TP as select from Z_I_TEAM 
...
association[0..*] to Z_I_PLAYER_TP as _zplayer on $projection.team_uuid = _zplayer.team_uuid
association[0..*] to Z_I_COACH_TP as _zcoach on $projection.team_uuid = _zcoach.team_uuid
{
    ...
    @ObjectModel.association.type:[ #TO_COMPOSITION_CHILD ]
    _zcoach,
    @ObjectModel.association.type:[ #TO_COMPOSITION_CHILD ]
    _zplayer,
...
}

Create the Consumption Views.

These are the Views that will be exposed via an OData service. They will read off of the composite views. They are created much like the BASIC and COMPOSITE views in that you must create them, create the child view then add the child view back as an association to the parent. I will just give them in their entirety here. The one thing to take note of is the @ObjectModel.transactionalProcessingDelegated: true This delegates the transactional processing to the composite views.

Z_C_SPORT

@AbapCatalog.sqlViewName: 'ZCSPORT'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Sport Consumption View'
@VDM.viewType: #CONSUMPTION


@ObjectModel.compositionRoot: true
@ObjectModel.transactionalProcessingDelegated: true
@ObjectModel.createEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.updateEnabled:true
@ObjectModel.draftEnabled: true

@ObjectModel.semanticKey:['sport_no']

@Metadata.allowExtensions: true

@UI.headerInfo.description.label: 'Sport'

define view Z_C_SPORT as select from Z_I_SPORT_TP 
    association[0..*] to Z_C_LEAGUE as _zleague on $projection.sport_uuid = _zleague.sport_uuid
{
 //Z_I_SPORT_TP
 key sport_uuid,
 @ObjectModel.readOnly: true
 sport_no,
 name,
 crea_date_time,
 crea_uname,
 lchg_date_time,
 lchg_uname,
 /* Associations */
@ObjectModel.association.type: [ #TO_COMPOSITION_CHILD ]
 _zleague   
}

Z_C_LEAGUE

@AbapCatalog.sqlViewName: 'ZCLEAGUE'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The League Consumption View'
@VDM.viewType: #CONSUMPTION

@ObjectModel.semanticKey:['league_no']
@Metadata.allowExtensions: true


@ObjectModel.createEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.updateEnabled:true

@UI.headerInfo.description.label: 'League'

define view Z_C_LEAGUE as select from Z_I_LEAGUE_TP 
    association[1..1] to Z_C_SPORT as _zsport on $projection.sport_uuid = _zsport.sport_uuid
    association[0..*] to Z_C_TEAM as _zteam on $projection.league_uuid = _zteam.league_uuid
{
  //Z_I_LEAGUE_TP
  key league_uuid,
  
  sport_uuid,
  
  @ObjectModel.readOnly: true
  league_no,
  name,
  crea_date_time,
  crea_uname,
  lchg_date_time,
  lchg_uname,
  /* Associations */
  //Z_I_LEAGUE_TP
  @ObjectModel.association.type:  [ #TO_COMPOSITION_ROOT, #TO_COMPOSITION_PARENT ]
  _zsport,
  @ObjectModel.association.type:  [ #TO_COMPOSITION_CHILD ]
  _zteam  
}

Z_C_TEAM

@AbapCatalog.sqlViewName: 'ZCTEAM'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Team Consumption View'
@VDM.viewType: #CONSUMPTION

@ObjectModel.semanticKey:['team_no']
@Metadata.allowExtensions: true


@ObjectModel.createEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.updateEnabled:true

@UI.headerInfo.description.label: 'Team'

define view Z_C_TEAM as select from Z_I_TEAM_TP 
  association[1..1] to Z_C_SPORT as _zsport on $projection.sport_uuid = _zsport.sport_uuid
  association[1..1] to Z_C_LEAGUE as _zleague on $projection.league_uuid = _zleague.league_uuid
  association[0..*] to Z_C_COACH as _zcoach on $projection.team_uuid = _zcoach.team_uuid
  association[0..*] to Z_C_PLAYER as _zplayer on $projection.team_uuid = _zplayer.team_uuid
{
    //Z_I_TEAM_TP
    key team_uuid,
    league_uuid,
    sport_uuid,
    @ObjectModel.readOnly: true
    team_no,
    name,
    crea_date_time,
    crea_uname,
    lchg_date_time,
    lchg_uname,
    /* Associations */
    //Z_I_TEAM_TP
     @ObjectModel.association.type:  [ #TO_COMPOSITION_CHILD ]
    _zcoach,
    @ObjectModel.association.type:  [ #TO_COMPOSITION_PARENT ]
    _zleague,
     @ObjectModel.association.type:  [ #TO_COMPOSITION_CHILD ]
    _zplayer,
    @ObjectModel.association.type:  [ #TO_COMPOSITION_ROOT ]
    _zsport
}

Z_C_COACH

@AbapCatalog.sqlViewName: 'ZCCOACH'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Coach Consumption View'

@VDM.viewType: #CONSUMPTION

@ObjectModel.semanticKey:['coach_no']
@Metadata.allowExtensions: true


@ObjectModel.createEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.updateEnabled:true

@UI.headerInfo.description.label: 'Coach'

define view Z_C_COACH as select from Z_I_COACH_TP 
  association[1..1] to Z_C_SPORT as _zsport on $projection.sport_uuid = _zsport.sport_uuid
  association[1..1] to Z_C_TEAM as _zteam on $projection.team_uuid = _zteam.team_uuid
{
//Z_I_COACH_TP
key coach_uuid,
team_uuid,
sport_uuid,

@ObjectModel.readOnly: true
coach_no,
name,
crea_date_time,
crea_uname,
lchg_date_time,
lchg_uname,
/* Associations */
//Z_I_COACH_TP
 @ObjectModel.association.type:  [ #TO_COMPOSITION_ROOT ]
_zsport,
 @ObjectModel.association.type:  [ #TO_COMPOSITION_PARENT ]
_zteam
    
}

Z_C_PLAYER

@AbapCatalog.sqlViewName: 'ZCPLAYER'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'The Player Consumption View'

@VDM.viewType: #CONSUMPTION

@ObjectModel.semanticKey:['player_no']
@Metadata.allowExtensions: true


@ObjectModel.createEnabled:true
@ObjectModel.deleteEnabled:true
@ObjectModel.updateEnabled:true

@UI.headerInfo.description.label: 'Player'

define view Z_C_PLAYER as select from Z_I_PLAYER_TP 
  association[1..1] to Z_C_SPORT as _zsport on $projection.sport_uuid = _zsport.sport_uuid
  association[1..1] to Z_C_TEAM as _zteam on $projection.team_uuid = _zteam.team_uuid
{
//Z_I_PLAYER_TP
key player_uuid,
team_uuid,
sport_uuid,

@ObjectModel.readOnly: true
player_no,
name,
crea_date_time,
crea_uname,
lchg_date_time,
lchg_uname,
/* Associations */
//Z_I_PLAYER_TP
 @ObjectModel.association.type:  [ #TO_COMPOSITION_ROOT ]
_zsport,
 @ObjectModel.association.type:  [ #TO_COMPOSITION_PARENT ]
_zteam
    
}

That completes the 3 levels of CDS Views that are needed on top of the tables. The next step is to create metadata extensions for the Consumption Views to drive the front end elements app.

Add Metadata Extensions

Z_E_SPORT

@Metadata.layer: #CUSTOMER
annotate view Z_C_SPORT
    with 
{
         @UI.facet: [{
                 label : 'General Information',
                 id : 'GeneralInfo',
                 purpose: #STANDARD,
                 type : #COLLECTION,
                 position: 10
             },
             {
                 label:'Sport Info',
                 id : 'SportInfo',
                 purpose: #STANDARD,
                 parentId : 'GeneralInfo',
                 type : #FIELDGROUP_REFERENCE,
                 targetQualifier : 'basic',
                 position: 20
             },
             {
                 label: 'Leagues',
                 id  : 'leagues',
                 purpose: #STANDARD,
                 type : #LINEITEM_REFERENCE,
                 targetElement: '_zleague',
                 position: 30
             }]

  @UI.lineItem: [{ importance: #HIGH, label: 'Sport Number', position: 40 }]
  @UI.fieldGroup: [{    label: 'Sport Number',
                         qualifier: 'basic',
                         position: 40   }]
  sport_no;

  @UI.lineItem: [{ importance: #HIGH, label: 'Sport Name', position: 50 }]
  @UI.fieldGroup: [{    label: 'Sport Name',
                        qualifier: 'basic',
                        position: 50   }]
  name;
    
    
}

Z_E_LEAGUE

@Metadata.layer: #CUSTOMER
annotate view Z_C_LEAGUE
    with 
{
         @UI.facet: [{
                 label : 'General Information',
                 id : 'GeneralInfo',
                 purpose: #STANDARD,
                 type : #COLLECTION,
                 position: 10
             },
             {
                 label:'League Info',
                 id : 'LeagueInfo',
                 purpose: #STANDARD,
                 parentId : 'GeneralInfo',
                 type : #FIELDGROUP_REFERENCE,
                 targetQualifier : 'basic',
                 position: 20
             },
             {
                 label: 'Teams',
                 id  : 'teams',
                 purpose: #STANDARD,
                 type : #LINEITEM_REFERENCE,
                 targetElement: '_zteam',
                 position: 30
             }]

  @UI.lineItem: [{ importance: #HIGH, label: 'League Number', position: 40 }]
  @UI.fieldGroup: [{    label: 'League Number',
                         qualifier: 'basic',
                         position: 40   }]
  league_no;

  @UI.lineItem: [{ importance: #HIGH, label: 'League Name', position: 50 }]
  @UI.fieldGroup: [{    label: 'League Name',
                        qualifier: 'basic',
                        position: 50   }]
  name;
    
    
}

Z_E_TEAM

@Metadata.layer: #CUSTOMER
annotate view Z_C_TEAM
    with 
{
         @UI.facet: [{
                 label : 'General Information',
                 id : 'GeneralInfo',
                 purpose: #STANDARD,
                 type : #COLLECTION,
                 position: 10
             },
             {
                 label:'Team Info',
                 id : 'TeamInfo',
                 purpose: #STANDARD,
                 parentId : 'GeneralInfo',
                 type : #FIELDGROUP_REFERENCE,
                 targetQualifier : 'basic',
                 position: 20
             },
             {
                 label: 'Players',
                 id  : 'player',
                 purpose: #STANDARD,
                 type : #LINEITEM_REFERENCE,
                 targetElement: '_zplayer',
                 position: 30
             },
             {
                 label: 'Coaches',
                 id  : 'coaches',
                 purpose: #STANDARD,
                 type : #LINEITEM_REFERENCE,
                 targetElement: '_zcoach',
                 position: 40
             }]

  @UI.lineItem: [{ importance: #HIGH, label: 'Team Number', position: 50 }]
  @UI.fieldGroup: [{    label: 'Team  Number',
                         qualifier: 'basic',
                         position: 50   }]
  team_no;

  @UI.lineItem: [{ importance: #HIGH, label: 'Team Name', position: 60 }]
  @UI.fieldGroup: [{    label: 'Team Name',
                        qualifier: 'basic',
                        position: 60   }]
  name;
    
    
}

Z_E_PLAYER

@Metadata.layer: #CUSTOMER
annotate view Z_C_PLAYER
    with 
{
         @UI.facet: [{
                 label : 'General Information',
                 id : 'GeneralInfo',
                 purpose: #STANDARD,
                 type : #COLLECTION,
                 position: 10
             },
             {
                 label:'Player Info',
                 id : 'PlayerInfo',
                 purpose: #STANDARD,
                 parentId : 'GeneralInfo',
                 type : #FIELDGROUP_REFERENCE,
                 targetQualifier : 'basic',
                 position: 20
             }]

  @UI.lineItem: [{ importance: #HIGH, label: 'Player Number', position: 30 }]
  @UI.fieldGroup: [{    label: 'Player  Number',
                         qualifier: 'basic',
                         position: 30   }]
  player_no;

  @UI.lineItem: [{ importance: #HIGH, label: 'Player Name', position: 40 }]
  @UI.fieldGroup: [{    label: 'Player Name',
                        qualifier: 'basic',
                        position: 40   }]
  name;
    
    
}

Z_E_COACH

@Metadata.layer: #CUSTOMER
annotate view Z_C_COACH
    with 
{
         @UI.facet: [{
                 label : 'General Information',
                 id : 'GeneralInfo',
                 purpose: #STANDARD,
                 type : #COLLECTION,
                 position: 10
             },
             {
                 label:'Coach Info',
                 id : 'CoachInfo',
                 purpose: #STANDARD,
                 parentId : 'GeneralInfo',
                 type : #FIELDGROUP_REFERENCE,
                 targetQualifier : 'basic',
                 position: 20
             }]

  @UI.lineItem: [{ importance: #HIGH, label: 'Coach Number', position: 30 }]
  @UI.fieldGroup: [{    label: 'Coach Number',
                         qualifier: 'basic',
                         position: 30   }]
  coach_no;

  @UI.lineItem: [{ importance: #HIGH, label: 'Coach Name', position: 40 }]
  @UI.fieldGroup: [{    label: 'COach Name',
                        qualifier: 'basic',
                        position: 40   }]
  name;
    
    
}

Add Determinations to the BOPF Business Object

In order to internally number the records with a non UUID field, a determination needs to be added to each node of the BOPF Business Object.

1. Open the generated business object in ADT.

2. Select Z_I_SPORT_TP node from the drop down menu beside the BO name

3. Click on “Determinations” then Click “New”

4. Give a Name SPORT_ASSIGN_NO and a Description then click finish.

5. Hold down and click the newly added “SPORT_ASSIGN_NO” in the determinations list

6. Press Activate

7. Click on “Implementation Class”

8. Enter the below code into the /BOBF/IF_FRW_DETERMINATION~EXECUTE method

 method /BOBF/IF_FRW_DETERMINATION~EXECUTE.
    WITH +both AS ( SELECT sport_no FROM zsport  
        UNION ALL
        SELECT sport_no FROM zsport_d )          
    SELECT SINGLE
        FROM +both
        FIELDS MAX( sport_no ) AS sport_no
        INTO @DATA(lv_max_sport_no).

    IF lv_max_sport_no IS INITIAL.
        lv_max_sport_no = '0000000001'.
    ENDIF.

    DATA lt_data TYPE ztisport_tp.

    io_read->retrieve(
        EXPORTING
            iv_node                 = is_ctx-node_key   
            it_key                  = it_key            
        IMPORTING
            eo_message              = eo_message        
            et_data                 = lt_data           
            et_failed_key           = et_failed_key     
    ).

    LOOP AT lt_data REFERENCE INTO DATA(lr_data).
      IF lr_data->sport_no IS INITIAL.
        ADD 1 TO lv_max_sport_no.
        lr_data->sport_no = lv_max_sport_no.

          lr_data->sport_no = |{ lr_data->sport_no alpha = IN }|.

        io_modify->update(
          EXPORTING
            iv_node           = is_ctx-node_key     
            iv_key            = lr_data->key     
            is_data           = lr_data             
            it_changed_fields = VALUE #( ( zif_i_sport_tp_c=>sc_node_attribute-z_i_sport_tp-sport_no ) )
        ).
      ENDIF.
    ENDLOOP.
  endmethod.

Similar implementations of the EXECUTE method are needed for the rest of the nodes. Code for each posted below.

LEAGUE_ASSIGN_NO

method /BOBF/IF_FRW_DETERMINATION~EXECUTE.
      WITH +both AS ( SELECT league_no FROM zleague
        UNION ALL
        SELECT league_no FROM zleague_d )
    SELECT SINGLE
        FROM +both
        FIELDS MAX( league_no ) AS league_no
        INTO @DATA(lv_max_league_no).

    IF lv_max_league_no IS INITIAL.
        lv_max_league_no = '0000000001'.
    ENDIF.

    DATA lt_data TYPE ztileague_tp.

    io_read->retrieve(
        EXPORTING
            iv_node                 = is_ctx-node_key
            it_key                  = it_key
        IMPORTING
            eo_message              = eo_message
            et_data                 = lt_data
            et_failed_key           = et_failed_key
    ).

    LOOP AT lt_data REFERENCE INTO DATA(lr_data).
      IF lr_data->league_no IS INITIAL.
        ADD 1 TO lv_max_league_no.
        lr_data->league_no = lv_max_league_no.

          lr_data->league_no = |{ lr_data->league_no alpha = IN }|.

        io_modify->update(
          EXPORTING
            iv_node           = is_ctx-node_key
            iv_key            = lr_data->key
            is_data           = lr_data
            it_changed_fields = VALUE #( ( zif_i_sport_tp_c=>sc_node_attribute-z_i_league_tp-league_no ) )
        ).
      ENDIF.
    ENDLOOP.
  endmethod.

TEAM_ASSIGN_NO

method /BOBF/IF_FRW_DETERMINATION~EXECUTE.
      WITH +both AS ( SELECT team_no FROM zteam
        UNION ALL
        SELECT team_no FROM zteam_d )
    SELECT SINGLE
        FROM +both
        FIELDS MAX( team_no ) AS team_no
        INTO @DATA(lv_max_team_no).

    IF lv_max_team_no IS INITIAL.
        lv_max_team_no = '0000000001'.
    ENDIF.

    DATA lt_data TYPE ztiteam_tp.

    io_read->retrieve(
        EXPORTING
            iv_node                 = is_ctx-node_key
            it_key                  = it_key
        IMPORTING
            eo_message              = eo_message
            et_data                 = lt_data
            et_failed_key           = et_failed_key
    ).

    LOOP AT lt_data REFERENCE INTO DATA(lr_data).
      IF lr_data->team_no IS INITIAL.
        ADD 1 TO lv_max_team_no.
        lr_data->team_no = lv_max_team_no.

          lr_data->team_no = |{ lr_data->team_no alpha = IN }|.

        io_modify->update(
          EXPORTING
            iv_node           = is_ctx-node_key
            iv_key            = lr_data->key
            is_data           = lr_data
            it_changed_fields = VALUE #( ( zif_i_sport_tp_c=>sc_node_attribute-z_i_team_tp-team_no ) )
        ).
      ENDIF.
    ENDLOOP.
  endmethod.

PLAYER_ASSIGN_NO

method /BOBF/IF_FRW_DETERMINATION~EXECUTE.
      WITH +both AS ( SELECT player_no FROM zplayer
        UNION ALL
        SELECT player_no FROM zplayer_d )
    SELECT SINGLE
        FROM +both
        FIELDS MAX( player_no ) AS player_no
        INTO @DATA(lv_max_player_no).

    IF lv_max_player_no IS INITIAL.
        lv_max_player_no = '0000000001'.
    ENDIF.

    DATA lt_data TYPE ztiplayer_tp.

    io_read->retrieve(
        EXPORTING
            iv_node                 = is_ctx-node_key
            it_key                  = it_key
        IMPORTING
            eo_message              = eo_message
            et_data                 = lt_data
            et_failed_key           = et_failed_key
    ).

    LOOP AT lt_data REFERENCE INTO DATA(lr_data).
      IF lr_data->player_no IS INITIAL.
        ADD 1 TO lv_max_player_no.
        lr_data->player_no = lv_max_player_no.

          lr_data->player_no = |{ lr_data->player_no alpha = IN }|.

        io_modify->update(
          EXPORTING
            iv_node           = is_ctx-node_key
            iv_key            = lr_data->key
            is_data           = lr_data
            it_changed_fields = VALUE #( ( zif_i_sport_tp_c=>sc_node_attribute-z_i_player_tp-player_no ) )
        ).
      ENDIF.
    ENDLOOP.
  endmethod.

COACH_ASSIGN_NO

method /BOBF/IF_FRW_DETERMINATION~EXECUTE.
      WITH +both AS ( SELECT coach_no FROM zcoach
        UNION ALL
        SELECT coach_no FROM zcoach_d )
    SELECT SINGLE
        FROM +both
        FIELDS MAX( coach_no ) AS coach_no
        INTO @DATA(lv_max_coach_no).

    IF lv_max_coach_no IS INITIAL.
        lv_max_coach_no = '0000000001'.
    ENDIF.

    DATA lt_data TYPE zticoach_tp.

    io_read->retrieve(
        EXPORTING
            iv_node                 = is_ctx-node_key
            it_key                  = it_key
        IMPORTING
            eo_message              = eo_message
            et_data                 = lt_data
            et_failed_key           = et_failed_key
    ).

    LOOP AT lt_data REFERENCE INTO DATA(lr_data).
      IF lr_data->coach_no IS INITIAL.
        ADD 1 TO lv_max_coach_no.
        lr_data->coach_no = lv_max_coach_no.

          lr_data->coach_no = |{ lr_data->coach_no alpha = IN }|.

        io_modify->update(
          EXPORTING
            iv_node           = is_ctx-node_key
            iv_key            = lr_data->key
            is_data           = lr_data
            it_changed_fields = VALUE #( ( zif_i_sport_tp_c=>sc_node_attribute-z_i_coach_tp-coach_no ) )
        ).
      ENDIF.
    ENDLOOP.
  endmethod.

That completes the CDS/BOPF work needed in ADT.

The next step is to expose the Consumption views as an OData service.

Create an OData service

1. Launch T-Code SEGW on the S/4 System.
2. Create a new Service

3. Right Click on “Data Model” and select “Reference”->”Data Source”
4. Enter the name of the root Consumption (Z_C_SPORT)
5. Start with SPORT and select all child nodes (See Below)

6. Press the “Generate Runtime Objects” button and accept the defaults

That’s it for the Service! Now log into the Gateway system and expose the service

Add Service to the Gateway System

1. Run T-Code /n/iwfnd/maint_service on the gateway system.
2. Click “Add Service”
3. Set the System Alias for your S/4 System.
4. Enter the Service Name and Press “Get Services”

5. Select the service and press “Add Selected Services”
6. Assign a package, accept defaults and press continue.

The service is now exposed on the gateway.

Create the FIORI Elements List Report.

1. Login to WebIDE on the the SAP Cloud platform.
2. Your gateway system should be configured as a destination on your cloud platform account.
3. Click on “File”->”New”->”Project From Template”
4. Select “List Report Application” and Click “Next”
5. Enter the Basic Project Information and Click “Next”

6. Select your gateway system and service and click “Next”

7. Select both annotation files and click “Next”
8. Select the data bindings and check “Flexible Column Layout”

At this point, this is a working Draft Enabled list report. However, you cannot create or edit Players or Coaches. By default, in the data binding wizard, you can only specify 3 layers. So, manifset.json must be edited manually to add the Player and Coach object pages.

  1. Open manifest.json
  2. find the entry for “ObjectPage|to_zteam”
  3. Add a pages attribute after the component attribute.
"ObjectPage|to_zteam":{
   "navigationProperty":"to_zteam",
   "entitySet":"Z_C_TEAM",
   "component":{
      "name":"sap.suite.ui.generic.template.ObjectPage"
   },
   "pages":{
      "ObjectPage|to_zcoach":{
         "navigationProperty":"to_zcoach",
         "entitySet":"Z_C_COACH",
         "component":{
            "name":"sap.suite.ui.generic.template.ObjectPage"
         }
      },
      "ObjectPage|to_zplayer":{
         "navigationProperty":"to_zplayer",
         "entitySet":"Z_C_PLAYER",
         "component":{
            "name":"sap.suite.ui.generic.template.ObjectPage"
         }
      }
   }
}

That’s it. You should now be able to create/edit/delete Players and Coaches.

Leave a Reply

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