Friday, November 23, 2012

LSO & M3 discussion forums

I was trolling through some older posts on Karin's blog and spotted a reference to a LSO discussion forum in the comments.

The address is http://www.lawsonguru.com/forums/ux/lso/   Thanks Karin for pointing this out :-)

For generic M3 / Movex issues the discussion forum http://erp.ittoolbox.com/groups/technical-functional/intentia-l/ is a great place to ask questions and contribute to collective community knowledge base.

Friday, November 16, 2012

Lawson Web Services and .Net - a guest post

My friend and colleague Paul Grooby at Resene Paints in Wellington was kind enough to contribute the post below about his trials and tribulations with consuming Lawson Web Services in .Net.



Using Visual Studio .Net and Lawson Web Services.
Paul Grooby – 2012-11-02.

Using Lawson WebServices is a doddle, once you understand how it fits together. And if you want to generate simpler data entry and display screens (without SQL and wrapping API’s then this is the ticket).  Once you've created your web-services in Lawson Web Services and published these you can see them in a browser before you start testing them using SOAPUI.


Click on the WSDL file link above – this will load the XML file in the browser that you can then save.


Choose File ->Save from the menu to save the file as a WSDL file on your system.

Now a caveat – while Lawson Web Services generated xsd:datetime definitions for some of the API fields, it doesn't accept accept xsd:datetime inputs to them – so open the saved WSDL file in a text editor and do a find and replace on any datetime fields.


Replace the datetime fields with a string datatype. Once you've checked the remainder of the WSDL file its time to do the cool stuff and fire up Visual Studio (we’ll need this WSDL file later).  I'm going to create a blank web application for this demo (it could as easily be a console application or windows application).

I normally add the external WSDL files to the application and place them in their own folder in the project.  Locate the file and add to the project.


From here select the file and in the properties box of the studio project select and copy the file path.

With the file path copied to the clipboard, select the option to add a service reference to the application..

The following dialog box will be displayed:
Paste the file path into the Address bar – Don’t click Ok just yet – click the advanced button to check a couple of important options.

 Ensure that these two check boxes are checked and then click the OK button.

Give the webservice a relevant namespace. As a naming convention I tend to leave these the same name as the API / Webservice so that these can be traced back.

Once you click the Ok button a list of service endpoints will be displayed.

Click Ok – Visual Studio will do its thing and generate the necessary references and stubs for the program.

The project will now look like the following 

If your project doesn't show all of the files then click on the button highlighted to see them.

The two files with the extension 'svcinfo' contain details of where the endpoint of the service is located. This is important in that you’d normally develop, test and then deploy in the different environments. If you don’t remember that the endpoints are embedded in these files you could be pointing at the wrong environment.

However to save a little hassle, and this is why we copied the file locally we can edit our WSDL file that's contained in the project.

The endpoint is embedded in the WSDL – if you change this to point to say PROD and regenerate the files, the files with the svcinfo extension will be updated – follow this process here:

1: Alter the end point in the WSDL file. Save.

2: Against the service select the option to configure the service reference
This will display the follow dialogue box:
Make sure the address is pointing to your local file then click Ok.  

From the project menu for the service select the option to update the service reference:

You can check that the service has taken by opening the svcinfo files and checking the end point.

This process also updates the web.config file in this case so we just need to check that this has been updated.

If we see two endpoints with the same contract we’re in trouble – delete the incorrect one as the process will not work.

So that’s all there is to it for the adding of a reference. If you want to use the interactions you’ll need to write a bit of code. The following section has a simple class that consumes the objects used above.

The following class is used to interact with the webservice (I won’t teach you about programming – suffice to say this works a treat).  It creates a 'Customer' object, calls the various methods to populate the relevant bits of information and retains that information.

using System;
using SampleLWSApplication.CRS610MI;

namespace SampleLWSApplication
{
    public class Customer
    {
        #region PrivateVariables
        private String _company = "";

