Skip to content

Lesson 23 — Final Project: S/4HANA Side-by-Side Stock Check Extension ​

Guided learning: 3 hours
Independent project: 3 hours (see separate Practice)
Environment: SAP BTP ABAP Trial + Eclipse ADT
Prerequisites: An SAP BTP ABAP Trial project in Eclipse ADT, permission to create repository objects, and basic ABAP/RAP familiarity. No previously created Service Consumption Model or console class is needed. This guide walks you through creating both. The remote portion additionally requires a BTP Trial tenant where the shared SAP S/4HANA demo communication scenario ZBTP_TRIAL_SAP_COM_0309 has been provisioned.

This is the final hands-on project. You will build an application that records stock checks locally in BTP and lets users select actual S/4HANA product IDs. You will not change master data in S/4HANA: the connection to S/4HANA is read-only.

1. What is the business application? ​

Warehouse staff need to record occasional spot checks. Each check has a product, warehouse, counted quantity, status and notes. The check records are our application's data; the product IDs come from S/4HANA.

text
Fiori Elements Stock Check app (on SAP BTP)
      |                              |
      | Save local checks            | Product value help
      v                              v
Managed RAP BO                CDS Custom Entity
      |                              |
      v                              v
Your own BTP table       IF_RAP_QUERY_PROVIDER class
                                     |
                                     v
                            OData V2 Client Proxy
                                     |
                                     v
                            S/4HANA Product API

Why is this an extension? Your app adds a new stock-check process in BTP, without modifying the S/4HANA product master. SAP's published API supplies the product choices. Users can create and update their own stock-check entries locally.

Definition of done ​

FeatureDemonstration
Local stock checksCreate and save a stock-check record in Fiori Elements
Product ID fieldStores a product ID with 40-character capacity
Remote value helpSelect a product returned by S/4HANA, not a hard-coded list
Search resultsShow Product, Product Type and Product Group
Safe data flowChanges update only the BTP stock-check table
DiagnosticsDistinguish broken remote connection from a local RAP save issue

What this project will not do: update actual inventory quantities in S/4HANA. CountedQty is an observation stored in BTP, not an official S/4HANA stock posting.

2. Initial checks and naming rules ​

Name your own objects using your initials ​

Every ## in this handout is a placeholder for your TWO uppercase initials. For example, Alex Brown uses AB: ZPKG_L23_## becomes ZPKG_L23_AB and ZSC_L23_PROD_## becomes ZSC_L23_PROD_AB. Replace ## consistently in all ABAP and CDS source code, object names, @ObjectModel.query.implementedBy, and string-based proxy model keys before activation. Do not include literal # characters in an object name.

If two people share the same initials, coordinate a unique permitted two-character suffix with your instructor. Names shown below are student objects; the SAP-defined API name and shared BTP communication scenario must stay unchanged.

Object to createName with student placeholder
Development packageZPKG_L23_##
Local stock-check tableZL23_STOCK_##
OData Service Consumption Model and generated classZSC_L23_PROD_##
Connection test classZCL_L23_API_TEST_##
Query-provider classZCL_L23_PRODUCTS_##
Custom CDS entityZCE_L23_PROD_##
Query exception classZCX_L23_QUERY_##

What is external and cannot be created by this exercise? ​

The shared backend communication scenario ZBTP_TRIAL_SAP_COM_0309 is SAP/tenant-provided configuration. It was observed working in one ABAP Trial setup, but not every tenant is guaranteed to have it. You will create your own SCM from the EDMX, but you cannot invent the remote endpoint or credentials if the communication arrangement is absent.

You will first build and save stock checks locally. If the remote scenario is unavailable, you can still complete that milestone and inspect the rest of the design, but you must mark remote product value help as not verified.

3. Create a new package for your final project ​

  1. In Eclipse, open Window → Perspective → Open Perspective → Other… → ABAP, if necessary.
  2. In Project Explorer, expand your SAP BTP ABAP Trial project.
  3. Choose a writable package area within your allowed software component (often ZLOCAL).
  4. Right-click the package area → New → ABAP Package. If the menu differs, use New → Other ABAP Repository Object… → ABAP Package.
  5. Name your package ZPKG_L23_##, description Side by side stock check project.
  6. Choose the transport/package settings offered by your Trial system and click Finish.

Replace every ## placeholder with your two uppercase initials in Eclipse before creating or activating anything. Do not change the shared SAP scenario name.

4. Create the local database table ​

We create only one new persistence table. The RAP generator will create the transactional BO and Fiori service artifacts, saving us a large amount of repetitive setup.

Step 4.1 — Create the table repository object ​

  1. Right-click ZPKG_L23_##.
  2. Select New → Other ABAP Repository Object….
  3. Search for Database Table, select it, and click Next.
  4. Name: ZL23_STOCK_##; Description: Local warehouse stock checks.
  5. Complete the wizard and replace the generated table definition with the full source below.

