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)
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!
No comments:
Post a Comment