If you want to consume an OData service in your ABAP coding there is a nice feature available in SAP Cloud Platform, ABAP Environment that lets you generate an OData Client Proxy by importing the EDMX file of an OData V2 service. The import is offered by creating a so called Service Consumption Model.
Though the OData Client Proxy itself is also available in SAP S/4HANA 1709 and later, the Service Consumption Model is not yet available in SAP S/4HANA 1909. But is planned to be available in an upcoming SAP S/4HANA release.
Also Read: SAP S/4HANA Conversion and SAP System Upgrade Certification Preparation Guide
So how can you use the OData Client Proxy without having the option to generate it by creating a Service Consumption Model?
The problem when using the OData Client Proxy is that it requires an OData V4 model which is conveniently created for you in SAP Cloud Platform, ABAP Environment when you create a Service Consumption Model.
Since creating an OData V4 model manually just from analyzing the $metadata document of the OData V2 service that you want to consume is a tedious and error-prone task I thought whether there are workarounds that can be used.
When thinking about the problem it came to my mind that the Service Builder offers the possibility to generate an OData V2 service by importing the $metadata file of remote OData V2 service. Though this will not generate the desired OData V4 model as a result a V2 model provider class is generated which contains code (TYPES and ABAP code). These artefacts can partly be reused using Cut&Paste when creating a OData V4 model provider class for your OData Client Proxy.
The steps you have to follow are the following:
- Create a Service Builder project and import the $metadata file
- Get the TYPES statement that has been generated for the structure of the entity type(s) you want to consume
- Check the entity type specific define method of the entity type to retrieve the Edm Names of the properties of the entity set.
Use case
The use case is the integration of any OData service punlished by other SAP systems that run on premise or in the cloud or other OData services.
In this blog I will describe how to call the Sales Order A2X service that can reside either in another SAP S/4 HANA system (on premise or cloud) from a SAP S/4HANA on premise system.
We will retrieve a list of sales orders using the following OData call
/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder?$top=5
Applicable releases
The scenario described can not only be implemented in SAP S/4HANA 1909 but also in the following releases since the SAP Gateway Framework has been down ported as described in SAP Note 2512479 – SAP Gateway Foundation Support Package Stack Definition
- SAP S/4HANA 1909 SP01
- SAP S/4HANA 1809 SP03
- SAP S/4HANA 1709 (SAP NetWeaver 7.52 SP05)
- SAP S/4HANA 1609 (SAP NetWeaver 7.51 SP09)
- SAP NetWeaver 7.52 SP05
- SAP NetWeaver 7.51 SP09
Create a Service Builder Project
- We start by creating a new project Z_CONSUME_SO in the Service Builder (transaction SEGW), right click on the folder Data Model and select Import –> Data Model from File from the context menu.
- Select the EDMX file of your OData Service. In this example I chose the EDMX file of the OData service API_SALES_ORDER_SRV that is part of SAP S/4 HANA and contains lots of entity sets.
- We now can generate the project which amongst other repository objects will generate a model provider class ZCL_Z_CONSUME_SO_MPC. You can ignore the warnings.
- If you check the code you will find the type definition for the type TS_A_SALESORDERTYPE.
So in the generated code we will find code snippets that we will reuse in the following step.
This is the entity type specific define method DEFINE_A_SALESORDERTYPE.
method DEFINE_A_SALESORDERTYPE.
*&---------------------------------------------------------------------*
*& Generated code for the MODEL PROVIDER BASE CLASS &*
*& &*
*& !!!NEVER MODIFY THIS CLASS. IN CASE YOU WANT TO CHANGE THE MODEL &*
*& DO THIS IN THE MODEL PROVIDER SUBCLASS!!! &*
*& &*
*&---------------------------------------------------------------------*
data:
lo_annotation type ref to /iwbep/if_mgw_odata_annotation, "#EC NEEDED
lo_entity_type type ref to /iwbep/if_mgw_odata_entity_typ, "#EC NEEDED
lo_complex_type type ref to /iwbep/if_mgw_odata_cmplx_type, "#EC NEEDED
lo_property type ref to /iwbep/if_mgw_odata_property, "#EC NEEDED
lo_entity_set type ref to /iwbep/if_mgw_odata_entity_set. "#EC NEEDED
****************************************************************************
* ENTITY - A_SalesOrderType
****************************************************************************
lo_entity_type = model->create_entity_type( iv_entity_type_name = 'A_SalesOrderType' iv_def_entity_set = abap_false ). "#EC NOTEXT
lo_entity_type->set_label_from_text_element( iv_text_element_symbol = '002' iv_text_element_container = gc_incl_name ). "#EC NOTEXT
****************************************************************************
*Properties
****************************************************************************
lo_property = lo_entity_type->create_property( iv_property_name = 'SalesOrder' iv_abap_fieldname = 'SALESORDER' ). "#EC NOTEXT
lo_property->set_label_from_text_element( iv_text_element_symbol = '003' iv_text_element_container = gc_incl_name ). "#EC NOTEXT
lo_property->set_is_key( ).
lo_property->set_type_edm_string( ).
lo_property->set_maxlength( iv_max_length = 10 ). "#EC NOTEXT
lo_property->set_creatable( abap_true ).
lo_property->set_updatable( abap_true ).
lo_property->set_sortable( abap_true ).
lo_property->set_nullable( abap_false ).
lo_property->set_filterable( abap_true ).
lo_property->/iwbep/if_mgw_odata_annotatabl~create_annotation( 'sap' )->add(
EXPORTING
iv_key = 'unicode'
iv_value = 'false' ).
Of special interest here is the code iv_property_name = ‘SalesOrder’ that sets the external EDM (=Entity Data Model) name of the property of an entity type. For example:
lo_property = lo_entity_type->create_property( iv_property_name = 'SalesOrder' iv_abap_fieldname = 'SALESORDER' ). "#EC NOTEXT
As well as the TYPES definition ts_a_salesordertype that we will use to receive the returned data
types:
begin of TS_A_SALESORDERTYPE,
SALESORDER type C length 10,
SALESORDERTYPE type C length 4,
SALESORGANIZATION type C length 4,
DISTRIBUTIONCHANNEL type C length 2,
ORGANIZATIONDIVISION type C length 2,
SALESGROUP type C length 3,
SALESOFFICE type C length 4,
SALESDISTRICT type C length 6,
SOLDTOPARTY type C length 10,
CREATIONDATE type TIMESTAMP,
...
OVERALLTOTALDELIVERYSTATUS type C length 1,
OVERALLSDDOCUMENTREJECTIONSTS type C length 1,
end of TS_A_SALESORDERTYPE .
types:
TT_A_SALESORDERTYPE type standard table of TS_A_SALESORDERTYPE .
Please note that we have to change the data type for the fields that contain dates such as CREATIONDATE from timestamp to tzntstmps.
Create an OData V4 model provider class
You can create an OData V4 model provider class which inherits from the super class /iwbep/cl_v4_abs_model_prov.
Please note:
1. You have to replace the type timestamp by the type tzntstmps. If you do not do this when calling the OData Client Proxy you will receive the exception:
Entity list could not be deserialized.
2. You have to copy the statementsiv_property_name = ‘SalesOrderType’.from the method DEFINE_A_SALESORDERTYPE to create the coding shown below that sets the correct Edm.Name for the entity set that you want to consume.If you do not do this the OData Client Proxy will return no data, that means you will get a list that only contains empty fields.
CLASS zcl_api_sales_order_srv_v4_mdl DEFINITION
PUBLIC
INHERITING FROM /iwbep/cl_v4_abs_model_prov
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS /iwbep/if_v4_mp_basic~define REDEFINITION.
PROTECTED SECTION.
PRIVATE SECTION.
METHODS define_salesorder
IMPORTING
io_model TYPE REF TO /iwbep/if_v4_med_model
RAISING
/iwbep/cx_gateway .
TYPES:
BEGIN OF ts_a_salesordertype,
salesorder TYPE c LENGTH 10,
salesordertype TYPE c LENGTH 4,
salesorganization TYPE c LENGTH 4,
distributionchannel TYPE c LENGTH 2,
organizationdivision TYPE c LENGTH 2,
salesgroup TYPE c LENGTH 3,
salesoffice TYPE c LENGTH 4,
salesdistrict TYPE c LENGTH 6,
soldtoparty TYPE c LENGTH 10,
creationdate TYPE tzntstmps,
createdbyuser TYPE c LENGTH 12,
lastchangedate TYPE tzntstmps,
lastchangedatetime TYPE tzntstmps,
purchaseorderbycustomer TYPE c LENGTH 35,
customerpurchaseordertype TYPE c LENGTH 4,
customerpurchaseorderdate TYPE tzntstmps,
salesorderdate TYPE tzntstmps,
totalnetamount TYPE p LENGTH 9 DECIMALS 3,
transactioncurrency TYPE c LENGTH 5,
sddocumentreason TYPE c LENGTH 3,
pricingdate TYPE tzntstmps,
requesteddeliverydate TYPE tzntstmps,
shippingcondition TYPE c LENGTH 2,
completedeliveryisdefined TYPE flag,
shippingtype TYPE c LENGTH 2,
headerbillingblockreason TYPE c LENGTH 2,
deliveryblockreason TYPE c LENGTH 2,
incotermsclassification TYPE c LENGTH 3,
incotermstransferlocation TYPE c LENGTH 28,
incotermslocation1 TYPE c LENGTH 70,
incotermslocation2 TYPE c LENGTH 70,
incotermsversion TYPE c LENGTH 4,
customerpaymentterms TYPE c LENGTH 4,
paymentmethod TYPE c LENGTH 1,
assignmentreference TYPE c LENGTH 18,
referencesddocument TYPE c LENGTH 10,
referencesddocumentcategory TYPE c LENGTH 4,
customertaxclassification1 TYPE c LENGTH 1,
taxdeparturecountry TYPE c LENGTH 3,
vatregistrationcountry TYPE c LENGTH 3,
salesorderapprovalreason TYPE c LENGTH 4,
salesdocapprovalstatus TYPE c LENGTH 1,
overallsdprocessstatus TYPE c LENGTH 1,
totalcreditcheckstatus TYPE c LENGTH 1,
overalltotaldeliverystatus TYPE c LENGTH 1,
overallsddocumentrejectionsts TYPE c LENGTH 1,
END OF ts_a_salesordertype .
DATA ls_salesorder TYPE ts_a_salesordertype.
ENDCLASS.
CLASS zcl_api_sales_order_srv_v4_mdl IMPLEMENTATION.
METHOD /iwbep/if_v4_mp_basic~define.
define_salesorder( io_model ).
io_model->set_schema_namespace( 'API_SALES_ORDER_SRV' ).
ENDMETHOD.
METHOD define_salesorder.
DATA: lt_primitive_properties TYPE /iwbep/if_v4_med_element=>ty_t_med_prim_property,
lo_entity_set TYPE REF TO /iwbep/if_v4_med_entity_set,
lo_nav_prop TYPE REF TO /iwbep/if_v4_med_nav_prop,
lo_entity_type TYPE REF TO /iwbep/if_v4_med_entity_type
",
"lv_referenced_cds_view type gty_cds_views-salesorder
. " As internal ABAP name we use the name of the CDS view
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Create entity type
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
lo_entity_type = io_model->create_entity_type_by_struct(
EXPORTING
iv_entity_type_name = 'SALESORDER'
is_structure = ls_salesorder
iv_add_conv_to_prim_props = abap_true
iv_add_f4_help_to_prim_props = abap_true
iv_gen_prim_props = abap_true ).
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Set external EDM name for entity type
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
lo_entity_type->set_edm_name( 'A_SalesOrderType' ).
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Rename external EDM names of properties so that CamelCase notation is used
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
lo_entity_type->get_primitive_properties( IMPORTING et_property = lt_primitive_properties ).
LOOP AT lt_primitive_properties INTO DATA(lo_primitive_property).
lo_primitive_property->set_edm_name( to_mixed( val = lo_primitive_property->get_internal_name( ) ) ).
ENDLOOP.
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Set key field(s)
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
lo_primitive_property = lo_entity_type->get_primitive_property( 'SALESORDER' ).
lo_primitive_property->set_is_key( ).
lo_primitive_property->set_edm_name( 'SalesOrder' ).
"Set edm names
DATA lt_fieldnames TYPE TABLE OF /iwbep/if_v4_med_element=>ty_e_med_edm_name.
DATA iv_property_name TYPE /iwbep/if_v4_med_element=>ty_e_med_edm_name .
iv_property_name = 'SalesOrderType'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'SalesOrganization'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'DistributionChannel'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'OrganizationDivision'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'SalesGroup'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'SalesOffice'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'SalesDistrict'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'SoldToParty'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'CreationDate'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'CreatedByUser'.
APPEND iv_property_name TO lt_fieldnames.
"can be null
iv_property_name = 'LastChangeDate'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'LastChangeDateTime'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'PurchaseOrderByCustomer'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'CustomerPurchaseOrderType'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'customerpurchaseorderdate'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'salesorderdate'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'TotalNetAmount'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'TransactionCurrency'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'SDDocumentReason'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'PricingDate'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'RequestedDeliveryDate'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'ShippingCondition'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'CompleteDeliveryIsDefined'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'ShippingType'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'HeaderBillingBlockReason'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'DeliveryBlockReason'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'IncotermsClassification'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'IncotermsTransferLocation'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'IncotermsLocation1'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'IncotermsLocation2' .
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'IncotermsVersion'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'CustomerPaymentTerms'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'PaymentMethod'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'AssignmentReference'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'ReferenceSDDocument'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'ReferenceSDDocumentCategory'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'CustomerTaxClassification1'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'TaxDepartureCountry' .
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'VATRegistrationCountry'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'SalesOrderApprovalReason' .
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'SalesDocApprovalStatus'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'OverallSDProcessStatus' .
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'TotalCreditCheckStatus' .
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'OverallTotalDeliveryStatus'.
APPEND iv_property_name TO lt_fieldnames.
iv_property_name = 'OverallSDDocumentRejectionSts'.
APPEND iv_property_name TO lt_fieldnames.
LOOP AT lt_fieldnames INTO DATA(ls_fieldname).
lo_primitive_property = lo_entity_type->get_primitive_property( to_upper( ls_fieldname ) ).
lo_primitive_property->set_edm_name( ls_fieldname ).
CASE ls_fieldname.
WHEN 'LastChangeDate'.
lo_primitive_property->set_is_nullable( ).
ENDCASE.
ENDLOOP.
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Create entity set
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
lo_entity_set = lo_entity_type->create_entity_set( 'SALESORDER' ).
lo_entity_set->set_edm_name( 'A_SalesOrder' ).
ENDMETHOD.
ENDCLASS.
Register the OData Client Proxy
Using transaction /n/IWBEP/CP_ADMIN we can now register a new client proxy based on the OData V4 model provider class that we have created in the step before.
If you press the create button you have to provide the following information in order to create the OData Client Proxy model.
Proxy Model ID | ZSC_EDMX_IMPORT |
Version | 1 |
Model Provider Class |
zcl_api_sales_order_srv_v4_mdl |
Description |
V4 Model for API_SALES_ORDER_SRV |
Package |
$TMP |
The newly created OData Proxy Model will show up in the list.
Call the OData Client Proxy
Now we can develop a test class to call the OData service in order to retrieve the first 5 sales orders from the list.
Since for calling the OData Client Proxy we need a http client we will create one based on an existing http destination that you have to create using transaction SM59.
In addition we need to provide the repository id (default) where our model has been created alongside with the Proxy Model ID (see above) and the the version as well as the relative service root URL.
The output of the class that implements the if_oo_adt_classrun interface should now be as follows:
CLASS zcl_my_odata_proxy DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_my_odata_proxy IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
TYPES:
BEGIN OF ts_a_salesordertype,
salesorder TYPE c LENGTH 10,
salesordertype TYPE c LENGTH 4,
salesorganization TYPE c LENGTH 4,
distributionchannel TYPE c LENGTH 2,
organizationdivision TYPE c LENGTH 2,
salesgroup TYPE c LENGTH 3,
salesoffice TYPE c LENGTH 4,
salesdistrict TYPE c LENGTH 6,
soldtoparty TYPE c LENGTH 10,
creationdate TYPE tzntstmps,
createdbyuser TYPE c LENGTH 12,
lastchangedate TYPE tzntstmps,
lastchangedatetime TYPE tzntstmpsl,
purchaseorderbycustomer TYPE c LENGTH 35,
customerpurchaseordertype TYPE c LENGTH 4,
customerpurchaseorderdate TYPE tzntstmps,
salesorderdate TYPE tzntstmps,
totalnetamount TYPE p LENGTH 9 DECIMALS 3,
transactioncurrency TYPE c LENGTH 5,
sddocumentreason TYPE c LENGTH 3,
pricingdate TYPE tzntstmps,
requesteddeliverydate TYPE tzntstmps,
shippingcondition TYPE c LENGTH 2,
completedeliveryisdefined TYPE flag,
shippingtype TYPE c LENGTH 2,
headerbillingblockreason TYPE c LENGTH 2,
deliveryblockreason TYPE c LENGTH 2,
incotermsclassification TYPE c LENGTH 3,
incotermstransferlocation TYPE c LENGTH 28,
incotermslocation1 TYPE c LENGTH 70,
incotermslocation2 TYPE c LENGTH 70,
incotermsversion TYPE c LENGTH 4,
customerpaymentterms TYPE c LENGTH 4,
paymentmethod TYPE c LENGTH 1,
assignmentreference TYPE c LENGTH 18,
referencesddocument TYPE c LENGTH 10,
referencesddocumentcategory TYPE c LENGTH 4,
customertaxclassification1 TYPE c LENGTH 1,
taxdeparturecountry TYPE c LENGTH 3,
vatregistrationcountry TYPE c LENGTH 3,
salesorderapprovalreason TYPE c LENGTH 4,
salesdocapprovalstatus TYPE c LENGTH 1,
overallsdprocessstatus TYPE c LENGTH 1,
totalcreditcheckstatus TYPE c LENGTH 1,
overalltotaldeliverystatus TYPE c LENGTH 1,
overallsddocumentrejectionsts TYPE c LENGTH 1,
END OF ts_a_salesordertype .
TYPES:
tt_a_salesordertype TYPE STANDARD TABLE OF ts_a_salesordertype .
DATA: lt_salesorder TYPE tt_a_salesordertype,
lo_client_proxy TYPE REF TO /iwbep/if_cp_client_proxy,
lo_read_request TYPE REF TO /iwbep/if_cp_request_read_list,
lo_read_response TYPE REF TO /iwbep/if_cp_response_read_lst.
DATA lv_relative_service_root TYPE string.
lv_relative_service_root = '/sap/opu/odata/sap/API_SALES_ORDER_SRV/'.
TRY.
"throws an exception if service document cannot be read
" Using SM59 destination for HTTP client object
cl_http_client=>create_by_destination(
EXPORTING
destination = 'LOCAL_HTTP_AF'
IMPORTING
client = DATA(lo_http_client)
EXCEPTIONS
OTHERS = 0 ).
IF sy-subrc <> 0.
out->write( 'error create by http destination').
EXIT.
ENDIF.
lo_client_proxy = /iwbep/cl_cp_client_proxy_fact=>create_v2_remote_proxy(
io_http_client = lo_http_client
is_proxy_model_key = VALUE #( repository_id = /iwbep/if_cp_registry_types=>gcs_repository_id-default
proxy_model_id = 'ZSC_EDMX_IMPORT'
proxy_model_version = 0001 )
iv_relative_service_root = lv_relative_service_root ).
" 'SALESORDER' is the ABAP internal name of the entityset of the V4 model
lo_read_request = lo_client_proxy->create_resource_for_entity_set( 'SALESORDER' )->create_request_for_read( ).
lo_read_request->set_top( iv_top = 5 ).
lo_read_response = lo_read_request->execute( ).
" Retrieve the business data
lo_read_response->get_business_data( IMPORTING et_business_data = lt_salesorder ).
LOOP AT lt_salesorder INTO DATA(ls_salesorder).
out->write( ls_salesorder ).
ENDLOOP.
CATCH /iwbep/cx_cp_remote INTO DATA(lx_cp_remote).
" Error handling
out->write( lx_cp_remote->get_longtext( ) ).
CATCH /iwbep/cx_gateway INTO DATA(lx_gateway).
" Error Handling
out->write( lx_gateway->get_longtext( ) ).
ENDTRY.
ENDMETHOD.
ENDCLASS.