Updates to my Object Renumbering Tool

Back in the end of 2014 I published a renumbering tool for NAV objects.  Using DotNet I was able to increase the renumbering speed for text object files dramatically.

Since then I have been asked if I could upgrade it to work with IDs and Field Numbers.

Now I have.

What’s more, it is also on GitHub.

The Process functions are the following;

  • Read Object Lines – Creates renumbering lines base on the objects in the selected object file.
  • Suggest IDs – Suggest new object numbers in the range from 50.000 based on the available objects in the current license.
  • Read from Excel – Reads object renumbering lines from Excel Sheet created with the Write to Excel process.
  • Write to Excel – Writes current renumbering lines to a new Excel Sheet to me managed within Excel and reread into the renumbering lines.
  • Renumber Using Lines – Prompts for a file to read and for a new file to save with renumbered objects based on the rules in the renumbering lines.
  • Renumber Using Controls – Prompts for a file to read and for a new file to save with renumbered objects based on the rules in the control IDs setup.

I have done some fixes to the renumbering function and have added support for the EventSubscriber.

Go to GitHub to download Page and Table 50000, try this out and submit improvements.

When I am processing an object file I have it open in my text editor.  When I see something to renumber I update the control ranges and execute the renumbering process, reading and writing to the same object file.  My editor will reload the file and I can see the results immediately.

 

Inspired by a Microsoft Developer

Yes it happens.

When reading the C/AL code in NAV written by other developers you normally pick up smart way to do things.  Yesterday I got one.

In my solutions I have been using a function to check if a string is numeric and another function to extract the numeric value from a string.

IsNumeric

All good and well.  Working fine so far.  But if you can write each function with a single line, would that not be better ?

NewIsNumeric

Thanks for the inspiration Microsoft.

Potential hole in handling default dimension

Since NAV 2013 we have been using Dimension Set instead of the Dimension Entry tables.  One of the function that was changed was GetDefaultDimID in Codeunit 408.

In NAV 2009 and older this would return a set of dimension for any of the dimension entry tables.  In NAV 2013 and newer this will return the Dimension Set Id and updated Global Dimension Codes.

Here; “updated” is the key word.  One could think that when a function is returning these codes you should be able to trust that it does.  If we look at the CreateDim function in the Sales Line table the code is

[code]
SourceCodeSetup.GET;
TableID[1] := Type1;
No[1] := No1;
TableID[2] := Type2;
No[2] := No2;
TableID[3] := Type3;
No[3] := No3;
"Shortcut Dimension 1 Code" := ”;
"Shortcut Dimension 2 Code" := ”;
GetSalesHeader;
"Dimension Set ID" :=
DimMgt.GetDefaultDimID(
TableID,No,SourceCodeSetup.Sales,
"Shortcut Dimension 1 Code","Shortcut Dimension 2 Code",
SalesHeader."Dimension Set ID",DATABASE::Customer);
DimMgt.UpdateGlobalDimFromDimSetID("Dimension Set ID","Shortcut Dimension 1 Code","Shortcut Dimension 2 Code");[/code]

But a better version might be

[code]
SourceCodeSetup.GET;
TableID[1] := Type1;
No[1] := No1;
TableID[2] := Type2;
No[2] := No2;
TableID[3] := Type3;
No[3] := No3;
GetSalesHeader;
"Shortcut Dimension 1 Code" := SalesHeader."Shortcut Dimension 1 Code";
"Shortcut Dimension 2 Code" := SalesHeader."Shortcut Dimension 2 Code";
"Dimension Set ID" :=
DimMgt.GetDefaultDimID(
TableID,No,SourceCodeSetup.Sales,
"Shortcut Dimension 1 Code","Shortcut Dimension 2 Code",
SalesHeader."Dimension Set ID",DATABASE::Customer);[/code]

The reason is that the function DimMgt.GetDefaultDimID will update the “Shortcut Dimension 1 Code” and “Shortcut Dimension 2 Code” only if they are changed. If the Dimension Set used in the Sales Header contains dimension values for “Shortcut Dimension 1 Code” or “Shortcut Dimension 2 Code” we need to add the DimMgt.UpdateGlobalDimFromDimSetID function to make sure that all the correct data is in place.

The lesson is; if using InheritFromDimSetID in the function DimMgt.GetDefaultDimID then make sure it is followed by calling DimMgt.UpdateGlobalDimFromDimSetID.

