SAP Master Data Governance

Material Master Data – compare data in two systems

In Master Data Hub scenario the MDG system is the “source of truth”, master data is replicated to satellite systems and can be extended there, but in general the common part should not be changed locally because at next replication it will get back overwritten with the version from MDG.

In some cases, it may happen that either naughty user or external interface modify part of the data which should not be changed, and it is often needed to detect such situation ASAP.

On the market exist tools to compare the master data between systems, but usually they are not free, have complex configuration and it takes time to learn how they work.

In this blog I provide a solution how to compare Material Master Data between two systems with relatively short coding (in total about 1k lines of ABAP) and “some clicks of the mouse”. The code can be implemented in Sandbox system and is ready to compare data in any systems to which you set up an RFC connection.

How it works

A Delta Collection Program connects to two selected systems (RFC destinations must be set up in SM59), and reads Material Master Data tables from System1 and System2 (with standard RFC function module RFC_READ_TABLE – read carefully note 382318). After field-by-field comparison the calculated delta is shown on output.

As the Material Master Data tables are usually huge and the delta collection takes time – the output of the program mentioned above can be saved in a Custom Delta Table.

For nice visualization of the comparison there is simple FPM application to be created (however you can also use MS Excel).

As result we get something like below:

Where Delta Collection Program looks like:

and the Custom Delta Table:

How to set it up

(I’ve tried to provide detailed instructions to make it clear for person with just basic ABAP knowledge)

First, the Custom Delta Table where collected delta records will be stored, so SE11 and create table as below (to make it simple I’ve used generic data elements, but of course you can set up your own with nice labels)

Then the class used to collect the data and calculate delta:

Simply create new class ZCL_TABLES_COMPARE, switch into “Source Code-Based” mode and paste the code below (+ save and activate). See the comments at call of ‘RFC_READ_TABLE’

CLASS zcl_tables_compare DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    TYPES:
      ty_fields_t TYPE STANDARD TABLE OF rfc_db_fld WITH DEFAULT KEY .
    TYPES:
      BEGIN OF ty_log,
        rkey   TYPE ztab_compare-rkey,
        tfname TYPE ztab_compare-tfname,
        v1     TYPE ztab_compare-v1,
        v2     TYPE ztab_compare-v2,
        fdescr TYPE string,
      END OF ty_log .
    TYPES:
      ty_log_t TYPE STANDARD TABLE OF ty_log WITH KEY rkey tfname .

    CONSTANTS gc_key_delimeter TYPE char1 VALUE '/' ##NO_TEXT.

    CLASS-METHODS run_comparison
      IMPORTING
        !iv_rfc_dest1    TYPE rfcdest
        !iv_rfc_dest2    TYPE rfcdest
        !iv_tabname      TYPE tabname
        !iv_key_fields   TYPE string
        !iv_excl_fields  TYPE string OPTIONAL
        !iv_range_from   TYPE char100
        !iv_range_to     TYPE char100
        !iv_ec_s1        TYPE abap_bool DEFAULT abap_true
        !iv_ec_s2        TYPE abap_bool DEFAULT abap_true
      RETURNING
        VALUE(rt_result) TYPE ty_log_t .
    CLASS-METHODS get_table_key_fields
      IMPORTING
        !iv_tabname      TYPE tabname
      RETURNING
        VALUE(rv_result) TYPE string .
    CLASS-METHODS get_table_field_descr
      IMPORTING
        !iv_tfname       TYPE ztab_compare-tfname
      RETURNING
        VALUE(rv_result) TYPE string .
  PROTECTED SECTION.
  PRIVATE SECTION.

    CLASS-METHODS is_status_different
      IMPORTING
        !iv_s1           TYPE any
        !iv_s2           TYPE any
      RETURNING
        VALUE(rv_result) TYPE abap_bool .
    CLASS-METHODS compare_tables
      IMPORTING
        !iv_tabname TYPE tabname
        !ir_tab1    TYPE REF TO data
        !ir_tab2    TYPE REF TO data
        !iv_ec_s1   TYPE abap_bool
        !iv_ec_s2   TYPE abap_bool
      CHANGING
        !ct_log     TYPE ty_log_t .
    CLASS-METHODS read_table
      IMPORTING
        !iv_rfc_dest     TYPE rfcdest
        !iv_tabname      TYPE tabname
        !iv_key_fields   TYPE string
        !iv_excl_fields  TYPE string OPTIONAL
        !iv_range_from   TYPE char100 OPTIONAL
        !iv_range_to     TYPE char100 OPTIONAL
      RETURNING
        VALUE(rt_result) TYPE REF TO data .
ENDCLASS.



