Sebastian Wey,

FS-CD

FI-CA Contract Accounts Receivable and Payable, on which FS-CD is based as an industry-specific enhancement for insurance companies, provides a two-level enhancement concept for industry-specific and customer-specific enhancements. This concept is based on events triggered by FI-CA or FS-CD. Currently 1,686 events are defined, of which 1,365 are from FI-CA and 321 from FS-CD (ERP 6.05 SP 8).

For each event, function modules can be entered in Customizing (transaction FQEVENTS), whereby the interface of the modules is predefined by SAP. SAP also determines whether an event can only be implemented once or several times. This distinction can be explained by the characteristics of the event. If the event has a notification character, that is, the triggering program wants to draw attention to a certain situation without expecting a certain result, the event can be implemented several times. This corresponds to the Observer pattern. However, if the caller needs a result from the enhancement, the event can only be implemented once. This corresponds to the Template-Method-Pattern.

FI-CA provides sample modules. FS-CD also provides industry-specific modules. For certain events, FS-CD can exclude the possibility that customer-specific modules are allowed. For events that can be implemented once, the first of the following modules - if available - is called: Customer module, FS-CD module, sample module. If multiple implementations are possible, all FS-CD modules are processed first and then all customer modules. In this case, the sample module is empty and is only used to define the interface.

But what happens if the requirements of several departments are to be mapped in the same system and in the same client? If events can be implemented several times, all departments could define their additional functions, but it is the responsibility of the respective developer that the additional functions of his department are not executed for other departments. For events that can be implemented once, several departments would have to share a function module. The following problems may occur:

The simplest solution would of course be separate clients, but this is not realistic in most cases. For this reason I would like to present a concept for the solution of this problem, which I could implement recently with a customer. This is a framework that uses the Mediator between the event and the user department specific functionality. This framework, I have called it "FS-CD Business Domain Filter", should determine the current department in a first step from the available information, which is available at the respective event. In a second step, the framework calls the department-specific additional functionality. The framework requires the following components:

Derivation of user department

A data element and a fixed value domain ZCD_BDOMAIN are created in the DDIC. The data type is arbitrary, e.g. CHAR 3. The fixed values correspond to the departments. Different information is available at each point in time, which can be used to derive the department. I will use the contract account type as an example, which is available at many times. Other candidates are, for example, the company code, product group or business area. Sometimes more complex derivations may be required, for example, from the contract account number to the contract account category for the department.

A method is defined in interface ZIF_CD_BDOMAIN_DERIVE for each derivation method. These methods are then implemented in class ZCL_CD_BDOMAIN_DERIVE. It makes sense to define interfaces to achieve a loose coupling between a class and its users. Mapping the contract account type to department is stored in the new Customizing table ZCD_VKTYP_BDOM:

AttributeKey?Data elementTypeLengthDescription
CLIENTXMANDTCLNT3Client
VKTYPXVKTYP_KKCHAR2Contract Account Category
BDOMAINZCD_BDOMAINCHAR3Department

Of course, a maintenance view with maintenance dialog should be created for this and all other Customizing tables, but I will not describe this in more detail here.

Since many events are also called in batch processes, it's recommended to load the Customizing table completely in the constructor of the class for performance reasons. We define the instance attribute GT_VKTYP_BDOM in the class ZCL_CD_BDOMAIN_DERIVE with direct type input:
TYPE HASHED TABLE OF zcd_vktyp_bdom WITH UNIQUE KEY vktyp.

You can also define a corresponding table type in the DDIC or in a TYPE-POOL. The table entries are then loaded completely in the constructor:
SELECT * FROM zcd_vktyp_bdom INTO TABLE gt_vktyp_bdom.

The derivation method then looks like this:

