SAP S/4HANA Key User Extensibility powered by Embedded Steampunk: Custom Field with ABAP implemented Value Help

Do you want to know how you can enrich the key user custom fields feature set with developer extensibility means or did you ever wish to freely implement a custom fields value help based on ABAP coding?

If your answer is yes, then this blog post is just  for you.

This blog post covers the following topics:

  1. Create a custom entity in ADT
  2. Implement the custom entity logic using an ABAP class
  3. Create a custom field of type “Value Help based on CDS View” using the custom entity as value help

As this extension scenario is quite a complex one, we make it as tangible as possible and follow a concrete example, which is close to real world use cases we know from several SAP customers:

A customer uses several SAP S/4HANA systems (cloud and on premise) for several of his subsidiaries to manage his purchasing processes.
In addition to that, the customer implemented a central purchasing approver determination application within SAP Business Technology Platform (SAP BTP). Based on a large set of business rules, this kind of application identifies the approvers allowed to approve purchasing documents  based on a given company code. As identifying attribute (the logical key) for the approver this central application uses the email address of the approver.

In order to assure that all purchasing systems only use allowed approvers, the customer wants to introduce  the custom field “Allowed Approver” in each of the SAP S/4HANA systems in every affected purchasing document. That custom field only shows allowed approvers in the value help.
In this blog, we’ll focus solely on the aspect of creating a custom field of type “Value Help based on CDS View” for each of the purchasing systems where only value help values are offered that comply to the rules of the SAP BTP central approver determination application.

Extension%20use%20case

Extension use case

How to implement this scenario:

Implementation%20overview

Implementation overview

1. Implement Custom Entity in Embedded Steampunk

Within ADT (Embedded Steampunk – you are working in client 080), create a custom entity referencing a query provider class via the annotation @ObjectModel.query.implementedBy. The class implements the data retrieval of the value help entries by implementing the RAP query provider interface IF_RAP_QUERY_PROVIDER. The custom entity can be seen below:

@EndUserText.label: 'ApproverId Value Help Custom Entity'
@ObjectModel.dataCategory: #VALUE_HELP
@Search.searchable: true
@ObjectModel.query.implementedBy: 'ABAP:ZCL_APPROVERID_VHLP_SIMPLE'
define custom entity ZC_APPROVERID_VHLP_CE_SIMPLE { @Search: { defaultSearchElement: true, ranking: #HIGH, fuzzinessThreshold: 0.8 } @ObjectModel.text.element: ['Name'] key EMailAddress : zemailaddress; @Semantics.text: true @Search.defaultSearchElement: true Name : zbusiness_partner_full_name; CompanyCode : zcompany_code;
}

The projection list of the custom entity defines the fields of the value help:

  • The approver is identified with his email address; that is why this field is the key
  • The full name of the approver (concatenation of first and last name)
  • The company code of the company to which the approver belongs

Note that the values ‘EMailAddress’ and ‘Name’ have been annotated as ‘defaultSearchElement’, as they are the ones where the user wants to define searches on.

With the header annotation

@ObjectModel.query.implementedBy: ‘ABAP:ZCL_APPROVERID_VHLP_SIMPLE’

the class implementing the value help data retrieval is referenced.

Further information regarding unmanaged queries or custom entities can be found in the official documentation.

3. Release the custom entity for “Use in Key User Apps”

After you activated your custom entity and your query provider class, make sure that you make your custom entity available for key user tools.

Navigate to the ‘Properties’ tab of your custom entity and select the “API State” sub tab. Press the “Add Release Contract” button and choose the option “Use System-Internally (Contract C1)” as release contract. Set the release state to “Released” and the visibility to “Use in Key User Apps” and activate the custom entity afterwards.

Further information regarding the release contract C1 can be found in the official documentation.

3. Create and publish the custom field and try out the value help
With this we have finished all our development in ADT in the development tenant. We’ll leave ADT now and log into the SAP Fiori launchpad in the customizing tenant( client 100 ).
Open the app “Manage Purchase Orders” and select one of the purchase orders listed to navigate to the purchase order details screen. Then switch into the Adapt UI mode. If you click right within the basic data section, a popup menu appears where “Add: Field” needs to be selected (see screenshot below).

Add%20field

Add field

A dialog pops up showing the list of available fields. Click the “+” button on the top right corner of the screen to create new fields.  A new browser tab opens that displays the “Custom Fields” tab of the “Custom Fields” app. On the top right corner of this tab, click “+” once again  to create a custom field. A dialog appears that allows to specify a new custom field. After providing label, identifier, and tooltip, select the field type “Code List based on CDS View”. Open the value help to select the desired value help view.Provide a “Yes” for the custom entity filter.The previously created custom entity “ZC_APPROVERID_VHLP_SIMPLE_CE” should be available. Select this custom entity and publish the custom field afterwards by selecting “Create and Publish” (see screenshot below).

Choose%20Custom%20Entity%20as%20value%20help%20view

Choose Custom Entity as value help view

Further information regarding the creation of a custom field of type “Value Help based on CDS View” can be found in the official documentation.

After the custom field has been published, make the field visible in the UI within the basic data section once again using the “Add Field” option in Adapt UI. This time, the “Approver” field is offered in the list of fields within the dialog that is popping up. In addition to the “Approver” field, the “Approver (Desc.)” field can be found, too. This field provides the full name of the approver. Make both fields visible in the UI within Adapt UI and activate the UI changes. Now, restart the UI and navigate to the purchase order detail screen. Opening the value help of the approver field, your screen will look like this:

Valuer%20help%20at%20runtime

Valuer help at runtime

Now, you are done! Congratulations!

This blog focuses on SAP S/4HANA Cloud using Embedded Steampunk. We have some good news: this doesn’t only work in SAP S/4HANA Cloud using Embedded Steampunk starting with release 2208, but it also works in your SAP S/4HANA on premise system starting with release 2022.

Please note that in case of issues, you can debug the coding in your query provider class using ADT. You can debug not only in the development tenant (client  080), but also in the customizing tenant( client 100) directly starting from your SAP Fiori UI. How to do that is described in the blog post SAP S/4HANA Key User Extensibility powered by Embedded Steampunk: How to debug Key User Extensibility extensions (e.g. Cloud BAdI’s) in the customizing tenant( client 100 )“ blogpost.
After you’ve tested your extension successfully, you can now release your respective transport(s) in ADT in the Development Tenant. In the customizing tenant in the key user tool “Export Software Collection”, add the custom field to the respective software collection and export this software collection respectively. Afterwards, you can import both the transport and the software collection in your test system and finalize your tests there.

Please note as well that in order to be able to implement and test this extension scenario, you need a respective user in the development tenant and in the customizing tenant having at least the following business catalogs (within SAP S/4HANA Cloud):

User in the development tenant – the following business roles have to be assigned: SAP_BR_DEVELOPER, SAP_BR_PURCHASER

User in the customizing tenant – the following business roles have to be assigned:  SAP_BR_PURCHASER, SAP_CORE_BC_EXT_FLD, SAP_CORE_BC_EXT_FLEX

Feedback, Comments, or Questions:

You’re cordially invited to provide them in the comments section below. Also, please follow my profile to get updates when I publish more posts on these topics.
Also feel invited to post and answer questions  here (https://answers.sap.com/tags/270c4f37-c335-46e1-bfad-a256637d5e26) and read other posts on the topic (https://blogs.sap.com/tags/270c4f37-c335-46e1-bfad-a256637d5e26/).

I want to thank Yasmina van Rooij and Karsten Schaser( Karsten Schaser ) for their extremely  helpful contributions.

Appendix: Implementation details

For those of you who are interested in the details of the implementation of the query provider implementation we demonstrate the coding of this class. Please note that the coding alone is not sufficient. The HTTP communication to the remote system is based on a service consumption model and a communication scenario (including the respective outbound service) and a communication agreement which will not be explained here.

You can get information about these topics in the following tutorial: https://developers.sap.com/tutorials/abap-environment-create-service-consumption-model.html .

Please note that parts of the coding have been cleared in order to comply to privacy standards. These parts of the coding are replaced with an “add your data here” statement.

CLASS zcl_approverid_vhlp_simple DEFINITION PUBLIC FINAL CREATE PUBLIC . PUBLIC SECTION. INTERFACES if_rap_query_provider . PROTECTED SECTION. PRIVATE SECTION. TYPES ty_gt_value_help_entry TYPE STANDARD TABLE OF zc_approverid_vhlp_ce_simple WITH DEFAULT KEY. TYPES ty_gt_emailaddress_range TYPE RANGE OF zc_approverid_vhlp_ce_simple-EMailAddress. TYPES ty_gt_name_range TYPE RANGE OF zc_approverid_vhlp_ce_simple-Name. TYPES ty_gt_company_code_range TYPE RANGE OF zc_approverid_vhlp_ce_simple-CompanyCode. TYPES ty_gts_allowed_appr_emails TYPE SORTED TABLE OF zc_approverid_vhlp_ce_simple-EMailAddress WITH UNIQUE KEY table_line. METHODS determine_allowed_approvers IMPORTING it_emailaddress_range TYPE ty_gt_emailaddress_range it_name_range TYPE ty_gt_name_range it_company_code_range TYPE ty_gt_company_code_range io_paging TYPE REF TO if_rap_query_paging it_sort_elements TYPE if_rap_query_request=>tt_sort_elements iv_search_expression TYPE string RETURNING VALUE(rt_value_help_entries) TYPE ty_gt_value_help_entry. METHODS get_allowed_apprvs_via_http IMPORTING it_emailaddress_range TYPE ty_gt_emailaddress_range it_company_code_range TYPE ty_gt_company_code_range io_paging TYPE REF TO if_rap_query_paging it_sort_elements TYPE if_rap_query_request=>tt_sort_elements RETURNING VALUE(rt_allowed_approvers_emails) TYPE ty_gts_allowed_appr_emails. METHODS get_provided_ranges IMPORTING io_request TYPE REF TO if_rap_query_request EXPORTING et_emailaddress_range TYPE ty_gt_emailaddress_range et_name_range TYPE ty_gt_name_range et_company_code_range TYPE ty_gt_company_code_range RAISING cx_rap_query_prov_not_impl cx_rap_query_provider. METHODS process_descr_only_request IMPORTING it_emailaddress_range TYPE ty_gt_emailaddress_range io_response TYPE REF TO if_rap_query_response. METHODS is_descriptions_only_request IMPORTING it_emailaddress_range TYPE ty_gt_emailaddress_range it_name_range TYPE ty_gt_name_range it_company_code_range TYPE ty_gt_company_code_range RETURNING VALUE(rv_is_descr_only_request) TYPE abap_bool RAISING cx_rap_query_prov_not_impl cx_rap_query_provider. ENDCLASS. CLASS zcl_approverid_vhlp_simple IMPLEMENTATION. METHOD if_rap_query_provider~select. DATA lt_value_help_entries TYPE STANDARD TABLE OF zc_approverid_vhlp_ce_simple . DATA ls_value_help_entry TYPE zc_approverid_vhlp_ce_simple. DATA(lo_paging) = io_request->get_paging( ). DATA(lt_sort_elements) = io_request->get_sort_elements( ) . "io_request->get_requested_elements( ) --> could be used for optimizations DATA(lv_search_expression) = io_request->get_search_expression( )."Basic search term get_provided_ranges( EXPORTING io_request = io_request IMPORTING et_emailaddress_range = DATA(lt_emailaddress_range) et_name_range = DATA(lt_name_range) et_company_code_range = DATA(lt_companycode_range) ). IF is_descriptions_only_request( it_emailaddress_range = lt_emailaddress_range it_name_range = lt_name_range it_company_code_range = lt_companycode_range ). process_descr_only_request( it_emailaddress_range = lt_emailaddress_range io_response = io_response ). ELSE. if lt_companycode_range is not initial. lt_value_help_entries = determine_allowed_approvers( it_emailaddress_range = lt_emailaddress_range it_name_range = lt_name_range it_company_code_range = lt_companycode_range io_paging = lo_paging it_sort_elements = lt_sort_elements iv_search_expression = lv_search_expression ). endif. io_response->set_data( lt_value_help_entries ). io_response->set_total_number_of_records( lines( lt_value_help_entries ) ). ENDIF. **********************************************************************
* How to implement exception handling:
* "! @raising cx_rap_query_prov_not_impl | Should be raised if the provider lacks the ability to fulfill the request at hand
* "! in its current state of implementation.
* "! @raising cx_rap_query_provider | General failure. Must be raised if an error prevents successful query processing.
********************************************************************** ENDMETHOD. METHOD determine_allowed_approvers. TYPES ty_gts_email_address TYPE STANDARD TABLE OF zc_approverid_vhlp_ce_simple-emailaddress. DATA lt_allowed_approvers_emails TYPE ty_gts_email_address. DATA ls_value_help_entry TYPE zc_approverid_vhlp_ce_simple. **********************************************************************
* HTTP call to SAP BTP service for allowed approvers
********************************************************************** lt_allowed_approvers_emails = get_allowed_apprvs_via_http( it_emailaddress_range = it_emailaddress_range it_company_code_range = it_company_code_range io_paging = io_paging it_sort_elements = it_sort_elements )."Note: for simplicity reasons we do not respect name filtering and we do ignore the search expression
********************************************************************** IF lt_allowed_approvers_emails IS NOT INITIAL."enrich with name SELECT FROM zemployee_details FIELDS emailaddress, name FOR ALL ENTRIES IN @lt_allowed_approvers_emails WHERE emailaddress = @lt_allowed_approvers_emails-table_line INTO TABLE @DATA(lt_allowed_vhlp_entries) . LOOP AT lt_allowed_vhlp_entries REFERENCE INTO DATA(lr_allowed_vhlp_entry). MOVE-CORRESPONDING lr_allowed_vhlp_entry->* TO ls_value_help_entry. INSERT ls_value_help_entry INTO TABLE rt_value_help_entries. ENDLOOP. ENDIF. ENDMETHOD. METHOD get_provided_ranges. TRY. DATA(lt_ranges) = io_request->get_filter( )->get_as_ranges( ). LOOP AT lt_ranges REFERENCE INTO DATA(lr_range). CASE lr_range->name. WHEN 'EMAILADDRESS'. LOOP AT lr_range->range REFERENCE INTO DATA(lr_range_entry). INSERT VALUE #( sign = lr_range_entry->sign option = lr_range_entry->option low = CONV #( lr_range_entry->low ) high = CONV #( lr_range_entry->high ) ) INTO TABLE et_emailaddress_range. ENDLOOP. WHEN 'NAME'. LOOP AT lr_range->range REFERENCE INTO lr_range_entry. INSERT VALUE #( sign = lr_range_entry->sign option = lr_range_entry->option low = CONV #( lr_range_entry->low ) high = CONV #( lr_range_entry->high ) ) INTO TABLE et_name_range. ENDLOOP. WHEN 'COMPANYCODE'. LOOP AT lr_range->range REFERENCE INTO lr_range_entry. INSERT VALUE #( sign = lr_range_entry->sign option = lr_range_entry->option low = CONV #( lr_range_entry->low ) high = CONV #( lr_range_entry->high ) ) INTO TABLE et_company_code_range. ENDLOOP. ENDCASE. ENDLOOP. CATCH cx_rap_query_filter_no_range INTO DATA(lx_previous). "Exception handling needed - not implemented yet ENDTRY. ENDMETHOD. METHOD get_allowed_apprvs_via_http. DATA lt_allowed_approvers TYPE TABLE OF zzzi_allowed_approvers. TRY. " Create http client DATA(lo_destination) = cl_http_destination_provider=>create_by_comm_arrangement( comm_scenario = ‘add your data here’ service_id = ‘add your data here’ ). DATA(lo_http_client) = cl_web_http_client_manager=>create_by_http_destination( lo_destination ). DATA(lo_client_proxy) = cl_web_odata_client_factory=>create_v2_remote_proxy( iv_service_definition_name = ‘add your data here’ io_http_client = lo_http_client iv_relative_service_root = ‘add your data here’ ). " Navigate to the resource and create a request for the read operation DATA(lo_request) = lo_client_proxy->create_resource_for_entity_set( ‘add your data here’ )->create_request_for_read( ). " Create the filter DATA(lo_filter_factory) = lo_request->create_filter_factory( ). IF it_company_code_range IS NOT INITIAL. DATA(lo_company_code_filter) = lo_filter_factory->create_by_range( iv_property_path = 'COMPANYCODE' it_range = it_company_code_range ). IF it_emailaddress_range IS NOT INITIAL. DATA(lo_concatenated_filter) = lo_company_code_filter->and( lo_filter_factory->create_by_range( iv_property_path = 'EMAIL' it_range = it_emailaddress_range ) ). lo_request->set_filter( lo_concatenated_filter ). ELSE. lo_request->set_filter( lo_company_code_filter ). ENDIF. ENDIF. lo_request->set_top( io_paging->get_page_size( ) )->set_skip( io_paging->get_offset( ) ). DATA lt_sort_order TYPE /iwbep/if_cp_runtime_types=>ty_t_sort_order. LOOP AT it_sort_elements REFERENCE INTO DATA(lr_sort_element). IF lr_sort_element->element_name IS NOT INITIAL. IF lr_sort_element->element_name = 'EMAILADDRESS'. DATA(lv_property_path) = 'EMAIL'. ELSE. lv_property_path = lr_sort_element->element_name. ENDIF. INSERT VALUE #( property_path = CONV #( lv_property_path ) descending = lr_sort_element->descending ) INTO TABLE lt_sort_order. ENDIF. ENDLOOP. lo_request->set_orderby( CONV #( lt_sort_order ) ). " Execute the request and retrieve the business data DATA(lo_response) = lo_request->execute( ). lo_response->get_business_data( IMPORTING et_business_data = lt_allowed_approvers ). LOOP AT lt_allowed_approvers REFERENCE INTO data(lr_allowed_approver). INSERT lr_allowed_approver->Email INTO TABLE rt_allowed_approvers_emails. ENDLOOP. CATCH /iwbep/cx_cp_remote INTO DATA(lx_remote). " Handle remote Exception " It contains details about the problems of your http(s) connection CATCH /iwbep/cx_gateway INTO DATA(lx_gateway). " Handle Exception CATCH cx_web_http_client_error INTO DATA(lx_http_client_error). "handle exception CATCH cx_http_dest_provider_error INTO DATA(lx_http_dest_provider_error). "handle exception ENDTRY. ENDMETHOD. METHOD is_descriptions_only_request. rv_is_descr_only_request = abap_false. IF it_emailaddress_range IS NOT INITIAL AND it_name_range IS INITIAL. rv_is_descr_only_request = abap_true. ENDIF. ENDMETHOD. METHOD process_descr_only_request. DATA lt_value_help_entries TYPE STANDARD TABLE OF zc_approverid_vhlp_ce_simple . DATA ls_value_help_entry TYPE zc_approverid_vhlp_ce_simple. SELECT FROM zemployee_details FIELDS emailaddress, name WHERE emailaddress IN @it_emailaddress_range INTO TABLE @DATA(lt_emails_with_names) . LOOP AT lt_emails_with_names REFERENCE INTO DATA(lr_email_with_name). MOVE-CORRESPONDING lr_email_with_name->* TO ls_value_help_entry. INSERT ls_value_help_entry INTO TABLE lt_value_help_entries. ENDLOOP. io_response->set_data( lt_value_help_entries ). io_response->set_total_number_of_records( lines( lt_value_help_entries ) ). ENDMETHOD. ENDCLASS.

You can see that the class implements the interface if_rap_query_provider which specifies the select method in which the data retrieval for the value help is implemented:

  1. The filter values are extracted and transformed into range tables by evaluating filter information provided by the importing parameter io_request within the private method get_provided_ranges (shown below)
  2. Based on the company code field, we then determine the allowed approvers.
    This is implemented in a private method. We don’t dive into the implementation of this method, but basically, a call to the remote BTP system is executed here. Within this call, the filter value of the company code is transferred to the allowed approver determination service, which returns the list of the email addresses of these allowed approvers. Afterwards, based on a select on the SAP standard CDS View ‘I_BusinessUserBasic’, the full name of the approver with the respective email address is added. Finally, the list of approvers with their email addresses and full names is returned. The details of how the HTTP request is created and triggered is not shown and explained here.
  3. The resulting data is set on the io_response The number of records is set on the response object, too.