Importing BUS100 Budgets via the APIs

After spending years looking at the M3 APIs in disdain and grudgingly using them, it looks like I’ve finally been converted.

Over the past 12 months due to a variety of projects for different people I’ve relied upon them more and more and found them to be a lot better than my early encounters. And being able to call them directly within Smart Office 10.x without needing to authenticate has been a real boon.

Now those of you that have long memories will recall that I created a script which added a button to BUS100 which would read a spreadsheet and import the data in to BUS100 budgets, we do this as the manual entry is horrible, especially if you have different monthly amounts during the year. Due to the way the BUS100 worked I couldn’t use the same method I used for the GLS100 imports so I resorted to use the APIs wrapped in a webservice. It worked well for IFL…

Roll on 2014 and Upgrade-X (more of this in another post), and I needed to uplift the webservices. The webservices I originally created were pre-grid and were going to need some tweaking along with some other non-technical challenges I figured I’d spend the time converting the code to use the APIs instead. The structure of the script made it fairly trivial and now means that it’s easier for others to use and when further upgrades are done I don’t need to worry about messing around with webservices.

First off, here is the spreadsheet template, it should be fairly self-evident how to use it…

https://drive.google.com/file/d/0Bxi1gJ1tQyDRdDN4SE8zcExZOTQ/view?usp=sharing

And now the updated script…(I was facing time pressure so it’s not terribly pretty but it is functional)

//
// 20121214 v005    - changed the division so it is no longer statically set
// 20121214 v007    - the name and description of the budget were being ignored and set as TEST
// 20141122 v008	- convert to UpgradeX
//
import System;
import System.IO;
import System.Net;
import System.Windows;
import System.Windows.Controls;
import MForms;

import System.Web.Services.Protocols;
import System.Xml;

import Lawson.M3.MI;
import Mango.UI;

import System.Text;
import Excel;
import System.Reflection;

