SAP Change Documents in RAP – CDS Views, BDEF and Change Document Annotation

In Part 1 of this series we built the foundations — custom tables, a Change Document Object in SCDO, and a clean ABAP wrapper class. Now we take it further. In this second and final part, we expose the same customer object through a full RAP Business Object — CDS views, projection views with Fiori annotations, and a Behaviour Definition.

Recommend reading the previous blog: How to Implement SAP Change Document. We will need some objects created in the last blog.

Note: The native changedocuments annotation in the BDEF requires S/4HANA 2022 or later. If you are on an earlier release, you will need to implement change documents manually in the saver class.

I will initially create a RAP application without a Change Document, then amend the relevant objects to enable the Change Document. If you already have a RAP application and would like to enable change document, you need to create a Change Document Object and then jump to Enabling Change Document Integration .

Checklist of Objects

Object TypeObject NamePurpose
TableZZCUSTOMERHeader Table
TableZZCUSTOMER_BANKItem Table
Change Document ObjectZZCUSTOMERTo log Changes
Interface ViewsZZI_Customer, ZZI_Bank
Behaviour DefinitionZZI_CUSTOMER
Projection ViewZZC_Customer, ZZC_Bank
Metadata ExtensionZZC_CUSTOMER, ZZC_BANKUI Annotation
Behaviour Definition (Projection)ZZC_CUSTOMER
Service DefinitionZZCUSTOMER_MGMT
Service BindingZZUI_CUSTOMER_O4

Creating the usual List and Object Page RAP Application for tables ZZCUSTOMER and ZZCUSTOMER_BANK.

Interface View ZZI_Customer

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Customer'
@Metadata.ignorePropagatedAnnotations: true

define root view entity ZZI_Customer
  as select from zzcustomer

  composition [0..*] of ZZI_Bank                as _Bank
{
  key zzcustomer.customer_id                                  as CustomerId,

      zzcustomer.name                                         as Name,
      zzcustomer.street                                       as Street,
      zzcustomer.city                                         as City,
      zzcustomer.postal_code                                  as PostalCode,
      zzcustomer.country                                      as Country,

      @Semantics.user.createdBy: true
      local_created_by                                        as LocalCreatedBy,
      @Semantics.systemDateTime.createdAt: true
      local_created_at                                        as LocalCreatedAt,
      @Semantics.user.localInstanceLastChangedBy: true
      local_last_changed_by                                   as LocalLastChangedBy,
      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      local_last_changed_at                                   as LocalLastChangedAt,
      @Semantics.systemDateTime.lastChangedAt: true
      last_changed_at                                         as LastChangedAt,

      _Bank // Make association public

}

Interface View ZZI_Bank

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Customer Bank'
@Metadata.ignorePropagatedAnnotations: true

define view entity ZZI_Bank
  as select from zzcustomer_bank
  association to parent ZZI_Customer as _Customer on $projection.CustomerId = _Customer.CustomerId
{

  key zzcustomer_bank.bankid       as BankId,

      zzcustomer_bank.customer_id  as CustomerId,

      zzcustomer_bank.bank_country as BankCountry,
      zzcustomer_bank.bank_key     as BankKey,
      zzcustomer_bank.bank_account as BankAccount,
      zzcustomer_bank.currency     as Currency,


      @Semantics.user.createdBy: true
      local_created_by             as LocalCreatedBy,
      @Semantics.systemDateTime.createdAt: true
      local_created_at             as LocalCreatedAt,
      @Semantics.user.localInstanceLastChangedBy: true
      local_last_changed_by        as LocalLastChangedBy,
      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      local_last_changed_at        as LocalLastChangedAt,
      @Semantics.systemDateTime.lastChangedAt: true
      last_changed_at              as LastChangedAt,


      _Customer // Make association public
}

Behaviour Definition ZZI_CUSTOMER

Before you activate below, please create draft tables zzcustomer_d and zzcus_bank_d using the quick fix in ADT. Also create class zbp_zi_customer.

