Skip to Content
Author's profile photo Diego Borja

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 😀

 

 

Assigned Tags

      32 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær

      Can't wait to try this out. Thanks!

      Author's profile photo Syambabu Allu
      Syambabu Allu

      Yes indeed blog for S/4HANA developer.

      Thanks fo sharing.

       

      BR,

      Syam

      Author's profile photo Sitakant Tripathy
      Sitakant Tripathy

       

      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.

       

      Author's profile photo Diego Borja
      Diego Borja
      Blog Post Author

      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

      Author's profile photo Sitakant Tripathy
      Sitakant Tripathy

       

      Hi Diego,

      thanks, makes perfect sense now..magical world of annotations 🙂

       

      Regards,

      Sitakant.

       

      Author's profile photo Clement Lau
      Clement Lau

      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

      Author's profile photo Diego Borja
      Diego Borja
      Blog Post Author

      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

       

      Author's profile photo VIGNESH SV
      VIGNESH SV

      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.

      Author's profile photo Diego Borja
      Diego Borja
      Blog Post Author

      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

      Author's profile photo Alex Caruntu
      Alex Caruntu

      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

      Author's profile photo Diego Borja
      Diego Borja
      Blog Post Author

      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

      Author's profile photo Alex Caruntu
      Alex Caruntu

      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    
      }
      
      Author's profile photo Benjamin Kreuscher
      Benjamin Kreuscher

      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

      Author's profile photo Diego Borja
      Diego Borja
      Blog Post Author

      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

      Author's profile photo Benjamin Kreuscher
      Benjamin Kreuscher

      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

      Author's profile photo Aashwin Jain
      Aashwin Jain

      Hey Benjamin,

      i was currently working on a freestyle application which required draft functionality.

      Would you be able to elaborate how did you achieve this using the DraftController class?

       

      Thanks,

      Aashwin jain

      Author's profile photo Marco Hernandez
      Marco Hernandez

      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.

      Author's profile photo Diego Borja
      Diego Borja
      Blog Post Author

      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.
      Author's profile photo Adarsh Priya Chary Yellenki
      Adarsh Priya Chary Yellenki

      Hi Diego,

       

      I am facing the same issue now. Could yo please let me know how did you solve this issue?

       

      Author's profile photo Vineesh Varghese
      Vineesh Varghese

      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?

       

      Author's profile photo Diego Borja
      Diego Borja
      Blog Post Author

      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

      Author's profile photo Vineesh Varghese
      Vineesh Varghese

      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

      Author's profile photo Vineesh Varghese
      Vineesh Varghese

      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.

      Author's profile photo Timothy Muchena
      Timothy Muchena

      Hi

      Thank you for a very insightful blog.

      What happens in cases where I am not creating a draft and just want to create a business object using a BAPI for example. Reason is my system AS ABAP 7.50 and it does not have draft capability.

       

      Kind regards

      Author's profile photo Abhijeet Kankani
      Abhijeet Kankani

      Hi Diego Borja,

      Is it possible to use CDS as Reference data source and Using bopf both together using draft concept instead of using annotation :odata.publish :true.

       

      Regards,

      Abhijeet Kankani

      Author's profile photo Diego Borja
      Diego Borja
      Blog Post Author

      Hi Abhijeet,

      Yes, and it is better to do it that way.

       

      Regards,

       

      Diego

      Author's profile photo Jono Thomas Kannankara
      Jono Thomas Kannankara

      Hi Diego Borja,

      I created CDS with the draft option allowing only the update for my scenario. Updates are happenning using the class that you described on the top. Unfortunately i'm getting an error when i try to cancel the draft data. Im getting the responce in odata saying "Deleting operations are disabled for entity '<entity_name>". Do we really need to enable the delete opration inorder to cancel/Delete the draft? Or is there any method i need to redefine to perform this?

      Thank You

      Jono Thomas

      Author's profile photo Diego Borja
      Diego Borja
      Blog Post Author

      You have you enable the delete operation in the @ObjectModel annotation. That will not delete active entities on its own so you don't have to worry about that. It will only delete the draft ones.

      To delete active entities there is a class that gets created when you generate your business object that gets called when any delete operation on your root object happens, whether is an active entity or not. Inside that class, there is a method "execute" I believe, where you have to write the code that deletes the active entity from the standard tables. As long as you don't implement that method, your persisted objects are safe.

       

      Cheers,

       

      Diego

      Author's profile photo Kishore Gokara
      Kishore Gokara

      Hi Diego,

      Thanks for the wonderful blog. What if I dont want a draft functionality. How to read the data and update/create the records? Will there be a different class and methods generated in case of non-draft functionality?

      Thanks,

      Kishore.

      Author's profile photo Diego Borja
      Diego Borja
      Blog Post Author

      Hi Kishore,

      If you dont want draft capabilities and you are creating a managed scenario in which you need to handle the final posting to the database, then you don’t need to use BOPF. What you need to do is just build your CDS views to handle the reads and the annotations, then create a new project in SEGW and reference the CDS as a data source so that you can still delegate the read operations to SADL. Finally you can implement the create/update/delete operations by hand inside the DPC_EXT class.

       

      Cheers,

       

      Diego

       

      Author's profile photo CHENGALARAYULU DAMALACHERUVU
      CHENGALARAYULU DAMALACHERUVU

      can you please help me how I can implement method copy_draft_to_active_entity step wise

      Author's profile photo Radoslaw Chudziak
      Radoslaw Chudziak

      The official documentation doesn't cover it because according to manual of course: S4D435 Transactional Apps with CDS-based BOPF Objects - it says that this method you described ('manual implementation of active persistence') is for SAP only and not released for customers. What's the point of this framework then if we can't (officially) do such a basic thing and call a BAPI...