Recently found the Environment Check functionality in BAS, which really helped me to check destinations on different customer BAS instances, where I didn’t have direct access to the BTP instance. I’m just posting this here so I don’t forget.

SAP
Recently found the Environment Check functionality in BAS, which really helped me to check destinations on different customer BAS instances, where I didn’t have direct access to the BTP instance. I’m just posting this here so I don’t forget.
Report to import a ZIP file, containing the cofiles and data parts of a transport request. Related reports:
https://nocin.eu/abap-download-transport-as-zip/
https://nocin.eu/abap-create-toc-for-a-given-transport-release-it-and-download-it-as-zip/
This report can be handy, especially since S/4HANA 2023 seems to have restricted the “classic” import way by using TCode CG3Y and CG3Z. See note 1949906, where it is recommended to create a custom report.
*&---------------------------------------------------------------------*
*& Report Z_IMPORT_TRANSPORT_FROM_ZIP
*&---------------------------------------------------------------------*
*&
*&---------------------------------------------------------------------*
REPORT z_import_transport_from_zip.
SELECTION-SCREEN BEGIN OF BLOCK bl02 WITH FRAME TITLE TEXT-t02.
PARAMETERS p_lcldir TYPE string LOWER CASE OBLIGATORY DEFAULT 'C:\temp\'.
PARAMETERS p_sapdir TYPE char255 LOWER CASE OBLIGATORY.
SELECTION-SCREEN END OF BLOCK bl02.
SELECTION-SCREEN BEGIN OF BLOCK bl03 WITH FRAME.
PARAMETERS p_import TYPE boolean DEFAULT 'X' AS CHECKBOX.
SELECTION-SCREEN END OF BLOCK bl03.
AT SELECTION-SCREEN OUTPUT.
CALL 'C_SAPGPARAM' ID 'NAME' FIELD 'DIR_TRANS' ID 'VALUE' FIELD p_sapdir.
AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_lcldir.
DATA lt_files TYPE filetable.
DATA lv_rc TYPE i.
DATA lv_action TYPE i.
TRY.
cl_gui_frontend_services=>file_open_dialog( EXPORTING window_title = 'Import'
default_extension = '.zip'
initial_directory = 'C:\temp\'
multiselection = abap_false
CHANGING file_table = lt_files
rc = lv_rc
user_action = lv_action ).
IF lv_action = cl_gui_frontend_services=>action_ok AND lines( lt_files ) > 0.
p_lcldir = lt_files[ 1 ]-filename.
ENDIF.
CATCH cx_root INTO DATA(e_text).
MESSAGE e_text->get_text( ) TYPE 'I'.
ENDTRY.
START-OF-SELECTION.
DATA lv_zip_size TYPE i.
DATA lt_zip_data TYPE solix_tab.
DATA lv_xstring TYPE xstring.
cl_gui_frontend_services=>gui_upload( EXPORTING filename = p_lcldir
filetype = 'BIN'
IMPORTING filelength = lv_zip_size
CHANGING data_tab = lt_zip_data ).
CALL FUNCTION 'SCMS_BINARY_TO_XSTRING'
EXPORTING
input_length = lv_zip_size
IMPORTING
buffer = lv_xstring
TABLES
binary_tab = lt_zip_data.
" write zip content to filesystem
DATA(lo_zipper) = NEW cl_abap_zip( ).
DATA(lt_zip_entries) = lo_zipper->splice( lv_xstring ).
lo_zipper->load( lv_xstring ).
LOOP AT lt_zip_entries INTO DATA(ls_zip_entry).
lo_zipper->get( EXPORTING name = ls_zip_entry-name
IMPORTING content = DATA(lv_data) ).
DATA(lv_file) = COND #( WHEN ls_zip_entry-name(1) = 'K' THEN p_sapdir && '/cofiles/' && ls_zip_entry-name
ELSE p_sapdir && '/data/' && ls_zip_entry-name ).
TRY.
OPEN DATASET lv_file FOR OUTPUT IN BINARY MODE.
TRANSFER lv_data TO lv_file.
CLOSE DATASET lv_file.
CATCH cx_root INTO DATA(e_text).
MESSAGE e_text->get_text( ) TYPE 'E'.
ENDTRY.
ENDLOOP.
" now add transport to stms
DATA lv_ret_code TYPE trretcode.
DATA ls_exception TYPE stmscalert.
DATA lt_logptr TYPE TABLE OF tplogptr.
DATA lt_stdout TYPE TABLE OF tpstdout.
DATA(lv_trkorr) = CONV trkorr( lt_zip_entries[ 1 ]-name+8(3) && 'K' && lt_zip_entries[ 1 ]-name+1(6) ).
DATA(lv_system) = CONV tmssysnam( sy-sysid ).
SELECT SINGLE domnam FROM tmscsys INTO @DATA(lv_transport_domain).
" add transport to queue
CALL FUNCTION 'TMS_MGR_FORWARD_TR_REQUEST'
EXPORTING
iv_request = lv_trkorr
iv_tarcli = sy-mandt
iv_target = lv_system
iv_source = lv_system
iv_tardom = lv_transport_domain
iv_srcdom = lv_transport_domain
IMPORTING
ev_tp_ret_code = lv_ret_code
es_exception = ls_exception
TABLES
tt_stdout = lt_stdout
EXCEPTIONS
OTHERS = 99.
IF sy-subrc <> 0 OR lv_ret_code <> 0.
cl_demo_output=>display( lt_stdout ).
MESSAGE 'Could not add transport to queue' TYPE 'E'.
ENDIF.
" also directly import if checked
IF p_import = abap_true.
CALL FUNCTION 'TMS_MGR_IMPORT_TR_REQUEST'
EXPORTING
iv_system = lv_system
iv_request = lv_trkorr
iv_client = sy-mandt
iv_ignore_cvers = abap_true "ignore invalid component version vector (CVERS)
IMPORTING
ev_tp_ret_code = lv_ret_code
es_exception = ls_exception
TABLES
tt_logptr = lt_logptr
tt_stdout = lt_stdout
EXCEPTIONS
read_config_failed = 1
table_of_requests_is_empty = 2
OTHERS = 3.
IF sy-subrc <> 0 OR lv_ret_code <> 0.
cl_demo_output=>display( lt_stdout ).
MESSAGE 'Could not import transport' TYPE 'E'.
ENDIF.
ENDIF.
MESSAGE 'Transport successfully imported' TYPE 'S'.
If you have connected any workflows to a pernr via BUS1065 (like it is described here), you can receive all related workflows/workitems related to this pernr via the following code:
NEW cl_def_im_com_bsp_workflow( )->if_ex_com_bsp_workflow~read_workitems_for_object( EXPORTING iv_swo_objtype = 'BUS1065'
iv_swo_objkey = CONV #( lv_pernr )
IMPORTING et_workitems = DATA(lt_workitems) ).
LOOP AT lt_workitems ASSIGNING FIELD-SYMBOL(<workitems>).
"If you are only interested in specific workflows, you could filter here
"WHERE wi_rh_task = 'WSxxxxxxxx'
"AND wi_stat = 'STARTED'.
NEW /iwwrk/cl_wf_read_workitem( <workitems>-wi_id )->get_wi_container( IMPORTING et_wi_container = DATA(lt_wi_container) ).
" access container items via lt_wi_container[ element = 'IV_PERNR' ]-value
ENDLOOP.
Recently, I was confronted with the “Failed to load catalogs” error message when starting the Fiori Launchpad (/ui2/flp
) and navigating to the App Finder.
The system was quite new and the Fiori Customizing was not completely done yet. When opening the Dev Tools, I saw a failed request to
/sap/opu/odata/ui2/page_builder_pers/PageSets('%2FUI2%2FFiori2LaunchpadHome')
Opening this failed request in a new tab results in an HTTP 404 from an Apache web service. So it looked like an Apache was set up as reverse proxy in front of the sap system. I tried to call the PageSets endpoint without providing the key %2FUI2%2FFiori2LaunchpadHome
and got an HTTP 200. So in general, Apache was working, and the service endpoint page_builder_pers
was responding. Next, I did the same service calls from the gateway client and both calls were working fine. So it looked like Apache was the problem when providing the PageSets key. After a quick search, I found this post on Stack Overflow:
https://stackoverflow.com/questions/37834925/apache-reverse-proxy-blocking-sap-fiori-launchpad-url
So the issue was some (in this case) incorrect decoding of ‘%2F’. After contacting the basis team and adding the proposed Apache configs, the request finally resolved successfully, and the Launchpad was displaying some apps.
DATA(work_schedule) = cl_hcmfab_employee_api=>get_instance( )->get_work_schedule( iv_pernr = 1
iv_begda = sy-datum
iv_endda = sy-datum ).
cl_demo_output=>display_data( work_schedule ).
PA20 – Workflow-Overview
To attach your own workflow in this overview, you have to add the Task TS51900010 to your workflow and pass over the employee number to the Business Object “Employee” (BUS1065)
I was having a situation, where I needed to access file content via an association. This led to two problems, one in the backend and one in the frontend.
My data-model.cds looked like this.
entity MainEntity: cuid, managed {
file : Association to Files @mandatory @assert.target;
}
entity Files : cuid, managed {
content : LargeBinary @stream @Core.MediaType: mediaType @Core.ContentDisposition.Filename: fileName @Core.ContentDisposition.Type: 'inline';
mediaType : String @Core.IsMediaType: true;
fileName : String @mandatory;
size : Integer;
}
The file content is actually stored in an external system and is only read when the content is explicitly requested, with a call like this:
### Get file content
GET http://localhost:4004/odata/v4/admin/Files({{ID}})/content
Authorization: Basic user:password
For this kind of scenario, I have found the perfect sample code here: https://github.com/SAP-samples/cloud-cap-samples/blob/main/media/srv/media-service.js
But in my case, I needed to call the file content via an association like this:
### Get file content via association
GET http://localhost:4004/odata/v4/admin/MainEntity({{ID}})/file/content
Authorization: Basic user:password
This did not work, because in this case, we don’t get the required file ID in the Files
handler in req.data.ID
(find the reason here), which is needed to read the file from the external system. Therefore, I had to implement the following workaround (line 5-8), which checks from which entity we are coming and is fetching the requested file ID from the DB.
srv.on('READ', Files, async (req, next) => {
//if file content is requested, return only file as stream
if (req.context.req.url.includes('content')) {
// workaround: when File is requested via Association from MainEntity, as the ID is then not provided directly
if (req.context.req.url.includes('MainEntity')) {
req.data.ID = await SELECT.one.from(req.subject).columns('ID')
}
const file = await SELECT.from(Files, req.data.ID)
if (!file) return next() // if file not found, just handover to default handler to get 404 response
try {
const stream = await getMyStreamFromExternalSystem(req)
return [{ value: stream }]
} catch (err) {
req.error(`Could not read file content`)
}
} else return next() // else delegate to next/default handlers without file content
})
This way, the file content can now be read directly via File
and also via MainEntity
following the association.
The next challenge was to display this file content in a Fiori Elements app. This works out of the box, if the file content is called directly from the Files
entity, means not over an association. But if the file content is coming via an association, it seems like the Fiori Elements framework is creating an incorrect backend call. It tries to call the mediaType from the MainEntity
instead of the Files
entity, resulting in a failing odata call, which looks like this
/odata/v4/service/MainEntity(key)/mediaType
instead of /odata/v4/service/MainEntity(key)/file/mediaType
.
The only workaround I found was to overwrite the @Core.MediaType
annotation coming from the Files
entity by setting the mediaType to a hard value in the annotation.yaml of the Fiori Elements App.
annotate service.fileservice@(
UI.FieldGroup #FileGroup : {
$Type: 'UI.FieldGroupType',
Data : [
{
$Type: 'UI.DataField',
label: 'Main ID',
Value: ID,
},
{
$Type: 'UI.DataField',
label: 'File ID',
Value: file.ID,
},
{
$Type: 'UI.DataField',
Value: file.content,
},
{
$Type: 'UI.DataField',
Value: file.mediaType,
},
{
$Type: 'UI.DataField',
Value: file.fileName,
},
{
$Type: 'UI.DataField',
Value: file.size,
},
],
},
UI.Facets : [
{
$Type : 'UI.ReferenceFacet',
ID : 'GeneratedFacet2',
Label : 'File Information',
Target: '@UI.FieldGroup#FileGroup',
},
],
);
// Workaround as currently display file content via an association in Fiori Elements is incorrectly trying to fetch the media type.
// Therefore add a fix value for the media type. Of course, this only works, if you only expect a specific file type.
annotate service.Files with {
@Core.MediaType : 'application/pdf'
content
};
In the Fiori Elements App it will now be displayed like this and by clicking on the Context, it will successfully load the file from the backend:
While searching on how to validate a given JSON string, I found two options. The first simply returns a Boolean value, the second also returns information about what could be wrong.
DATA(lv_json) = '{'
&& '"employee": {'
&& |"name" : "Max", |
&& |"age" : 43, |
&& '}'
&& '}'.
" option 1:
DATA(is_valid) = /ui5/cl_json_util=>is_wellformed( lv_json ).
" option 2:
DATA(lo_reader) = cl_sxml_string_reader=>create( cl_abap_codepage=>convert_to( lv_json ) ).
TRY.
lo_reader->next_node( ).
lo_reader->skip_node( ).
CATCH cx_sxml_parse_error INTO DATA(lx_parse_error).
WRITE lx_parse_error->get_text( ).
ENDTRY.
There are many blogs describing how to create a QR-Code in the context of SAPscript or Smartforms (e.g. here, here, here and here). But I was looking for a way to generate a QR-Code and only receive the graphical data stream from it, without the need for any manual steps such a creation via SE73. During my search, I found these two notes, which contained all the information I needed:
Following my little test report:
PARAMETERS p_text TYPE string DEFAULT 'My QR-Code Content'.
DATA: lv_action TYPE i.
DATA: lv_filename TYPE string.
DATA: lv_fullpath TYPE string.
DATA: lv_path TYPE string.
TRY.
cl_rstx_barcode_renderer=>qr_code( EXPORTING i_module_size = 25 " Size of smallest module (in pixel, max: 32000)
i_barcode_text = p_text " Barcode text
* i_mode = 'A' " Mode ('N', 'A', 'L', 'B', 'K', 'U', '1', '2'; note 2030263)
* i_error_correction = 'H' " Error correction ('L', 'M', 'Q', 'H')
* i_rotation = 0 " Rotation (0, 90, 180 or 270)
IMPORTING e_bitmap = DATA(e_bitmap) ). " Bitmap in BMP format
DATA(lt_raw_data) = cl_bcs_convert=>xstring_to_solix( e_bitmap ).
" Save-Dialog
cl_gui_frontend_services=>file_save_dialog( EXPORTING default_file_name = 'QR-Code'
default_extension = 'bmp'
file_filter = |{ cl_gui_frontend_services=>filetype_all }|
CHANGING filename = lv_filename
path = lv_path
fullpath = lv_fullpath
user_action = lv_action ).
IF lv_action EQ cl_gui_frontend_services=>action_ok.
" Download file to disk
cl_gui_frontend_services=>gui_download( EXPORTING filename = lv_fullpath
filetype = 'BIN'
bin_filesize = xstrlen( e_bitmap )
CHANGING data_tab = lt_raw_data ).
ENDIF.
CATCH cx_rstx_barcode_renderer INTO DATA(lo_exp).
MESSAGE lo_exp->get_text( ) TYPE 'I'.
ENDTRY.
This is discussed for many years and unfortunately will not be implemented in the UI5 framework itself (see here). There are already different blogs describing how to build a wrapper for oData requests (for example here and here).
But with ES2024 it now got super simple to do this:
async function readData(model, entitySet) {
const [promise, resolve, reject] = Promise.withResolver( )
model.read(entitySet, {
success: data => resolve(data),
error: error => reject(error)
})
return promise
}
const user = await readData(oDataModel, "/user")