Skip to Content

B1 Service Layer: Develop extensions by embedding JavaScript

Since 9.2 Patch 04, Service Layer (SL) allows users to develop their own extension application by embedding JavaScript in the server side.

1. JavaScript Parsing Engine

SL makes use of Chrome V8 Engine as the JavaScript parsing engine due to the following considerations.

  • The parsing performance is of significant importance for SL. V8 is just the script engine known for its excellent performance.
  • SL and V8 are both written in C++. This would make the integration more seamless and more easily.

The V8 JavaScript Engine is an open source JavaScript engine developed by The Chromium Project for the Google Chrome web browser. To get more details of V8, access https://developers.google.com/v8 .

2. JavaScript Extension Framework

To facilitate the development of extension application, SL provides a JavaScript framework for users to easily operate the business objects and services. The diagram as below shows the basic structure of the framework.


[Note]

  • Besides the DICore and SLCore, V8, as a new C++ component, is integrated into SL.
  • SL adds the C++/JavaScript interop layer to be responsible for the interaction between JavaScript and C++.
  • On top of the interop layer, JavaScript SDK is designed to hiding the interactive details and provide a high level and simplified API for the application layer.
  • Considering the fact that switching the context between C++ and JavaScript stack is not good for the performance, one target of providing the SDK is to decrease the context switching frequency.
  • Users' JavaScript Extension application is suggested to be developed based on the JavaScript SDK.

3. JavaScript Entry Function

As each executable file has a main entry function, each script file has to define entry functions. Conventionally, it is better to define four entry functions in each script file, just corresponding to the CRUD operations on entities.

Each entry function has the same name with the HTTP method. On receiving a request, the entry function having the same name with the http method of this request is triggered.

//The entry function for http request with the GET method

function GET(){

...

}

//The entry function for http request with the POST method

function POST(){

...

}

//The entry function for http request with the PATCH method

function PATCH(){

...

}

//The entry function for http request with the DELETE method

function DELETE(){

...

}

[Note]

  • Due to a keyword compatibility issue in JavaScript, each entry function should be in upper-case, otherwise the function would be unrecognized.

4. JavaScript URL Mapping

Script files are triggered to run by sending requests to the specific script URL. To differentiate the script URL from the normal URL, SL provides a specific URL resource path for script by appending /script to the original path /b1s/v1 as below:

/b1s/v1/script/ 

Considering the fact that different partner might define the script with the same name, SL identifies which script to run by combining the partner name and the script name as the unique identifier. The mapping rule for the URL pattern is:

/b1s/v1/script/{{partner name}}/{{script name}} 

Requests sending to URLs with the above pattern are dispatched to the corresponding script function defined by the corresponding partner.

[Example]

The following request

POST /b1s/v1/script/mtcsys/items 

will trigger the execution of the function POST defined in item.js provided by partner mtcsys.

[Note]

  • The prerequisite is to ensure the script file with the corresponding ard file are deployed into SLD by the partner. For more details about how to deploy scripts, please refer to chapter [JavaScript Deployment] .

5. JavaScript SDK

Similar with the DIAPI, the JavaScript SDK is intended to provide a group of APIs for programmers to easily operate on business services and business Objects. The APIs consist of entity CRUD, entity query, transactions, exceptions and http request/response.