I have however suggested to Microsoft that they make a change to the DimMgt.GetDefaultDimID function to close this hole. The original version is

[code]
GetGLSetup;
IF InheritFromDimSetID > 0 THEN
GetDimensionSet(TempDimSetEntry0,InheritFromDimSetID);
TempDimBuf2.RESET;
TempDimBuf2.DELETEALL;
IF TempDimSetEntry0.FINDSET THEN
REPEAT
TempDimBuf2.INIT;
TempDimBuf2."Table ID" := InheritFromTableNo;
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Code" := TempDimSetEntry0."Dimension Code";
TempDimBuf2."Dimension Value Code" := TempDimSetEntry0."Dimension Value Code";
TempDimBuf2.INSERT;
UNTIL TempDimSetEntry0.NEXT = 0;

NoFilter[2] := ”;
FOR i := 1 TO ARRAYLEN(TableID) DO BEGIN
IF (TableID[i] <> 0) AND (No[i] <> ”) THEN BEGIN
DefaultDim.SETRANGE("Table ID",TableID[i]);
NoFilter[1] := No[i];
FOR j := 1 TO 2 DO BEGIN
DefaultDim.SETRANGE("No.",NoFilter[j]);
IF DefaultDim.FINDSET THEN
REPEAT
IF DefaultDim."Dimension Value Code" <> ” THEN BEGIN
TempDimBuf2.SETRANGE("Dimension Code",DefaultDim."Dimension Code");
IF NOT TempDimBuf2.FINDFIRST THEN BEGIN
TempDimBuf2.INIT;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Code" := DefaultDim."Dimension Code";
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END ELSE BEGIN
IF DefaultDimPriority1.GET(SourceCode,DefaultDim."Table ID") THEN BEGIN
IF DefaultDimPriority2.GET(SourceCode,TempDimBuf2."Table ID") THEN BEGIN
IF DefaultDimPriority1.Priority < DefaultDimPriority2.Priority THEN BEGIN
TempDimBuf2.DELETE;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END;
END ELSE BEGIN
TempDimBuf2.DELETE;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END;
END;
END;
IF GLSetupShortcutDimCode[1] = TempDimBuf2."Dimension Code" THEN
GlobalDim1Code := TempDimBuf2."Dimension Value Code";
IF GLSetupShortcutDimCode[2] = TempDimBuf2."Dimension Code" THEN
GlobalDim2Code := TempDimBuf2."Dimension Value Code";
END;
UNTIL DefaultDim.NEXT = 0;
END;
END;
END;
TempDimBuf2.RESET;
IF TempDimBuf2.FINDSET THEN BEGIN
REPEAT
DimVal.GET(TempDimBuf2."Dimension Code",TempDimBuf2."Dimension Value Code");
TempDimSetEntry."Dimension Code" := TempDimBuf2."Dimension Code";
TempDimSetEntry."Dimension Value Code" := TempDimBuf2."Dimension Value Code";
TempDimSetEntry."Dimension Value ID" := DimVal."Dimension Value ID";
TempDimSetEntry.INSERT;
UNTIL TempDimBuf2.NEXT = 0;
NewDimSetID := GetDimensionSetID(TempDimSetEntry);
END;
EXIT(NewDimSetID);[/code]

and the modified would be

[code]
GetGLSetup;
IF InheritFromDimSetID > 0 THEN
GetDimensionSet(TempDimSetEntry0,InheritFromDimSetID);
TempDimBuf2.RESET;
TempDimBuf2.DELETEALL;
IF TempDimSetEntry0.FINDSET THEN
REPEAT
TempDimBuf2.INIT;
TempDimBuf2."Table ID" := InheritFromTableNo;
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Code" := TempDimSetEntry0."Dimension Code";
TempDimBuf2."Dimension Value Code" := TempDimSetEntry0."Dimension Value Code";
TempDimBuf2.INSERT;
UNTIL TempDimSetEntry0.NEXT = 0;