CLASS ZCL_TABLES_COMPARE IMPLEMENTATION.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Private Method ZCL_TABLES_COMPARE=>COMPARE_TABLES
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_TABNAME                     TYPE        TABNAME
* | [--->] IR_TAB1                        TYPE REF TO DATA
* | [--->] IR_TAB2                        TYPE REF TO DATA
* | [--->] IV_EC_S1                       TYPE        ABAP_BOOL
* | [--->] IV_EC_S2                       TYPE        ABAP_BOOL
* | [<-->] CT_LOG                         TYPE        TY_LOG_T
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD compare_tables.
    DEFINE copy_tab.
      ASSIGN &1->* TO <lt_data>.
      IF sy-subrc EQ 0.
        &2 = CORRESPONDING #( <lt_data> ).
        SORT &2.
      ENDIF.
    END-OF-DEFINITION.

    DEFINE check1.
      IF sy-subrc EQ 0.
        IF &1 NE &2.
          LOOP AT lo_str_descr->components ASSIGNING <ls_comp>.
            ASSIGN COMPONENT <ls_comp>-name OF STRUCTURE &1 TO <lv_value1>.
            ASSIGN COMPONENT <ls_comp>-name OF STRUCTURE &2 TO <lv_value2>.
            IF ';VPSTA;PSTAT;' CS |;{ <ls_comp>-name };|. " status field
              IF abap_true EQ is_status_different( iv_s1 = <lv_value1> iv_s2 = <lv_value2> ).
                APPEND VALUE #( rkey = lv_key tfname = |{ iv_tabname }-{ <ls_comp>-name }| v1 = <lv_value1> v2 = <lv_value2> ) TO ct_log.
              ENDIF.
            ELSE.
              IF <lv_value1> NE <lv_value2>.
                APPEND VALUE #( rkey = lv_key tfname = |{ iv_tabname }-{ <ls_comp>-name }| v1 = <lv_value1> v2 = <lv_value2> ) TO ct_log.
              ENDIF.
            ENDIF.
          ENDLOOP.
        ENDIF.
      ELSE.
        IF iv_ec_s1 EQ abap_true.
        APPEND VALUE #( rkey = lv_key tfname = iv_tabname v1 = 'record exists' v2 = 'does not exist' ) TO ct_log. " key does not exist in System 2
        ENDIF.
      ENDIF.
    END-OF-DEFINITION.

    DEFINE check2.
      IF sy-subrc NE 0.
        APPEND VALUE #( rkey = lv_key tfname = iv_tabname v1 = 'does not exist' v2 = 'record exists' ) TO ct_log. " key does not exist in System 1
      ENDIF.
    END-OF-DEFINITION.

    DATA: lt_mara1     TYPE STANDARD TABLE OF mara WITH KEY matnr,
          lt_mara2     LIKE lt_mara1,
          lt_marc1     TYPE STANDARD TABLE OF marc WITH KEY matnr werks,
          lt_marc2     LIKE lt_marc1,
          lt_makt1     TYPE STANDARD TABLE OF makt WITH KEY matnr spras,
          lt_makt2     LIKE lt_makt1,
          lt_mard1     TYPE STANDARD TABLE OF mard WITH KEY matnr werks lgort,
          lt_mard2     LIKE lt_mard1,
          lt_marm1     TYPE STANDARD TABLE OF marm WITH KEY matnr meinh,
          lt_marm2     LIKE lt_marm1,
          lt_mbew1     TYPE STANDARD TABLE OF mbew WITH KEY matnr bwkey bwtar,
          lt_mbew2     LIKE lt_mbew1,
          lt_mean1     TYPE STANDARD TABLE OF mean WITH KEY matnr meinh ean11, "this one is special case - LFNUM may differ between sytstems
          lt_mean2     LIKE lt_mean1,
          lt_mlgn1     TYPE STANDARD TABLE OF mlgn WITH KEY matnr lgnum,
          lt_mlgn2     LIKE lt_mlgn1,
          lt_mlgt1     TYPE STANDARD TABLE OF mlgt WITH KEY matnr lgnum lgtyp,
          lt_mlgt2     LIKE lt_mlgt1,
          lt_mvke1     TYPE STANDARD TABLE OF mvke WITH KEY matnr vkorg vtweg,
          lt_mvke2     LIKE lt_mvke1,
          lt_qmat1     TYPE STANDARD TABLE OF qmat WITH KEY art matnr werks,
          lt_qmat2     LIKE lt_qmat1,
          lo_str_descr TYPE REF TO cl_abap_structdescr,
          lv_key       TYPE string.

    FIELD-SYMBOLS: <lt_data>   TYPE ANY TABLE,
                   <ls_comp>   TYPE abap_compdescr,
                   <lv_value1> TYPE any,
                   <lv_value2> TYPE any.

    lo_str_descr = CAST #( cl_abap_structdescr=>describe_by_name( iv_tabname ) ).

    CASE iv_tabname.
      WHEN 'MARA'.
        copy_tab ir_tab1 lt_mara1.
        copy_tab ir_tab2 lt_mara2.

        LOOP AT lt_mara1 ASSIGNING FIELD-SYMBOL(<ls_mara1>).
          lv_key = |{ <ls_mara1>-matnr ALPHA = OUT }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_mara2 WITH KEY matnr = <ls_mara1>-matnr ASSIGNING FIELD-SYMBOL(<ls_mara2>) BINARY SEARCH.
          check1 <ls_mara1> <ls_mara2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_mara2 ASSIGNING <ls_mara2>.
            lv_key = |{ <ls_mara1>-matnr ALPHA = OUT }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_mara1 WITH KEY matnr = <ls_mara2>-matnr TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

      WHEN 'MARC'.
        copy_tab ir_tab1 lt_marc1.
        copy_tab ir_tab2 lt_marc2.

        LOOP AT lt_marc1 ASSIGNING FIELD-SYMBOL(<ls_marc1>).
          lv_key = |{ <ls_marc1>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_marc1>-werks }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_marc2 WITH KEY matnr = <ls_marc1>-matnr werks = <ls_marc1>-werks ASSIGNING FIELD-SYMBOL(<ls_marc2>) BINARY SEARCH.
          check1 <ls_marc1> <ls_marc2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_marc2 ASSIGNING <ls_marc2>.
            lv_key = |{ <ls_marc2>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_marc2>-werks }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_marc1 WITH KEY matnr = <ls_marc2>-matnr werks = <ls_marc2>-werks TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

      WHEN 'MAKT'.
        copy_tab ir_tab1 lt_makt1.
        copy_tab ir_tab2 lt_makt2.

        LOOP AT lt_makt1 ASSIGNING FIELD-SYMBOL(<ls_makt1>).
          lv_key = |{ <ls_makt1>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_makt1>-spras }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_makt2 WITH KEY matnr = <ls_makt1>-matnr spras = <ls_makt1>-spras ASSIGNING FIELD-SYMBOL(<ls_makt2>) BINARY SEARCH.
          check1 <ls_makt1> <ls_makt2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_makt2 ASSIGNING <ls_makt2>.
            lv_key = |{ <ls_makt2>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_makt2>-spras }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_makt1 WITH KEY matnr = <ls_makt2>-matnr spras = <ls_makt2>-spras TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

      WHEN 'MARD'.
        copy_tab ir_tab1 lt_mard1.
        copy_tab ir_tab2 lt_mard2.

        LOOP AT lt_mard1 ASSIGNING FIELD-SYMBOL(<ls_mard1>).
          lv_key = |{ <ls_mard1>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mard1>-werks }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mard1>-lgort }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_mard2 WITH KEY matnr = <ls_mard1>-matnr werks = <ls_mard1>-werks lgort = <ls_mard1>-lgort ASSIGNING FIELD-SYMBOL(<ls_mard2>) BINARY SEARCH.
          check1 <ls_mard1> <ls_mard2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_mard2 ASSIGNING <ls_mard2>. " check form the other side to detect missing records in System 1
            lv_key = |{ <ls_mard2>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mard2>-werks }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mard2>-lgort }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_mard1 WITH KEY matnr = <ls_mard2>-matnr werks = <ls_mard2>-werks lgort = <ls_mard2>-lgort TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

      WHEN 'MARM'.
        copy_tab ir_tab1 lt_marm1.
        copy_tab ir_tab2 lt_marm2.

        LOOP AT lt_marm1 ASSIGNING FIELD-SYMBOL(<ls_marm1>).
          lv_key = |{ <ls_marm1>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_marm1>-meinh }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_marm2 WITH KEY matnr = <ls_marm1>-matnr meinh = <ls_marm1>-meinh ASSIGNING FIELD-SYMBOL(<ls_marm2>) BINARY SEARCH.
          check1 <ls_marm1> <ls_marm2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_marm2 ASSIGNING <ls_marm2>.
            lv_key = |{ <ls_marm2>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_marm2>-meinh }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_marm1 WITH KEY matnr = <ls_marm2>-matnr meinh = <ls_marm2>-meinh TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

      WHEN 'MBEW'.
        copy_tab ir_tab1 lt_mbew1.
        copy_tab ir_tab2 lt_mbew2.

        LOOP AT lt_mbew1 ASSIGNING FIELD-SYMBOL(<ls_mbew1>).
          lv_key = |{ <ls_mbew1>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mbew1>-bwkey }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mbew1>-bwtar }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_mbew2 WITH KEY matnr = <ls_mbew1>-matnr bwkey = <ls_mbew1>-bwkey bwtar = <ls_mbew1>-bwtar ASSIGNING FIELD-SYMBOL(<ls_mbew2>) BINARY SEARCH.
          check1 <ls_mbew1> <ls_mbew2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_mbew2 ASSIGNING <ls_mbew2>. " check form the other side to detect missing records in System 1
            lv_key = |{ <ls_mbew2>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mbew2>-bwkey }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mbew2>-bwtar }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_mbew1 WITH KEY matnr = <ls_mbew2>-matnr bwkey = <ls_mbew2>-bwkey bwtar = <ls_mbew2>-bwtar TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

      WHEN 'MEAN'.
        copy_tab ir_tab1 lt_mean1.
        copy_tab ir_tab2 lt_mean2.

        LOOP AT lt_mean1 ASSIGNING FIELD-SYMBOL(<ls_mean1>).
          lv_key = |{ <ls_mean1>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mean1>-meinh }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mean1>-ean11 }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_mean2 WITH KEY matnr = <ls_mean1>-matnr meinh = <ls_mean1>-meinh ean11 = <ls_mean1>-ean11 ASSIGNING FIELD-SYMBOL(<ls_mean2>) BINARY SEARCH.
          CLEAR: <ls_mean1>-lfnum, <ls_mean2>-lfnum. " we don't want to compare this field, it may be different in both systems
          check1 <ls_mean1> <ls_mean2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_mean2 ASSIGNING <ls_mean2>. " check form the other side to detect missing records in System 1
            lv_key = |{ <ls_mean2>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mean2>-meinh }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mean2>-ean11 }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_mean1 WITH KEY matnr = <ls_mean2>-matnr meinh = <ls_mean2>-meinh ean11 = <ls_mean2>-ean11 TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

      WHEN 'MLGN'.
        copy_tab ir_tab1 lt_mlgn1.
        copy_tab ir_tab2 lt_mlgn2.

        LOOP AT lt_mlgn1 ASSIGNING FIELD-SYMBOL(<ls_mlgn1>).
          lv_key = |{ <ls_mlgn1>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mlgn1>-lgnum }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_mlgn2 WITH KEY matnr = <ls_mlgn1>-matnr lgnum = <ls_mlgn1>-lgnum ASSIGNING FIELD-SYMBOL(<ls_mlgn2>) BINARY SEARCH.
          check1 <ls_mlgn1> <ls_mlgn2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_mlgn2 ASSIGNING <ls_mlgn2>.
            lv_key = |{ <ls_mlgn2>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mlgn2>-lgnum }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_mlgn1 WITH KEY matnr = <ls_mlgn2>-matnr lgnum = <ls_mlgn2>-lgnum TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

      WHEN 'MLGT'.
        copy_tab ir_tab1 lt_mlgt1.
        copy_tab ir_tab2 lt_mlgt2.

        LOOP AT lt_mlgt1 ASSIGNING FIELD-SYMBOL(<ls_mlgt1>).
          lv_key = |{ <ls_mlgt1>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mlgt1>-lgnum }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mlgt1>-lgtyp }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_mlgt2 WITH KEY matnr = <ls_mlgt1>-matnr lgnum = <ls_mlgt1>-lgnum lgtyp = <ls_mlgt1>-lgtyp ASSIGNING FIELD-SYMBOL(<ls_mlgt2>) BINARY SEARCH.
          check1 <ls_mlgt1> <ls_mlgt2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_mlgt2 ASSIGNING <ls_mlgt2>. " check form the other side to detect missing records in System 1
            lv_key = |{ <ls_mlgt2>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mlgt2>-lgnum }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mlgt2>-lgtyp }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_mlgt1 WITH KEY matnr = <ls_mlgt2>-matnr lgnum = <ls_mlgt2>-lgnum lgtyp = <ls_mlgt2>-lgtyp TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

      WHEN 'MVKE'.
        copy_tab ir_tab1 lt_mvke1.
        copy_tab ir_tab2 lt_mvke2.

        LOOP AT lt_mvke1 ASSIGNING FIELD-SYMBOL(<ls_mvke1>).
          lv_key = |{ <ls_mvke1>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mvke1>-vkorg }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mvke1>-vtweg }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_mvke2 WITH KEY matnr = <ls_mvke1>-matnr vkorg = <ls_mvke1>-vkorg vtweg = <ls_mvke1>-vtweg ASSIGNING FIELD-SYMBOL(<ls_mvke2>) BINARY SEARCH.
          check1 <ls_mvke1> <ls_mvke2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_mvke2 ASSIGNING <ls_mvke2>. " check form the other side to detect missing records in System 1
            lv_key = |{ <ls_mvke2>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mvke2>-vkorg }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_mvke2>-vtweg }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_mvke1 WITH KEY matnr = <ls_mvke2>-matnr vkorg = <ls_mvke2>-vkorg vtweg = <ls_mvke2>-vtweg TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

      WHEN 'QMAT'.
        copy_tab ir_tab1 lt_qmat1.
        copy_tab ir_tab2 lt_qmat2.

        LOOP AT lt_qmat1 ASSIGNING FIELD-SYMBOL(<ls_qmat1>).
          lv_key = |{ <ls_qmat1>-art }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_qmat1>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_qmat1>-werks }|.
          CONDENSE lv_key NO-GAPS.
          READ TABLE lt_qmat2 WITH KEY art = <ls_qmat1>-art matnr = <ls_qmat1>-matnr werks = <ls_qmat1>-werks ASSIGNING FIELD-SYMBOL(<ls_qmat2>) BINARY SEARCH.
          check1 <ls_qmat1> <ls_qmat2>.
        ENDLOOP.

        IF iv_ec_s2 EQ abap_true.
          LOOP AT lt_qmat2 ASSIGNING <ls_qmat2>. " check form the other side to detect missing records in System 1
            lv_key = |{ <ls_qmat2>-art }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_qmat2>-matnr ALPHA = OUT }{ zcl_tables_compare=>gc_key_delimeter }{ <ls_qmat2>-werks }|.
            CONDENSE lv_key NO-GAPS.
            READ TABLE lt_qmat1 WITH KEY art = <ls_qmat2>-art matnr = <ls_qmat2>-matnr werks = <ls_qmat2>-werks TRANSPORTING NO FIELDS BINARY SEARCH.
            check2.
          ENDLOOP.
        ENDIF.

    ENDCASE.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_TABLES_COMPARE=>GET_TABLE_FIELD_DESCR
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_TFNAME                      TYPE        ZTAB_COMPARE-TFNAME
* | [<-()] RV_RESULT                      TYPE        STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD get_table_field_descr.
    TYPES: BEGIN OF lty_tf_name,
             tfname TYPE ztab_compare-tfname,
             fdescr TYPE string,
           END OF lty_tf_name.

    STATICS: st_fields TYPE HASHED TABLE OF lty_tf_name WITH UNIQUE KEY tfname,
             sv_tables TYPE string.

    READ TABLE st_fields ASSIGNING FIELD-SYMBOL(<ls_fld>) WITH TABLE KEY tfname = iv_tfname.
    IF sy-subrc EQ 0.
      rv_result = <ls_fld>-fdescr.
      RETURN.
    ENDIF.

    DATA(lv_tname) = substring_before( val = iv_tfname sub = '-' ).
    IF lv_tname IS INITIAL OR sv_tables CS lv_tname.
      RETURN.
    ENDIF.

    sv_tables = |{ sv_tables };{ lv_tname }|.

    DATA(lo_struc_descr) = CAST cl_abap_structdescr( cl_abap_structdescr=>describe_by_name( lv_tname ) ).
    LOOP AT lo_struc_descr->get_ddic_field_list( p_langu = sy-langu ) ASSIGNING FIELD-SYMBOL(<ls_comp>).
      INSERT VALUE #( tfname = |{ lv_tname }-{ <ls_comp>-fieldname }| fdescr = <ls_comp>-scrtext_l ) INTO TABLE st_fields.
    ENDLOOP.

    rv_result = VALUE #( st_fields[ tfname = iv_tfname ]-fdescr OPTIONAL ).
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_TABLES_COMPARE=>GET_TABLE_KEY_FIELDS
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_TABNAME                     TYPE        TABNAME
* | [<-()] RV_RESULT                      TYPE        STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD get_table_key_fields.
    SELECT fieldname INTO TABLE @DATA(lt_key_fields) FROM dd03l
      WHERE tabname EQ @iv_tabname AND keyflag EQ @abap_true AND fieldname NE 'MANDT'
      ORDER BY position.

    CONCATENATE LINES OF lt_key_fields INTO rv_result SEPARATED BY ';'.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Private Method ZCL_TABLES_COMPARE=>IS_STATUS_DIFFERENT
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_S1                          TYPE        ANY
* | [--->] IV_S2                          TYPE        ANY
* | [<-()] RV_RESULT                      TYPE        ABAP_BOOL
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD is_status_different.
    DATA: lv_pos TYPE i VALUE 0,
          lv_len TYPE i.

    rv_result = abap_false.
    lv_len = strlen( iv_s1 ).

    IF lv_len NE strlen( iv_s2 ).
      rv_result = abap_true. RETURN.
    ENDIF.

    DO.
      IF lv_pos GE lv_len.
        RETURN.
      ENDIF.
      IF iv_s2 NS iv_s1+lv_pos(1).
        rv_result = abap_true. RETURN.
      ENDIF.
      lv_pos = lv_pos + 1.
    ENDDO.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Private Method ZCL_TABLES_COMPARE=>READ_TABLE
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_RFC_DEST                    TYPE        RFCDEST
* | [--->] IV_TABNAME                     TYPE        TABNAME
* | [--->] IV_KEY_FIELDS                  TYPE        STRING
* | [--->] IV_EXCL_FIELDS                 TYPE        STRING(optional)
* | [--->] IV_RANGE_FROM                  TYPE        CHAR100(optional)
* | [--->] IV_RANGE_TO                    TYPE        CHAR100(optional)
* | [<-()] RT_RESULT                      TYPE REF TO DATA
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD read_table.
    DATA: lt_options    TYPE STANDARD TABLE OF rfc_db_opt WITH DEFAULT KEY,
          lt_all_fields TYPE ty_fields_t,
          lt_key_fields TYPE ty_fields_t,
          lt_sel_fields TYPE ty_fields_t,
          lt_pkg_fields TYPE ty_fields_t,
          lt_data       TYPE STANDARD TABLE OF tab512 WITH DEFAULT KEY,
          lt_comp       TYPE cl_abap_structdescr=>component_table,
          lr_tab        TYPE REF TO data,
          lr_wa         TYPE REF TO data,
          lt_tr_fields  TYPE ty_fields_t,
          lv_rowcount   TYPE soid-accnt VALUE 0.

    FIELD-SYMBOLS: <lt_out> TYPE INDEX TABLE.


    DATA: lt_packages TYPE stringtab,
          lv_offset   TYPE i.

    " first compare if structures of table are the same in both systems
    CALL FUNCTION 'RFC_READ_TABLE'
      DESTINATION iv_rfc_dest
      EXPORTING
        query_table = iv_tabname
        no_data     = abap_true
      TABLES
        fields      = lt_all_fields
      EXCEPTIONS
        OTHERS      = 7.

    " remove excluded fields and get key fields
    LOOP AT lt_all_fields ASSIGNING FIELD-SYMBOL(<ls_field>).
      IF iv_key_fields CS <ls_field>-fieldname.
        APPEND <ls_field> TO lt_key_fields.
        CONTINUE.
      ENDIF.
      IF iv_excl_fields CS <ls_field>-fieldname.
        CONTINUE.
      ENDIF.
      APPEND <ls_field> TO lt_sel_fields.
    ENDLOOP.

    ASSERT lt_key_fields IS NOT INITIAL.

    " ... and prepare selection range for WHERE clause
    DATA(lv_first_key) = VALUE #( lt_key_fields[ 1 ]-fieldname ).
    IF iv_range_from IS INITIAL AND iv_range_to IS INITIAL.
      lt_options = VALUE #( ( text = |1 EQ 1| ) ).  " select all but with some fuse
      lv_rowcount = 100000.
    ELSEIF iv_range_from IS NOT INITIAL AND iv_range_to IS NOT INITIAL.
      lt_options = VALUE #( ( text = |{ lv_first_key } BETWEEN '{ iv_range_from }' AND '{ iv_range_to }'| ) ).
    ELSEIF iv_range_from IS NOT INITIAL.
      lt_options = VALUE #( ( text = |{ lv_first_key } EQ '{ iv_range_from }'| ) ).
    ELSE. " upper limit provided without lower
      ASSERT 1 EQ 2.
    ENDIF.