KindParameterType
IMPORTINGVALUE( IV_VKTYP )ZCD_VKTYP_BDOM-VKTYP
RETURNINGVALUE( RV_BDOMAIN )ZCD_VKTYP_BDOM-BDOMAIN
METHOD zif_cd_bdomain_derive~get_bdomain_by_vktyp.
  DATA ref_vktyp_bdom TYPE REF TO zcd_vktyp_bdom.
  READ TABLE gt_vktyp_bdom REFERENCE INTO ref_vktyp_bdom WITH TABLE KEY vktyp = iv_vktyp.
  IF sy-subrc EQ 0.
    MOVE ref_vktyp_bdom->bdomain TO rv_bdomain.
  ELSE.
    CLEAR rv_bdomain.
  ENDIF.
ENDMETHOD.

Determination of the function module to be called

A Customizing table ZCD_BDOMAIN_FUNC with the following structure is required to determine the user department specific module:

AttributKey?Data elementTypLengthDescription
CLIENTXMANDTCLNT3Client
BDOMAINXZCD_BDOMAINCHAR3Department
EVENTXFBEVE_KKCHAR4Event
FUNCTIONFUNCC_KKCHAR30Function module

The primary key selected in the Customizing table allows you to transport a change for a user department without affecting other user departments. If the user department were not in the key, a change to an entry may affect another user department.

The method for determining and calling the function module is encapsulated in a separate class ZCL_CD_EVENT_INVOKER, which implements the interface ZIF_CD_EVENT_INVOKER, independently of the logic for determining the user department.

KindParameterType
IMPORTINGVALUE( IV_BDOMAIN )ZCD_BDOMAIN_FUNC-BDOMAIN
IMPORTINGVALUE( IV_EVENT )ZCD_BDOMAIN_FUNC-EVENT
IMPORTINGIT_PARMBINDABAP_FUNC_PARMBIND_TAB
IMPORTINGIT_EXCPBINDABAP_FUNC_EXCPBIND_TAB
RETURNINGVALUE( RV_SUBRC )SYSUBRC
METHOD zif_cd_event_invoker~invoke_event.
  DATA lv_function TYPE zcd_bdomain_func-function.
  lv_function = get_function_by_bdomain(
    iv_bdomain = iv_bdomain
    iv_event   = iv_event ).
  IF NOT lv_function IS INITIAL.
    CALL FUNCTION lv_function
      PARAMETER-TABLE
        it_parmbind
      EXCEPTION-TABLE
        it_excpbind.
  ENDIF.
  MOVE sy-subrc TO rv_subrc.
ENDMETHOD.

The types of the two internal tables IT_PARMBIND and IT_EXCPBIND are defined in the type-pool ABAP and are used to pass the parameters to a function module completely generically. The actual determination of the module to be called takes place in method GET_FUNCTION_BY_BDOMAIN.

KindParameterType
IMPORTINGVALUE( IV_BDOMAIN )ZCD_BDOMAIN_FUNC-BDOMAIN
IMPORTINGVALUE( IV_EVENT )ZCD_BDOMAIN_FUNC-EVENT
RETURNINGVALUE( RV_FUNCTION )ZCD_BDOMAIN_FUNC-FUNCTION
METHOD get_function_by_domain.
  DATA:
    ref_bdomain_func TYPE REF TO zcd_bdomain_func,
    ls_tfkfbm        TYPE tfkfbm,
    ref_func         TYPE REF TO tfkfbc,
    lt_func          LIKE TABLE OF ref_func->*.
  CLEAR rv_function.
  READ TABLE gt_bdomain_func
       REFERENCE INTO ref_bdomain_func
       WITH TABLE KEY event   = iv_event
                      bdomain = iv_bdomain.
  IF sy-subrc NE 0.
    SELECT SINGLE * INTO ls_tfkfbm
           FROM tfkfbm WHERE fbeve = iv_event.
    IF ls_tfkfbm-xmehr EQ abap_true.
      RETURN.
    ENDIF.
    CALL FUNCTION 'FKK_FUNC_MODULE_DETERMINE'
      EXPORTING
        i_fbeve            = iv_event
        i_applk            = 'V'
        i_only_application = abap_true
      TABLES
        t_fbstab           = lt_func.
    READ TABLE lt_func REFERENCE INTO ref_func INDEX 1.
    IF sy-subrc NE 0.
      CREATE DATA ref_func.
      MOVE ls_tfkfbm-funcm TO ref_func->funcc.
    ENDIF.
    CREATE DATA ref_bdomain_func.
    MOVE:
      iv_bdomain      TO ref_bdomain_func->bdomain,
      iv_event        TO ref_bdomain_func->event,
      ref_func->funcc TO ref_bdomain_func->function.
    INSERT ref_bdomain_func->* INTO TABLE gt_bdomain_func.
  ENDIF.
  MOVE ref_bdomain_func->function TO rv_function.
