Export (0) Print
Expand All
25 out of 29 rated this helpful - Rate this topic

Talking with Navision: Accessing Navision Business Layer through a Web Service

 

Manuel Oliveira
Microsoft Portugal

António Nobre
Gerimática

December 2004

Applies to:
   Microsoft® Navision®

Summary: Previously in this "Talking with Navision" series, you read about how to remotely and asynchronously access Navision code and data. By combining this possibility with the strength of Navision business layer, a web service can be built which exposes that layer, thus opening new paths for application integration. (21 printed pages)

Contents:

Introduction
The scenario and its architecture
Designing the communication protocol
Setting up the environment
The first Web Method: GetItem
Interacting with Navision data: InsertItem
Completing the Web Service: PostItemAdjustment
After-thoughts and conclusions

Introduction

Having an application sending a message to Navision and then receiving a response back from Navision is a form of client/server architecture. With a very simple example, the concept has been proved. Now, it is time to engineer on this and use this simple mechanism to wrap our ERP system into a web service which exposes Navision business layer to 3rd party applications.

There is not much to it, in fact. If you understand the communication model presented before in this series and if you have been developing your own web services for a while, it should be quite clear how to add a web service layer around Navision. Our main concern is on how to take each method's arguments and repack them in a way that they are understood when the request is forwarded to Navision.

Likewise, when receiving back a response, unpacking may be needed to retrieve the data and building a response which is compliant with the service specification.

This article shows you how to take a somewhat confined Navision module (further complexity takes us to a discussion beyond the scope for this document) and expose part of its functionality through a representative set of web service methods.

We could compare the "Say Hello to Navision and Expect Navision to Be Polite" and this article to J.R.R Tolkien's "The Hobbit" and "The Lord of the Rings", respectively. He showed us a whole new world with the first, but it was in the second that we found the real magic!

The scenario and its architecture

Suppose your customer runs an ERP system and you are developing a very business-specific application for them, for instance, a warehouse management tool which the warehouse employees will be able to access from their handheld devices or from any web browser.

When designing such application, you clearly see a very tight interaction between your application and the ERP system which centralizes the customer's warehouse items and surrounding information. By providing such connection between both systems, you can be sure your application will always use the latest information as it enters your ERP system.

However, when using a system like Navision, such connection means using Navision Application Server, a communication subsystem such as named pipes, sockets or message queuing and a protocol both systems must be aware of for communication purposes. This is not a problem, it works as expected. Nonetheless, from the developer's point of view, it would be a much easier task if all of the requirements above are "hidden" behind some sort of an Application Programming Interface (API).

Figure 1 depicts the proposed layered architecture which allows the wrapping of the internal communication details inside the web service layer.

Figure 1. The proposed two-layered architecture

Let us take the example used above. We will start by choosing the set of Navision functions we would like to expose and then discuss how they map onto the architecture.

At least two functions must be exposed, one that allows for data reading and another one which performs data writing. For exhaustiveness purposes, we will consider a third function which exercises the ERP business layer, thus going one step ahead of simple read/write operations:

  • GetItem (Item no.) which returns an item instance
  • InsertItem (Item no., Description, ...) which inserts a new item based on a set of field values
  • PostItemAdjustment (Item no., PostingDate, Quantity, ...) which posts an item adjustment, either positive or negative, thus exercising the business layer.

Each of these functions/procedures will have a counterpart both within Navision Database Server and at the highest level, being exposed as Web Methods.

When requesting an item find, for instance, the Web Method will get the argument (the item number) and will forward a message to Navision. The ERP will then read the message and route it to the appropriate handler within its business layer. The response, if available, is sent back as a message and the Web Method must be able to transform it according to the external representation.

Note   Although they might look similar, the data format used by Navision and externally are completely independent of each other. Their design may be similar and that will ease the translation process, but there is no formal reason for doing so.

Designing the communication protocol

As described above, there are two layers in this system, and their nature is very distinct. Our main concern will be the establishment of a common protocol both layers must be aware of and respect.

None of the layers' nature imposes a specific format so the architect is free to design the communication protocol and that is exactly what we are presenting here.

From the web service down to the business layer

When a Web Method is called, along with it goes a set of arguments. All of this information must reach the business layer which will then respond accordingly. For this purpose, instead of using a complex XML structure, we have chosen to pack both the method name and the arguments into a string, pretty much as if it were a local call:

<Method Call> ::= <Method Name> ( <Argument>* )

All that Navision has to do is parsing that string, retrieving both the method name and the arguments. Later you will see how this parsing may be performed.