*>>> prepare output table
    LOOP AT lt_all_fields ASSIGNING <ls_field>.
      APPEND VALUE #( name = <ls_field>-fieldname type = cl_abap_elemdescr=>get_c( p_length = CONV #( <ls_field>-length ) ) ) TO lt_comp.
    ENDLOOP.

    DATA(lo_tab_struc) = cl_abap_structdescr=>create( p_components = lt_comp ).
    DATA(lt_ddic) = CAST cl_abap_structdescr( cl_abap_structdescr=>describe_by_name( iv_tabname ) )->get_ddic_field_list( ).
    SORT lt_ddic BY fieldname.

    CREATE DATA lr_wa TYPE HANDLE lo_tab_struc.
    ASSIGN lr_wa->* TO FIELD-SYMBOL(<ls_wa_out>).

    DATA(lt_keys) = VALUE abap_table_keydescr_tab( ( name = 'MAIN'
                                                     is_primary = abap_true
                                                     is_unique = abap_true
                                                     access_kind = cl_abap_tabledescr=>tablekind_sorted
                                                     key_kind = cl_abap_tabledescr=>keydefkind_user "keydefkind_user
                                                     components = CORRESPONDING #( lt_key_fields MAPPING name = fieldname ) ) ).
    TRY.
        DATA(lo_tab_descr) = cl_abap_tabledescr=>create_with_keys( p_line_type = lo_tab_struc
                                                                   p_keys = lt_keys ).
      CATCH cx_sy_table_creation INTO DATA(lx_error).
        RETURN.
    ENDTRY.
    CREATE DATA lr_tab TYPE HANDLE lo_tab_descr.

    ASSIGN lr_tab->* TO <lt_out>.
*<<<

    " collect data in packages up to 512 characters
    DO.
      IF lt_sel_fields IS INITIAL.
        EXIT. " DO..ENDDO
      ENDIF.

      CLEAR: lv_offset, lt_pkg_fields, lt_tr_fields, lt_data.

      LOOP AT lt_key_fields ASSIGNING <ls_field>.
        lv_offset = lv_offset + <ls_field>-length.
        APPEND <ls_field> TO lt_pkg_fields.
      ENDLOOP.

      LOOP AT lt_sel_fields ASSIGNING <ls_field>.
        lv_offset = lv_offset + <ls_field>-length.
        IF lv_offset GE 512.
          EXIT. "loop
        ENDIF.

        APPEND <ls_field> TO: lt_pkg_fields, lt_tr_fields.
        DELETE lt_sel_fields USING KEY loop_key.
      ENDLOOP.

      CALL FUNCTION 'RFC_READ_TABLE'
        DESTINATION iv_rfc_dest
        EXPORTING
          query_table = iv_tabname
          rowcount    = lv_rowcount
        TABLES
          options     = lt_options
          fields      = lt_pkg_fields
          data        = lt_data
        EXCEPTIONS
          OTHERS      = 7.

      IF sy-subrc <> 0.
* Implement suitable error handling here
      ENDIF.

      LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<ls_data>).
        CLEAR <ls_wa_out>.
        LOOP AT lt_pkg_fields ASSIGNING <ls_field>.
          " the RFC FM is a bit crappy, when e.g. field is of type P length 3 dec 1 (example MDA MARC-LGRAD for 23430/0400)
          " it tries to convert the 99.0 into CHAR3 and in result there is set '*.0' there !?!?
          " in fact the FM is obsolete (see note 382318), see also something new in notes 2246160 and 3139000
          ASSIGN COMPONENT <ls_field>-fieldname OF STRUCTURE <ls_wa_out> TO FIELD-SYMBOL(<lv_fvalue>).
          IF sy-subrc EQ 0.
            READ TABLE lt_ddic ASSIGNING FIELD-SYMBOL(<ls_ddic>) WITH KEY fieldname = <ls_field>-fieldname.
            IF sy-subrc EQ 0 AND <ls_ddic>-inttype EQ 'P'.
              IF <ls_data>-wa+<ls_field>-offset(<ls_field>-length) CS '*'.
                <lv_fvalue> = '6.9'. " set here something not initial to at least detect comparison with 0
              ELSE.
                <lv_fvalue> = <ls_data>-wa+<ls_field>-offset(<ls_field>-length).
              ENDIF.
            ELSE.
              <lv_fvalue> = <ls_data>-wa+<ls_field>-offset(<ls_field>-length).
            ENDIF.
          ENDIF.
        ENDLOOP.
        READ TABLE <lt_out> FROM <ls_wa_out> ASSIGNING FIELD-SYMBOL(<ls_found>).
        IF sy-subrc EQ 0.
          LOOP AT lt_tr_fields ASSIGNING <ls_field>.
            ASSIGN COMPONENT <ls_field>-fieldname OF STRUCTURE <ls_wa_out> TO FIELD-SYMBOL(<lv_src>).
            IF sy-subrc EQ 0.
              ASSIGN COMPONENT <ls_field>-fieldname OF STRUCTURE <ls_found> TO FIELD-SYMBOL(<lv_dst>).
              IF sy-subrc EQ 0.
                <lv_dst> = <lv_src>.
              ENDIF.
            ENDIF.
          ENDLOOP.
        ELSE.
          INSERT <ls_wa_out> INTO TABLE <lt_out>.
        ENDIF.
      ENDLOOP.

    ENDDO.

    rt_result = lr_tab.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_TABLES_COMPARE=>RUN_COMPARISON
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_RFC_DEST1                   TYPE        RFCDEST
* | [--->] IV_RFC_DEST2                   TYPE        RFCDEST
* | [--->] IV_TABNAME                     TYPE        TABNAME
* | [--->] IV_KEY_FIELDS                  TYPE        STRING
* | [--->] IV_EXCL_FIELDS                 TYPE        STRING(optional)
* | [--->] IV_RANGE_FROM                  TYPE        CHAR100
* | [--->] IV_RANGE_TO                    TYPE        CHAR100
* | [--->] IV_EC_S1                       TYPE        ABAP_BOOL (default =ABAP_TRUE)
* | [--->] IV_EC_S2                       TYPE        ABAP_BOOL (default =ABAP_TRUE)
* | [<-()] RT_RESULT                      TYPE        TY_LOG_T
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD run_comparison.
    " get data from RFC1
    DATA(lr_data1) = read_table( EXPORTING iv_rfc_dest = iv_rfc_dest1
                                           iv_tabname = iv_tabname
                                           iv_key_fields = iv_key_fields
                                           iv_excl_fields = iv_excl_fields
                                           iv_range_from = iv_range_from
                                           iv_range_to = iv_range_to ).
    " get data from RFC2
    DATA(lr_data2) = read_table( EXPORTING iv_rfc_dest = iv_rfc_dest2
                                           iv_tabname = iv_tabname
                                           iv_key_fields = iv_key_fields
                                           iv_excl_fields = iv_excl_fields
                                           iv_range_from = iv_range_from
                                           iv_range_to = iv_range_to ).

    compare_tables( EXPORTING iv_tabname = iv_tabname
                              ir_tab1 = lr_data1
                              ir_tab2 = lr_data2
                              iv_ec_s1 = iv_ec_s1
                              iv_ec_s2 = iv_ec_s2
                    CHANGING  ct_log = rt_result ).
  ENDMETHOD.
ENDCLASS.

Now the program which collects the delta:

Create new executable program ZPST_TEST_COMPARE with the code below (program name is nowhere used, so set it as you wish):

*&---------------------------------------------------------------------*
*& Report ZPST_TEST_COMPARE
*&---------------------------------------------------------------------*
*&
*&---------------------------------------------------------------------*
REPORT zpst_test_compare.

DATA: gr_salv TYPE REF TO cl_salv_table,
      gt_log  TYPE zcl_tables_compare=>ty_log_t.

PARAMETERS: p_rfc1 TYPE rfcdest DEFAULT 'MDGCLNT100',
            p_rfc2 TYPE rfcdest DEFAULT 'ECCCLNT100'.

SELECT-OPTIONS: s_matnr FOR ('MATNR') NO-EXTENSION OBLIGATORY DEFAULT '000000000000042373' TO '000000000000044335',
                s_fexcl FOR ('USMD_FIELDNAME') NO INTERVALS.

PARAMETERS: p_mara TYPE c AS CHECKBOX DEFAULT 'X',
            p_marc TYPE c AS CHECKBOX,
            p_makt TYPE c AS CHECKBOX,
            p_mard TYPE c AS CHECKBOX,
            p_marm TYPE c AS CHECKBOX,
            p_mbew TYPE c AS CHECKBOX,
            p_mean TYPE c AS CHECKBOX,
            p_mlgn TYPE c AS CHECKBOX,
            p_mlgt TYPE c AS CHECKBOX,
*            p_mpgd TYPE c AS CHECKBOX,
*            p_mpop TYPE c AS CHECKBOX,
            p_mvke TYPE c AS CHECKBOX,
            p_qmat TYPE c AS CHECKBOX.
*            p_mdma  TYPE c AS CHECKBOX,

SELECTION-SCREEN SKIP.

PARAMETERS: p_s1_ec TYPE abap_bool AS CHECKBOX DEFAULT abap_true,
            p_s2_ec TYPE abap_bool AS CHECKBOX DEFAULT abap_false.

INCLUDE zpst_test_compare_forms.
*-----------------------------------------------------------------------------------
START-OF-SELECTION.

  IF p_mara EQ abap_true.
    PERFORM compare_table USING 'MARA' CHANGING gt_log.
  ENDIF.

  IF p_marc EQ abap_true.
    PERFORM compare_table USING 'MARC' CHANGING gt_log.
  ENDIF.

  IF p_makt EQ abap_true.
    PERFORM compare_table USING 'MAKT' CHANGING gt_log.
  ENDIF.

  IF p_mard EQ abap_true.
    PERFORM compare_table USING 'MARD' CHANGING gt_log.
  ENDIF.

  IF p_marm EQ abap_true.
    PERFORM compare_table USING 'MARM' CHANGING gt_log.
  ENDIF.

  IF p_mbew EQ abap_true.
    PERFORM compare_table USING 'MBEW' CHANGING gt_log.
  ENDIF.

  IF p_mean EQ abap_true.
    PERFORM compare_table USING 'MEAN' CHANGING gt_log.
  ENDIF.

  IF p_mlgn EQ abap_true.
    PERFORM compare_table USING 'MLGN' CHANGING gt_log.
  ENDIF.

  IF p_mlgt EQ abap_true.
    PERFORM compare_table USING 'MLGT' CHANGING gt_log.
  ENDIF.

  IF p_mvke EQ abap_true.
    PERFORM compare_table USING 'MVKE' CHANGING gt_log.
  ENDIF.

  IF p_qmat EQ abap_true.
    PERFORM compare_table USING 'QMAT' CHANGING gt_log.
  ENDIF.

  SORT gt_log BY rkey tfname.
*-----------------------------------------------------------------------------------
END-OF-SELECTION.
  DATA(gv_repid) = sy-repid.

  TRY.
      cl_salv_table=>factory( IMPORTING r_salv_table = gr_salv
                              CHANGING  t_table      = gt_log ).
    CATCH cx_salv_msg.                                  "#EC NO_HANDLER
      RETURN.
  ENDTRY.

  gr_salv->set_screen_status( pfstatus      = 'SALV_STANDARD'
                              report        = gv_repid
                              set_functions = cl_salv_table=>c_functions_all ).

  PERFORM salv_set_columns USING gr_salv.

  gr_salv->get_layout( )->set_key( VALUE #( report = gv_repid ) ).
  gr_salv->get_layout( )->set_default( abap_true ).
  gr_salv->get_layout( )->set_save_restriction( if_salv_c_layout=>restrict_none ).

  gr_salv->get_functions( )->set_print_preview( abap_false ).

  DATA(gr_events) = NEW lcl_handle_events( ).               "#EC NEEDED
  SET HANDLER gr_events->on_user_command FOR gr_salv->get_event( ).

  gr_salv->display( ).

Double click:

And create the include with code below:

*&---------------------------------------------------------------------*
*&  Include           ZPST_TEST_COMPARE_FORMS
*&---------------------------------------------------------------------*
CLASS lcl_handle_events DEFINITION.
  PUBLIC SECTION.
    METHODS:
      on_user_command FOR EVENT added_function OF cl_salv_events IMPORTING e_salv_function.
ENDCLASS.

CLASS lcl_handle_events IMPLEMENTATION.
  METHOD on_user_command.
    PERFORM show_function_info USING e_salv_function.
  ENDMETHOD.                    "on_user_command
ENDCLASS.

*-----------------------------------------------------------------------------------
FORM salv_set_columns USING ir_alv TYPE REF TO cl_salv_table .
  DATA: lo_col TYPE REF TO cl_salv_column_table.

  DATA(lo_columns) = ir_alv->get_columns( ).
  lo_columns->set_optimize( ).

  TRY.
      " set Code Text columns names
      DATA(lt_columns) = lo_columns->get( ).
      LOOP AT lt_columns ASSIGNING FIELD-SYMBOL(<ls_column>).
        lo_col = CAST #( <ls_column>-r_column ).
        CASE <ls_column>-columnname.
          WHEN 'RKEY'. lo_col->set_long_text( CONV #( 'Key' ) ).
          WHEN 'TFNAME'. lo_col->set_long_text( CONV #( 'Table-Field Name' ) ).
          WHEN 'V1'. lo_col->set_long_text( CONV #( |Value in { p_rfc1 }| ) ).
          WHEN 'V2'. lo_col->set_long_text( CONV #( |Value in { p_rfc2 }| ) ).
          WHEN 'FDESCR'. lo_col->set_long_text( CONV #( 'Field Description' ) ).
        ENDCASE.
      ENDLOOP.
    CATCH cx_salv_not_found.
      RETURN.
  ENDTRY.
ENDFORM.

FORM show_function_info USING i_function TYPE salv_de_function.
  DATA: ls_ztab_compare TYPE ztab_compare,
        lt_ztab_compare TYPE STANDARD TABLE OF ztab_compare WITH KEY tfname rkey.

  CASE i_function.
    WHEN 'SAVEDB'.
      IF gt_log IS NOT INITIAL.
        LOOP AT gt_log ASSIGNING FIELD-SYMBOL(<ls_log>).
          ls_ztab_compare = CORRESPONDING #( <ls_log> ).
          ls_ztab_compare-v1 = condense( val = ls_ztab_compare-v1 from = '' ).
          ls_ztab_compare-v2 = condense( val = ls_ztab_compare-v2 from = '' ).
          IF ls_ztab_compare-v1 EQ 'does not exist'. CLEAR ls_ztab_compare-v1. ENDIF.
          IF ls_ztab_compare-v2 EQ 'does not exist'. CLEAR ls_ztab_compare-v2. ENDIF.
          IF ls_ztab_compare-v1 EQ 'record exists'. ls_ztab_compare-v1 = abap_true. ENDIF.
          IF ls_ztab_compare-v2 EQ 'record exists'. ls_ztab_compare-v2 = abap_true. ENDIF.
          APPEND ls_ztab_compare TO lt_ztab_compare.
        ENDLOOP.
        MODIFY ztab_compare FROM TABLE lt_ztab_compare.
        COMMIT WORK.
        MESSAGE s208(00) WITH 'Data saved in ZTAB_COMPARE'.
      ENDIF.

    WHEN 'CLEARDB'.
      DELETE FROM ztab_compare.
      COMMIT WORK.
      MESSAGE s208(00) WITH 'Content of ZTAB_COMPARE deleted'.
  ENDCASE.
ENDFORM.

*-----------------------------------------------------------------------------------
FORM get_excluded USING iv_tabname TYPE tabname
                  CHANGING cv_excluded TYPE string.
  CLEAR cv_excluded.
  LOOP AT s_fexcl INTO DATA(ls_so) WHERE sign EQ 'I' AND option EQ 'EQ'.
    IF ls_so-low CP |{ iv_tabname }-*|.
      IF cv_excluded IS INITIAL.
        cv_excluded = substring_after( val = ls_so-low sub = |{ iv_tabname }-| ).
      ELSE.
        cv_excluded = cv_excluded && ';' && substring_after( val = ls_so-low sub = |{ iv_tabname }-| ).
      ENDIF.
    ENDIF.
  ENDLOOP.
ENDFORM.

FORM compare_table USING iv_tabname TYPE tabname
                   CHANGING ct_log TYPE zcl_tables_compare=>ty_log_t.
  DATA: lv_excluded TYPE string,
        lt_log      TYPE zcl_tables_compare=>ty_log_t.

  PERFORM get_excluded USING iv_tabname CHANGING lv_excluded.
  lt_log = zcl_tables_compare=>run_comparison( iv_rfc_dest1 = p_rfc1
                                               iv_rfc_dest2 = p_rfc2
                                               iv_tabname = iv_tabname
                                               iv_key_fields = zcl_tables_compare=>get_table_key_fields( iv_tabname )
                                               iv_excl_fields = lv_excluded
                                               iv_range_from = VALUE #( s_matnr[ 1 ]-low OPTIONAL )
                                               iv_range_to = VALUE #( s_matnr[ 1 ]-high OPTIONAL )
                                               iv_ec_s1 = p_s1_ec
                                               iv_ec_s2 = p_s2_ec ).

  "update_field_descriptions
  LOOP AT lt_log ASSIGNING FIELD-SYMBOL(<ls_log>).
    <ls_log>-fdescr =  zcl_tables_compare=>get_table_field_descr( <ls_log>-tfname ).
  ENDLOOP.

  APPEND LINES OF lt_log TO ct_log.
ENDFORM.

Create GUI status SALV_STANDARD (Normal Screen):

Copy from template like below:

Use template status: SALV_STANDARD of program SALV_DEMO_TABLE_FUNCTIONS

Delete the MYFUNCTION and adjust the three function codes:

(Save and activate)

Then program’s selection texts:

After activation the program should work, now a few words abut the selection parameters

RFC System 1 and 2 – these are RFC connections configured in SM59 pointing to two systems from which you want to compare the material master data. If the connections are set up without user/password (recommended) then each time at program execution there will be logon window shown (standard behavior with RFC call). Usually select the Data Hub as System 1 and as System 2 the data destination system.

In Material Number Range provide the range of materials you want to compare. It is recommended to first test on some small range (e.g., 100 materials) how fast they are processed, the speed will depend on size of the sub-tables (MARC, MVKE, …). Normally you run the comparison in packages of a few thousands records and save each comparison result in Custom Delta Table with “Save DB” button (see below).

Excluded field names – here you list fields which you don’t want to compare these will be “created on”, “created by” and all the fields which are “editable” in satellite system (like forecasting, costing, current period/year, stocks, etc.). Usually, you can identify such fields by running the delta collection without exclusion of any fields (on small amount of records) and checking which fields have many delta records. The form of the field to be set here is <table name>-<field name>, e.g. MARA-AENAM, MARC-LFGJA, MAKT-MAKTG

Table Names Checkboxes – here you select which tables you want to compare (the program can be easily extended with other material tables like MPOP, MDMA or custom extensions)

The final two checkboxes are used to collect records which exist in one system but not in the other. By default, the first is ON (we want to find if all records from source system we replicated to destination) and the second is OFF (in destination system the material can be enhanced with additional plants, sales orgs, etc. if there is many of such enhancements the delta table may grow up rapidly if have the flag ON)

After running the program, you should get in result something like below

With the two pointed above buttons you can: save the result in table ZTAB_COMPARE (appending the collected records to existing ones) and clean up the whole table ZTAB_COMPARE.

Results Display Program

Already at this point you can export the collected records from Custom Delta Table to xlsx and format them as needed. However, for those who like FPM but don’t know how to use it I’m describing below the steps how to create simple report.

First create new class ZCL_TABLES_COMPARE_FEEDER from the code below:

class ZCL_TABLES_COMPARE_FEEDER definition
  public
  final
  create public .

public section.

  interfaces IF_FPM_GUIBB .
  interfaces IF_FPM_GUIBB_LIST .
  PROTECTED SECTION.
  PRIVATE SECTION.

    TYPES:
      BEGIN OF ty_difference,
        tabname TYPE tabname16,
        tfname  TYPE ztab_compare-tfname,
        fdescr  TYPE text40,
        rkey    TYPE ztab_compare-rkey,
        v1      TYPE ztab_compare-v1,
        v2      TYPE ztab_compare-v2,
      END OF ty_difference .
    TYPES:
      ty_difference_tab TYPE STANDARD TABLE OF ty_difference WITH DEFAULT KEY .

    CLASS-DATA gt_data TYPE ty_difference_tab .
ENDCLASS.



CLASS ZCL_TABLES_COMPARE_FEEDER IMPLEMENTATION.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_TABLES_COMPARE_FEEDER->IF_FPM_GUIBB_LIST~CHECK_CONFIG
* +-------------------------------------------------------------------------------------------------+
* | [--->] IO_LAYOUT_CONFIG               TYPE REF TO IF_FPM_GUIBB_LIST_CONFIG
* | [<---] ET_MESSAGES                    TYPE        FPMGB_T_MESSAGES
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD IF_FPM_GUIBB_LIST~CHECK_CONFIG.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_TABLES_COMPARE_FEEDER->IF_FPM_GUIBB_LIST~FLUSH
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_CHANGE_LOG                  TYPE        FPMGB_T_CHANGELOG
* | [--->] IT_DATA                        TYPE REF TO DATA
* | [--->] IV_OLD_LEAD_SEL                TYPE        I(optional)
* | [--->] IV_NEW_LEAD_SEL                TYPE        I(optional)
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD IF_FPM_GUIBB_LIST~FLUSH.
    FIELD-SYMBOLS: <lt_data> LIKE gt_data.
    ASSIGN it_data->* TO <lt_data>.
    gt_data = <lt_data>.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_TABLES_COMPARE_FEEDER->IF_FPM_GUIBB_LIST~GET_DATA
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_EVENTID                     TYPE REF TO CL_FPM_EVENT
* | [--->] IT_SELECTED_FIELDS             TYPE        FPMGB_T_SELECTED_FIELDS(optional)
* | [--->] IV_RAISED_BY_OWN_UI            TYPE        BOOLE_D(optional)
* | [--->] IV_VISIBLE_ROWS                TYPE        I(optional)
* | [--->] IV_EDIT_MODE                   TYPE        FPM_EDIT_MODE(optional)
* | [--->] IO_EXTENDED_CTRL               TYPE REF TO IF_FPM_LIST_ATS_EXT_CTRL(optional)
* | [<---] ET_MESSAGES                    TYPE        FPMGB_T_MESSAGES
* | [<---] EV_DATA_CHANGED                TYPE        BOOLE_D
* | [<---] EV_FIELD_USAGE_CHANGED         TYPE        BOOLE_D
* | [<---] EV_ACTION_USAGE_CHANGED        TYPE        BOOLE_D
* | [<---] EV_SELECTED_LINES_CHANGED      TYPE        BOOLE_D
* | [<---] EV_DND_ATTR_CHANGED            TYPE        BOOLE_D
* | [<---] EO_ITAB_CHANGE_LOG             TYPE REF TO IF_SALV_ITAB_CHANGE_LOG
* | [<-->] CT_DATA                        TYPE        DATA
* | [<-->] CT_FIELD_USAGE                 TYPE        FPMGB_T_FIELDUSAGE
* | [<-->] CT_ACTION_USAGE                TYPE        FPMGB_T_ACTIONUSAGE
* | [<-->] CT_SELECTED_LINES              TYPE        RSTABIXTAB
* | [<-->] CV_LEAD_INDEX                  TYPE        SYTABIX
* | [<-->] CV_FIRST_VISIBLE_ROW           TYPE        I
* | [<-->] CS_ADDITIONAL_INFO             TYPE        FPMGB_S_ADDITIONAL_INFO(optional)
* | [<-->] CT_DND_ATTRIBUTES              TYPE        FPMGB_T_DND_DEFINITION(optional)
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD IF_FPM_GUIBB_LIST~GET_DATA.
    DATA: lv_tname TYPE string,
          lv_fname TYPE string.

    IF iv_eventid->mv_event_id EQ 'FPM_START'.
      SELECT tfname, rkey, v1, v2 INTO CORRESPONDING FIELDS OF TABLE @gt_data FROM ztab_compare ORDER BY PRIMARY KEY.
      LOOP AT gt_data ASSIGNING FIELD-SYMBOL(<ls_data>).
        SPLIT <ls_data>-tfname AT '-' INTO <ls_data>-tabname lv_fname.

        IF 'MAKT;MARA;MARC;MARD;MARM;MBEW;MEAN;MLGN;MLGT;MVKE;QMAT' CS <ls_data>-tfname.
          IF <ls_data>-v1 EQ abap_true.
            <ls_data>-fdescr = | Record exists only in System 1|.
          ELSE.
            <ls_data>-fdescr = | Record exists only in System 2|.
          ENDIF.
        ELSE.
          <ls_data>-fdescr = |{ lv_fname } : { zcl_tables_compare=>get_table_field_descr( <ls_data>-tfname ) }|.
        ENDIF.
      ENDLOOP.
      ct_data = gt_data.
      ev_data_changed =  abap_true.
    ENDIF.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_TABLES_COMPARE_FEEDER->IF_FPM_GUIBB_LIST~GET_DEFAULT_CONFIG
* +-------------------------------------------------------------------------------------------------+
* | [--->] IO_LAYOUT_CONFIG               TYPE REF TO IF_FPM_GUIBB_LIST_CONFIG
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD if_fpm_guibb_list~get_default_config.
    DEFINE add_column.
      io_layout_config->add_column( iv_name               = &2
                                    iv_display_type       = 'TV'
                                    iv_index              = &1
                                    iv_header	          = &3 ).
    END-OF-DEFINITION.

    io_layout_config->set_settings( iv_height_mode_ats = if_fpm_list_types=>cs_height_mode_ats-all_rows
                                    iv_export_to_excel = abap_true
                                    iv_export_format = if_fpm_list_types=>cs_export_format-office_open_xml
                                    iv_fit_to_table_width = abap_true
                                    iv_selection_mode_ats = if_fpm_list_types=>cs_selection_mode-single_no_lead
                                    iv_scroll_mode = if_fpm_list_types=>cs_scroll_mode-scrolling
                                    iv_allow_sorting = if_fpm_list_types=>cs_settings_allow_sorting-only_ad_hoc
                                    iv_allow_grouping = if_fpm_list_types=>cs_settings_allow_grouping-no_grouping
                                    iv_sort_by_relevance = abap_true
                                    it_default_sorting_ats = VALUE #( ( column_name = 'TABNAME' is_grouped = abap_true )
                                                                      ( column_name = 'FDESCR' is_grouped = abap_true ) )
                                  ).

    TRY.
        add_column 1 'TABNAME' 'Table'.
        add_column 2 'FDESCR' 'Field'.
        add_column 3 'RKEY' 'Key'.
        add_column 4 'V1' 'Value in System 1'.
        add_column 5 'V2' 'Value in System 2'.
      CATCH cx_fpm_configuration.                       "#EC NO_HANDLER
    ENDTRY.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_TABLES_COMPARE_FEEDER->IF_FPM_GUIBB_LIST~GET_DEFINITION
* +-------------------------------------------------------------------------------------------------+
* | [<---] EO_FIELD_CATALOG               TYPE REF TO CL_ABAP_TABLEDESCR
* | [<---] ET_FIELD_DESCRIPTION           TYPE        FPMGB_T_LISTFIELD_DESCR
* | [<---] ET_ACTION_DEFINITION           TYPE        FPMGB_T_ACTIONDEF
* | [<---] ET_SPECIAL_GROUPS              TYPE        FPMGB_T_SPECIAL_GROUPS
* | [<---] ES_MESSAGE                     TYPE        FPMGB_S_T100_MESSAGE
* | [<---] EV_ADDITIONAL_ERROR_INFO       TYPE        DOKU_OBJ
* | [<---] ET_DND_DEFINITION              TYPE        FPMGB_T_DND_DEFINITION
* | [<---] ET_ROW_ACTIONS                 TYPE        FPMGB_T_ROW_ACTION
* | [<---] ES_OPTIONS                     TYPE        FPMGB_S_LIST_OPTIONS
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD IF_FPM_GUIBB_LIST~GET_DEFINITION.
    eo_field_catalog ?= cl_abap_tabledescr=>describe_by_data( gt_data ).

    et_field_description = VALUE #( ( name = 'TFNAME' technical_field = abap_true )
                                    ( name = 'TABNAME' allow_sort = abap_true group_same_cells = abap_true )
                                    ( name = 'FDESCR' allow_sort = abap_true group_same_cells = abap_true )
                                    ( name = 'RKEY' )
                                    ( name = 'V1' )
                                    ( name = 'V2' ) ).
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_TABLES_COMPARE_FEEDER->IF_FPM_GUIBB_LIST~PROCESS_EVENT
* +-------------------------------------------------------------------------------------------------+
* | [--->] IO_EVENT                       TYPE REF TO CL_FPM_EVENT
* | [--->] IV_RAISED_BY_OWN_UI            TYPE        BOOLE_D(optional)
* | [--->] IV_LEAD_INDEX                  TYPE        SYTABIX
* | [--->] IV_EVENT_INDEX                 TYPE        SYTABIX
* | [--->] IT_SELECTED_LINES              TYPE        RSTABIXTAB
* | [--->] IO_UI_INFO                     TYPE REF TO IF_FPM_LIST_ATS_UI_INFO(optional)
* | [<---] EV_RESULT                      TYPE        FPM_EVENT_RESULT
* | [<---] ET_MESSAGES                    TYPE        FPMGB_T_MESSAGES
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD IF_FPM_GUIBB_LIST~PROCESS_EVENT.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_TABLES_COMPARE_FEEDER->IF_FPM_GUIBB~GET_PARAMETER_LIST
* +-------------------------------------------------------------------------------------------------+
* | [<-()] RT_PARAMETER_DESCR             TYPE        FPMGB_T_PARAM_DESCR
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD IF_FPM_GUIBB~GET_PARAMETER_LIST.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_TABLES_COMPARE_FEEDER->IF_FPM_GUIBB~INITIALIZE
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_PARAMETER                   TYPE        FPMGB_T_PARAM_VALUE
* | [--->] IO_APP_PARAMETER               TYPE REF TO IF_FPM_PARAMETER(optional)
* | [--->] IV_COMPONENT_NAME              TYPE        FPM_COMPONENT_NAME(optional)
* | [--->] IS_CONFIG_KEY                  TYPE        WDY_CONFIG_KEY(optional)
* | [--->] IV_INSTANCE_ID                 TYPE        FPM_INSTANCE_ID(optional)
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD IF_FPM_GUIBB~INITIALIZE.
  ENDMETHOD.
ENDCLASS.

Then, FPM application (some FPM screens will look different on newer SAP versions):

Set up OVP layout:

add there list UIBB with the feeder class created previously:

Provide „Config ID“ and „Title“, Save and click „Configure UIBB“ (ignore errors):

Because of some coding in the feeder class you don’t need to set anything more manually here except one crucial configuration:

Set “Collapse Groups by Default” – otherwise the application will want to show all the records at once which might be too big challenge in case of big deltas.

(In S/4 the screen below looks different, but still the option can be found there)

After Save the application should work, you can start it from SE80 or with configured link in SAP GUI:

Tips on usage of the tool

  • In remote systems there is only one function module called: RFC_READ_TABLE, it just reads data, however might be blocked in your systems.
  • To collect the data run the Delta Collection Program several times with smaller ranges of materials (and save each result in Custom Delta Table).
  • Initially do some trial runs on <1000 materials to identify fields which you don’t want to compare, and save the excluded fields in selection variant for later use.

Each line of the code provided here was written by me and you can freely use/modify it.