There was a question posted in the cross area forum where an SAP S/4HANA MTE customer was using the supplier invoice API to post multiple invoices at a time from SAP Cloud Platform Integration (CPI) using the OData $batch feature in the API but having trouble figuring out which invoices error-ed out in the response payload.  This was especially problematic when the payloads contained dozens or hundreds of invoices--there was no way to tell exactly which invoices failed based on the response payload from SAP S/4HC.

The supplier invoice API is a synchronous OData API that returns a success/failure message to the calling system as part of one request/response cycle.  When you are posting one invoice the calling system (CPI) knows right away the status of the request.  However, when you use $batch, each message is in the payload and treated the same way as if it were an individual request.  Unfortunately the information returned by S/4HC doesn't contain any information to easily identify the specific invoice that failed--it's more of a success/failure with the payload and error code.  The reply when using a $batch in OData standard is not a "record 55 /100 failed".  Technically, this is due to the fact that each batch request is treated as an individual HTTP operation because you can mix and match operations (GET, POST, PATCH, DELETE).

For example, here is a sample response from a $batch submission of posted invoices.  To save space, I removed the entire invoice payload from the successful request but in the response below you can see that the 1st and 3rd invoices failed and the 2nd one was successful.

<batchPartResponse>

<batchChangeSetResponse>

<batchChangeSetPartResponse>

<headers>

<Accept></Accept>

<location>https://my999999-api.s4hana.ondemand.com/sap/opu/odata/sap/API_SUPPLIERINVOICE_PROCESS_SRV/A_SupplierInvoice(SupplierInvoice=&apos;&apos;, FiscalYear=&apos;&apos;)</location>

<Accept-Language></Accept-Language>

<Content-Length>1250</Content-Length>

<dataserviceversion>1.0</dataserviceversion>

<Content-Type>;charset=utf-8</Content-Type>

</headers>

<statusInfo>Bad Request</statusInfo>

<contentId/>

<body>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;error xmlns=&quot;http://schemas.microsoft.com/ado/2007/08/dataservices/metadata&quot;&gt;&lt;code&gt;M8/485&lt;/code&gt;&lt;message xml:lang=&quot;en&quot;&gt;Cash discount amount cannot be posted. See long text&lt;/message&gt;&lt;innererror&gt;&lt;application&gt;&lt;component_id&gt;MM-IV-GF&lt;/component_id&gt;&lt;service_namespace&gt;/SAP/&lt;/service_namespace&gt;&lt;service_id&gt;API_SUPPLIERINVOICE_PROCESS_SRV&lt;/service_id&gt;&lt;service_version&gt;0001&lt;/service_version&gt;&lt;/application&gt;&lt;transactionid&gt;801eda37b9724e1f866e0b212e478fdd&lt;/transactionid&gt;&lt;timestamp/&gt;&lt;Error_Resolution&gt;&lt;SAP_Transaction/&gt;&lt;SAP_Note&gt;See SAP Note 1797736 for error analysis (https://service.sap.com/sap/support/notes/1797736)&lt;/SAP_Note&gt;&lt;Batch_SAP_Note&gt;See SAP Note 1869434 for details about working with $batch (https://service.sap.com/sap/support/notes/1869434)&lt;/Batch_SAP_Note&gt;&lt;/Error_Resolution&gt;&lt;errordetails&gt;&lt;errordetail&gt;&lt;code&gt;M8/485&lt;/code&gt;&lt;message&gt;Cash discount amount cannot be posted. See long text&lt;/message&gt;&lt;longtext_url&gt;/sap/opu/odata/iwbep/message_text;o=LOCAL/T100_longtexts(MSGID=&apos;M8&apos;,MSGNO=&apos;485&apos;,MESSAGE_V1=&apos;0.00&apos;,MESSAGE_V2=&apos;90.00&apos;,MESSAGE_V3=&apos;USD&apos;,MESSAGE_V4=&apos;&apos;)/$value&lt;/longtext_url&gt;&lt;propertyref/&gt;&lt;severity&gt;error&lt;/severity&gt;&lt;target/&gt;&lt;transition&gt;false&lt;/transition&gt;&lt;/errordetail&gt;&lt;/errordetails&gt;&lt;/innererror&gt;&lt;/error&gt;</body>

<statusCode>400</statusCode>

</batchChangeSetPartResponse>

</batchChangeSetResponse>
<batchChangeSetResponse>

<batchChangeSetPartResponse>

<headers>

<Accept></Accept>

<location>https://my999999-api.s4hana.ondemand.com/sap/opu/odata/sap/API_SUPPLIERINVOICE_PROCESS_SRV/A_SupplierInvoice(SupplierInvoice=&apos;5100020457&apos;, FiscalYear=&apos;2019&apos;)</location>

<Accept-Language></Accept-Language>

<Content-Length>6417</Content-Length>

<dataserviceversion>2.0</dataserviceversion>

<Content-Type>application/atom+xml;type=entry</Content-Type>

</headers>

<statusInfo>Created</statusInfo>

<contentId/>

<body>

<A_SupplierInvoice> ENTIRE BODY WOULD BE HERE NORMALLY </A_SupplierInvoice>

</body>

<statusCode>201</statusCode>

</batchChangeSetPartResponse>

</batchChangeSetResponse>

<batchChangeSetResponse>

