Route optimization for MWS410 with OptiMap

Here is a script for Lawson Smart Office that integrates the Delivery Toolbox – MWS410/B with OptiMap – Fastest Roundtrip Solver to calculate and show on Google Maps the fastest roundtrip for the selected Routes. It’s a solution to the Travelling salesman problem (TSP) for M3 Routes.

This is interesting for a company to reduce overall driving time and cost, and it’s interesting for a driver to optimize its truck load according to the order of delivery.

To solve the TSP on Google Maps, the authors of OptiMap implemented several algorithms, including brute force, nearest-neighbor, and the Ant Colony Optimization, and released the code for the TSP Solver for Google Maps API as open source with an MIT License. Because the TSP is an NP-complete problem the solution only works well for up to 10 cities on current desktop computers. But OptiMap can apparently solve more than 15 cities. You can read more about OptiMap at Behind the Scenes of OptiMap and OptiMap version 4 is here.

To integrate OptiMap with Smart Office I wrote a simple Personalized Script that gets the addresses of the selected Routes in MWS410/B, and that opens OptiMap in a web browser with the delivery addresses in the URL. The GET parameters are explained in OptiMap’s Optimize Your Trips.

Setup

To install and use the script:

  1. Deploy this script in the mne\jscript\ folder in your Smart Office server:
  2. Create a Shortcut in MWS410/B to run this script; for that go to MWS410/B > Tools > Personalize > Shortcuts > Advanced, expand Script Shortcut, set the Name to OptiMap, set the Script name to OptiMap, click Add, and click Save:
  3. Create a View (PAVR) in MWS410/B that shows the address columns ADR1, ADR2, ADR3:
  4. Select multiple Routes in the list (press CTRL to select multiple rows), and click the OptiMap Shortcut to run the script:

Result

The script will launch OptiMap for the selected Routes, and OptiMap will optimize the order of delivery:

Source code
Here is the complete source code for the script:

import System;
import System.Web;
import System.Windows;
import Mango.UI.Services.Lists;
import MForms;

/*
Integrates the Smart Office Delivery Toolbox - MWS410/B with OptiMap - Fastest Roundtrip Solver, http://www.optimap.net/
to calculate and show on Google Maps the fastest roundtrip for the selected Routes.
This is interesting to reduce driving time and cost, and for a driver to optimize its truck load according to the order of delivery.
1) Deploy this script in the mne\jscript\ folder in your Smart Office server
2) Create a Shortcut in MWS410/B to run this script; for that go to MWS410/B > Tools > Personalize > Shortcuts > Advanced > Script Shortcut, set the Name to OptiMap, and set the Script name to OptiMap
3) Create a View (PAVR) in MWS410/B that shows the address columns ADR1, ADR2, ADR3
4) Select multiple Routes in the list (press CTRL to select multiple rows)
5) Click the OptiMap Shortcut to run this script and launch OptiMap for the selected Routes
For more information and screenshots refer to https://thibaudatwork.wordpress.com/2012/10/04/route-optimizer/
Thibaud Lopez Schneider, Infor, October 4, 2012 (rev.2)
*/
package MForms.JScript {
    class OptiMap {
        public function Init(element: Object, args: Object, controller : Object, debug : Object) {
            try {
                // get the list
                var listControl: MForms.ListControl = controller.RenderEngine.ListControl;
                var listView: System.Windows.Controls.ListView = controller.RenderEngine.ListViewControl;
                if (listControl == null || listView == null) { MessageBox.Show('Error: Couldn\'t find the list.'); return; }
                // get the selected rows
                var rows = listView.SelectedItems; // System.Windows.Controls.SelectedItemCollection
                if (rows == null || rows.Count == 0) { MessageBox.Show('Error: No rows selected.'); return; }
                // get the address columns ADR1, ADR2, ADR3
                var column1: int = listControl.GetColumnIndexByName('ADR1');
                var column2: int = listControl.GetColumnIndexByName('ADR2');
                var column3: int = listControl.GetColumnIndexByName('ADR3');
                if (column1 == -1 || column2 == -1 || column3 == -1) { MessageBox.Show('Error: Couldn\'t find the address columns ADR1, ADR2, ADR3.'); return; }
                // construct the URL
                var query: String = '';
                for (var i: int = 0; i < rows.Count; i++) {
                    var row: ListRow = rows[i];
                    var ADR1: String = row[column1];
                    var ADR2: String = row[column2];
                    var ADR3: String = row[column3];
                    var address: String = ADR1 + ',' + ADR2 + ',' + ADR3;
                    query += 'loc' + i + '=' + HttpUtility.UrlEncode(address) + '&';
                }
                var uri: Uri = new Uri('http://www.optimap.net/?' + query);
                // launch OptiMap
                ScriptUtil.Launch(uri);
            } catch (ex: Exception) {
                MessageBox.Show(ex);
            }
        }
    }
}

