Skip to Content

Co-authored by Dinesh Shingare and Kapil Changrani

Requirements and Corresponding Solutions.

One of our clients has a requirement to use FIORI My Inbox App and SAP GUI for their PO and PR approval process, client is using SAP S4HANA 1610.

Purchase order workflow is pretty standard so we will not talk about it in this blog.

Purchase requisition workflow is heavily customized and we had to follow certain steps to make it work on FIORI My Inbox App.

Duplicate Level of Approval

Usually for each level in release strategy separate work item goes to the corresponding approver(s) inbox. However here business requirement was little different.  If the same approver(s) is maintained at consecutive levels, then he/she should able to approve the levels in one go.  System should check who is maintained at the next level and accordingly he/she should able to process those many levels. All these levels get enabled to release based on the roles provided to the approver. If approver doesn’t have a role for any particular level then he/she will not able to see the release button enabled .

Example:  If approver X is maintained at levels Y1,Y2,Y3 then X should able to release the PR from Y1 to Y3 once he open the PR for release and SAVE only once. After the SAVE next workflow for next release strategy should trigger i.e Y4.

PS: This multilevel approval in one go can only be done in SAP GUI and not in My Inbox APP or at least we did not find a way to handle it, because My Inbox App is replication of SBWP and when you have same approver at consecutive levels, the approver will receive a new work item for each level, and those will be shown one by one in My Inbox App.

 

Processor Name in PR Header

Business requirement was to have the release strategy window at PR header level. We could have achieved this just by configuring the approvers along with the release strategy and have them automatically populated on the header level once the PR gets created. However in current business requirement multiple or single approver for any particular level so the config was not possible, to achieve this we have maintained a custom matrix table.

Business wanted to see the approvers in the header in the field called ‘PROCESSOR’. This field gets populated only if there is some value else this column will not be visible. So we required to find out a way which will populate the values for processor. If there are multiple approver’s then populate MULTIPLE for that level else for single approver populate the name of the approver.

Standard Function Module ME_REL_GET_RESPONSIBLE_EBAN is enhanced for this purpose. Export parameter ex_actors needs to be populated with the desired processors/approvers.

 

Processor field will now populate dynamically as shown below:

 

Passing FIORI Links in Email.

Business requirement was to get a fiori link in the SBWP work item and in an email so that approver will click on the link and get an access to the Fiori Launchpad directly.

Solution is provided by using the dictionary element ZCFX_HYPERLINK.

 

Work item in SBWP would look like :

 

Email going to approver would look like :

 

We need to declare a workflow container element of below type.

 

Piece of code required to appear the given link as hyperlink and which ideally should open the internet explorer or your default web browser.

To populate GV_LINKS we have used standard texts (SO10) to maintain links for different systems – Dev, QA and Production. We have fetched the links in workflow step using function module read_text. You can have your own logic to fetch FIORI links in email.

FIORI Configuration

FrontEnd System

Check your release task in workflow and maintain it in scenario definition.

 

Make sure to use the System Alias as assigned to the TaskProcessing Service in transaction /iwfnd/maint_services and assign it to Custom Release Task, in our case its TS90100009.

BackEnd System

In our scenario we have 2 step ids in workflow which are dialog tasks for different levels of approvers based on the workflow logic.

So we need to maintain both 28 and 59 in Task Names and Decision Options, so both can appear in My Inbox App

Both Task should have same decision keys.

Transaction SWFVISU (Define Visualization Parameter)

Copy the settings of standard task TS20000159 and add our custom workflow release task TS90100009. (even though we have 2 separate release steps in workflow Node 28 and 59 PFTC task for both is same)

 

BADI /IWWRK/BADI_WF_BEFORE_UPD_IB to read release codes and FIORI rejection and approval comments. 

For Implementation – Make sure to add correct filter values

For below we have WS90100004 (PR Approval Workflow) with Task 28 and 59 as decision tasks for different levels based on workflow logic.