NoFilter[2] := ”;
FOR i := 1 TO ARRAYLEN(TableID) DO BEGIN
IF (TableID[i] <> 0) AND (No[i] <> ”) THEN BEGIN
DefaultDim.SETRANGE("Table ID",TableID[i]);
NoFilter[1] := No[i];
FOR j := 1 TO 2 DO BEGIN
DefaultDim.SETRANGE("No.",NoFilter[j]);
IF DefaultDim.FINDSET THEN
REPEAT
IF DefaultDim."Dimension Value Code" <> ” THEN BEGIN
TempDimBuf2.SETRANGE("Dimension Code",DefaultDim."Dimension Code");
IF NOT TempDimBuf2.FINDFIRST THEN BEGIN
TempDimBuf2.INIT;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Code" := DefaultDim."Dimension Code";
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END ELSE BEGIN
IF DefaultDimPriority1.GET(SourceCode,DefaultDim."Table ID") THEN BEGIN
IF DefaultDimPriority2.GET(SourceCode,TempDimBuf2."Table ID") THEN BEGIN
IF DefaultDimPriority1.Priority < DefaultDimPriority2.Priority THEN BEGIN
TempDimBuf2.DELETE;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END;
END ELSE BEGIN
TempDimBuf2.DELETE;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END;
END;
END;
END;
UNTIL DefaultDim.NEXT = 0;
END;
END;
END;
TempDimBuf2.RESET;
IF TempDimBuf2.FINDSET THEN BEGIN
REPEAT
IF GLSetupShortcutDimCode[1] = TempDimBuf2."Dimension Code" THEN
GlobalDim1Code := TempDimBuf2."Dimension Value Code";
IF GLSetupShortcutDimCode[2] = TempDimBuf2."Dimension Code" THEN
GlobalDim2Code := TempDimBuf2."Dimension Value Code";
DimVal.GET(TempDimBuf2."Dimension Code",TempDimBuf2."Dimension Value Code");
TempDimSetEntry."Dimension Code" := TempDimBuf2."Dimension Code";
TempDimSetEntry."Dimension Value Code" := TempDimBuf2."Dimension Value Code";
TempDimSetEntry."Dimension Value ID" := DimVal."Dimension Value ID";
TempDimSetEntry.INSERT;
UNTIL TempDimBuf2.NEXT = 0;
NewDimSetID := GetDimensionSetID(TempDimSetEntry);
END;
EXIT(NewDimSetID);[/code]

Where I have moved the lines that update GlobalDim1Code and GlobalDim2Code to the loop in the end of the function. By doing this the call to DimMgt.UpdateGlobalDimFromDimSetID is no longer needed and that extra loop through the Dimension Set Entries will save some time.

Run a Table in NAV

One of the things we developers and consultants miss the most from the “Classic Times” is the ability to run a table to edit the data. Sure we can run a table from the Developement Environment but we are not always working with direct access to the SQL database.

My solution is to have a Page running on the object table (Table2000000001). From the page I can start Pages, Reports, Codeunits and XML Ports directly with a simple line of code, but to start a Table is more complex.

The first solution was to use HYPERLINK on the result from the GETURL function. That works fine in a local environment, but when you have installed multiple Dynamics NAV versions or running Dynamics NAV from a ClickOnce installation things start to break.

To fix this I stop using HYPERLINK and start the Dynamics NAV client with arguments to run a table. First step is to find the current client path. This path can be the usual System Drive path but it can also be a User Application Path if using ClickOnce. The function to locate the client path uses DotNet.

[code] PROCEDURE GetClientPath@1100408003() : Text;
VAR
ClientAssembly@1100408001 : DotNet "’mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Reflection.Assembly" RUNONCLIENT;
ClientPath@1100408000 : DotNet "’mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.Path" RUNONCLIENT;
BEGIN
ClientAssembly := ClientAssembly.GetExecutingAssembly;
EXIT(ClientPath.GetDirectoryName(ClientAssembly.Location));
END;[/code]

And based on the client Path I look for the ClientConfiguration.config file and start the client with the GETURL results.