Step 4.2 — Paste the complete table definition ​

abap
@EndUserText.label : 'Warehouse Stock Check'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zl23_stock_## {

  key client               : abap.clnt not null;
  key check_uuid           : sysuuid_x16 not null;

  product_id               : abap.char(40);
  warehouse                : abap.char(10);
  counted_qty              : abap.int4;
  check_status             : abap.char(1);
  notes                    : abap.char(120);

  local_created_by         : abp_creation_user;
  local_created_at         : abp_creation_tstmpl;
  local_last_changed_by    : abp_locinst_lastchange_user;
  local_last_changed_at    : abp_locinst_lastchange_tstmpl;
  last_changed_at          : abp_lastchange_tstmpl;
}

Press Ctrl+S, then Ctrl+F3 to activate. Before activation, replace the table name suffix with your initials.

CheckUuid is the technical RAP key. Product ID is a business field, not the primary key. The administrative fields support the generated RAP behavior, draft and concurrency handling. We will not write low-level SQL modifications to this table.

5. Generate a complete managed RAP/Fiori starter application ​

This is the fastest supported way to build the familiar RAP layers. Instead of copying a BDEF that may not match the generated artifacts, use the actual ADT generator.

Step 5.1 — Start the generator ​

  1. In Project Explorer, right-click database table ZL23_STOCK_## (not the package).
  2. Select Generate ABAP Repository Objects….
  3. Choose OData UI Service in the Cloud generator (or the longer ABAP RESTful Application Programming Model: UI Service label shown by some ADT versions).
  4. Click Next and verify package ZPKG_L23_##.
  5. On Configure Generator, leave Artifacts Prefix and Artifacts Suffix blank if you are entering complete names. Expand Business Object → Data Model, Business Object → Behavior, Service Projection, and Business Service, then enter the names below. Some ADT versions organize the tree slightly differently.

Step 5.2 — Enter the generator names ​

Generator fieldUse this example name
Root data definitionZI_L23_STOCK_##
Entity aliasStockCheck
Behavior implementation classZBP_I_L23_STOCK_##
Draft tableZL23_STKD_##
CDS projectionZC_L23_STOCK_##
Projection behavior implementation class (if offered)ZBP_C_L23_STOCK_##
Service definitionZUI_L23_STOCK_##
Service bindingZUI_L23_STOCK_O4_##
Service binding typeOData V4 – UI

The generator may propose slightly different defaults or require names in a different order. Check every generated name on the review page before clicking Finish. SAP's current generator produces a draft-enabled managed RAP stack; some releases expose fewer configurable fields. Record the actual names and do not mix them with these examples. If ADT reports a conflicting or invalid object name, choose a valid unique name and carry it consistently through later steps.

Step 5.3 — Generate and activate ​

  1. Click Next to review objects.
  2. Choose a transport if required, then click Finish.
  3. Expand your package; you should now see a root data model, base behavior, behavior implementation, projection, service definition and service binding, plus a draft table.
  4. Activate any inactive objects; address generator errors before proceeding.
  5. Open the Service Binding ZUI_L23_STOCK_O4_## (or the name actually generated).
  6. Click Publish if the editor shows the service as unpublished.
  7. Select the StockCheck entity and choose Preview to open the generated Fiori Elements application.

Step 5.4 — Verify local RAP without S/4HANA ​

In the Fiori preview:

  1. Click Create.
  2. Fill ProductId = A001, Warehouse = WH01, CountedQty = 8, CheckStatus = N, and Notes = First guided check (use the field labels your generator exposes).
  3. Save/activate the draft using the UI's Create/Save action as applicable.
  4. Return to the list. Confirm you can see the check you just created.

At this stage A001 is manually typed. We have verified local RAP persistence, not yet remote product selection. If the generator labels the field differently, inspect the generated CDS projection: we will need the ProductId field name next.

Checkpoint: Your local stock-check app works before you introduce the external connection.

6. Create the S/4HANA connection and SCM within this project ​

You now have a working local Stock Check app. The next steps are completely self-contained: you will download the Product OData metadata, create a new SCM in your package, inspect the generated ABAP class, and run a test console. There is no need to open or reuse objects from any previous lesson.

Step 6.1 — Confirm the tenant-provided communication arrangement exists ​

The shared training scenario is ZBTP_TRIAL_SAP_COM_0309. It is used by the provided SAP BTP ABAP Trial configuration for a read-only S/4HANA Product API. This scenario is not a student-created naming placeholder.

You normally cannot browse a complete communication-arrangement setup from an ordinary ABAP class editor. The easiest check in this environment is to run the API test class in Step 6.5. If the destination lookup fails, your BTP Trial tenant may not provide this arrangement; ask for an approved connection rather than making up a URL or password.