managed implementation in class zbp_zi_customer unique;
strict ( 2 );
with draft;
define behavior for ZZI_Customer alias Customer persistent table zzcustomer
draft table zzcustomer_d
lock master total etag LastChangedAt
etag master LastChangedAt
authorization master ( instance )
{
  create ( authorization : none );
  update ( precheck );
  delete;

  field ( readonly, numbering : managed ) CustomerId;
  field ( readonly ) LocalCreatedBy, LocalCreatedAt, LocalLastChangedBy, LocalLastChangedAt, LastChangedAt ;
  field ( mandatory ) Name, Street, City, PostalCode, Country;

  association _Bank { create; with draft; }

  draft action Edit;
  draft action Resume;
  draft action Discard;
  draft action Activate optimized;
  draft determine action Prepare;

  mapping for zzcustomer
    {
      CustomerId         = customer_id;
      Name               = name;
      Street             = street;
      City               = city;
      PostalCode         = postal_code;
      Country            = country;
      LocalCreatedBy     = local_created_by;
      LocalCreatedAt     = local_created_at;
      LocalLastChangedBy = local_last_changed_by;
      LocalLastChangedAt = local_last_changed_at;
      LastChangedAt      = last_changed_at;
    }

}

define behavior for ZZI_Bank alias Bank persistent table zzcustomer_bank
draft table zzcus_bank_d
lock dependent by _Customer
authorization dependent by _Customer
{
  update;
  delete;

  field ( readonly, numbering : managed ) BankId;
  field ( readonly ) CustomerId, LocalCreatedBy, LocalCreatedAt, LocalLastChangedBy, LocalLastChangedAt, LastChangedAt;
  field ( mandatory ) BankCountry, BankKey, BankAccount, Currency;

  association _Customer { with draft; }

  mapping for zzcustomer_bank
    {
      BankId             = bankid;
      CustomerId         = customer_id;
      BankCountry        = bank_country;
      BankKey            = bank_key;
      BankAccount        = bank_account;
      Currency           = currency;
      LocalCreatedBy     = local_created_by;
      LocalCreatedAt     = local_created_at;
      LocalLastChangedBy = local_last_changed_by;
      LocalLastChangedAt = local_last_changed_at;
      LastChangedAt      = last_changed_at;
    }

}

Projection View ZZC_Customer

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Customer Projection'
@Metadata.ignorePropagatedAnnotations: true
@Metadata.allowExtensions: true

@Search.searchable: true
@ObjectModel.semanticKey: ['Name']

define root view entity ZZC_Customer
  provider contract transactional_query
  as projection on ZZI_Customer
{
  key CustomerId,

      @Search.defaultSearchElement: true
      @Search.fuzzinessThreshold: 0.8
      Name,

      Street,
      City,
      PostalCode,
      Country,

      @Semantics: { user.createdBy: true }
      LocalCreatedBy,
      @Semantics: { systemDateTime.createdAt: true }
      LocalCreatedAt,
      @Semantics: { user.localInstanceLastChangedBy: true }
      LocalLastChangedBy,
      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      LocalLastChangedAt,
      @Semantics: { systemDateTime.lastChangedAt: true   }
      LastChangedAt,

      _Bank       : redirected to composition child ZZC_Bank
}

Projection View ZZC_BANK

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Bank Projection'
@Metadata.ignorePropagatedAnnotations: true

@Metadata.allowExtensions: true
@Search.searchable: true

define view entity ZZC_Bank
  as projection on ZZI_Bank
{
  key BankId,
      CustomerId,

      BankCountry,
      BankKey,

      @Search.defaultSearchElement: true
      @Search.fuzzinessThreshold: 0.8
      BankAccount,

      Currency,

      @Semantics: { user.createdBy: true }
      LocalCreatedBy,
      @Semantics: { systemDateTime.createdAt: true }
      LocalCreatedAt,
      @Semantics: { user.localInstanceLastChangedBy: true }
      LocalLastChangedBy,
      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      LocalLastChangedAt,
      @Semantics: { systemDateTime.lastChangedAt: true   }
      LastChangedAt,

      _Customer: redirected to parent ZZC_Customer
}

