SAP B1 – DI Transactions, a deeper look.

Introduction

In this tutorial, we will discuss the different approaches to use when packing multiple business operations together in SAP Business One and how we can avoid common mistakes that lead to incomplete processes.

This article can be useful if:

  1. You’re planning on building an interface for SAP B1 and want to know your options.
  2. You want to optimize an existing process for one of your customer where multiple business objects are being added and/or updated.
  3. You wish to learn more about how transactions work in SAP B1.

Table of Contents

Context

You customer wants you to develop an interface where both an order and its invoice are being added to the system together.

Let us take as an example an order with two lines and its corresponding invoice.

In SAP B1, a document flow is maintained at line level. In this case:

  • The first line of the invoice is linked to the first line of the order.
  • The second line of the invoice is linked to the first line of the order.

Since the invoice is created after the order; for this relation to work, you need to know in advance the unique identifier of the order. Otherwise, you cannot bind these four lines together.

What not do do ❌

If we break this down, the natural way of handling it would be:

  1. First, add the sales order
  2. Retrieve the unique identifier assigned by the system (which is called the DocEntry)
  3. Create the invoice based on the order’s DocEntry and add it.
Add an order and an invoice with two separate calls (happy scenario).

This is working well… Unless the addition of the A/R Invoice fails. This would result in an orphaned order and you would need to handle this situation by either:

  • Cancelling the order in the B1 client, then retrying the scenario. But then you’d need to manage the fact that the order was already created.
    Or
  • Creating the invoice in the B1 client (via the Copy To button) without retrying the scenario.

In other words, you’d need a manual operation to fix it, and we don’t want that. Instead, we want to perform those two operations in one pass and rollback if either one of them fails.

This particular point is often referred as Atomicity in the ACID principles. Quoting Wikipedia:

Atomicity Transactions are often composed of multiple statements. Atomicity guarantees that each transaction is treated as a single "unit", which either succeeds completely or fails completely: if any of the statements constituting a transaction fails to complete, the entire transaction fails and the database is left unchanged.

Wikipedia

In this example, this could be a DI error at invoice creation:

				
					{
   "error" : {
      "code" : "-10",
      "message" : "Quantity falls into negative inventory  [DocumentLines.ItemCode][line: 1]"
   }
}
				
			

… as you can still add the order even if you don’t have enough quantity in stock but not the invoice.

Transactions to the rescue!

We can address this problem with the use of transactions, simply said:

  1. Open a transaction
  2. Add order
  3. Add invoice
  4. Close transaction

Luckily for us, SAP addresses this issue by providing solutions to handle transactions:

  1. With the DI-API
  2. With the Service Layer /$batch endpoint
  3. With a script extension (user-made JS script installed on top of the service layer).

In this article, we will see the differences between these three methods. Which one you’ll use depends on the type of project you’re building (API/stand-alone executable program) and the amount of flexibility you need inside of your transactions. 

Transactions' pitfalls

You may wonder what’s really going on when you’re using transactions. An easy way to track it down for MSSQL versions of SAP is to open the SQL Server Profiler and invoke the StartTransaction() and EndTransaction() methods of the DI-API.

What happens in the database when we start and end a B1 DI transaction.

We can see on the screenshot that the isolation level is set to SERIALIZABLE when starting a transaction. Taking a look at microsoft documentation:

SERIALIZABLE

      Statements cannot read data that has been modified but not yet committed by other transactions.
      No other transactions can modify data that has been read by the current transaction until the current transaction completes.
      Other transactions cannot insert new rows with key values that would fall in the range of keys read by any statements in the current transaction until the current transaction completes.

Just trying to draw your attention on the fact that transactions do come with a cost and can even freeze the B1 client for users that are tyring to access modified data in your transaction.

It’s up to you to judge whether the use of transactions are really needed, especially if you’re running such a process during business hours. I would simply advise to:

  1. Do as many things as you can prior to starting your transaction and only start it when you’re ready to send the data to the ERP.
  2. Avoid time-consuming operations within a transaction.
  3. Avoid packing too many business operations within a single transaction.

The next sections will all deal with transactions, so keep that in mind that the warning above applies for all of them.

DI-API - With transactions