Step 6.2 — Download the Product API EDMX from SAP ​

  1. Open your browser and go to SAP Business Accelerator Hub — Product Master Data Including Classification (Read).
  2. On that API page, find API Specification. The layout and login prompts may vary.
  3. Download the OData EDMX specification and save it to your computer as API_CLFN_PRODUCT_SRV.edmx (or keep the downloaded .edmx filename).
  4. Verify that the saved file ends in .edmx and contains XML metadata. It must not be a JSON payload, PDF, or shortcut to the Hub page.

This EDMX describes the API's entities and fields. It contains no live product records. Actual records arrive only when your ABAP class sends a request.

Step 6.3 — Create a new Service Consumption Model in Eclipse ADT ​

  1. In Eclipse Project Explorer, right-click your ZPKG_L23_## package.
  2. Choose New → Other ABAP Repository Object….
  3. Under Business Services, select Service Consumption Model, then Next.
  4. Set Name to ZSC_L23_PROD_## (after replacing ##) and Description to Product API for Stock Check.
  5. Choose Remote Consumption Mode = OData, then Next.
  6. At Service Metadata File, click Browse… and select your downloaded API_CLFN_PRODUCT_SRV.edmx.
  7. If the wizard asks for a generated class name, use ZSC_L23_PROD_## where allowed. Record the actual generated class name from your wizard, even if it differs.
  8. Proceed to Components of OData Service, locate the Product entity set A_ClfnProduct and ensure it is included. Keep the appropriate generated Product entity/type components selected.
  9. If the wizard shows an ETag Support page, keep the defaults. This is a read-only exercise.
  10. Review the object-generation summary and select a transport request if prompted. Click Finish.
  11. Refresh the package using F5. Open the SCM and activate any inactive generated objects using Ctrl+F3 (or Ctrl+Shift+F3 when activating related inactive objects together).

ADT version note: Newer Service Consumption Model wizards may display the generated class name and component selection on slightly different pages. Always use the values shown in your actual model editor.

Step 6.4 — Record your SCM's Model Passport ​

  1. Double-click your Service Consumption Model.
  2. Select the A_ClfnProduct entity set and its Read List example.
  3. Copy the generated sample to a temporary text file, and find create_v2_remote_proxy, is_proxy_model_key, the product entity-set ID, and the generated row type.
  4. Open the generated ABAP class in Project Explorer; locate its public TYPES declarations.

For the Product EDMX previously tested in BTP Trial, these were the values:

Model propertyExample (check your own SCM)
repository_idDEFAULT
proxy_model_idZSC_L23_PROD_##
proxy_model_version0001
create_resource_for_entity_setA_CLFN_PRODUCT
Generated ABAP row typeZSC_L23_PROD_##=>TYS_A_CLFN_PRODUCT_TYPE
Useful ABAP fieldsproduct, product_type, product_group, base_unit

Replace example class, type, model ID, version and entity-set ID with the actual generated identifiers if yours differ. The remote EDMX entity name A_ClfnProduct and ABAP proxy identifier A_CLFN_PRODUCT are related but are not spelled identically.

Step 6.5 — Create and run the complete connection test class ​

  1. Right-click your package → New → ABAP Class (or New → Other ABAP Repository Object… → ABAP Class).
  2. Enter name ZCL_L23_API_TEST_## and description Test S4HANA Product API.
  3. Click Finish and replace the entire global class source with the code below.
  4. Perform Replace All for ## using your two initials.
abap
CLASS zcl_l23_api_test_## DEFINITION
  PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES if_oo_adt_classrun.
ENDCLASS.

CLASS zcl_l23_api_test_## IMPLEMENTATION.
  METHOD if_oo_adt_classrun~main.

    DATA lt_products TYPE STANDARD TABLE OF
      zsc_l23_prod_##=>tys_a_clfn_product_type
      WITH EMPTY KEY.

    TRY.
        DATA(lo_destination) =
          cl_http_destination_provider=>create_by_comm_arrangement(
            comm_scenario = 'ZBTP_TRIAL_SAP_COM_0309' ).

        DATA(lo_http_client) =
          cl_web_http_client_manager=>create_by_http_destination(
            i_destination = lo_destination ).

        DATA(lo_proxy) =
          /iwbep/cl_cp_factory_remote=>create_v2_remote_proxy(
            is_proxy_model_key = VALUE #(
              repository_id       = 'DEFAULT'
              proxy_model_id      = 'ZSC_L23_PROD_##'
              proxy_model_version = '0001' )
            io_http_client           = lo_http_client
            iv_relative_service_root = '' ).

        "The communication-arrangement destination already points to
        "the Product service root: no second service path is needed.
        DATA(lo_read) =
          lo_proxy->create_resource_for_entity_set(
            'A_CLFN_PRODUCT' )->create_request_for_read( ).

        lo_read->set_top( 5 ).
        DATA(lo_response) = lo_read->execute( ).
        lo_response->get_business_data(
          IMPORTING et_business_data = lt_products ).

        out->write( |Remote products: { lines( lt_products ) }| ).
        LOOP AT lt_products INTO DATA(ls_product).
          out->write(
            |{ ls_product-product } / | &&
            |{ ls_product-product_type } / | &&
            |{ ls_product-product_group }| ).
        ENDLOOP.

      CATCH cx_root INTO DATA(lx_error).
        out->write( |Remote API error: { lx_error->get_text( ) }| ).
    ENDTRY.
  ENDMETHOD.
