Post

Identifying Unused Custom Code During S/4 Migration

Identifying Unused Custom Code During S/4 Migration

When migrating to SAP S/4, one of the key tasks in the preparation phase is identifying unused custom code to optimize the transition. While SAP offers various reports, none of them met our specific requirements, so we decided to create our own custom report to detect unused code. In this blog post, I will walk you through the steps we took to create this report, from setting up data collection to checking the objects in the productive system and generating the final results.


🛠️ The Approach

To effectively identify unused custom code, we needed to gather usage data over a long period. SAP recommends running SCMON (the Custom Code Migration Workbench) for at least 13 months to collect meaningful data. This usage data forms the foundation of the report we created. We exported this data from SUSG (Usage Data Snapshot) and imported it into our development system, where we could combine it with a remote existence check to ensure the objects were also present in the production system.


📝 1. Get All Relevant Objects to Check

The first step was to gather all relevant objects that needed to be checked. This included programs, classes, and function groups. We executed a select statement to pull these objects and their associated metadata from the TADIR and PROGDIR tables, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
" Get the relevant programs/classes/function groups - Local system
SELECT DISTINCT object, tadir~obj_name, progdir~subc AS psubc, progdir~cdat AS pcdat, tcode
  FROM tadir
  LEFT OUTER JOIN progdir
    ON progdir~name EQ tadir~obj_name
  LEFT OUTER JOIN tstc
    ON tstc~pgmna EQ tadir~obj_name
  INTO TABLE @DATA(lt_objname)
  WHERE object IN @s_objtyp
    AND tadir~obj_name IN @s_objnam
    AND tadir~delflag EQ @abap_false
    AND tadir~genflag EQ @abap_false
    AND tadir~devclass IN @s_devc.

This select statement retrieves all objects that are still active and relevant for analysis, excluding deleted or generated objects.


🔍 2. Check in the Productive System

Next, we needed to check if these objects were also available in the production system. To do this, we used a remote function call (RFC) to verify the existence of each object in the target system:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
METHOD get_remote_objects.

  DATA: lf_where_clause TYPE string,
        lf_message      TYPE char1024.

  DATA: lt_options TYPE TABLE OF rfc_db_opt,
        lt_temp    TYPE TABLE OF rfc_db_opt,
        lt_fields  TYPE TABLE OF rfc_db_fld,
        lt_data    TYPE TABLE OF t_remote_tadir.

  APPEND ` delflag EQ '' ` TO lt_options.
  APPEND ` AND genflag EQ '' ` TO lt_options.

  " Object type checks
  IF s_objtyp[] IS NOT INITIAL.
    lt_temp = morph_selopt_to_rfc( if_fieldname = 'object' it_selopt = s_objtyp[] ).
    IF lt_temp IS NOT INITIAL.
      APPEND ` AND  ` TO lt_options.
      APPEND LINES OF lt_temp TO lt_options.
    ENDIF.
  ENDIF.

  " object name
  IF s_objnam[] IS NOT INITIAL.
    lt_temp = morph_selopt_to_rfc( if_fieldname = 'obj_name' it_selopt = s_objnam[] ).
    IF lt_temp IS NOT INITIAL.
      APPEND ` AND  ` TO lt_options.
      APPEND LINES OF lt_temp TO lt_options.
    ENDIF.
  ENDIF.

  " package
  IF s_devc[] IS NOT INITIAL.
    lt_temp = morph_selopt_to_rfc( if_fieldname = 'devclass' it_selopt = s_devc[] ).
    IF lt_temp IS NOT INITIAL.
      APPEND ` AND  ` TO lt_options.
      APPEND LINES OF lt_temp TO lt_options.
    ENDIF.
  ENDIF.

  APPEND VALUE #( fieldname = 'OBJECT' ) TO lt_fields.
  APPEND VALUE #( fieldname = 'OBJ_NAME' ) TO lt_fields.

  " get objects
  CALL FUNCTION 'RFC_READ_TABLE'
    DESTINATION p_rfcdes
    EXPORTING
      query_table           = 'TADIR'
    TABLES
      options               = lt_options
      fields                = lt_fields
      data                  = lt_data
    EXCEPTIONS
      system_failure        = 1 MESSAGE lf_message
      communication_failure = 2 MESSAGE lf_message
      OTHERS                = 3.

  LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<s_data>).
    INSERT <s_data> INTO TABLE gt_remote_objects.
  ENDLOOP.