WS90100006 and Task 5 is rejection workflow which triggers Rejection Notification to the PR creator.

 

Code block of Method Before Update – you may need to tweak it based on your scenario.

  • Below we are showing on how to capture release codes – mandatory step to make sure approve / reject works as required.
  • Also we have captured Approve and Reject comments and are appending to those texts in Purchase requisition header.

 

"  Copied from CL_MM_PUR_REQ_APPR_ACTION_BADI
*---------------------------------------------------------------------*
* Program Name        : ZCL_P2P_PR_APP
* Author              : KCHANGRANI
* Creation Date       : June 19 2018
* RICEF ID            : INC0080003
* Description         : Capture user decision.
*                     :
* Purpose(optional)   :
*---------------------------------------------------------------------*
* Change History
* Date        Programmer Transport   Chg Req#   Description
* 06/11/2018  DShingare  HADK908472  INC0080003 Changes regarding texts
*----------------------------------------------------------------------*
  method /iwwrk/if_wf_wi_before_upd_ib~before_update.
    data ls_object          type swr_obj_2.
*    DATA lv_objtype         TYPE swr_struct-object_typ.
*    DATA lv_objkey          TYPE swr_struct-object_key.
    data lv_retcode         type sy-subrc.
    data lt_container       type table of swr_cont.
    data ls_container_line  type swr_cont.
    data ls_container_line2  type swr_cont.

    data formnumber         type swxformabs-formnumber.
    data ls_formabs         type swxformabs.
    data lv_release_code    type frgco.
    data ls_contianer       type swr_cont.
    data: lv_pr_num       type banfn,
          lv_pr_itm       type bnfpo,
          lv_user_id      type syuname,
          ls_wiid_boident type mmpur_utils_workitem_boident,
          ls_wfl_inb      type mmpur_utils_workflow_task,
          lo_wf_api       type ref to cl_mm_pur_util_apv_wf_api,
          lv_bo_itm       type swo_objtyp value cl_mmpur_constants=>if_mmpur_constants_archive~bus2009,
          lv_bo_hdr       type swo_objtyp value cl_mmpur_constants=>if_mmpur_constants_archive~bus2105,
          lo_const        type ref to cl_mmpur_constants,
          lt_swwwihead    type standard table of swwwihead,
          ls_swwwihead    type swwwihead,
          lv_index        type sy-index.
    data lt_msg_lines type sapi_msg_lines.
    data: lt_msg_struc        type sapi_msg_struc,
          lt_subcontainer_bor type /iwwrk/tt_wf_container,
          lt_subcontainer_all type /iwwrk/tt_wf_container,
          lt_object           type swrtobject.

    data:
      lv_td_name   type tdobname,
      lv_td_object type tdobject value 'PROC_WFL',
      lt_line      type tline_t,
      lt_lines     type tline_t,
      ls_line      type tline,
      ls_header    type thead.


*    DATA lv_retcode TYPE sy-subrc.
* Get PR object ID and item number from UTIl class.
    lo_wf_api = cl_mm_pur_util_apv_wf_api=>get_instance( ).
    lv_user_id = sy-uname.
    ls_wfl_inb-workitem_id = is_wi_details-wi_id.

*   CREATE OBJECT lo_const.

    if lo_wf_api is not initial.
* Get the Object Information from WorkItem.
      call method lo_wf_api->get_boident_for_workitem
        exporting
          iv_user_id        = lv_user_id
          is_workflow_inbox = ls_wfl_inb
        importing
          es_wiid_boident   = ls_wiid_boident.
* Check, that the PR is found for the given Workitem ID
      if ls_wiid_boident is not initial.
        lv_pr_num = ls_wiid_boident-object_id.
        lv_pr_itm = ls_wiid_boident-object_line.
      else.
