API write example out of Pentaho Spoon

This is an example how content can be send to M3 via Pentaho Spoon. I am using a “User defined java class” to perform the API calls.

Here is my transformation:


Basically we have 3 parts in this transformation:


In this first part I am reading informations from a csv file and prepare all stuff that the input is ready for upload via API



In this central part the API is called to write the informations to M3. All stuff is done via Java in a “user defined java class” step



In the 3. part basically I am looking for errors and write a success and error log. finally an email is send to an elaborator with the faulty records.


Here is the content of my “user defined java class” step. In it I call 2 transaction from the PMS070MI. First the GetOperation to retrieve missing informations and then the RptOperation to reports hours on operations of production orders.

import java.util.regex.Pattern;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import java.text.*;
import MvxAPI.*;

private StringBuffer transStr;
private StringBuffer transStr3;
private StringBuffer sErrLine;
private StringBuffer sSucLine;
private StringBuffer sHeader;

private boolean firstErrLogLine;
private boolean firstSucLogLine;

private String mvxCONO; // Company
private String mvxFACI; // Facility
private String mvxPRNO; // Product number
private String mvxMFNO; // Manufacturing order number
private String mvxOPNO; // Operation number
private String mvxWOSQ; // Reporting number
private String mvxPLNO; // Production lot number
private String mvxRPDT; // Reporting date
private String mvxRPTM; // Reporting time
private String mvxPEWA; // Payroll period
private String mvxUMAT; // Used labor run time
private String mvxUMAS; // Used labor setup time
private String mvxUPIT; // Used machine run time
private String mvxUSET; // Used machine setup time
private String mvxRUDI; // Run disturbance
private String mvxSEDI; // Setup disturbance
private String mvxMAQA; // Manufactured quantity
private String mvxMAUN; // Manufacturing U/M
private String mvxSCQA; // Scrap quantity alternative unit
private String mvxSCRE; // Rejection reason
private String mvxSCWC; // Scrap source work center
private String mvxREND; // Manual completion flag
private String mvxEMNO; // Employee number
private String mvxPCTP; // Costing type
private String mvxSHFC; // Shift
private String mvxMSNO; // Resource
private String mvxTRDT; // Transaction date(not used)
private String mvxTRTM; // Transaction time(not used)
private String mvxPRNP; // Planned number of workers – run time
private String mvxSENP; // Planned number of workers – setup
private String mvxREWK; // Rework
private String mvxDPLG; // Deviating work center
private String mvxSEPR; // Setup price
private String mvxSUNO; // Supplier number
private String mvxPIPR; // Unit price
private String mvxTODL; // Total amount direct labor
private String mvxREMK; // Remark
private String mvxWAFA; // Time rate
private String mvxKIWG; // Pay element
private String mvxCHID; // Changed by
private String mvxDSP1; // Warning indicator 1
private String mvxDSP2; // Warning indicator 2
private String mvxDSP3; // Warning indicator 3
private String mvxDSP4; // Warning indicator 4
private String mvxCAMU; // Container
private String mvxBANO; // Lot number
private String mvxRMAQ; // Remaining quantity to manufacture

private String Status;

MvxSockJ obj2, obj3;
int i, j;
String str,str2, str3;
String sendstr;
double ver;
String test, MvxERR;

public boolean processRow(StepMetaInterface smi, StepDataInterface sdi) throws KettleException

Object[] r = getRow();

