Custom UI for MEC

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:1

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:
3

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:
4

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\

5

Without restarting the server, I access the page at https://hostname:22108/grid/ui/#thibaud.HelloPage/MECSRVDEV:
6

I add the usual UI widgets: labels, buttons, images, table, links, etc.:
8

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:
7

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.

Published by

thibaudatwork

M3 Technical Consultant

5 thoughts on “Custom UI for MEC”

  1. 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

    Like

    1. 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?

      Like

      1. 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.

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s