Behaviour Definition (Projection) ZZC_CUSTOMER

projection;
strict ( 2 );
use draft;

define behavior for ZZC_Customer 
{
  use create;
  use update;
  use delete;

  use action Edit;
  use action Resume;
  use action Discard;
  use action Activate;
  use action Prepare;

  use association _Bank { create; with draft; }
}

define behavior for ZZC_Bank 
{
  use update;
  use delete;

  use association _Customer { with draft; }
}

Metadata Extention ZZC_CUSTOMER

@Metadata.layer: #CORE

@UI: {
  headerInfo: {
    typeName: 'Customer',
    typeNamePlural: 'Customers',
    title: { type: #STANDARD, value: 'Name' }
  },
  presentationVariant: [{
    sortOrder: [{ by: 'Name', direction: #DESC }],
    visualizations: [{type: #AS_LINEITEM}]
  }]
}

annotate entity ZZC_Customer with
{

  @UI.facet: [
    {
      id: 'CustomerDetails',
      purpose: #STANDARD,
      type: #IDENTIFICATION_REFERENCE,
      label: 'Customer Details',
      position: 10
    },
    {
      id : 'idAdmin' ,
      type: #FIELDGROUP_REFERENCE ,
      label : 'Admin',
      targetQualifier: 'fgAdmin' ,
      position: 20
    },
    {
      id: 'Banks',
      purpose: #STANDARD,
      type: #LINEITEM_REFERENCE,
      label: 'Banks',
      position: 30,
      targetElement: '_Bank'
    }
  ]


  @UI.hidden: true
  CustomerId;

  @UI.lineItem: [{ position: 10, importance: #HIGH }]
  @UI.identification: [{ position: 10 }]
  @UI.selectionField: [{ position: 10 }]
  Name;

  @UI.lineItem: [{ position: 20, importance: #HIGH }]
  @UI.identification: [{ position: 20 }]
  @UI.selectionField: [{ position: 20 }]
  Street;

  @UI.lineItem: [{ position: 30, importance: #HIGH }]
  @UI.identification: [{ position: 30 }]
  @UI.selectionField: [{ position: 30 }]
  City;

  @UI.identification: [{ position: 40 }]
  PostalCode;

  @UI.lineItem: [{ position: 50, importance: #HIGH }]
  @UI.identification: [{ position: 50 }]
  @UI.selectionField: [{ position: 50 }]
  @Consumption.valueHelpDefinition: [{ entity: { name: 'I_CountryVH', element: 'Country' }  }]
  Country;

  @UI.lineItem: [{ position: 60, importance: #HIGH }]
  @UI.selectionField: [{ position: 60 }]
  @UI.fieldGroup: [{ qualifier: 'fgAdmin' , position: 10 }]
  @Consumption.valueHelpDefinition: [{ entity: { name: 'I_CreatedByUser', element: 'UserName'} }]
  LocalCreatedBy;

  @UI.lineItem: [{ position: 70, importance: #HIGH }]
  @UI.selectionField: [{ position: 70 }]
  @UI.fieldGroup: [{ qualifier: 'fgAdmin' , position: 20 }]
  LocalCreatedAt;

  @UI.fieldGroup: [{ qualifier: 'fgAdmin' , position: 30 }]
  @Consumption.valueHelpDefinition: [{ entity: { name: 'I_CreatedByUser', element: 'UserName'} }]
  LocalLastChangedBy;

  @UI.fieldGroup: [{ qualifier: 'fgAdmin' , position: 50 }]
  LocalLastChangedAt;

}

Metadata Extension ZZC_BANK

@Metadata.layer: #CORE
@UI: {
  headerInfo: {
    typeName: 'Bank',
    typeNamePlural: 'Banks',
    title: { type: #STANDARD, value: 'BankAccount' }
  }
}
annotate entity ZZC_Bank with
{

  @UI.facet: [{
    id: 'BankDetails',
    purpose: #STANDARD,
    type: #IDENTIFICATION_REFERENCE,
    label: 'Bank Details',
    position: 10
  }]


  @UI.hidden: true
  BankId;

  @UI.lineItem: [{ position: 10, importance: #HIGH }]
  @UI.identification: [{ position: 10 }]
  @Consumption.valueHelpDefinition: [{ entity: { name: 'I_CountryVH', element: 'Country' } }]
  BankCountry;

  @UI.lineItem: [{ position: 20, importance: #HIGH }]
  @UI.identification: [{ position: 20 }]
  BankKey;

  @UI.lineItem: [{ position: 30, importance: #HIGH }]
  @UI.identification: [{ position: 30 }]
  BankAccount;

  @UI.lineItem: [{ position: 40, importance: #HIGH }]
  @UI.identification: [{ position: 40 }]
  @Consumption.valueHelpDefinition: [{ entity: { name: 'I_CurrencyStdVH', element: 'Currency'} }]
  Currency;

}

Service Definition ZZCUSTOMER_MGMT

@EndUserText.label: 'Customer Service Definition'
define service ZZCUSTOMER_MGMT {
  expose ZZC_Customer as Customer;
  expose ZZC_Bank     as Bank;
}

Service Binding ZZUI_CUSTOMER_O4

Create service binding using OData V4 – UI and publish it. Preview application on entity Customer, you should see a working Fiori App.

Enabling Change Documents Integration

Create Change Document Object ZZCUSTOMER as described here.

Change Behaviour Definition ZZI_CUSTOMER.

  1. Define changedocuments master ( zzcustomer ) on the root entity Customer. Where zzcustomer is name of Change Document Object.
  2. Define changedocuments dependent for the dependent Bank entity.
  3. Add changedocuments ( create : key, update : data, delete : none ); in both entities.
managed implementation in class zbp_zi_customer unique;
strict ( 2 );
with draft;
define behavior for ZZI_Customer alias Customer persistent table zzcustomer
draft table zzcustomer_d
changedocuments master ( zzcustomer )
lock master total etag LastChangedAt
etag master LastChangedAt
authorization master ( instance )
{
  create ( authorization : none );
  update ( precheck );
  delete;

  field ( readonly, numbering : managed ) CustomerId;
  field ( readonly ) LocalCreatedBy, LocalCreatedAt, LocalLastChangedBy, LocalLastChangedAt, LastChangedAt ;
  field ( mandatory ) Name, Street, City, PostalCode, Country;

  changedocuments ( create : key, update : data, delete : none );

  association _Bank { create; with draft; }

  draft action Edit;
  draft action Resume;
  draft action Discard;
  draft action Activate optimized;
  draft determine action Prepare;

  mapping for zzcustomer
    {
      CustomerId         = customer_id;
      Name               = name;
      Street             = street;
      City               = city;
      PostalCode         = postal_code;
      Country            = country;
      LocalCreatedBy     = local_created_by;
      LocalCreatedAt     = local_created_at;
      LocalLastChangedBy = local_last_changed_by;
      LocalLastChangedAt = local_last_changed_at;
      LastChangedAt      = last_changed_at;
    }

}

define behavior for ZZI_Bank alias Bank persistent table zzcustomer_bank
draft table zzcus_bank_d
changedocuments dependent
lock dependent by _Customer
authorization dependent by _Customer
{
  update;
  delete;

  field ( readonly, numbering : managed ) BankId;
  field ( readonly ) CustomerId, LocalCreatedBy, LocalCreatedAt, LocalLastChangedBy, LocalLastChangedAt, LastChangedAt;
  field ( mandatory ) BankCountry, BankKey, BankAccount, Currency;

  changedocuments ( create : key, update : data, delete : none );

  association _Customer { with draft; }


  mapping for zzcustomer_bank
    {
      BankId             = bankid;
      CustomerId         = customer_id;
      BankCountry        = bank_country;
      BankKey            = bank_key;
      BankAccount        = bank_account;
      Currency           = currency;
      LocalCreatedBy     = local_created_by;
      LocalCreatedAt     = local_created_at;
      LocalLastChangedBy = local_last_changed_by;
      LocalLastChangedAt = local_last_changed_at;
      LastChangedAt      = last_changed_at;
    }

}

Create or change data in RAP Fiori Application, you should see change documents created in CDHDR and CDPOS tables.

Displaying SAP Change Document Data in RAP Fiori Application

To display change document data in the Fiori Application do following changes:

  1. Change Interface View ZZI_Customer to include a field ChangeDocObject. Add association to I_ChangeDocument_2 using ChangeDocObjectClass = 'ZZCUSTOMER' and new field ChangeDocObject. We have to add this new field ChangeDocObject to convert customer_id which is is in RAW to character, to make it compatible with I_ChangeDocument_2-ChangeDocObject. Expose the association _ChangeDocument. Add annotation @AccessControl.privilegedAssociations: [ '_ChangeDocument' ] as CDS I_ChangeDocument_2 is Privileged view.
  2. Change Behaviour Definition ZZI_CUSTOMER to add field ChangeDocObject as readonly.
  3. Change Projection View ZZC_CUSTOMER add field ChangeDocObject, redirect association _ChangeDocument to C_ChangeDocument_2. Add annotation @AccessControl.privilegedAssociations: [ '_ChangeDocument' ].
  4. Change metadata extension ZZC_Customer to add new facet of type #LINEITEM_REFERENCE with targetElement: '_ChangeDocument'.
  5. Change Service Definition ZZCUSTOMER_MGMT to expose view C_ChangeDocument_2.

Changes in Interface View ZZI_Customer

@AccessControl.privilegedAssociations: [ '_ChangeDocument' ]

define root view entity ZZI_Customer
  as select from zzcustomer

  association of one to many I_ChangeDocument_2 as _ChangeDocument on  _ChangeDocument.ChangeDocObjectClass = 'ZZCUSTOMER'
                                                                   and _ChangeDocument.ChangeDocObject      = $projection.ChangeDocObject

{
  key zzcustomer.customer_id                                  as CustomerId,

      cast( bintohex( zzcustomer.customer_id ) as cdobjectv ) as ChangeDocObject,

      _ChangeDocument,
      _Bank // Make association public

}

Change in Behaviour Definition ZZI_CUSTOMER

 field ( readonly ) ChangeDocObject;

Changes in Projection View ZZC_CUSTOMER

@AccessControl.privilegedAssociations: [ '_ChangeDocument' ]


define root view entity ZZC_Customer
  as projection on ZZI_Customer
{
  key CustomerId,

      ChangeDocObject,

      _ChangeDocument : redirected to C_ChangeDocument_2,
      _Bank           : redirected to composition child ZZC_Bank
}

Change in metadata extension ZZC_Customer

annotate entity ZZC_Customer with
{

  @UI.facet: [
    {
      id: 'CustomerDetails',
      purpose: #STANDARD,
      type: #IDENTIFICATION_REFERENCE,
      label: 'Customer Details',
      position: 10
    },
    {
      id : 'idAdmin' ,
      type: #FIELDGROUP_REFERENCE ,
      label : 'Admin',
      targetQualifier: 'fgAdmin' ,
      position: 20
    },
    {
      id: 'Banks',
      purpose: #STANDARD,
      type: #LINEITEM_REFERENCE,
      label: 'Banks',
      position: 30,
      targetElement: '_Bank'
    } ,
    {
      id: 'ChangeDocs',
      purpose: #STANDARD,
      type: #LINEITEM_REFERENCE,
      label: 'Change Documents',
      position: 40,
      targetElement: '_ChangeDocument'
    }
  ]

}

Change in Service Definition ZZCUSTOMER_MGMT

@EndUserText.label: 'Customer Service Definition'
define service ZZCUSTOMER_MGMT {
  expose ZZC_Customer       as Customer;
  expose ZZC_Bank           as Bank;
  expose C_ChangeDocument_2 as ChangeDocument;
}

Activate all changes and you should see a new facet on object page with Change document records.

References

Integrating Change Documents

RAP – Changedocuments

RAP – Changedocuments, Options

One Reply to “SAP Change Documents in RAP – CDS Views, BDEF and Change Document Annotation

Leave a Reply