ENDMETHOD.

The module to be called is first searched in the internal table GT_BDOMAIN_FUNC. This internal table is defined as an instance attribute (direct type entry TYPE HASHED TABLE OF ZCD_BDOMAIN_FUNC WITH UNIQUE KEY EVENT BDOMAIN) and was loaded completely from the Customizing table ZCD_BDOMAIN_FUNC in the constructor. If no suitable entry is found there, the system checks whether the event can be implemented more than once. If the event can be implemented more than once, the method is exited, since in this case the FS-CD modules have already been called. However, if the event can only be implemented once, the corresponding FS-CD or FI-CA module must be called in this case if no department-specific module was found. First, the function module FKK_FUNC_MODULE_DETERMINE with the flag I_ONLY_APPLICATION set is used to determine the industry-specific module. If a function module is found (since it is only a simple-implementable event, access with INDEX 1 is OK), it will not be returned as a module to be called, unless the default module is returned, but not without first memorizing it in our internal table GT_BDOMAIN_FUNC for the next time.

Parameter transfer

As already shown above, the two internal tables IT_PARMBIND and IT_EXCPBIND are used to pass parameters dynamically and bind the exceptions dynamically. I chose this way to keep the later coding in the event modules as short as possible and to enable unit tests for the event modules. These binding tables are created by method ZIF_CD_EVENT_INVOKER~BUILD_BINDING:

KindParameterType
IMPORTINGVALUE( IV_EVENT )ZCD_BDOMAIN_FUNC-EVENT
EXPORTINGET_PARMBINDABAP_FUNC_PARMBIND_TAB
EXPORTINGET_EXCPBINDABAP_FUNC_EXCPBIND_TAB
METHOD zif_cd_event_invoker~build_binding.
  DATA:
    lv_funcname TYPE rs38l-name,
    lt_exc      TYPE rsfb_exc,
    lt_exp      TYPE TABLE OF rsexp,
    lt_imp      TYPE TABLE OF rsimp,
    lt_cha      TYPE TABLE OF rscha,
    lt_tbl      TYPE TABLE OF rstbl.
  CONCATENATE gc_func_prefix iv_event INTO lv_funcname.
  CALL FUNCTION 'FUNCTION_IMPORT_INTERFACE'
    EXPORTING
      funcname           = lv_funcname
    TABLES
      exception_list     = lt_exc
      export_parameter   = lt_exp
      import_parameter   = lt_imp
      changing_parameter = lt_cha
      tables_parameter   = lt_tbl.
  CALL METHOD build_excpbind
    EXPORTING
      it_rsexc    = lt_exc
    CHANGING
      ct_excpbind = et_excpbind.
  CALL METHOD build_parmbind
    EXPORTING
      it_parm     = lt_exp
      iv_kind     = abap_func_importing
    CHANGING
      ct_parmbind = et_parmbind.
  CALL METHOD build_parmbind
    EXPORTING
      it_parm     = lt_imp
      iv_kind     = abap_func_exporting
    CHANGING
      ct_parmbind = et_parmbind.
  CALL METHOD build_parmbind
    EXPORTING
      it_parm     = lt_cha
      iv_kind     = abap_func_changing
    CHANGING
      ct_parmbind = et_parmbind.
  CALL METHOD build_parmbind
    EXPORTING
      it_parm     = lt_tbl
      iv_kind     = abap_func_tables
    CHANGING
      ct_parmbind = et_parmbind.
