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:
- 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.
- Get the Draft data from the parent object and from the children that allow modifications. Use the io_read object to retrieve the data.
- Use the Draft data to populate the interface of whichever BAPI or handler class you are using.
- 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.
- 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 😀
Can't wait to try this out. Thanks!
Yes indeed blog for S/4HANA developer.
Thanks fo sharing.
BR,
Syam
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,
thanks, makes perfect sense now..magical world of annotations 🙂
Regards,
Sitakant.
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:
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
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.
Thanks,
Diego
Hi Diego,
I have it set there also.
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
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
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
Hi Diego,
I am facing the same issue now. Could yo please let me know how did you solve this issue?
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:
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.
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
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
Hi Abhijeet,
Yes, and it is better to do it that way.
Regards,
Diego
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
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
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.
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
Â
can you please help me how I can implement method copy_draft_to_active_entity step wise
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...