Skip to Content
Technical Articles

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.

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. For more info, see the “ABAP Programming Model for SAP FIORI” documentation.

 

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 <CTRL> 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.

 

Summary

While this is a lengthy blog post. It’s straightforward and shows how to push a Draft Enabled List report a little beyond the standard 3 level option available in the binding wizard.

 

 

9 Comments
You must be Logged on to comment or reply to a post.
  • Thanks for the blog paul,. I faced the issue with multi child level cds views with Transactional capabilities sometime back. Later I realized that root cds view key should be available not only in the child cds views but also in the sub child cds views.

  • Hi Paul,

    have you thought about providing the source of your examples as a abapGit project? That would avoid all the manual creation steps and others could contribute to this great example.

    Best regards
    Gregor

  • Is Deep Insert capability from OData (DPC EXT Class) compatible with Fiori Elements/Smart Templates List Report? Tried other example here but only allow me to update 1 item at a time – the method is not being called at all.

    • Not exactly sure what you’re asking but no, I do not believe the framework does a deep entity create. As you are navigating from object to object it does a create in the draft tables. When you press save it copies the data from the draft tables into the tables specified in the @ObjectModel.writeActivePersistence: annotations.

      In other words, draft mode sort of simulates a deep entity by buffering data in draft tables. 

  • Hi Paul,

    Thanks for detailed explanation. I just have one question, I followed all steps, as mentioned by you, but I run into error while activating the “EXECUTE” method.

     DATA lt_data TYPE ztisport_tp.

    ZTISPORT_TP is not available, how do I generate this or what should be the type of this. Please clarify

  • Hi Paul,

    Thanks for the detailed blog. I followed your blog exactly but I’m getting the below error while saving the draft.

    ‘Error – You can’t save. The draft is not consistent.’

    The draft is working fine.

    I checked the draft table and the field ‘DRAFTENTITYCONSISTENCYSTATUS’ is empty.Please clarify.