Friday, 29 March 2019

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)

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications

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.

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications

Once the sales order detail is opened, click the “edit” button in the right corner of the screen to switch to edit mode.

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications

Change the Order quantity, you’ll notice that the item price changes (the header total as well).

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications

Click the “Save” button to change the actual Sales Order. The messages returned by the BAPI will be shown on the screen.

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications

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