Monday, 11 January 2021

FI-CO Master data APIs via Abstract Factory Objects

 Background

There are not many options in the SAP ERP system for consuming FI-CO and its associated master data objects especially in a single Work Process (LUW). Other constraints below:

◉ BDC screen recordings come in the way of consumption – no options for Class based APIs for these FI-CO master data objects especially for master data maintenance (these guys are as old as the dinosaurs but they are not extinct yet!).

◉ MDG/MDM is not used by majority of the customers who use SAP ERP.

◉ There are some Function Modules available in the system but they are not even wrapped up for data maintenance in a single instance – they are also not object oriented as a result the caller has to map data every time and  subsequently call those Function Modules again.

◉ They all live happily in their own Function Groups with global data dependency and call sequence dependency (E.g. FM 2 of Function Group does nothing unless FM 1 of the same FG was called prior to calling FM 2)

◉ These APIs could be helpful for anyone looking to consume them from anywhere in the SAP system or outside – for example, Fiori OData or a Webservice or from a non-SAP system via an inbound PI interface; these API objects could be called within their Helper Class for example.

Business Requirement

For the business requirement I was working on, the following needed to happen from a business process perspective:

◉ A new Adobe Interactive Form (ISR Processes and Forms) to be able to enter master data attributes for CRUD operations (except the Delete).

◉ The Adobe form is validated, existence of master data objects are checked, master data object creation/updates are simulated before user is able to send the data for review and approval via Business Workflow.

◉ New Workflow is triggered when the Adobe form is submitted.

◉ Create/Update of Master data objects takes place at the final level approval of the Workflow.

The FI-CO master data objects are:

◉ Create/Update of a GL Account in two separate Chart of Accounts (FSP0) and the flexibility to:

     ◉ Create a GL Account in Chart of Account with a Reference to an existing GL Account.

◉ Create/Update of a GL Account in Company Code (FS00, FSS0) and the flexibility to:

     ◉ Create a GL Account in Company Code with a Reference to an existing GL Account.

◉ Create/Update (i.e. Assignment) of a GL Account in two separate Financial Statement Version (FSV) (FSE2)

◉ Create/Update of a Commitment Item (related to the GL Account) (FMCIA) and the flexibility to:

     ◉ Create a Commitment Item with a Reference to an existing Commitment Item.

◉ Create/Update of a Cost Element (related to the GL Account) (KA01/02) and the flexibility to:

     ◉ Create a Cost Element with Reference to an existing Cost Element.

Example of a Consumption (Adobe Interactive Form and Workflow)

I will not go into too much details for the Adobe form design and implementation because the focus of this blog are the FI-GL APIs which are consumption agnostic i.e. it does not matter if a form is trying to consume it or a Fiori OData Service implementation Class or a PI interface – just think of the form as a trigger to consume the Abstract Factory objects.

The ISR form implementation is driven by its BAdI (Definition QISR1) implementation where these API objects are called – short view of code snippet below from the BAdI implementation (this is where the object simulation happens during form data validation)

The int_service_request_check method of the BAdI Interface validates the data after Check button is clicked on the ISR form.

METHOD if_ex_qisr1~int_service_request_check.

    me->set_mode( mode ).

    me->set_command( user_command ).

    me->set_view( form_view ).

    me->set_special( special_data ).

    me->set_additional( additional_data ).

    me->set_control( CHANGING ct_special_data = special_data ).

    CHECK me->get_view( )     = |ISR_REQUEST|                            AND

          NOT me->get_fldvalue( |DRAFT| )                                AND

        ( me->get_mode( )     = |CREATE| OR me->get_mode( ) = |CHANGE| ) AND

        ( me->get_command( )  = |CHECK|  OR me->get_command( ) = |START| ).

    TRY.

        me->check_mandatory( ).

        me->simulate( ).

      CATCH BEFORE UNWIND zcx_fi_general INTO DATA(lo_exception).

        IF lo_exception->is_resumable IS NOT INITIAL.

          RESUME.

        ELSE.

          me->set_messages( lo_exception->get_messages( ) ).

          message_list = me->get_messages( ).

          me->clear_fields( ).                                 

          special_data = me->get_special( ).

          return = VALUE #( message_list[ 1 ] OPTIONAL ).

          RETURN.

        ENDIF.

    ENDTRY.

    message_list = me->get_messages( ).

    return = VALUE #( message_list[ 1 ] OPTIONAL ).

    special_data = me->get_special( ).

  ENDMETHOD.