*   Handle error
        return.
      endif.
    endif.

    "Access the workflow data
    call function 'SAP_WAPI_GET_OBJECTS'
      exporting
        workitem_id      = is_wi_details-wi_id
      importing
        leading_object_2 = ls_object.

    "Get the formnumber which is the key to the absence table
    move ls_object-instid to formnumber.
    "Select the details of the absence from the table SWXFORMABS
    select single * from swxformabs
    into ls_formabs
    where formnumber = formnumber.

    " get the work item id where we can get the release code.
    " 1 get related work item id Function Module SWI_GET_RELATED_WORKITEMS
    " 2 get release code Function module SAP_WAPI_READ_CONTAINER

    call function 'SWI_GET_RELATED_WORKITEMS'
      exporting
        wi_id       = is_wi_details-wi_id
      tables
        related_wis = lt_swwwihead.

    if sy-subrc = 0.
      " Keep looking for
      loop at lt_swwwihead into ls_swwwihead.
        "Read the workflow's container data
        clear   lt_container.
        refresh lt_container.        call function 'SAP_WAPI_READ_CONTAINER'
          exporting
            workitem_id              = ls_swwwihead-wi_id
*           LANGUAGE                 = SY-LANGU
*           USER                     = SY-UNAME
*           BUFFERED_ACCESS          = 'X'
          importing
            return_code              = lv_retcode
*           IFS_XML_CONTAINER        =
*           IFS_XML_CONTAINER_SCHEMA =
          tables
            simple_container         = lt_container
            message_lines            = lt_msg_lines
            message_struct           = lt_msg_struc
            subcontainer_bor_objects = lt_subcontainer_bor
            subcontainer_all_objects = lt_subcontainer_all.

        call function 'SAP_WAPI_GET_ATTACHMENTS'
          exporting
            workitem_id    = ls_swwwihead-wi_id
*           USER           = SY-UNAME
*           LANGUAGE       = SY-LANGU
*           COMMENT_SEMANTIC_ONLY       = ' '
          importing
            return_code    = lv_retcode
          tables
            attachments    = lt_object
            message_lines  = lt_msg_lines
            message_struct = lt_msg_struc.


        " Check which decision was selected and set the data
        " values appropriately
        read table lt_container into ls_contianer with key element = 'GV_REL_CODE'.
        if sy-subrc eq 0.
          lv_release_code = ls_contianer-value.
          exit. " exit the loop as we found the release code we were looking for.
        endif.
      endloop.

   endif.


    if lv_pr_num is initial.
      lv_pr_num = ls_object-instid(10).
    endif.

    case iv_decision_key.

      when 0001. "Approved
        ls_container_line-value = 'A'.
        ls_formabs-procstate    = 'A'.

        try.
            call method me->set_decision_release
              exporting
                iv_pr_num       = lv_pr_num
                iv_pr_itm_num   = lv_pr_itm
                iv_release_code = lv_release_code.

          catch /iwbep/cx_mgw_busi_exception .
            "Handle error
            return.
        endtry.

        "Once  approval is done update FIORI Approval Notes in PR.
        read table it_wf_container_tab into data(ls_comment)
               with key element = 'ACTION_COMMENTS'.
        if sy-subrc = 0.
          if lv_pr_itm is initial.
            lv_td_name = lv_pr_num.
          else.
            concatenate lv_pr_num lv_pr_itm into lv_td_name.
          endif.
          concatenate sy-uname ':' lv_release_code ':' ls_comment-value into  ls_line-tdline.
*        ls_line-tdline = ls_comment-value.
          ls_line-tdformat = `/`.

          call function 'READ_TEXT'
            exporting
