Here is how to create a custom user interface for M3 Enterprise Collaborator (MEC) in the Infor Grid.
The problem
At my current customer, users create purchase orders like this: a buyer creates a purchase order in PPS170/PPS200, then M3 sends an MBM document to MEC, and then MEC sends a cXML PunchOut OrderRequest to the supplier. For some reason PPS200 does not allow changing the status of an order then, so MEC cannot give feedback to the buyer that the supplier acknowledged the order or that there was an error, i.e. the buyer does not know the outcome. The workaround is to setup error handling to have MEC send an email to an administrator in case of error, and the administrator to tell the buyer. But it is a poor design because it is a negative goal – it informs the user only in the absence of acknowledgment – it is a weak link – the administrator may or may not inform the buyer – and it is reactive, not pro-active.
A solution
Instead, I will build a custom page that shows the status of the outgoing purchase orders in MEC, with timestamp, company CONO, division DIVI, and purchase order number PUNO, I will give access to M3 users, and I will place it as a shortcut in PPS200. It will be a useful feedback loop for end-users. It is not as tight a loop as I would like it to be, but at least buyers will be able to verify the status of their orders on their own.
Inspiration
I was inspired by the MessageSearchPage of the Infor ION Grid Management Pages > MEC_UI:
I decompiled the ec-gridui-x-y-z.jar file to see how it builds the contents of the page:
and to see how StateQuery gets the data from the MEC database:
My own content
The mapping adds the CONO, DIVI, and PUNO to the manifest in a Java function with:
setManifestInfo("map:keyValue1", CONO); setManifestInfo("map:keyValue2", DIVI); setManifestInfo("map:keyValue3", PUNO);
I prepared an SQL query that gets the status of the outgoing purchase orders with the CONO, DIVI and PUNO from the MEC manifests:
HelloWorld Page
Here is a simple page:
package thibaud; import com.lawson.grid.ui.framework.PageProvider; import com.lawson.grid.ui.framework.UIRequest; import com.lawson.grid.ui.framework.UIResponse; import com.lawson.grid.ui.meta.widget.Page; public class HelloPage implements PageProvider { public Page getPage(UIRequest request, UIResponse response) { Page p = new Page("Hello Page"); p.add("Hello, World!"); return p; } }
I drop the class in the custom folder of the MEC Grid server at:
D:\Infor\LifeCycle\?\grid\?\grids\?\applications\MECSRV\MecServer\custom\
Without restarting the server, I access the page at https://hostname:22108/grid/ui/#thibaud.HelloPage/MECSRVDEV:
I add the usual UI widgets: labels, buttons, images, table, links, etc.:
I restrict access to authenticated users by adding this annotation:
@Session
I can also restrict access to a specific Grid role mapping, such as app-user, or to my own role PunchOut that I would have to create and assign users to:
@Session(roles={"app-user"}) // MECSRVDEV/app-user @Session(roles={"PunchOut"}) // MECSRVDEV/PunchOut
When I modify the class file, I stop and start the MEC_UI node:
Result
Here is the result of my page; it is similar to the built-in MessageSearchPage, but it is filtered to show outgoing purchase orders only, with additional columns company CONO, division DIVI, and purchase order number PUNO, and it is accessible by end users, except the link to the logs which is for administrators:
Full source code
Here is the full source code of my page; this code is more recent than the older screenshot above, and the code is not finished, but it is a start for you:
package thibaud; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Date; import java.util.TimeZone; import com.intentia.ec.db.ConnectionPool; import com.lawson.ec.gridui.PropertiesSourceFactory; import com.lawson.ec.gridui.page.MessageDetailPage; import com.lawson.ec.gridui.page.ViewFilePage; import com.lawson.ec.gridui.page.ViewMessageLogsPage; import com.lawson.ec.gridui.util.DateUtility; import com.lawson.grid.proxy.access.Session; import com.lawson.grid.ui.framework.PageProvider; import com.lawson.grid.ui.framework.UIRequest; import com.lawson.grid.ui.framework.UIResponse; import com.lawson.grid.ui.meta.widget.Label; import com.lawson.grid.ui.meta.widget.Link; import com.lawson.grid.ui.meta.widget.Page; import com.lawson.grid.ui.meta.widget.URLLink; import com.lawson.grid.ui.meta.widget.WidgetTable; import com.lawson.grid.ui.meta.widget.iface.Container; import com.lawson.grid.ui.meta.widget.iface.Param; import com.lawson.grid.ui.meta.widget.iface.StyledText; import com.lawson.grid.util.logging.GridLogger; @Session public class OrderRequestsPage implements PageProvider { static GridLogger cat = GridLogger.getLogger(OrderRequestsPage.class); public Page getPage(UIRequest request, UIResponse response) { // prepare the results table Page p = new Page("PunchOut OrderRequests Page"); WidgetTable table = new WidgetTable(); table.setHeaders(new String[] { "", "Time (" + TimeZone.getDefault().getDisplayName() + ")", "Company", "Division", "Purchase order", "State", "Message Details", "Message Logs", "MBM identifier", "cXML OrderRequest", "Response" /*, "IsOK", "IsReprocessed", "IsRetried", "IsVerified", "IsWaiting", "IsRecoverable"*/ }); table.setColumnAlignment(new Container.HAlign[] { Container.HAlign.RIGHT, // # Container.HAlign.LEFT, // Time Container.HAlign.CENTER, // Company Container.HAlign.CENTER, // Division Container.HAlign.CENTER, // Purchase order Container.HAlign.LEFT, // State Container.HAlign.CENTER, // Message Details Container.HAlign.CENTER, // Message Logs Container.HAlign.LEFT, // MBM identifier Container.HAlign.CENTER, // cXML OrderRequest Container.HAlign.CENTER, // Response }); table.setBorderStyle(Container.BorderStyle.SOLID_WEAK); table.setAutoSequenceRowHighlight(WidgetTable.RowHighlight.BLUE_WEAK); p.add(table); // build the query String query = "SELECT DISTINCT TOP 100\n" + "H.UUID,\n" + "S.LogTime,\n" + "M1.ManifestValue AS CONO,\n" + "M2.ManifestValue AS DIVI,\n" + "M3.ManifestValue AS PUNO,\n" + "M4.ManifestValue AS mbmIdentifier,\n" + "M5.ManifestValue AS FilePath,\n" + "S.State,\n" + "S.IsOK,\n" + "S.IsReprocessed,\n" + "S.IsRetried,\n" + "S.IsVerified,\n" + "S.IsWaiting,\n" + "S.IsRecoverable\n" + "FROM\n" + "dbo.DocManifestsHeader AS H,\n" + "dbo.DocStates AS S,\n" + "dbo.DocManifests AS M1,\n" + "dbo.DocManifests AS M2,\n" + "dbo.DocManifests AS M3,\n" + "dbo.DocManifests AS M4,\n" + "dbo.DocManifests AS M5\n" + "WHERE H.UUID=S.UUID AND S.UUID=M1.UUID AND M1.UUID=M2.UUID AND M2.UUID=M3.UUID AND M3.UUID=M4.UUID AND M4.UUID=M5.UUID\n" + "AND Partner='PunchOut' AND Agreement='Out_PunchOut_OrderRequest_SendAll'\n" + "AND M1.Name='map:keyValue1'\n" + "AND M2.Name='map:keyValue2'\n" + "AND M3.Name='map:keyValue3'\n" + "AND M4.Name='mvx:mbmIdentifier'\n" + "AND M5.Name='map:keyValue4'\n" + "ORDER BY LogTime DESC\n"; query = query.replace("dbo.", ConnectionPool.getCatalogSchema()); cat.debug(query); Connection con; Statement stmt; ResultSet rs; try { // execute the query con = ConnectionPool.getConnection(); stmt = con.createStatement(); rs = stmt.executeQuery(query); // render the result int row = 0; while (rs.next()) { String uuid = rs.getString("UUID"); // row number table.setData(row, 0, "" + (row + 1) + "."); // time Date date = new Date(rs.getLong("LogTime")); table.setData(row, 1, DateUtility.getFormattedDate(date)); // CONO String CONO = rs.getString("CONO"); table.setData(row, 2, CONO); // DIVI table.setData(row, 3, rs.getString("DIVI")); // PUNO String PUNO = rs.getString("PUNO"); try { String CONO_ = URLEncoder.encode(CONO, "UTF-8"); String PUNO_ = URLEncoder.encode(PUNO, "UTF-8"); String bookmark = "mforms://bookmark/?program=PPS200&tablename=MPHEAD&panel=B&includestartpanel=True&requirepanel=True&suppressconfirm=False&sortingorder=1&view=STD01-01&keys=IACONO%2c" + CONO_ + "%2cIAPUNO%2c" + PUNO_ + "&fields=IAPUNO%2c" + PUNO_; URLLink bookmarkLink = new URLLink(new URL(bookmark), PUNO); // PENDING table.setData(row, 4, bookmarkLink); } catch (UnsupportedEncodingException e) { table.setData(row, 4, PUNO); } catch (MalformedURLException e) { table.setData(row, 4, PUNO); } // State... String state = rs.getString("State"); int isOK = rs.getInt("IsOK"); StyledText.Category c; if (isOK == 0) { c = StyledText.Category.ERROR; } else { c = StyledText.Category.OK; } table.setData(row, 5, new Label(state, StyledText.Size.NORMAL, StyledText.Style.BOLD, c)); // Message Details Link messageLink = new Link("show", MessageDetailPage.class, new Param[] { new Param("uuid", uuid) }); messageLink.setOpenInNewWindow(true); table.setData(row, 6, messageLink); // Message Logs Link eventLink = new Link("show", ViewMessageLogsPage.class, new Param[] { new Param("uuid", uuid) }); eventLink.setOpenInNewWindow(true); table.setData(row, 7, eventLink); // MBM identifier; PENDING: see com.lawson.ec.gridui.page.MessageDetailPage.getAllData(String uuid) table.setData(row, 8, rs.getString("mbmIdentifier")); // cXML OrderRequest String host = PropertiesSourceFactory.getInstance().getProperties().getProperty("Server.Context"); cat.debug(host); String filepath = rs.getString("FilePath"); Link xmlLink = new Link("show", ViewFilePage.class, new Param[] { new Param("host", host == null ? "local" : host), new Param("type", "xml"), new Param("path", filepath) }); xmlLink.setOpenInNewWindow(true); table.setData(row, 9, xmlLink); // cXML response table.setData(row, 10, "PENDING"); row++; } // cleanup rs.close(); stmt.close(); ConnectionPool.putConnection(con); } catch (SQLException e) { cat.error("SQLException when building PunchOut page", e); p.add(new Label("SQLException when building PunchOut page: " + e.getMessage())); } return p; } }
Future work
Some ideas for future work are:
- Tighten the loop even more, i.e. inform the user directly in PPS200 when they are creating the order
- Get the state label; see com.lawson.ec.gridui.page.MessageSearchPage.getStateLabel()
- Finish the MForms Bookmark link
- Add a link to the MBM document (.rcv file)
- Add a link to the cXML OrderRequest response received from the supplier
- Register the page in the MEC menu
- Get URL parameters from UIClientRequest
- Add pagination, see com.lawson.ec.gridui.page.MessageSearchPage.buildPagingPanel()
- Develop a Mashup that combines PPS200 and the SQL from MEC DB
Conclusion
That was a solution to develop custom UI pages for MEC; for example in my case a page for buyers to see the status of the outgoing purchase orders, with timestamp, company CONO, division DIVI, and purchase order number PUNO. It is a simple solution to provide feedback to end users so they can be pro-active and verify the outcome of their actions on their own.
That’s it.
Thanks for reading. Leave us a comment in the section below. And if you liked this, please subscribe, like, share, and come write the next blog post with us.
Great post.
The setManifestInfo function can be used for the Advanced Search. MEC will put in its “Manifest Database” anything you type after “map:”, and you can edit the file ManifestFilter.xml to support the new manifest.
Jonathan
LikeLike
Thank you. I did not know about the ManifestFilter.xml. Great. My colleague said the setManifestInfo only works with names map:keyField and map:keyValue? Is that true? Can’t it be map:CONO or simply CONO?
LikeLike
You can type anything you like as long as you start it with “map:”. For instance, I use setManifestInfo(“map:EDI_CUNO”, REF_CUNO); to add the Customer’s PO order number in EDI message.
Also, it is possible to retrieve date from the Manifest during XML Transformation, for instance getManifestInfo(“UUID”) will get you the UUID that was generated.
Jonathan.
LikeLike
Great news. Thank you.
LikeLike
This is fantastic work! Did you implemented your “future work”? Or if you wish to make it Open source, we can contribute.
LikeLike