View source | Discuss this page | Page history | Printable version   

Modules:How to create a Carrier Integration Module

Contents

Objective

The objective of this How to article is to describe how to develop a new module Integrating a Carrier platform with Openbravo using the Freight Management.

The development of the integration might differ between carriers. They might need different parameters in different formats. And they can use different protocols to connect to their platforms. In this how to document is used as example the Seur Spain Integration module. This carrier has a web service platform using the SOAP protocol. To connect to that web services and properly integrate the deliveries some parameters that are not included in Openbravo are needed, so a new table and window is needed to store them.

Before starting to develop your own Carrier Integration Module is strongly recommended to be in contact with the Carrier. You need to know how to connect with their platform. In case web services are used how to format the XML or the JSON that has to be sent and the information that needs to be included. Having this information it has to be decided if a parametrization table/window is needed or existing entities like Pack or Box needs to be extended.

Module Definition

The module is defined as a regular extension module. The dependency must be set on Carrier Base Integration module. Additional dependencies might be added when needed.

Application Dictionary

The module needs to include a record in the Carrier Integration window of System Administrator. In this window is defined the Java Class that includes the integration process. This record is exported as sourcedata so it is not needed to create any Dataset. This is needed as later when each Carrier is configured it will be needed to select the Carrier Integration.

Parametrization window

The Seur platform needs some Entity specific parameters that are not in Openbravo by default. For example a User and password, a company identifier,.... To store them a Configuration table OBEUSES_Seur_Conf is created. As the parameters are Entity specific the table is created with System/Client Data Access Level. This allows to have a different parametrization on each Entity in the Openbravo's instance.

OBEUSES_Seur_Conf table's columns
Name Columnname Type Default Value
Obeuses_Seur_Conf_ID Obeuses_Seur_Conf_ID ID
Client AD_Client_ID TableDir @AD_CLIENT_ID@
Organization AD_Org_ID TableDir @AD_ORG_ID@
Active Isactive YesNo Y
Creation Date Created DateTime SYSDATE
Created By Createdby Search
Updated Updated DateTime SYSDATE
Updated By Updatedby Search
Name Name String
URL Url String
Carrier Conf Obeuci_Carrier_ID TableDir 65E310719961414F8CF63F685B24A996
Username Username String
Password Password Password (decryptable)
Printer_Brand Printer_Brand String
Printer_Model Printer_Model String
Print_Format Print_Format List
File_Name File_Name String
Franchise_Code Franchise_Code String
CC_Code CC_Code String
Invoker Invoker String
CCC_Code CCC_Code String
Code_CI Code_CI String

The table has a foreign key to the Carrier Integration window. The column has as default value UUID of the record added by the module. As that information is stored as sourcedata this ID won't change on different instances of Openbravo. This makes easier to retrieve the configuration parameters having from the Carrier Integration record.

The rest of the Columns includes values that need to be included in the XML sent to Seur platform and are not available in the System.

Other extended entities

The Seur platform also includes some other parameters that can be different on each delivery. Instead of defining them on the Parametrization table these values are included extending existing entities.

When a delivery is registered in the platform it is needed to choose a Service (24 hours, same day,48 hours,...) and a Seur Product (Standard, Multipack, Cold). These values are configured on each Freight defined on the Carrier. New fields and columns are added in the Freight tab of the Carrier window (OBMFM_Freight table). These fields uses new Lists references with all available values provided by Seur. When the SOAP Message is being built the Service and Seur Product are retrived from the Freight assigned to the delivered Pack.

The integration process returns a Print Trace for each delivered Pack with box's labels. This is stored as a hidden blob column in the OBWPACK_PackingH_Id table. So it can be used later by additional custom processes.

CarrierIntegration Java Class implementation

Once the Application Dictionary data is created it is time to create the Java Class extending CarrierIntegration where the integration process is developed. The name of this class has to match the Java Class defined in the Carrier Integration window.

public class SeurIntegration extends CarrierIntegration 

Overview

The CarrierIntegration class managed the integration process. This class is initialized and executed by any process in the ERP that launches an integration with a Carrier platform.

Each Pack is integrated separately in different integrations. This means that each Pack uses its own instance of the CarrierIntegration class. If more than one pack is going to be delivered they are sent in different instances.

