This blog will provide an example of how to call the Sales Order API in SAP S/4HANA Cloud (S/4HC) from SAP Cloud Platform Integration (CPI) and post header & line items.  This is the first blog in a two piece series–in this one we’ll look at the implementation and the second blog will show how to execute the scenario using Postman and then will also take a look at the logs generated by the iFlow.

The second blog can be found here.

Scenario

For our fictional scenario, we’ll assume that a customer has a requirement to create sales orders in an S/4HC system with multiple line items from a 3rd party system.  The customer will use the white listed API on SAP API hub for Sales Order creation.

In this blog, we’ll create an iFlow to accept, transform and post the sales order data into the S/4HC system.  What makes this scenario unique is that the OData service API_SALES_ORDER_SRV only supports a deep insert to get line items posted and a simple CREATE is currently not possible to include header and line items.  Therefore, special handling needs to occur in CPI to setup the create deep in the iFlow.  Essentially, we’ll need to add the line items back to the request payload after our mapping step for the header items.

Some information before we get started:

The API we’ll use is “Process Sales Order” which can be found on SAP’s API hub: https://api.sap.com/shell/discover/contentpackage/SAPS4HANACloud/api/API_SALES_ORDER_SRV

You can read more about OData payload structures here: https://blogs.sap.com/2017/09/06/payload-structures-in-odata-v2-adapter-for-sap-cloud-platform-integration/

This blog follows the approach outlined in this blog which covers multiple entities and how to handle them in CPI and should be read before this one: https://blogs.sap.com/2016/06/10/deep-insert-suport-in-odata-provisioning-in-hci/

On a side note: There is a great guide attached to scope item 1RW that provides an example of side by side extensibility—it has very similar concepts to the ones discussed in this blog (consuming/ updating S/4HC via whitelisted APIs).  https://rapid.sap.com/bp/#/browse/scopeitems/1RW

Configure S/4HANA Cloud to enable Sales Order API

In order to enable the APIs, a communication user, communication system and the communication arrangement need to be created.  This is well documented already in section 3.2 of the 1RW scope item setup guide.

The Communication arrangement for sales order API is SAP_COM_0109.

CPI Scenario

In our sample application, we will expose an HTTP service that accepts a JSON payload to “simulate” the 3rd party sending the sales order to the interface on CPI.  The iFlow will then transform that data for creating sales order(s) with multiple line items in order to post into S/4HC.

Setting up Mock Sender System

Normally, an external system would send the sales order to CPI for posting in S/4HC.  However, here we will expose an HTTP service on CPI to accept a JSON payload and use the Postman application to send sales orders via JSON manually.

In order to get an xsd file for the mapping step on CPI, I took a sample JSON payload that I used to call the API directly from Postman and converted to XML and then converted the XML to XSD using online tools.

Here are the tools I used and steps followed:

For converting JSON to XSL:

tool: https://www.utilities-online.info/xmltojson/#.Wj03y99KtPY

The JSON I used to convert into XML.

{ "salesOrder":[ { "salesOrderType":[ { "SoldToParty": "USCU_L02", "DistributionChannel": "10", "SalesOrderType": "OR", "OrganizationDivision": "00", "SalesOrganization": "1710", "PurchaseOrderByCustomer": "45000049106" , "lineItem": [ { "Material": "MZ-TG-Y200", "SalesOrderItem": "10", "RequestedQuantityUnit": "PC", "NetAmount": "89" }, { "Material": "MZ-FG-M550", "SalesOrderItem": "20", "RequestedQuantityUnit": "PC", "NetAmount": "75" } ] } ] } ] } 

 

Then convert generated XML to XSD using xmlgrild tool: https://xmlgrid.net/xml2xsd.html

Now you have generated the xsd file to use in the mapping step in the iFlow.

iFlow Development

High Level Flow

At a high level, there is a “Sender” in this scenario, which is the HTTPS call simulating another system submitting sales order via JSON.  Then the iFlow converts the payload to XML and extracts the sales order line items to a header variable.  Then the mapping to the SalesOrder API occurs followed by another Groovy script to (“RecreatePayload”) to add the line item entities back to the payload.  The payload is logged (informational purposes only) and then submitted to the S/4HC system (Request-Reply).  Finally, I log the salesOrder response as well just for informational purposes.

Obviously, you wouldn’t need the scripts that only log the payload before and after the Request-Reply but I did this to show the payloads along the flow, which we’ll take a look at in the next blog.

 

 

iFlow Details

HTTP Sender

The HTTPS sender has a path created for https access and the user role required for access.  In this example, I assigned my CPI tenant user ESBMessaging.Send role (this would be an S user or I number, for example).  The URL would be available as follows:

https://####-iflmap.hcisbt.us2.hana.ondemand.com/http/salesorders

 

JSON to XML Converter

The JSON to XML Converter is just the default converter settings

 

ExtractLineItemsToHeader

