Skip to Content
Technical Articles

Creating a draft enabled Sales Order Fiori App using the new ABAP Programming Model – Part 2: Virtual Data Model & Consumption views

Building the Virtual Data Model

To provide our app with data and draft capabilities, we’ll build two CDS views which will form the base for our business object. I won’t go into details on how to create CDS views here, as there are enough guides available on how to create them.

Our business object will consist out of 2 nodes:

  • Sales Order Header Node (root node)
  • Sales Order Item Node

We’ll use CDS annotations to model our business object. As you’ll notice, both CDS reference each other so you’ll need to activate both of them at the same time in order to get them activated.

Sales Order Header node

Create following CDS view. This view is just a regular join of sales order header data (VBAK) and partner data (VBPA). We’ll also need to expose our item data using an association to have them available as a subnode in our business object.

This business object is defined by the @ObjectModel annotations. More information about these annotations can be found in the documentation delivered by SAP.

@AbapCatalog.sqlViewName: 'ZSD_I_SOHEAD_V'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales order header model view'

@ObjectModel: { compositionRoot: true,
                modelCategory: #BUSINESS_OBJECT,
                transactionalProcessingEnabled: true,
                createEnabled: true,
                updateEnabled: true,
                deleteEnabled: true,
                semanticKey: ['vbeln'],
                draftEnabled: true,
                writeDraftPersistence: 'ZSD_SOHEAD_D' }

