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 ABAP Development Tools (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 their 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 their 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.

2. 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. Choose “Yes” in the dropdown for the custom entity filter. The custom entity “ZC_APPROVERID_VHLP_SIMPLE_CE” you’ve created previously 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

Value 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 that starts 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

If you interested in the details how to implement a Custom Entity I recommend the blog post “How to implement a custom entity in the ABAP RESTful Programming Model using remote function modules”   from Andre Fischer .

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( @yasmina.vanrooij ) and Karsten Schaser (Karsten Schaser) for their extremely helpful contributions.

Other related blog posts:

SAP S/4HANA Key User Extensibility: Custom field with context-dependent value help

SAP S/4HANA Key User Extensibility powered by Embedded Steampunk: How to debug Key User Extensibility extensions (e.g. Cloud BAdI’s) in client 100

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 above)
  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.
Sara Sampaio

Sara Sampaio

Author Since: March 10, 2022

0 0 votes
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x