package MForms.JScript
{
	class BUS100_BudgetImport_008
	{
		var giicInstanceController : IInstanceController = null;    // this is where we will store the IInstanceController to make it available to the rest of the class
		var ggrdContentGrid : Grid = null;          // this is the Grid that we get passed by the Init()
		var gexaApplication = null;                 // Excel.Application object        

		var gwbWorkbook = null;                     // here we will store the Workbook object
		
		var giStartRow : int = 7;                   // the starting row in the Spreadsheet
		var giMaxRow : int = 10000;                 // the end row in the Spreadsheet
		var giCurrentRowBUS101_B1 : int = 7;        // the row we will start from

		var gstrCompany : String = null;
		var gstrDivision : String = null;
		var gstrBudgetNumber : String = null;
		var gstrBudgetVersion : String = null;

		//var gstrUsername : String = null;           // the username that we will use to log in to the WebServices
		//var gstrPassword : String = null;           // the password that we will use to log in to the WebServices
		
		//var gstrBaseURI : String = null;			// this is the URI to the webservice

		var btnImportBudget : Button = null;

		var wndPasswordPrompt : Window = null;      // this is the window we will display requesting the username and password

		var gdebug;

		public function Init(element: Object, args: Object, controller : Object, debug : Object)
		{
			gdebug = debug;
			var content : Object = controller.RenderEngine.Content;

			// add a button to retrieve the free caps of an item
			btnImportBudget = new Button();
			// the button will display "FreeCap"
			btnImportBudget.Content = "Import";

			// set the position of the button
			Grid.SetColumnSpan(btnImportBudget, 10);
			Grid.SetColumn(btnImportBudget, 11);
			Grid.SetRow(btnImportBudget, 0);

			// actually add the button to the panel
			content.Children.Add(btnImportBudget);

			// we want to know about the click and unloaded events, so we register our interest here
			btnImportBudget.add_Click(OnbtnImportBudgetClick);
			btnImportBudget.add_Unloaded(OnbtnImportBudgetClickUnloaded);
		}


		public function OnbtnImportBudgetClickUnloaded(sender: Object, e: RoutedEventArgs)
		{
			// remove the events that we are subscribed to
			btnImportBudget.remove_Click(OnbtnImportBudgetClick);
			btnImportBudget.remove_Unloaded(OnbtnImportBudgetClickUnloaded);
		}

		// this will submit the header to the webservice
		// we statically set the Company & Division in our case
		private function handleHeader()
		{
			// "WETX40" - Description
			// "WETX15" - Name
			// "WEBSPR" - Start Period
			// "WECRTP" - Exchange Rate
			// "WENPAM" - Number of Periods
			// "WEUPDB" - Update Balance File (Checkbox)
			//gstrUsername = retrieveFromActiveSheet("B1");           // retrieve the username from the spreadsheet
			//gstrPassword = retrieveFromActiveSheet("B2");           // retrieve the password from the spreadsheet

			var w1buno : String = retrieveFromActiveSheet(gwbWorkbook, "B3");    // budget number
			var w1bver : String = retrieveFromActiveSheet(gwbWorkbook, "D3");    // version number

			gstrCompany = "100";                                    // statically set our company for us
			// gstrDivision = "IFL";                                   // statically set out division
			gstrDivision = retrieveFromActiveSheet(gwbWorkbook, "F3");    // Division
			gstrBudgetNumber = w1buno;
			gstrBudgetVersion = w1bver;


			var wetx40 : String = retrieveFromActiveSheet(gwbWorkbook, "B4");    // description
			var wetx15 : String = retrieveFromActiveSheet(gwbWorkbook, "B4");    // name
			var webspr : String = retrieveFromActiveSheet(gwbWorkbook, "D4");    // start period
			// var wecrtp : String = "1"; //retrieveFromActiveSheet("B3");
			var wecrtp : String = retrieveFromActiveSheet(gwbWorkbook, "K4");    // exchange rate tp
			var wenpam : String = retrieveFromActiveSheet(gwbWorkbook, "G4");    // number of periods
			// var weupdb : String = retrieveFromActiveSheet("B3");
			var weupdb : String = retrieveFromActiveSheet(gwbWorkbook, "I4");    // should we update the budget
			var strUpdate : String = "1";

			// check to ensure a value for the update budget
			if(true == doWeHaveAValueFromSpreadsheet(weupdb))
			{
				if(0 == String.Compare(weupdb, "true", true))
				{
					strUpdate = "1";
				}
				else if(0 == String.Compare(weupdb, "yes", true))
				{
					strUpdate = "0";
				}
			}
			
			if(wetx15.length > 15)
			{
				ConfirmDialog.ShowWarningDialog("Warning", "Budget name is > 15 characters - it will be truncated");
				wetx15 = wetx15.substring(0,15);
			}
			
			
			var mirRequest = new MIRequest();
			// we only want to filter on the company
			var mirInRecord = new MIRecord();
			mirInRecord["CONO"] = gstrCompany;
			mirInRecord["DIVI"] = gstrDivision;
			mirInRecord["BUNO"] = gstrBudgetNumber;	// budget number
			mirInRecord["BVER"] = gstrBudgetVersion;	// budget version
			mirInRecord["TX40"] = wetx40;	// Description
			mirInRecord["TX15"] = wetx15;	// Name
			mirInRecord["BSPR"] = webspr;	// start period budget
			mirInRecord["CRTP"] = wecrtp;	// exchange rate type
			mirInRecord["NPAM"] = wenpam;	// number periods
			//mirInRecord["ROPP"] = UserContext.CurrentCompany;	// rounding off category
			mirInRecord["UPDB"] = strUpdate;	// Update Balance File
			//mirInRecord["DTMP"] = UserContext.CurrentCompany;	// Allocaton Template
			//mirInRecord["ACGR"] = UserContext.CurrentCompany;	// Object Access Group

			mirRequest.Program = "BUS100MI";
			mirRequest.Record = mirInRecord;
			mirRequest.Transaction = "AddBudgetHeader";
			mirRequest.Tag = gwbWorkbook;
			
			MIWorker.Run(mirRequest, addBudgetHeader_OnComplete);
			
			// handleLines();
		}

		
		private function addBudgetHeader_OnComplete(amirResponse : MIResponse)
		{
			gdebug.Debug("addBudgetHeader_OnComplete start");
			if((null != amirResponse) && (false == amirResponse.HasError))
			{
				handleLines(amirResponse.Tag);
			}
			else
			{
				if(null == amirResponse)
				{	
					ConfirmDialog.ShowErrorDialog("Error", "No response from the APIs");
				}
				else
				{
					ConfirmDialog.ShowErrorDialog("Error", "Error returned from the APIs: " + amirResponse.ErrorCode + " " + amirResponse.ErrorMessage);
				}
				CleanUp();
			}
			gdebug.Debug("addBudgetHeader_OnComplete end");
		}
		
		// this function will go through the spreadsheet looking for values to submit
		// to the SOAP interface
		private function handleLines(workbook)
		{
			gdebug.Debug("handleLines start");
			
			var argumentArray = new Array();
			var iArrayPosition = 0;
			gdebug.Debug("handleLines MIMultWorker created");
			
			while(giCurrentRowBUS101_B1 <= giMaxRow)    // we will loop through until we hit MaxRows
			{
				gdebug.Debug("Dimension 1, " + "A" + giCurrentRowBUS101_B1);
				var w1ait1 : String = retrieveFromActiveSheet(workbook, "A" + giCurrentRowBUS101_B1);     // retrieve the first dimension from the spreadsheet
				gdebug.Debug("Dimension 1, " + "A" + giCurrentRowBUS101_B1 + " = '" + w1ait1 + "'");

				if(true == doWeHaveAValueFromSpreadsheet(w1ait1))   // if the dimension doesn't exist then there isn't any point retrieving any other dimensions
				{
					if(0 == String.Compare("End", w1ait1, true))    // if we come across End we should break from our line loop
					{
						break;
					}

					var w1ait2 : String = retrieveFromActiveSheet(workbook, "B" + giCurrentRowBUS101_B1);     // dimension 1
					var w1ait3 : String = retrieveFromActiveSheet(workbook, "C" + giCurrentRowBUS101_B1);     // dimension 2
					var w1ait4 : String = retrieveFromActiveSheet(workbook, "D" + giCurrentRowBUS101_B1);     // dimension 4
					var w1ait5 : String = retrieveFromActiveSheet(workbook, "E" + giCurrentRowBUS101_B1);     // dimension 5
					var w1ait6 : String = retrieveFromActiveSheet(workbook, "F" + giCurrentRowBUS101_B1);     // dimension 6
					var w1ait7 : String = retrieveFromActiveSheet(workbook, "G" + giCurrentRowBUS101_B1);     // dimension 7
					var w1cucd : String = retrieveFromActiveSheet(workbook, "H" + giCurrentRowBUS101_B1);     // 
					var w1amtn : String = retrieveFromActiveSheet(workbook, "I" + giCurrentRowBUS101_B1);     // 

					var strPeriod1 : String = retrieveFromActiveSheet(workbook, "J" + giCurrentRowBUS101_B1);     // value for period 1
					var strPeriod2 : String = retrieveFromActiveSheet(workbook, "K" + giCurrentRowBUS101_B1);     // value for period 2
					var strPeriod3 : String = retrieveFromActiveSheet(workbook, "L" + giCurrentRowBUS101_B1);     // value for period 3
					var strPeriod4 : String = retrieveFromActiveSheet(workbook, "M" + giCurrentRowBUS101_B1);     // value for period 4
					var strPeriod5 : String = retrieveFromActiveSheet(workbook, "N" + giCurrentRowBUS101_B1);     // value for period 5
					var strPeriod6 : String = retrieveFromActiveSheet(workbook, "O" + giCurrentRowBUS101_B1);     // value for period 6
					var strPeriod7 : String = retrieveFromActiveSheet(workbook, "P" + giCurrentRowBUS101_B1);     // value for period 7
					var strPeriod8 : String = retrieveFromActiveSheet(workbook, "Q" + giCurrentRowBUS101_B1);     // value for period 8
					var strPeriod9 : String = retrieveFromActiveSheet(workbook, "R" + giCurrentRowBUS101_B1);     // value for period 9
					var strPeriod10 : String = retrieveFromActiveSheet(workbook, "S" + giCurrentRowBUS101_B1);    // value for period 10
					var strPeriod11 : String = retrieveFromActiveSheet(workbook, "T" + giCurrentRowBUS101_B1);    // value for period 11
					var strPeriod12 : String = retrieveFromActiveSheet(workbook, "U" + giCurrentRowBUS101_B1);    // value for period 12

					
					var mirRequest : MIRequest = new MIRequest();
					var mirInRecord : MIRecord = new MIRecord();
					mirInRecord["CONO"] = gstrCompany;
					mirInRecord["DIVI"] = gstrDivision;
					mirInRecord["BUNO"] = gstrBudgetNumber;
					mirInRecord["BVER"] = gstrBudgetVersion;
					mirInRecord["AIT1"] = w1ait1;
					mirInRecord["AIT2"] = w1ait2;
					mirInRecord["AIT3"] = w1ait3;
					mirInRecord["AIT4"] = w1ait4;
					mirInRecord["AIT5"] = w1ait5;
					mirInRecord["AIT6"] = w1ait6;
					mirInRecord["AIT7"] = w1ait7;
					mirInRecord["CUCD"] = w1cucd;
					mirInRecord["BCU1"] = convertTo2DecimalPlaces(strPeriod1);
					mirInRecord["BCU2"] = convertTo2DecimalPlaces(strPeriod2);
					mirInRecord["BCU3"] = convertTo2DecimalPlaces(strPeriod3);
					mirInRecord["BCU4"] = convertTo2DecimalPlaces(strPeriod4);
					mirInRecord["BCU5"] = convertTo2DecimalPlaces(strPeriod5);
					mirInRecord["BCU6"] = convertTo2DecimalPlaces(strPeriod6);
					mirInRecord["BCU7"] = convertTo2DecimalPlaces(strPeriod7);
					mirInRecord["BCU8"] = convertTo2DecimalPlaces(strPeriod8);
					mirInRecord["BCU9"] = convertTo2DecimalPlaces(strPeriod9);
					mirInRecord["BC10"] = convertTo2DecimalPlaces(strPeriod10);
					mirInRecord["BC11"] = convertTo2DecimalPlaces(strPeriod11);
					mirInRecord["BC12"] = convertTo2DecimalPlaces(strPeriod12);

					mirRequest.Program = "BUS100MI";
					mirRequest.Record = mirInRecord;
					mirRequest.Transaction = "AddBudgetLines";
					// mirRequest.Tag = workbook;
					argumentArray[iArrayPosition] = mirRequest;
					iArrayPosition++;
				}

				giCurrentRowBUS101_B1++;        // increment our spreadsheet position
			}
			
			gdebug.Debug("iArrayPosition = " + iArrayPosition);
			gdebug.Debug("giCurrentRowBUS101_B1 = " + giCurrentRowBUS101_B1);
			
			if(argumentArray.length > 0)
			{
				gdebug.Debug("Argument Array length = " + argumentArray.length);
				
				var mirRequestArray = new MIRequest[argumentArray.length];
				for(var i = 0; i < argumentArray.length; i++)
				{
					mirRequestArray[i] = argumentArray[i];
				}
				addBudgetLines(mirRequestArray);
				//mwWorker.RunWorkerAsync(req, addBudgetLines_OnComplete);
			}
			
			gdebug.Debug("handleLines end");
			
		}

		private function addBudgetLines(req)
		{
			if(null != req)
			{
				gdebug.Debug("req count: " + req.length);
				var mwWorker = new MIMultiWorker();
				mwWorker.RunWorkerAsync(req, addBudgetLines_OnComplete);
			}
		}
		
		private function addBudgetLines_OnComplete(amirResponse : MIMultiResult)
		{
			try
			{
				if(null != amirResponse && amirResponse.HasError == false)
				{
					gdebug.Debug("No errors");
					ConfirmDialog.ShowInformationDialogNeverHidden("Complete", "Completed without any errors");
				}
				else
				{
					gdebug.Debug("Errors!");
					if(null != amirResponse.ResponseList && amirResponse.ResponseList.Count > 0)
					{
						var strErrors : String = null;
						for(var i = 0; i < amirResponse.ResponseList.Count; i++)
						{
							var reponse : MIResponse = amirResponse.ResponseList[i];
							if(true == reponse.HasError)
							{
								if(false == String.IsNullOrEmpty(strErrors))
								{
									strErrors += Environment.NewLine;
								}
								strErrors += "Response[" + i + "]: " + reponse.Error;
							}
						}
						if(false == String.IsNullOrEmpty(strErrors))
						{
							ConfirmDialog.ShowErrorDialog("Error", "The following errors were encountered" + Environment.NewLine + strErrors);
						}
						else
						{
							ConfirmDialog.ShowErrorDialog("Error", "Undetermined errors occured creating the budget lines, please check the Smart Office logs");
						}
					}
					else
					{
						ConfirmDialog.ShowErrorDialog("Error", "Undetermined errors occured creating the budget lines");
					}
				}
			
				try
				{
					gwbWorkbook.Save();                     // save the spreadsheet
				}
				catch(ext)
				{
				}
				
				CleanUp();

			}
			catch(ex)
			{
				ConfirmDialog.ShowErrorDialog("Exception", "An Exception occurred, please check the import" + Environment.NewLine + ex);
			}
		}
		
		public function OnbtnImportBudgetClick(sender: Object, e: RoutedEventArgs)
		{
			try
			{
				// here we do some initialisation of Excel
				InitialiseExcel();

				var strFilename : String = retrieveImportFile();            // retrieve the filename of the Excel spreadsheet to open
				if((null != strFilename) && (null != gexaApplication))      // ensure that not only do we have a filename, but we also managed to initialise Excel
				{
					gwbWorkbook = gexaApplication.Workbooks.Open(strFilename);  // open the spreadsheet
					if(null != gwbWorkbook)
					{
						gwbWorkbook.Saved = true;               // get rid of those annoying save messages

						giMaxRow = gwbWorkbook.ActiveSheet.Cells(gwbWorkbook.ActiveSheet.Rows.Count,1).End(-4162).Row;      // get the end row
						
						handleHeader();                         // kick off the creation of the header, this in turn will create the lines
						//gwbWorkbook.Save();                     // save the spreadsheet

					}
					else ConfirmDialog.ShowErrorDialog("Error", "Failed to Open Workbook '" + strFilename + "'");
				}
				else ConfirmDialog.ShowErrorDialog("Error", "Filename or Excel doesn't exist: " + strFilename);
			}
			catch(exException)
			{
				ConfirmDialog.ShowErrorDialog("Error", "Error: " + exException.description);
			}
			
		}




		// retrieve some data from the active spreadsheet
		// at a specific location
		private function retrieveFromActiveSheet(awbWorkBook, astrPosition)
		{
			//gdebug.Debug("retrieveFromActiveSheet() start: " + astrPosition);
			var result = "";
			
			if(null != awbWorkBook)
			{
				if(null != awbWorkBook.ActiveSheet)
				{
					result = awbWorkBook.ActiveSheet.Range(astrPosition).Value;
					if(true == String.IsNullOrEmpty(result))
					{
						result = "";
					}
					else if(0 == String.Compare(result, "undefined"))
					{
						result = "";
					}
				}
				else
				{
					gdebug.Error("No active worksheet");
				}
			}
			else
			{
				gdebug.Error("Workbook is null");
			}
			

			return(result);
		}

		// display an OpenFileDialog box
		// and extract the result
		private function retrieveImportFile()
		{
			var result : String = null;
			var ofdFile = new System.Windows.Forms.OpenFileDialog();    // we have to use the forms OpenFileDialog unfortunately
			if(null != ofdFile)
			{
				ofdFile.Multiselect = false;
				ofdFile.Filter = "Excel Files (*.xls;*.xlsx)|*.xls;*.xlsx|All Files (*.*)|*.*"; // filter on xls or xlsx files only
				
				if(true == ofdFile.ShowDialog())
				{
					result = ofdFile.FileName;
				}
			}
			return(result);
		}

		// our central cleanup function
		private function CleanUp()
		{
			CleanUpExcel();
		}

		// our Import button is being unloaded, now's a good time to clean
		// everything up
		private function OnImportFromExcelUnloaded(sender : Object, e : RoutedEventArgs)
		{
			if(null != btnImportBudget)
			{
				btnImportBudget.remove_Click(OnbtnImportBudgetClick);
				btnImportBudget.remove_Unloaded(OnbtnImportBudgetClickUnloaded);
			}
		}

		private function convertTo2DecimalPlaces(astrValue : String)
		{
			var strResult : String = "";

			if(false == String.IsNullOrEmpty(astrValue))
			{
				var dblTemp : double = astrValue;
				strResult = dblTemp.ToString("#.##");
			}

			return(strResult);
		}

		// check to ensure that we have a value when we extract from the 
		// spreadsheet
		private function doWeHaveAValueFromSpreadsheet(astrValue : String)
		{
			var bResult : boolean = false;
			if(false == String.IsNullOrEmpty(astrValue))
			{
				if(0 != String.Compare(astrValue, "undefined"))
				{
					bResult = true;
				}
			}
			return(bResult);
		}

		// close Excel and any workbooks
		private function CleanUpExcel()
		{
				// check to ensure we have a Workbook object
				// before we attempt to close the workbook
				if(null != gwbWorkbook)
				{
					gwbWorkbook.Close();
					gwbWorkbook = null;
				}
				// make sure we have actually created
				// the Excel Application object before
				// we Quit
				if(null != gexaApplication)
				{
					gexaApplication.Quit();
					gexaApplication = null;
				}
		}

		// Initialise Excel, essentially start the Excel Application and set it to visible
		private function InitialiseExcel()
		{
			var result = null;
			try
			{
				gexaApplication = new ActiveXObject("Excel.Application");
				gexaApplication.Visible = true;
				
				result = gexaApplication;
			}
			catch(exException)
			{
				ConfirmDialog.ShowErrorDialog("Error", "Error: " + exException.Message + Environment.NewLine + exException.StackTrace);
			}
			return(result);
		}

		
		
	}
}

 