ENDMETHOD.

The constant GC_FUNC_PREFIX is a prefix with which the name of the event module is formed (see below).

The auxiliary method BUILD_EXCPBIND has the following code:

KindParameterType
IMPORTINGIT_RSEXCRSFB_EXC
CHANGINGCT_EXCPBINDABAP_FUNC_EXCPBIND_TAB
METHOD build_excpbind.
  DATA:
    lr_excp     TYPE REF TO  rsexc,
    ls_excpbind LIKE LINE OF ct_excpbind.
  LOOP AT it_rsexc REFERENCE INTO lr_excp.
    MOVE:
      lr_excp->exception TO ls_excpbind-name,
      sy-tabix           TO ls_excpbind-value.
    INSERT ls_excpbind INTO TABLE ct_excpbind.
  ENDLOOP.
ENDMETHOD.

Since the auxiliary method BUILD_PARMBIND is to be used for importing, exporting and changing parameters, the parameter IT_PARM has the type ANY TABLE and access is dynamic with field symbols:

KindParameterType
IMPORTINGIT_PARMANY TABLE
IMPORTINGIV_KINDABAP_FUNC_PARMBIND-KIND
CHANGINGCT_PARMBINDABAP_FUNC_PARMBIND_TAB
METHOD build_parmbind.
  DATA:
    ls_parmbind LIKE LINE OF ct_parmbind.
  FIELD-SYMBOLS:
    <parameter> TYPE ANY,
    <name>      TYPE ls_parmbind-name.
  LOOP AT it_parm ASSIGNING <parameter>.
    ASSIGN COMPONENT 'PARAMETER' OF STRUCTURE <parameter> TO <name>.
    ASSERT sy-subrc EQ 0.
    MOVE:
      <name>  TO ls_parmbind-name,
      iv_kind TO ls_parmbind-kind.
    INSERT ls_parmbind INTO TABLE ct_parmbind.
  ENDLOOP.
ENDMETHOD.

This means that we have already filled the two internal binding tables. Only the references to the concrete data objects (attribute ABAP_FUNC_PARMBIND-VALUE) are missing, which can only be added in the event modules.

The event modules

As mentioned above, we need a separate event function module for each relevant event. These are created by copying the corresponding SAP sample modules into one (or, depending on the number of modules, several) customer-specific function groups. I use the function group ZF_CD_DOMAIN_FILTER, and as naming convention for the event modules I use Z_CD_FILTER_EVENT_{event}, for example Z_CD_FILTER_EVENT_1102. The constant GC_FUNC_PREFIX in the class ZCL_CD_EVENT_INVOKER is set to 'Z_CD_FILTER_EVENT_'.

In each module, you determine the department from the available information using class ZCL_CD_DOMAIN_DERIVE and bind the parameters and exceptions using class ZCL_CD_EVENT_INVOKER and determine and call the department specific module.

The objects of both classes are created and stored in the TOP include of the function group:

DATA:
  gr_derive TYPE REF TO zif_cd_domain_derive,
  gr_invoke TYPE REF TO zif_cd_event_invoker.
LOAD-OF-PROGRAM.
  IF gr_derive IS INITIAL.
    CREATE OBJECT gr_derive TYPE zcl_cd_domain_derive.
  ENDIF.
  IF gr_invoke IS INITIAL.
    CREATE OBJECT gr_invoke TYPE zcl_cd_event_invoker.
  ENDIF.