From Navision back up to the web service

After the business layer has processed the request, it has to bubble the data back up to the calling Web Method, which will then return it. Now we must rely upon a somewhat complex structure which may hold all the bubbling data.

We have decided to include some complementary data along with our inventory items: general product and inventory posting groups, locations and units of measure. Figure 2 shows you a graphical representation of the schema both parties will assume.

Figure 2. The schema both layers must respect.

Later, we will get deeper into this schema. For now, let us just assume that our core entity is the item. An item is identified by its number and it contains zero or more item ledger entries. Also, three of the item's properties are validated against three tables: its base unit of measure and its product and inventory posting groups. Finally, each ledger entry has a location code which is validated against the location collection.

This schema maps onto the Navision object model. Well, at least it maps onto a very small subset of the object model and this will simplify the Navision procedure which will populate the resulting dataset.

Setting up the environment

From the viewpoint of Navision, establishing the environment is not an easy task, but once you have done it, you will recognize a pattern which may be briefly described as follows. Essentially, you ought to:

  • Ensure your Navision database is being served by either a Navision Database Server or a Microsoft SQL Server;
  • Configure a Navision Application Server, making sure it points to a specific start-up parameter value and a specific company within a specific database located on a specific server.
  • Create two message queues which will be used to support the bidirectional communication.
  • Add a new codeunit which will receive the messages, parse the requests and respond to them, making sure this codeunit is marked as single instance.
  • Edit trigger NasHandler () on Codeunit 1, adding a new case option for the Parameter based on the start-up parameter value you used on the second step and calling the codeunit you created on the fourth step.

This article will focus on the fourth and fifth steps. For the first two, please take a look at the article Talking with Navision: Say Hello to Navision and Expect Navision to be Polite and for a reference about message queuing, the Platform SDK: Message Queuing (MSMQ) is an excellent starting point.

In our development environment, we have used a WEBSERVICE start-up parameter value and we have created a new codeunit named Web Service Handler whose ID is 90000. We have added the following code (in bold) to the NasHandler trigger in codeunit 1:

IF CGNASStartedinLoop = FALSE THEN
  CASE Parameter OF
    'WEBSERVICE':
      WSHandler.RUN;
    'MAILLOG':
      CODEUNIT.RUN(CODEUNIT::"E-Mail Dispatcher");
    ELSE
      ...

The variable WSHandler is a reference to the above mentioned codeunit.

Additionally, we have Microsoft Visual Studio .NET and all its requirements for the development of ASP.NET web services. Check out the Web Services Developer Center if you are not yet familiar with web services.

The first Web Method: GetItem

In order to present the first Web Method, we will split the whole process in 4 parts:

  • Packing the method name and its single argument, the item number and sending the string to Navision;
  • Having Navision receiving the string, unpacking it and redirecting to the appropriate code which will serve the request.
  • Serving the request, which means finding the item whose number is the one received as argument, packing the data into an XML document conformant to the schema and sending it back
  • Receiving the response, validating it against the schema, building the dataset and returning it to the caller system.

Packing and sending the request to Navision

Once the Web Service has been created, we need to bring in the two message queues we created for this project. By naming them mqFromNavision and mqToNavision it should be easier to understand when to use each.

Now we are ready to create the first web method, as follows:

[Web Method]
public NavDS GetItem(string No)
{
   string request = "GetItem(" + No + ")";
   mqToNavision.Send (request, "Navision MSMQ-BA");
   ...
}

In these two lines, a message is built from the method name and its single argument and then sent to a queue. The rest of the code deals with the reception and validation of the response and will be completed below.

Having Navision parsing and redirecting the request accordingly

By adding a handler codeunit, Navision is prepared to receive the request. However, we still need to add a procedure which will parse the request, retrieving both the method name and the argument collection.

Let us assume our codeunit has two global variables:

  • Request—Text (50);
  • Parameters—Text (50) with the Dimensions property set to a number that may hold the largest amount of arguments; 200 is much more than enough.

Such a procedure might look like this:

ParseRequest(string : Text[250])
Request := COPYSTR (string, 1, STRPOS (string, '(') - 1);
auxstring := COPYSTR (string, STRPOS (string, '(') + 1, STRLEN (string) - STRPOS (string, '(') - 1);
argpos := 1;
commapos := STRPOS (auxstring, ',');
WHILE (commapos <> 0) DO
  BEGIN
    Parameters[argpos] := COPYSTR (auxstring, 1, commapos - 1);
    auxstring := COPYSTR (auxstring, STRPOS (auxstring, ',') + 1);
    argpos := argpos + 1;
    commapos := STRPOS (auxstring, ',');
  END;