Enjoy! 🙂

Posted in Development, M3 / MoveX, Misc | Leave a comment

TXS150 – the Dread Tax Panel

I like Canada. I like Canadians, but well with the change in behaviour in M3 13.2 (aka 15.1.2) to accommodate tax changes has made staff grumpy, which has made me grumpy, which means I’m not happy with Canada J

In APS100, Supplier Invoice Entry we used to have a fairly clean data entry process.

  1. You select your function
  2. Key in a supplier number, invoice number and invoice amount
  3. Press enter
  4. You go to APS100/F and you could adjust the tax (typically the default is fine)
  5. Press enter
  6. Then you are taken to GLS120/J1 where you could do further breakdowns / adjustments

Under 13.2

  1. You select your function
  2. Key in a supplier number, invoice number and invoice amount
  3. Press enter
  4. You go to APS100/F then

Figure 1 – we used to have the tax adjustment fields on this panel

  1. Press enter
  2. TXS150 pops up and you can adjust the tax if needs be.
  3. Press F3 to close
  4. Then you are taken to GLS120/J1 where you could do further breakdowns / adjustments

From a data entry perspective, this is just horrible – it destroys any flow you have because you are now consciously moving your focus to an unused set of keys, or as with the case of many people, they end up grabbing the mouse.

Now, the key thing here is to remember that we can still change our tax values in the GLS120/J1 panel, so for IFL we can safely close TXS150 with a script in most instances. For those instances we do need to change the tax, our staff will change it in the subsequent panel.