Master data object simulation happens below – I have only shown two because all others are similar.

Note they all call the same method MAINTAIN_DATA of the API Interface ZIF_MASTERDATA_FACTORY.

  METHOD simulate.

    me->set_simulate( abap_true ).

    me->set_api_mode( SWITCH #( me->get_fldvalue( |REQUEST_TYPE| )

                       WHEN 1

                       THEN action-insert

                       ELSE action-update

                               )

                     ).

    "Simulate Maintain account in COA 2000

    me->set_coa_data( SWITCH #( me->get_api_mode( )

                      WHEN action-insert

                      THEN me->coa_mapper( )

                      ELSE me->read_glmast(

                            VALUE #(

                             saknr = me->get_fldvalue( |REF_GLACCT| )

                             ktopl = me->get_fldvalue( |REF_COA| )

                                   )

                                          )-coa_data

                             )

                   ).

    me->set_acct_names( me->acctname_mapper( ) ).

    me->lo_gl_factory = me->get_md_factory( 

                         VALUE #( LET api_mode = me->get_api_mode( ) IN

                           saknr = CONV #( me->get_fldvalue(

                                            SWITCH #( api_mode

                                            WHEN action-insert

                                            THEN |NEW_GLACCT|

                                            ELSE |REF_GLACCT|

                                                    )

                                                           )

                                         )

                            ktopl = CONV #( me->get_fldvalue(

                                             SWITCH #( api_mode

                                             WHEN action-insert

                                             THEN |NEW_COA|

                                             ELSE |REF_COA|

                                                     )

                                                            )

                                           )

                                 )

                                           ).

    IF me->lo_gl_factory IS BOUND.

      me->lo_gl_factory->zif_masterdata_factory~maintain_data( ).

    ENDIF.

    "Simulate Maintain account in COA 1000

    me->set_coa_data( SWITCH #( me->get_api_mode( )

                       WHEN action-insert

                       THEN me->coa_mapper( coa_oper_tafe )

                       ELSE me->read_glmast( 

                             VALUE #(

                               saknr = me->get_fldvalue( |REF_GLACCT2| )

                               ktopl = me->get_fldvalue( |REF_COA2| )

                                     )

                                           )-coa_data

                              )

                    ).

    me->set_acct_names( me->acctname_mapper( coa_oper_tafe ) ).

    me->lo_gl_factory = me->get_md_factory( 

                         VALUE #( LET api_mode = me->get_api_mode( ) IN

                           saknr = CONV #( me->get_fldvalue(

                                            SWITCH #( api_mode

                                             WHEN action-insert

                                             THEN |NEW_GLACCT2|

                                             ELSE |REF_GLACCT2|

                                                           )

                                                    )

                                         )

                             ktopl = CONV #( me->get_fldvalue(

                                              SWITCH #( api_mode

                                               WHEN action-insert

                                               THEN |NEW_COA2|

                                               ELSE |REF_COA2|

                                                       )

                                                             )

                                            )

                                   )

                                          ).

    IF me->lo_gl_factory IS BOUND.

      me->lo_gl_factory->zif_masterdata_factory~maintain_data( ).

    ENDIF.

    "Simulate Commitment Item

    me->set_commitment( me->commitment_mapper( ) ).

    me->lo_gl_factory = me->get_md_factory( 

                         VALUE #( LET api_mode = me->get_api_mode( ) IN

                          fikrs = CONV #( me->get_fldvalue(

                                           SWITCH #( api_mode

                                            WHEN action-insert

                                            THEN |NEW_FM|

                                            ELSE |REF_FM|

                                                   )

                                                          )

                                          )

                          fipex = CONV #( me->get_fldvalue(

                                           SWITCH #( api_mode

                                            WHEN action-insert

                                            THEN |NEW_COMITEM|

                                            ELSE |REF_COMITEM|

                                                   )

                                                          )

                                         )

                                 )

                                          ).

    IF me->lo_gl_factory IS BOUND.

      me->lo_gl_factory->zif_masterdata_factory~maintain_data( ).

    ENDIF.

    "Simulate Maintain account in CCODE 1020

    me->set_ccode_data( me->ccode_mapper( ) ).

    me->lo_gl_factory = me->get_md_factory( 

                         VALUE #( LET api_mode = me->get_api_mode( ) IN

                          saknr = CONV #( me->get_fldvalue(

                                           SWITCH #( api_mode

                                            WHEN action-insert

                                            THEN |NEW_GLACCT4|

                                            ELSE |REF_GLACCT4|

                                                   )

                                                           )

                                         )

                          bukrs = CONV #( me->get_fldvalue(

                                           SWITCH #( api_mode

                                            WHEN action-insert

                                            THEN |NEW_COMPCODE4|

                                            ELSE |REF_COMPCODE4|

                                                   )

                                                          )

                                        )

                                 )

                                          ).

    IF me->lo_gl_factory IS BOUND.

      me->lo_gl_factory->zif_masterdata_factory~maintain_data( ).

    ENDIF.

    "Simulate Section Account Lock/Unlock

    IF me->get_fldvalue( |REQUEST_TYPE| ) = 3.

      me->lo_gl_factory =   NEW zcl_fi_glmast_factory(

                              iv_saknr      = CONV #( me->get_fldvalue(

                                                |REF_ACCTLOCK| ) )

                              iv_ktopl      = CONV #( me->get_fldvalue(

                                                |REF_COALOCK| ) )

                              iv_mode       = me->get_api_mode( )

                              iv_commit     = xsdbool( NOT me->get_simulate( ) )

                              iv_simulate   = me->get_simulate( )

                           ).

      IF me->lo_gl_factory IS BOUND.

        CAST zcl_fi_glmast_factory( me->lo_gl_factory )->set_coa_block( 

               CONV #( me->get_fldvalue( |NEW_LOCK| ) ) ).

      ENDIF.

    ENDIF.

    "Simulate Section Account Set/Clear Deletion Flag

    IF me->get_fldvalue( |REQUEST_TYPE| ) = 4.

      me->lo_gl_factory =  NEW zcl_fi_glmast_factory(

                            iv_saknr      = CONV #( me->get_fldvalue(

                                             |REF_ACCTDEL| ) )

                            iv_ktopl      = CONV #( me->get_fldvalue(

                                             |REF_COADEL| ) )

                            iv_mode       = me->get_api_mode( )

                            iv_commit     = xsdbool( NOT me->get_simulate( ) )

                            iv_simulate   = me->get_simulate( )

                         ).

      IF me->lo_gl_factory IS BOUND.

        CAST zcl_fi_glmast_factory( me->lo_gl_factory )->set_coa_delete(

               CONV #( me->get_fldvalue( |NEW_DEL| ) ) ).

      ENDIF.

    ENDIF.

  ENDMETHOD.

The Master Data Factory Abstract Class is instantiated here

  METHOD get_md_factory.

* @TODO use Structure for import params instead of multiple single fields

    TRY.

        ro_md_factory = zcl_fico_masterdata_factory=>create(

                          iv_saknr      = is_keys-saknr

                          iv_costelem   = is_keys-kstar

                          iv_comitem    = is_keys-fipex

                          iv_ktopl      = is_keys-ktopl

                          iv_bukrs      = is_keys-bukrs

                          iv_fikrs      = is_keys-fikrs

                          iv_versn      = is_keys-versn

                          iv_ergsl      = is_keys-ergsl

                          is_coa_data   = me->get_coa_data( )

                          is_ccode_data = me->get_ccode_data( )

                          is_acct_name  = me->get_acct_names( )

                          iv_coarea     = co_kokrs 

                          is_citemdata  = me->get_commitment( )

                          is_costinput  = me->get_costelement( )

*                      iv_keydate    = SY-DATUM

*                      iv_coelclass  = '1'

                          iv_mode       = me->get_api_mode( )

                          iv_simulate   = me->get_simulate( )

                          iv_commit     = xsdbool( NOT me->get_simulate( ) )

                        ).


      CATCH zcx_fi_general.

        "this is necessary for a better user (UI) experience

        DATA(raise) = COND #( WHEN is_keys-saknr IS NOT INITIAL OR 

                                   is_keys-fipex IS NOT INITIAL

                        THEN abap_true

                        ELSE THROW RESUMABLE 

                              zcx_fi_general(

                               textid    = zcx_fi_general=>incorrect_params

                               gv_string = |Incorrect Parameters|

                                            )

                            ).

    ENDTRY.

  ENDMETHOD.