ENDCLASS.
  1. Save with Ctrl+S and activate with Ctrl+F3.
  2. With the class editor active, press F9 or choose Run As → ABAP Application (Console).
  3. Inspect the ABAP Console. A working shared Trial test previously returned products such as A001 and B001, but your tenant may have different data and order.

Checkpoint: A nonempty list of product IDs proves your own SCM and tenant's remote connectivity work. If you receive an error, read it before changing code: a missing communication arrangement, an incorrect model version, and an incorrect service root are different problems.

Stop point for remote prerequisites: If this step fails because ZBTP_TRIAL_SAP_COM_0309 is absent, the remaining Fiori integration cannot be verified in that tenant. Continue to demonstrate the already-working local Stock Check app, and report the remote result as not executed / blocked, rather than hard-coding authentication or pretending manual data came from S/4HANA.

7. Create a CDS Custom Entity for remote products ​

A CDS custom entity does not directly select from a database table. Instead it delegates requests to an ABAP class that implements IF_RAP_QUERY_PROVIDER. This is a supported way to expose data provided by another OData API. https://help.sap.com/docs/abap-cloud/abap-rap/using-cds-custom-entity-for-data-modeling

Step 7.1 — Create the exception and query classes ​

  1. Create the exception class ZCX_L23_QUERY_## using the complete source in Section 8.1, then activate it.
  2. Right-click package ZPKG_L23_## → New → ABAP Class.
  3. Name ZCL_L23_PRODUCTS_##; description S4 remote products query provider.
  4. Paste the complete class in Section 8.2 and replace ## with your initials.
  5. Activate the query class after checking your Model Passport from Section 6.

Step 7.2 — Create a Data Definition ​

  1. Right-click the package → New → Other ABAP Repository Object….
  2. Search for Data Definition under Core Data Services.
  3. Enter name ZCE_L23_PROD_## and description Products from S4HANA.
  4. Click Next through the transport screen to select the template.
  5. Choose Define Custom Entity with Parameters, then Finish. If your ADT offers a parameterless custom-entity template, that works too.
  6. Replace the entire generated source, including removing the with parameters ... placeholder.

Step 7.3 — Paste the complete custom entity ​

abap
@EndUserText.label: 'Remote S4HANA Products'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_L23_PRODUCTS_##'
define custom entity ZCE_L23_PROD_##
{
  @EndUserText.label: 'Product'
  @UI.lineItem: [{ position: 10 }]
  key Product          : abap.char(40);

  @EndUserText.label: 'Product Type'
  @UI.lineItem: [{ position: 20 }]
  ProductTypeName      : abap.char(4);

  @EndUserText.label: 'Product Group'
  @UI.lineItem: [{ position: 30 }]
  ProductGroupName     : abap.char(9);
}

The query class name in @ObjectModel.query.implementedBy must match your ABAP class. The Product field is CHAR(40) to match the key length published by the remote service. We deliberately use ProductTypeName and ProductGroupName to distinguish CDS field names from the remote OData property names.

Activation order: The query class implements the required interface and supplies fields with the same names/types as this entity. Create and activate the exception class in Section 8.1, then activate the query class and the CDS custom entity. This avoids a circular dependency. A temporary inactive reference while the class is not yet implemented is normal.

8. Complete query-provider ABAP class ​

This class receives a Fiori query, delegates it to the OData client proxy you created in Section 6, and returns matching data to Fiori. It includes count, paging, and sorting of supported properties, as well as filters for the displayed fields. These are important: a value-help dialog is not just a single GET request.

Step 8.1 — Required exception class ​

To ensure remote failures are reported as failures rather than silently shown as an empty list, first create a dedicated exception type. This uses the standard ABAP Class wizard, setting a suitable exception superclass.

  1. Right-click package → New → ABAP Class.
  2. Enter name ZCX_L23_QUERY_## and description RAP Product Query Error. Click Next.
  3. Where ADT requests Superclass, enter CX_RAP_QUERY_PROVIDER; this is what makes the new global class a RAP query-provider exception subclass. Complete the transport wizard.
  4. Replace its source with:
abap
CLASS zcx_l23_query_## DEFINITION
  PUBLIC
  INHERITING FROM cx_rap_query_provider
  FINAL
  CREATE PUBLIC.
ENDCLASS.

CLASS zcx_l23_query_## IMPLEMENTATION.
ENDCLASS.

Activate it. This lets the query provider report unexpected API failures to the RAP runtime instead of pretending that no product exists.

Step 8.2 — Replace the full source of ZCL_L23_PRODUCTS_## ​

abap
CLASS zcl_l23_products_## DEFINITION
  PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    " RAP calls this interface when the custom entity is queried.
    " The connection to this class is declared in the custom entity's
    " @ObjectModel.query.implementedBy annotation.
    INTERFACES if_rap_query_provider.
ENDCLASS.

CLASS zcl_l23_products_## IMPLEMENTATION.

  METHOD if_rap_query_provider~select.

    " io_request describes what Fiori asks our local service to return.
    " io_response is where we give RAP the data and count for Fiori.
    " lo_read and lo_response below belong to the REMOTE API call.
    " This method reads products; it does not save Stock Check records.

    " 1. Prepare two data shapes: remote API rows and local CDS rows.
    " The SCM generated this ABAP row type from the Product EDMX.
    " A table of this type holds products returned by the client proxy.
    TYPES ty_remote_rows TYPE STANDARD TABLE OF
      zsc_l23_prod_##=>tys_a_clfn_product_type
      WITH EMPTY KEY.

    " Our custom entity exposes only these three fields. Their names
    " and lengths must match ZCE_L23_PROD_##, not the remote row type.
    TYPES: BEGIN OF ty_result,
             Product          TYPE c LENGTH 40,
             ProductTypeName  TYPE c LENGTH 4,
             ProductGroupName TYPE c LENGTH 9,
           END OF ty_result.
    TYPES ty_result_rows TYPE STANDARD TABLE OF ty_result
      WITH EMPTY KEY.

    " lt_remote receives API data; lt_result holds the mapped CDS data.
    DATA lt_remote TYPE ty_remote_rows.
    DATA lt_result TYPE ty_result_rows.

    TRY.
        " 2. Inspect the query options supplied by Fiori.
        " Free-text search ($search) differs from field filters.
        " This example supports field filters, but rejects free-text
        " search so it cannot accidentally return unfiltered results.
        DATA(lv_search) = io_request->get_search_expression( ).
        IF lv_search IS NOT INITIAL.
          RAISE EXCEPTION TYPE zcx_l23_query_##.
        ENDIF.

        " Requested elements are the columns Fiori wants ($select).
        " We read them for inspection, but do not use lt_requested to
        " restrict the result: this example returns all three fields.
        DATA(lt_requested) = io_request->get_requested_elements( ).
        " Sorting specifies which fields to sort by and each direction.
        DATA(lt_sorts) = io_request->get_sort_elements( ).

        " 3. Prepare access to the remote Product API.
        " The communication arrangement supplies the destination and
        " authentication settings; we do not embed credentials here.
        DATA(lo_destination) =
          cl_http_destination_provider=>create_by_comm_arrangement(
            comm_scenario = 'ZBTP_TRIAL_SAP_COM_0309' ).

        " The HTTP client will carry requests to that destination.
        DATA(lo_http_client) =
          cl_web_http_client_manager=>create_by_http_destination(
            i_destination = lo_destination ).

        " The OData V2 proxy uses our generated SCM to translate ABAP
        " requests and response data to/from the remote OData format.
        " VALUE #( ... ) builds the model-key structure. Its fields
        " identify the repository, model and model version from the SCM.
        " '0001' is the model version, not the OData protocol version.
        " For this Trial destination the service root is already set,
        " so iv_relative_service_root adds no extra path.
        DATA(lo_proxy) =
          /iwbep/cl_cp_factory_remote=>create_v2_remote_proxy(
            is_proxy_model_key = VALUE #(
              repository_id       = 'DEFAULT'
              proxy_model_id      = 'ZSC_L23_PROD_##'
              proxy_model_version = '0001' )
            io_http_client           = lo_http_client
            iv_relative_service_root = '' ).

        " Select the Product collection using its generated proxy ID
        " and prepare a read-list request. We configure it below;
        " execute( ) later performs the remote product read.
        DATA(lo_read) =
          lo_proxy->create_resource_for_entity_set(
            'A_CLFN_PRODUCT' )->create_request_for_read( ).

        " 4. Translate local field filters into remote filter nodes.
        " Each entry has a field name and a range table containing
        " SIGN, OPTION, LOW and HIGH (for example I / EQ / SERV).
        " I means include; EQ means equal; LOW holds the value SERV.
        " HIGH holds the upper limit when the condition is an interval.
        " Filters that cannot be expressed as ranges raise an exception,
        " handled at the end of this method.
        DATA(lt_ranges) = io_request->get_filter( )->get_as_ranges( ).
        " The factory creates filter objects understood by the proxy.
        DATA(lo_factory) = lo_read->create_filter_factory( ).
        " lo_root will hold the combined filter. Initially it is unbound.
        DATA lo_root TYPE REF TO /iwbep/if_cp_filter_node.

        LOOP AT lt_ranges INTO DATA(ls_filter).
          " RAP supplies uppercase local field names. SWITCH maps them
          " to ABAP proxy property paths, not remote OData URL names.
          " For example: ProductTypeName -> PRODUCT_TYPE -> ProductType.
          DATA(lv_property) = SWITCH string( ls_filter-name
            WHEN 'PRODUCT'          THEN 'PRODUCT'
            WHEN 'PRODUCTTYPENAME'  THEN 'PRODUCT_TYPE'
            WHEN 'PRODUCTGROUPNAME' THEN 'PRODUCT_GROUP'
            ELSE '' ).

          " An unknown field must fail rather than have its filter ignored.
          IF lv_property IS INITIAL.
            RAISE EXCEPTION TYPE zcx_l23_query_##.
          ENDIF.

          " Turn all range conditions for this field into one filter node.
          DATA(lo_node) = lo_factory->create_by_range(
            iv_property_path = lv_property
            it_range         = ls_filter-range ).

          " The first field starts the filter; each later field is ANDed.
          " Example: Product = 'A001' AND ProductType = 'SERV'.
          IF lo_root IS NOT BOUND.
            lo_root = lo_node.
          ELSE.
            lo_root = lo_root->and( lo_node ).
          ENDIF.
        ENDLOOP.

        " Attach the filter only if one was built. With no field filters,
        " the request reads the collection using the paging below.
        IF lo_root IS BOUND.
          lo_read->set_filter( lo_root ).
        ENDIF.

        " 5. Translate sorting and ensure a deterministic row order.
        " The proxy expects its own sort-table type and property paths.
        DATA lt_order TYPE /iwbep/if_cp_runtime_types=>ty_t_sort_order.
        " Track whether Fiori already asked to sort by the unique key.
        DATA lv_key_sorted TYPE abap_bool VALUE abap_false.

        LOOP AT lt_sorts INTO DATA(ls_sort).
          " Map the sort field using the same names as the filter mapping.
          DATA(lv_sort_property) = SWITCH string( ls_sort-element_name
            WHEN 'PRODUCT'          THEN 'PRODUCT'
            WHEN 'PRODUCTTYPENAME'  THEN 'PRODUCT_TYPE'
            WHEN 'PRODUCTGROUPNAME' THEN 'PRODUCT_GROUP'
            ELSE '' ).
          IF lv_sort_property IS INITIAL.
            RAISE EXCEPTION TYPE zcx_l23_query_##.
          ENDIF.
          IF lv_sort_property = 'PRODUCT'.
            lv_key_sorted = abap_true.
          ENDIF.
          " Preserve Fiori's sort priority and direction for each field.
          APPEND VALUE #(
            property_path = lv_sort_property
            descending    = ls_sort-descending ) TO lt_order.
        ENDLOOP.

        " Products can share a type/group. Sorting by Product last breaks
        " those ties, giving stable pages when the underlying data is unchanged.
        " With no requested sort, Product ascending becomes the default.
        IF lv_key_sorted = abap_false.
          APPEND VALUE #(
            property_path = 'PRODUCT'
            descending    = abap_false ) TO lt_order.
        ENDIF.
        " A count-only request does not need sorted rows.
        IF io_request->is_data_requested( ).
          lo_read->set_orderby( lt_order ).
        ENDIF.

        " 6. Request the page and, separately, the total matching count.
        " Fiori may request rows, a count, or both. Check each flag.
        " Let S/4HANA apply paging instead of fetching all rows into ABAP.
        IF io_request->is_data_requested( ).
          DATA(lo_paging) = io_request->get_paging( ).
          DATA(lv_page_size) = lo_paging->get_page_size( ).
          IF lv_page_size = if_rap_query_paging=>page_size_unlimited.
            " No finite page size was supplied. This teaching service
            " rejects that request rather than fetching the whole catalog.
            RAISE EXCEPTION TYPE zcx_l23_query_##.
          ELSEIF lv_page_size > 0.
            " top = maximum rows to return; skip = rows to pass over.
            " Example: top 20 / skip 20 requests the second page.
            " CONV i converts RAP paging values to the proxy's integer type.
            lo_read->set_top( CONV i( lv_page_size ) ).
            lo_read->set_skip(
              CONV i( lo_paging->get_offset( ) ) ).
          ELSE.
            " A zero-size page asks for no product rows.
            lo_read->request_no_business_data( ).
          ENDIF.
        ELSE.
          " For a count-only query, suppress the remote product payload.
          lo_read->request_no_business_data( ).
        ENDIF.

        " The count covers ALL matching products, before paging.
        " A page might contain 20 rows while the matching count is 137.
        IF io_request->is_total_numb_of_rec_requested( ).
          lo_read->request_count( ).
        ENDIF.

        " 7. Execute the configured remote request and receive its response.
        " Connection or API failures go to the exception handlers below.
        DATA(lo_response) = lo_read->execute( ).

        " 8. Transfer the remote result to RAP's local response.
        " Return the remote total, not lines( lt_remote ), which would
        " describe only the current page and mislead Fiori's paging.
        IF io_request->is_total_numb_of_rec_requested( ).
          io_response->set_total_number_of_records(
            lo_response->get_count( ) ).
        ENDIF.

        IF io_request->is_data_requested( ).
          " Copy the response's business rows into the SCM-typed table.
          lo_response->get_business_data(
            IMPORTING et_business_data = lt_remote ).

          " Map each remote row to the three fields of our custom entity.
          " ProductTypeName and ProductGroupName contain codes here,
          " not descriptive texts, despite their local field names.
          LOOP AT lt_remote INTO DATA(ls_remote).
            APPEND VALUE #(
              Product          = ls_remote-product
              ProductTypeName  = ls_remote-product_type
              ProductGroupName = ls_remote-product_group ) TO lt_result.
          ENDLOOP.
          " Give RAP the mapped rows to return to the Fiori value help.
          " An empty table is valid when the query finds no matches.
          io_response->set_data( lt_result ).
        ENDIF.

      " 9. Preserve failures as failures instead of returning an empty list.
      CATCH zcx_l23_query_## INTO DATA(lx_query).
        " Our explicit query exceptions already have the required type.
        RAISE EXCEPTION lx_query.
      CATCH cx_root INTO DATA(lx_error).
        " Wrap other exceptions in the type accepted by the RAP query
        " interface. previous retains the original cause for diagnosis.
        RAISE EXCEPTION TYPE zcx_l23_query_##
          EXPORTING previous = lx_error.
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

