Technical Articles
Creating a draft enabled Sales Order Fiori App using the new ABAP Programming Model – Part 6: Converting the draft instance into an active instance & wrap-up
Converting the draft instance into an active instance
In the previous part we were able to provide our app with basic functionalities and draft handling. By now our app should be able to:
- Search for Sales Orders
- List Sales Orders
- Display Sales Order details
- Lock Sales Orders
- Delete Sales Orders
- Create new Sales Order draft instances
- Convert an existing Sales Order into a draft instance
In this last part of the blog series we’ll focus on converting the draft instances into actual Sales Orders (Create / Change).
When we activated the virtual data model, the framework generated a draft handler class. You can find this class by navigating to the root node of the business object. (in this case the Sales Order header node)
Creating a new Sales Order
To have a clear separation between Sales Order creation and change, I created a separate private method for Sales Order creation in the draft handler class.
The implementation is quite straight forward, we’ll just map the draft instance data to the BAPI parameter structures and call BAPI_SALESORDER_CREATEFROMDAT2.
Method definition
METHODS _create_sales_order
IMPORTING is_draft_header TYPE zssdi_soheader
it_draft_items TYPE ztsdi_soitem
EXPORTING ev_vbeln TYPE vbeln
et_messages TYPE bapiret2_t.
Method implementation
METHOD _create_sales_order.
DATA: ls_header TYPE bapisdhd1,
ls_headerx TYPE bapisdhd1x,
lt_items_in TYPE TABLE OF bapisditm,
lt_items_inx TYPE TABLE OF bapisditmx,
lt_schedule TYPE TABLE OF bapischdl,
lt_schedulex TYPE TABLE OF bapischdlx,
lt_partners TYPE TABLE OF bapiparnr,
lv_updateflag TYPE updkz_d.
* Set sales order header data
ls_header = VALUE #( doc_type = is_draft_header-auart
sales_org = is_draft_header-vkorg
distr_chan = is_draft_header-vtweg
division = is_draft_header-spart
sales_off = is_draft_header-vkbur
sales_grp = is_draft_header-vkgrp
purch_no_c = is_draft_header-bstnk ).
ls_headerx = VALUE #( doc_type = abap_true
sales_org = abap_true
distr_chan = abap_true
division = abap_true
sales_off = abap_true
sales_grp = abap_true
purch_no_c = abap_true ).
* Set sales order partners
lt_partners = VALUE #( ( partn_role = 'AG' partn_numb = is_draft_header-kunnr )
( partn_role = 'WE' partn_numb = is_draft_header-kunwe ) ).
* Set item data
LOOP AT it_draft_items INTO DATA(ls_item_data).
IF ls_item_data-hasactiveentity = abap_true.
lv_updateflag = 'U'.
ELSE.
lv_updateflag = 'I'.
ENDIF.
* Set item data
APPEND VALUE #( itm_number = ls_item_data-posnr
material = ls_item_data-matnr
target_qty = ls_item_data-kwmeng
target_qu = ls_item_data-vrkme
sales_unit = ls_item_data-vrkme
short_text = ls_item_data-arktx ) TO lt_items_in.
APPEND VALUE #( updateflag = lv_updateflag
itm_number = ls_item_data-posnr
material = abap_true
target_qty = abap_true
target_qu = abap_true
sales_unit = abap_true
short_text = abap_true ) TO lt_items_inx.
* Set schedule data
APPEND VALUE #( itm_number = ls_item_data-posnr
sched_line = '0001'
req_qty = ls_item_data-kwmeng ) TO lt_schedule.
APPEND VALUE #( itm_number = ls_item_data-posnr
sched_line = '0001'
req_qty = abap_true
updateflag = abap_true ) TO lt_schedulex.
ENDLOOP.
* Call Sales Order create BAPI
CALL FUNCTION 'BAPI_SALESORDER_CREATEFROMDAT2'
EXPORTING
order_header_in = ls_header
order_header_inx = ls_headerx
IMPORTING
salesdocument = ev_vbeln
TABLES
return = et_messages
order_items_in = lt_items_in
order_items_inx = lt_items_inx
order_partners = lt_partners
order_schedules_in = lt_schedule
order_schedules_inx = lt_schedulex.
ENDMETHOD.
Changing an existing Sales Order
In this part we’ll create the private method to implement the Sales Order change functionality. The implementation is also quite straight forward, but needs a little bit more parameters and checks.
In this method we’ll:
- Check if the partners have changed in the draft instance compared to the active Sales Order
- Check for new items
- Check for item changes
- Check for item deletion
- Call BAPI_SALESORDER_CHANGE to update the Sales Order
Remark: There is one known issue with the Sales Order change logic, to be able to change an existing order the durable locking needs to be disabled. This is because the BAPI tries to lock the Sales Order again which will cause the BAPI to fail.
Method definition
METHODS _change_sales_order
IMPORTING is_draft_header TYPE zssdi_soheader
it_draft_items TYPE ztsdi_soitem
EXPORTING et_messages TYPE bapiret2_t.
Method implementation
METHOD _change_sales_order.
DATA: ls_header TYPE bapisdh1,
ls_headerx TYPE bapisdh1x,
lt_items_in TYPE TABLE OF bapisditm,
lt_items_inx TYPE TABLE OF bapisditmx,
lt_schedule TYPE TABLE OF bapischdl,
lt_schedulex TYPE TABLE OF bapischdlx,
lt_partners TYPE TABLE OF bapiparnr,
lt_partner_changes TYPE TABLE OF bapiparnrc,
lv_updateflag TYPE updkz_d.
* Set sales order header data
ls_header = VALUE #( sales_org = is_draft_header-vkorg
distr_chan = is_draft_header-vtweg
division = is_draft_header-spart
sales_off = is_draft_header-vkbur
sales_grp = is_draft_header-vkgrp
purch_no_c = is_draft_header-bstnk ).
ls_headerx = VALUE #( updateflag = 'U'
sales_org = abap_true
distr_chan = abap_true
division = abap_true
sales_off = abap_true
sales_grp = abap_true
purch_no_c = abap_true ).
* Set item data
LOOP AT it_draft_items INTO DATA(ls_item_data).
IF ls_item_data-hasactiveentity = abap_true.
lv_updateflag = 'U'.
ELSE.
lv_updateflag = 'I'.
ENDIF.
* Set item data
APPEND VALUE #( itm_number = ls_item_data-posnr
material = ls_item_data-matnr
target_qty = ls_item_data-kwmeng
target_qu = ls_item_data-vrkme
sales_unit = ls_item_data-vrkme
short_text = ls_item_data-arktx ) TO lt_items_in.
APPEND VALUE #( updateflag = lv_updateflag
itm_number = ls_item_data-posnr
material = abap_true
target_qty = abap_true
target_qu = abap_true
sales_unit = abap_true
short_text = abap_true ) TO lt_items_inx.
* Set schedule data
APPEND VALUE #( itm_number = ls_item_data-posnr
sched_line = '0001'
req_qty = ls_item_data-kwmeng ) TO lt_schedule.
APPEND VALUE #( itm_number = ls_item_data-posnr
sched_line = '0001'
req_qty = abap_true
updateflag = lv_updateflag ) TO lt_schedulex.
ENDLOOP.
* Read original sales order partners
SELECT vbeln,posnr,parvw,kunnr FROM vbpa
INTO TABLE @DATA(lt_sales_order_partners)
WHERE vbeln = @is_draft_header-vbeln
AND posnr = '000000'
AND ( parvw = 'AG' OR parvw = 'WE' ).
LOOP AT lt_sales_order_partners INTO DATA(ls_sales_order_partner).
CASE ls_sales_order_partner-parvw.
WHEN 'AG'.
IF ls_sales_order_partner-kunnr <> is_draft_header-kunnr.
APPEND VALUE #( document = ls_sales_order_partner-vbeln
itm_number = ls_sales_order_partner-posnr
updateflag = 'U'
partn_role = ls_sales_order_partner-parvw
p_numb_old = ls_sales_order_partner-kunnr
p_numb_new = is_draft_header-kunnr ) TO lt_partner_changes.
APPEND VALUE #( partn_role = 'AG' partn_numb = is_draft_header-kunnr ) TO lt_partners.
ENDIF.
WHEN 'WE'.
IF ls_sales_order_partner-kunnr <> is_draft_header-kunwe.
APPEND VALUE #( document = ls_sales_order_partner-vbeln
itm_number = ls_sales_order_partner-posnr
updateflag = 'U'
partn_role = ls_sales_order_partner-parvw
p_numb_old = ls_sales_order_partner-kunnr
p_numb_new = is_draft_header-kunwe ) TO lt_partner_changes.
APPEND VALUE #( partn_role = 'WE' partn_numb = is_draft_header-kunwe ) TO lt_partners.
ENDIF.
ENDCASE.
ENDLOOP.
* Read original sales order items (to determine if items were deleted)
SELECT vbeln,posnr FROM vbap
INTO TABLE @DATA(lt_sales_order_items)
WHERE vbeln = @is_draft_header-vbeln.
LOOP AT lt_sales_order_items INTO DATA(ls_sales_order_item).
* Check if item is still available in draft instance
READ TABLE it_draft_items WITH KEY vbeln = ls_sales_order_item-vbeln posnr = ls_sales_order_item-posnr TRANSPORTING NO FIELDS.
IF sy-subrc = 0.
CONTINUE.
ENDIF.
* Item is not available in draft instance => flag for deletion
* Set item data
APPEND VALUE #( itm_number = ls_sales_order_item-posnr ) TO lt_items_in.
APPEND VALUE #( updateflag = 'D'
itm_number = ls_sales_order_item-posnr ) TO lt_items_inx.
* Set schedule data
APPEND VALUE #( itm_number = ls_sales_order_item-posnr
sched_line = '0001' ) TO lt_schedule.
APPEND VALUE #( itm_number = ls_sales_order_item-posnr
sched_line = '0001'
updateflag = 'D' ) TO lt_schedulex.
ENDLOOP.
* Call Sales Order change BAPI
CALL FUNCTION 'BAPI_SALESORDER_CHANGE'
EXPORTING
salesdocument = is_draft_header-vbeln
order_header_in = ls_header
order_header_inx = ls_headerx
no_status_buf_init = abap_true
TABLES
return = et_messages
order_item_in = lt_items_in
order_item_inx = lt_items_inx
schedule_lines = lt_schedule
schedule_linesx = lt_schedulex
partners = lt_partners
partnerchanges = lt_partner_changes.
ENDMETHOD.
Putting it all together
The draft handler class implements an interface method /bobf/if_frw_draft~copy_draft_to_active_entity which we’ll need to implement. Within this method we’ll:
- Read the Sales Order header data
- Read the Sales Order item data
- Check if the draft instance has an active entity
- If yes => call Sales Order change logic
- If no => call Sales Order create logic
- Map the Draft instance UUID with the active Sales Order to notify the framework
- That the Sales Order was succesfully changed
- The the Sales Order was succesfully created
- Pass all messages generated by the BAPI to the framework
METHOD /bobf/if_frw_draft~copy_draft_to_active_entity.
DATA: lr_active_key TYPE REF TO data,
lt_header_data TYPE ztsdi_soheader,
lt_item_data TYPE ztsdi_soitem,
lt_messages TYPE bapiret2_t,
lt_draft_unlock TYPE if_draft_admin_lock=>tt_key,
lv_vbeln TYPE vbeln.
* Initialize BOPF configuration
DATA(lo_conf) = /bobf/cl_frw_factory=>get_configuration( iv_bo_key = is_ctx-bo_key ).
* Read header data with the given keys
io_read->retrieve(
EXPORTING
iv_node = is_ctx-node_key " uuid of node name
it_key = it_draft_key " keys given to the determination
IMPORTING
eo_message = eo_message " pass message object
et_data = lt_header_data " itab with node data
et_failed_key = et_failed_draft_key " pass failures
).
READ TABLE lt_header_data INTO DATA(ls_header_data) INDEX 1.
* Read item data with the given keys
io_read->retrieve_by_association(
EXPORTING
iv_node = is_ctx-node_key
it_key = it_draft_key
iv_association = zif_sd_i_soheader_c=>sc_association-zsd_i_soheader-_items
iv_fill_data = abap_true
IMPORTING
eo_message = eo_message
et_data = lt_item_data
et_failed_key = et_failed_draft_key
).
* If the draft instance doesn't have an active entity => create new sales order
IF ls_header_data-hasactiveentity = abap_false.
* Create sales order
me->_create_sales_order(
EXPORTING
is_draft_header = ls_header_data
it_draft_items = lt_item_data
IMPORTING
ev_vbeln = lv_vbeln
et_messages = lt_messages
).
ELSE.
* Draft instance has an active entity => change sales order
lv_vbeln = ls_header_data-vbeln.
* Change sales order
me->_change_sales_order(
EXPORTING
is_draft_header = ls_header_data
it_draft_items = lt_item_data
IMPORTING
et_messages = lt_messages
).
ENDIF.
IF line_exists( lt_messages[ type = 'E' ] ).
* Rollback changes
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
* Set draft key as failed
et_failed_draft_key = it_draft_key.
ELSE.
* Commit changes
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING
wait = abap_true.
* Get BOPF configuration
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) ).
* Move sales order key to structure for conversion to GUID
CREATE DATA lr_active_key TYPE (ls_altkey_active_key-data_type).
ASSIGN lr_active_key->* TO FIELD-SYMBOL(<fs_active_entity_key>).
<fs_active_entity_key> = lv_vbeln.
* 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.
ENDIF.
* Create message object
eo_message = /bobf/cl_frw_factory=>get_message( ).
* Pass messages from BAPI to BOPF framework
LOOP AT lt_messages INTO DATA(ls_message).
eo_message->add_message(
EXPORTING
is_msg = VALUE #( msgty = ls_message-type
msgid = ls_message-id
msgno = ls_message-number
msgv1 = ls_message-message_v1
msgv2 = ls_message-message_v2
msgv3 = ls_message-message_v3
msgv4 = ls_message-message_v4 )
).
ENDLOOP.
ENDMETHOD.
Testing creating / changing Sales Orders using our new Fiori App
Now that we’ve implemented all required elements to provide the desired functionality, it’s time to try out our new Fiori App.
Load the Fiori App and search for an existing sales order, click the result line to open the sales order details.
Once the sales order detail is opened, click the “edit” button in the right corner of the screen to switch to edit mode.
Change the Order quantity, you’ll notice that the item price changes (the header total as well).
Click the “Save” button to change the actual Sales Order. The messages returned by the BAPI will be shown on the screen.
Wrap-up
To get this proof-of-concept up and running we had to:
- Create a virtual data model and BOPF business object using CDS
- Create consumption views for our virtual data model using CDS
- Add UI annotations to our consumption views
- Expose our consumption views as an OData service
- Generate a Fiori Elements List Report app
- Add logic to our BOPF business object using ABAP
As you might notice, quite a few steps more to consider while developing a new transactional application compared to classic development methods. Although the additional work required results in easy reusability (either via OData service or Classic ABAP development).
During this blog series we’ve covered the basics to create a BOPF draft enabled Fiori app. But there are still some more functionalities to explore (e.g. BOPF actions, BOPF validations, …).
In my personal opinion the ABAP Programming Model and BOPF framework is great way to quickly create a transactional Fiori app without a lot of effort. I’m really excited to see what the future will bring regarding this approach on developing transactional apps!
I really look forward to your opinions, results and feedback on the subject!
Great Series with step by step tutorials.. True, APM looks very promising and with draft feature enabled it makes end to end Fiori app development way easier.. Hopefully the upcoming Restful Application model will make this more easier.
Thanks Mahesh Kumar Palavalli!
I completely agree, the ABAP Programming Model really makes Fiori App development easier. Although in the beginning everything can get quite confusing, getting to know all the different frameworks / things involved. Which off course was the whole point of this step by step blog series.
Looking forward to what the future brings!
Nice blog. Very interesting.
It calls my attention that for the new technologies there is a lot to code... but as you said (I hope), it will pay later.
J.
Thank you Jesús!
It's true, this requires a little bit more of backend coding. But it already pays off because you don't have to code anything in the Fiori App itself. There are off course scenario's where you might want to create extensions to the generated Fiori App, but in my opinion we should try to keep those extensions to a minimum.
Dear Geert-Jan,
Thank you for this fantastic blog series, which was a pleasure to read because it is so clearly written and perfectly manages to combine a focus on the essential with covering all the bases.
For me, it closes several gaps in my hands-on knowledge, especially converning the legacy access classes, and I will be sure to use it as a reference when working with existing business objects (which is of course often the case) or technical objects such as BAL logs as parts of an object composition.
Keep up the good work! Can't wait to read more from you!
Cheers,
Thorsten
Hi Thorsten,
Thank you for this great feedback!
Nice to hear that the blog series were very clear, I always try to write these type of documents in a way that someone without any prior knowledge should be able to get the desired results.
For me actually this was a first-time hands-on experience for creating an app from scratch, as you mentioned most of the time I'm also working with existing business objects and extending them to fit the needs of the customer.
Cheers,
Geert-Jan
Nice blog. Very interesting... i have learned the flow
Excellent series Geert-Jan ... looking forward to the next one(s) 🙂
Thank you for the excellent documentation. Every step was exactly as detailed as needed, I could follow this tutorial easily and have it working now.
Mark
Thanks for a GREAT series! One of the best step-by-step series I have read on here to date (and I have read a lot! haha)
I do appreciate the depth and detail you went into for each step and point. As Thorsten Franz said above, it really tied together some loose ends for me. That said, I HIGHLY recommend folks read through (or already know) the per-requisite blogs you listed (the ones on durable locking and BOPF were essential!!!!)
You made it all look really REALLY easy though I know how much work was going on behind the scenes......like one of those TV shows where they miraculously renovate an old house in 1 hour. haha Given that, which part (or pieces) would you say took the most time?
One thing I would have liked to seen (just me being picky) is perhaps some kind of discussion about how to "test" each piece as you went along (ex. running your CDS views to make sure the data you expect to see it correct).
Thanks again!
Hi Christopher,
Thanks for the feedback, great to hear this blog series is helping out quite a lot of people to tie up some loose ends!
For me personally the getting to know how to implement the BOPF logic was the most time consuming part. Although I already had experience using the BOPF framework in some S/4HANA implementations, I never implemented a BOPF Business Object from scratch before. And I must say that I'm quite impressed with how much of the objects get generated automatically, this really speeds up the implementation process in my opinion!
I thought about adding some testing, but I wasn't sure this would be a good idea as I really wanted to focus on what mattered and didn't want to make the blog series even more "bulky". But something I'll for sure consider in the future!
Best regards,
Geert-Jan
Excellent series. Open several doors to me. Almost a year after it was published.
If you could still answer, I have few questions:
Best regards
Rafał
Hi Rafal,
2- you need to loop at your condition price table into a referenced structure.
To get your node elements use io_read->retrieve if you are in the right node for conditions or io_read->retrieve_by_association if you want to get a subnode elements
then use BAPI_SALESORDER_SIMULATE new price conditions to update the referenced structure and then change the Fiori result by using io_modify->update
3- You need to add your sales type in the transactionnal cds with annotation
@ObjectModel.readOnly: 'EXTERNAL_CALCULATION'
then you can display your field the way you want in the first class of determination category
Action_and_field_control
inside the method, the field ls_header_data-hasactiveentity tells you if you are in creation mode (abap_false) or modofication mode (abap_true)
then use lo_property_helper->set_attribute_read_only() to grey the field.
Hello,
Thanks a lot for these excellent series, it was a very good start for a complex BOPF-based list Report I am working on. Though I still have a tiny problem to finish it. The mysterious lock.
So there two things:
1- the function module ENQUEUE_EVVBAKE works fine but we need to use DEQUEUE_EVVBAKE after the user exits sales order modification otherwise the draft keeps showing that user is locking the object. So where can I dequeue?
2- function module DEQUEUE_EVVBAKE is not working before calling the change BAPI, I ve read it should be called twice but still it did'nt work
I hope you are still reading this so you might bring us some solutions 🙂
Excellent series. I am able to do all exercises and didn't get any issue anywhere. Really appreciable!!
Hi Geert-Jan,
In first I wanted to say thank you for this very useful blog and very grateful for your sharing ! probably one of the best blog I've never seen ! Great Job !
Concerning the FM DEQUEUE_EVVBAKE that is not working before calling the change BAPI the solution is below:
while calling a BAPI which will implicitly lock again inside, this will cause locking error as the lock is not created in the new durable lock enqueue context. So we need to call the below code.
DATA(enqueue_context) = /bobf/cl_lib_enqueue_context=>get_instance( ).
enqueue_context->attach( ls_head-key ) .
" Any BAPI or FM call which will lock inside again
CALL FUNCTION 'BAPI_SALESORDER_CHANGE' ...
enqueue_context->detach( ) .
Cheers.
Armand.