        // also based on this customer we should be able to also use the GetBasicData
        private String _customername = "";

        private String _customernumber = "";
        private String _customerpassword = "";
        private String _customersordernumber = "";
        private String _division = "";
        private String _facility= "";

        // get the other details such as order number , etc
        private String _ordertype = "";
        private String _password = "";
        private String _payer = "";
        private DateTime _requesteddeliverydate ;
        private String _transactionreason= "";
        private String _username = "";
        private String _warehouse= "";
        private String _emailaddress = "";
        private String _specialnstructions = "";
        #endregion

        public Customer()
        {
            // create the class
        }
        /// <summary>
        /// Get CustomerData is to pull the data through for the customer
        /// </summary>
        /// <param name="CustomerNumber"></param>
        public void GetBasicData(String CustomerNumber)
        {
            // code to do a lookup against the customer and retrieve the basic data from the system
            // always need a client
            CRS610MI.CRS610MIClient crs610MIClient = new CRS610MIClient();
            // create the soap header
            CRS610MI.headerType mwsheader = new CRS610MI.headerType();
            mwsheader.user = _username; // user name for the API from the configuration file
            mwsheader.password = _password; // password for the API from the configuration file
            mwsheader.company = _company;
            mwsheader.division = "xxx";
            // create a new object to hold the details
            CRS610MI.GetBasicDataItem getBasicData = new GetBasicDataItem();
            // set the variables for the process
            getBasicData.Company = 100;
            // set to the passed
            getBasicData.CustomerNumber = CustomerNumber.ToUpper(); // force to upper case

            CRS610MI.GetBasicDataCollection collection = new GetBasicDataCollection();
            collection.maxRecords = 1000;
            collection.GetBasicDataItem = new GetBasicDataItem[1];
            collection.GetBasicDataItem[0] = getBasicData;

            CRS610MI.GetBasicDataRequest crs610MIRequest = new GetBasicDataRequest(mwsheader, collection);

            try
            {
                // pul back the data responses
                CRS610MI.GetBasicDataResponse responseItem = crs610MIClient.GetBasicData(crs610MIRequest);
                // turn this into an array
                CRS610MI.GetBasicDataResponseItem[] ri = responseItem.GetBasicDataResponse1;
                // loop through the array
                for (int i = 0; i < ri.Length; i++)
                {
                    // output the data
                    // set the values
                    _customername = ri[i].CustomerName;
                    _transactionreason = ri[i].FreeField3;
                    _division = ri[i].Division;
                }
            }
            catch (System.ServiceModel.FaultException ex)
            {
                Console.Write(ex.Message);
            }
        }

        /// <summary>
        /// Get CustomerData is to pull the data through for the customer
        /// </summary>
        /// <param name="CustomerNumber"></param>
        public void GetOrderInfo(String CustomerNumber)
        {
            // code to do a lookup against the customer and retrieve the basic data from the system
            // always need a client
            CRS610MI.CRS610MIClient crs610MIClient = new CRS610MIClient();
            // create the soap header
            CRS610MI.headerType mwsheader = new CRS610MI.headerType();
            mwsheader.user = _username; // user name for the API from the configuration file
            mwsheader.password = _password; // password for the API from the configuration file
            mwsheader.company = _company;
            mwsheader.division = "xxx";
            // create a new object to hold the details
            CRS610MI.GetOrderInfoItem getOrderInfo = new GetOrderInfoItem();
            // set the variables for the process
            getOrderInfo.Company = 100;
            // set to the passed
            getOrderInfo.CustomerNumber = CustomerNumber.ToUpper(); // force to upper case

            CRS610MI.GetOrderInfoCollection collection = new GetOrderInfoCollection();
            collection.maxRecords = 1000;
            collection.GetOrderInfoItem = new GetOrderInfoItem[1];
            collection.GetOrderInfoItem[0] = getOrderInfo;

            CRS610MI.GetOrderInfoRequest crs610MIRequest = new GetOrderInfoRequest(mwsheader, collection);

            try
            {
                // pull back the data responses
                CRS610MI.GetOrderInfoResponse responseItem = crs610MIClient.GetOrderInfo(crs610MIRequest);
                // turn this into an array
                CRS610MI.GetOrderInfoResponseItem[] ri = responseItem.GetOrderInfoResponse1;
                // loop through the array
                for (int i = 0; i < ri.Length; i++)
                {
                    // output the data
                    // set the values
                    _facility = ri[i].Facility;
                    _warehouse = ri[i].Warehouse;
                    _payer = ri[i].Payer;
                    _division = ri[i].Division;
                    _ordertype = ri[i].CustomerOrderType;
                }
            }
            catch (System.ServiceModel.FaultException ex)
            {
                Console.Write(ex.Message);
            }
        }