A delivery of a Pack is done in 3 steps. The first one is to initialize the CarrierInstance. This steps initializes some global variables. The Pack and its Boxes ordered by the Box Number. And the result to unfinish and sets the Error Msg to empty String.

The second step are the validations. There are two validations common to all Carriers that are always checked, the existence of a Business Partner and a Partner Address in the Pack. If they are empty an exception is thrown. It also checks the integration status of the Pack to avoid sending again to the platform the same pack. Each carrier integration might add new validations implementing the doValidate() abstract method. Any failure in a validation should throw an exception.

The last step is the process. The process must be implemented by the Integration Modules extending the doProcess() abstract method. This method implements the needed actions to connect to the Carrier Platform and integrate the delivery of the Pack to the Business Partner in the pack's Partner Address. This process must update the Tracking Numbers of each box with the result of the integration. The process can set any error message or change the result using the setResult() and setErrorMsg(). This values are read when the process is finished to build the message that it is shown to the user when the process is finished. Any exception is caught by the CarrierIntegration process, in this case a rollback is performed and the result is set to Error also the exception message is parsed and set in the Error Msg.

Initialization

The class includes an additional class private object with the SeurConf record to be used. The init() method is overridden to initialize it.

  private SeurConf seurConf = null;

  @Override
  public void init(Packing _pack) {
    super.init(_pack);
    List<SeurConf> seurConfs = pack.getOBEUCIFreight().getCarrier().getOBEUCICarrierIntegration()
        .getOBEUSESSeurConfList();
    if (seurConfs.size() > 0) {
      seurConf = seurConfs.get(0);
    }
  }

Note that getOBEUSESSeurConfList() automatically filters the list by the Client. So only the records of the current client will be retrieved. As it should only be one the first record of the List is used.

Validation

The next method that needs to be overridden is the doValidate. In this case 4 additional validations are done.

  @Override
  public void doValidate() {
    if (seurConf == null) {
      throw new OBException("@OBEUSES_ConfNotFound@");
    }
    if (pack.getOBEUCIFreight().getOBEUSESProduct() == null) {
      throw new OBException("@OBEUSES_SeurProductNotFound@");
    }
    if (pack.getOBEUCIFreight().getOBEUSESService() == null) {
      throw new OBException("@OBEUSES_SeurServiceNotFound@");
    }
    if (StringUtils.isEmpty(pack.getDocumentNo())) {
      throw new OBException("@OBEUSES_PackNoDocumentNumber@");
    }

  }

Process

Finally the doProcess() method is implemented. In the case of the Seur platform the integration is done with web services using the SOAP protocol. To create and manage SOAP protocol the classes in javax.xml.soap package are used.

The process is structure on 3 steps. Create SOAP message, Send it and Read the response.

  @Override
  public void doProcess() {
    // Create SOAP Envelop
    SOAPMessage msg = createSOAPMessage();

    SOAPMessage response = sendSOAPMsg(msg);

    readResponse(response);

  }


Create SOAP Message

The message is created using standard methods adding a custom Namespace to the Envelope.

     MessageFactory msgFactory = MessageFactory.newInstance();
     SOAPMessage msg = msgFactory.createMessage();
     SOAPPart part = msg.getSOAPPart();
     SOAPEnvelope envelope = part.getEnvelope();
     envelope.addNamespaceDeclaration("imp", "http://{service url}");
     SOAPBody body = envelope.getBody();

Once we have the SOAPBody it is time to add the parameters needed for the Service that we are going to use. In this case we are using a service that integrates the Pack that it is being delivered and returns a Tracking Number for each Box of the Pack and a Print Trace for the labels. This service has 10 input parameters. In the below code snippet is shown how some of them are added to the body;

     SOAPElement printElement = body.addChildElement("{service name}", "imp");
     printElement.addChildElement("in0", "imp").addTextNode(seurConf.getUsername());
     final String strPassword = FormatUtilities.encryptDecrypt(seurConf.getPassword(), false);
     printElement.addChildElement("in1", "imp").addTextNode(strPassword);
...
     printElement.addChildElement("in5", "imp").addTextNode(getBoxesXML());
...
     printElement.addChildElement("in7", "imp").addTextNode(getNIF());
...
     String strInvoker = seurConf.getInvoker();
     if (StringUtils.isEmpty(strInvoker)) {
       strInvoker = "Openbravo";
     }
     printElement.addChildElement("in10", "imp").addTextNode(strInvoker);