Parameters[argpos] := auxstring;
ParCount := argpos;

This procedure starts by retrieving the method name from the string cutting it by the open parenthesis. Then, it loops through the comma-separated argument collection and builds the Parameters array with those values.

Now that we have a request parser, we are able to fill the code of the message received trigger like this:

CC2::MessageReceived(VAR InMessage : Automation "''.IDISPATCH")

// load the message into an XML document and find the string node
InMsg := InMessage;
InS := InMsg.GetStream();
XMLDom.load (InS);
XMLNode := XMLDom.selectSingleNode ('string');

// parse the request and according to the Request variable, redirect to
// the appropriate function
ParseRequest (XMLNode.text);
CASE Request OF
  'GetItem':
    BizLayer.GetItem (Parameters[1], XMLDom);
  ELSE
END;

The following global variables have been used in this trigger:

  • InMsg – 'Navision Communication Component version 2'.InMessage
  • InS – InStream
  • XMLDom – 'Microsoft XML, v3.0'.DOMDocument
  • XMLNode - 'Microsoft XML, v3.0'.IXMLDOMNode
  • BizLayer – Codeunit (BizLayer Entry Point)

Serving the request and responding

In the message received trigger, we redirect the request to the GetItem procedure on a new codeunit which represents the business layer entry point (in fact, the authors have named the new codeunit BizLayer Entry Point and its ID is 90001).

In order to respond to the web service methods, we have chosen to create this codeunit which will hold the Navision counterpart for each method.

We created two auxiliary procedures that help us building the XML document representing the dataset:

AddElement
  (VAR XMLNode : Automation "'Microsoft XML, v3.0'.DOMDocument";
   NodeName : Text[250];
   VAR CreatedXMLNode : Automation "'Microsoftt XML, v3.0'.IXMLDOMNode")
NewChildNode := XMLNode.ownerDocument.createNode('element', NodeName, '');
XMLNode.appendChild(NewChildNode);
CreatedXMLNode := NewChildNode;

AddAttribute
  (VAR XMLNode : Automation "'Microsoft XML, v3.0'.IXMLDOMNode";
   Name : Text[260];NodeValue : Text[260])
IF NodeValue <> '' THEN BEGIN
  XMLNewAttributeNode := XMLNode.ownerDocument.createAttribute(Name);
  XMLNewAttributeNode.nodeValue := NodeValue;
  XMLNode.attributes.setNamedItem(XMLNewAttributeNode);
END;

Now, getting back to the Navision counterpart for the GetItem specific situation, this could be the code to add:

GetItem(No : Code[30];VAR XMLDom : Automation "'Microsoft XML, v3.0'.DOMDocument")
XMLDom.loadXML ('<?xml version="1.0"?><NavDS/>');
XMLRoot := XMLDom.documentElement;
Item.SETFILTER ("No.", No);
IF Item.FIND('-') THEN
  REPEAT
    AddElement (XMLRoot,'NavItem',XMLNode);
    AddAttribute (XMLNode, 'No', Item."No.");
    AddAttribute (XMLNode, 'Description', Item.Description);
    AddAttribute (XMLNode, 'UnitCost', 
      FORMAT (Item."Unit Cost",0,'<Integer><Decimals><Comma,.>'));
    AddAttribute (XMLNode, 'UnitPrice', 
      FORMAT (Item."Unit Price",0,'<Integer><Decimals><Comma,.>'));
    Item.CALCFIELDS (Inventory);
    AddAttribute (XMLNode, 'Inventory', 
      FORMAT (Item.Inventory,0,'<Sign><Integer>'));
    AddAttribute (XMLNode, 'ProfitPercent', 
      FORMAT (Item."Profit %",0,'<Integer><Decimals><Comma,.>'));
    AddAttribute (XMLNode, 'ProductPostGr', 
      Item."Gen. Prod. Posting Group");
    AddAttribute (XMLNode, 'InvtPostGr', Item."Inventory Posting Group");
    AddAttribute (XMLNode, 'BaseUoM', Item."Base Unit of Measure");
    AddAttribute (XMLNode, 'xmlns', '');

    ItemLedgEntry.SETRANGE ("Item No.", Item."No.");
    IF ItemLedgEntry.FIND ('-') THEN
      REPEAT
        AddElement (XMLNode,'NavItemLedgerEntry',ChildNode);
        AddAttribute (ChildNode, 'EntryNo', 
          FORMAT (ItemLedgEntry."Entry No.",0,'<Sign><Integer>'));
        AddAttribute (ChildNode, 'EntryType', 
          FORMAT (ItemLedgEntry."Entry Type"));
        AddAttribute (ChildNode, 'PostingDate', 
          FORMAT (ItemLedgEntry."Posting Date"));
        AddAttribute (ChildNode, 'DocumentNo', 
          ItemLedgEntry."Document No.");
        AddAttribute (ChildNode, 'Description',
          ItemLedgEntry.Description);
        AddAttribute (ChildNode, 'Quantity', 
          FORMAT (ItemLedgEntry.Quantity,0,'<Sign><Integer>'));
        AddAttribute (ChildNode, 'InvoicedQty', 
          FORMAT (ItemLedgEntry."Invoiced Quantity",0,'<Sign><Integer>'));
        AddAttribute (ChildNode, 'RemQty', 
          FORMAT (ItemLedgEntry."Remaining Quantity",0,'<Sign><Integer>'));
        AddAttribute (ChildNode, 'LocationCode', 
          ItemLedgEntry."Location Code");
        AddAttribute (ChildNode, 'xmlns', '');
      UNTIL (ItemLedgEntry.NEXT = 0);
  UNTIL (Item.NEXT = 0);

