Skip to Content

ABAP Programming Model for Fiori – Transactional Apps with Draft Capabilities using Standard Tables

Those familiar with the new ABAP Programming Model for SAP Fiori might have noticed that in the official documentation, the scenarios covered for coding transactional applications are limited to ones that read and write to custom tables. As we all know, most real life scenarios require applications that read from and write back to standard delivered tables. And as we also know, you always update standard tables using either the standard delivered BAPI’s or handler classes.

I was curious to know if there was a way to leverage the new programming model to create an application that will read from standard tables (tables with non-guid keys in particular) and will save the data back to the database using the aforementioned methods, and it turns out there is (or this blog wouldn’t exist ūüôā ). Now, I don’t know if this is documented somewhere, but at least I couldn’t find it anywhere.

So here are the basic steps to get this scenario working using purchase order tables.

** Disclaimer **

This example is just to explain how to leverage the framework for this scenario, it is missing all the validations and extra annotations you will need for a production ready application. There is enough documentation for that, so it is not covered in this post.

Defining the CDS Data Model for the Draft Business Object

I suggest first creating the views and the associations without any annotations first. Once the CDS views are active, then add the @ObjectModel annotations that will create the Business Object. For this example, there will be a view for the PO header and one for the PO items.

PO Header Model View
dead@AbapCatalog.sqlViewName: 'Z_I_POHEAD_V'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PO Header Model View'

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