DI-API handles transactions. Let us take the same example : adding an order and an invoice in a single transaction (see highlighted lines #17 and #47):

				
					            SAPbobsCOM.IDocuments
                oOrder = oCompany.GetBusinessObject(SAPbobsCOM.BoObjectTypes.oOrders) as SAPbobsCOM.IDocuments,
                oInvoice = oCompany.GetBusinessObject(SAPbobsCOM.BoObjectTypes.oInvoices) as SAPbobsCOM.IDocuments;

            // Preparing the order payload
            oOrder.CardCode = "C001";
            oOrder.DocDate = DateTime.Today;
            oOrder.DocDueDate = DateTime.Today.AddDays(30);
            oOrder.Lines.SetCurrentLine(0); // Not needed, for the sake of clarity
            oOrder.Lines.ItemCode = "A0001";
            oOrder.Lines.Quantity = 1;
            oOrder.Lines.Price = 50;

            try
            {
                //  ***************** BEGIN TRANSACTION *****************
                oCompany.StartTransaction();

                if (oOrder.Add() != 0)
                {
                    throw new Exception(oCompany.GetLastErrorDescription());
                }

                // Since we are in a transaction, we cannot get the DocEntry out of the order object
                // Else, we will use a small trick
                int orderDocEntry = Int32.Parse(oCompany.GetNewObjectKey(), CultureInfo.InvariantCulture);

                // The order was successfully added!
                // Preparing the invoice payload
                oInvoice.CardCode = oOrder.CardCode;
                oInvoice.DocDate = oOrder.DocDate;
                oInvoice.DocDueDate = oOrder.DocDueDate;
                oInvoice.Lines.ItemCode = oOrder.Lines.ItemCode;
                oInvoice.Lines.Quantity = oOrder.Lines.Quantity;
                oInvoice.Lines.Price = oOrder.Lines.Price;
                oInvoice.Lines.SetCurrentLine(0); // Not needed, for the sake of clarity
                oInvoice.Lines.BaseEntry = orderDocEntry;
                oInvoice.Lines.BaseLine = 0;
                oInvoice.Lines.BaseType = (int)SAPbobsCOM.BoObjectTypes.oOrders;

                if (oInvoice.Add() != 0)
                {
                    throw new Exception(oCompany.GetLastErrorDescription());
                }

                // Both order and invoices were successfully added, commit transaction!
                oCompany.EndTransaction(SAPbobsCOM.BoWfTransOpt.wf_Commit);
                //  ***************** END TRANSACTION *****************
            }
            catch (Exception ex)
            {
                // Should any DI operation fails, the transaction is automatically rollback
                // We don't need to call EndTransaction here

                // However, we can display the error message :)
                Console.WriteLine(ex.Message);
            }
				
			

Advantages

You can’t beat the DI-API flexibility. After opening the transaction with the use of oCompany.StartTransaction(), you can pretty much do whatever you want. This is both useful and dangerous.

Drawbacks

The DI-API is not meant to be used for a Web API project where lots of concurrent requests can be received and treated. However, you can perfectly use this in either a stand-alone program or an add-on.

Don’t forget:

With great power comes great responsibility.
Uncle Ben
After finding out he froze all B1 clients of his customer.

Service Layer - Batch without references

If you’ve worked with the service layer before, you may have noticed that it does support batch operations. What does that mean? Taken from the documentation:

Service Layer supports executing multiple operations sent in a single HTTP request through the use of batching. A batch request must be represented as a Multipart MIME (Multipurpose Internet Mail Extensions) v1.0 message.

SAP Document - Working with SAP Business One Service Layer

Let’s start with a very simple case where we’ll add two new items (OITM records) in the system with a single API call:

Service layer request (Begin)

POST     https://:50000/b1s/v1/$batch
Content-Type: multipart/mixed;boundary=ad20e139-15ad-448b-9fe6-9af9d32c6ceb

				
					--ad20e139-15ad-448b-9fe6-9af9d32c6ceb
Content-Type: multipart/mixed; boundary=changeset_2c0c74e9-52df-4816-992e-beed5a3a1b0c

--changeset_2c0c74e9-52df-4816-992e-beed5a3a1b0c
Content-Type: application/http; msgtype=request
content-transfer-encoding: binary
Content-ID:1

POST /b1s/v1/Items

{
    "ItemCode":"A0001",
    "ItemName": "A first item"
}
--changeset_2c0c74e9-52df-4816-992e-beed5a3a1b0c
Content-Type: application/http; msgtype=request
content-transfer-encoding: binary
Content-ID:2

POST /b1s/v1/Items

{
    "ItemCode":"A0002",
    "ItemName": "A second item"
}
--changeset_2c0c74e9-52df-4816-992e-beed5a3a1b0c--

--ad20e139-15ad-448b-9fe6-9af9d32c6ceb--

				
			
Service layer request (End)

I have highlighted lines 7 and 18 because they identify both requests with a unique id (with the use of the Content-ID header), this will be important for the next part.

This all works pretty well! We’ve created our two items, but they do not share any common property. And we don’t need any information from Item #1 in order to create Item #2.

This is slightly different from what we’ve seen above where we needed the information of the order in order to create the invoice.

So how can we achieve the same thing in such case?

Advantages

  • Easy to manage, you only need to understand how to forge multipart requests.
  • A single HTTP call to perform all of your operations.

Drawbacks

  • You have less flexibility than DI-API as you can only send basic CRUD operations packed together.

Service Layer - Batch with references

For that purpose, the OData protocol introduced a keyword to reference an object within the same batch payload. Taken from the documentation:

Referencing Content ID: New entities created by a POST request within a change set can be referenced by subsequent requests within the same change set by referring to the value of the Content-ID header prefixed with a $ character. When used in this way, $ acts as an alias for the URI that identifies the new entity.

SAP Document - Working with SAP Business One Service Layer
Let’s now try to reproduce our scenario where we want to add an order and its linked invoice in a single API call.
In the payload below, the DocEntry of the order is then used in the invoice payload as $1 to fill in the BaseEntry of the invoice line.
Service layer request (Begin)

POST     https://:50000/b1s/v1/$batch
Content-Type: multipart/mixed;boundary=ad20e139-15ad-448b-9fe6-9af9d32c6ceb

				
					--ad20e139-15ad-448b-9fe6-9af9d32c6ceb
Content-Type: multipart/mixed; boundary=changeset_2c0c74e9-52df-4816-992e-beed5a3a1b0c

--changeset_2c0c74e9-52df-4816-992e-beed5a3a1b0c
Content-Type: application/http; msgtype=request
content-transfer-encoding: binary
Content-ID:1

POST /b1s/v1/Orders

{
    "CardCode":"C001",
    "DocDate":"2022-05-07",
    "DocDueDate":"2022-05-30",
    "DocumentLines":[
        {
            "LineNum":0,
            "ItemCode": "A0001",
            "Quantity":1,
            "Price": 50
        }
    ]
}
--changeset_2c0c74e9-52df-4816-992e-beed5a3a1b0c
Content-Type: application/http; msgtype=request
content-transfer-encoding: binary
Content-ID:2

POST /b1s/v1/Invoices

{
    "CardCode":"C001",
    "DocDate":"2022-05-07",
    "DocumentLines":[
        {
            "LineNum":0,
            "ItemCode": "A0001",
            "Quantity":1,
            "Price": 50,
            "BaseType" : 17,
            "BaseEntry" : $1,
            "BaseLine" : 0
        }
    ]
}
--changeset_2c0c74e9-52df-4816-992e-beed5a3a1b0c--

--ad20e139-15ad-448b-9fe6-9af9d32c6ceb--

				
			
Service layer request (End)

This single API call contains:

  1. The creation of the Sales Order (Content-ID 1)
  2. The creation of the A/R Invoice (Content-ID 2)

I have highlighted lines 7 and 41 because this is where the magic happens:

  • The Sales Order payload is identified with Content-ID 1.
  • In the A/R Invoice line #0, we reference the order’s DocEntry (that it not known at this point) by providing the Content-ID number prefixed with a dollar sign $.

Should either one of these two requests fails, both documents won’t be added to the system!

Graphically:

Add two documents in a single Service Layer call

You could also use the b1s/v1/$batch endpoint to send two independent requests by specifying multiple changesets. In this case, all requests inside the payload will be treated without consequences on the rest. Meaning that you can have one failed requests amongst the payload.

Service Layer - With user-transactions

Here is the interesting part, how can we get :

  • A tool that is adapted for web architectures
  • With the same flexibility as the DI-API (C#)

The answer lies in service layer script extensions, relying on the B1 Javascript SDK.

⚠️ SAP limits the number of operations to 10 within a same javascript-extension transaction.

Since I got a MSSQL B1 test environement, I can find the B1 JS SDK in:

				
					C:\Program Files\SAP\SAP Business One ServerTools\ServiceLayer\scripts\b1s_sdk
				
			

The SDK is also available for Service Layer version for SAP HANA.

Coding the script

Let’s see how that works for us by creating a small sample and using the B1 Javascript SDK. For that, we will create a directory and open it with VSCode.

				
					mkdir b1sl-jssdk-sample
cd b1sl-jssdk-sample
code .
				
			

Then, we will create a new file called main.js (the name is not important) and create an entry function for each of the HTTP Verbs : GET, POST, PATCH and DELETE:

				
					//The entry function for http request with the GET method
function GET() {}

//The entry function for http request with the GET method
function POST() {}

//The entry function for http request with the GET method
function PATCH() {}

//The entry function for http request with the GET method
function DELETE() {}
				
			

In order to use the SDK, we will copy/paste the whole content of the b1s_sdk folder in it. Both main.js and SDK files need to be in the same directory.

If, like me, you’re using VSCode, this is how it should look like at this point:

We will now load all relevant files we’d need for our scenario by adding these 5 lines of code at the beginning of our file:

				
					var HttpModule = require("./HttpModule");
var ServiceLayerContext = require("./ServiceLayerContext");
var Document = require("./EntityType/Document");
var DocumentLine = require("./ComplexType/DocumentLine");

//The entry function for http request with the GET method
function GET() {}

//The entry function for http request with the GET method
function POST() {}

//The entry function for http request with the GET method
function PATCH() {}

//The entry function for http request with the GET method
function DELETE() {}

				
			

Let’s now try and reproduce our DI-API scenario with the javascript SDK. There are some slight differences but you should understand pretty well what’s going on.

The code below is a rough translation of the C# code:

				
					//The entry function for http request with the GET method
function POST() {
  console.log("POST Method was called.");

  let oServiceLayerContext = new ServiceLayerContext();

  // Preparing the order payload
  // We will prepare as many things as possible before starting the transaction
  let oOrder = new Document();
  oOrder.CardCode = "C001";
  oOrder.DocDate = "2022-01-01";
  oOrder.DocDueDate = "2022-01-31";

  let oOrderLine = new DocumentLine();
  oOrderLine.ItemCode = "A0001";
  oOrderLine.Quantity = 1;
  oOrderLine.Price = 50;

  oOrder.DocumentLines = [oOrderLine];

  console.log(JSON.stringify(oOrder));

  try {
    // Starting our transaction
    oServiceLayerContext.startTransaction();

    /** @type {SAPB1.DataServiceResponse} */
    let oAddResult = oServiceLayerContext.Orders.add(oOrder);

    console.log("*** Order add result ***");
    console.log(JSON.stringify(oAddResult, null, 4));

    if (!oAddResult.isOK()) {
      throw HttpModule.ScriptException(
        HttpModule.HttpStatus.HTTP_BAD_REQUEST,
        oAddResult.getErrMsg()
      );
    }

    // The order was successfully added!
    // Preparing the invoice payload
    let oInvoice = new Document();
    oInvoice.CardCode = oOrder.CardCode;
    oInvoice.DocDate = oOrder.DocDate;
    oInvoice.DocDueDate = oOrder.DocDueDate;

    let oInvoiceLine = new DocumentLine();
    oInvoiceLine.ItemCode = oOrderLine.ItemCode;
    oInvoiceLine.Quantity = oOrderLine.Quantity;
    oInvoiceLine.Price = oOrderLine.Price;

    // Unlike the DI-API, we can directly get the DocEntry of the newly-created invoice out of the SL response
    oInvoiceLine.BaseEntry = oAddResult.body.DocEntry;
    oInvoiceLine.BaseLine = 0;
    oInvoiceLine.BaseType = 17; // Object type of Sales Order is 17

    oInvoice.DocumentLines = [oInvoiceLine];

    // Log invoice content
    console.log(JSON.stringify(oInvoice));

    // Finally, add the invoice
    oAddResult = oServiceLayerContext.Invoices.add(oInvoice);

    console.log("*** Invoice add result ***");
    console.log(JSON.stringify(oAddResult, null, 4));

    if (!oAddResult.isOK()) {
      throw HttpModule.ScriptException(
        HttpModule.HttpStatus.HTTP_BAD_REQUEST,
        oAddResult.getErrMsg()
      );
    }

    // If both order and invoice were added, commit transaction
    oServiceLayerContext.commitTransaction();

    console.log("POST Method ended.");

    // We will return an HTTP Status Code 201 (Created) + the information of both order and invoice
    HttpModule.response.send(HttpModule.HttpStatus.HTTP_CREATED, {
      Order: oOrder,
      Invoice: oInvoice,
    });
  } catch (oError) {
    // Unlike the DI-API, we need to manually rollback the transaction
    oServiceLayerContext.rollbackTransaction();

    // Return an HTTP error response
    HttpModule.response.send(HttpModule.HttpStatus.HTTP_BAD_REQUEST, oError);
  }
}
				
			

Side note: I am calling the console.log method that will ultimately logs information in the Service Layer installation folder.

				
					C:\Program Files\SAP\SAP Business One ServerTools\ServiceLayer\logs\script
				
			

We will prevent the user from calling the other three methods by simply providing an HTTP response code

				
					//The entry function for http request with the GET method
function GET() {
  HttpModule.response.send(HttpModule.HttpStatus.HTTP_METHOD_NOT_ALLOWED, null);
}

//The entry function for http request with the GET method
function PATCH() {
  HttpModule.response.send(HttpModule.HttpStatus.HTTP_METHOD_NOT_ALLOWED, null);
}

//The entry function for http request with the GET method
function DELETE() {
  HttpModule.response.send(HttpModule.HttpStatus.HTTP_METHOD_NOT_ALLOWED, null);
}
				
			

⚠️ I would strongly advise you to log your actions through the console.log(...) method as you cannot debug the script directly from VSCode. This is also one of the drawbacks of this method it lacks debugging support, making it unfit for big projects.

Deployment

You can deploy the script in the same way you would deploy a lightweight extension. 

To do so, simply create an ard file (I called it myextension.ard but the name has no impact) next to your main.js file with the following content:

				
					<?xml version="1.0" encoding="utf-8"?>
<AddOnRegData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    SlientInstallation=""
    SlientUpgrade=""
    Partnernmsp="TheJonathanPapaCompany"
    SchemaVersion="3.0"
    Type="ServiceLayerScript"
    OnDemand=""
    OnPremise=""
    ExtName="OrderInvoice"
    ExtVersion="1.00"
    Contdata="sa"
    Partner="TheJonathanPapaCompany"
    DBType="Both"
    ClientType="S">
    <ServiceLayerScripts>
        <Script Name="OrderInvoice"
            FileName="main.js"></Script>
    </ServiceLayerScripts>
    <XApps>
        <XApp Name=""
            Path=""
            FileName="" />
    </XApps>
</AddOnRegData>
				
			

At this point, the project should looke like that:

Finally, zip both the myextension.ard and main.js files (do not put the sdk files into the archive).

I called the archive myextension.zip but the file name has no importance for the rest of the process.

The resulting .zip archive can then be loaded through the Extension Manager (as you would load a lightweight extension add-on for SAP B1).

import-1
Importing the script - Uploading the .zip

Don’t forget to assign it to your favorite company (for me, that is 🇦🇺 ❤️).

import-3
Importing the script - Company Assignment
Testing

You have to restart the service layer after importing the script.

Once this is done, you can easily test your script with Postman or any HTTP request creation tool.

postmansuccess
It works!

Advantages

  • As flexible as the DI-API can be! Beside, if you’re familiar with it, you’d pretty much find the same methods.
  • Adapted to Web API design.
  • Easy to deploy with the Extension Manager (same procedure as with add-ons).

Drawbacks

  • Limited to 10 operations per transaction (that’s not really an issue, it’s not likely that you would pack so many transactions together unless you’re making data import).
  • Not debuggable (you have to deploy and check the logs to understand what’s going on).
  • Javascript (it pretty much says it all).

Conclusion

So far, we’ve seen 3 different methods of performing « grouped operations »:

  • Via the DI-API by using both StartTransaction() and EndTransaction() methods
  • Via the Service Layer using the /$batch endpoint.
    • With non-coupled business objects (two independant items)
    • With coupled business objects (an order and its invoice)
  • Via script extension of the Service Layer, using a javascript file.

I hope this article gives you a better view of the options you have for building interfaces with SAP B1. 

Then, it’s up to you to choose which method you’ll use for your developement. Of course, every project is different. Just keep in mind that the DI-API is not adapted for web api while the service layer is. And that transactions come at a high cost whether they’re being used from the DI-API or the Service Layer. 😉

Cheers 🤙🏼

Retour en haut