JavaScript, as a weak-typed programming language, is built in with many favorable dynamic features. However, for the sake of programming experience and coding efficiency, the JavaScript SDK is designed like a static-language library, so as to make most use of the auto-complete and IntelliSense functionalities provided by the modern IDE. The recommended one is the Visual Studio 2013/2015 with a Node.js plug-in (https://www.visualstudio.com/en-us/features/node-js-vs.aspx), as shown below:

Admittedly, you can also choose to program dynamically and enjoy the flexible features born with JavaScript.

[Note]

  • This SDK is designed to purposely follow the Common JavaScript Specification and approximates the Node.js grammar, which is exactly the reason why the Node.js plug-in is recommended.
5.1 Http Request API

Http request functions as below are packaged in the module HttpModule.js, which is an essential module to be required to handle http request.

API NameAPI Description
getContet()Returns the raw content from the request payload.
getJsonObject()Returns the JSON format of the request payload.
getMethod()Returns the Htttp Verb, e.g. GET, POST, PATCH, DELETE.
getContentType()Returns the MIME type of the request body (e.g. APPLICATION/JSON)
getParameter(name)Returns the value of a request parameter as a String, or null if the parameter does not exist.
getParameterNames()Returns an array of String objects containing the names of the parameters contained in this request.
getEntityKey()Returns the entity key from the URL resource part.
getHeader(name)Returns the value of the specified request header as a String.
5.2 Http Response API

Http response functions as below are packaged in the module HttpModule.js, which is an essential module to be required to handle http response.

API NameAPI Description
setHeader(name, value)Adds a response header with the given name and value.
setContentType(contentType)Set the content type of the response being sent to client.
setCharSet(charset)Sets the character encoding (MIME charset) of the response being sent to the client, for example, to UTF-8.
setStatus(status)Sets the status code for this response.
setContent(content)Set the content in the response body
send(status, content)Send back the response to the client with the optional http status and content

[Example]

For such a request as below,

PATCH /b1s/v1/script/mtcsys/items('i001')?key1=val1 & key2=val2

DataServiceVersion:3.0 


{ "ItemName": "new name" }

apply the following script to handle this request:

var http = require('HttpModule.js');

function PATCH() {

    console.log("testing the http request and http response API...")

    var ret = {};

    ret.content = http.request.getJsonObj();

    ret.method = http.request.getMethod();

    ret.contentType = http.request.getContentType();

    ret.dataServiceVersion = http.request.getHeader("DataServiceVersion");

    ret.paramNames = http.request.getParameterNames();

    if (ret.paramNames && ret.paramNames.length) {

        ret.paramNames.forEach(function (param) {

            ret[param] = http.request.getParameter(param);

        });

    }

    ret.key = http.request.getEntityKey();

    http.response.setContentType(http.ContentType.APPLICATION_JSON);

    http.response.setStatus(http.HttpStatus.HTTP_OK);

    http.response.setContent(ret);

    http.response.send();

}

On success, SL returns:

HTTP/1.1 200 OK

{

"content": { "ItemName": "new name" },

"method": "PATCH",

"contentType": "text/plain;charset=UTF-8",

"dataServiceVersion": "3.0",

"paramNames": [ "key1", "key2" ],

"key1": "val1",

"key2": "val2",

"key": "'i001'"

}

[Note]

  • Similar as Node.js, require is a global function to import a module and return a reference to that module. The above example indicates http is a reference of the module HttpModule.js.
  • request and response are two members of http, representing a pre-created HttpRequest instance of and a HttpResponse instance respectively.
  • To facilitate HTTP programming, module HttpModule.js also defines HTTP utility constants, e.g. HttpStatus, ContentType.
5.3 Entity CRUD API

Each exposed entity supports CRUD operations by default. The relevant APIs are packaged in the module ServiceLayerContext.js.

  • For most cases, to perform CRUD operations on an entity, an entity instance has to be created at first if the entity name is known in advance. Then call the following group of APIs defined in the prototype of EntitySet:

[Prototype of EntitySet]

API NameAPI Description
add(content, callback)Create an entity by the content and the optional callback function on creation.
get(key, callback)Retrieve an entity by the key and the optional callback function on retrieval.
update(content, key, callback)Update an entity by the content, key and the optional callback function on update.
remove(key, callback)Remove an entity by the key and the optional callback function on removal.
......
  • For the scenario where the entity name is not know in advance or the entity is a dynamically created UDO, a ServiceLayerContext instance has to be created at first. Then call the following group of APIs against this instance.

[Prototype of ServiceLayerContext]

API NameAPI Description
add(name, content, callback)Create an entity by the name, content and the optional callback function on creation.
get(name, key, callback)Retrieve an entity by the name, key, and the optional callback function on retrieval.
update(name, content, key, callback)Update an entity by the name, content and key and the optional callback function on update.
remove(name, key, callback)Remove an entity by the name, key and the optional callback function on removal.
......

[Example]

For such a request as below,

POST /b1s/v1/script/mtcsys/test_items_more  

apply the following script to handle this request:

var ServiceLayerContext = require('ServiceLayerContext.js');

var Item = require('EntityType/Item.js');

var http = require('HttpModule.js');

var test_item_code = "i001";

function POST() {

    var slContext = new ServiceLayerContext();

    var ret = [];

    var item = new Item();

    item.ItemCode = test_item_code;

    var dataSrvRes = slContext.Items.add(item);

    if (!dataSrvRes.isOK()) {

        throw http.ScriptException(http.HttpStatus.HTTP_BAD_REQUEST, "create entity failure")

    }

    ret.push({ "operation": dataSrvRes.operation, "status": dataSrvRes.status });

    var key = test_item_code;

    var dataSrvRes = slContext.Items.get(key);

    if (!dataSrvRes.isOK()) {

        throw http.ScriptException(http.HttpStatus.HTTP_INTERNAL_SERVER_ERROR, "retrieve entity failure")

    }

    ret.push({ "operation": dataSrvRes.operation, "status": dataSrvRes.status });

    item.ItemName = 'new_item_name';

    dataSrvRes = slContext.update("Items", item, key);//equivalent to slContext.Items.update(item, key);

    if (!dataSrvRes.isOK()) {

        throw http.ScriptException(http.HttpStatus.HTTP_INTERNAL_SERVER_ERROR, "update entity failure")

    }

    ret.push({ "operation": dataSrvRes.operation, "status": dataSrvRes.status });

    dataSrvRes = slContext.remove("Items", key);//equivalent to slContext.Items.remove(key);

    if (!dataSrvRes.isOK()) {

        throw http.ScriptException(http.HttpStatus.HTTP_INTERNAL_SERVER_ERROR, "delete entity failure")

    }

    ret.push({ "operation": dataSrvRes.operation, "status": dataSrvRes.status });

    http.response.send(http.HttpStatus.HTTP_OK, ret);

}

On success, SL returns:

HTTP/1.1 200 OK

[

{ "operation": "add", "status": 201 },

{ "operation": "get", "status": 200 },

{ "operation": "update", "status": 204 },

{ "operation": "remove", "status": 204 }

]

5.4 Entity Query API

Query APIs are packaged in the module ServiceLayerContext.js, and similar with the CRUD API, they are defined both on the EntitySet and the ServiceLayerContext prototype.

[Prototype of EntitSet]

API NameAPI Description
query(queryOption, isCaseInsensitive)Perform a case-sensitive or case-insensitive query and return the entities satisfying the query options.
count(queryOption, isCaseInsensitive)Perform a case-sensitive or case-insensitive query and return the number of the entities satisfying the query options.
......

[Prototype of ServiceLayerContext]

API NameAPI Description
query(name, queryOption, isCaseInsensitive)Perform a case-sensitive or case-insensitive query and return the entities with the given name and satisfying the query options.
count(name, queryOption, isCaseInsensitive)Perform a case-sensitive or case-insensitive query and return the number of the entities with the given name and satisfying the query options.
......

[Note]

  • By default, the query is case-sensitive, due to the default Unicode collation for Hana database.
  • Specifying the flag isCaseInsensitive as true would issue a case insensitive query. However, the query performance would not be as efficient as the case-insensitive case.

[Example]

For such a request as below,

GET /b1s/v1/script/mtcsys/test_query_businesspartner  

apply the following script to handle this request:

var ServiceLayerContext = require('ServiceLayerContext.js');

var http = require('HttpModule.js');

function GET() {

    var queryOption = "$select=CardName, CardCode & $filter=contains(CardCode, 'c1') & $top=5 & $orderby=CardCode";

    var slContext = new ServiceLayerContext();

    var retCaseSensitive = slContext.BusinessPartners.query(queryOption);

    var retCaseInsensitive = slContext.query("BusinessPartners", queryOption, true);

    http.response.setStatus(http.HttpStatus.HTTP_OK);

    http.response.setContent({ "CaseSensitive": retCaseSensitive.toArray(), "CaseInsensitive": retCaseInsensitive.toArray() });

    http.response.send();

}

On Success, SL returns:

HTTP/1.1 200 OK

{

  "CaseSensitive": [

    {

      "CardCode": "c1",

      "CardName": "customer c11"

    }

  ],

  "CaseInsensitive": [

    {

      "CardCode": "c1",

      "CardName": "customer c11"

    },

    {

      "CardCode": "C11",

      "CardName": null

    },

    {

      "CardCode": "C12",

      "CardName": null

    }

  ]

}

5.5 Transaction API

Transaction APIs as below are packaged in the module ServiceLayerContext.js, which is an essential module to be required to control transactions.

API NameAPI Description
startTransactionStart a transaction.
commitTransactionCommit a transaction.
rollbackTransactionRollback a transaction
isInTransactionreturn true if the current operation is in a transaction

[Example]

For such a request as below,

POST /b1s/v1/script/mtcsys/test_create_businesspartner

[

  {

    "CardCode": "c001",

    "CardName": "c001"

  },

  {

    "CardCode": "c002",

    "CardName": "c002"

  },

  {

    "CardCode": "c003",

    "CardName": "c003"

  },

  {

    "CardCode": "c004",

    "CardName": "c004"

  },

  {

    "CardCode": "c005",

    "CardName": "c005"

  }

]

apply the following script to handle this request:

var ServiceLayerContext = require('ServiceLayerContext.js');

var http = require('HttpModule.js');

var BusinessPartner = require('EntityType/BusinessPartner.js');

function POST() {

    var slContext = new ServiceLayerContext();

    var bpList = http.request.getJsonObj();

    if (!(bpList instanceof Array)) {

        throw http.ScriptException(http.HttpStatus.HTTP_BAD_REQUEST, "invalid format of payload");

    }

    slContext.startTransaction();

    for (var i = 0; i < bpList.length; ++i) {

        var res = slContext.BusinessPartners.add(bpList[i]);

        if (!res.isOK()) {

            slContext.rollbackTransaction();

            throw http.ScriptException(http.HttpStatus.HTTP_BAD_REQUEST, res.getErrMsg());

        }

    };

    slContext.commitTransaction();

    http.response.setContentType(http.ContentType.TEXT_PLAIN);

    http.response.send(http.HttpStatus.HTTP_OK, "transaction committed");

}

On Success, SL returns:

HTTP/1.1 200 OK

transaction committed

Send this request again, SL returns:

HTTP/1.1 400 Bad Request

{

  "error": {

    "code": 600,

    "message": {

      "lang": "en-us",

      "value": "1320000140 - Business partner code 'c001' already assigned to a business partner; enter a unique business partner code"

    }

  }

}

[Note]

  • Programmers should be aware of the transaction operations are expensive and big transactions are bad for web service throughput. Thus, SL imposes a limitation on the transaction size. The total operations in one transaction should be no more than 10.
  • Please keep in mind startTransaction/commitTransaction or startTransaction/rollbackTransaction should be called in pair.
5.6 Exception API
5.6.1 Compile Exception

SL responds error message to client if there is compilation error in users' script.

[Example]

var Document = require('EntityType/Document.js');

//type mistake: ';' should be ','

var line = Document.DocumentLine.create({

  ItemCode: 'i001'; Quantity: 2, UnitPrice: 10

});

var lines = new Document.DocumentLineCollection();

lines.add(line);

The above code would result in such an error message as below:

{

  "error": {

    "code": 511,

    "message": {

      "lang": "en-us",

      "value": "Script error: compile error [SyntaxError: Unexpected token ;]."

    }

  }

}

5.6.2 Runtime Exception

SL responds error message to client if there is runtime error in users' script.

[Example]

var ServiceLayerContext = require('ServiceLayerContext.js');

//var Bank = require('EntityType/Bank.js');

var bank = new Bank();

bank.BankCode = 'bank01';

var res = new ServiceLayerContext().Banks.add(bank);

if (!res.isOK) {

}

The above code would result in such an error message as below:

HTTP/1.1 400 Bad Request

{

  "error": {

    "code": 512,

    "message": {

      "lang": "en-us",

      "value": "Script error: runtime error [ReferenceError: Bank is not defined]."

    }

  }

}

5.6.3 Users' Exception

SL also allows users to explicitly propagate exception by throwing ScriptException exported from the http module.

[Example]

var ServiceLayerContext = require('ServiceLayerContext.js');

var Order = require('EntityType/Document.js');

var http = require('HttpModule.js');

var slContext = new ServiceLayerContext();

var res = slContext.Orders.get(10000);

if (!res.isOK()) {

  throw new http.ScriptException(http.HttpStatus.HTTP_NOT_FOUND, "the given order is not found");

}

The above code would result in such an error message as below:

HTTP/1.1 404 Not Found

{

  "error": {

    "code": 600,

    "message": {

      "lang": "en-us",

      "value": "the given order not found"

    }

  }

}

6 Logging

Currently debugging script is not supported. However, uses are allowed to log the key information during script programming by using the API console.log

console.log('Hello, Service Layer Scripting!');

[Note]

  • console is a global object. Literally, the output of this object should be printed in the console. However, considering SL is a backend service, the output is redirected to log files under {SL Installation Path}/logs/script/

7 JavaScript SDK Generator Tool

Considering in each patch there might be new business objects exposed or new changes made on the existing objects, the SDK would be adjusted to adapt to the changes accordingly.

To manually maintain the SDK would not only need huge efforts, but also be error-prone. To automatically address this issue, a tool named Metadata2JavaScript is provided to generate the SDK according to the metadata, as metadata reflects all changes on the business objects.

This tool supports to generate the SDK in two ways:

[ From a local metadata file ]

Metadata2JavaScript -a {local metadata file} -o {output folder, default is ./b1s_sdk}

or

Metadata2JavaScript --addr {local metadata file} --output {output folder, default is ./b1s_sdk}

For example:

Metadata2JavaScript -a metadata.xml -o ./output

[ From a remote SL instance ]

Metadata2JavaScript -a {SL base url} -u {user} -p {password} -c {company} -o {output folder, default is ./b1s_sdk}

or

Metadata2JavaScript --addr {SL base url} --user {user} --password {password} --company {company} --output {output folder, default is ./b1s_sdk}

For example:

Metadata2JavaScript --addr https://10.58.136.174:50000/b1s/v1/ --user manager --password 1234 --company SBODEMOUS

[Note]

  • This tool is released together with SL and available in the bin folder of the SL installation path.

8 JavaScript Deployment

SL reuses the extension manager to manage the life cycle of script files. Similar with the DIAPI add-on, extension applications developed by SL are deployed to SLD as well.

Assume you have a script file Items.js, take the following steps to deploy it.

1. Create an ard file named Items.ard as the below format to describe the meta of this script file. Meanwhile, the ard file is specifically used for the purpose of determining the script URL path.

<?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="mtcsysnm" SchemaVersion="3.0"

  Type="ServiceLayerScript" OnDemand="" OnPremise="" ExtName="ItemsExt"

  ExtVersion="1.00" Contdata="sa" Partner="mtcsys" DBType="HANA" ClientType="S">

  <ServiceLayerScripts>

  <Script Name="items" FileName="Items.js"></Script>

  </ServiceLayerScripts>

  <XApps>

  <XApp Name="" Path="" FileName="" />

  </XApps>

</AddOnRegData>

2. Compress the ard file and script file into a zip file (e.g. Items.zip).

3. Upload Items.zip to the extension manager from the Extension Import Wizard.

4. Assign the extension application to one company from the Extension Assignment Wizard.

5. Login the company with SL and access the script by the following URL.

/b1s/v1/script/mtcsys/items 

[Note]

  • The script URL is a combination of partner name and the script name attribute separated by a '/' appended to the SL base URL /b1s/v1/
  • Currently, SL does not support compressing multiple script files into one ard file.
  • About more details about how to deploy extension applications, please look up the release documentation of SAP Business One Extension Manager.
  • Do not name the value of the attribute Partner as test, as test is a reserved word for internal testing.

9 Typical User Cases to Apply Script

9.1 Complex Transactions

Scripting can be used in the transaction scenarios, which is an important complement to the OData Batch operations. The following example is to add an order and a delivery based on the order in one transaction, which would be impossible without scripting.

[Example]

var ServiceLayerContext = require('ServiceLayerContext.js');

var http = require('HttpModule.js');

var Order = require('EntityType/Document.js');

var DeliveryNote = require('EntityType/Document.js');

/*

* Entry function for the POST http request.

*

*/

function POST() {

    var order = new Order();

    order.CardCode = 'c1';

    order.DocDate = new Date();

    order.DocDueDate = new Date();

    var line = new Order.DocumentLine();

    line.ItemCode = 'i1';

    line.Quantity = 1;

    line.UnitPrice = 10;

    var line2 = new Order.DocumentLine();

    line2.ItemCode = 'i2';

    line2.Quantity = 1;

    line2.UnitPrice = 10;

    order.DocumentLines = new Order.DocumentLineCollection();

    order.DocumentLines.add(line);

    order.DocumentLines.add(line2);

    var slContext = new ServiceLayerContext();

    //start the transaction

    slContext.startTransaction();

    var res = slContext.Orders.add(order);

    if (!res.isOK()) {

        slContext.rollbackTransaction();

        return http.response.send(http.HttpStatus.HTTP_BAD_REQUEST, res.body);

    }

    //get the newly created order from the response body.

    var newOrder = Order.create(res.body);

    //create a delivery based on the order

    var deliveryNote = new DeliveryNote();

    deliveryNote.DocDate = newOrder.DocDate;

    deliveryNote.DocDueDate = newOrder.DocDueDate;

    deliveryNote.CardCode = newOrder.CardCode;

    deliveryNote.DocumentLines = new DeliveryNote.DocumentLineCollection();

    for (var lineNum = 0; lineNum < order.DocumentLines.length; ++lineNum) {

        var line = new DeliveryNote.DocumentLine();

        line.BaseType = 17;

        line.BaseEntry = newOrder.DocEntry;

        line.BaseLine = lineNum;

        deliveryNote.DocumentLines.add(line);

    }

    res = slContext.DeliveryNotes.add(deliveryNote);

    if (!res.isOK()) {

        slContext.rollbackTransaction();

        return http.response.send(http.HttpStatus.HTTP_BAD_REQUEST, res.body);

    } else {

        slContext.commitTransaction();

        return http.response.send(http.HttpStatus.HTTP_CREATED, res.body);

    }

}

9.2 Customized Business Logic (e.g. UDO)

Another typical case for scripting is to add customized business logic during the process of operating UDO. The following example is to do some validations and calculate the DocTotal when creating the UDO named MyOrder.

POST /b1s/v1/script/mtcsys/test_myorder

{

  "U_CustomerName": "c1",

  "U_DocTotal": 0,

  "MyOrderLinesCollection": [

    {

      "U_ItemName": "i1",

      "U_Price": 100,

      "U_Quantity": 3

    },

    {

      "U_ItemName": "i2",

      "U_Price": 80,

      "U_Quantity": 4

    }

  ]

}

Apply the following script to handle this above request:

function POST() {

    //Before creating the UDO, users are allowed to add extra logic.

    var myOrder = http.request.getJsonObj();

    var slContext = new ServiceLayerContext();

    //Example 1 : added logic to validate if each item exists and the item stock is enough.

    myOrder.MyOrderLinesCollection.forEach(function (line) {

        var dataSvcRes = slContext.Items.get(line.U_ItemName);

        if (!dataSvcRes.isOK()) {

            throw new http.ScriptException(http.HttpStatus.HTTP_NOT_FOUND, "item not found");

        } else {

            //Convert weak type to strong type by calling Item.create. The conversion is not a must.

            //You can also use dataSvcRes.body.QuantityOnStock

            var item = Item.create(dataSvcRes.body);

            if (item.QuantityOnStock < line.U_Quantity) {

                throw new http.ScriptException(http.HttpStatus.HTTP_BAD_REQUEST, "not enough items on stock");

            }

        }

    });

    //Example 2 : added logic to calculate the DocTotal

    myOrder.U_DocTotal = 0;

    myOrder.MyOrderLinesCollection.forEach(function (line) {

        myOrder.U_DocTotal += (line.U_Price * line.U_Quantity);

    });

    //Add this UDO

    var res = slContext.add("MyOrder", myOrder);

    if (res.isOK()) {

        http.response.send(http.HttpStatus.HTTP_CREATED, res.body);

    } else {

        http.response.send(http.HttpStatus.HTTP_BAD_REQUEST, res.body);

    }

}

10 Consume Script Service from .Net Application

For the consideration of flexibility, SL allows the response from the script is highly-customized. It is not appropriate to define the fixed meta data for the scripting, and as such, using the single WCF framework is not possible to consume the script service. As an alternative, it is suggested to turn to programming with the .NET Web Http library mixed with WCF, illustrated with the below code snippet.

[TestFixture]

    class ScriptOrdersTest : AppCommon.GeneralTestGroup

    {

        [SetUp]

        public void setup()

        {

            ServicePointManager.ServerCertificateValidationCallback += delegate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors ssl) { return true; };

            ServicePointManager.Expect100Continue = false;

            ServicePointManager.MaxServicePointIdleTime = 2000;

        }

        private string m_cookie = AppCommon.WebConnection.Instance.SessionID;

        private Uri m_baseUri = new Uri(AppCommon.ConfigInfo.Instance().SL_URL);

        private int m_docEntry = 0;

        [Test]

        public void test01_create()

        {

            Document order = new Document();

            order.CardCode = "c1";

            order.DocDate = DateTime.Now;

            order.DocDueDate = DateTime.Now;

            {

                DocumentLine line = new DocumentLine();

                line.LineNum = 1;

                line.ItemCode = "i1";

                line.Quantity = 1;

                line.UnitPrice = 10;

                order.DocumentLines.Add(line);

            }

            {

                DocumentLine line = new DocumentLine();

                line.LineNum = 2;

                line.ItemCode = "i2";

                line.Quantity = 1;

                line.UnitPrice = 10;

                order.DocumentLines.Add(line);

            }

            try

            {

                var setting = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore };

                string json = JsonConvert.SerializeObject(order, setting);

                var data = Encoding.ASCII.GetBytes(json);

                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri(m_baseUri, "script/test/test_orders"));

                request.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore);

                request.Method = "POST";

                request.KeepAlive = false;

                request.Headers["Cookie"] = m_cookie;

                request.ContentType = "application/json;odata=minimalmetadata";

                request.ContentLength = data.Length;

                using (var stream = request.GetRequestStream())

                {

                    stream.Write(data, 0, data.Length);

                }

                HttpWebResponse response = (HttpWebResponse)request.GetResponse();

                Assert.AreEqual(response.StatusCode, HttpStatusCode.Created);

                var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();

                Document newEntity = JsonConvert.DeserializeObject<Document>(responseString);

                Assert.IsTrue(newEntity.DocEntry > 0);

                Assert.AreEqual(newEntity.DocumentLines.Count(), order.DocumentLines.Count());

                response.Close();

                m_docEntry = newEntity.DocEntry;

            }

            catch (WebException ex)

            {

                WebResponse response = ex.Response;

                if (response == null)

                {

                    throw SetResultMessage(ex);

                }

                var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();

                throw SetResultMessage(new Exception(responseString));

            }

            catch (Exception ex)

            {

                throw SetResultMessage(ex);

            }

        }

Tags:
Former Member