IF UnitOfMeasure.FIND('-') THEN
  REPEAT
    AddElement (XMLRoot,'NavUnitOfMeasure',XMLNode);
    AddAttribute (XMLNode, 'Code', UnitOfMeasure.Code);
    AddAttribute (XMLNode, 'Description', UnitOfMeasure.Description);
  UNTIL (UnitOfMeasure.NEXT = 0);

IF Location.FIND('-') THEN
  REPEAT
    AddElement (XMLRoot,'NavLocation',XMLNode);
    AddAttribute (XMLNode, 'Code', Location.Code);
    AddAttribute (XMLNode, 'Name', Location.Name);
  UNTIL (Location.NEXT = 0);

IF ProductPG.FIND('-') THEN
  REPEAT
    AddElement (XMLRoot,'NavProductPGroup',XMLNode);
    AddAttribute (XMLNode, 'Code', ProductPG.Code);
    AddAttribute (XMLNode, 'Description', ProductPG.Description);
  UNTIL (ProductPG.NEXT = 0);

IF InvtPG.FIND('-') THEN
  REPEAT
    AddElement (XMLRoot,'NavInventoryPG',XMLNode);
    AddAttribute (XMLNode, 'Code', InvtPG.Code);
    AddAttribute (XMLNode, 'Description', InvtPG.Description);
  UNTIL (InvtPG.NEXT = 0);

The following variables must be declared prior to compiling the codeunit:

  • XMLRoot (global) - 'Microsoft XML, v3.0'.IXMLDOMNode
  • XMLNode (global) - 'Microsoft XML, v3.0'.IXMLDOMNode
  • ChildNode (global) - 'Microsoft XML, v3.0'.IXMLDOMNode
  • Item (global) – Record (Item)
  • ItemLedgEntry (global) – Record (Item Ledger Entry)
  • UnitOfMeasure (local) – Record (Unit of Measure)
  • Location (local) – Record (Location)
  • ProductPG (local) – Record (Gen. Product Posting Group)
  • InvtPG (local) – Record (Inventory Posting Group)

By passing a reference to XMLDom, when this procedure ends, that variable will hold a populated dataset with all the locations, all the product and inventory posting groups, all the units of measure, items and their item ledger entries.

We have decided to include all of the locations, product groups and units of measure as they represent a small amount of data and will not overburden our dataset instances.

Now that the second argument of the function has been filled with an XML document representing the dataset we would like to return, we have to complete the message received trigger by adding the code which will instantiate a message that will be sent up to the web service:

CC2::MessageReceived(VAR InMessage : Automation "''.IDISPATCH")

// load the message into an XML document and find the string node
InMsg := InMessage;
InS := InMsg.GetStream();
XMLDom.load (InS);
XMLNode := XMLDom.selectSingleNode ('string');

// parse the request and according to the Request variable, redirect to
// the appropriate function
ParseRequest (XMLNode.text);
CASE Request OF
  'GetItem':
    BizLayer.GetItem (Parameters[1], XMLDom);
  ELSE
END;