define view ZSD_I_SOHEADER as select from vbak
  association [1..*] to ZSD_I_SOITEM as _items on $projection.vbeln = _items.vbeln
  association [0..1] to vbpa as _shipTo on $projection.vbeln = _shipTo.vbeln and _shipTo.posnr = '000000' and _shipTo.parvw = 'WE' {

  @ObjectModel.readOnly: true
  key vbak.vbeln,
      vbak.auart,
      vbak.vkorg,
      vbak.vtweg,
      vbak.spart,
      vbak.vkbur,
      vbak.vkgrp,
      vbak.kunnr,
      cast( _shipTo.kunnr as kunwe preserving type ) as kunwe,
      vbak.bstnk,
      vbak.ernam,
      vbak.erdat,
      vbak.erzet,
      vbak.aedat,
      vbak.netwr,
      vbak.waerk,

      @ObjectModel: {
        association: {
            type: [#TO_COMPOSITION_CHILD]
        }
      }
      _items,
      _shipTo
}
where vbak.vbtyp = 'C'

 

Sales Order Item node

@AbapCatalog.sqlViewName: 'ZSD_I_SOITEM_V'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales order item model view'

@ObjectModel: { updateEnabled: true,
                createEnabled: true,
                deleteEnabled: true,
                semanticKey: ['vbeln', 'posnr'],
                writeDraftPersistence: 'ZSD_SOITEM_D'}

define view ZSD_I_SOITEM as select from vbap
  association [1..1] to ZSD_I_SOHEADER as _header on $projection.vbeln = _header.vbeln {
  @ObjectModel.readOnly: true
  key vbap.vbeln,
  @ObjectModel.readOnly: true
  key vbap.posnr, 
      vbap.matnr,   
      vbap.arktx,
      vbap.kwmeng,
      vbap.vrkme,
      vbap.netwr, 
      vbap.waerk, 
      vbap.ernam, 
      vbap.erdat, 
      vbap.erzet,
      vbap.aedat, 
      
      @ObjectModel: {
        association: {
            type: [#TO_COMPOSITION_PARENT,#TO_COMPOSITION_ROOT]
        }
      }
      _header
}

After activating both CDS views the system generates a business object and all required repository objects. We’ll use the generated objects throughout this blog series to add functionality to our business object.

 

Building the consumption views

To expose the business object data as an oData service, we’ll create a consumption for both of the previously created views. In these views we’ll:

  • Change internal fieldnames to a more readable format (e.g. VBELN -> orderNumber)
  • Determine which fields are mandatory or read only
  • Add foreign key associations to fields where possible (this will automatically add search helps to the fields in our Fiori App)
  • Expose the view as oData service using the @OData.publish annotation
  • Separate our UI annotations using metadata extensions by setting the @Metadata.allowExtensions annotation to true

Sales Order Header Consumption view

@AbapCatalog.sqlViewName: 'ZSD_C_SOHEAD_V'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales order header consumption view'

@OData.publish: true

@Metadata.allowExtensions: true

@VDM.viewType: #CONSUMPTION

@ObjectModel: { compositionRoot: true,
                transactionalProcessingDelegated: true,
                createEnabled: true,
                updateEnabled: true,
                deleteEnabled: true,
                semanticKey: ['orderNumber'],
                draftEnabled: true }
define view ZSD_C_SOHEADER as select from ZSD_I_SOHEADER
    association [1..*] to ZSD_C_SOITEM as _items on $projection.orderNumber = _items.orderNumber
    association [1..1] to I_SalesOrderType as _orderType on $projection.orderType = _orderType.SalesOrderType
    association [1..1] to I_SalesOrganization as _salesOrg on $projection.salesOrganization = _salesOrg.SalesOrganization
    association [1..1] to I_DistributionChannel as _distributionChannel on $projection.distributionChannel = _distributionChannel.DistributionChannel
    association [1..1] to I_Division as _division on $projection.division = _division.Division
    association [0..1] to I_SalesOffice as _salesOffice on $projection.salesOffice = _salesOffice.SalesOffice
    association [0..1] to I_SalesGroup as _salesGroup on $projection.salesGroup = _salesGroup.SalesGroup
    association [0..1] to I_Customer as _soldTo on $projection.soldTo = _soldTo.Customer 
    association [0..1] to I_Customer as _shipTo on $projection.shipTo = _shipTo.Customer
    {
    
    @ObjectModel.readOnly: true 
    key ZSD_I_SOHEADER.vbeln as orderNumber,
    
    @ObjectModel.readOnly: true
    @ObjectModel.foreignKey.association: '_orderType'
    ZSD_I_SOHEADER.auart as orderType,
    
    @ObjectModel.mandatory: true
    @ObjectModel.foreignKey.association: '_salesOrg'
    ZSD_I_SOHEADER.vkorg as salesOrganization,
    
    @ObjectModel.mandatory: true
    @ObjectModel.foreignKey.association: '_distributionChannel'
    ZSD_I_SOHEADER.vtweg as distributionChannel,
    
    @ObjectModel.mandatory: true
    @ObjectModel.foreignKey.association: '_division'
    ZSD_I_SOHEADER.spart as division, 
    
    @ObjectModel.foreignKey.association: '_salesOffice'
    ZSD_I_SOHEADER.vkbur as salesOffice, 
    
    @ObjectModel.foreignKey.association: '_salesGroup'
    ZSD_I_SOHEADER.vkgrp as salesGroup, 
    
    @ObjectModel.mandatory: true
    @ObjectModel.foreignKey.association: '_soldTo'
    ZSD_I_SOHEADER.kunnr as soldTo, 
    
    @ObjectModel.readOnly: true
    _soldTo.CustomerName as soldToName,
    
    @ObjectModel.mandatory: true
    @ObjectModel.foreignKey.association: '_shipTo'
    ZSD_I_SOHEADER.kunwe as shipTo, 
    
    @ObjectModel.readOnly: true
    _shipTo.CustomerName as shipToName,
    
    ZSD_I_SOHEADER.bstnk as customerReference,
    
    @ObjectModel.readOnly: true 
    ZSD_I_SOHEADER.ernam as createdBy,
    
    @ObjectModel.readOnly: true
    ZSD_I_SOHEADER.erdat as createdOn, 
    
    @ObjectModel.readOnly: true
    ZSD_I_SOHEADER.erzet as createdAt, 
    
    @ObjectModel.readOnly: true
    ZSD_I_SOHEADER.aedat as changedOn, 
    
    @ObjectModel.readOnly: true
    @Semantics.amount.currencyCode: 'currency'
    ZSD_I_SOHEADER.netwr as netValue, 
    
    @ObjectModel.readOnly: true
    @Semantics.currencyCode: true 
    ZSD_I_SOHEADER.waerk as currency,
    
    @ObjectModel.association: {
        type: [#TO_COMPOSITION_CHILD]
    }
    _items,
    _orderType,
    _salesOrg,
    _distributionChannel,
    _division,
    _salesOffice,
    _salesGroup,
    _soldTo,
    _shipTo
}

 

Sales Order Item Consumption view

@AbapCatalog.sqlViewName: 'ZSD_C_SOITEM_V'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales order item consumption view'

@Metadata.allowExtensions: true

@VDM.viewType: #CONSUMPTION

@ObjectModel: { updateEnabled: true,
                createEnabled: true,
                deleteEnabled: true,
                semanticKey: ['orderNumber', 'orderItem']}

define view ZSD_C_SOITEM as select from ZSD_I_SOITEM
    association [1..1] to ZSD_C_SOHEADER as _header on $projection.orderNumber = _header.orderNumber
    association [0..1] to I_Material as _material on $projection.material = _material.Material
    association [0..1] to I_UnitOfMeasure as _unitOfMeasure on $projection.salesUnit = _unitOfMeasure.UnitOfMeasure {
    
    @ObjectModel.readOnly: true 
    key ZSD_I_SOITEM.vbeln as orderNumber,
     
    @ObjectModel.readOnly: true 
    key ZSD_I_SOITEM.posnr as orderItem,
    
    @ObjectModel.mandatory: true
    @ObjectModel.foreignKey.association: '_material' 
    ZSD_I_SOITEM.matnr as material, 
    
    @ObjectModel.mandatory: true
    ZSD_I_SOITEM.arktx as itemDescription,
    
    @ObjectModel.mandatory: true 
    @Semantics.quantity.unitOfMeasure: 'salesUnit'
    ZSD_I_SOITEM.kwmeng as orderQuantity, 
    
    @ObjectModel.mandatory: true
    @Semantics.unitOfMeasure: true
    @ObjectModel.foreignKey.association: '_unitOfMeasure' 
    ZSD_I_SOITEM.vrkme as salesUnit, 
    
    @ObjectModel.readOnly: true 
    @Semantics.amount.currencyCode: 'currency'
    ZSD_I_SOITEM.netwr as netValue,
    
    @ObjectModel.readOnly: true
    @Semantics.currencyCode: true  
    ZSD_I_SOITEM.waerk as currency,
    
    @ObjectModel.readOnly: true  
    ZSD_I_SOITEM.ernam as createdBy, 
    
    @ObjectModel.readOnly: true 
    ZSD_I_SOITEM.erdat as createdOn, 
    
    @ObjectModel.readOnly: true 
    ZSD_I_SOITEM.erzet as createdAt,
    
    @ObjectModel.readOnly: true  
    ZSD_I_SOITEM.aedat as changedOn,
    
    @ObjectModel.association: {
        type: [#TO_COMPOSITION_PARENT, #TO_COMPOSITION_ROOT]
    }
    _header,
    _material,
    _unitOfMeasure
}

After activating the consumption views the data model for our Fiori app is ready for use. We can now continue with the next part of the blog series to define the UI and generate the Fiori Elements app.

 

Quick navigation

3 Comments
You must be Logged on to comment or reply to a post.
  • Thank you for the detailed blog.

    Just wondering what the structure of ZSD_SOHEAD_D and ZSD_SOITEM_D looks like as I am getting errors

    [BO check] Active Entity DB fields not marked as KEY (view ZSD_I_SOHEADER, table ZSD_SOHEAD_D)

    [BO check] Active Entity DB fields not marked as KEY (view ZSD_I_SOITEM, table ZSD_SOITEM_D) 

     

    Thank you

    • Hi Timothy,

      These two database tables normally shouldn’t exist before trying to activate the CDS views, these are the actual draft tables and get generated (or regenerated) when you activate the CDS views.

      Maybe it’s a system error and you need to implement some notes to get this fixed? (I wrote this guide using an S/4HANA 1809 so the basis level was 7.53)

      If you take a look at this wiki for example, maybe note “#2005370 – CDS views: Key fields not copied from DDL source” is relevant for your system?

      Best regards,

      Geert-Jan Klaps

  • Thank you for the detailed blog Geert-Jan,

    We are an S/4HANA 1809 with basis level 7.53 too, but I am getting below error when I am trying to add “transactionalProcessingEnabled: true” statement for draft mode.

    Error Message: “[BO Sync] Neither ‘draft pers.’ or ‘active pers.’ not supported”

    Are we missing a patch or parameter on S4/HANA to make draft mode enabled?

    I Appreciate your feedback.

    Regards,

    Roy