ENDMETHOD.

This method helps us verify if the objects we are interested in are also present and valid in the production environment.


📊 3. Get SCMON Data and Create the Result

Finally, we integrate the SCMON data to identify which objects are actually being used. This step is crucial because it provides the real usage data needed to flag unused objects. We loop through the objects and check their usage data from SCMON. Below is the code to retrieve and display the results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
LOOP AT lt_objname ASSIGNING FIELD-SYMBOL(<s_objname>).
  DATA(lf_remote) = abap_false.

  " Check if available in remote
  READ TABLE gt_remote_objects WITH KEY object = <s_objname>-object
                                        obj_name = <s_objname>-obj_name
                                TRANSPORTING NO FIELDS.
  IF sy-subrc EQ 0.
    lf_remote = abap_true.

  ENDIF.

  CASE <s_objname>-object.
    WHEN 'PROG'.
      " Report/Include
      CHECK <s_objname>-psubc IN s_psubc.
      CHECK <s_objname>-pcdat IN s_pcdat.

      IF <s_objname>-psubc EQ '1'.
        " Program will be used as is
        LOOP AT get_usage_data_prog( <s_objname>-obj_name ) ASSIGNING FIELD-SYMBOL(<s_outtab>).
          <s_outtab>-cdat = <s_objname>-pcdat.
          <s_outtab>-remote = lf_remote.
          <s_outtab>-tcode = <s_objname>-tcode.

          SELECT SINGLE text
            FROM trdirt
            INTO @<s_outtab>-title
            WHERE name EQ @<s_objname>-obj_name
              AND sprsl EQ @sy-langu.
          IF sy-subrc NE 0.
            SELECT SINGLE text
              FROM trdirt
              INTO @<s_outtab>-title
              WHERE name EQ @<s_objname>-obj_name.

          ENDIF.

          APPEND <s_outtab> TO gt_outtab.

        ENDLOOP.

      ELSE.
        " Include will be "extracted" to the main program
        LOOP AT get_usage_data_include( <s_objname>-obj_name ) ASSIGNING <s_outtab>.
          <s_outtab>-cdat = <s_objname>-pcdat.
          <s_outtab>-remote = lf_remote.
          <s_outtab>-tcode = <s_objname>-tcode.
          APPEND <s_outtab> TO gt_outtab.

        ENDLOOP.

      ENDIF.

    WHEN 'CLAS'.
      " Handle Class
      LOOP AT get_usage_data_clas( <s_objname>-obj_name ) ASSIGNING <s_outtab>.
        <s_outtab>-cdat = <s_objname>-pcdat.
        <s_outtab>-remote = lf_remote.
        APPEND <s_outtab> TO gt_outtab.

      ENDLOOP.

    WHEN 'FUGR'.
      " Handle Function Group
      LOOP AT get_usage_data_fugr( <s_objname>-obj_name ) ASSIGNING <s_outtab>.
        <s_outtab>-cdat = <s_objname>-pcdat.
        <s_outtab>-remote = lf_remote.
        APPEND <s_outtab> TO gt_outtab.

      ENDLOOP.

    WHEN OTHERS.
      CONTINUE.

  ENDCASE.

ENDLOOP.

This part of the report collects the relevant usage data for each object and checks whether it is being used remotely (in the production system). It then appends the results for later display in an ALV grid.


🧠 Final Thoughts

By combining SCMON data, remote checks, and custom reports, we were able to efficiently identify unused custom code during our SAP S/4 migration. This process helped us clean up the codebase, ensuring that only the necessary and relevant custom code was carried over to the new system.

If you are facing similar challenges during your S/4 migration, I hope this approach will save you time and effort in identifying unused code. Feel free to adapt the solution to your own needs or extend it to cover additional scenarios. Best of luck with your migration journey!

Happy coding!

This post is licensed under CC BY 4.0 by the author.