// open the response queue and create a new message
MQBus.OpenWriteQueue('.\fromNavision',0,0);
OutMsg := CC2.CreateoutMessage('Message queue://.\fromNavision');
XMLDom.save (OutMsg.GetStream());

// fill the message and send it
OutMsg.Send(0);

Note   The authors are using public queues. The main difference when it gets to the code is that one has to add private$\ before the queue name if one would rather use private queues.

Receiving and validating the response and returning the dataset

The request has been sent to Navision, Navision understood it, handled it, responded to it and now a dataset is on a queue waiting to be received, validated and then returned to the web method caller.

Let us add the code that will do exactly what is missing in this scenario:

[Web Method]
public NavDS GetItem(string No)
{
   string request = "GetItem(" + No + ")";
   mqToNavision.Send (request, "Navision MSMQ-BA");
   mqFromNavision.Formatter = new
      System.Messaging.XmlMessageFormatter (new Type[] {typeof (NavDS)});
   System.Messaging.Message msg = 
      mqFromNavision.Receive (new System.TimeSpan (0,0,0,30));
   NavDS nds = new NavDS ();
   nds.ReadXml (msg.BodyStream, System.Data.XmlReadMode.Auto);
   return nds;
 }
Note   Even though the above code may work in simple testing scenarios, some sort of correlation mechanism must be accommodated in real scenarios so as to guarantee that the correct response message is being read from the queue. One such possibility consists of using the msg.Id and msg.CorrelationId properties.

What is this NavDS we are seeing in the code? We are creating an object of this type and using its ReadXML () method to read the contents of the message. Well, it is a dataset whose schema has already been depicted in figure 2 above. Here goes its formal XML representation:

<?xml version="1.0" ?>
<xs:schema id="NavDS" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
 attributeFormDefault="unqualified" elementFormDefault="unqualified">
 <xs:element name="NavDS" msdata:IsDataSet="true" msdata:EnforceConstraints="True">
  <xs:complexType>
   <xs:choice maxOccurs="unbounded">
    <xs:element name="NavItem" form="unqualified">
     <xs:complexType>
      <xs:sequence>
       <xs:element name="NavItemLedgerEntry" form="unqualified" minOccurs="0" maxOccurs="unbounded">
        <xs:complexType>
         <xs:attribute name="EntryNo" form="unqualified" type="xs:int" />
         <xs:attribute name="EntryType" form="unqualified" type="xs:string" />
         <xs:attribute name="PostingDate" form="unqualified" type="xs:string" />
         <xs:attribute name="DocumentNo" form="unqualified" type="xs:string" />
         <xs:attribute name="Description" form="unqualified" type="xs:string" />
         <xs:attribute name="Quantity" form="unqualified" type="xs:int" />
         <xs:attribute name="InvoicedQty" form="unqualified" type="xs:int" />
         <xs:attribute name="RemQty" form="unqualified" type="xs:int" />
         <xs:attribute name="LocationCode" form="unqualified" type="xs:string" />
        </xs:complexType>
       </xs:element>
      </xs:sequence>
      <xs:attribute name="No" form="unqualified" type="xs:string" />
      <xs:attribute name="Description" form="unqualified" type="xs:string" />
      <xs:attribute name="UnitPrice" form="unqualified" type="xs:double" />
      <xs:attribute name="UnitCost" form="unqualified" type="xs:double" />
      <xs:attribute name="Inventory" type="xs:int" />
      <xs:attribute name="ProfitPercent" type="xs:double" />
      <xs:attribute name="ProductPostGr" type="xs:string" />
      <xs:attribute name="InvtPostGr" type="xs:string" />
      <xs:attribute name="BaseUoM" type="xs:string" />
     </xs:complexType>
    </xs:element>
    <xs:element name="NavLocation" form="unqualified">
     <xs:complexType>
      <xs:attribute name="Code" form="unqualified" type="xs:string" />
      <xs:attribute name="Name" form="unqualified" type="xs:string" />
     </xs:complexType>
    </xs:element>
    <xs:element name="NavProductPGroup">
     <xs:complexType>
      <xs:sequence />
      <xs:attribute name="Code" type="xs:string" />
      <xs:attribute name="Description" type="xs:string" />
     </xs:complexType>
    </xs:element>
    <xs:element name="NavInventoryPG">
     <xs:complexType>
      <xs:sequence />
      <xs:attribute name="Code" type="xs:string" />
      <xs:attribute name="Description" type="xs:string" />
     </xs:complexType>
    </xs:element>
    <xs:element name="NavUnitOfMeasure">
     <xs:complexType>
      <xs:sequence />
      <xs:attribute name="Code" type="xs:string" />
      <xs:attribute name="Description" type="xs:string" />
     </xs:complexType>
    </xs:element>
   </xs:choice>
  </xs:complexType>
  <xs:key name="NavUoMPK">
   <xs:selector xpath=".//NavUnitOfMeasure" />
   <xs:field xpath="@Code" />
  </xs:key>
  <xs:key name="NavPPGRPK">
   <xs:selector xpath=".//NavProductPGroup" />
   <xs:field xpath="@Code" />
  </xs:key>
  <xs:key name="NavIPGRPK">
   <xs:selector xpath=".//NavInventoryPG" />
   <xs:field xpath="@Code" />
  </xs:key>
  <xs:key name="NavLocationPK">
   <xs:selector xpath=".//NavLocation" />
   <xs:field xpath="@Code" />
  </xs:key>
  <xs:key name="NavItemPK">
   <xs:selector xpath=".//NavItem" />
   <xs:field xpath="@No" />
  </xs:key>
  <xs:key name="NavLedgerEntryPK">
   <xs:selector xpath=".//NavItemLedgerEntry" />
   <xs:field xpath="@EntryNo" />
  </xs:key>
  <xs:keyref name="NavUnitOfMeasureNavItem" refer="NavUoMPK">
   <xs:selector xpath=".//NavItem" />
   <xs:field xpath="@BaseUoM" />
  </xs:keyref>
  <xs:keyref name="NavProductPGroupNavItem" refer="NavPPGRPK">
   <xs:selector xpath=".//NavItem" />
   <xs:field xpath="@ProductPostGr" />
  </xs:keyref>
  <xs:keyref name="NavInventoryPGNavItem" refer="NavIPGRPK">
   <xs:selector xpath=".//NavItem" />
   <xs:field xpath="@InvtPostGr" />
  </xs:keyref>
  <xs:keyref name="NavLocationNavItemLedgerEntry" refer="NavLocationPK">
   <xs:selector xpath=".//NavItemLedgerEntry" />
   <xs:field xpath="@LocationCode" />
  </xs:keyref>
 </xs:element>