Application Design

◉ Design Pattern Abstract Factory to the rescue including an Interface for all Master Data object CRUD operations.

◉ The idea is to increase the level of abstraction for flexibility and reusability within all the objects of this solution as well as within the development landscape in general so that:

   ◉ It is easy to add a new master data object in this same model in future by simply using object Inheritance.

◉ Within this design and implementation, any master data factory object is the Abstract object and a distinct (concrete) factory object is implemented for each master data object (e.g. GL, Commitment Items and so on).

◉ Two Abstract Super classes are used; one for returning the concrete Subclass (API Superclass) and the other for returning the concrete Factory Subclass (Factory Superclass).



◉ Interface used in the Abstract API Superclass


◉ Interface used in the Abstract Factory Superclass


Implementation


The implementation was complex for the business requirement I was working on because it involved a business process around:

1. End user completes and submits a PDF interactive Form for approval

◉ PDF Form driven by its (ISR) framework BAdI -> Data Validations + Master data update Simulation

2. Workflow is triggered by the ISR framework

◉ Handle business logic, derive Level 1 Agents for the WF to send notification for approval
◉ On approval, derive Level 2 Agents for the WF to send notification for approval
◉ On approval, derive Level 3 Agents for the WF to send notification for approval
   ◉ Maintain (Create/Update) the following FI-CO master data objects as input on PDF Form:
      ◉ GL account 123 in Chart of Accounts (CoA) XX
      ◉ GL account 123 in CoA YY
      ◉ GL account  123 in Company Code ZZ
      ◉ Update Financial Statement for GL account 123 in Version XX
      ◉ Update Financial Statement for GL account 123 in Version YY
      ◉ Commitment Items for GL account 123
      ◉ Cost Elements for GL account 123
      ◉ Update a custom transparent table with GL account 123