That’s it!

Related posts

See also version OptiMap_V2.

Published by

thibaudatwork

ex- M3 Technical Consultant

10 thoughts on “Route optimization for MWS410 with OptiMap”

  1. I’ve tried to add the script, but when creating the shortcut, I only get the errormessage ‘Script OptiMap.js contains error’. Is the script above correct?
    – Learning LSO

    Like

  2. Hi Jon. Yes the script above is correct. It might not work for all versions of Smart Office. I used LSO 10.0.4.1.39. Which version do you have? Also, it may be a copy/paste issue of the source code; it can happen that WordPress wraps a line on the screen, and after copy/paste it becomes two lines thus failing at compilation. Also, try running the script in the Script Tool (mforms://jscript) and see what compilation error you receive.

    Like

  3. Hi ,

    I wrote Jscript to validate common no (list view column) of the selected records in MWS410 program .

    If selected records don’t have common no value . I need to stop process .

    In OnRequested event , I have unregister the event . when I scroll down , script call onRequesting event , then OnRequested event . Then first time I press F19 , script reinitialized and call Init method . but not call onRequesting event . therefor my validation fail . I have attached my jscript for your reference . please can you suggest workaround ..

    Why Init method is called after scroll down ?

    Thank you

    import System;
    import System.Windows;
    import System.Windows.Controls;
    import MForms;
    import Mango.UI.Services.Lists;
    import Mango.UI;
    import Mango.UI.Core;
    import MForms;
    import Mango.UI.Services;
    import Mango.Core.Util;
    import Mango.Services;
    import Lawson.M3.MI;

    package MForms.JScript {
    class MWS410_B1_MASNOVALSU {

      var  _element : Object;
      var  _args: Object;
      var  _controller : Object;
      var  _debug : Object;
      var _RORN : String;
      var _payer : String;
      var _listControl;
      var _listView;
      var _RORC;
      var _view;
    
      public function Init(element: Object, args: Object, controller : Object, debug : Object) {
         debug.WriteLine("Script Initializing.");
         if(element != null) {
            debug.WriteLine("Connected element: " + element.Name);
         }
    
         var content : Object = controller.RenderEngine.Content;
    
         // TODO Add your code here
         _element = element;
         _args = args;
         _controller = controller;
         _debug = debug;
         _listControl = controller.RenderEngine.ListControl;
         _listView = controller.RenderEngine.ListViewControl;
         _view = controller.PanelState.View;
    
           //Get Order Number
          _controller.add_Requesting(OnRequesting);  
          _controller.add_Requested(OnRequested);
          _RORN = ScriptUtil.FindChild(content, "WWRIDN").Text; 
          _RORC = ScriptUtil.FindChild(content, "WWRORC").Text; 
          getPayer();
          MessageBox.Show("init");
          MessageBox.Show(controller.Tag);
          controller.Tag = "1";
    
    
      }
    
       function OnRequested(sender: Object, e: RequestEventArgs){
       MessageBox.Show("OnRequested " + _controller.PanelState.PanelHeader);
    
        /*  if(!_controller.PanelState.View.Equals(_view))
           {
            MessageBox.Show("clear");
             _view = _controller.PanelState.View;
             _controller.remove_Requesting(OnRequesting);  
             _controller.remove_Requested(OnRequested);
           }*/
    
        /* if(!_controller.PanelState.PanelHeader.Contains("MWS410/B") && !_controller.PanelState.View.Equals(_view)){
           MessageBox.Show("MWS410");
             _controller.remove_Requesting(OnRequesting);  
             _controller.remove_Requested(OnRequested);
          }*/
    
           _controller.remove_Requesting(OnRequesting);  
           _controller.remove_Requested(OnRequested);
      } 
    
    
      public function OnRequesting(sender : Object, e : CancelRequestEventArgs)        
      { 
            MessageBox.Show("OnRequesting");
            if (_controller.PanelState.PanelHeader.Contains("MWS410/B") && e.CommandType == "LSTOPT" && e.CommandValue == "19" && _payer.Equals("BRA01107") && _RORC.Contains(3)) {
    

      var selectedRows = _listView.SelectedItems;
    var rowCount = 0;
    var selectedRowCount = _listView.SelectedItems.Count;

               while (rowCount < selectedRowCount) 
               {
                  var _ETRN =  _listControl.GetColumnValue("ETRN",selectedRows[rowCount]);
                   
                  if(_ETRN == null)
                  {
                      MessageBox.Show("Common No column does not found , please add it , before process ");
                      e.Cancel = true;
                      break;
                  }
                  else if(_ETRN == "")
                  {
                      MessageBox.Show("Please enter Common number ");
                      e.Cancel = true;
                      break;
                  }
    
                  ++rowCount;
               }
    
            }
      }
    
      public function getPayer()
      {
            try {
    
                var record = new MIRecord();
    
                var api = "OIS100MI";
                var trans = "GetHead";
    
                record["CONO"] = UserContext.CurrentCompany;
                record["ORNO"] = _RORN;
    
                MIWorker.Run(api, trans, record, OnPayerResponse);
    
            }catch (ex) {
                _debug.WriteLine("Error accessing OIS100MI-GetHead: "+ex);
                return;
            }
       }
    
    
        public function OnPayerResponse(response : MIResponse) {
    
            if(!response.HasError) {    
                var record = response.Item;
                _payer = record["PYNO"];
                _debug.WriteLine("Payer : "+ _payer);
            } else {                
                _debug.WriteLine("Error retrieving Ref Order Cat: " + response.Error.ToString());
                return;     
            }
        }
    

    }
    }

    Like

  4. Hi Priyantha. Yes, every time the user refreshes (F5) the panel where the script is attached, the Init method will be called; that is part of the lifecycle of a script. Likewise, when a user cancels (F12) and returns to the previous panel where the script was attached, the Init method will be called again. But if you move forward to the next panel (ENTER) the Init method is not called again. There are many combinations depending on the M3 program’s response, and I don’t think it’s deterministic, but you can figure out the most common ones. I can’t help you with your script, but it looks like you are on the right track. I think in your case, on F19, you are not getting the OnRequesting event because youunregistered it before; you have to unregister it only on the desired states of the lifecycle. See if this other post can help you, https://m3ideas.org/2012/09/21/how-to-add-a-column-to-a-list-continued/ Maybe you are missing the event RequestCompleted; read the Smart Office Developer’s Guide and/or the Smart Office SDK Help (Infor Xtreme Downloads). Also, maybe you need a StartDelegate; there are examples on this blog and on Karin’s blog. To help you troubleshoot, you can use Fiddler to see when the MIWorker calls the M3 API; I find it quicker than refreshing the Smart Office log and scrolling down. You can also use the tail program on the Smart Office log for continuous feedback. Hope it helps. –Thibaud

    Like

  5. Hi,
    I have gone through javascript document and not get any relevant document for life cycle of javascript on panel.
    I only understand init method.
    I am new in java script for ISO and not able to understand flow and execution of a script.

    I also have a query..
    I want to make visible a button on B panel after selection of a record but it is gets visible on load of panel -B in OIS350 and as require I need to make it visible after selection of a record (one click from mouse).

    below is my code :
    please help

    Thanks

    import System;

    import System.Windows;

    import System.Windows.Controls;

    import MForms;

    import Mango.UI.Core;

    import Mango.Services;

    import Mango.UI.Services;

    package MForms.JScript {

    class Output {

        //private var content : Object; 
        private var subFileList : Object;
    
        var controller : Object;
    
        var currentProgram : String;
    
        var list : ListView;
    
        var column;
    
        var debug : Object; 
        var button : Button;
        var host,buttonClose;
        var item;
        var content: Object;
    
        //var programName = "HcsMom: /// invoiceNo =?";
        //var programName = "HcsMom:///invoiceNo=";
        var programName = "report:///?invoiceNo=";
    
    
    
    
      public function Init(element: Object, args: Object, controller : Object, debug : Object) {
            this.controller = controller;
            this.debug = debug;
            this.list = controller.RenderEngine.ListControl.ListView;
            this.column = controller.RenderEngine.ListControl.Columns;      
            this.content= controller.RenderEngine.Content;
    
    
            debug.WriteLine("Script Initializing.");
    
            this.item = list.SelectedItem;
            debug.WriteLine("Script Initializing.............");        
    
    
          //  if(item != null) {
    
            button=new Button();
            button.Content = "Click";
            button.Width = 50;
            button.HorizontalAlignment = HorizontalAlignment.Center;
            Grid.SetRow(button, 2);
            Grid.SetColumn(button, 50);
            Grid.SetColumnSpan(button, 90); 
    
            content.Children.Add(button);
            button.add_Click(OnClickOpen);
    
    
        //  }
    
    
    
    
      }//init
    
    
    
    
     private function OnClickOpen(sender: Object, e: RoutedEventArgs)
        {
                var columnVal1 : String = "";
                var columnName : String = "IVNO";
                var invoiceNo: String;
    

    try {

            invoiceNo = item[ getColumnId("IVNO") ];
            DashboardTaskService.Manager.LaunchTask(new Task(programName + invoiceNo));
            //DashboardTaskService.Manager.LaunchTask(new Task(programName));
            debug.WriteLine("Script Initializing...........+++++.."); 
    
    
         } 
    
         catch(ex) {
            Log(ex);         
         }
    
            }//onclick
    
    
    
        public function getColumnId( columnName : String ) {
            var id;
            for(var i=0; i<column.Count; i++) {
                if (this.column[i] == columnName) {
                    id = i;
                    break;
                }               
            }
    
            return id;
        }
    
    
      public function OnClickClose(sender: Object, e: RoutedEventArgs) {         
         if(host != null) {
            host.Close();
            host = null;
         }
      }
       function Log(text : String)
      {
         debug.WriteLine(text);
      }
    
      }//class
      }//output
    

    Like

    1. Hi,

      Smart Office script are written in JScript.NET (not JavaScript).

      If you attach the script to the panel as a shortcut (shortcuts toolbar on the right), then Smart Office invokes the script’s Init method when the user clicks the shortcut (that’s the example in this blog post). If you attach the script from the scripts menu, then Smart Office invokes the script’s Init method whenever it renders a panel (start, F5-Refresh, F12-Cancel, etc.). You can also invoke a script with the mforms://runscript URI.

      I recommend reading the Smart Office M3 Developer’s Guide for the script lifecycle, and the Microsoft .NET documentation for the Buttons, ListView, etc. Read also the other blogs. Read also the Smart Office SDK documentation.

      In your case, you are reading selectedItem too soon; the user hasn’t had a chance to select a row because the panel is still rendering. You should read selectedItem in the OnClick event handler, and add a condition if (list.selectedIndex == -1) return or something to check that a row has been selected.

      As for your question on how to show/hide the button on selectedItem, I will argue that it’s bad usability. Instead, I would keep the button visible always, and simply enable/disable the button on selectedItem. In either case, you need an OnSelected event on the ListView (I forgot the actual event), and change the properties of the button in the event handler.

      Hope it helps.

      –Thibaud

      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 )

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