[code] LOCAL PROCEDURE ViewRecords@1100408000();
VAR
AddinMgt@1100408000 : Codeunit 10000207;
PathHelper@1100408008 : DotNet "’mscorlib’.System.IO.Path";
ClientFileHelper@1100408007 : DotNet "’mscorlib’.System.IO.File" RUNONCLIENT;
ClientProcess@1100408005 : DotNet "’System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Diagnostics.Process" RUNONCLIENT;
ClientProcessWindowStyle@1100408004 : DotNet "’System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Diagnostics.ProcessWindowStyle" RUNONCLIENT;
ClientProcessStartInfo@1100408003 : DotNet "’System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Diagnostics.ProcessStartInfo" RUNONCLIENT;
StartCommand@1100408002 : Text;
ArgumentList@1100408006 : Text;
ClientPath@1100408001 : Text;
ClientConfigurationPath@1100408009 : Text;
BEGIN
ClientPath := AddinMgt.GetClientPath;
ClientConfigurationPath := PathHelper.Combine(ClientPath,’ClientUserSettings.config’);
IF ClientFileHelper.Exists(ClientConfigurationPath) THEN
ArgumentList := STRSUBSTNO(‘-settings:"%1" ‘,ClientConfigurationPath);
ArgumentList += GETURL(CLIENTTYPE::Windows, COMPANYNAME, OBJECTTYPE::Table, ID);
ClientPath := PathHelper.Combine(ClientPath,’Microsoft.Dynamics.Nav.Client.exe’);
ClientProcessStartInfo := ClientProcessStartInfo.ProcessStartInfo(ClientPath);
ClientProcessStartInfo.Arguments := ArgumentList;
ClientProcessStartInfo.WindowStyle := ClientProcessWindowStyle.Normal;
ClientProcess := ClientProcess.Start(ClientProcessStartInfo);
END;[/code]

To repeat my last comment, just use DotNet 🙂

User friendly error messages in NAV

One of the most challenging thing when programming is to have proper error handling. This is one of the weakness of the standard NAV code. We are now seeing this change when Microsoft introduced the simplified NAV.

An example of this can be found in report 1306.

Rep1306

Here Microsoft have created a dedicated function for error handling.

CompInfoErrorHandling

This type of error handling will tell the user what the problem is and how to fix that problem.

ErrorHandlingQuestion

The flow is; when printing the invoice NAV checks to see if payment information has been entered into the Company Information table. If not a question will be asked if the user wants to update the payment information. If the user agrees then the Company Information page will be displayed and the invoice printing will continue after that page is closed.

I wanted to adapt to this method – perhaps a new error handling pattern – when I write an add-on or a customization.  Here is an video from Mark Brummel in line with this new pattern.

I have added this kind of error handling but in some cases a little bit more is required.  As an example, I am doing a series of tests when the Payment Method Code is validated in a Sales Header.  The problem here is that NAV is in the middle of a transaction when I have that question to ask.

To solve this I use a function that is intended for testing only, so using it is not really supported by Microsoft. Perhaps, if they read this blog they will see a useful application for this function in the standard solution.

AssertErrorTest

Here I have two options for you to consider. Doing an ASSERTERROR ERROR(”) will rollback the transaction without stopping the code execution. By doing this before the CONFIRM question I make sure that the users will not be blocking the database while deciding what to do.

FixNow

Pressing Yes will open the page.

ReportSelection

After fixing this error the user can retry the previous action.  I will most likely use a code like this.

RepairReportSelection

Use this carefully.

Selection Filter to Clipboard

I have now seen three posts about using Excel to help create a Pipe Filter.  The latest one from Mohana is similar to the way I normally do this.  His post is a followup from the original video from Kerry Rosvold and an alternative from Mark Brummel.

If this is something you frequently need to do, why don’t use NAV and skip the Excel part ?

Lets imagine how that could look like.

CustomerList

We add a button the ribbon, select what ever we like from the customer list and click the button.

Clipboard

Now it just a matter of using the filter. Paste it where ever you need it.

FilteredCustomerList

And this is with just a few lines of code.

CustomerListModification

Page21 Delta file

Insert network printers automatically

In my current upgrade project I need to upgrade a solution I did for Classic NAV where I used the “‘Windows Script Host Object Model’.WshNetwork” Automation object to add a network printer automatically.

I looked a round and did not find any dotnet object capable of adding a network printer.  However, there was a c# code I found that was able to do this.  So, I created a NAV Add-in with this code

[code lang=”csharp”]using System;
using System.Runtime.InteropServices;

namespace NAVPrinterAdd_in
{
public static class PrinterControls
{
[DllImport("winspool.drv")]
static extern bool AddPrinterConnection(string pName);

public static bool AddNetworkPrinter(string networkPrinterPath, ref string networkMessage)
{
bool result;
try
{
result = AddPrinterConnection(networkPrinterPath);
networkMessage = "";
}
catch (Exception e)
{
result = false;
networkMessage = e.Message;
}
return result;
}
}
}

[/code]

The Add-in is attached. Next I created a Codeunit in NAV.

AddPrinter

And used in Codeunit 1, FindPrinter trigger.

codeunit1

Now I can make sure that the correct printer is installed for the client and also for the server when printing from NAS session.

NAVPrinterAdd-in