getBoxesXML method

The getBoxesXML method creates the XML with the information of the Boxes and the Delivery contact and address. To create the XML the org.dom4j package is used.

   final Document boxesXML = DocumentHelper.createDocument();
   final Element root = boxesXML.addElement("root");
   final Element exp = root.addElement("exp");

Inside the exp element is added a bulto element for each delivered box. Each box contains all the information needed for the delivery. All the elements of the box are stored in a Map<String, Object> later this Map is converted to elements inside the bulto element.

   for (PackingBox box : boxes) {
     final Element bulto = exp.addElement("bulto");
     Map<String, Object> boxDef = new HashMap<String, Object>();
     ...
     boxDef.put("servicio", pack.getOBEUCIFreight().getOBEUSESService());
     boxDef.put("producto", pack.getOBEUCIFreight().getOBEUSESProduct());
     boxDef.put("total_bultos", Integer.toString(pack.getOBWPACKBoxList().size()));
     boxDef.put("total_kilos", pack.getTotalweight());
     boxDef.put("pesoBulto", box.getWeight());
     ...
     // Customer contact
     if (!OBDao.getActiveOBObjectList(customer, BusinessPartner.PROPERTY_ADUSERLIST).isEmpty()) {
       User contact = (User) OBDao.getActiveOBObjectList(customer,
           BusinessPartner.PROPERTY_ADUSERLIST).get(0);
       boxDef.put("email_consignatario", contact.getEmail());
       boxDef.put("sms_consignatario", contact.getPhone());
       ...
     }
     for (String key : boxDef.keySet()) {
       Object value = boxDef.get(key);
       if (value == null) {
         value = "";
       }
       if (value instanceof Long) {
         value = Long.toString((Long) value);
       }
       if (value instanceof BigDecimal) {
         value = ((BigDecimal) value).toPlainString();
       }
       bulto.addElement(key).addText((String) value);
     }

Finally the encoding required by Seur is set and the XML file is generated. The xml needs to be included in CDATA tags, they are also included.

   try {
     final OutputFormat format = OutputFormat.createPrettyPrint();
     format.setEncoding("ISO-8859-1");
     format.setTrimText(false);
     final StringWriter out = new StringWriter();
     final XMLWriter writer = new XMLWriter(out, format);
     writer.startCDATA();
     writer.write(boxesXML);
     writer.endCDATA();
     writer.close();
     return out.toString();
   } catch (final Exception e) {
     throw new OBException(e);
   }

The result is a XML with the following structure:

<?xml version="1.0" encoding="ISO-8859-1"?>
<root><exp>
  <bulto>
    <nif>BXXXXXXXX</nif>
    <servicio>31</servicio>
    <producto>2</producto>
    <total_bultos>2</total_bultos>
    <total_kilos>14</total_kilos>
    <pesoBulto>10.0000</pesoBulto>
    <referencia_expedicion>DEMO-01</referencia_expedicion>
    <ref_bulto>DEMO-01-1</ref_bulto>
    ...
  </bulto>
  <bulto>
    <nif>BXXXXXXXX</nif>
    <servicio>31</servicio>
    <producto>2</producto>
    <total_bultos>2</total_bultos>
    <total_kilos>14</total_kilos>
    <pesoBulto>4.0000</pesoBulto>
    <referencia_expedicion>DEMO-01</referencia_expedicion>
    <ref_bulto>DEMO-01-2</ref_bulto>
    ...
  </bulto>
</exp></root>

The resulting SOAP message has the following structure:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:imp="http://{webservice url}">
  <soapenv:Header/>
  <soapenv:Body>
    <imp:{service name}>
      <imp:in0>{username}</imp:in0>
      <imp:in1>{password}</imp:in1>
      <imp:in2>{printer brand}</imp:in2>
      <imp:in3>{printer model}</imp:in3>
      <imp:in4>{print format}</imp:in4>
      <imp:in5><![CDATA[<?xml version="1.0" encoding="ISO-8859-1"?>
      </imp:in5>
      ...
      <imp:in10>{invoker}</imp:in10>
    </imp:{service name}>
  </soapenv:Body>
</soapenv:Envelope>

Send SOAP Message

The SOAP Message is sent using the SOAPConnection class. The call method sends the message to the specified URL and returns the response SOAP MEssage.

   try {
     SOAPConnectionFactory connectionFactory = SOAPConnectionFactory.newInstance();
     SOAPConnection connection = connectionFactory.createConnection();
     URL url = new URL(seurConf.getURL());
     SOAPMessage response = connection.call(msg, url);
     return response;
   } catch (SOAPException e) {
     throw new OBException("Error conecting to web service: " + e.getMessage(), e);
   } catch (MalformedURLException e) {
     throw new OBException("@OBEUSES_WrongURL@" + e.getMessage(), e);
   }

Read response

The final step is to read the response to retrieve the tracking numbers and the print trace. Or getting the error message in case there has been any issue in the process.

When the integration has been successful the SOAP Message contains an element ECB which contains as many parameters as boxes with the Tracking Numbers. And an element traza with the print trace.

<ns1:{service name} xmlns:ns1="{webservice url}">
  <ns1:out>
    <ECB xsi:nil="true" xmlns="{}">
      <imp:out0>{tracking number 0}</imp:out0>
      ...
      <imp:outN>{tracking number N}</imp:outN>
    </ECB>
    <mensaje xmlns="{}">
    <traza xsi:nil="true" xmlns="{}">{print trace}</traza>
  </ns1:out>
</ns1:{servicename}>

When the integration return an error the SOAP Message has the error message in an element called mensaje and no tracking number or print trace is included.

<ns1:{service name} xmlns:ns1="{webservice url}">
  <ns1:out>
    <ECB xsi:nil="true" xmlns="{}"/>
    <mensaje xmlns="{}">{Error message}</mensaje>
    <traza xsi:nil="true" xmlns="{}"/>
  </ns1:out>
</ns1:{service name}>


These messages are parsed with the following code. First the message is parsed to get the out element. That contains the relevant elements with the desired information.

SOAPBody body = response.getSOAPBody();
SOAPElement base = (SOAPElement) body.getChildNodes().item(0);
SOAPElement out = (SOAPElement) base.getChildNodes().item(0);

Then is checked the mensaje element to retrieve any error message. In case there is any message it is parsed and an exception is thrown. The message can be an XML so it is loaded to print it.

NodeList errorMsgList = out.getElementsByTagName("mensaje");
String strErrorMsg = errorMsgList.item(0).getTextContent();
if (StringUtils.isNotEmpty(strErrorMsg) && !"OK".equals(strErrorMsg)) {
  OBDal.getInstance().rollbackAndClose();
  try {
    final Document errorMsgXml = DocumentHelper.parseText(strErrorMsg);
    throw new OBException(errorMsgXml.toString());
  } catch (DocumentException e) {
    throw new OBException(strErrorMsg);
  }
}

If there isn't any message the ECBs and the traza is read to store them in the corresponding fields of the Boxes and the Pack. Note that the List of boxes has been initializated ordered by the box number so they are always iterated in the same order.

SOAPElement ecbElement = (SOAPElement) out.getElementsByTagName("ECB").item(0);
Iterator<?> ecbs = ecbElement.getChildElements();
int boxNo = 0;
while (ecbs.hasNext()) {
  SOAPElement ecb = (SOAPElement) ecbs.next();
  String strTrackingNo = ecb.getTextContent();
  PackingBox box = boxes.get(boxNo);
  box.setTrackingNo(strTrackingNo);
  OBDal.getInstance().save(box);
  boxNo++;
}

SOAPElement printTraceElement = (SOAPElement) out.getElementsByTagName("traza").item(0);
String strPrintTrace = printTraceElement.getTextContent();
pack.setOBEUSESPrintTrace(strPrintTrace.getBytes());
OBDal.getInstance().save(pack);

In order to be able to debug the SOAP Messages an private method is created to convert them to String.

 private String getMsgAsString(SOAPMessage message) {
   String msg = null;
   try {
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     message.writeTo(baos);
     msg = baos.toString();
   } catch (Exception e) {
     e.printStackTrace();
   }
   return msg;
 }

Retrieved from "http://wiki.openbravo.com/wiki/Modules:How_to_create_a_Carrier_Integration_Module"

This page has been accessed 7,425 times. This page was last modified on 14 March 2014, at 09:25. Content is available under Creative Commons Attribution-ShareAlike 2.5 Spain License.