Figure 2 – 9900 is out GST tax code, we can change this manually if needs be

 

Now I just went, hey, that’s easy. I’ll just use the PressKey method and issue the MNEProtocol.KeyF03 to close the panel from the Init() method.

So it would literally be adding one line of code to the Init() method

controller.PressKey(MNEProtocol.KeyF03);

So I throw that together, a grin of smug self-satisfaction on my face…

And it didn’t work…

Nothing happened…

Not a thing…

A quick check of the logs and my code was being executed, however I seemed to get a “Duplicate request skipped”

Which prompted a “huh?”

So it looks like we can’t call the normal F3 close from within the Init() function. Ok, so how about if we call it from a timer…and that’s exactly what I did and it works!

See the code below:

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

import System.Windows.Threading;

package MForms.JScript
{
	class TXS150_Close
	{
		// var gTimer : Timer;
		var gTimer : DispatcherTimer;
		var gController;
		var gDebug;
		
		public function Init(element: Object, args: Object, controller : Object, debug : Object)
		{
			debug.Debug("TXS150_Close::Init() Start");
			
			gController = controller;
			gDebug = debug;
			
			gTimer = new DispatcherTimer();
			gTimer.add_Tick(timer_Close);
			gTimer.Interval = new TimeSpan(1);
			gTimer.Start();
			
			debug.Debug("TXS150_Close::Init() End");
		}
		
		public function timer_Close(sender : Object, e : EventArgs)
		{
			gDebug.Debug("TXS150_Close::timer_Close() Start");
			try
			{
				gController.PressKey(MNEProtocol.KeyF03);
				gDebug.Debug("TXS150_Close::timer_Close() F3 simulated");
				
				gTimer.Stop();
			}
			catch(ex)
			{
				gDebug.Error(ex);
			}
			
			gDebug.Debug("TXS150_Close::timer_Close() End");
		}
	}
}

 

