[[{“value”:”
Introduction
Building a self-service data utility isn’t just about writing efficient code; it’s about shifting manual transactional burdens off the IT department’s plate and empowering the business user. By combining a flexible selection screen, clean format branching, and an object-oriented email architecture, this pattern becomes easily reusable for other common data objects like Customer Master, Vendor Master, or open Sales Orders.
Solution Overview
The report is structured around a simple, linear flow, with each step handled by its own form routine:
Selection Screen: The user specifies a range of material numbers (S_matnr) and picks one output format via radio buttons — TXT, CSV, XLS, or XLSX — along with the destination email address.
Data Retrieval: A single select reads eight key fields from Mara: material number, material type, material group, base unit of measure, creation date, creator, last-changed date, and cross-plant material status.
File Construction: Based on the selected format, the report assembles the file content in memory:
TXT — tab-separated values.
CSV — comma-separated values.
XLS — an HTML table that Excel can open natively.
XLSX — a real Excel workbook, built using the open-source abap2xlsx library (ZCL_EXCEL).
Text-based content is converted to Xstring via CL_ABAP_CONV_OUT_CE, since email attachments require binary data.
Email Dispatch (Send Email)
The report uses the BCS (Business Communication Service) API CL_BCS, CL_DOCUMENT_BCS, and CL_CAM_ADDRESS_BCS to build a document, attach the generated file, set the recipient, and send it immediately (set_send_immediately( abap_true)).
One data table, four formats – Rather than writing four separate reports, the logic branches on the same in-memory table (gt_mat) and reuses a shared string_to_xstring routine for the text-based formats.
Architectural Decisions: When designing self-service tools, developers often face trade-offs between quick development and long-term maintainability. Here is why this report is engineered the way it is:
One Report vs. Four Separate Programs: It is incredibly common to see separate custom reports for “Material Extract to CSV” and “Material Extract to Excel.” However, managing multiple codebases creates an unnecessary maintenance burden. By supporting four distinct formats within a single program, we drastically reduce our Total Cost of Ownership (TCO). All four outputs branch downstream from a single in-memory internal table gt_mat. If a business requirement changes tomorrow — such as adding a 9th material field — we only have to modify one SQL statement and one data type rather than updating four separate programs.
Why CL_BCS Over Legacy Function Modules: While older functional alternatives like SO_NEW_DOCUMENT_ATT_SEND_API1 are technically less verbose to write, they rely on rigid, legacy dictionary structures that are difficult to debug and lack modern flexibility. The object-oriented BCS API (CL_BCS) provides robust native exception handling (CX_BCS), cleanly isolates the document configuration layer from the recipient routing engine and natively accepts raw XSTRING binary payloads without forcing complex text-wrapping mechanics. This makes the code future-proof and drastically lowers the risk of email transmission dumps in modern SAP NetWeaver and S/4HANA environments.
Selection screen:
Report program:
Declarations and Selection Screen
The program starts with the structure definition for the extract (ty_mat), the working variables, and the selection screen itself. The selection screen groups three logical blocks: the material number range, the output-format radio buttons, and the recipient email address.
*&—————————————————————*
*& Report ZMATERIAL_EMAIL_EXTRACT
*&—————————————————————*
*& Selection screen: pick material(s) + output format (TXT/CSV/XLS/XLSX)
*& Fetches 8 fields of material data, converts to chosen format,
*& and sends it as an email attachment.
*&—————————————————————*
TABLES: mara.
TYPES: BEGIN OF ty_mat,
matnr TYPE mara-matnr, ” Material Number
mtart TYPE mara-mtart, ” Material Type
matkl TYPE mara-matkl, ” Material Group
meins TYPE mara-meins, ” Base Unit of Measure
ersda TYPE mara-ersda, ” Created On
ernam TYPE mara-ernam, ” Created By
laeda TYPE mara-laeda, ” Last Changed On
mstae TYPE mara-mstae, ” Cross-Plant Material Status
END OF ty_mat.
DATA: gt_mat TYPE STANDARD TABLE OF ty_mat,
gs_mat TYPE ty_mat,
gv_xstring TYPE xstring,
gv_filename TYPE string,
gv_format TYPE string.
*—————————————————————-*
* SELECTION SCREEN
*—————————————————————-*
SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-001.
SELECT-OPTIONS: s_matnr FOR mara-matnr.
SELECTION-SCREEN END OF BLOCK b1.
SELECTION-SCREEN BEGIN OF BLOCK b2 WITH FRAME TITLE TEXT-002.
PARAMETERS: p_txt RADIOBUTTON GROUP fmt DEFAULT ‘X’,
p_csv RADIOBUTTON GROUP fmt,
p_xls RADIOBUTTON GROUP fmt,
p_xlsx RADIOBUTTON GROUP fmt.
SELECTION-SCREEN END OF BLOCK b2.
SELECTION-SCREEN BEGIN OF BLOCK b3 WITH FRAME TITLE TEXT-003.
PARAMETERS: p_email TYPE ad_smtpadr OBLIGATORY.
SELECTION-SCREEN END OF BLOCK b3.
Main Control Flow:
Start of selection ties the whole report together it works out the chosen format, pulls the material data, and only if data was actually found builds the file and fires off the email.
*—————————————————————-*
* MAIN LOGIC
*—————————————————————-*
START-OF-SELECTION.
PERFORM determine_format.
PERFORM get_material_data.
IF gt_mat IS NOT INITIAL.
PERFORM build_file_content.
PERFORM send_email.
ELSE.
MESSAGE ‘No material data found for selection’ TYPE ‘I’.
ENDIF.
Determining the Output Format and Filename :
This routine reads whichever radio button is active and builds a dynamic, timestamped filename in the pattern Material Extract <timestamp>. <Ext>, so repeated runs never overwrite each other.
*&—————————————————————*
*& Form DETERMINE_FORMAT
*& Reads whichever radio button is active and builds a
*& dynamic filename: Material_Extract_<timestamp>.<ext>
*&—————————————————————*
FORM determine_format.
DATA: lv_ext TYPE string,
lv_ts TYPE string.
CASE abap_true.
WHEN p_txt. gv_format = ‘TXT’.
WHEN p_csv. gv_format = ‘CSV’.
WHEN p_xls. gv_format = ‘XLS’.
WHEN p_xlsx. gv_format = ‘XLSX’.
ENDCASE.
lv_ext = to_lower( gv_format ).
CONCATENATE sy-datum sy-uzeit INTO lv_ts. ” e.g. 2026081914301
CONCATENATE ‘Material_Extract_’ lv_ts ‘.’ lv_ext INTO gv_filename.
ENDFORM.
Data Retrieval:
A single, straightforward select against MARA pulls all eight fields for the materials in the selected range. Keeping this as one query rather than one per format is what lets the rest of the report stay format agnostic.
*&—————————————————————*
*& Form GET_MATERIAL_DATA
*&—————————————————————*
FORM get_material_data.
SELECT matnr mtart matkl meins ersda ernam laeda mstae
FROM mara
INTO TABLE gt_mat
WHERE matnr IN s_matnr.
ENDFORM.
File Construction: Dispatching by Format:
build file content is the branch point of the whole report. Depending on which radio button was selected, it hands off to the matching format-specific block below and populates gv xstring with the final binary payload.
*&—————————————————————*
*& Form BUILD_FILE_CONTENT
*& Builds gv_xstring + gv_filename based on selected radio button
*&—————————————————————*
FORM build_file_content.
DATA: lv_string TYPE string,
lv_line TYPE string.
CASE abap_true.
WHEN p_txt. ” —- TXT (tab separated) —-
LOOP AT gt_mat INTO gs_mat.
CONCATENATE gs_mat-matnr gs_mat-mtart gs_mat-matkl gs_mat-meins
gs_mat-ersda gs_mat-ernam gs_mat-laeda gs_mat-mstae
INTO lv_line SEPARATED BY cl_abap_char_utilities=>horizontal_tab.
CONCATENATE lv_string lv_line cl_abap_char_utilities=>cr_lf
INTO lv_string.
ENDLOOP.
gv_filename = ‘Material_Extract.txt’.
PERFORM string_to_xstring USING lv_string CHANGING gv_xstring.
WHEN p_csv. ” —- CSV (comma separated) —-
LOOP AT gt_mat INTO gs_mat.
CONCATENATE gs_mat-matnr gs_mat-mtart gs_mat-matkl gs_mat-meins
gs_mat-ersda gs_mat-ernam gs_mat-laeda gs_mat-mstae
INTO lv_line SEPARATED BY ‘,’.
CONCATENATE lv_string lv_line cl_abap_char_utilities=>cr_lf
INTO lv_string.
ENDLOOP.
gv_filename = ‘Material_Extract.csv’.
PERFORM string_to_xstring USING lv_string CHANGING gv_xstring.
WHEN p_xls. ” —- XLS (HTML table trick – opens fine in Excel) —-
CONCATENATE lv_string
‘<table border=”1″>’
‘<tr><th>Material</th><th>Type</th><th>Group</th><th>UoM</th>’
‘<th>Created On</th><th>Created By</th><th>Changed On</th><th>Status</th></tr>’
INTO lv_string.
LOOP AT gt_mat INTO gs_mat.
CONCATENATE lv_string
‘<tr><td>’ gs_mat-matnr ‘</td><td>’ gs_mat-mtart ‘</td><td>’ gs_mat-matkl
‘</td><td>’ gs_mat-meins ‘</td><td>’ gs_mat-ersda ‘</td><td>’ gs_mat-ernam
‘</td><td>’ gs_mat-laeda ‘</td><td>’ gs_mat-mstae ‘</td></tr>’
INTO lv_string.
ENDLOOP.
CONCATENATE lv_string ‘</table>’ INTO lv_string.
gv_filename = ‘Material_Extract.xls’.
PERFORM string_to_xstring USING lv_string CHANGING gv_xstring.
WHEN p_xlsx. ” —- XLSX (real Excel via abap2xlsx) —-
PERFORM build_xlsx_xstring CHANGING gv_xstring.
gv_filename = ‘Material_Extract.xlsx’.
ENDCASE.
ENDFORM.
TXT and CSV: Shared Delimited-Format Logic:
Since TXT and CSV differ only by their separator character, both are served by the same build delimited helper one parameter, one routine, no duplicated loop logic.
*&—————————————————————*
*& Form BUILD_DELIMITED
*& Shared logic for TXT/CSV – only the separator differs
*&—————————————————————*
FORM build_delimited USING iv_sep TYPE clike.
DATA: lv_string TYPE string,
lv_line TYPE string.
LOOP AT gt_mat INTO gs_mat.
CONCATENATE gs_mat-matnr gs_mat-mtart gs_mat-matkl gs_mat-meins
gs_mat-ersda gs_mat-ernam gs_mat-laeda gs_mat-mstae
INTO lv_line SEPARATED BY iv_sep.
CONCATENATE lv_string lv_line cl_abap_char_utilities=>cr_lf
INTO lv_string.
ENDLOOP.
PERFORM string_to_xstring USING lv_string CHANGING gv_xstring.
ENDFORM.
XLS: HTML-Table Trick:
Excel happily opens a .xls named file that’s actually just an HTML table, which avoids pulling in a real spreadsheet library for this format. build_xls_xstring assembles the table markup row by row.
*&—————————————————————*
*& Form BUILD_XLS_XSTRING
*& *** Re-added – must have been missing/truncated on import ***
*&—————————————————————*
FORM build_xls_xstring.
DATA: lv_string TYPE string.
CONCATENATE lv_string
‘<table border=”1″>’
‘<tr><th>Material</th><th>Type</th><th>Group</th><th>UoM</th>’
‘<th>Created On</th><th>Created By</th><th>Changed On</th><th>Status</th></tr>’
INTO lv_string.
LOOP AT gt_mat INTO gs_mat.
CONCATENATE lv_string
‘<tr><td>’ gs_mat-matnr ‘</td><td>’ gs_mat-mtart ‘</td><td>’ gs_mat-matkl
‘</td><td>’ gs_mat-meins ‘</td><td>’ gs_mat-ersda ‘</td><td>’ gs_mat-ernam
‘</td><td>’ gs_mat-laeda ‘</td><td>’ gs_mat-mstae ‘</td></tr>’
INTO lv_string.
ENDLOOP.
CONCATENATE lv_string ‘</table>’ INTO lv_string.
PERFORM string_to_xstring USING lv_string CHANGING gv_xstring.
ENDFORM.
Shared Helper: String to Binary Conversion
Both the TXT/CSV and XLS branches end by converting a plain ABAP string into binary Xstring, since email attachments require binary data rather than character data. This single routine keeps that conversion in one place.
*&—————————————————————*
*& Form STRING_TO_XSTRING
*&—————————————————————*
FORM string_to_xstring USING iv_string TYPE string
CHANGING cv_xstring TYPE xstring.
DATA: lo_conv TYPE REF TO cl_abap_conv_out_ce.
lo_conv = cl_abap_conv_out_ce=>create( encoding = ‘UTF-8’ ).
lo_conv->convert( EXPORTING data = iv_string
IMPORTING buffer = cv_xstring ).
ENDFORM.
XLSX: Real Excel Workbook via abap2xlsx
For a genuine .xlsx workbook rather than an HTML-table workaround, the report calls into the open-source abap2xlsx library. It creates a workbook, writes a header row, loops through the material data to populate the sheet, and then serializes the workbook to Xstring with the 2007 writer.
*&—————————————————————*
*& Form BUILD_XLSX_XSTRING
*& Uses abap2xlsx (ZCL_EXCEL). If not installed, replace with
*& CL_SALV_TABLE export instead.
*&—————————————————————*
FORM build_xlsx_xstring CHANGING cv_xstring TYPE xstring.
DATA: lo_excel TYPE REF TO zcl_excel,
lo_sheet TYPE REF TO zcl_excel_worksheet,
lo_writer TYPE REF TO zif_excel_writer,
lv_row TYPE i VALUE 1.
lo_excel = NEW zcl_excel( ).
lo_sheet = lo_excel->get_active_worksheet( ).
lo_sheet->set_title( ‘Material Data’ ).
lo_sheet->set_cell( ip_column = ‘A’ ip_row = 1 ip_value = ‘Material’ ).
lo_sheet->set_cell( ip_column = ‘B’ ip_row = 1 ip_value = ‘Type’ ).
lo_sheet->set_cell( ip_column = ‘C’ ip_row = 1 ip_value = ‘Group’ ).
lo_sheet->set_cell( ip_column = ‘D’ ip_row = 1 ip_value = ‘UoM’ ).
lo_sheet->set_cell( ip_column = ‘E’ ip_row = 1 ip_value = ‘Created On’ ).
lo_sheet->set_cell( ip_column = ‘F’ ip_row = 1 ip_value = ‘Created By’ ).
lo_sheet->set_cell( ip_column = ‘G’ ip_row = 1 ip_value = ‘Changed On’ ).
lo_sheet->set_cell( ip_column = ‘H’ ip_row = 1 ip_value = ‘Status’ ).
LOOP AT gt_mat INTO gs_mat.
lv_row = lv_row + 1.
lo_sheet->set_cell( ip_column = ‘A’ ip_row = lv_row ip_value = gs_mat-matnr ).
lo_sheet->set_cell( ip_column = ‘B’ ip_row = lv_row ip_value = gs_mat-mtart ).
lo_sheet->set_cell( ip_column = ‘C’ ip_row = lv_row ip_value = gs_mat-matkl ).
lo_sheet->set_cell( ip_column = ‘D’ ip_row = lv_row ip_value = gs_mat-meins ).
lo_sheet->set_cell( ip_column = ‘E’ ip_row = lv_row ip_value = gs_mat-ersda ).
lo_sheet->set_cell( ip_column = ‘F’ ip_row = lv_row ip_value = gs_mat-ernam ).
lo_sheet->set_cell( ip_column = ‘G’ ip_row = lv_row ip_value = gs_mat-laeda ).
lo_sheet->set_cell( ip_column = ‘H’ ip_row = lv_row ip_value = gs_mat-mstae ).
ENDLOOP.
CREATE OBJECT lo_writer TYPE zcl_excel_writer_2007.
cv_xstring = lo_writer->write_file( lo_excel ).
ENDFORM.
Email Dispatch
With the file content ready in gv xstring, this final routine builds the outgoing email using the BCS API: it converts the binary payload to a solix table, creates the document, attaches the file with the correct type and subject, sets the recipient, and sends immediately. Any BCS exception is caught and surfaced to the user as an error message.
*&—————————————————————*
*& Form SEND_EMAIL
*&—————————————————————*
FORM send_email.
DATA: lo_send_request TYPE REF TO cl_bcs,
lo_document TYPE REF TO cl_document_bcs,
lo_recipient TYPE REF TO if_recipient_bcs,
lt_attachment TYPE solix_tab,
lv_size TYPE so_obj_len,
lv_att_subject TYPE sood-objdes,
lv_att_type TYPE so_obj_tp,
lx_error TYPE REF TO cx_bcs.
TRY.
lo_send_request = cl_bcs=>create_persistent( ).
CASE abap_true.
WHEN p_txt. lv_att_type = ‘TXT’.
WHEN p_csv. lv_att_type = ‘CSV’.
WHEN p_xls. lv_att_type = ‘XLS’.
WHEN p_xlsx. lv_att_type = ‘XLSX’.
ENDCASE.
lv_size = xstrlen( gv_xstring ).
CALL FUNCTION ‘SCMS_XSTRING_TO_BINARY’
EXPORTING
buffer = gv_xstring
TABLES
binary_tab = lt_attachment.
lv_att_subject = gv_filename.
lo_document = cl_document_bcs=>create_document(
i_type = ‘RAW’
i_text = VALUE soli_tab( ( line = ‘Please find attached the material data extract.’ ) )
i_subject = ‘Material Data Extract’ ).
lo_document->add_attachment(
i_attachment_type = lv_att_type
i_attachment_subject = lv_att_subject
i_attachment_size = lv_size
i_att_content_hex = lt_attachment ).
lo_send_request->set_document( lo_document ).
lo_recipient = cl_cam_address_bcs=>create_internet_address( p_email ).
lo_send_request->add_recipient( lo_recipient ).
lo_send_request->set_send_immediately( abap_true ).
lo_send_request->send( ).
COMMIT WORK.
MESSAGE ‘Email sent successfully’ TYPE ‘S’.
CATCH cx_bcs INTO lx_error.
MESSAGE lx_error->get_text( ) TYPE ‘E’.
ENDTRY.
ENDFORM.
Conclusion:
Practical example of combining a selection screen, dynamic format handling, and SAP’s BCS email API into a single self-service reporting tool. It shows how a modest ABAP report — with a bit of format branching and a reusable conversion routine — can remove a recurring manual task from a business user’s plate. With a few refinements (email validation, filename uniqueness, and cleanup of legacy code), this pattern is easily reusable across other extract-and-email scenarios beyond material master data.
The Memory Scale Wall: Building massive strings in memory and instantiating large Excel workbooks via abap2xlsx is resource intensive. If a user runs a wide-open selection criteria covering hundreds of thousands of materials, the application server is highly likely to hit a TSV_TNEW_PAGE_ALLOC_FAILED memory allocation short dump. As a lesson learned from live deployments, you should always implement a hard ceiling constraint (e.g., checking Lines(gt_mat) and throwing an error if it exceeds 30,000 records) or force large queries to process via background batch routing.
The Security & Authorization Blindspot: By giving users a custom self-service report, you are effectively bypassing the standard, transparent visual authorization checks they would normally face inside standard transactions (like Se16n or MM60). To prevent data leaks or unauthorized data extraction via email, never assume the user has rights to the data. Always explicitly embed a defensive authority check (AUTHORITY-CHECK OBJECT ‘M_MATE_MAR’) right before your primary Select statement to guarantee your automated tool complies with corporate security governance.
“}]]
Read More Technology Blog Posts by Members articles
#abap