define view Z_I_POHEADER
  as select from ekko
  association [1..*] to Z_I_POITEM as _items on $projection.ebeln = _items.ebeln
{
       @ObjectModel.readOnly: true
  key  ekko.ebeln,
       ekko.bukrs,
       ekko.bstyp,
       ekko.bsart,
       ekko.loekz,
       ekko.aedat,
       ekko.ernam,
       @ObjectModel.readOnly: true
       ekko.lastchangedatetime,
       ekko.lifnr,
       ekko.zterm,
       ekko.ekorg,
       ekko.ekgrp,
       ekko.waers,
       ekko.bedat,
       ekko.knumv,
       ekko.kalsm,
       ekko.stafo,
       ekko.lifre,
       ekko.lands,
       ekko.memory,
       ekko.procstat,
       ekko.reason_code,
       ekko.memorytype,
       @ObjectModel: {
           association: {
               type: [#TO_COMPOSITION_CHILD]
           }
       }
       _items
}
PO Item Model View
@AbapCatalog.sqlViewName: 'Z_I_POITEM_V'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PO Item Model View'

@ObjectModel: { updateEnabled: true,
                createEnabled: true,
                deleteEnabled: true,
                semanticKey: ['ebeln', 'ebelp'],
                writeDraftPersistence: 'ZPO_ITEM_D'}

define view Z_I_POITEM
  as select from ekpo
  association [1..1] to Z_I_POHEADER as _header on $projection.ebeln = _header.ebeln
{
       @ObjectModel.readOnly: true
  key  ekpo.ebeln,
       @ObjectModel.readOnly: true
  key  ekpo.ebelp,
       ekpo.loekz,
       ekpo.statu,
       ekpo.aedat,
       ekpo.txz01,
       ekpo.matnr,
       ekpo.ematn,
       ekpo.bukrs,
       ekpo.werks,
       ekpo.lgort,
       ekpo.matkl,
       ekpo.infnr,
       ekpo.menge,
       ekpo.meins,
       ekpo.bprme,
       ekpo.bpumz,
       ekpo.bpumn,
       ekpo.umrez,
       ekpo.umren,
       ekpo.netpr,
       ekpo.peinh,
       ekpo.netwr,
       ekpo.brtwr,
       ekpo.prdat,
       ekpo.bstyp,
       ekpo.effwr,
       ekpo.xoblr,
       ekpo.adrn2,
       @ObjectModel: {
       association: {
               type: [#TO_COMPOSITION_PARENT,#TO_COMPOSITION_ROOT]
           }
       }
       _header
}

Defining the CDS Consumption Views

This step is the same as explained in SAP’s documentation as you can check here. From these views you will be generating the OData service so I suggest using this layer to give user friendly names to the properties.

PO Header Consumption View
@AbapCatalog.sqlViewName: 'Z_C_POHEAD_V'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PO 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 Z_C_POHEADER
  as select from Z_I_POHEADER
  association [1..*] to Z_C_POITEM as _items on $projection.OrderNumber = _items.OrderNumber
{

  key Z_I_POHEADER.ebeln              as OrderNumber,
      @ObjectModel.readOnly: true
      Z_I_POHEADER.bukrs              as CompanyCode,
      @ObjectModel.mandatory: true
      Z_I_POHEADER.bsart              as OrderType,
      @ObjectModel.readOnly: true
      Z_I_POHEADER.aedat              as CreatedAt,
      @ObjectModel.readOnly: true
      Z_I_POHEADER.ernam              as CreatedBy,
      @ObjectModel.readOnly: true
      Z_I_POHEADER.lastchangedatetime as ChangedAt,
      @ObjectModel.mandatory: true
      Z_I_POHEADER.lifnr              as Vendor,
      @ObjectModel.mandatory: true
      Z_I_POHEADER.ekorg              as PurchasingOrg,
      @ObjectModel.mandatory: true
      Z_I_POHEADER.ekgrp              as PurchasingGrp,
      @ObjectModel.mandatory: true
      Z_I_POHEADER.waers              as CurrencyKey,
      @ObjectModel.association: {
        type: [#TO_COMPOSITION_CHILD]
      }
      _items
}
PO Item Consumption View
@AbapCatalog.sqlViewName: 'Z_C_POITEM_V'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PO Item Consumption View'

@Metadata.allowExtensions: true

@VDM.viewType: #CONSUMPTION

@ObjectModel: { updateEnabled: true,
                createEnabled: true,
                deleteEnabled: true,
                semanticKey: ['OrderNumber', 'OrderItem']}

define view Z_C_POITEM
  as select from Z_I_POITEM
  association [1..1] to Z_C_POHEADER as _header on $projection.OrderNumber = _header.OrderNumber
{
      @ObjectModel.readOnly: true
  key Z_I_POITEM.ebeln as OrderNumber,
      @ObjectModel.readOnly: true
  key Z_I_POITEM.ebelp as OrderItem,
      Z_I_POITEM.loekz,
      Z_I_POITEM.statu,
      Z_I_POITEM.aedat,
      Z_I_POITEM.txz01,
      Z_I_POITEM.matnr,
      Z_I_POITEM.ematn,
      Z_I_POITEM.bukrs,
      Z_I_POITEM.werks,
      Z_I_POITEM.lgort,
      Z_I_POITEM.matkl,
      Z_I_POITEM.infnr,
      Z_I_POITEM.menge,
      Z_I_POITEM.meins,
      Z_I_POITEM.bprme,
      Z_I_POITEM.bpumz,
      Z_I_POITEM.bpumn,
      Z_I_POITEM.umrez,
      Z_I_POITEM.umren,
      Z_I_POITEM.netpr,
      Z_I_POITEM.peinh,
      Z_I_POITEM.netwr,
      Z_I_POITEM.brtwr,
      Z_I_POITEM.prdat,
      Z_I_POITEM.bstyp,
      Z_I_POITEM.effwr,
      Z_I_POITEM.xoblr,
      Z_I_POITEM.adrn2,
      @ObjectModel.association: {
            type: [#TO_COMPOSITION_PARENT, #TO_COMPOSITION_ROOT]
      }
      _header
}

Implement SAVE and UPDATE logic in Business Object

Now here is where the magic happens ūüôā . If you open the BO that was generated by the CDS view in transaction BOBX and you change the view to “Extended” (on the top menu Goto-> Standard <-> Extended, you can see that when you generate a Business Object from tables with non-guid keys it uses the class¬†/BOBF/CL_DAC_UNION to access data. In the case where the tables you used have guid keys, then it will not need to use the legacy data access and I will point the differences on how to save the data later, but the concept is the same.

A transactional application with Draft capabilities is great because it allows a user to create or edit an existing document without committing the changes without losing the data. And what is even greater is that all the life-cycle of the draft object is taken care of by the framework. So when you create new draft the framework will create the corresponding entries in the draft tables you specified with the annotation “writeDraftPersistence”.

So going back to the Business Object, if you check your root note you will see that it will have a Draft class that was generated when you activated the model view. The names start with “ZCL_DR_*” and inherit from the class /bobf/cl_lib_dr_classic_app. Like I mentioned before the framework will take care of creating the draft objects for you as well as copying the data from an existing object to a draft. The latter scenario happens when you are editing an object that already exists in the database. For example, in our simple purchase scenario, if you edit a purchase order that already exists, the method¬†/bobf/if_frw_draft~create_draft_for_active_entity from that draft class will be called. The class¬†/bobf/cl_lib_dr_classic_app already contains an implementation of that method you don’t need to do anything for that part to work.

The real question is, once I’m ready to save the changes I’ve done to an existing document or to a newly created document, how do I do it? Since all of the changes (creates and updates) for this type of applications are done to the Draft entity, then regardless on whether you are creating or updating a draft, the framework will treat it the same. Basically, a function import will be exposed in your service that allows you to activate a Draft. This function import is mapped to a BOPF action which ends up calling a method from your draft class. The method is¬†/bobf/if_frw_draft~copy_draft_to_active_entity, and here is where you will code your logic to save the data back to the database using whichever standard deliver method SAP offers for the object you are manipulating.

It is very important to understand how this part works. In this method you have two exporting tables, et_failed_draft_key and et_key_link.

You use et_failed_draft_key  to notify the framework that the activation of draft failed, and et_key_link to notify it that the activation was successful. Also et_key_link allows you to link the key of the active entity and the draft entity. This is crucial for the process because the framework needs this link between the keys in order to delete the draft entity for the active object.

In that method you will need to do the following:

  1. Read the data from the Draft entity your are trying to “activate” to determine whether is an UPDATE to an existing entity or is a new object you need to CREATE. You can do something like this to determine that.
 "Read node data
    io_read->retrieve(
      EXPORTING
        iv_node       = is_ctx-node_key
        it_key        = it_draft_key
      IMPORTING
        et_data       = <ft_data>
    ).

    IF lines( <ft_data> ) NE 1.
      RAISE EXCEPTION TYPE /bobf/cx_frw_fatal.
    ENDIF.

    ""Check if Draft has an active entity
    READ TABLE <ft_data> ASSIGNING FIELD-SYMBOL(<fs_data>) INDEX 1.
    ASSIGN COMPONENT if_draft_constants=>co_db_fieldname-has_active_entity OF STRUCTURE <fs_data> TO FIELD-SYMBOL(<fs_has_active>).
   
    IF <fs_has_active> EQ abap_true.
      "Update - Call BAPI to UPDATE
    ELSE.
      "Create - Call BAPI to CREATE
    ENDIF.
  1. Get the Draft data from the parent object and from the children that allow modifications. Use the io_read object to retrieve the data.
  2. Use the Draft data to populate the interface of whichever BAPI or handler class you are using.
  3. Get the key from the newly created object. You only need the key of the root object, not from any of the children. The framework will take care of deleting all the entries from the associated tables of the Draft object.
  4. Map the key form the active object to the draft object.
  • For then the key of the active object is a guid, all you need to do is populate the table et_key_link like so:
INSERT VALUE #( active = <fs_guid> draft = it_draft_key[ 1 ]-key ) INTO TABLE et_key_link.
  • For then the key of the active object is not a guid, like in the case of the purchase order you need to map the key structure to a guid generated using a magical class called /bobf/cl_dac_legacy_mapping like so:
    DATA: lr_active_key TYPE REF TO data.

    "Get BOPF configuration
    DATA(lo_conf) = /bobf/cl_frw_factory=>get_configuration( iv_bo_key = is_ctx-bo_key ).
    lo_conf->get_node( EXPORTING iv_node_key = is_ctx-node_key
                       IMPORTING es_node     = DATA(ls_node_conf) ).

    "Get key structure for active object
    lo_conf->get_altkey( 
       EXPORTING 
        iv_node_key    = is_ctx-node_key
        iv_altkey_name = /bobf/if_conf_cds_link_c=>gc_alternative_key_name-draft-active_entity_key
       IMPORTING 
        es_altkey      = DATA(ls_altkey_active_key) ).

    CREATE DATA lr_active_key TYPE (ls_altkey_active_key-data_type).
    ASSIGN lr_active_key->* TO FIELD-SYMBOL(<ls_active_entity_key>).
    
    "Map your key to structure like with whichever method you want, I use move-corresponding
    MOVE-CORRESPONDING <fs_key> TO <ls_active_entity_key>.
    
    "Map the active key structure to a GUID key
    DATA(lo_dac_map) = /bobf/cl_dac_legacy_mapping=>get_instance( iv_bo_key = is_ctx-bo_key 
                                                                  iv_node_key = is_ctx-node_key ).
    lo_dac_map->map_key_to_bopf( EXPORTING ir_database_key = lr_active_key
                                 IMPORTING es_bopf_key     = DATA(ls_key) ).
    
    INSERT VALUE #( active = ls_key-key draft = it_draft_key[ 1 ]-key ) INTO TABLE et_key_link.

 

And done!!

As I mentioned before in this blog I’m not covering locking, deleting standard objects, validations, etc. For that there is some good documentation here. For locking and deleting objects, when your Business Object gets generated there will be two actions called LOCK_<name_of_bo> and DELETE_<name_of_bo> with Z* implementation classes also generated by the framework. You can put your code there.

I hope this information has been useful ūüėÄ

 

 

21 Comments
You must be Logged on to comment or reply to a post.
  •  

    Hi Diego,

    Nice one, could you please clarify something for me.

    Disclaimer: not hands on in using BOPF yet..

    BOPF does standardize the developer effort to a large extent and developers would generally be reduced to building the business logic in terms of validations and actions with even the database updates dynamically controlled and tied up with entity node contents.

    With your hack in method  /bobf/if_frw_draft~copy_draft_to_active_entity, how do you break the logical flow in framework for it to not do the built in database updates at the end of the save event.

    so in your example the nodes representing EKKO and EKPO would still be created and would still be updated as part of the framework and looks like duplicate DB updates considering the BAPI calls.

    Regards,

    Sitakant.

     

    • Hi¬†Sitakant,

      The BOPF framework will only do updates on the Draft tables because you are not setting any

      tables with the “writeActivePersistence” annotation. For this reason the BOPF will never try to update EKKO and EKPO in the example I put. The¬†/bobf/if_frw_draft~copy_draft_to_active_entity implementation is not a hack. When you only use the annotation “writeDraftPersistence”, the pattern used to persist active data is through an “Activate” action.

      I hope this clarifies your question.

      Cheers,

      Diego

  • Hi Diego,

    Very helpful blog. I am working on a Fiori app with similar requirement to your example, but it needs to include file upload. Does BOPF currently support file/media upload? Are you able to show me an example of this implementation

    Previously with OData V2, I was able to redefine the method /IWBEP/IF_MGW_APPL_SRV_RUNTIME~CREATE_STREAM. But the OData service class generated through this method does not include this method anymore.

    Any help would be appreciated.

     

    Thanks,

    Clement

    • Hi Clement,

       

      I think that that scenario is not supported using only CDS and the Odata.publish: true annotation. However I think there might be a way to work around it using the Gateway service builder (SEGW).

      First thing would be creating the CDS view with all the model annotations that generate the BO, but don’t use the Odata:publish true annotation, since we don’t want a service to be generated automatically.

      I would suggest then trying to create a new project in SEGW, and immediately create a data source reference that points to your CDS main consumption view. This will allow you to select all the Entity types and associations that you declared in your view and expose them in the service metadata.

      Then, I would try creating a new entity type in the project directly called Attachment, and manually implement the create/read stream methods. You could use this to save the attachments for only draft entities either on a custom table, or the document repository of your choice using the draft GUID as key of course. Then you will need to implement some logic on the Activate method called inside the BO where you would read the draft image and save it to the document repository of the activated object.

      I haven’t tried this scenario to be honest, but I don’t see why it wouldn’t work. The advantage of using a SEGW project to manually create the SADL mappings allows you to have your own MPC and DPC classes where you can make method redefinitions that otherwise would be a bit harder.

      Anyway, I hope this helps and let me know if you manage to resolve the issue.

       

      Cheers,

       

      Diego

       

  • Hi Diego,

    Thanks for the blog. The blog really helped me to create an App using CDS & BOPF for my project. I got struck when developing the app using CDS & BOPF.

    Issue is on my Object Page, there are 3 facets(refer Facet_Page image). When I click on ‘Edit’ on Object page, in the 3rd facet there is a “+” button to add entries which appears(refer Facet_Page image). When I click on “+” I can able to add entries in its particular object page(refer Notes_Page image). When I click on ‘Add’, it navigates back to Object page where all Facets are present (refer No_entries image).

    Issue here is the line item doesn’t appear on my 3rd Facet. After I click on ‘Save’ and committed to DB successfully, the line item appears on 3rd facet.

    Attached are the images for your reference.

    Please provide suggestion!

     

    Thanks,

    Vignesh.

    • Hi¬†Vignesh,

      It is very hard to know what might be causing your issue by just looking at your application. If they are being saved when you activate that means that they got stored for your draft as well, so maybe try looking calling the service directly for a draft entity and see if the notes get send back in the response and troubleshoot from there.

       

      Cheers,

       

      Diego

      • Hi Diego,

        I have a transactional app, not using draft functionality, with one header consumption view, one for item and one for texts, using annotations:

        • @ObjectModel.association.type: [#TO_COMPOSITION_ROOT, #TO_COMPOSITION_PARENT]
        • @ObjectModel.association.type: #TO_COMPOSITION_CHILD

        When I created the Project, in WebIDE, via template List Report based on my OData from my Header CDS, I`ve choose the main OData service and navigation to_Item, so leaving out the Text view.

        In result I have the following scenario:

        Both header and item has the “insert” functionality, but not the Text Table. This is a normal behavior? Or did I forget something in my annotations?

        My MDE file which annotates my header CDS view

        @Metadata.layer: #PARTNER
        @UI.headerInfo: { typeName: 'Handle Type - Header',
                          typeNamePlural: 'Handle Types - Header',
                          title: { type: #STANDARD, value: 'HandleType'}}
        
        annotate view YFIORI_FB_C_300 with
        {
        
          @UI.facet: [  {
                      label : 'General Information',
                      id : 'GeneralInfo',
                      purpose: #STANDARD,
                      type : #COLLECTION,
                      position: 10
                  },
                  {
                      label: 'Basic Data',
                      id : 'BasicData',
                      purpose: #STANDARD,
                      parentId : 'GeneralInfo',
                      type : #IDENTIFICATION_REFERENCE,
                      targetQualifier : 'one',
                      position: 20
                  },
        
                  {
                      label: 'Handle Part Type',
                      id  : 'HPTYPE',
                      purpose: #STANDARD,
                      type : #LINEITEM_REFERENCE,
                      targetElement: '_Item',
                      position: 30
                  },
        
                  {
                      label: 'Text Table',
                      id  : 'HTTEXT',
                      purpose: #STANDARD,
                      type : #LINEITEM_REFERENCE,
                      targetElement: '_HText',
                      position: 40
                  }]
        
          @UI.fieldGroup : [{ qualifier : 'one',
                              position : 10 }]
        
          @UI: { selectionField: [{ position: 10 }],
                 lineItem      : [{position: 10}],
                 identification: [{position: 10, importance: #HIGH, label: 'Handle Type'}]}
          handletype;
        
        
          @UI.fieldGroup : [{ qualifier : 'one',
                             position : 20 }]
        
          @UI: { lineItem      : [{position: 20, importance: #HIGH, label: 'Handle Cutout' }],
                 identification: [{position: 20, importance: #HIGH, label: 'Handle Cutout' }],
                 textArrangement: #TEXT_LAST}
          handlecutout;
        
        }

        How can I enable the “insert” also on the Text Table (third facet) ?

         

        Thanks in advance,

        Alex

        • Hi Alex,

          I wouldn’t know without looking at your CDS views. But you can always check that the text consumption has the following annotation.

          @ObjectModel: {
              updateEnabled: true,
              createEnabled: true,
              deleteEnabled: true
          }

          Thanks,

          Diego

          • Hi Diego,

            I have it set there also.

            @AbapCatalog.sqlViewName: 'YFIORI_F_VC300T'
            @AbapCatalog.compiler.compareFilter: true
            @AbapCatalog.preserveKey: true
            @AccessControl.authorizationCheck: #NOT_REQUIRED
            @EndUserText.label: 'Handle Type - Texts'
            @VDM.viewType: #CONSUMPTION
            
            @ObjectModel:{
                createEnabled: true,
                updateEnabled: true,
                deleteEnabled: true,
                semanticKey: ['handletype', 'langu']
            }
            
            define view YFIORI_FB_C_300T
              as select from YFIORI_FB_I_300T as Text
              association [1..1] to YFIORI_FB_C_300 as _Header on $projection.handletype = _Header.handletype
            {
            
              @UI: { lineItem      : [{position: 10}],
                     identification: [{position: 10, importance: #HIGH, label: 'Handle Type' }]}
              key Text.handletype,
              
              @UI: { lineItem      : [{position: 20}],
                     identification: [{position: 20, importance: #HIGH, label: 'Language' }]}
              key Text.langu,
              
              @UI: { lineItem      : [{position: 30}],
                     identification: [{position: 30, importance: #HIGH, label: 'Description' }]}
                  Text.description,
                  
              @ObjectModel.association.type: [#TO_COMPOSITION_PARENT, #TO_COMPOSITION_ROOT]
              _Header    
            }
            
  • Hey!

     

    very cool Blog! Enjoyed reading and tried out immediately. I was searching for such a possibility for days.

     

    Do you have any hints on how to comsume the oData-Draft Service in UI5? I mean not using a SmartTemplate which could be used in the Web IDE.

     

    Do you know where this has been done or maybe done it yourself?

    Greetings

    Benjamin

    • Hi Benjamin,

       

      I’ve used it with Fiori Elements applications, but it can be done with a custom app as well. If you debug in Chrome, the http requests from the Fiori Elements list report application that consumes a service with draft capabilities you can see how they need to look like. It seems pretty straight forward, although it might be very time consuming so I would suggest trying the Fiori elements approach first.

       

      Regards,

       

      Diego

      • Hey Diego,

         

        thanks for your fast feedback. I searched a lot but there is so little information out there actually.

        I came around the class sap.ui.generic.app.transaction.DraftController which could help me implementing Draft in a freestyle app. A Fiori Elements App will not satisfy our needs, thats why I went to freestyle UI5 Application.

        Maybe the class is of interrest for you as well.

         

        Greetings

        Benjamin

  • Hi Diego,

    Excellent post. I’m using this approach for apps that need to communicate via BAPIs to the system.

    One thing is that I keep getting dumps when mapping the keys in the last step using¬† /bobf/cl_dac_legacy_mapping . Did you experience such problems? In your code you have¬†<fs_key>, which following your example I would assume it’s a reference to the order number.

    • Hi Marco,

      I didn’t experience problems for this. Keep in mind that the code I wrote in this blog is for illustration purposes only. The value of <fs_key> is not a data reference, is just a structure that contains the values for your legacy key. In the case of the PO it will only contain one field, which is the PO number, but in some scenarios they key could be multiple fields and hence it can be a structure. I used a simple utility class to explain this logic, maybe take a look at method get_uuid_for_legacy_key and it will make more sense.

       

      Cheers,

       

      Diego

      CLASS zcl_lib_draft_utility DEFINITION
        PUBLIC
        FINAL
        CREATE PRIVATE .
      
        PUBLIC SECTION.
          CLASS-METHODS get_instance RETURNING VALUE(ro_instance) TYPE REF TO zcl_lib_draft_utility.
          METHODS get_uuid_for_legacy_key IMPORTING !is_key               TYPE any
                                                    !is_ctx               TYPE /bobf/s_frw_ctx_draft
                                          RETURNING VALUE(rv_active_uuid) TYPE guid
                                          RAISING   /bobf/cx_frw.
          METHODS draft_has_active_entity IMPORTING !is_ctx                     TYPE /bobf/s_frw_ctx_draft
                                                    !it_draft_key               TYPE /bobf/t_frw_key
                                                    !io_read                    TYPE REF TO /bobf/if_frw_read
                                          RETURNING VALUE(rv_has_active_entity) TYPE abap_bool
                                          RAISING   /bobf/cx_frw.
        PROTECTED SECTION.
        PRIVATE SECTION.
          CLASS-DATA mo_instance TYPE REF TO zcl_lib_draft_utility.
      ENDCLASS.
      
      
      
      CLASS zcl_lib_draft_utility IMPLEMENTATION.
      
        METHOD get_instance.
          "Singleton implementation
          IF mo_instance IS NOT BOUND.
            mo_instance = NEW #( ).
          ENDIF.
          ro_instance = mo_instance.
      
        ENDMETHOD.
      
        METHOD get_uuid_for_legacy_key.
      
          DATA: lr_active_key TYPE REF TO data.
      
          "Get BOPF configuration object
          DATA(lo_conf) = /bobf/cl_frw_factory=>get_configuration( iv_bo_key = is_ctx-bo_key ).
      
          "Get node specific configuration data
          lo_conf->get_node( EXPORTING iv_node_key = is_ctx-node_key
                             IMPORTING es_node     = DATA(ls_node_conf) ).
      
          "Get alternative key data from node
          lo_conf->get_altkey( EXPORTING iv_node_key    = is_ctx-node_key
                                         iv_altkey_name = /bobf/if_conf_cds_link_c=>gc_alternative_key_name-draft-active_entity_key
                               IMPORTING es_altkey      = DATA(ls_altkey_active_key) ).
      
          "Create data reference with type of the alternative key structure
          CREATE DATA lr_active_key TYPE (ls_altkey_active_key-data_type).
          ASSIGN lr_active_key->* TO FIELD-SYMBOL(<ls_active_entity_key>).
      
          "Map values from importing structure to alternative key structure
          MOVE-CORRESPONDING is_key TO <ls_active_entity_key>.
      
          "Map alternative key to BOPF node
          DATA(lo_dac_map) = /bobf/cl_dac_legacy_mapping=>get_instance( iv_bo_key = is_ctx-bo_key iv_node_key = is_ctx-node_key ).
          lo_dac_map->map_key_to_bopf( EXPORTING ir_database_key = lr_active_key
                                       IMPORTING es_bopf_key     = DATA(ls_key) ).
      
          "Return active UUID
          rv_active_uuid = ls_key-key.
      
        ENDMETHOD.
      
      
        METHOD draft_has_active_entity.
      
          DATA: lr_data TYPE REF TO data.
          FIELD-SYMBOLS: <ft_data> TYPE STANDARD TABLE.
      
          "Get BOPF configuration object
          DATA(lo_conf) = /bobf/cl_frw_factory=>get_configuration( iv_bo_key = is_ctx-bo_key ).
      
          "Get node specific configuration data
          lo_conf->get_node( EXPORTING iv_node_key = is_ctx-node_key
                             IMPORTING es_node     = DATA(ls_node_conf) ).
      
          "Create data reference for BOPF node table
          CREATE DATA lr_data TYPE (ls_node_conf-data_table_type).
          ASSIGN lr_data->* TO <ft_data>.
      
          "Read node data
          io_read->retrieve(
            EXPORTING
              iv_node       = is_ctx-node_key
              it_key        = it_draft_key
            IMPORTING
              et_data       = <ft_data>
          ).
      
          IF lines( <ft_data> ) NE 1.
            RAISE EXCEPTION TYPE /bobf/cx_frw_fatal.
          ENDIF.
      
          ""Check if Draft has an active entity
          READ TABLE <ft_data> ASSIGNING FIELD-SYMBOL(<fs_data>) INDEX 1.
          ASSIGN COMPONENT if_draft_constants=>co_db_fieldname-has_active_entity OF STRUCTURE <fs_data> TO FIELD-SYMBOL(<fs_has_active>).
          rv_has_active_entity = <fs_has_active>.
      
        ENDMETHOD.
      
      ENDCLASS.
  • Hello Diego:

    I tried to create a similar functionality.

    However, when testing the EntitySet service using Gateway client….it gives below error.

    “Draft 2.0 object CDS_XXXXX~XXXX requires selection condition on IsActiveEntity”

    Any idea, what could be the reason for this?

     

    • Hi Vineesh,

      That error is because when you have an application using drafts, the entity keys are composite. This means that on top of whatever your entity key might be, the framework adds a new key property called “IsActiveEntity”. It is required to be able to differentiate between active and draft instances of the same object.

      Just add the missing property to your requests and it should work. Something like this:

      /EnntityTypeSet(Guid=guid'000c292f-5c6a-1ee9-91b8-41416c9be78c',IsActiveEntity=true)

      Regards,

      Diego

    • Hello Diego:

      Not sure. I already tried passing the same, but it gave below error:

      The request URI contains an invalid key predicate.

      Also, as I had mentioned….I am testing the Odata service via SAP Gateway Client….

      It seems that there is an SAP Note to correct this error. Atleast the class method which is triggering this exception has been modified to prevent sending exception

      2566749 ‚Äď Odata $filter on draft enabled entities does not
      work

      • Okay, I was able to determine the issue.

        The problem was with the template I had selected. With the correct template, Fiori app displays data correctly now.

        Thank you for your help and this detalied blog.