Enjoy!

Posted in Uncategorized | Leave a comment

Database and Column Field Information – Monitor

A couple of weeks ago I was at a customer site providing some training when we were talking about getting field information (I usually do the ctrl+shift right click -> Debug -> Show Response XML)

Iit was pointed out to me that in Smart Office there is a Monitor menu item. So if you go in to a non- A or B panel (eg. MMS001/E) and right click on a field and select Monitor -> Show All

The fields will change colour and now we can hover over any of our non-calculated fields and…

…we can see the field name and the table that the data came from.

Pretty kewl isn’t it!

Thanks Eric for pointing this out to me.

Posted in M3 / MoveX | 2 Comments

M3-API-WS under M3 15.2

Though you don’t see me talking much about the M3 REST services (mainly because the meta-data was broken when I first went to play with them), they are there and they are very handy and I actually used them with a 3PL application I developed for my old employer.

I have a customer embarking upon an Upgrade-X project, and in the process of validating I noticed that the M3-API-WS was missing

It turns out that it has moved in to the BE

But that’s not the kewl thing. If we go to its Grid Management Pages -> API Repository we get descriptions!

And if we click on a MI program it gets even better

And still, more details :-O

Credit to the developers for adding this information!

I don’t know how many hours in frustration I’ve spent dealing with the APIs.

