Skip to Content
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!

 

Quick navigation

11 Comments
You must be Logged on to comment or reply to a post.
  • 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!

    • 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

  • 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