The ABAP event LOAD-OF-PROGRAM is something like the constructor of a function group. The objects are only created if they do not yet exist, for example, by a unit test. This makes it possible to use mocks for unit tests instead of the two classes.

I have combined the parameter binding and the call of the department-specific module in a macro in the top include that can be used in each of the event modules:

DEFINE invoke_event.
  data:
    lt_parm  type abap_func_parmbind_tab,
    lr_parm  type ref to abap_func_parmbind,
    lt_excp  type abap_func_excpbind_tab,
    lv_tab   type string,
    lv_subrc type sysubrc.
  field-symbols:
    <fs> type any.
  call method gr_invoke->build_binding
    exporting
      iv_event    = &2
    importing
      et_parmbind = lt_parm
      et_excpbind = lt_excp.
  loop at lt_parm reference into lr_parm.
    if lr_parm->kind eq abap_func_tables.
      assign (lr_parm->name) to <fs>.
      get reference of <fs> into lr_parm->tables_wa.
      concatenate lr_parm->name '[]' into lv_tab.
      assign (lv_tab) to <fs>.
      get reference of <fs> into lr_parm->value.
    else.
      assign (lr_parm->name) to <fs>.
      get reference of <fs> into lr_parm->value.
    endif.
  endloop.
  lv_subrc = gr_invoke->invoke_event(
      iv_domain   = &1
      iv_event    = &2
      it_parmbind = lt_parm
      it_excpbind = lt_excp ).
  move lv_subrc to sy-subrc.
END-OF-DEFINITION.

In this macro, the binding tables are first filled with the method BUILD_BINDING. References to the data objects in the parameter table are then added - for internal tables also the header line (work area). These data objects are only visible in the body of the event module, hence the solution with the macro. Finally, the department-specific module is determined and called using the method INVOKE_EVENT.

Finally, the coding of the event 1102:

FUNCTION z_cd_filter_event_1102.
*"----------------------------------------------------------------------
*"*"Local Interface:
*"  IMPORTING
*"     REFERENCE(I_FKKOP) LIKE  FKKOP STRUCTURE  FKKOP
*"     REFERENCE(I_FKKKO) LIKE  FKKKO STRUCTURE  FKKKO OPTIONAL
*"     REFERENCE(I_CALLID) LIKE  CALLID STRUCTURE  CALLID OPTIONAL
*"  EXPORTING
*"     REFERENCE(E_FKKOP) LIKE  FKKOP STRUCTURE  FKKOP
*"  TABLES
*"      T_MESSA STRUCTURE  FIMSG OPTIONAL
*"----------------------------------------------------------------------
  DATA lv_bdomain TYPE zcd_bdomain_func-bdomain.
  lv_bdomain = gr_derive->get_bdomain_by_vktyp( i_fkkop-vktyp ).
  invoke_event lv_bdomain '1102'.
ENDFUNCTION.

Pretty neat, huh? As a small performance optimization I have defined all parameters as call-by-reference. This is not critical here, since the data is not changed. Depending on the setting, call-by-value is used again when it is transferred to the department specific function module.

Recap

This framework makes it possible to separate the requirements of the departments and thus avoid dependencies. In individual cases, it can be difficult to determine the department as planned at certain times, as the information provided is not sufficient. Additional accesses to additional tables, such as the payment lot or the parameters of a mass activity, may be required to find the information required for derivation. In some cases it is necessary to remember information between specific times, since specific times are processed in a sequence (for example, 0010, 0020 and 0030). Using the where-used list of the SAP sample modules, you can usually find the call points in FI-CA or FS-CD in order to analyze the SAP coding for further to find ways to derive the department.

An essential feature of the presented solution is the unit test capability. This is made possible by encapsulating the actual logic in classes that are only communicated with via interfaces. This allows the two classes involved and the event modules to be tested in isolation from each other, which is very important for a cross-cutting topic like this.

Translated with DeepL


Back to list < Dependency Injection with ABAP Objects