Posted in M3 / MoveX | 3 Comments

Log Viewer

Earlier this month I was in Stockholm, Sweden and had the privilege of meeting Fredrik, Karin and Peter from Infor – it was great to meet face to face with some of the contributors to the Smart Office Blog and hear a bit about some of the challenges they face in giving us the tools to streamline our business processes. We had a really good discussion about Smart Office and I thought I’d share a few of the things that they told me about.

Log Viewer Widget

In later versions of Smart Office there is a Log Viewer widget – this handy little widget provides real-time updates of what is written to the log – you can also set a filter on it so you only see events that you want to see in real-time. It’s invaluable to the development process.

Debug and writing to the Smart Office logs

I, like many others muck around getting the log4net handle so we can log messages to the Smart Office logs. As it turns out the debug object in the Init() method was extended so we could use it to log to the Smart Office logs.

Here we can see the Script Tool – the Log Viewer and the Log Viewer Widget.

And these are the options:

So I often change the Origin to the name of the script I am working with do filter the events.

Enjoy!

Posted in Development, M3 / MoveX | Leave a comment

Getting Those Assemblies to develop against multiple versions of Smart Office

Sometimes you’re developing against multiple versions of Smart Office and on the odd occasion you get errors if you are using different assemblies to those that Smart Office uses. I have had this on a couple of occasions and most recently where Smart Office 10.2.x was deployed but my project had been running 10.1.x assemblies and it threw an odd error.