Before activating: If your new generated SCM class is not ZSC_L23_PROD_##, replace that reference with your own generated class. Likewise, use the model ID, version and product entity-set identifier from your SCM → A_ClfnProduct → Read List.

Important scope: This classroom implementation supports basic range filters, requested sorts on its three properties (plus Product key tie-breaking), bounded paging, and remote count requests. It rejects free-text search, unsupported sort/filter fields, and unbounded page requests instead of returning misleading data. It does not implement arbitrary advanced expressions or production authorization. This example is a teaching integration, not a production-ready value-help provider. Do not imply those operations are already supported. A production query provider must handle or explicitly reject every supported query option, protect access, and apply the full API contract.

Step 8.3 — Activate the dependent objects ​

  1. Save the query-provider class and custom entity.
  2. Ensure the exception class ZCX_L23_QUERY_## is active first.
  3. Activate the query provider ZCL_L23_PRODUCTS_## (Ctrl+F3).
  4. Activate the custom entity ZCE_L23_PROD_## (Ctrl+F3).
  5. Fix any model-name, syntax or type errors before continuing.

Checkpoint: Both the CDS custom entity and the ABAP query class are active. At this stage the remote query has not yet been exposed to Fiori.

9. Expose the product collection in your Fiori service ​

Step 9.1 — Extend the generated Service Definition ​

  1. In Project Explorer, open the generated Service Definition ZUI_L23_STOCK_## (or your actual name).
  2. Keep the existing stock-check exposure.
  3. Add this line before the closing brace:
abap
expose ZCE_L23_PROD_## as Product;

For example, if your generated definition contains expose ZC_L23_STOCK_## as StockCheck;, it should now resemble:

abap
@EndUserText.label: 'Stock Check Service'
define service ZUI_L23_STOCK_## {
  expose ZC_L23_STOCK_## as StockCheck;
  expose ZCE_L23_PROD_## as Product;
}

Caution: This is an illustrative complete service definition using the expected generator names. If your generator assigned different projection/exposure names, preserve those and only add the expose ZCE_L23_PROD_## as Product; line.

  1. Save and activate the service definition (Ctrl+F3).

Step 9.2 — Publish or refresh the Service Binding ​

  1. Open Service Binding ZUI_L23_STOCK_O4_##.
  2. Keep the existing binding type OData V4 – UI.
  3. Press Publish / Refresh as required by your ADT version.
  4. Confirm that both StockCheck and Product entity sets appear in the binding editor.

If Product does not appear, check whether your custom entity and service definition are both active, and refresh the service binding.

10. Add real S/4HANA product value help to the local form ​

Step 10.1 — Find the ProductId field in the projection ​

  1. In Project Explorer, open the generated CDS projection ZC_L23_STOCK_## (or its actual name).
  2. Find the field mapped from database column product_id.
  3. The generator will usually expose it as ProductId. Verify this in your source; if different, place the annotation above the actual projection field name.

