Introduction:
From Help:
The ABAP programming model for SAP Fiori defines the architecture for efficient end-to-end development of intrinsically SAP HANA-optimized Fiori apps in SAP S/4HANA. It supports the development of all types of Fiori applications like transactional, search, analytics and planning apps and is based on customer-proven technologies and frameworks such as Core Data Services (CDS) for defining semantically rich data models, OData protocol, ABAP-based application services for custom logic and SAPUI5-based user interface.
Using the ABAP Programming model for Fiori, you can develop two kinds of applications
“Transactional apps ‘without’ the Draft features”:
If the applications are developed without Draft, there are many limitations & issues:
1. You cannot create an item and its subitems at the same time. e.g., you cannot create the sales order header and it’s item at the same time. You have to save the header first and later you have to create the items one by one.
2. You cannot dynamically make the fields visible/hidden/editable/readonly. It has very limited field control capabilities.
3. Exclusive locking like the locking in GUI/Webdynpro applications is not available.
4. Context depended search help is not there.. e.g., if you enter a country, you need to show only that entered country states, this is not possible.
5. and many more, you can check the links
“Transactional apps ‘with’ the Draft features”
(Available from 7.51 SP02)
If we develop the apps with Draft capabilities, then we will have all the above and much more awesome features.
So what is this “Draft”..? Like the name “Draft” suggests, It’s something that is not final, its just a temporary version.
When we create or edit an entry in the Draft enabled Fiori app, an entry is created in the draft table(which is configured in the CDS view). When we enter any data in the inputs, a request will go and save the data to the draft table. So even after refreshing the browser, the data which we entered before refreshing the Fiori app in the edit mode will be retrieved back.
What we will get by using this so called “Draft” features while developing the new Fiori apps.
1. The much awaited and most wanted feature “Not the Draft” if you are wondering (atleast for me).. But it is the exclusive locking- “the Durable locks”, which were not possible with fiori till sometime back. BTW I will have another blog on this topic
2. Now of course the “Draft” feature itself- Data loss protection.
3. Device Switch: So using this, you can start on one device(laptop) and continue the changes on another device(mobile/tablet).
4. This will not have any limitations that I mentioned for “Transactional apps without using the Draft features”
So let’s see how it works:
The above image is taken from the SAP UI5 documentation, link available below. It has a very good information about the draft features.
Draft data: temporary data(copy of active data with the new changes)., which will be removed after the final save.
Taking the BP example here.
◈ First if we open an existing BP and click on Edit, it will create the draft version for the actual version and from here on, all the changes you do there will be saved in the draft version.
◈ If we open the role of the business partner and change some data there, roles data will be saved in the role draft table.
◈ So after clicking on the SAVE, it will save all the data from the draft version to the active version and deletes the draft entries.
This draft table is not the actual table, it is the copy of the original table, which we will define in the CDS view. BTW we do not need to create this table manually, SAP will create it automatically when the CDS view is generated.
An overview of how to develop the Draft capable apps using this framework:
1. Create the Base CDS views to fetch the data from the DB tables,
2. Create the “Transactional” type CDS view which will be draft enabled. Here we will just mention the draft table name, SAP framework will create the draft table automatically based on the CDS view.
3. After the activation of the the CDS view, the framework will create the BOPF object, where all our transactional processing will be handled like Field control, validations, lockings, CRUD operations etc.,
4. Then we will create the Consumption view and will provide the annotations, which the Smart Fiori template will use and generate the UI dynamically without us writing the UI5 code
To be frank, there is no big difference between the draft & non draft in terms of development for us, it’s mostly taken care by SAP but the main difference comes in terms of functionality.
Now let’s see the development part of it.
Development:
This example will be very similar to the one that was provided in the help, please check the help links that I’ve provided first before going through this. The only thing that changes in this is instead of using the GUID as the key field, I will use a normal character field.
Before going to the Development part, let’s see a small demo of it.
Tables:
Let’s create 2 new custom tables.( we can use your existing custom tables or standard tables ).
Sales Order header
CLIENT | MANDT(Key) |
SALESORDER | VBELN(Key) |
BUSINESSPARTNER | SNWD_PARTNER_ID |
OVERALLSTATUS | SNWD_SO_OA_STATUS_CODE |
CHANGEDAT | TIMESTAMPL |
CREATEDAT | TIMESTAMPL |
CHANGEDBY | UNAME |
CREATEDBY | UNAME |
Sales Order Item
CLIENT | MANDT(Key) |
SALESORDER | VBELN(Key) |
SALESORDERITEM | SNWD_SO_ITEM_POS (Key) |
PRODUCT | SNWD_PRODUCT_ID |
GROSSAMOUNT | SNWD_TTL_GROSS_AMOUNT |
CURRENCYCODE | SNWD_CURR_CODE |
QUANTITY | INT4 |
Base CDS Views:
We create the base CDS views to fetch the data from the actual database tables, we usually put the logic in these views like doing some conversions or calculations or text changes or writing some join and fetching any data if required. But here in our case we won’t do anything as this is just a basic app.
Sales order Header
@AbapCatalog.sqlViewName: 'ZVSOH'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Orders Headers'
@VDM: {
viewType: #BASIC
}
define view ZSalesOrderHeaders
as select from zso_head
{
key zso_head.salesorder,
zso_head.businesspartner,
zso_head.overallstatus,
zso_head.changedat,
zso_head.createdat,
zso_head.changedby,
zso_head.createdby
}
Sales order Items
@AbapCatalog.sqlViewName: 'ZVSOI'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Items'
@VDM: {
viewType: #BASIC
}
define view ZSalesOrderItems
as select from zso_items
{
key zso_items.salesorder,
key zso_items.salesorderitem,
zso_items.product,
zso_items.grossamount,
zso_items.currencycode,
zso_items.quantity
}
Transaction type CDS Views & BOPF BO:
To enable the draft functionality and Transnational processing.
Sales Order Header
@AbapCatalog.sqlViewName: 'ZSOHTP'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Header BO Layer'
@VDM: {
viewType: #TRANSACTIONAL
}
//****---->>>> Add the below objectModel code after activation of both header and items
@ObjectModel: {
transactionalProcessingEnabled: true,
compositionRoot: true,
createEnabled: true,
updateEnabled: true,
deleteEnabled: true,
draftEnabled: true,
writeDraftPersistence: 'zsoh_d',
semanticKey: [ 'salesorder' ]
}
define view ZI_SalesOrdersHeadTP
as select from ZSalesOrderHeaders as Header
// Assocations
association [0..*] to ZI_SalesOrderItems as _items on _items.salesorder = $projection.salesorder
// Values Helps
association [0..1] to SEPM_I_BusinessPartner as _BusinessPartner on $projection.businesspartner = _BusinessPartner.BusinessPartner
association [0..1] to Sepm_I_SalesOrdOverallStatus as _Status on $projection.overallstatus = _Status.SalesOrderOverallStatus
{
@ObjectModel.readOnly: true
key Header.salesorder,
@ObjectModel.foreignKey.association: '_BusinessPartner'
Header.businesspartner,
@ObjectModel.foreignKey.association: '_Status'
Header.overallstatus,
@ObjectModel.readOnly: true
Header.changedat,
@ObjectModel.readOnly: true
Header.createdat,
@ObjectModel.readOnly: true
Header.changedby,
@ObjectModel.readOnly: true
Header.createdby,
@ObjectModel.association: {
type: [ #TO_COMPOSITION_CHILD ]
}
_items,
// Value help assocations
_Status,
_BusinessPartner
}
The ObjectModel annotations provides the draft & transactional capabilities to the CDS view.
transactionalProcessingEnabled: true,
compositionRoot: true,
The above two will tell that CDS view has transaction processing is enabled and this particular CDS view is the root view(top level view).
draftEnabled: true,
writeDraftPersistence: 'zsoh_d',
The above two will tell that the CDS view is having the draft capabilities and we will just propose a draft table name. The draft table will be auto generated when the view is activated.
semanticKey: [ 'salesorder' ]
semanticKey tells the SAP framework what the key is, it is used mostly for the CDS views which were developed using the GUID as the key field. Using this SAP will understand what the actual key is instead of the GUID.
This is also used by the List template UI5 application to show “Draft” status under the column in the List report app, which it identifies based on the semantickey. So this is better to give for all the CDS views.
@ObjectModel.foreignKey.association: '_BusinessPartner'
Header.businesspartner,
The above are for the value helps
@ObjectModel.readOnly: true
This will make the tell the view that the column is be readonly.
@ObjectModel.association: {
type: [ #TO_COMPOSITION_CHILD ]
}
_items
This will create the association from sales order header to sales order items in the Business Object.
Sales Order Items
@AbapCatalog.sqlViewName: 'ZSOITP'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Items BO Layer'
@VDM: {
viewType: #TRANSACTIONAL
}
//****---->>>> Add the below objectModel code after activation of both header and items
@ObjectModel:{
createEnabled: true,
updateEnabled: true,
deleteEnabled: true,
writeDraftPersistence: 'zsoi_d',
semanticKey:['salesorder', 'salesorderitem']
}
define view ZI_SalesOrderItems
as select from ZSalesOrderItems as Items
association [1..1] to ZI_SalesOrdersHeadTP as _header on _header.salesorder = $projection.salesorder
// Value Help
association [0..1] to SEPM_I_Product_E as _product on $projection.product = _product.Product
association [0..1] to SEPM_I_Currency as _currency on $projection.currencycode = _currency.Currency
{
@ObjectModel.readOnly: true
key Items.salesorder,
@ObjectModel.readOnly: true
key Items.salesorderitem,
@ObjectModel.mandatory: true
@ObjectModel.foreignKey.association: '_product'
Items.product,
@Semantics.amount.currencyCode: 'currencycode'
Items.grossamount,
@ObjectModel.foreignKey.association: '_currency'
@Semantics.currencyCode: true
Items.currencycode,
Items.quantity,
@ObjectModel.association: {
type: [ #TO_COMPOSITION_PARENT, #TO_COMPOSITION_ROOT ]
}
_header,
_product,
_currency
}
Here we just need to mention the draftpersistance table and we do not need to mention the draftEnabled and transactionalProcessingEnabled as the parent view takes this child view data and use it in the BO.
Other things are same as the above, we just need to mention the association to the parent and the root.
So after create the associations and all add the object model to both the header and item cds view and activate them, this will create the BOPF BO and the Draft tables.
How to check the created BO? Just hover on the header CDS view, where the “transactionalProcessingEnabled” is set as true.
Now click on the Business Object, which will navigate to the BO.
Now click on the “Go to the ROOT node” and open the Draft class that is generated.
So what happens is that when we click on create button in the Fiori app, a draft entry will be created in the auto generated “Draft Table”. Then if we enter any of the data in the fiori app, it will saved to the draft table.
(All the logic regarding this will be taken care by the BOPF framework to create the draft entry, to delete and to update using the above class)
So what we need to do is to write the code to save the Draft data from the draft table to the Actual DB tables. To do this, we need to implement the code for the method: copy_draft_to_active_entity. I’ve provide comments inside the code to understand what is going on. The below code will take care of the create and update scenarios.
I am using the direct DB updates, instead you can use the BAPI or Update FMs.
DATA:
ls_msg TYPE symsg.
" Get the message container, to push any messages to the UI if requred
IF eo_message IS NOT BOUND.
eo_message = /bobf/cl_frw_message_factory=>create_container( ).
ENDIF.
" header and items data declaration
DATA(lt_header) = VALUE ztisalesordersheadtp( ).
DATA(lt_items) = VALUE ztisalesorderitems( ).
"Read Header Data
"Pass the node key, which tells if we are trying to read header or item, in this case header
"Pass the draft keys, if we create an entry in the Fiori, they will be saved as draft intitial, those keys we will pass to get the data
io_read->retrieve(
EXPORTING
iv_node = zif_i_salesordersheadtp_c=>sc_node-zi_salesordersheadtp
it_key = it_draft_key
iv_fill_data = abap_true
IMPORTING
et_data = lt_header
).
" Read the Items data
" We need to read via the assocation, so passing the assocation key here
io_read->retrieve_by_association(
EXPORTING
iv_node = zif_i_salesordersheadtp_c=>sc_node-zi_salesordersheadtp " Node Name
it_key = it_draft_key " Key Table
iv_association = zif_i_salesordersheadtp_c=>sc_association-zi_salesordersheadtp-_items
iv_fill_data = abap_true
IMPORTING
et_data = lt_items " Data Return Structure
).
" Always 1 header only expected as we can only create one sales order at a time
DATA(ls_so_header) = CORRESPONDING zso_head( lt_header[ 1 ] ).
" Updating the header
GET TIME STAMP FIELD ls_so_header-changedat.
ls_so_header-changedby = sy-uname.
" I am checking if the sales order number is initial, it means it is the new entry
" Else it is an existing entry
" Alternatively we can also use lt_header[ 1 ]-HASACTIVEENTRY, which will tell if the active entry is available
" If the active entry is available means it is update sceanrio else it is create scenario
IF ls_so_header-salesorder IS NOT INITIAL.
MODIFY zso_head FROM ls_so_header.
ELSE.
GET TIME STAMP FIELD ls_so_header-createdat.
ls_so_header-createdby = sy-uname.
CALL FUNCTION 'NUMBER_GET_NEXT'
EXPORTING
nr_range_nr = '01'
object = 'ZSONR'
IMPORTING
number = ls_so_header-salesorder.
INSERT zso_head FROM ls_so_header.
ENDIF.
" Now update/create the items
DATA:
lt_items_save TYPE TABLE OF zso_items.
" Updating the items
SELECT COUNT( * )
FROM zso_items
INTO @DATA(lv_count)
WHERE salesorder = @ls_so_header-salesorder.
" Setting the item number here.
LOOP AT lt_items REFERENCE INTO DATA(lo_item) WHERE hasactiveentity EQ abap_false.
lv_count = lv_count + 1.
lo_item->salesorder = ls_so_header-salesorder.
lo_item->salesorderitem = lv_count.
CALL FUNCTION 'CONVERSION_EXIT_ALPHA_INPUT'
EXPORTING
input = lo_item->salesorderitem
IMPORTING
output = lo_item->salesorderitem. " Internal display of INPUT, any category
ENDLOOP.
lt_items_save = CORRESPONDING #( lt_items ).
IF lt_items_save IS NOT INITIAL.
MODIFY zso_items FROM TABLE lt_items_save.
ENDIF.
" Now we need to tell the BOPF framework to delete the draft entries as we have successfully created the data in the DB
" But BOPF only understands the data in in GUIDs, so we need to convert the sales order number to BOPF key, we just need to parent key
DATA(lr_key_util) = /bobf/cl_lib_legacy_key=>get_instance( zif_i_salesordersheadtp_c=>sc_bo_key ).
DATA(lv_bobf_key) = lr_key_util->convert_legacy_to_bopf_key(
iv_node_key = zif_i_salesordersheadtp_c=>sc_node-zi_salesordersheadtp
is_legacy_key = ls_so_header-salesorder ).
APPEND VALUE #( draft = it_draft_key[ 1 ]-key active = lv_bobf_key ) TO et_key_link.
Issue in the Update Scenario for the Non GUID key Fields:
When it comes to Update scenario(Sales order is already created and saved) and if you try to create the new sales order items, the items will be added but will not be displayed in the Fiori app till we save it to the database.
This issue is coming because the framework will not know copy the “SalesOrderNo” from header to the newly created “Draft SO Item”. So we have to update the draft entry with the salesorder number manually in the code. Then the app will understand that the SO item belongs to the SO header.
For the Create scenario, it will not happen.
To fix this, we need to create a determination for the BOPF BO sales order item. So this determination will be called at the time of create and update.
BTW I found this solution in the Standard BP Fiori App which also uses the non GUID key fields.
The code is provided with enough comments to understand.
" This is used in the update scenario of the sales order(which has already been created) and when we are creating sales order items
DATA:
lt_header TYPE ztisalesordersheadtp,
lt_items TYPE ztisalesorderitems.
" read the sales order items
io_read->retrieve(
EXPORTING
iv_node = zif_i_salesordersheadtp_c=>sc_node-zi_salesorderitems " Node Name
it_key = it_key
IMPORTING
et_data = lt_items
).
" Obviously one item is only expected, check if the sales order number is initial for the draft entry
READ TABLE lt_items REFERENCE INTO DATA(lo_item) INDEX 1.
IF lo_item->salesorder IS INITIAL.
" If initial, then get the header draft entry, which will have the sales order number via the assocation from child to header
io_read->retrieve_by_association(
EXPORTING
iv_node = zif_i_salesordersheadtp_c=>sc_node-zi_salesorderitems " Node Name
it_key = it_key " Key Table
iv_association = zif_i_salesordersheadtp_c=>sc_association-zi_salesorderitems-to_root
iv_fill_data = abap_true
IMPORTING
et_data = lt_header
).
" Update the salesorder item with the so number
lo_item->salesorder = lt_header[ key = lo_item->parent_key ]-salesorder.
io_modify->update(
EXPORTING
iv_node = zif_i_salesordersheadtp_c=>sc_node-zi_salesorderitems " Node
iv_key = lo_item->key " Key
iv_root_key = lo_item->root_key " NodeID
is_data = lo_item
).
ENDIF.
For deletion, we have any action for that
Go to the auto generated class and write the code to delete the sales order.
" To get only the active data as this code wil trigger for draft deletion as well
" For draft deletion the framework wil take care, for active, we need to.
/bobf/cl_lib_draft=>/bobf/if_lib_union_utilities~separate_keys(
EXPORTING iv_bo_key = is_ctx-bo_key
iv_node_key = is_ctx-node_key
it_key = it_key
IMPORTING
et_active_key = DATA(lt_active_bopf_keys)
).
"we will get the data in BOPF keys format, we need to get the sales order number from that
CHECK lt_active_bopf_keys IS NOT INITIAL.
DATA(ls_header) = VALUE zsk_isalesordersheadtp_active( ).
" Convert the bopf key to active document key
/bobf/cl_lib_draft=>/bobf/if_lib_union_utilities~get_active_document_key(
EXPORTING iv_bo_key = is_ctx-bo_key
iv_node_key = is_ctx-node_key
iv_bopf_key = lt_active_bopf_keys[ 1 ]-key
IMPORTING es_active_document_key = ls_header ).
" Delete the data
DELETE FROM zso_head WHERE salesorder = ls_header-salesorder.
DELETE FROM zso_items WHERE salesorder = ls_header-salesorder.
So till here we completed the business object logic. Now lets see how to create the odata service and generate the Fiori app.
Consumption Processing CDS Views:
Create the Consumption CDS views for both the header and the items.
Sales Order Header
@AbapCatalog.sqlViewName: 'ZCSOH'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Orders Headers Consumption View'
@VDM.viewType: #CONSUMPTION
@Metadata: {
allowExtensions: true
}
@ObjectModel.transactionalProcessingDelegated: true
@ObjectModel: {
compositionRoot: true,
createEnabled: true,
updateEnabled: true,
deleteEnabled: true,
draftEnabled: true
}
@ObjectModel.semanticKey: [ 'salesorder' ]
@OData: {
publish: true
}
define view ZC_SalesOrdersHead
as select from ZI_SalesOrdersHeadTP as SalesOrderHeader
association [0..*] to ZC_SalesOrderItems as _items on _items.salesorder = $projection.salesorder
{
@UI.selectionField: [{
position: 10
}]
@UI.lineItem.position: 10
@UI.identification: [{
position: 10
}]
key SalesOrderHeader.salesorder,
@UI.lineItem.position: 20
@UI.identification: [{
position: 20
}]
SalesOrderHeader.businesspartner,
@UI.lineItem.position: 30
@UI.identification: [{
position: 30
}]
SalesOrderHeader.overallstatus,
@UI.lineItem.position: 40
@UI.lineItem.label:'Created on'
@UI.identification: [{
position: 40,
label:'Created on'
}]
SalesOrderHeader.createdat,
@UI.lineItem.position: 50
@UI.lineItem.label:'Created by'
@UI.identification: [{
position: 50,
label:'Created by'
}]
SalesOrderHeader.createdby,
@UI.lineItem.position: 60
@UI.lineItem.label: 'Changed on'
@UI.identification: [{
position: 60,
label:'Changed on'
}]
SalesOrderHeader.changedat,
@UI.lineItem.position: 70
@UI.lineItem.label:'Changed by'
@UI.identification: [{
position: 70,
label:'Changed by'
}]
SalesOrderHeader.changedby,
@ObjectModel.association: {
type: [ #TO_COMPOSITION_CHILD ]
}
_items,
// Associations
SalesOrderHeader._BusinessPartner,
SalesOrderHeader._Status
}
It is mandatory to mention the ObjectModel & Associations again here as they are utilized for the Odata service generation. This is because they both are having View scope, this documentation you can find in the help.
CDS rule: Remember to double-maintain the annotations that have the VIEW scope. In CDS views, only the annotations with ELEMENT and ASSIOCIATION scope are inherited from the business object view.
@UI.lineItem.position: 50
@UI.identification: [{
position: 50
}]
Line item position is to for showing the item in the table and identification position is to show the it i the form, when opened.
@OData: {
publish: true
}
This will auto create the Odata service for us.
Sales Order Items
@AbapCatalog.sqlViewName: 'ZCSOI'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Items Consumption View'
@VDM.viewType: #CONSUMPTION
@Metadata: {
allowExtensions: true
}
@ObjectModel: {
semanticKey:['salesorder', 'salesorderitem'],
createEnabled: true,
deleteEnabled: true,
updateEnabled: true
}
define view ZC_SalesOrderItems
as select from ZI_SalesOrderItems as Items
association [1..1] to ZC_SalesOrdersHead as _header on _header.salesorder = $projection.salesorder
{
@UI.hidden: true
key Items.salesorder,
@UI.identification.position: 10
@UI.lineItem.position: 10
key Items.salesorderitem,
@UI.identification.position: 20
@UI.lineItem.position: 20
Items.product,
@UI.identification.position: 30
@UI.lineItem.position: 30
Items.grossamount,
@UI.lineItem.label: 'Currency'
@UI.hidden: true
Items.currencycode,
@UI.lineItem.label: 'Quantity'
@UI.lineItem.position: 50
@UI.identification: [{
position: 50,
label: 'Quantity'
}]
Items.quantity,
@ObjectModel: {
association: {
type: [ #TO_COMPOSITION_PARENT, #TO_COMPOSITION_ROOT ]
}
}
_header,
Items._product,
Items._currency
}
Now active both the views, which will create the odata service. We just need to go to the /n/IWFND/MAINT_SERVICE and register the odata service.
So now the consumption view part is done, so we just need to use the webide to generate the list report app from the odata service that we just registered.
Fiori App using the List report template:
There are a lot of blogs out there where they show the process of generating the app based on the template using the webide.
Now run the app, which will be same as the app that was shown in the above video.
I strongly recommend you guys to try out the example in the SAP help as well. It has very good documentation(especially for the GUID keys based).
First of all thanks for sharing such an excellent blog. It has all the details I have been looking for a long time. Now, I have one issue with this. When I am trying to save the data I am getting a message like '
ReplyDeleteYou can't save. The draft is not consistent.'. I tried to resolve this myself but couldn't find a solution. Could you please help me in solving this ?
Regards,
Nithin