You have two options – grab the SDK if you have access to it for that version (which creates other problems) or

  1. launch a normal Smart Office (not the developer instance) you are working against
  2. open Task Manager (right click on an empty section of the StartBar -> Task Manager).
  3. Right click on the Smart Office instance and select “Open file location”
  4. And now we have assembly goodness
  5. Copy the .dll assemblies to a convenient location (eg. <drive>:\LSO10.2.0.0.32\Bin)
  6. In Visual Studio, Right Click on your project and select Properties
  7. Select Reference Paths
  8. And enter the folder you copied the assemblies in to (in the screenshot above, this is actually the SDK assemblies I copied – but this works if you are developing a C# compiled assembly aswell)

Doing this will also help get around some style errors.

Posted in Development, M3 / MoveX, Misc | Leave a comment

SDK – Cannot find property DisableChannelCreate

Last night I was working on updating a project I had created as a feature within the SDK and came across an interesting scenario where I launched my old project and it didn’t run out of the box. Smart Office would load, prompt me for a password and then display this error message.

The environment I was running against was the new 10.2.x, and the project had been developed against 10.1.1.x IIRC.

I ended up grabbing the new Smart Office SDK for 10.2.x and pointed my projects references to the extracted directory (Projects properties -> Reference Paths)

And then recompiled and I peace in my world was once again a reality :-). Usually I don’t have issues with older projects against newer versions of Smart Office.

(I use the Reference Paths nowadays so I can develop against multiple versions of Smart Office without too many hassles).

Posted in Misc, SDK | Leave a comment

The case of the JScripts that wouldn’t update

Recently I have been doing some work for another company, adding a couple of JScripts to streamline a process and allow for better reporting. Fundamentally the scripts are pretty basic, add a lookup box, bring up a selection of options and then a little bit of magic is invoked to add a record to a table/update some records. They are using an older version of Smart Office (9.x) which did create a few headaches as some of the staples that I use in my scripts weren’t available (no DataGrid 😦 ) .

So once I was happy the script was working, it was deployed to the Jscript directory on the server. I’d get some additional testing done and discover some little quirks and address them. Then the script would be copied back to the server.

And this is where the oddities started. The script was copied to the server, and yet staff would report that when testing they were still having the old issues. I verified I had copied the new script (timestamps) but the old script which didn’t exist would still come down to the client (even after a clearcache). I spent time verifying that I wasn’t using an incorrect directory but no.

So, when looking through the Smart Office logs I noticed that the logs themselves provide a helpful URI which shows where Smart Office was downloading the script from (this is a screenshot a 10.x environment not 9.x)

So, I plugged the address in to a web-browser

And I can see the script – however it was an old one. Infact, in one of the instances a new script had been in place for over a week and yet the ‘previous’ version was shown when I used this method to investigate.

In my scripts I will generally try to put a change log, so it was pretty easy to see if the new version had come down.

I then discovered if I used Windows Explorer and went to the server directory where the script was and then right clicked on it and selected Open or Open With and opened in it a program like Notepad, if I did a refresh on my browser the new script would come down.

A quick clearcache on the client, and the new script would come down and the problems would disappear.

I’ve not come across this issue before, and in may be related to some specific version of the M3 UI Adapter in use but it had me very puzzled for a while – hopefully this will save someone else the frustration!

 