</xs:schema>

Interacting with Navision data: InsertItem

A Web Service that is capable of reading data from Navision does not fully prove that interaction with the ERP data is possible. That is why we wanted to add a web method which could add an item, thus establishing a level of interaction with Navision data.

Let us check out what changes to apply on both ends of our system.

Here is the complete web method:

[Web Method]
public NavDS InsertItem
  (string No, string Description, float UnitPrice, float UnitCost,
   string ProductPG, string InventoryPG, string UoM)
{
   string request = 
      "InsertItem(" + No + "," + Description + "," + UnitPrice + "," + 
       UnitCost + "," + ProductPG + "," + InventoryPG + "," + UoM +")";
   mqFromNavision.Formatter = new
      System.Messaging.XmlMessageFormatter (new Type[] {typeof (NavDS)});
   mqToNavision.Send (request, "Navision MSMQ-BA");
   System.Messaging.Message msg = 
      mqFromNavision.Receive (new System.TimeSpan (0,0,0,30));
   NavDS nds = ((NavDS)msg.Body);
   return nds;
}

Note   For robustness reasons, it would be a good idea to call the GetItem () method and check whether the item already existed prior to sending the request to Navision. Also, it would be a good idea to check if those fields depending on other data tables were valid as well.

Notice that we are packing 7 arguments along with the method name. That means we should be able to successfully parse and retrieve 7 arguments from the request string within Navision. Let us check out the changes to operate there.

On the Web Service Handler codeunit, the message received trigger must be changed so that it may recognize the new request type:

CC2::MessageReceived(VAR InMessage : Automation "''.IDISPATCH")

// load the message into an XML document and find the string node
InMsg := InMessage;
InS := InMsg.GetStream();
XMLDom.load (InS);
XMLNode := XMLDom.selectSingleNode ('string');

// parse the request and according to the Request variable, redirect to
// the appropriate function
ParseRequest (XMLNode.text);
CASE Request OF
  'GetItem':
    BizLayer.GetItem (Parameters[1], XMLDom);
  'InsertItem':
    BizLayer.InsertItem
      (Parameters[1], Parameters[2], Parameters[3], Parameters[4],
       Parameters[5], Parameters[6], Parameters[7], XMLDom);
  ELSE
END;