*             CLIENT                  = SY-MANDT
              id                      = 'B02' "Approve Notes(FIORI)
              language                = sy-langu
              name                    = lv_td_name
              object                  = 'EBANH'
            tables
              lines                   = lt_lines
            exceptions
              id                      = 1
              language                = 2
              name                    = 3
              not_found               = 4
              object                  = 5
              reference_check         = 6
              wrong_access_to_archive = 7
              others                  = 8.
          if lt_lines is not initial.
            append ls_line to lt_lines.
            ls_header-tdobject = 'EBANH'.
            ls_header-tdname   = lv_td_name.
            ls_header-tdid     = 'B02'. "Approve Notes(FIORI)
            ls_header-tdspras     = sy-langu.

            call function 'INSERT_TEXT_AFTER_COMMIT'
              exporting
                header = ls_header
              tables
                lines  = lt_lines.
            if sy-subrc eq 0.

            endif.
          elseif lt_lines is initial. " no previous text
            append ls_line to lt_lines. " new approval notes.
            ls_header-tdobject = 'EBANH'.
            ls_header-tdname   = lv_td_name.
            ls_header-tdid     = 'B02'. "Approve Notes(FIORI)
            ls_header-tdspras     = sy-langu.
            call function 'INSERT_TEXT_AFTER_COMMIT'
              exporting
                header = ls_header
              tables
                lines  = lt_lines.
            if sy-subrc eq 0.

            endif.
          else.
*    do nothing
          endif.
        endif.
      when 0002. "Rejected
        ls_container_line-value = 'R'.
        ls_formabs-procstate    = 'R'.

        try.
            call method me->set_decision_reject
              exporting
                iv_pr_num       = lv_pr_num
                iv_pr_itm_num   = lv_pr_itm
                iv_release_code = lv_release_code.

          catch /iwbep/cx_mgw_busi_exception .
            "Handle error
            return.
        endtry.
        " get rejection comments.
        read table it_wf_container_tab into ls_comment
                       with key element = 'ACTION_COMMENTS'.
        if sy-subrc = 0.
          if lv_pr_itm is initial.
            lv_td_name = lv_pr_num.
          else.
            concatenate lv_pr_num lv_pr_itm into lv_td_name.
          endif.
          concatenate sy-uname ':' lv_release_code ':' ls_comment-value into  ls_line-tdline.
*        ls_line-tdline = ls_comment-value.
          ls_line-tdformat = `/`.

          call function 'READ_TEXT'
            exporting
*             CLIENT                  = SY-MANDT
              id                      = 'B03'
              language                = sy-langu
              name                    = lv_td_name
              object                  = 'EBANH'
            tables
              lines                   = lt_lines
            exceptions
              id                      = 1
              language                = 2
              name                    = 3
              not_found               = 4
              object                  = 5
              reference_check         = 6
              wrong_access_to_archive = 7
              others                  = 8.
          if lt_lines is not initial.

            append ls_line to lt_lines.

            ls_header-tdobject = 'EBANH'.
            ls_header-tdname   = lv_td_name.
            ls_header-tdid     = 'B03'.
            ls_header-tdspras     = sy-langu.

            call function 'INSERT_TEXT_AFTER_COMMIT'
              exporting
                header = ls_header
              tables
                lines  = lt_lines.
            if sy-subrc eq 0.

            endif.
          elseif lt_lines is initial.
            append ls_line to lt_lines.
            ls_header-tdobject = 'EBANH'.
            ls_header-tdname   = lv_td_name.
            ls_header-tdid     = 'B03'.
            ls_header-tdspras     = sy-langu.
            call function 'INSERT_TEXT_AFTER_COMMIT'
              exporting
                header = ls_header
              tables
                lines  = lt_lines.
            if sy-subrc eq 0.

            endif.
          else.