        // Properties here

        #region Properties

        public String SpecialInstructions
        {
            get { return _specialnstructions; }
            set {_specialnstructions = value;}


        }
        public String Company
        {
            get { return _company; }
            set { _company = value; }
        }

        public String EmailAddress
        {
            get { return _emailaddress; }
            set { _emailaddress = value; }
        }
        public String CustomerName
        {
            get { return _customername; }
            set { _customername = value; }
        }

        public String CustomerNumber
        {
            get { return _customernumber; }
            set { _customernumber = value; }
        }

        public String CustomerOrderNumber
        {
            get { return _customersordernumber; }
            set { _customersordernumber = value; }
        }

        public String CustomerPassword
        {
            get { return _customerpassword; }
            set { _customerpassword = value; }
        }

        public String Division
        {
            get { return _division; }
            set { _division = value; }
        }

        public String Facility
        {
            get { return _facility; }
            set { _facility = value; }
        }

        public String OrderType
        {
            get { return _ordertype; }
            set { _ordertype = value; }
        }

        public String Password
        {
            get { return _password; }
            set { _password = value; }
        }

        public String Payer
        {
            get { return _payer; }
            set { _payer = value; }
        }

        public DateTime RequestedDeliveryDate
        {
            get { return _requesteddeliverydate; }
            set { _requesteddeliverydate = value; }
        }

        public String TransactionReason
        {
            get { return _transactionreason; }
            set { _transactionreason = value; }
        }

        public String UserName
        {
            get { return _username; }
            set { _username = value; }
        }

        public String Warehouse
        {
            get { return _warehouse; }
            set { _warehouse = value; }
        }

        #endregion Properties

       
    }
}

A note or two about the code.

The basic structure for each call is the following:
  • Create a client
  • Create a mwsheader object (this is the SOAP Header)
  • Create a base item for the method to pass parameter
  • Create a collection of correct return types
  • Add the base item as array 0 to pass
  • Add a request of the correct type
  • Call the method and return a collection of responseitems of the correct type
  • Check for any errors and if none use the returned collection of types in any manner that you like – for 'Get' methods its likely that there is only one object returned, for List methods there is likely to be 0 to many.

Importantly – You’ll also find problems with the service if using a List type operation where the amount of data returned by the service exceeds the file size (not sure where this is set but it seems to be around 64K) – to get around this problems use the following syntax when creating the client.

Note that I've also added complexity by having this endpoint embedded in the web.config file – its' my style of programming.

String endPoint = System.Configuration.ConfigurationManager.AppSettings["INTERNETORDERSENDPOINT"];

WebOrders.InternetOrdersClient orderClient = new InternetOrdersClient(new asicHttpBinding(BasicHttpSecurityMode.None) { MaxReceivedMessageSize = 2147483647, MaxBufferSize = 2147483647 }, new EndpointAddress(endPoint));



In the above example we've set the return size to a huge number – this get around the limitation but still may not be enough – in which case you’d probably want to look at the fields being output from the Web service and whether you need them all.