<batchChangeSetPartResponse>

<headers>

<Accept></Accept>

<location>https://my999999-api.s4hana.ondemand.com/sap/opu/odata/sap/API_SUPPLIERINVOICE_PROCESS_SRV/A_SupplierInvoice(SupplierInvoice=&apos;&apos;, FiscalYear=&apos;&apos;)</location>

<Accept-Language></Accept-Language>

<Content-Length>1250</Content-Length>

<dataserviceversion>1.0</dataserviceversion>

<Content-Type>;charset=utf-8</Content-Type>

</headers>

<statusInfo>Bad Request</statusInfo>

<contentId/>

<body>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;error xmlns=&quot;http://schemas.microsoft.com/ado/2007/08/dataservices/metadata&quot;&gt;&lt;code&gt;M8/485&lt;/code&gt;&lt;message xml:lang=&quot;en&quot;&gt;Cash discount amount cannot be posted. See long text&lt;/message&gt;&lt;innererror&gt;&lt;application&gt;&lt;component_id&gt;MM-IV-GF&lt;/component_id&gt;&lt;service_namespace&gt;/SAP/&lt;/service_namespace&gt;&lt;service_id&gt;API_SUPPLIERINVOICE_PROCESS_SRV&lt;/service_id&gt;&lt;service_version&gt;0001&lt;/service_version&gt;&lt;/application&gt;&lt;transactionid&gt;801eda37b9724e1f866e0b212e478fdd&lt;/transactionid&gt;&lt;timestamp/&gt;&lt;Error_Resolution&gt;&lt;SAP_Transaction/&gt;&lt;SAP_Note&gt;See SAP Note 1797736 for error analysis (https://service.sap.com/sap/support/notes/1797736)&lt;/SAP_Note&gt;&lt;Batch_SAP_Note&gt;See SAP Note 1869434 for details about working with $batch (https://service.sap.com/sap/support/notes/1869434)&lt;/Batch_SAP_Note&gt;&lt;/Error_Resolution&gt;&lt;errordetails&gt;&lt;errordetail&gt;&lt;code&gt;M8/485&lt;/code&gt;&lt;message&gt;Cash discount amount cannot be posted. See long text&lt;/message&gt;&lt;longtext_url&gt;/sap/opu/odata/iwbep/message_text;o=LOCAL/T100_longtexts(MSGID=&apos;M8&apos;,MSGNO=&apos;485&apos;,MESSAGE_V1=&apos;0.00&apos;,MESSAGE_V2=&apos;90.00&apos;,MESSAGE_V3=&apos;USD&apos;,MESSAGE_V4=&apos;&apos;)/$value&lt;/longtext_url&gt;&lt;propertyref/&gt;&lt;severity&gt;error&lt;/severity&gt;&lt;target/&gt;&lt;transition&gt;false&lt;/transition&gt;&lt;/errordetail&gt;&lt;/errordetails&gt;&lt;/innererror&gt;&lt;/error&gt;</body>

<statusCode>400</statusCode>

</batchChangeSetPartResponse>

</batchChangeSetResponse>

</batchPartResponse>


You will notice that the statusCode field is 201 for a successful operation in this case (invoice POST operation) and if it is not 201 then the invoice failed.  You can also see statusInfo field is either "created" or something else, which is what we'll check for in this blog.

The requirement is to know which invoices failed in order to compare the responses to the original payload and determine which invoice(s) failed by matching their position in the payload.  However, in a production scenario with hundreds of invoices, manually doing this is not a feasible approach.  We can do this in CPI by iterating the response payload using Groovy Script and log which numbers failed in order to send an email for further action.  For example, if invoice 21 out of 50 failed, the administrator could pull the submitted 50 and know invoice position 21 failed.

In this example, XMLSlurper will be used to meet this requirement.

To iterate the payload above, we can use the following Groovy Script

import com.sap.gateway.ip.core.customdev.util.Message;

import java.util.HashMap;

def Message processData(Message message) {

    //Body 

    def body = message.getBody() as String;

     def messageLog = messageLogFactory.getMessageLog(message);

    

    def errorLog = "<root>";

       

    def root = new XmlSlurper().parseText(body);

    int i =0;

    def status = "";

    

     root.batchChangeSetResponse.each {

        i = i+1;

         status = "${it.batchChangeSetPartResponse.statusInfo}";

         

        if(status != "Created") {

            errorLog = errorLog + "<id>"+i+"</id><error>"+"${it.batchChangeSetPartResponse.body}"+"</error>";

        }




     }




    errorLog = errorLog + "</root>";

    message.setBody(errorLog);

    messageLog.setStringProperty("ResponsePayload", "Printing Payload As Attachment")

    messageLog.addAttachmentAsString("Supplier Invoice Error Log:", errorLog, "text/xml");

    

       return message;

}

The above code checks for the HTTP statusInfo field = "Created" (you can also check status field for "201").  It also produces a string object with the id of the error from the original file along with the error that can then be emailed to an administrator showing exactly which invoices failed.

You of course could write more code to strip out the message tag of the XML and clean up the payload to include only the id and the error message.  The screen shot below shows the entire error message payload.

screenShot.png

As shown in this blog, the XMLSlurper is a powerful class for working with XML objects in Groovy Script and has many uses.

Thanks,

Marty