// open the response queue and create a new message
MQBus.OpenWriteQueue('.\fromNavision',0,0);
OutMsg := CC2.CreateoutMessage('Message queue://.\fromNavision');
XMLDom.save (OutMsg.GetStream());

// fill the message and send it
OutMsg.Send(0);

Notice that we are using the 7 arguments identified above to call a function we added to codeunit 90001 (BizLayer Entry Point). Its code looks like this:

InsertItem
  (No : Code[20];Description : Text[50];UnitPrice : Text[30];
   UnitCost : Text[30];ProductPG : Code[20];InventoryPG : Code[20];
   BaseUoM : Code[20];
   XMLDom : Automation "'Microsoft XML, v3.0'.DOMDocument")

// if the item does not yet exist
IF NOT Item.GET (No) THEN
  BEGIN

    // create a new record
    Item."No." := No;
    Item.INSERT;

    // creation of an Item Unit of Measure record
    UoM."Item No." := No;
    UoM.Code := BaseUoM;
    UoM."Qty. per Unit of Measure" := 1;
    UoM.INSERT;

    // fill the new item record fields and update the item
    Item.VALIDATE (Description, Description);
    Item.VALIDATE ("Gen. Prod. Posting Group", ProductPG);
    Item.VALIDATE ("Inventory Posting Group", InventoryPG);
    Item.VALIDATE ("Base Unit of Measure", BaseUoM);
    EVALUATE (uc, UnitCost);
    EVALUATE (up, UnitPrice);
    Item.VALIDATE ("Unit Cost", uc);
    Item.VALIDATE ("Unit Price", up);
    Item.MODIFY;
  END;
GetItem (No, XMLDom);

The following local variables have been used:

  • uc – Decimal
  • up – Decimal
  • UoM – Record (Unit of Measure)

This procedure fills an item instance from the arguments received and it also creates an item unit of measure record for this item. Finally, it calls the GetItem procedure which will fill a dataset for this newly created item.

Completing the Web Service: PostItemAdjustment

Now that our Web Service is capable of reading data from Navision and inserting simple data, having it interacting with the ERP business processes and functionality would be nice. In this section we will show you how we could post an item adjustment, a process that starts from the filling of a journal line which is transformed into Item Ledger Entries and Value Entries, among others.

Again, let us take a look at the web method first:

[Web Method]
public NavDS PostItemAdjustment
  (string ItemNo, string PostingDate, string LocationCode, int Quantity,
   string Description, float Amount, float UnitCost, string DocumentNo,
   int EntryType)
{
   string request = 
      "PostItemAdjustment(" + ItemNo + "," + PostingDate + "," + 
       LocationCode + "," + Quantity.ToString() + "," + Description + "," + 
       Amount.ToString () + "," + UnitCost.ToString () + "," + 
       DocumentNo + "," + EntryType.ToString() + ")";
   mqFromNavision.Formatter = new
      System.Messaging.XmlMessageFormatter (new Type[] {typeof (NavDS)});
   mqToNavision.Send (request, "Navision MSMQ-BA");
   System.Messaging.Message msg = 
      mqFromNavision.Receive (new System.TimeSpan (0,0,0,30));
   NavDS nds = ((NavDS)msg.Body);
   return nds;
}

Similarly to the InsertItem method, there are changes to be performed on both codeunits, as follows:

CC2::MessageReceived(VAR InMessage : Automation "''.IDISPATCH")

// load the message into an XML document and find the string node
InMsg := InMessage;
InS := InMsg.GetStream();
XMLDom.load (InS);
XMLNode := XMLDom.selectSingleNode ('string');

// parse the request and according to the Request variable, redirect to
// the appropriate function
ParseRequest (XMLNode.text);
CASE Request OF
  'GetItem':
    BizLayer.GetItem (Parameters[1], XMLDom);
  'InsertItem':
    BizLayer.InsertItem
      (Parameters[1], Parameters[2], Parameters[3], Parameters[4],
       Parameters[5], Parameters[6], Parameters[7], XMLDom);
  'PostItemAdjustment':
    BizLayer.PostItemAdjustment
      (Parameters[1], Parameters[2], Parameters[3], Parameters[4], 
       Parameters[5], Parameters[6], Parameters[7], Parameters[8],
       Parameters[9], XMLDom);
  ELSE
END;

// open the response queue and create a new message
MQBus.OpenWriteQueue('.\fromNavision',0,0);
OutMsg := CC2.CreateoutMessage('Message queue://.\fromNavision');
XMLDom.save (OutMsg.GetStream());

// fill the message and send it
OutMsg.Send(0);