Enjoy :-)


Open source alternative to MvxSock for API interactions with M3

I've recently come across an open source alternative to Infor's MvxSock library called MvxLib (http://mvxlib.sourceforge.net/).  I haven't had the time yet to look into this in detail but there are a few potential benefits I see from this:

  • Firstly it's a native .Net library, so no more wrapping the old style MvxSock dll whenever we need to use it in a .Net application;
  • Secondly it's open source, so with some work this could be used as the basis for building (for example) applications in Windows Mobile, Android or IOS;
  • Thirdly it's open source.  I know I already mentioned that one but it's worth reiterating.  There is an ecosystem of 3rd party consultants and software solutions that interact with M3.  Wouldn't it be nice to be able to do so without being beholden to Infor for access to the underlying library that allows this?
For my next project I'm certainly going to look at this as an option.  And and when I do (or someone else does and lets me know about it) I'll post my conclusions on its suitability as a MvxSock replacement.

Friday, October 12, 2012

Another JScript and Mashups blog

I've just spotted another JScripts and Mashups blog over at http://www.joshgeving.com

It appears to be really focussed on S3, but would be worth checking out.

Friday, January 13, 2012

New Lawson S3 Technology Blog

There's a new Lawson S3 Technology Blog at lawsons3tech.wordpress.com.  As there is lots of technology (like LSO, PFI and LBI) shared between S3 and M3 this may be an interesting blog to follow for M3 users as well.

<Edit>
Note that the blog has now moved to Infor's site http://blogs.infor.com/s3tech/
</Edit>

Tuesday, January 10, 2012

JScripts and toolbox programs


Thibaud has posted a entry on how JScripts that can be programmed to be generic across multiple screens.  This reminded me of a solution I've used for dealing with M3 toolbox screens where the columns can change based on the panel version selected by the user.

Columns in LSO can be queried by looking at the controller.RenderEngine.ListControl.Columns list.  The following JScript parses through the list of columns (in my case in mms080) to find the columns that store the ORCA (order category) and RIDN (order number) columns.

   var category_column = 100;
   var orderno_column = 100;
   var listControl = controller.RenderEngine.ListControl;
   var columns = listControl.Columns;
   for(var i=0; i < columns.Count; i++) {
      if(columns[i] == "ORCA") { category_column = i;}
      if(columns[i] == "RIDN") { orderno_column = i;}
   }


From there I can check to see if I have the columns I need to run my JScript logic, and if I do not then advise the user accordingly.

   if (qty_column != 100 && category_column != 100') {
      //apply logic
   } else {
      //advise the user that we don't have the required information to run the script
   }

This approach allows robust JScripts that work with user-configurable toolbox screens.

Saturday, January 7, 2012

Screen design changes using JScript

I continue to be impressed with what can be achieved with JScript and LSO. The other day I wanted to improve the readability of a screen. Using personalisations I hid unnecessary fields, but I was then left with fields scattered across a screen that I wanted to group together. JScript allows me to find controls on the screen and move these around with the Grid.SetColumn and Grid.SetRow functions:

import System;
import System.Windows;
import System.Windows.Controls;
import MForms;

package MForms.JScript {
   class MMS001_ChangeItemFieldPos {
      public function Init(element: Object, args: Object, controller : Object, debug : Object) {
         var content : Object = controller.RenderEngine.Content;

         //Find the control we want to move
         var textBoxItem = ScriptUtil.FindChild(content, "MMITNO");

         //Specify where we want to move the control to
         Grid.SetColumn(textBoxItem, 50);
         Grid.SetRow(textBoxItem, 1);

      }
   }
}
Prior to running this script the screen looks like this:

After running the script the Item number field has been moved:


Combined with the ability to set the Tab Order in LSO we're able to create streamlined panels that show just the information we're interested in, and group them together into business or user-specific groupings.