[[{“value”:”
Introduction:
If you work with SAP, you know that Purchase Orders (POs), Purchase Requisitions (PRs), and Long-Term Agreements (LTAs) almost always require an approval process, known as a release strategy.
When audit season rolls around, the same questions always pop up:
Who approved this document? When exactly did they do it? What level of approval was it? And how does it tie back to the original PR, LTA, or Sales Order?
Here is the frustrating part: SAP updates the current status in tables like EKKO and EBAN, but it doesn’t easily show the historical “who” and “when.” Since there is no out-of-the-box standard report that tracks this entire chain (PO → PR → LTA → SO), auditors usually have to dig through documents manually, one by one.
To solve this, I’ll show you how to build a custom PO Release Audit Report in ABAP. This report captures the approver’s user ID, full name, exact date, time, and approval level. Plus, it features a clean, single-screen ALV grid using CL_SALV_TABLE—allowing you to click and drill down into the details, and jump back with a single click, completely eliminating annoying pop-up windows.
Solution Review:
Solution at a Glance
The report reads the release status and its change history, finds the linked documents, and shows everything in one ALV where each click goes one level deeper.
Selection screen (PO / PR / LTA / SO)
│
▼
GET_DATA ──► starting documents (EKKO / EBAN / VBAK)
│
▼
GET_RELEASE_HISTORY ──► CDHDR + CDPOS + USER_ADDR
│
▼
ALV display (CL_SALV_TABLE)
│
click Document No. click Source
│ │
▼ ▼
DO_DRILL DO_BACK
(GET_CHAIN → next level) (restore previous list)
Design Decisions
Release data from change documents
EKKO and EBAN hold only the current release status (FRGZU). The user, date and time are in the change documents:
CDHDR has the username, date and time.
CDPOS has the new value of FRGZU.
USER_ADDR has the full name.
The report uses object class EINKBELEG for PO and LTA, and BANF for PR.
Same ALV, no popup
Instead of opening a new ALV each time, the report replaces the data and refreshes the same grid. Popups are avoided because they stack up and are harder to navigate.
Advantages
One report instead of checking ME23N, ME53N, VA03 and change documents one by one.
Shows release user, date, time and level together.
Follows the whole PO → PR → LTA → SO chain.
Simple navigation, with no popups.
The history-stack pattern is reusable in other ALV reports.
Step 1: Output structure
TYPES: BEGIN OF ty_out,
lvl TYPE n LENGTH 1, ” PO=1 PR=2 LTA=3 SO=4
source TYPE char3,
docno TYPE char10,
doctype TYPE char4,
erdat TYPE erdat,
ernam TYPE ernam,
frggr TYPE ekko-frggr,
frgsx TYPE ekko-frgsx,
frgke TYPE ekko-frgke,
frgzu TYPE ekko-frgzu,
rel_level TYPE char2,
uname TYPE cdhdr-username,
fullname TYPE user_addr-name_textc,
udate TYPE cdhdr-udate,
utime TYPE cdhdr-utime,
objcl TYPE cdhdr-objectclas, ” helper (hidden)
objid TYPE cdhdr-objectid, ” helper (hidden)
END OF ty_out.
Step 2: Selection screen
Fields appear or disappear depending on the radio button:
AT SELECTION-SCREEN OUTPUT.
LOOP AT SCREEN.
CASE screen-group1.
WHEN ‘PO’.
IF r_po = ‘X’. screen-active = 1. ELSE. screen-active = 0. ENDIF.
WHEN ‘PR’.
IF r_pr = ‘X’. screen-active = 1. ELSE. screen-active = 0. ENDIF.
WHEN ‘LTA’.
IF r_lta = ‘X’. screen-active = 1. ELSE. screen-active = 0. ENDIF.
WHEN ‘SO’.
IF r_so = ‘X’. screen-active = 1. ELSE. screen-active = 0. ENDIF.
ENDCASE.
MODIFY SCREEN.
ENDLOOP.
Step 3: Read the starting documents.
WHEN r_po.
SELECT ebeln, bsart, aedat, ernam, frggr, frgsx, frgke, frgzu
FROM ekko
WHERE ebeln IN @s_ebeln
AND aedat IN @s_aedat
AND bsart IN @s_bsart
AND bstyp = ‘F’
INTO TABLE @DATA(lt_po).
PR, LTA and SO follow the same pattern, using EBAN, EKKO (bstyp K or L) and VBAK.
Step 4: Release history
SELECT objectclas, objectid, changenr, username, udate, utime
FROM cdhdr
FOR ALL ENTRIES IN @LT_keys
WHERE objectclas = @LT_keys-objectclas
AND objectid = @LT_keys-objectid
INTO TABLE @DATA(lt_hdr).
SELECT objectclas, objectid, changenr, tabname, fname, value_new
FROM cdpos
FOR ALL ENTRIES IN @LT_hdr
WHERE objectclas = @LT_hdr-objectclas
AND objectid = @LT_hdr-objectid
AND changenr = @LT_hdr-changenr
AND tabname IN (‘EKKO’, ‘EBAN’)
AND fname = ‘FRGZU’
INTO TABLE @DATA(lt_pos).
The release level is the number of X characters in the new status:
lv_cnt = count( val = ls_pos-value_new sub = ‘X’ ).
IF lv_cnt > 0.
ls_final-rel_level = |{ lv_cnt }|.
ENDIF.
Step 5: Find linked documents
From To Link
PO PR EKPO-BANFN
PO or PR LTA EKPO-KONNR, EBAN-KONNR
LTA or PO SO EKKN-VBELN
PR SO EBKN-VBELN, VBEP-BANFN” PO -> PR
SELECT DISTINCT banfn FROM ekpo
WHERE ebeln = @PV_docno
AND banfn <> @Space
APPENDING TABLE @LT_prs.
” keep only real agreements (contract K / scheduling agreement L)
SELECT ebeln FROM ekko
FOR ALL ENTRIES IN @LT_cand
WHERE ebeln = @LT_cand-table_line
AND bstyp IN (‘K’, ‘L’)
INTO TABLE @LT_ltas.
Step 6: ALV with hotspots:
cl_salv_table=>factory( IMPORTING r_salv_table = go_alv
CHANGING t_table = gt_out ).
” DOCNO = go down one level
lo_col_tab ?= lo_cols->get_column( ‘DOCNO’ ).
lo_col_tab->set_cell_type( if_salv_c_cell_type=>hotspot ).
” SOURCE = go back one level
lo_col_tab ?= lo_cols->get_column( ‘SOURCE’ ).
lo_col_tab->set_cell_type( if_salv_c_cell_type=>hotspot ).
” hide helper columns
lo_cols->get_column( ‘OBJCL’ )->set_visible( abap_false ).
lo_cols->get_column( ‘OBJID’ )->set_visible( abap_false ).
lo_cols->get_column( ‘LVL’ )->set_visible( abap_false ).
SET HANDLER go_handler->on_link_click FOR lo_events.
Step 7: Click handler
METHOD on_link_click.
CASE column.
WHEN ‘DOCNO’. PERFORM do_drill USING row.
WHEN ‘SOURCE’. PERFORM do_back.
ENDCASE.
ENDMETHOD.
Step 8: Drill down (one level per click)
CASE ls_row-source.
WHEN ‘PO’. lv_next = ‘PR’.
WHEN ‘PR’. lv_next = ‘LTA’.
WHEN ‘LTA’. lv_next = ‘SO’.
ENDCASE.
PERFORM get_chain USING ls_row-source ls_row-docno
CHANGING lt_new lv_dummy.
DELETE lt_new WHERE source <> lv_next. ” keep ONLY the next level
” remember the current level
ls_hist-title = gv_title.
ls_hist-data = gt_out.
APPEND ls_hist TO gt_hist.
” show the next level in the SAME alv
gt_out = lt_new.
gv_title = lv_title.
PERFORM refresh_alv.
Step 9: Refresh the same ALV:
FORM refresh_alv.
go_alv->get_display_settings( )->set_list_header( gv_title ).
go_alv->get_columns( )->set_optimize( abap_true ).
go_alv->refresh( ).
ENDFORM.
Step:10 Display
*&———————————————————————*
*& Form DISPLAY_ALV (single full-screen ALV, no popup)
*&———————————————————————*
FORM display_alv.
DATA: lo_cols TYPE REF TO cl_salv_columns_table,
lo_col_tab TYPE REF TO cl_salv_column_table,
lo_events TYPE REF TO cl_salv_events_table.
IF gt_out IS INITIAL.
MESSAGE ‘No data found for the given selection’ TYPE ‘S’ DISPLAY LIKE ‘W’.
RETURN.
ENDIF.
TRY.
cl_salv_table=>factory( IMPORTING r_salv_table = go_alv
CHANGING t_table = gt_out ).
go_alv->get_functions( )->set_all( abap_true ).
go_alv->get_display_settings( )->set_list_header( gv_title ).
go_alv->get_display_settings( )->set_striped_pattern( abap_true ).
lo_cols = go_alv->get_columns( ).
lo_cols->set_optimize( abap_true ).
PERFORM set_col USING lo_cols ‘SOURCE’ ‘Source’ ‘Source’ ‘Source’.
PERFORM set_col USING lo_cols ‘DOCNO’ ‘Document’ ‘Document No.’ ‘Document Number’.
PERFORM set_col USING lo_cols ‘DOCTYPE’ ‘Doc.Type’ ‘Doc. Type’ ‘Document Type’.
PERFORM set_col USING lo_cols ‘ERDAT’ ‘Created On’ ‘Created On’ ‘Created On’.
PERFORM set_col USING lo_cols ‘ERNAM’ ‘Created By’ ‘Created By’ ‘Created By’.
PERFORM set_col USING lo_cols ‘FRGGR’ ‘Rel.Group’ ‘Release Group’ ‘Release Group’.
PERFORM set_col USING lo_cols ‘FRGSX’ ‘Rel.Strat.’ ‘Rel. Strategy’ ‘Release Strategy’.
PERFORM set_col USING lo_cols ‘FRGKE’ ‘Rel.Ind.’ ‘Rel. Indicator’ ‘Release Indicator’.
PERFORM set_col USING lo_cols ‘FRGZU’ ‘Rel.Status’ ‘Release Status’ ‘Release Status’.
PERFORM set_col USING lo_cols ‘REL_LEVEL’ ‘Rel.Level’ ‘Release Level’ ‘Release Level’.
PERFORM set_col USING lo_cols ‘UNAME’ ‘User ID’ ‘User ID’ ‘User ID’.
PERFORM set_col USING lo_cols ‘FULLNAME’ ‘Full Name’ ‘Full Name’ ‘Full Name’.
PERFORM set_col USING lo_cols ‘UDATE’ ‘Rel.Date’ ‘Release Date’ ‘Release Date’.
PERFORM set_col USING lo_cols ‘UTIME’ ‘Rel.Time’ ‘Release Time’ ‘Release Time’.
” hide helper columns
lo_cols->get_column( ‘OBJCL’ )->set_visible( abap_false ).
lo_cols->get_column( ‘OBJID’ )->set_visible( abap_false ).
lo_cols->get_column( ‘LVL’ )->set_visible( abap_false ).
” DOCNO = hotspot: go DOWN one level
lo_col_tab ?= lo_cols->get_column( ‘DOCNO’ ).
lo_col_tab->set_cell_type( if_salv_c_cell_type=>hotspot ).
” SOURCE = hotspot: go BACK one level
lo_col_tab ?= lo_cols->get_column( ‘SOURCE’ ).
lo_col_tab->set_cell_type( if_salv_c_cell_type=>hotspot ).
lo_events = go_alv->get_event( ).
CREATE OBJECT go_handler.
SET HANDLER go_handler->on_link_click FOR lo_events.
go_alv->display( ).
CATCH cx_salv_msg cx_salv_not_found cx_salv_data_error INTO DATA(lx).
MESSAGE lx->get_text( ) TYPE ‘E’.
ENDTRY.
ENDFORM.
*&———————————————————————*
*& Form SET_COL – column headings
*&———————————————————————*
FORM set_col USING po_cols TYPE REF TO cl_salv_columns_table
pv_name
pv_short
pv_med
pv_long.
DATA: lo_col TYPE REF TO cl_salv_column,
lv_name TYPE lvc_fname,
lv_short TYPE scrtext_s,
lv_med TYPE scrtext_m,
lv_long TYPE scrtext_l.
lv_name = pv_name.
lv_short = pv_short.
lv_med = pv_med.
lv_long = pv_long.
TRY.
lo_col = po_cols->get_column( lv_name ).
lo_col->set_short_text( lv_short ).
lo_col->set_medium_text( lv_med ).
lo_col->set_long_text( lv_long ).
CATCH cx_salv_not_found.
ENDTRY.
Full Report
*&———————————————————————*
*& Report ZPO_RELEASE_AUDIT
*& PO Release Audit Report
*& Same-ALV drill-down, ONE level per click (no popup)
*& Click PO document no. -> PR records
*& Click PR document no. -> LTA records
*& Click LTA document no. -> SO records
*& Click SOURCE -> go back one level
*&———————————————————————*
REPORT zpo_release_audit.
TABLES: ekko, eban, vbak.
*———————————————————————-*
* Types and data
*———————————————————————-*
TYPES: BEGIN OF ty_out,
lvl TYPE n LENGTH 1, ” helper: PO=1 PR=2 LTA=3 SO=4
source TYPE char3,
docno TYPE char10,
doctype TYPE char4,
erdat TYPE erdat,
ernam TYPE ernam,
frggr TYPE ekko-frggr,
frgsx TYPE ekko-frgsx,
frgke TYPE ekko-frgke,
frgzu TYPE ekko-frgzu,
rel_level TYPE char2,
uname TYPE cdhdr-username,
fullname TYPE user_addr-name_textc,
udate TYPE cdhdr-udate,
utime TYPE cdhdr-utime,
objcl TYPE cdhdr-objectclas,
objid TYPE cdhdr-objectid,
END OF ty_out.
TYPES tt_out TYPE STANDARD TABLE OF ty_out WITH DEFAULT KEY.
TYPES tt_num TYPE STANDARD TABLE OF char10 WITH DEFAULT KEY.
” history of previous levels (for going back)
TYPES: BEGIN OF ty_hist,
title TYPE lvc_title,
data TYPE tt_out,
END OF ty_hist.
DATA: gt_out TYPE tt_out, ” data shown in the ALV
gs_out TYPE ty_out,
gv_year TYPE gjahr,
gv_title TYPE lvc_title, ” current ALV header
gt_hist TYPE STANDARD TABLE OF ty_hist, ” previous levels
go_alv TYPE REF TO cl_salv_table.
*———————————————————————-*
* ALV hotspot handler
*———————————————————————-*
CLASS lcl_handler DEFINITION.
PUBLIC SECTION.
METHODS on_link_click
FOR EVENT link_click OF cl_salv_events_table
IMPORTING row column.
ENDCLASS.
CLASS lcl_handler IMPLEMENTATION.
METHOD on_link_click.
CASE column.
WHEN ‘DOCNO’.
PERFORM do_drill USING row.
WHEN ‘SOURCE’.
PERFORM do_back.
ENDCASE.
ENDMETHOD.
ENDCLASS.
DATA go_handler TYPE REF TO lcl_handler.
*———————————————————————-*
* Selection screen
*———————————————————————-*
SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME.
SELECT-OPTIONS: s_year FOR gv_year, “Year (always)
s_ebeln FOR ekko-ebeln MODIF ID po, “Purchasing Document
s_aedat FOR ekko-aedat, “Created On (always)
s_bsart FOR ekko-bsart MODIF ID dty, “Doc. Type
s_banfn FOR eban-banfn MODIF ID pr, “Purchase Requisition
s_lta FOR ekko-ebeln MODIF ID lta, “Long Term Agreement
s_vbeln FOR vbak-vbeln MODIF ID so. “Sales Order
SELECTION-SCREEN END OF BLOCK b1.
SELECTION-SCREEN BEGIN OF BLOCK b2 WITH FRAME.
PARAMETERS: r_po RADIOBUTTON GROUP g1 DEFAULT ‘X’ USER-COMMAND uc1,
r_pr RADIOBUTTON GROUP g1,
r_lta RADIOBUTTON GROUP g1,
r_so RADIOBUTTON GROUP g1.
SELECTION-SCREEN END OF BLOCK b2.
SELECTION-SCREEN BEGIN OF BLOCK b3 WITH FRAME.
PARAMETERS: p_po AS CHECKBOX DEFAULT ‘X’ MODIF ID chk,
p_pr AS CHECKBOX DEFAULT ‘X’ MODIF ID chk,
p_so AS CHECKBOX DEFAULT ‘X’ MODIF ID chk,
p_lta AS CHECKBOX DEFAULT ‘X’ MODIF ID chk.
SELECTION-SCREEN END OF BLOCK b3.
*———————————————————————-*
* Initialization: field texts and default year range
*———————————————————————-*
INITIALIZATION.
%_s_year_%_app_%-text = ‘Year’.
%_s_ebeln_%_app_%-text = ‘Purchasing Document’.
%_s_aedat_%_app_%-text = ‘Created On’.
%_s_bsart_%_app_%-text = ‘Purchasing Doc. Type’.
%_s_banfn_%_app_%-text = ‘Purchase Requisition’.
%_s_lta_%_app_%-text = ‘Long Term Agreement’.
%_s_vbeln_%_app_%-text = ‘Sales Order’.
%_r_po_%_app_%-text = ‘PO Selection’.
%_r_pr_%_app_%-text = ‘PR Selection’.
%_r_lta_%_app_%-text = ‘LTA Selection’.
%_r_so_%_app_%-text = ‘SO Selection’.
%_p_po_%_app_%-text = ‘PO’.
%_p_pr_%_app_%-text = ‘PR’.
%_p_so_%_app_%-text = ‘SO’.
%_p_lta_%_app_%-text = ‘LTA’.
s_year-sign = ‘I’.
s_year-option = ‘BT’.
s_year-low = ‘2023’.
s_year-high = ‘2026’.
APPEND s_year.
*———————————————————————-*
* When a radio button is clicked: reset all checkboxes to ticked.
*———————————————————————-*
AT SELECTION-SCREEN.
IF sy-ucomm = ‘UC1’.
p_po = ‘X’.
p_pr = ‘X’.
p_so = ‘X’.
p_lta = ‘X’.
ENDIF.
*———————————————————————-*
* Dynamic screen handling
*———————————————————————-*
AT SELECTION-SCREEN OUTPUT.
LOOP AT SCREEN.
CASE screen-group1.
WHEN ‘PO’.
IF r_po = ‘X’. screen-active = 1. ELSE. screen-active = 0. ENDIF.
WHEN ‘PR’.
IF r_pr = ‘X’. screen-active = 1. ELSE. screen-active = 0. ENDIF.
WHEN ‘LTA’.
IF r_lta = ‘X’. screen-active = 1. ELSE. screen-active = 0. ENDIF.
WHEN ‘SO’.
IF r_so = ‘X’. screen-active = 1. ELSE. screen-active = 0. ENDIF.
WHEN ‘DTY’. ” doc type not used for SO
IF r_so = ‘X’. screen-active = 0. ELSE. screen-active = 1. ENDIF.
ENDCASE.
IF screen-group1 = ‘CHK’.
IF screen-name CS ‘P_SO’.
IF r_po = ‘X’ OR r_so = ‘X’.
screen-active = 1.
ELSE.
screen-active = 0.
ENDIF.
ELSEIF screen-name CS ‘P_LTA’.
IF r_po = ‘X’ OR r_lta = ‘X’.
screen-active = 1.
ELSE.
screen-active = 0.
ENDIF.
ELSE.
screen-active = 1.
ENDIF.
screen-input = 1.
ENDIF.
MODIFY SCREEN.
ENDLOOP.
*———————————————————————-*
* Main processing
*———————————————————————-*
START-OF-SELECTION.
PERFORM get_data.
IF p_po IS INITIAL. DELETE gt_out WHERE source = ‘PO’. ENDIF.
IF p_pr IS INITIAL. DELETE gt_out WHERE source = ‘PR’. ENDIF.
IF p_so IS INITIAL. DELETE gt_out WHERE source = ‘SO’. ENDIF.
IF p_lta IS INITIAL. DELETE gt_out WHERE source = ‘LTA’. ENDIF.
PERFORM filter_year.
PERFORM get_release_history CHANGING gt_out.
gv_title = ‘PO Release Audit Report’.
PERFORM display_alv.
*&———————————————————————*
*& Form GET_DATA
*&———————————————————————*
FORM get_data.
CASE ‘X’.
WHEN r_po. ” —- Purchase Orders
SELECT ebeln, bsart, aedat, ernam, frggr, frgsx, frgke, frgzu
FROM ekko
WHERE ebeln IN @s_ebeln
AND aedat IN @s_aedat
AND bsart IN @s_bsart
AND bstyp = ‘F’
INTO TABLE @DATA(lt_po).
LOOP AT lt_po INTO DATA(ls_po).
CLEAR gs_out.
gs_out-source = ‘PO’.
gs_out-objcl = ‘EINKBELEG’.
gs_out-docno = ls_po-ebeln.
gs_out-objid = ls_po-ebeln.
gs_out-doctype = ls_po-bsart.
gs_out-erdat = ls_po-aedat.
gs_out-ernam = ls_po-ernam.
gs_out-frggr = ls_po-frggr.
gs_out-frgsx = ls_po-frgsx.
gs_out-frgke = ls_po-frgke.
gs_out-frgzu = ls_po-frgzu.
APPEND gs_out TO gt_out.
ENDLOOP.
WHEN r_pr. ” —- Purchase Requisitions
SELECT DISTINCT banfn, bsart, erdat, ernam, frggr, frgst, frgkz, frgzu
FROM eban
WHERE banfn IN @s_banfn
AND erdat IN @s_aedat
AND bsart IN @s_bsart
INTO TABLE @DATA(lt_pr).
LOOP AT lt_pr INTO DATA(ls_pr).
CLEAR gs_out.
gs_out-source = ‘PR’.
gs_out-objcl = ‘BANF’.
gs_out-docno = ls_pr-banfn.
gs_out-objid = ls_pr-banfn.
gs_out-doctype = ls_pr-bsart.
gs_out-erdat = ls_pr-erdat.
gs_out-ernam = ls_pr-ernam.
gs_out-frggr = ls_pr-frggr.
gs_out-frgsx = ls_pr-frgst.
gs_out-frgke = ls_pr-frgkz.
gs_out-frgzu = ls_pr-frgzu.
APPEND gs_out TO gt_out.
ENDLOOP.
WHEN r_lta. ” —- Long Term Agreements
SELECT ebeln, bsart, aedat, ernam, frggr, frgsx, frgke, frgzu
FROM ekko
WHERE ebeln IN @s_lta
AND aedat IN @s_aedat
AND bsart IN @s_bsart
AND bstyp IN (‘K’, ‘L’)
INTO TABLE @DATA(lt_lta).
LOOP AT lt_lta INTO DATA(ls_lta).
CLEAR gs_out.
gs_out-source = ‘LTA’.
gs_out-objcl = ‘EINKBELEG’.
gs_out-docno = ls_lta-ebeln.
gs_out-objid = ls_lta-ebeln.
gs_out-doctype = ls_lta-bsart.
gs_out-erdat = ls_lta-aedat.
gs_out-ernam = ls_lta-ernam.
gs_out-frggr = ls_lta-frggr.
gs_out-frgsx = ls_lta-frgsx.
gs_out-frgke = ls_lta-frgke.
gs_out-frgzu = ls_lta-frgzu.
APPEND gs_out TO gt_out.
ENDLOOP.
WHEN r_so. ” —- Sales Orders
SELECT vbeln, auart, erdat, ernam
FROM vbak
WHERE vbeln IN @s_vbeln
AND erdat IN @s_aedat
INTO TABLE @DATA(lt_so).
LOOP AT lt_so INTO DATA(ls_so).
CLEAR gs_out.
gs_out-source = ‘SO’.
gs_out-objcl = ‘VERKBELEG’.
gs_out-docno = ls_so-vbeln.
gs_out-objid = ls_so-vbeln.
gs_out-doctype = ls_so-auart.
gs_out-erdat = ls_so-erdat.
gs_out-ernam = ls_so-ernam.
APPEND gs_out TO gt_out.
ENDLOOP.
ENDCASE.
ENDFORM.
*&———————————————————————*
*& Form FILTER_YEAR
*&———————————————————————*
FORM filter_year.
DATA: ls_row TYPE ty_out,
lv_year TYPE gjahr.
LOOP AT gt_out INTO ls_row.
lv_year = ls_row-erdat(4).
IF lv_year NOT IN s_year.
DELETE gt_out INDEX sy-tabix.
ENDIF.
ENDLOOP.
ENDFORM.
*&———————————————————————*
*& Form GET_RELEASE_HISTORY
*& Release level, user id, full name, date, time from change documents
*&———————————————————————*
FORM get_release_history CHANGING ct_out TYPE tt_out.
TYPES: BEGIN OF ty_key,
objectclas TYPE cdhdr-objectclas,
objectid TYPE cdhdr-objectid,
END OF ty_key.
DATA: lt_keys TYPE STANDARD TABLE OF ty_key,
ls_key TYPE ty_key,
ls_out TYPE ty_out,
lt_final TYPE tt_out,
ls_final TYPE ty_out,
lv_found TYPE abap_bool,
lv_cnt TYPE i.
FIELD-SYMBOLS <ls_f> TYPE ty_out.
CHECK ct_out IS NOT INITIAL.
LOOP AT ct_out INTO ls_out WHERE source <> ‘SO’.
ls_key-objectclas = ls_out-objcl.
ls_key-objectid = ls_out-objid.
APPEND ls_key TO lt_keys.
ENDLOOP.
SORT lt_keys BY objectclas objectid.
DELETE ADJACENT DUPLICATES FROM lt_keys COMPARING objectclas objectid.
IF lt_keys IS NOT INITIAL.
SELECT objectclas, objectid, changenr, username, udate, utime
FROM cdhdr
FOR ALL ENTRIES IN @LT_keys
WHERE objectclas = @LT_keys-objectclas
AND objectid = @LT_keys-objectid
INTO TABLE @DATA(lt_hdr).
IF lt_hdr IS NOT INITIAL.
SELECT objectclas, objectid, changenr, tabname, fname, value_new
FROM cdpos
FOR ALL ENTRIES IN @LT_hdr
WHERE objectclas = @LT_hdr-objectclas
AND objectid = @LT_hdr-objectid
AND changenr = @LT_hdr-changenr
AND tabname IN (‘EKKO’, ‘EBAN’)
AND fname = ‘FRGZU’
INTO TABLE @DATA(lt_pos).
SORT lt_pos BY objectclas objectid changenr value_new.
DELETE ADJACENT DUPLICATES FROM lt_pos
COMPARING objectclas objectid changenr value_new.
SELECT bname, name_textc
FROM user_addr
FOR ALL ENTRIES IN @LT_hdr
WHERE bname = @LT_hdr-username
INTO TABLE @DATA(lt_name).
SORT lt_hdr BY objectclas objectid changenr.
SORT lt_name BY bname.
ENDIF.
ENDIF.
LOOP AT ct_out INTO ls_out.
lv_found = abap_false.
LOOP AT lt_pos INTO DATA(ls_pos)
WHERE objectclas = ls_out-objcl
AND objectid = ls_out-objid.
READ TABLE lt_hdr INTO DATA(ls_hdr)
WITH KEY objectclas = ls_pos-objectclas
objectid = ls_pos-objectid
changenr = ls_pos-changenr
BINARY SEARCH.
IF sy-subrc <> 0.
CONTINUE.
ENDIF.
ls_final = ls_out.
ls_final-uname = ls_hdr-username.
ls_final-udate = ls_hdr-udate.
ls_final-utime = ls_hdr-utime.
ls_final-frgzu = ls_pos-value_new.
lv_cnt = count( val = ls_pos-value_new sub = ‘X’ ).
IF lv_cnt > 0.
ls_final-rel_level = |{ lv_cnt }|.
ENDIF.
READ TABLE lt_name INTO DATA(ls_name)
WITH KEY bname = ls_hdr-username BINARY SEARCH.
IF sy-subrc = 0.
ls_final-fullname = ls_name-name_textc.
ENDIF.
APPEND ls_final TO lt_final.
lv_found = abap_true.
ENDLOOP.
IF lv_found = abap_false.
APPEND ls_out TO lt_final.
ENDIF.
ENDLOOP.
ct_out = lt_final.
LOOP AT ct_out ASSIGNING <ls_f>.
CASE <ls_f>-source.
WHEN ‘PO’. <ls_f>-lvl = 1.
WHEN ‘PR’. <ls_f>-lvl = 2.
WHEN ‘LTA’. <ls_f>-lvl = 3.
WHEN ‘SO’. <ls_f>-lvl = 4.
ENDCASE.
ENDLOOP.
SORT ct_out BY lvl docno udate utime.
ENDFORM.
*&———————————————————————*
*& GET_CHAIN : finds PO / PR / LTA / SO linked to the clicked row
*& (do_drill keeps only the next level)
*&———————————————————————*
FORM get_chain USING pv_source TYPE char3
pv_docno TYPE char10
CHANGING ct_det TYPE tt_out
cv_title TYPE lvc_title.
DATA: lt_pos TYPE tt_num, ” POs shown in the list
lt_rel TYPE tt_num, ” related POs (only used to find links)
lt_prs TYPE tt_num, ” PRs
lt_cand TYPE tt_num, ” LTA candidates
lt_ltas TYPE tt_num, ” real LTAs
lt_sos TYPE tt_num. ” SOs
CLEAR: ct_det, cv_title.
“——————————————————————
” 1) Starting point
“——————————————————————
CASE pv_source.
WHEN ‘PO’.
APPEND pv_docno TO lt_pos.
APPEND pv_docno TO lt_rel.
” PO -> PR (PO items)
SELECT DISTINCT banfn FROM ekpo
WHERE ebeln = @PV_docno
AND banfn <> @Space
APPENDING TABLE @LT_prs.
WHEN ‘PR’.
APPEND pv_docno TO lt_prs.
WHEN ‘LTA’.
APPEND pv_docno TO lt_cand.
” POs that reference this LTA (used to find sales orders)
SELECT DISTINCT ebeln FROM ekpo
WHERE konnr = @PV_docno
APPENDING TABLE @LT_rel.
ENDCASE.
“——————————————————————
” 2) PR -> related POs and LTA candidates
“——————————————————————
IF lt_prs IS NOT INITIAL.
SORT lt_prs.
DELETE ADJACENT DUPLICATES FROM lt_prs.
” POs created from the PR
SELECT ebeln FROM ekpo
FOR ALL ENTRIES IN @LT_prs
WHERE banfn = @LT_prs-table_line
APPENDING TABLE @LT_rel.
” PO / agreement number stored on the PR
SELECT ebeln FROM eban
FOR ALL ENTRIES IN @LT_prs
WHERE banfn = @LT_prs-table_line
AND ebeln <> @Space
APPENDING TABLE @LT_rel.
” agreement number entered on the PR
SELECT konnr FROM eban
FOR ALL ENTRIES IN @LT_prs
WHERE banfn = @LT_prs-table_line
AND konnr <> @Space
APPENDING TABLE @LT_cand.
ENDIF.
“——————————————————————
” 3) related POs -> agreement referenced on PO items
“——————————————————————
IF lt_rel IS NOT INITIAL.
SORT lt_rel.
DELETE ADJACENT DUPLICATES FROM lt_rel.
SELECT konnr FROM ekpo
FOR ALL ENTRIES IN @LT_rel
WHERE ebeln = @LT_rel-table_line
AND konnr <> @Space
APPENDING TABLE @LT_cand.
” scheduling agreements may be linked directly (EBELN)
APPEND LINES OF lt_rel TO lt_cand.
ENDIF.
“——————————————————————
” 4) keep only real agreements (contract K / scheduling agreement L)
“——————————————————————
SORT lt_cand.
DELETE ADJACENT DUPLICATES FROM lt_cand.
DELETE lt_cand WHERE table_line IS INITIAL.
IF lt_cand IS NOT INITIAL.
SELECT ebeln FROM ekko
FOR ALL ENTRIES IN @LT_cand
WHERE ebeln = @LT_cand-table_line
AND bstyp IN (‘K’, ‘L’)
INTO TABLE @LT_ltas.
ENDIF.
“——————————————————————
” 5) Sales orders
“——————————————————————
” LTA account assignment
IF lt_ltas IS NOT INITIAL.
SELECT vbeln FROM ekkn
FOR ALL ENTRIES IN @LT_ltas
WHERE ebeln = @LT_ltas-table_line
AND vbeln <> @Space
APPENDING TABLE @LT_sos.
ENDIF.
” PO account assignment
IF lt_rel IS NOT INITIAL.
SELECT vbeln FROM ekkn
FOR ALL ENTRIES IN @LT_rel
WHERE ebeln = @LT_rel-table_line
AND vbeln <> @Space
APPENDING TABLE @LT_sos.
ENDIF.
” PR account assignment and SO schedule lines (third-party items)
IF lt_prs IS NOT INITIAL.
SELECT vbeln FROM ebkn
FOR ALL ENTRIES IN @LT_prs
WHERE banfn = @LT_prs-table_line
AND vbeln <> @Space
APPENDING TABLE @LT_sos.
SELECT vbeln FROM vbep
FOR ALL ENTRIES IN @LT_prs
WHERE banfn = @LT_prs-table_line
APPENDING TABLE @LT_sos.
ENDIF.
“——————————————————————
” 6) Build output rows: PO, PR, LTA, SO
“——————————————————————
PERFORM add_ekko USING ‘PO’ lt_pos CHANGING ct_det.
PERFORM add_eban USING lt_prs CHANGING ct_det.
PERFORM add_ekko USING ‘LTA’ lt_ltas CHANGING ct_det.
PERFORM add_vbak USING lt_sos CHANGING ct_det.
” only the clicked document itself found = nothing linked
IF lines( ct_det ) <= 1.
CLEAR ct_det.
RETURN.
ENDIF.
cv_title = |Linked documents for { pv_source } { pv_docno }|.
” release level, user id, full name, date, time for every row
PERFORM get_release_history CHANGING ct_det.
ENDFORM.
*&———————————————————————*
*& ADD_EKKO : PO rows (pv_source = PO) or LTA rows (pv_source = LTA)
*&———————————————————————*
FORM add_ekko USING pv_source TYPE char3
pt_nums TYPE tt_num
CHANGING ct_det TYPE tt_out.
DATA: lt_n TYPE tt_num,
lr_bstyp TYPE RANGE OF ekko-bstyp,
ls_det TYPE ty_out.
lt_n = pt_nums.
SORT lt_n.
DELETE ADJACENT DUPLICATES FROM lt_n.
DELETE lt_n WHERE table_line IS INITIAL.
IF lt_n IS INITIAL.
RETURN.
ENDIF.
IF pv_source = ‘PO’.
lr_bstyp = VALUE #( ( sign = ‘I’ option = ‘EQ’ low = ‘F’ ) ).
ELSE.
lr_bstyp = VALUE #( ( sign = ‘I’ option = ‘EQ’ low = ‘K’ )
( sign = ‘I’ option = ‘EQ’ low = ‘L’ ) ).
ENDIF.
SELECT ebeln, bsart, aedat, ernam, frggr, frgsx, frgke, frgzu
FROM ekko
FOR ALL ENTRIES IN @LT_n
WHERE ebeln = @LT_n-table_line
AND bstyp IN @LR_bstyp
INTO TABLE @DATA(lt_ekko).
SORT lt_ekko BY ebeln.
LOOP AT lt_ekko INTO DATA(ls_ekko).
CLEAR ls_det.
ls_det-source = pv_source.
ls_det-objcl = ‘EINKBELEG’.
ls_det-docno = ls_ekko-ebeln.
ls_det-objid = ls_ekko-ebeln.
ls_det-doctype = ls_ekko-bsart.
ls_det-erdat = ls_ekko-aedat.
ls_det-ernam = ls_ekko-ernam.
ls_det-frggr = ls_ekko-frggr.
ls_det-frgsx = ls_ekko-frgsx.
ls_det-frgke = ls_ekko-frgke.
ls_det-frgzu = ls_ekko-frgzu.
APPEND ls_det TO ct_det.
ENDLOOP.
ENDFORM.
*&———————————————————————*
*& ADD_EBAN : PR rows
*&———————————————————————*
FORM add_eban USING pt_nums TYPE tt_num
CHANGING ct_det TYPE tt_out.
DATA: lt_n TYPE tt_num,
ls_det TYPE ty_out.
lt_n = pt_nums.
SORT lt_n.
DELETE ADJACENT DUPLICATES FROM lt_n.
DELETE lt_n WHERE table_line IS INITIAL.
IF lt_n IS INITIAL.
RETURN.
ENDIF.
SELECT banfn, bsart, erdat, ernam, frggr, frgst, frgkz, frgzu
FROM eban
FOR ALL ENTRIES IN @LT_n
WHERE banfn = @LT_n-table_line
INTO TABLE @DATA(lt_eban).
SORT lt_eban BY banfn.
DELETE ADJACENT DUPLICATES FROM lt_eban COMPARING banfn.
LOOP AT lt_eban INTO DATA(ls_eban).
CLEAR ls_det.
ls_det-source = ‘PR’.
ls_det-objcl = ‘BANF’.
ls_det-docno = ls_eban-banfn.
ls_det-objid = ls_eban-banfn.
ls_det-doctype = ls_eban-bsart.
ls_det-erdat = ls_eban-erdat.
ls_det-ernam = ls_eban-ernam.
ls_det-frggr = ls_eban-frggr.
ls_det-frgsx = ls_eban-frgst.
ls_det-frgke = ls_eban-frgkz.
ls_det-frgzu = ls_eban-frgzu.
APPEND ls_det TO ct_det.
ENDLOOP.
ENDFORM.
*&———————————————————————*
*& ADD_VBAK : SO rows
*&———————————————————————*
FORM add_vbak USING pt_nums TYPE tt_num
CHANGING ct_det TYPE tt_out.
DATA: lt_n TYPE tt_num,
ls_det TYPE ty_out.
lt_n = pt_nums.
SORT lt_n.
DELETE ADJACENT DUPLICATES FROM lt_n.
DELETE lt_n WHERE table_line IS INITIAL.
IF lt_n IS INITIAL.
RETURN.
ENDIF.
SELECT vbeln, auart, erdat, ernam
FROM vbak
FOR ALL ENTRIES IN @LT_n
WHERE vbeln = @LT_n-table_line
INTO TABLE @DATA(lt_vbak).
SORT lt_vbak BY vbeln.
LOOP AT lt_vbak INTO DATA(ls_vbak).
CLEAR ls_det.
ls_det-source = ‘SO’.
ls_det-objcl = ‘VERKBELEG’.
ls_det-docno = ls_vbak-vbeln.
ls_det-objid = ls_vbak-vbeln.
ls_det-doctype = ls_vbak-auart.
ls_det-erdat = ls_vbak-erdat.
ls_det-ernam = ls_vbak-ernam.
APPEND ls_det TO ct_det.
ENDLOOP.
ENDFORM.
*&———————————————————————*
*& DO_DRILL : click on Document No.
*& PO -> only PR rows
*& PR -> only LTA rows
*& LTA -> only SO rows
*&———————————————————————*
FORM do_drill USING pv_row TYPE i.
DATA: ls_row TYPE ty_out,
lt_new TYPE tt_out,
lv_title TYPE lvc_title,
lv_next TYPE char3,
ls_hist TYPE ty_hist,
lv_cnt TYPE i.
data: lv_dummy TYPE lvc_title.
READ TABLE gt_out INTO ls_row INDEX pv_row.
IF sy-subrc <> 0.
RETURN.
ENDIF.
IF ls_row-source = ‘SO’.
MESSAGE ‘No further details below Sales Order’ TYPE ‘S’ DISPLAY LIKE ‘W’.
RETURN.
ENDIF.
” which level should be shown next
CASE ls_row-source.
WHEN ‘PO’.
lv_next = ‘PR’.
lv_title = |PR details for PO { ls_row-docno }|.
WHEN ‘PR’.
lv_next = ‘LTA’.
lv_title = |LTA details for PR { ls_row-docno }|.
WHEN ‘LTA’.
lv_next = ‘SO’.
lv_title = |SO details for LTA { ls_row-docno }|.
ENDCASE.
” find all linked documents, then keep ONLY the next level
PERFORM get_chain USING ls_row-source ls_row-docno
CHANGING lt_new lv_dummy.
DELETE lt_new WHERE source <> lv_next.
IF lt_new IS INITIAL.
MESSAGE |No { lv_next } found for { ls_row-source } { ls_row-docno }|
TYPE ‘I’.
RETURN.
ENDIF.
” remember the current level so we can come back
ls_hist-title = gv_title.
ls_hist-data = gt_out.
APPEND ls_hist TO gt_hist.
” show the next level in the SAME alv
gt_out = lt_new.
gv_title = lv_title.
PERFORM refresh_alv.
lv_cnt = lines( lt_new ).
MESSAGE |{ lv_cnt } { lv_next } record(s) found| TYPE ‘S’.
ENDFORM.
*&———————————————————————*
*& DO_BACK : click on Source -> go back to the previous level
*&———————————————————————*
FORM do_back.
DATA lv_lines TYPE i.
FIELD-SYMBOLS <ls_hist> TYPE ty_hist.
lv_lines = lines( gt_hist ).
IF lv_lines = 0.
MESSAGE ‘You are already at the first level’ TYPE ‘S’ DISPLAY LIKE ‘W’.
RETURN.
ENDIF.
READ TABLE gt_hist ASSIGNING <ls_hist> INDEX lv_lines.
gt_out = <ls_hist>-data.
gv_title = <ls_hist>-title.
DELETE gt_hist INDEX lv_lines.
PERFORM refresh_alv.
ENDFORM.
*&———————————————————————*
*& REFRESH_ALV : reload the changed GT_OUT into the same ALV
*&———————————————————————*
FORM refresh_alv.
go_alv->get_display_settings( )->set_list_header( gv_title ).
go_alv->get_columns( )->set_optimize( abap_true ).
go_alv->refresh( ).
ENDFORM.
*&———————————————————————*
*& Form DISPLAY_ALV (single full-screen ALV, no popup)
*&———————————————————————*
FORM display_alv.
DATA: lo_cols TYPE REF TO cl_salv_columns_table,
lo_col_tab TYPE REF TO cl_salv_column_table,
lo_events TYPE REF TO cl_salv_events_table.
IF gt_out IS INITIAL.
MESSAGE ‘No data found for the given selection’ TYPE ‘S’ DISPLAY LIKE ‘W’.
RETURN.
ENDIF.
TRY.
cl_salv_table=>factory( IMPORTING r_salv_table = go_alv
CHANGING t_table = gt_out ).
go_alv->get_functions( )->set_all( abap_true ).
go_alv->get_display_settings( )->set_list_header( gv_title ).
go_alv->get_display_settings( )->set_striped_pattern( abap_true ).
lo_cols = go_alv->get_columns( ).
lo_cols->set_optimize( abap_true ).
PERFORM set_col USING lo_cols ‘SOURCE’ ‘Source’ ‘Source’ ‘Source’.
PERFORM set_col USING lo_cols ‘DOCNO’ ‘Document’ ‘Document No.’ ‘Document Number’.
PERFORM set_col USING lo_cols ‘DOCTYPE’ ‘Doc.Type’ ‘Doc. Type’ ‘Document Type’.
PERFORM set_col USING lo_cols ‘ERDAT’ ‘Created On’ ‘Created On’ ‘Created On’.
PERFORM set_col USING lo_cols ‘ERNAM’ ‘Created By’ ‘Created By’ ‘Created By’.
PERFORM set_col USING lo_cols ‘FRGGR’ ‘Rel.Group’ ‘Release Group’ ‘Release Group’.
PERFORM set_col USING lo_cols ‘FRGSX’ ‘Rel.Strat.’ ‘Rel. Strategy’ ‘Release Strategy’.
PERFORM set_col USING lo_cols ‘FRGKE’ ‘Rel.Ind.’ ‘Rel. Indicator’ ‘Release Indicator’.
PERFORM set_col USING lo_cols ‘FRGZU’ ‘Rel.Status’ ‘Release Status’ ‘Release Status’.
PERFORM set_col USING lo_cols ‘REL_LEVEL’ ‘Rel.Level’ ‘Release Level’ ‘Release Level’.
PERFORM set_col USING lo_cols ‘UNAME’ ‘User ID’ ‘User ID’ ‘User ID’.
PERFORM set_col USING lo_cols ‘FULLNAME’ ‘Full Name’ ‘Full Name’ ‘Full Name’.
PERFORM set_col USING lo_cols ‘UDATE’ ‘Rel.Date’ ‘Release Date’ ‘Release Date’.
PERFORM set_col USING lo_cols ‘UTIME’ ‘Rel.Time’ ‘Release Time’ ‘Release Time’.
” hide helper columns
lo_cols->get_column( ‘OBJCL’ )->set_visible( abap_false ).
lo_cols->get_column( ‘OBJID’ )->set_visible( abap_false ).
lo_cols->get_column( ‘LVL’ )->set_visible( abap_false ).
” DOCNO = hotspot: go DOWN one level
lo_col_tab ?= lo_cols->get_column( ‘DOCNO’ ).
lo_col_tab->set_cell_type( if_salv_c_cell_type=>hotspot ).
” SOURCE = hotspot: go BACK one level
lo_col_tab ?= lo_cols->get_column( ‘SOURCE’ ).
lo_col_tab->set_cell_type( if_salv_c_cell_type=>hotspot ).
lo_events = go_alv->get_event( ).
CREATE OBJECT go_handler.
SET HANDLER go_handler->on_link_click FOR lo_events.
go_alv->display( ).
CATCH cx_salv_msg cx_salv_not_found cx_salv_data_error INTO DATA(lx).
MESSAGE lx->get_text( ) TYPE ‘E’.
ENDTRY.
ENDFORM.
*&———————————————————————*
*& Form SET_COL – column headings
*&———————————————————————*
FORM set_col USING po_cols TYPE REF TO cl_salv_columns_table
pv_name
pv_short
pv_med
pv_long.
DATA: lo_col TYPE REF TO cl_salv_column,
lv_name TYPE lvc_fname,
lv_short TYPE scrtext_s,
lv_med TYPE scrtext_m,
lv_long TYPE scrtext_l.
lv_name = pv_name.
lv_short = pv_short.
lv_med = pv_med.
lv_long = pv_long.
TRY.
lo_col = po_cols->get_column( lv_name ).
lo_col->set_short_text( lv_short ).
lo_col->set_medium_text( lv_med ).
lo_col->set_long_text( lv_long ).
CATCH cx_salv_not_found.
ENDTRY.
ENDFORM.
What the user gets
Column Meaning
Source / Document No. Document type and number (clickable)
Release Group, Strategy, Indicator, Status Current release data
Release Level Number of approval levels released
User ID and Full Name Who released
Release Date and Time When it was released
Limitations
The release level assumes cumulative codes (X, XX, XXX). Check this against your own release strategy.
Reading change documents for many documents can be slow, so keep the selection narrow.
Add authorization checks before production use.
A toolbar “Back” button would be clearer than the SOURCE hotspot.
Conclusion
Out of the box, SAP doesn’t give us a standard report that shows who approved a PO, when they did it, or how that PO links back to its parent PR, LTA, and Sales Order. This custom report bridges that gap perfectly.
By pulling data directly from the change document tables, the report lays out the exact approver, date, time, and release level for every document. The drill-down feature allows auditors to follow the entire chain—from PO to PR to LTA to SO—right inside a single ALV grid. Best of all, the history-stack design means you can easily navigate backward, one level at a time. It’s a highly reusable pattern you can drop into any ALV report that needs smooth drill-down navigation.
Feel free to take this template and customize it further by adding authorization checks, an Excel export feature, or even more document types. I hope this helps you build better, more auditor-friendly reports.
If you have any questions, suggestions, or a different way of handling this in your own projects, let me know in the comments below!
Thanks For Reading, Best Wishes
#ABAP Development
“}]]
Read More Technology Blog Posts by Members articles
#abap