This  groovy script object iterates the payload and takes all of the and places them into the header variable “InlineEntry” in the correct XML format that the API expects (A_SalesOrderItemType).  Also, note that the Sales Order API expects SalesOrder and SalesOrderItem entries, and in this case they are sent with blank values for each line item (since SalesOrder # doesnt exist yet).

/* The integration developer needs to create the method processData This method takes Message object of package com.sap.gateway.ip.core.customdev.util which includes helper methods useful for the content developer: The methods available are: public java.lang.Object getBody() public void setBody(java.lang.Object exchangeBody) public java.util.Map getHeaders() public void setHeaders(java.util.Map exchangeHeaders) public void setHeader(java.lang.String name, java.lang.Object value) public java.util.Map getProperties() public void setProperties(java.util.Map exchangeProperties) public void setProperty(java.lang.String name, java.lang.Object value) */ import com.sap.gateway.ip.core.customdev.util.Message; import java.util.*; import com.sap.gateway.ip.core.customdev.logging.*; import java.util.HashMap; import java.text.SimpleDateFormat; import java.util.Date; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.w3c.dom.Node; def Message processData(message) { def headers = message.getHeaders(); StringBuffer reqXML = new StringBuffer(); def payload = message.getBody(java.lang.String) as String; def tokens = payload.split("(?=<)|(?<=>)"); def salesitems = false; def haslineitems = false; for(int i=0;i<tokens.length;i++) { //begin writing to stream for lineitems if(tokens[i]=="") { salesitems = true; haslineitems = true; } else if(tokens[i] =="") { salesitems = false; } if(salesitems == true) { reqXML.append(tokens[i]); } } def newdata = reqXML.toString(); newdata = newdata.replaceAll("",""); newdata = newdata.replaceAll("",""); newdata = newdata.replaceAll("",""); newdata = newdata.replaceAll("",""); def body = message.getBody(java.lang.String) as String; def messageLog = messageLogFactory.getMessageLog(message); for (header in headers) { messageLog.setStringProperty("header." + header.getKey().toString(), header.getValue().toString()) } for (property in properties) { messageLog.setStringProperty("property." + property.getKey().toString(), property.getValue().toString()) } if(messageLog != null){ messageLog.addAttachmentAsString("beforeMapping", newdata, "text/plain"); } if (newdata != null) { // log.logErrors(LogMessage.TechnicalError, "reqXML: "+ reqXML); message.setHeader("InlineEntry", newdata); log.logErrors(LogMessage.TechnicalError, "message header: "+ message.getHeaders().get("InlineEntry")); } return message; }

 Mapping1

Mapping of xsd to the API A_SalesOrder

 

RecreatePayload

This groovy script adds the lineItems back to the payload before making the call to S/4HC.

import com.sap.gateway.ip.core.customdev.util.Message; import java.util.*; import com.sap.gateway.ip.core.customdev.logging.*; def Message processData(message) { String payload = new String(message.getBody(), "UTF-8"); log.logErrors(LogMessage.TechnicalError, "payload in script2: "+payload); StringBuffer newReqXML = new StringBuffer(); String inlineEntry = message.getHeaders().get("InlineEntry"); log.logErrors(LogMessage.TechnicalError, "InlineEntry: "+inlineEntry); def tokens = payload.split("(?=<)|(?<=>)"); log.logErrors(LogMessage.TechnicalError, "tokens: "+tokens); for(int i=0;i<tokens.length;i++) { log.logErrors(LogMessage.TechnicalError, "tokens: "+tokens[i]); newReqXML.append(tokens[i]); if(tokens[i].contains("")){ newReqXML.append(inlineEntry+""); break; } } log.logErrors(LogMessage.TechnicalError, "newReqXML: "+newReqXML.toString()); message.setBody(newReqXML.toString()); return message; }

 

LogPayloadBeforeSubmission

A simple groovy script to log the payload before sending to S/4HC.

/* The integration developer needs to create the method processData This method takes Message object of package com.sap.gateway.ip.core.customdev.util which includes helper methods useful for the content developer: The methods available are: public java.lang.Object getBody() public void setBody(java.lang.Object exchangeBody) public java.util.Map getHeaders() public void setHeaders(java.util.Map exchangeHeaders) public void setHeader(java.lang.String name, java.lang.Object value) public java.util.Map getProperties() public void setProperties(java.util.Map exchangeProperties) public void setProperty(java.lang.String name, java.lang.Object value) */ import com.sap.gateway.ip.core.customdev.util.Message; import java.util.HashMap; def Message processData(Message message) { def body = message.getBody(java.lang.String) as String; def messageLog = messageLogFactory.getMessageLog(message); def propertyMap = message.getProperties() messageLog.setStringProperty("BeforeSubmission", "Printing Payload As Attachment") messageLog.addAttachmentAsString("Payload Before Submission After Mappings:", body, "text/xml"); return message; } 

Request-Reply

This is the call to S/4HC SalesOrder API via Request-Reply OData adapter to post the sales order data to the API.

 

salesOrderPayloadResponse

Same script as previous that logs the response from the S/4HC reply which is the new sales order data.

/* The integration developer needs to create the method processData This method takes Message object of package com.sap.gateway.ip.core.customdev.util which includes helper methods useful for the content developer: The methods available are: public java.lang.Object getBody() public void setBody(java.lang.Object exchangeBody) public java.util.Map getHeaders() public void setHeaders(java.util.Map exchangeHeaders) public void setHeader(java.lang.String name, java.lang.Object value) public java.util.Map getProperties() public void setProperties(java.util.Map exchangeProperties) public void setProperty(java.lang.String name, java.lang.Object value) */ import com.sap.gateway.ip.core.customdev.util.Message; import java.util.HashMap; def Message processData(Message message) { def body = message.getBody(java.lang.String) as String; def messageLog = messageLogFactory.getMessageLog(message); def propertyMap = message.getProperties() messageLog.setStringProperty("Logging#1", "Printing Payload As Attachment") messageLog.addAttachmentAsString("ResponsePayload:", body, "text/xml"); return message; }

That’s it!  The iFlow is ready to be tested, which we’ll cover in the next blog.