if (r == null) {
return false;

String M3_System = get(Fields.In, “M3_System”).getString(r);
int M3_Port = get(Fields.In, “M3_Port”).getInteger(r).intValue();
String M3_User = get(Fields.In, “M3_User”).getString(r);
String M3_Pass = get(Fields.In, “M3_Pass”).getString(r);
String OutPath = get(Fields.In, “OutPath”).getString(r);
Date dNow = new Date( );
//SimpleDateFormat ft = new SimpleDateFormat (“E yyyy.MM.dd ‘at’ hh:mm:ss a zzz”);
SimpleDateFormat ft = new SimpleDateFormat (“yyyyMMdd_HHmmss”);

Object[] outputRow = RowDataUtil.createResizedCopy(r, data.outputRowMeta.size());

//Initialize Log file first line check
firstErrLogLine = true;
firstSucLogLine = true;

// prepare regex and field helpers
if (first){
first = false;
// get the input and output fields
//assign system variables

// get all rows from the info stream and compile the regex field to patterns
//FieldHelper regexField = get(Fields.Info, “WHLO”);
RowSet infoStream = findInfoRowSet(“api_transactions”);
Object[] infoRow = null;
//Open M3 connection
obj2=new MvxSockJ(M3_System, M3_Port, “”, 0, “”);
obj2.DEBUG = true; // Have MvxSockJ print out debug info

* For old FPW we give LOCALA as the system name, new FPW and NextGen does not care.
* We use the AS400 account USER with password PASSWORD
* We want to connect to program CRS610MI in library MVXBOBJ.

i = obj2.mvxInit(“”, M3_User, M3_Pass, “PMS070MI”);
if(i>0) {
//System.out.println(“mvxInit() returned “+i+” “+obj2.mvxGetLastError());
get(Fields.Out, “Status”).setValue(outputRow, “Error: ” +obj2.mvxGetLastError());
putRow(data.outputRowMeta, outputRow);
return true;

//System.out.println(“The current version of MvxSockJ is: ” +ver);
get(Fields.Out, “version”).setValue(outputRow, ver);

//Prepare filenames for logging
String ErrorLogPatch = OutPath + “Log/ErrorLog_B4E2M3_” + new SimpleDateFormat(“yyyyMMdd_HHmmss”).format(Calendar.getInstance().getTime()) + “.txt”;
String SuccessLogPatch = OutPath + “Log/SuccessLog_B4E2M3_” + new SimpleDateFormat(“yyyyMMdd_HHmmss”).format(Calendar.getInstance().getTime()) + “.txt”;

while((infoRow = getRowFrom(infoStream)) != null){

mvxCONO = “001”;
mvxFACI = get(Fields.Info, “FACI”).getString(infoRow);
mvxPRNO = get(Fields.Info, “PRNO”).getString(infoRow);
mvxMFNO = get(Fields.Info, “MFNO”).getString(infoRow);
mvxOPNO = get(Fields.Info, “OPNO”).getString(infoRow);
mvxWOSQ = get(Fields.Info, “WOSQ”).getString(infoRow);
mvxPLNO = null;
mvxRPDT = get(Fields.Info, “RPDT”).getString(infoRow);
mvxRPTM = “”;
mvxPEWA = “”;
mvxUMAT = get(Fields.Info, “UMAT”).getString(infoRow);
mvxUMAS = null;
mvxUPIT = get(Fields.Info, “UPIT”).getString(infoRow);
mvxUSET = “”;
mvxRUDI = “”;
mvxSEDI = “”;
mvxMAQA = get(Fields.Info, “MAQA”).getString(infoRow);
mvxMAUN = “”;
mvxSCQA = “”;
mvxSCRE = “”;
mvxSCWC = “”;
mvxREND = get(Fields.Info, “REND”).getString(infoRow);
if (mvxREND == null) { mvxREND = “0”; }
mvxEMNO = get(Fields.Info, “EMNO”).getString(infoRow);
mvxPCTP = “”;
mvxSHFC = “”;
mvxMSNO = “”;
mvxTRDT = “”;
mvxTRTM = “”;
mvxPRNP = “”;
mvxSENP = “”;
mvxREWK = “”;
mvxDPLG = get(Fields.Info, “DPLG”).getString(infoRow);
mvxSEPR = “”;
mvxSUNO = “”;
mvxPIPR = “”;
mvxTODL = “”;
mvxREMK = “”;
mvxWAFA = “”;
mvxKIWG = “”;
mvxCHID = “”;
mvxDSP1 = “”;
mvxDSP2 = “”;
mvxDSP3 = “”;
mvxDSP4 = “”;
mvxCAMU = “”;
mvxBANO = “”;
//When operation is marked as finished first
if (mvxREND.equals(“1”)) {
* Build the priliminary transaction to catch the remaining quantity, if the operation should be closed

transStr3 = new StringBuffer(1024);

//Build the transaction string
for(int x=0; x<1024; x++) {
transStr3.setCharAt(x,’ ‘);

transStr3.insert(0, “GetOperation”);
transStr3.insert(15, mvxCONO);
//transStr3.insert(15, “002”);
transStr3.insert(18, mvxFACI);
transStr3.insert(21, mvxPRNO);
transStr3.insert(36, mvxMFNO);
transStr3.insert(43, mvxOPNO);

transStr3.setLength(75); // Adjust the length

//Run the transaction


if(str3.startsWith(“NOK”)) {
Status = “Error in retrieving open quantity, check reported quantity of operation:” + str3;

} else {
//Extract needed fields for second transaction
mvxRMAQ = str3.substring(385, 402).trim();
mvxMAQA = mvxRMAQ; // Assign the retrieved value to Manufactured quantity
Status = “Operation completed, remaining quantity will be reported”;


} else {
Status = “Operation not completed, no quantity reporting!”;
mvxRMAQ = “0”;
mvxMAQA = mvxRMAQ; // Assign the retrieved value to Manufactured quantity


* Build the main transaction – reporting of operation

transStr = new StringBuffer(1024);

//Build the transaction string
for(int x=0; x<1024; x++) {
transStr.setCharAt(x,’ ‘);

transStr.insert(0, “RptOperation”);
//transStr.insert(15, “003”);
if (mvxCONO != null) { transStr.insert(15, mvxCONO);}
if (mvxFACI != null) { transStr.insert(18, mvxFACI);}
if (mvxPRNO != null) { transStr.insert(21, mvxPRNO);}
if (mvxMFNO != null) { transStr.insert(36, mvxMFNO);}
if (mvxOPNO != null) { transStr.insert(43, mvxOPNO);}
if (mvxWOSQ != null) { transStr.insert(47, mvxWOSQ);}
if (mvxPLNO != null) { transStr.insert(56, mvxPLNO);}
if (mvxRPDT != null) { transStr.insert(66, mvxRPDT);}
if (mvxRPTM != null) { transStr.insert(76, mvxRPTM);}
if (mvxPEWA != null) { transStr.insert(82, mvxPEWA);}
if (mvxUMAT != null) { transStr.insert(88, mvxUMAT);}
if (mvxUMAS != null) { transStr.insert(97, mvxUMAS);}
if (mvxUPIT != null) { transStr.insert(106, mvxUPIT);}
if (mvxUSET != null) { transStr.insert(115, mvxUSET);}
if (mvxRUDI != null) { transStr.insert(124, mvxRUDI);}
if (mvxSEDI != null) { transStr.insert(127, mvxSEDI);}
if (mvxMAQA != null) { transStr.insert(130, mvxMAQA);}
if (mvxMAUN != null) { transStr.insert(147, mvxMAUN);}
if (mvxSCQA != null) { transStr.insert(150, mvxSCQA);}
if (mvxSCRE != null) { transStr.insert(167, mvxSCRE);}
if (mvxSCWC != null) { transStr.insert(169, mvxSCWC);}
if (mvxREND != null) { transStr.insert(177, mvxREND);}
if (mvxEMNO != null) { transStr.insert(178, mvxEMNO);}
if (mvxPCTP != null) { transStr.insert(188, mvxPCTP);}
if (mvxSHFC != null) { transStr.insert(189, mvxSHFC);}
if (mvxMSNO != null) { transStr.insert(192, mvxMSNO);}
if (mvxTRDT != null) { transStr.insert(207, mvxTRDT);}
if (mvxTRTM != null) { transStr.insert(217, mvxTRTM);}
if (mvxPRNP != null) { transStr.insert(223, mvxPRNP);}
if (mvxSENP != null) { transStr.insert(229, mvxSENP);}
if (mvxREWK != null) { transStr.insert(235, mvxREWK);}
if (mvxDPLG != null) { transStr.insert(236, mvxDPLG);}
if (mvxSEPR != null) { transStr.insert(244, mvxSEPR);}
if (mvxSUNO != null) { transStr.insert(262, mvxSUNO);}
if (mvxPIPR != null) { transStr.insert(272, mvxPIPR);}
if (mvxTODL != null) { transStr.insert(290, mvxTODL);}
if (mvxREMK != null) { transStr.insert(308, mvxREMK);}
if (mvxWAFA != null) { transStr.insert(338, mvxWAFA);}
if (mvxKIWG != null) { transStr.insert(356, mvxKIWG);}
if (mvxCHID != null) { transStr.insert(360, mvxCHID);}
if (mvxDSP1 != null) { transStr.insert(370, mvxDSP1);}
if (mvxDSP2 != null) { transStr.insert(371, mvxDSP2);}
if (mvxDSP3 != null) { transStr.insert(372, mvxDSP3);}
if (mvxDSP4 != null) { transStr.insert(373, mvxDSP4);}
if (mvxCAMU != null) { transStr.insert(374, mvxCAMU);}
if (mvxBANO != null) { transStr.insert(386, mvxBANO);}

transStr.setLength(400); // Adjust the length

//Run the transaction

// Start Log file Handling, if NOK write Error Log, if OK write Success Log
if(str.startsWith(“NOK”)) {
Status = Status + “; ” + “Error in time reporting:” + str;

//write errors to Error Log File
//as first line write the header
if (firstErrLogLine){
firstErrLogLine = false;

sHeader = new StringBuffer(1024);

//Build the transaction string
for(int x=0; x<1024; x++) {
sHeader.setCharAt(x,’ ‘);

sHeader.insert(0, “Product”);
sHeader.insert(16, “MO No.”);
sHeader.insert(24, “OP No.”);
sHeader.insert(31, “Rep.No.”);
sHeader.insert(41, “Rep.Date”);
sHeader.insert(52, “Man.Qty”);
sHeader.insert(70, “Rep.Time”);
sHeader.insert(80, “Compl.”);
sHeader.insert(87, “Emp.No.”);
sHeader.insert(98, “Dev.WC”);
sHeader.insert(106, “Status”);
sHeader.insert(367,”API String”);

FileWriter writer = new FileWriter(ErrorLogPatch, true);
sHeader.delete(0, sHeader.length());

//Write the lines
sErrLine = new StringBuffer(1024);

//Build the transaction string
for(int x=0; x<1024; x++) {
sErrLine.setCharAt(x,’ ‘);

if (mvxPRNO != null) { sErrLine.insert(0, mvxPRNO);}
if (mvxMFNO != null) { sErrLine.insert(16, mvxMFNO);}
if (mvxOPNO != null) { sErrLine.insert(24, mvxOPNO);}
if (mvxWOSQ != null) { sErrLine.insert(31, mvxWOSQ);}
if (mvxRPDT != null) { sErrLine.insert(41, mvxRPDT);}
if (mvxMAQA != null) { sErrLine.insert(52, mvxMAQA);}
if (mvxUMAT != null) { sErrLine.insert(70, mvxUMAT);}
if (mvxREND != null) { sErrLine.insert(80, mvxREND);}
if (mvxEMNO != null) { sErrLine.insert(87, mvxEMNO);}
if (mvxDPLG != null) { sErrLine.insert(98, mvxDPLG);}
if (Status != null) { sErrLine.insert(106, Status);}
if (transStr != null) { sErrLine.insert(367, transStr);}
FileWriter writer = new FileWriter(ErrorLogPatch, true);
sErrLine.delete(0, sErrLine.length());
catch(IOException e)

} else {
Status = Status + “; ” + “Transaction OK”;

//Write successed transactions to Success Log File

//as first line write the header
if (firstSucLogLine){
firstSucLogLine = false;

sHeader = new StringBuffer(1024);

//Build the transaction string
for(int x=0; x<1024; x++) {
sHeader.setCharAt(x,’ ‘);

sHeader.insert(0, “Product”);
sHeader.insert(16, “MO No.”);
sHeader.insert(24, “OP No.”);
sHeader.insert(31, “Rep.Number”);
sHeader.insert(41, “Rep.Date”);
sHeader.insert(52, “Man.Qty”);
sHeader.insert(70, “Rep.Time”);
sHeader.insert(80, “Compl.”);
sHeader.insert(87, “Emp.No.”);
sHeader.insert(98, “Dev.WC”);
sHeader.insert(106, “Status”);
//sHeader.insert(367,”API String”);

FileWriter writer = new FileWriter(SuccessLogPatch, true);
sHeader.delete(0, sHeader.length());

//Write the lines
sSucLine = new StringBuffer(1024);

//Build the transaction string
for(int x=0; x<1024; x++) {
sSucLine.setCharAt(x,’ ‘);

if (mvxPRNO != null) { sSucLine.insert(0, mvxPRNO);}
if (mvxMFNO != null) { sSucLine.insert(16, mvxMFNO);}
if (mvxOPNO != null) { sSucLine.insert(24, mvxOPNO);}
if (mvxWOSQ != null) { sSucLine.insert(31, mvxWOSQ);}
if (mvxRPDT != null) { sSucLine.insert(41, mvxRPDT);}
if (mvxMAQA != null) { sSucLine.insert(52, mvxMAQA);}
if (mvxUMAT != null) { sSucLine.insert(70, mvxUMAT);}
if (mvxREND != null) { sSucLine.insert(80, mvxREND);}
if (mvxEMNO != null) { sSucLine.insert(87, mvxEMNO);}
if (mvxDPLG != null) { sSucLine.insert(98, mvxDPLG);}
if (Status != null) { sSucLine.insert(106, Status);}
//if (transStr != null) { sSucLine.insert(367, transStr);}

FileWriter writer = new FileWriter(SuccessLogPatch, true);
sSucLine.delete(0, sSucLine.length());
catch(IOException e)


get(Fields.Out, “TimeStamp”).setValue(outputRow, ft.format(dNow));
get(Fields.Out, “ErrorLogPatch”).setValue(outputRow, ErrorLogPatch);
get(Fields.Out, “SuccessLogPatch”).setValue(outputRow, SuccessLogPatch);
putRow(data.outputRowMeta, outputRow);


i=obj2.mvxClose(); // close connection
return true;


Hope this is may usefull for someone… if you have questions please let me know




The big deal

So one of well known swiss speciality is milk chocolates. Taking into consideration that it will be soon Christmas, i have a very few days to organize my chocolate boxes distribution roundtrip. Chocolate does not wait. I also know my friends are eager to taste them… My deliveries are packed but in which order should i load them in my car ? So many destination points, are you kidding me ?!

Hopefully, Optimap will help me find the best trip to drop my boxes on time. A few settings in TOI module plus the use of API MYS450MI and i am ready to go !

Not reinventing the wheel

Starting from Thibaud’s last post, i would like to present you an example of integration with M3.

As soon as you have exported your deliveries to Optimap, it calculates the fastest round trip :

  • each delivery point is tied to a label (DLIX value) and you are able to manually modify the order on the Optimap site by playing with the “edit route” functionality; simply drag and drop the label up or down through the list


  • the START label represents the starting/ending point of the roundtrip and corresponds to the departure warehouse
  • once you are done, go to sub-menu “export / raw path with labels” and drag and drop the result in the textbox at the top of the window


API MYS450MI/AddDelivery is used to update loading and unloading sequences of each DLIX. This way, you are able to print the loading list from DRS100 for example with the proper order you have chosen.


Functional tips :

MYS450MI uses files from TOI : MYOPIH, MYOPID, MYOPIU


To make it work, we need a partner described in MMS865. This partner should be the default value in MWS410/P. The loop is the following one :

  1. starting point : deliveries are not downloaded (message OQMSGN = blank, download status OQIRST = 10 “ready to be downloaded”)
  2. download deliveries to MYS450 (use MYS410 or MWS410+related option 54) (OQMSGN gets a new value, OQIRST = 20)
  3. Update status of the message in MYS450 (from 10 to 20) to tell M3 the external system has downloaded those deliveries (mandatory to avoid error message later during API call)
  4. Call MYS450MI/AddDelivery to upload new values for OQSULS and OQMULS fields on each delivery + execute the message (MSGN in MYS450 should get status 90 = finished, OQIRST is reset to 10 and OQSULS/OQMULS are updated)


Business rules :

  • all deliveries sent to Optimap have the same departure warehouse
  • the partner E0PA is set in MWS410/P. If you change this value in MWS410/P you must close the panel and re-open it (because CSYSTR is updated on closing the panel)
  • in our example, all deliveries have the same unloading place. As a result, we will force OQMULS = 1. MULS normally depends from DRS021 settings.
  • the first DLIX to ship is the last one loaded on the truck; OQSULS has the same sorting as Optimap roundtrip


Technical choice :

As you can manually drag to re-order stop points on the website, the drop event in WPF to catch the desired route has been chosen. The update in M3 is done after a confirm dialog box and uses a background worker with a progress bar. The navigator is closed to minimize the memory consumption.

As there is no API to retrieve the partner E0PA, an SQL statement is done to retrieve the proper value in CSYSTR.

If one of the delivery of the selected list has no message OQMSGN or a bad download status (OQIRST < 20), the progress bar displays an error message and is stucked at 99%. You can enhance business rules checks at will.


Other possibility : you can also explore the XMLHttpRequest. Some examples are available into the LSO developers guide (example with PFI integration). A good idea for v5 !

Source code

Available on Thibaud’s GitHub.



Example here.



Sigh ! Here was an example of M3 integration with Optimap round trip solver, TOI module and WPF event drag and drop.

I can’t imagine the complexity Santa Claus will face in a few days…


That’s it !





How to call M3 API from Pentaho Spoon (PDI)

HI, I am Walter. This is my first post in this blog which was usefull just more than once for me in past … now I want to contribute something also from my side…

In past I have done a lot of data exports and imports via M3API over VBA in Excel. This works fine too, but this has 2 problems:

  1. you have to pay attention if you share the files (security)
  2. there are no good practicable solution for automation of the scripts

In past I have done a lot of work also with data integration tools, one of the most used is Pentaho Spoon (PDI). So I startet some investigation of how to integrate M3 API with PDI. This is a short how to call M3 API using the data integration tool Pentaho Spoon (ex. Kettle), also known as Pentaho PDI.

Data integration tools are usefull for extracting, manipulating and writing data from and to different in and outputs.


In this example I use Kettle to extract Customerdata, transform it and save it in a csv file.

Pentaho Spoon is a very powerful tool, is based on a opensource licence and the community edition can be downloaded here. The package is platform independent and the transformation files can be build in Windows, and used on a Linux environemnt or viceversa. Probably only some path variables must be changed.


This example is based on windows environment, but the same can be done without any problems also on Linux environment.

  1. Download the Pentaho PDI package from the link above. Unzip the package and copy/paste the folder where every you want. For example directly under C:\
  2. You need a newer version of API Toolkit where also java library is available. All you need for this example is the library MvxAPI.jar which normally is located in the MvxAPI Folder.
  3. Copy the MvxAPI.jar lib to the lib Folder of Pentaho PDI, normally located in the data-integration folder.
  4. Start Pentaho PDI

Lets start

The target is to read data from M3 via the M3 API. For this purpose we use the Java library. Pentaho PDI uses graphical transformation steps which help you to create a data transformation. For some of this steps it needs also little bit of programming knowledge. But don’t worry, there are many examples in the MvxAPI package about calling M3 API via Java. And also for Pentaho PDI there are many examples for each transformation step.

For calling M3 API via PDI we use the transformation step “User Defined Java Class” (UDJC)


In total for this example I have used 6 different transformation steps:

  1. Get Variables – call variables from kettle.properties file located under your home/.kettle directory
  2. User defined Java Class – Call API and forward all retrieved records to next step.
  3. Strings cut – split the API stream in individual fields.
  4. Select values – filtering of columns which should be forwarded to next step.
  5. Trim – trim the blanks before and after the column values.
  6. Text file output – write the selected and cleaned values in a csv file.

It looks like this now:


As just explained you need to make some java development in the UDJC (User defined java class) step. Here is my piece of cake:

import java.util.regex.Pattern;
import java.util.*;
import MvxAPI.*;

private Pattern p = null;
private FieldHelper fieldToTest = null;
private FieldHelper outputField = null;

private ArrayList keys = null;
private int idx = 0;

MvxSockJ obj2;
int i, j, x;
String str,str2;
String sendstr;
double ver;
String test, MvxERR;

public boolean processRow(StepMetaInterface smi, StepDataInterface sdi) throws KettleException
//Initialize rows
Object[] r = getRow();
if (r == null) {
return false;

//assign system variables from previous step (kettle.properties)
String M3_System = get(Fields.In, “M3_System”).getString(r);
int M3_Port = get(Fields.In, “M3_Port”).getInteger(r).intValue();
String M3_User = get(Fields.In, “M3_User”).getString(r);
String M3_Pass = get(Fields.In, “M3_Pass”).getString(r);

//Create new row output
r = createOutputRow(r, data.outputRowMeta.size());

* Setup communication parameters for M3
//open the connection with system variables forwarded from previous step
obj2=new MvxSockJ(M3_System, M3_Port, “”, 0, “”);

obj2.DEBUG = true; // Have MvxSockJ print out debug info

//Call the MI with user and pass forwarded from previous step
i = obj2.mvxInit(“”, M3_User, M3_Pass, “CRS610MI”);

if(i>0) {
get(Fields.Out, “error”).setValue(r, “Error: ” +obj2.mvxGetLastError());
putRow(data.outputRowMeta, r);
return true;

get(Fields.Out, “version”).setValue(r, ver);

* Build the transaction
sendstr = “LstByNumber    1            “;

//Set max records
str=obj2.mvxTrans(“SetLstMaxRec   50          “);

//Run the transaction

if(str.startsWith(“NOK”)) {
get(Fields.Out, “error”).setValue(r, “Error:” +str);


if(str != null) {
while (str.startsWith(“REP”)) {
* This prints out the whole returned string without trying to
* extract particular fields from the layout.

// Set key and value in a new output row
get(Fields.Out, “crs610_stream”).setValue(r, str);
get(Fields.Out, “idx”).setValue(r, x);

// Send the row to the next step.
putRow(data.outputRowMeta, r);
get(Fields.Out, “error”).setValue(r, “Error: ” +obj2.mvxGetLastError());
putRow(data.outputRowMeta, r);
i=obj2.mvxClose(); // close connection
return true;

The class is calling the CRS610MI and reads the first 50 records. This records are forwarded then to the next step.

At the end the of the transformation the output is saved in the file C:\TEMP\crs610.csv

The whole transformation with all contained steps will be saved in a file with .ktr extension. Here you can download the example file! You have to accept the self signed certificate. Don’t worry, there is no risk 😉


After a short time of investigation I was able to build a really performant transformation. For reading, transforming and writing up to 10.000 records the transformation it needs less the 30 seconds. In my eyes a good performance.

Clearly you can do all what do you want with the data, save in another database, combine the data with additional datasources, save in Excel or send to another server via REST, SOAP, JSON webservice, or whatever is your requirement.

If you are not familiar with Pentaho PDI (Spoon) there are many How to’s in the web, in the Examples folder are many examples which gives you an idea how to work with PDI.

And for me the most important function that the transformation can be scheduled in combination with “Kittchen”, which is also part of Pentaho PDI.

Next steps

My next target is to write data via API in M3 using PDI. I will keep you updated about it. Hope this is may usefull for somebody of you! Let me know.. if you have any questions let me know.

Thank you,

More interfaces

If you need to integrate Infor M3 with any of the following interfaces, here are most of the interfaces with which I have worked in the past two years for which I have not posted anything on this blog yet, and for which I may be able to help you if you have questions (contact me here). For that, I used a variety of Enterprise Collaborator, Smart Office scripts, and other code.

Also, check-out the other interfaces I have worked with.

Procurement PunchOut with cXML

Hi colleagues. It has been a while since I posted anything. Today I will write a quick post as part of an interface I am currently developing to do procurement PunchOut using cXML, an old protocol from 1999, for my customer and its suppliers. This will eventually end up in their Infor M3 and M3 Enterprise Collaborator implementation.

I only needed to test the Message Authentication Code (MAC) so I wrote a quick prototype in Python.

The cXML User’s Guide describes the MAC algorithm using HMAC-SHA1-96:

Here is my implementation in Python:

# Normalize the values
data = [fromDomain.lower(),

# Concatenate the UTF-8-encoded byte representation of the strings, each followed by a null byte (0x00)
data = b''.join([(bytes(x, "utf-8") + b'\x00') for x in data])

# Calculate the Message Authentication Code (MAC)
digest = hmac.new(password.encode("utf-8"), data, hashlib.sha1).digest()

# Truncate to 96 bits (12 bytes)
truncated = digest[0:12]

# Base-64 encode, and convert bytearray to string
mac = str(base64.b64encode(truncated), "utf-8")

# Set the CredentialMac in the XML document
credentialMac = xml.find("Header/Sender/Credential").find("CredentialMac")
credentialMac.attrib["creationDate"] = creationDate
credentialMac.attrib["expirationDate"] = expirationDate
credentialMac.text = mac

Here is my resulting MAC, and it matches that of the cXML User’s Guide, good:

I posted the full source code in my GitHub repository at https://github.com/M3OpenSource/cXML/blob/master/Test.py .

That’s it!

Thank you for continuing to support this blog.

Integrating Zeacom call center with Infor Smart Office

Here is a source code that a customer and I worked out to integrate the Zeacom call center with Infor Smart Office such that their customer service representatives can receive phone calls from their customers and automatically launch the respective M3 customer programs; this is similar to the previous integration work with Cisco Agent Desktop, Twilio, Skype, etc.

This source code is a script assembly in C#; for more information on script assemblies see here and here. The trick was to keep the z variable as a global variable, not as a local variable, so it can survive in memory for the event handlers.

You will need to reference the DLL files from your Zeacom software.

using Mango.UI;
using MForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;

namespace MForms.JScript
	public class ZeacomAssembly

		public void Init(object element, object args, object controller, object debug)
			string phoneExt = "1234";
			if (z.AddExtension(phoneExt))
				if (z.Initialise())
					z.OnNewExtensionCall += OnNewExtensionCall;

		private void OnNewExtensionCall(object Extension, object Call)
			Application.Current.Dispatcher.Invoke(new ShowInformationDelegate(ShowInformation), new object[] {Extension, Call});

		private delegate void ShowInformationDelegate(object Extension, object Call);

		private void ShowInformation(object Extension, object Call)
			ConfirmDialog.ShowInformationDialog("Incoming Call", "MLI Event: OnNewExtensionCall(: " + ((MLINTERFACELib.IExtension)Extension).AddressName + ", " + ((MLINTERFACELib.ICall)Call).CallReference + OutputCallInfo((MLINTERFACELib.ICall)Call));
		private string OutputCallInfo(MLINTERFACELib.ICall Call)
			StringBuilder CallString = new StringBuilder();
			if (Call != null)
				CallString.AppendLine("CalledID = " + Call.CalledId);
				CallString.AppendLine("Caller = " + Call.Caller);
				CallString.AppendLine("CallOrigin = 0x" + Call.CallOrigin.ToString("X"));
				CallString.AppendLine("CallReason = 0x" + Call.CallReason.ToString("X"));
				CallString.AppendLine("CallReference = 0x" + Call.CallReference.ToString("X"));
				CallString.AppendLine("CLI = " + Call.CLI);
				CallString.AppendLine("CLIA = " + Call.CLIA);
				CallString.AppendLine("CLIF = " + Call.CLIF);
				CallString.AppendLine("CLIP = " + Call.CLIP);
				CallString.AppendLine("DNIS = " + Call.DNIS);
				CallString.AppendLine("Pilot = " + Call.Pilot);
				CallString.AppendLine("Query = " + Call.Query);
				CallString.AppendLine("QueryName = " + Call.QueryName);
				CallString.AppendLine("Queue = " + Call.Queue);
				CallString.AppendLine("RelatedRef = 0x" + Call.RelatedRef.ToString("X"));
				CallString.AppendLine("TransferredID = " + Call.TransferredId);
				CallString.AppendLine("TransferrerID = " + Call.TransferrerId);
				CallString.AppendLine("Trunk = " + Call.Trunk);
				CallString.AppendLine("Type = " + Call.Type);
				CallString.AppendLine("Wait = " + Call.Wait);

				if (Call.DataCallPayloadString.Length > 0)
					CallString.AppendLine("DataCallPayloadString = '" + Call.DataCallPayloadString + "'");

			return CallString.ToString();

Walking directions in a warehouse (part 2)

Today I will illustrate how I started implementing my proof-of-concept for walking directions in a warehouse, and I will provide the source code. The goal is to show the shortest path a picker would have to walk in a warehouse to complete a picking list and calculate the distance for it. This is most relevant for big warehouses and for temporary staff that are not yet familiar with a warehouse. The business benefit is to minimize picking time, reduce labor costs, increase throughput, and gather performance metrics. I used this for my demo of M3 picking lists in Google Glass.

A* search algorithm

I used Will Thimbleby’s wonderful illustration of A* shortest path in Javascript. We can drag and drop the start and stock locations to move them around and recalculate the path, and we can draw walls. I made the map bigger, and I put a warehouse image as a background.


Here are the steps I performed:

  1. Double the map’s width/height
  2. Un-hard-code the map width/height
  3. Set the cell size and calculate the canvas width/height
  4. Un-hard-code the cell size
  5. Make a warehouse image in an image editor (I used Gimp)
  6. Add the warehouse image as background of the map
  7. Hide the heat map (search scores)
  8. Patiently draw the map, the walls, the doors, and the stock locations
  9. Save the drawing by serializing the map to JavaScript source code
  10. Replace startMap with the saved drawing
  11. Thicken the path’s stroke
  12. Hide the grid lines
  13. Hide the map
  14. Use diagonals
  15. Emphasize the path length

Here is a video of the making process (watch it in full-screen, HD, and 2x speed):


You can test the result for yourself on my website here.

Here is an animated GIF of the result:

Here is a video of the result for a small warehouse:

Here is a video of the result for a big warehouse:

Source code

I put the resulting HTML, JavaScript source code and images in my GitHub repository for you to download and participate.

Future work

Some of the future work includes:

  • Convert the path length into meters or feet
  • Project the geocoded stock location coordinates to the map’s coordinates
  • Set the start and end locations as input parameters
  • Automatically generate a screenshot of the path for printing alongside the picking list
  • Show the shortest path for an entire picking list using a Traveling Salesman Problem (TSP) algorithm
  • Improve performance for big maps
  • Provide a map editor to more accurately align the warehouse image with the map

Also, a much better implementation would be to use Google Maps Indoors.


That’s it. If you liked this, please thumbs up, leave a comment in the section below, share around you, and come author the next post with me.