The update of all the master data objects are triggered from an Update Function Module which is called from a (Workflow) Class which in turn is called from a Workflow Task at the appropriate place in the Workflow to synchronously update all the data sequentially (All or Nothing) raising proper exceptions in the Workflow Class which are handled by the Workflow.

But it does not need to be so complex depending on the business scenario – you may simply want to  create a new GL account or create a new GL account with Reference or just update an existing GL account as an asynchronous update as shown in the example below.

REPORT ztest.

DATA(p_saknr) = CONV saknr( |0000567864| ).

DATA(lo_output) = cl_demo_output=>new( ).

TRY.
*******CREATE A NEW GL IN COA 2000***********
    lo_output->begin_section( |Create GL Account { p_saknr ALPHA = OUT } in COA 2000| ).

    DATA(result) = zcl_fico_masterdata_factory=>create(
                     iv_saknr      = p_saknr                        " GL Account
                     iv_ktopl      = |2000|                         " Chart of Accounts
                     iv_bukrs      = VALUE #( )
                     is_coa_data   = VALUE #( gvtyp = abap_true     " Data for COA
                                              ktoks = |5600| )
                     is_acct_name  = VALUE #( txt20 = |short_{ p_saknr ALPHA = OUT }| " Data for Account Name
                                              txt50 = |long__{ p_saknr ALPHA = OUT }| )
                        )->zif_masterdata_factory~maintain_data( ).

    lo_output->write( COND #( WHEN result = abap_true THEN |GL Account { p_saknr ALPHA = OUT } in COA 2000 created| ) )->end_section( ).
*-------------------------------------

********CREATE A NEW GL IN COA 1000***********
    lo_output->begin_section( |Create GL Account { p_saknr ALPHA = OUT } in COA 1000| ).

    result = zcl_fico_masterdata_factory=>create(
                 iv_saknr      = p_saknr                         " GL Account
                 iv_ktopl      = |1000|                          " Chart of Accounts
                 iv_bukrs      = VALUE #( )
                 is_coa_data   = VALUE #( gvtyp = abap_true      " Data for COA
                                          ktoks = |5600|
                                          xbilk = abap_false
                                          bilkt = p_saknr )
                 is_acct_name  = VALUE #( txt20 = |short_{ p_saknr ALPHA = OUT }|  " Data for Account Name
                                          txt50 = |long__{ p_saknr ALPHA = OUT }| )
                   )->zif_masterdata_factory~maintain_data( ).

    lo_output->write( COND #( WHEN result = abap_true THEN |GL Account { p_saknr ALPHA = OUT } in COA 1000 created| ) )->end_section( ).
*-------------------------------------

********CREATE A NEW GL IN COMP CODE 1020***********
    lo_output->begin_section( |Create GL Account { p_saknr ALPHA = OUT } in Company Code 1020| ).

    result = zcl_fico_masterdata_factory=>create(
                 iv_saknr      = p_saknr                         " GL Account
                 iv_ktopl      = VALUE #( )
                 iv_bukrs      = |1020|                          " Company Code
                 is_ccode_data = VALUE #( fstag = |ICCF|         " Data for Company Code
                                          mwskz = |*|
                                          altkt = |6511|
                                          xmwno = abap_true
                                          xsalh = abap_true )
                is_acct_name  = VALUE #( txt20 = |short_{ p_saknr ALPHA = OUT }|  " Data for Account Name
                                         txt50 = |long__{ p_saknr ALPHA = OUT }| )
                   )->zif_masterdata_factory~maintain_data( ).

    lo_output->write( COND #( WHEN result = abap_true THEN |GL Account { p_saknr ALPHA = OUT } in Company Code 1020 created| ) )->end_section( ).
*--------------------------------------

********CREATE A NEW GL WITH REFERENCE IN COA 2000***********
    lo_output->begin_section( |Create GL Account 567863 with Reference { p_saknr ALPHA = OUT } in COA 2000| ).

    result = zcl_fico_masterdata_factory=>create(
                 iv_saknr      = |0000567863|                    " GL Account
                 iv_ktopl      = |2000|                          " Chart of Accounts
                 iv_bukrs      = VALUE #( )                      " Company Code
                 is_acct_name  = VALUE #( txt20 = |short_567863| " Data for Account Name
                                          txt50 = |long__567863| )
                    )->zif_masterdata_factory~maintain_data( iv_ref = p_saknr ).

    lo_output->write( COND #( WHEN result = abap_true THEN |GL Account 567863 with Reference { p_saknr ALPHA = OUT } in COA 2000 created| ) )->end_section( ).
*---------------------------------------

********UPDATE AN EXISTING GL IN COA 2000***********
    lo_output->begin_section( |Update GL Account 0000567863 in COA 2000| ).

    result = zcl_fico_masterdata_factory=>create(
                 iv_saknr      = |0000567863|                    " GL Account
                 iv_ktopl      = |2000|                          " Chart of Accounts
                 iv_bukrs      = VALUE #( )                      " Company Code
                 is_acct_name  = VALUE #( txt20 = |short_567863_udpate| " Data for Account Name
                                          txt50 = |long__567863_update| )
                 iv_mode       = |U|                                    " Update Mode
                    )->zif_masterdata_factory~maintain_data( ).

    lo_output->write( COND #( WHEN result = abap_true THEN |GL Account 567863 in COA 2000 updated| ) )->end_section( ).
*---------------------------------------

  CATCH zcx_fi_general INTO DATA(lo_exception).              " General Exception Object for FI
    lo_output->write( lo_exception->get_messages( ) )->end_section( ).
ENDTRY.

lo_output->display( ).

Another example of a simple implementation could be to create a new GL with reference in COA and Company Code then finally create a Commitment Item for the newly created GL asynchronously. The beauty of the Abstract model lies in its Polymorphism as we keep calling the same object method to achieve all the functionalities above – the consumption program simply calls the Interface Method maintain_data( ) and it does not need to know about the different business logic in each of its subclasses i.e. for the different master data objects involved in the process.

DATA(p_saknr) = CONV saknr( |0000567864| ).

DATA(lo_output) = cl_demo_output=>new( ).

TRY.
********CREATE A NEW GL WITH REFERENCE IN COA 2000***********
    lo_output->begin_section( |Create GL Account 567862 with Reference { p_saknr ALPHA = OUT } in COA 2000| ).

    DATA(result) = zcl_fico_masterdata_factory=>create(
                     iv_saknr      = |0000567862|                    " GL Account
                     iv_ktopl      = |2000|                          " Chart of Accounts
                     iv_bukrs      = VALUE #( )                      " Company Code
                     is_acct_name  = VALUE #( txt20 = |short_567862| " Data for Account Name
                                              txt50 = |long__567862| )
                        )->zif_masterdata_factory~maintain_data( iv_ref = p_saknr ).

    lo_output->write( COND #( WHEN result = abap_true THEN |GL Account 567862 with Reference { p_saknr ALPHA = OUT } in COA 2000 created| ) )->end_section( ).
*-----------------------------------------------

********CREATE A NEW GL WITH REFERENCE IN Comp Code 1020***********
    lo_output->begin_section( |Create GL Account 567862 with Reference { p_saknr ALPHA = OUT } in Comp Code 1020| ).

    result = zcl_fico_masterdata_factory=>create(
                 iv_saknr      = |0000567862|                        " GL Account
                 iv_ktopl      = VALUE #( )                          " Chart of Accounts
                 iv_bukrs      = |1020|                              " Company Code
                 is_acct_name  = VALUE #( txt20 = |short_567862|     " Data for Account Name
                                          txt50 = |long__567862| )
                    )->zif_masterdata_factory~maintain_data( iv_ref = p_saknr ).

    lo_output->write( COND #( WHEN result = abap_true THEN |GL Account 567862 with Reference { p_saknr ALPHA = OUT } in Comp Code 1020 created| ) )->end_section( ).
*-----------------------------------------------

********CREATE A NEW COMMITMENT ITEM WITH REFERENCE TO EXISTING COMITEM***********
    lo_output->begin_section( |Create Commitment Item 567862 with Reference { p_saknr ALPHA = OUT }| ).

    result = zcl_fico_masterdata_factory=>create(
                 iv_fikrs       = |1000|                                    "FM Area
                 iv_comitem     = |567862|                                  "Commitment Item
                    )->zif_masterdata_factory~maintain_data( iv_ref = p_saknr ).

    lo_output->write( COND #( WHEN result = abap_true THEN |Commitment Item 567862 with Reference { p_saknr ALPHA = OUT } created| ) )->end_section( ).
*-----------------------------------------------
  CATCH zcx_fi_general INTO DATA(lo_exception).              " General Exception Object for FI
    lo_output->write( lo_exception->get_messages( ) )->end_section( ).
ENDTRY.

lo_output->display( ).

Development resources – abapGit and SAPLINK


I have uploaded the source code here as abapGit files and also as SAPLINK Nugget files. Note that you do not need to implement all the objects (Classes) but choose the ones you need to meet your business requirement. If you would need a different FI-CO master data object for your process which is absent in the abapGit, you simply need to extend the model by creating a new API class and a new Factory class which Inherits the respective Super Classes.

No comments:

Post a Comment