*    do nothing
          endif.
        endif.
    endcase.

    "_WI_RESULT is what the workflow keys off to determine
    "which path to follow - Approve or Reject path
    ls_container_line-element = '_WI_RESULT'.
    "Modify the workflow's container data - we are updating the row that
    "holds _WI_RESULT which will be in the second row of the table
    read table lt_container into ls_container_line2 with key element = '_WI_RESULT'.
    if sy-subrc = 0.
      lv_index = sy-tabix.
      modify lt_container  index lv_index  from ls_container_line .
    else.
      append ls_container_line to lt_container.
    endif.


    call function 'SAP_WAPI_WRITE_CONTAINER'
      exporting
        workitem_id      = is_wi_details-wi_id
        language         = sy-langu
        actual_agent     = sy-uname
        do_commit        = 'X'
*       IFS_XML_CONTAINER                  =
*       OVERWRITE_TABLES_SIMPLE_CONT       = ' '
*       CHECK_INBOX_RESTRICTION            = ' '
      importing
        return_code      = lv_retcode
      tables
        simple_container = lt_container
        message_lines    = lt_msg_lines
        message_struct   = lt_msg_struc.

    "Update the Absence table with the updated data
    ls_formabs-approvdate = sy-datum.
    ls_formabs-approvby = sy-uname.
    update swxformabs from ls_formabs.
    " Commit workitem.
* trigger update task and persistency layer
    go_factory->commit( ).

    "Complete the task
    call function 'SAP_WAPI_WORKITEM_COMPLETE'
      exporting
        workitem_id    = is_wi_details-wi_id
        actual_agent   = sy-uname
        language       = sy-langu
*       SET_OBSOLET    = ' '
        do_commit      = 'X'
*       DO_CALLBACK_IN_BACKGROUND       = 'X'
*       IFS_XML_CONTAINER               =
*       CHECK_INBOX_RESTRICTION         = ' '
      importing
        return_code    = lv_retcode
*       NEW_STATUS     =
      tables
*       SIMPLE_CONTAINER                =
        message_lines  = lt_msg_lines
        message_struct = lt_msg_struc.

    "This task requires a confirm to be fully completed
    "and start the next step in the workflow

    call function 'SAP_WAPI_WORKITEM_CONFIRM'
      exporting
        workitem_id    = is_wi_details-wi_id
        actual_agent   = sy-uname
        language       = sy-langu
        do_commit      = 'X'
*       CHECK_INBOX_RESTRICTION         = ' '
*       DO_CALLBACK_IN_BACKGROUND       = 'X'
      importing
        return_code    = lv_retcode
*       NEW_STATUS     = 'STARTED'
      tables
        message_lines  = lt_msg_lines
        message_struct = lt_msg_struc.

*Below code is not necessary if you are using a simple workflow with only 1 dialog step for approval.

