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.
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.
- Existence of a SeurConf record.
- Existence of a Seur Product in the Pack's Freight.
- Existence of a Seur Service in the Pack's Freight.
- Existence of a Document Number in the Pack. The Seur platform requires a unique reference number for each pack delivered. The Document Number is used on this purpose.
@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);
- A new SOAPElement is added to the body with the service that is desired to use.
- The User Name is retrieved from global seurConf object.
- The password is sent decrypted. The FormatUtilities class is used to decrypt it.
- The 5th parameter is the boxes definition that it is described below. In this case the content is an xml file that is built in the getBoxesXML() method.
- The 7th parameter is the NIF or the Entity's Tax ID. It is used the value defined in the Legal Entity Organization of the Pack.
- The 10th parameter is the Invoker. If it is empty Openbravo is sent.
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; }