Step 10.2 — Add the value-help annotation ​

Immediately above the ProductId field in the projection view, add:

abap
@Consumption.valueHelpDefinition: [
  { entity: { name: 'ZCE_L23_PROD_##', element: 'Product' },
    useForValidation: false }
]

For example, the field declaration could then look like:

abap
@Consumption.valueHelpDefinition: [
  { entity: { name: 'ZCE_L23_PROD_##', element: 'Product' },
    useForValidation: false }
]
ProductId,

Do not paste this as a standalone CDS object. These lines belong inside your existing generated projection view. Keep all its other annotations, behaviors and fields unchanged.

  1. Activate the projection and service definition.
  2. Return to the Service Binding, refresh/publish if required, and reopen the Fiori Preview.

Step 10.3 — Test the value help ​

  1. Open the Fiori Stock Check list.
  2. Click Create.
  3. Find the Product ID input field and click its value-help icon.
  4. The dialog should query ZCE_L23_PROD_## and retrieve products from S/4HANA. Use the field/filter controls for exact values; the provider does not implement free-text search, so a generic Search box may return a not-supported error.
  5. Select A001 if present, or another actual product in the list.
  6. Enter Warehouse = WH01, CountedQty = 8, CheckStatus = N and a note.
  7. Save the record. Confirm it appears in your local Stock Check list.

Expected result if remote access is available: Fiori selects a remote S/4HANA Product ID, while the local RAP BO saves only your Stock Check data in BTP. This lesson uses useForValidation: false to leave value-help-based frontend input validation disabled. Setting it to true would enable that frontend check, but would not implement Product ID validation on the RAP server. Server-side product validity enforcement requires a separate business validation.

11. What did you actually build? ​

LayerRuns where?Responsibility
S/4HANA Product APIRemote S/4HANAOwns real product master data
Communication arrangement + SCMBTP ABAPConnects to and describes the external API
Custom Entity + Query ProviderBTP ABAPProvides Fiori a local, read-only query over S/4HANA products
Stock Check RAP BOBTP ABAPOwns new custom transactional stock-check records
Fiori ElementsBrowser + OData V4 UI serviceDisplays the form and remote product selection

This is the Clean Core connection: custom workflow and persistence live beside S/4HANA on BTP, and S/4HANA master data is read through a supported published API rather than through internal SAP tables.

12. Troubleshooting by symptom ​

What happensCheck first
Local Stock Check app fails before integrationGenerator objects, draft behavior, binding publication, Fiori preview
ZCL_L23_API_TEST_## cannot retrieve productsCheck the new SCM Model Passport, scenario access and service root
Custom Entity activation failsQuery class name in @ObjectModel.query.implementedBy and IF_RAP_QUERY_PROVIDER
TYS_A_CLFN_PRODUCT_TYPE unknownActual generated class and type from SCM Read List
Product value-help dialog is emptyFirst test the query provider and network; inspect filter name mapping and count handling
Wrong count or pageCheck remote request_count and get_count, set_top, set_skip, sorted Product key
API failure looks like zero rowsCheck exception propagation; do not catch-and-ignore the remote exception
Product field lacks value-help iconAnnotation is on the projection ProductId field, both entities are exposed in the service definition, binding is refreshed
Value help lists data but stock check cannot saveDiagnose the local RAP BO and draft service separately
Product ID gets truncatedLocal product_id field must accommodate the remote Product key (CHAR(40))

Remote connection fallback ​

If the shared Trial S/4HANA API is not reachable, complete the local RAP/Fiori stock-check flow from Sections 3–5 and show that it saves local data. Explain the custom-entity implementation and identify the precise backend failure. Do not present manually typed product IDs as a working remote value help.

13. Guided completion checklist ​

Next: Open Lesson 23 Practice and complete the warehouse supervisor's improvement ticket independently.

References ​

Verification boundary: The shared Trial Product API connection was observed working in a previous course-environment test. The SCM in this lesson is newly created and has not been tested in your tenant. The complete new project—especially this custom query provider, generated RAP app, value-help integration and Fiori Preview—has not been compiled or executed in a Trial tenant in this turn. Check activation/behavior in ADT before class. The code is written to follow SAP's documented APIs; no compilation or remote execution is claimed.