*start of change INC0080003-06/11/2018{ Dshingare

* Data declarations:

    data: lv_objkey      type sweinstcou-objkey,
          lt_cont        type table of swr_cont,
          ls_cont        type swr_cont,
          lt_rel_users   type table of zpreq_rel_users,
          lt_rel_temp    type table of zpreq_rel_users,
          ls_rel_users   type zpreq_rel_users,
          ls_preq_wfuser type zpreq_wfuser,
          ls_rel_temp    type zpreq_rel_users,
          lv_curr_code   type frgco,
          lv_ernam       type ernam,
          lv_kostl       type kostl,
          lv_werks       type ewerk,
          lv_lines       type i,
          lv_tabix       type sytabix,
          lv_next_code   type frgco,
          lv_last_code   type frgco.

    clear: lv_kostl,
           lv_werks,
           lv_tabix,
           lv_lines,
           lv_last_code,
           lv_next_code.



    if lv_release_code eq 'Y0'.
      lv_next_code = 'Y1'.
    else.

      select single kostl
              from ebkn
              into lv_kostl
             where banfn = lv_pr_num.
      if sy-subrc eq 0.

        select single werks
                 from eban
                 into lv_werks
                where banfn = lv_pr_num.
        if sy-subrc eq 0.

          select *
            from zpreq_rel_users
            into table lt_rel_users
            where kostl = lv_kostl
             and  werks = lv_werks.
          if sy-subrc eq 0.

            sort lt_rel_users[] by alevel.
            lt_rel_temp[] = lt_rel_users[].
            sort lt_rel_temp[] by alevel.
            delete adjacent duplicates from lt_rel_temp[] comparing alevel.

            sort lt_rel_temp[] by alevel ascending .

            read table lt_rel_temp[] into ls_rel_temp with key alevel = lv_release_code.
            describe table lt_rel_temp[] lines lv_lines.
            if sy-subrc eq 0.
              if lv_lines gt sy-tabix.
                lv_tabix = sy-tabix + 1.
              else.
                lv_tabix = sy-tabix.

                read table lt_rel_temp into ls_rel_temp index lv_lines.
                if sy-subrc eq 0.
                  if ls_rel_temp-alevel eq lv_release_code.
                    lv_last_code = 'X'.
                    exit.
                  endif.
                endif.

              endif.
              read table lt_rel_temp into ls_rel_temp index lv_tabix.
              if sy-subrc eq 0.
                lv_next_code = ls_rel_temp-alevel.

                if not lv_next_code is initial.
                  delete from zpreq_wfuser where banfn = lv_pr_num.

                  loop at  lt_rel_users into ls_rel_users where alevel = lv_next_code.

                    ls_preq_wfuser-banfn = lv_pr_num.
                    ls_preq_wfuser-bname = ls_rel_users-bname.
                    ls_preq_wfuser-alevel = ls_rel_users-alevel.

                    insert zpreq_wfuser from ls_preq_wfuser.
                    commit work.
                    clear: ls_preq_wfuser,
                           ls_preq_wfuser.

                  endloop.
                endif.
              endif.
            endif.
          endif.
        endif.
      endif.
    endif.

    select single ernam
           from eban
           into lv_ernam
          where banfn = lv_pr_num.
    ls_cont-element = 'REL_CODE_NEW'.
    ls_cont-value = lv_next_code.
    append ls_cont to lt_cont.

    ls_cont-element = 'PR_CREATOR'.
    ls_cont-value = lv_ernam.
    append ls_cont to lt_cont.

    lv_objkey = lv_pr_num.

    if iv_decision_key ne '0002'. "Event should not triggere when Rejection happens
      if lv_last_code ne 'X'.
*     Raise a workflow
        call function 'SAP_WAPI_CREATE_EVENT'
          exporting
            object_type     = 'BUS2105'
            object_key      = lv_objkey
            event           = 'Z_RELEASE_PR'
          tables
            input_container = lt_cont.
        if sy-subrc eq 0.
          clear lv_next_code.
        endif.
      endif.
    endif.

*} End of change INC0080003-06/11/2018

  endmethod.

 

Hope our attempt to share knowledge will help someone in community while using custom PR workflow and My Inbox FIORI App, please feel free to provide your comments so we can update the blog or write a better blog next time.

Acknowledgements

This blog and work was not possible without great contribution of all mentors of SAP FIORI Community and especially experts who are providing their guidance in My Inbox Wiki : @jocelyn.dart, @masayuki.sekihara@tejas.chouhan and many more people who are researching everyday and providing contributions on this wiki.

Special thanks to my friend and mentor @krishnakishor.kammaje2 for helping with initial steps.

Thanks to Hitachi SAP experts  Kishore Kunadharaju and Avinash Bhapkar for continued support.

Last but no least my partner Dinesh Shingare, thanks for all the hard-work to achieve all business requirements.

To report this post you need to login first.

1 Comment

You must be Logged on to comment or reply to a post.

  1. Nabheet Madan

    Thanks Kapil for the blog. I have one query which i could not understand is how did you accomplish the skip approval part if already approved? This could have been done in the workflow itself. PR workflow raises event at each level and you can determine while agent determination whether you want to skip/auto approve or get agent approval. This way it could have worked.

    Or am i missing something here?

    Nabheet

    (0) 

Leave a Reply