Posted in Misc | Leave a comment

2014 Jaunt – Sweden/Iceland

It’s been a wee while since my last post and there are several posts of code, hints and tips that I am prepping to post over the next couple of months but before I do that…

To mark the end of my current job, I’m planning on visiting Sweden and Iceland in July.

I’m going to be in Stockholm 30th June, 1st and 9th of July – I’ll be meandering around Iceland 2nd through to 9th (are there any Icelandic M3 customers?).  If anyone was interested in catching up for a chat over a beer and meal, please feel free to drop me a line.

I’m tied up from the afternoon of the 1st of July, but pretty flexible on the other days.

I can be emailed at my gmail.com account potatoit.

Cheers,
Scott

Posted in Uncategorized | Leave a comment

JScript Oddities – Returning Typed Arrays

As implied in my last post, I’ve been involved in developing some JScripts recently. Infact, nearly half a dozen scripts that are fairly large. In these scripts I created classes to handle data, and in some instances these scripts would return an array of these objects.

Infact, one of these scripts returned an array of these through three different methods like so

private function returnAnArray() : PotatoItTestClass[]

– this created an issue if I returned null. I’m sure that if you’ve read the JScript specification from start to end you’d already know this but I didn’t 🙂

Consider this piece of code. I have a called call PotatoITTestClass, I have a function which returns an array of these objects.

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

package MForms.JScript
{
   class ExceptionTest
   {
      public function Init(element: Object, args: Object, controller : Object, debug : Object)
      {
         try
         {
         	var myResults : PotatoItTestClass[] = returnAnArray();
         	if((null != myResults) && (myResults.length >= 1))
         	{
         		debug.WriteLine("Array count: " + myResults.length);
         	}
         }
         catch(ex)
         {
         		debug.WriteLine(ex);
         }

      }

      private function returnAnArray() : PotatoItTestClass[]
      {
      	var result = new PotatoItTestClass[2];
      	result[0] = new PotatoItTestClass();
      	result[1] = new PotatoItTestClass();

      	return(result);
      }
   }

   class PotatoItTestClass
   {
   	var gstrMessage : String = "This is a message";
   }
}

This is all well and good and works exactly as we expect when we return a non-null value.

If we make this little change so returnAnArray() returns a null as follows

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

package MForms.JScript
{
   class ExceptionTest
   {
      public function Init(element: Object, args: Object, controller : Object, debug : Object)
      {
         try
         {
         	var myResults : PotatoItTestClass[] = returnAnArray();
         	if((null != myResults) && (myResults.length >= 1))
         	{
         		debug.WriteLine("Array count: " + myResults.length);
         	}
         }
         catch(ex)
         {
         		debug.WriteLine(ex);
         }

      }

      private function returnAnArray() : PotatoItTestClass[]
      {
      	var result = new PotatoItTestClass[2];
      	result[0] = new PotatoItTestClass();
      	result[1] = new PotatoItTestClass();

      	// return null
      	result = null;

      	return(result);
      }
   }

   class PotatoItTestClass
   {
   	var gstrMessage : String = "This is a message";
   }
}

Then we end up getting an error “Error: Unable to cast object of type ‘System.DBNull’ to type ‘MForms.JScript.PotatoItTestClass[]’.” – not very kewl at all. I’ve done this sort of thing heaps of times in C# even done it in VB.Net without issue but in JScript…well…

Thankfully the solution to the issue is pretty straight forward, we don’t specify the type that holds the results or the type of the method

var myResults : PotatoItTestClass[] = returnAnArray();

becomes

var myResults = returnAnArray();

and

private function returnAnArray() : PotatoItTestClass[]

becomes

private function returnAnArray()

The complete code looks like this.

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

package MForms.JScript
{
   class ExceptionTest
   {
      public function Init(element: Object, args: Object, controller : Object, debug : Object)
      {
         try
         {
         	var myResults = returnAnArray();
         	if((null != myResults) && (myResults.length >= 1))
         	{
         		debug.WriteLine("Array count: " + myResults.length);
         	}
         }
         catch(ex)
         {
         		debug.WriteLine(ex);
         }

      }

      private function returnAnArray()
      {
      	var result = new PotatoItTestClass[2];
      	result[0] = new PotatoItTestClass();
      	result[1] = new PotatoItTestClass();

      	result = null;

      	return(result);
      }
   }

   class PotatoItTestClass
   {
   	var gstrMessage : String = "This is a message";
   }
}

Posted in Development, M3 / MoveX, Misc | Leave a comment