And finally, the new procedure PostItemAdjustment on codeunit BizLayer Entry Point:

PostItemAdjustment
  (ItemNo : Code[20];PostingDate : Text[30];
   LocationCode : Code[20];Quantity : Text[30];
   Description : Text[50];Amount : Text[30];
   UnitCost : Text [30];DocumentNo : Text[30];EntryType : Text[30];
   XMLDom : Automation "'Microsoft XML, v3.0'.DOMDocument")

// journal template selection
ItemJournalTemplate.SETRANGE (Recurring, FALSE);
ItemJournalTemplate.SETRANGE (Type, ItemJournalTemplate.Type::Item);
ItemJournalTemplate.FIND ('-');

// journal batch selection
ItemJournalBatch.SETRANGE ("Journal Template Name", ItemJournalTemplate.Name);
ItemJournalBatch.FIND ('-');

// new journal line
ItemJournalLine.DELETEALL;
ItemJournalLine."Journal Template Name" := ItemJournalTemplate.Name;
ItemJournalLine."Journal Batch Name" := ItemJournalBatch.Name;
ItemJournalLine."Line No." := 10000;
ItemJournalLine.VALIDATE ("Item No.", ItemNo);
ItemJournalLine.VALIDATE ("Location Code", LocationCode);
ItemJournalLine.VALIDATE (Description, Description);
ItemJournalLine.VALIDATE ("Document No.", DocumentNo);
EVALUATE (pdate, PostingDate);
EVALUATE (amt, Amount);
EVALUATE (qty, Quantity);
EVALUATE (uc, UnitCost);
EVALUATE (type, EntryType);
ItemJournalLine.VALIDATE ("Posting Date", pdate);
ItemJournalLine.VALIDATE (Quantity, qty);
ItemJournalLine.VALIDATE (Amount, amt);
ItemJournalLine.VALIDATE ("Unit Cost", uc);
ItemJournalLine.VALIDATE ("Entry Type", type);

// post the newly created journal line
ItemJnlLinePost.RUN (ItemJournalLine);

GetItem (ItemNo, XMLDom);

These were the local variables we declared:

  • ItemJnlLinePost – Codeunit (Item Jnl.-Post Line)
  • ItemJournalLine – Record (Item Journal Line)
  • ItemJournalBatch – Record (Item Journal Batch)
  • ItemJournalTemplate – Record (Item Journal Template)
  • pdate – Date
  • qty – Decimal
  • uc – Decimal
  • amt – Decimal
  • type – Integer.

As with the InsertItem procedure, we are calling GetItem at the end of this one, so that the caller system may immediately perceive the changes in the data after posting the adjustment.

After-thoughts and conclusions

First of all, this is definitely not a complete example. As stated above, some extra checking could be performed on both ends of the system for each request, so that it could be less error-prone. In this article, however and for simplicity reasons, we are assuming that the arguments being given to the web methods have been pre-checked.

As you could see, the magic of communication has already been presented. This article simply adds on top of that and allows you - as a software architect and/or developer – to conceal the specifics of such communication, thus providing those responsible for the upper layers of your applications with a simple interface with the ERP system.

Take a moment to recognize the kind of environments in which this solution may participate:

  • a complex system composed of multiple applications aimed at distinct goals, in which there is a central entity responsible for the overall synchronization. This entity would probably need to be able to recurrently read data from the ERP system and to post changed or new data onto it as well. By being able to call a set of web methods, this task would be much easier than otherwise having to go all the way to and from Navision;
  • a business-specific application whose processes merely intersect the scope of Navision, specially in its later stages when dealing with the company resources, such as customers, general ledger accounts or warehouse picks. When developing these connections, having a Web Service which takes care of these details seems a good resource;
  • an internal web portal for warehouse employees in which they can check the inventory and perform item adjustments, for instance.

Finally, this particular example has been developed on Navision 3.70. It should work with version 3.60 and above. However, if yours is to develop a scenario on Navision 4.0 using a similar approach; consider using XMLPorts instead of explicitly building an XML document from the data.

António Nobre is the General Manager of Gerimática, Informática Lda. He took his award-winning POS and created two versions in which the original business layer is replaced by Navision and by another local ERP system. For this, he developed a Web Service which both reads and posts data onto both systems accordingly.

Manuel Oliveira is a Technical Account Support Engineer working in Microsoft Portugal. He is a former Microsoft Business Solutions Product Manager and he participated in projects involving a multitude of systems whose interfaces were guaranteed by Web Services.

Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.