Pages

Monday, August 3, 2015

How to check if a Base Enum has a valid value

Base enums can use integer assignment to be set, but you can set it 0 to any positive valid integer up to 255 inclusive, and that does not mean it's a valid enum.

Take for example the base enum "ABC" (\Data Dictionary\Base Enums\ABC).  You can assign ABC=555, and it will store an integer value of 255 with no issue.

To check if an enum value is valid, you can use this method:

static boolean checkABCEnum(ABC _abc)
{
    return new DictEnum(enumNum(ABC)).value2Symbol(_abc));
}

Here is a sample job that will demonstrate how this can be an issue:

static void CheckIfEnumIsValid(Args _args)
{
    // Possible enum values 0, 1, 2, 3
    ABC         abcValid, abcInvalid;
        
    // Valid enum
    abcValid = ABC::C;
    info(strFmt("%1, %2", enum2int(abcValid), abcValid));
    
    // Invalid enum, but integer assignment works and is stored
    abcInvalid = 555;
    info(strFmt("%1, %2", enum2int(abcInvalid), abcInvalid));
    
    if(new DictEnum(enumNum(ABC)).value2Symbol(abcValid))
        info(strFmt("Enum with type %1 and integer value %2 (%3) is valid", typeOf(abcValid), enum2int(abcValid), abcValid));
    else
        error(strFmt("Enum with type %1 and value %2 is invalid", typeOf(abcValid), enum2int(abcValid)));
    
    if(new DictEnum(enumNum(ABC)).value2Symbol(abcInvalid))
        info(strFmt("Enum with type %1 and integer value %2 (%3) is valid", typeOf(abcInvalid), enum2int(abcInvalid), abcInvalid));
    else
        error(strFmt("Enum with type %1 and value %2 is invalid", typeOf(abcInvalid), enum2int(abcInvalid)));
    
    /*
        Output:
        3, C
        4, 
        Enum with type Enum and integer value 3 (C) is valid
        Enum with type Enum and value 4 is invalid
    */
}

Wednesday, June 3, 2015

How to export/import your MorphX VCS settings and history

When using MorphX for version control, sometimes you need to restore a backup of a database, and you don't want to lose all of your check in/out history.  I wrote this job to export and import your MorphX VCS settings.

Use at your own risk, but it's worked fine for me.

Enjoy!


static void AKBackupMorphXVCData(Args _args)
{
    SysDataExport           sysDataExport;
    SysDataImport           sysDataImport;
    
    Dialog                  dialog = new Dialog();
    FormBuildRadioControl   fbImportExport;
    FormRadioControl        radioResults;
    
    dialog.addText("Warning, if you choose Import, this will replace your VCS data and is not reversible!");
    
    // Add the radio button, name it anything
    fbImportExport = dialog.formBuildDesign().addControl(FormControlType::RadioButton, 'RadioButton1');
    fbImportExport.caption("Choose Import/Export");
    fbImportExport.items(2); 

    fbImportExport.item(1);
    fbImportExport.text("Export");
    
    fbImportExport.item(2);
    fbImportExport.text("Import");

    dialog.doInit();
    dialog.formRun().design().moveControl(fbImportExport.id());
    dialog.run();

    if (dialog.closedOk())
    {
        radioResults = dialog.formRun().control(fbImportExport.id());
        
        if (radioResults.selection() == 0) // Export
        {
                sysDataExport = new SysDataExport();
                sysDataExport.parmDoNotBypassDefIO(true);
                sysDataExport.parmServerAccess(true);
                sysDataExport.addTmpExpImpTable(tableNum(SysVersionControlMorphXItemTable), false);
                sysDataExport.addTmpExpImpTable(tableNum(SysVersionControlMorphXLockTable), false);
                sysDataExport.addTmpExpImpTable(tableNum(SysVersionControlMorphXRevisionTable), false);
                sysDataExport.addTmpExpImpTable(tableNum(SysVersionControlParameters), false);
                sysDataExport.addTmpExpImpTable(tableNum(SysVersionControlSynchronizeLog), false);

                if (sysDataExport.prompt())
                {   
                    sysDataExport.parmFiletype(FileType::Binary);
                    sysDataExport.run();
                }
        }
        else if (radioResults.selection() == 1) // Import
        {
            sysDataImport = new SysDataImport();

            if (sysDataImport.prompt())
            {
                sysDataImport.parmLoadAll(true);
                sysDataImport.parmInclTablesNotPerComp(true);
                sysDataImport.parmFiletype(FileType::Binary);
                sysDataImport.run();

                versioncontrol.init();
            }
        }
        
        info("Done!");
    }
}

Dynamic dialog controls at runtime

This is a job that demonstrates how to dynamically add controls (specifically radio button) to a dialog at runtime and also change their position and access their values.

You can use this style in custom advanced UI builders.

static void AKDynamicDialogExample(Args _args)
{
    Dialog                  dialog = new Dialog();
    FormBuildRadioControl   fbRadioControl;
    FormRadioControl        radioControl;
    
    // Add the radio button, name it anything
    fbRadioControl = dialog.formBuildDesign().addControl(FormControlType::RadioButton, 'RadioButton1');

    // Set radio basic properties
    fbRadioControl.caption("Test Radio Buttons");
    fbRadioControl.items(2); // This is needed

    fbRadioControl.item(1); // Switch to first item
    fbRadioControl.text("Item 1"); // Set first item's text
    
    fbRadioControl.item(2); // Switch to second item
    fbRadioControl.text("Item 2"); // Set second item's text

    // This is needed to instantiate the FormRun
    dialog.doInit();

    // Just passing one argument moves it UP.
    // So this moves it UP above the "OK/Cancel" buttons created
    dialog.formRun().design().moveControl(fbRadioControl.id());

    dialog.run();

    if (dialog.closedOk())
    {
        // You need to access it from the formRun() with the correct
        // form control
        radioControl = dialog.formRun().control(fbRadioControl.id());
        info(strFmt("%1", radioControl.selection()));
    }
}



Monday, May 11, 2015

How to create/update a phone number for a customer/vendor in X++ [AX 2012]

As a follow-up to my last post about finding phone numbers, here is sample code of how you can properly find/create/update a phone number for a customer/vendor.


static void CreatePhoneExample(Args _args)
{
    CustTable                           custTable = CustTable::find('100013'); // TODO - Change to your customer
    LogisticsElectronicAddress          logisticsElectronicAddress;
    container                           defaultRole = map2Con(LogisticsLocationEntity::getDefaultLocationRoleFromEntity(tableNum(DirPartyTable)));
    
    setPrefix(strFmt("Creating/Updating number for customer %1", custTable.AccountNum));
    
    // This will find/create a number for a customer
    ttsBegin;
    logisticsElectronicAddress.Type = LogisticsElectronicAddressMethodType::Phone;
    logisticsElectronicAddress.Locator = '555-555-5555';
    
    logisticsElectronicAddress.Location = DirPartyLocation::findOrCreate(custTable.Party, 0).Location;

    // This will find or create the new logisticsElectronicAddress
    // If it does not find it, it will do a .insert() which will only persist these fields (Location, Type, Locator, LocatorExtension)
    // so if you want to set the Description or if it's primary or not, you will need to update the record after this call
    logisticsElectronicAddress = LogisticsElectronicAddress::findOrCreate(logisticsElectronicAddress);
    
    // We re-select it for update in case this isn't a new number and it found an existing
    // because the "find" doesn't "select for update"
    logisticsElectronicAddress = LogisticsElectronicAddress::findRecId(logisticsElectronicAddress.RecId, true);
    
    logisticsElectronicAddress.Description = "New Primary Phone";
    
    // If you set the number to primary, during the insert/update it will handle unassigning previously
    // marked primary numbers if they exist
    logisticsElectronicAddress.IsPrimary = NoYes::Yes;
    
    logisticsElectronicAddress.update();
    
    // At this point, we need to mark the "purpose" of the number.  I'm just using the default role, which should be "Business"
    LogisticsEntityLocationRoleMap::createEntityLocationRoles(tableNum(LogisticsElectronicAddressRole), logisticsElectronicAddress.RecId, conPeek(defaultRole, 1), true);
    
    info(strFmt("Created/updated phone number [%1] for customer %2.", logisticsElectronicAddress.Locator, custTable.AccountNum));
    ttsCommit;
}

How the phone numbers relate to customers/vendors and how to search/enumerate them [AX 2012]

The customer/vendor phone number relation is not terribly complex, but there don't seem to be very many clear-cut examples of sample code on how the joins work.  So I typed up a few sample scenarios.

How to find all of a customer/vendor phone numbers:


    // Find all of the customer's phone numbers
    while select dirPartyLocation
        where dirPartyLocation.Party            == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress  == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc    
        where logisticsElectronicAddress.Location == dirPartyLocation.Location
    {
        info(strFmt("All Numbers\t%1 [Primary == %2]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary));
    }

How to find all of a customer/vendor phone numbers with the "Business" role type:

    // Find all of a customer's phone numbers with the purpose of "Business"
    while select dirPartyLocation
        where dirPartyLocation.Party                            == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress                  == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc
        where logisticsElectronicAddress.Location               == dirPartyLocation.Location
    join logisticsElectronicAddressRole
        where logisticsElectronicAddressRole.ElectronicAddress  == logisticsElectronicAddress.RecId
    join locationRole
        where logisticsElectronicAddressRole.LocationRole       == locationRole.RecId       &&
              locationRole.Type                                 == LogisticsLocationRoleType::Business
    {
        info(strFmt("All Business Numbers Only\t%1 [Primary == %2, Role == %3]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary, locationRole.Name));
    }

How to find all of a customer/vendor phone numbers and all marked roles:

    // Find all of a customer's phone numbers and all of the marked purposes for each number
    while select dirPartyLocation
        where dirPartyLocation.Party                    == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress          == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc
        where logisticsElectronicAddress.Location                == dirPartyLocation.Location
    {
        purposes = conNull();
        
        while select logisticsElectronicAddressRole
            where logisticsElectronicAddressRole.ElectronicAddress   == logisticsElectronicAddress.RecId
        join locationRole
            where logisticsElectronicAddressRole.LocationRole        == locationRole.RecId
     
        {       
            purposes += locationRole.Name;
        }
        
        info(strFmt("All Numbers and all roles\t%1 [Primary == %2, Roles == %3]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary, con2Str(purposes)));
    }

And if you are only looking for a primary contact, that information/link is also directly stored on DirPartyTable for the primaries only and you can query it this way:

    // Alternative method for finding the primary locator contact
    // Can see used here \Classes\DirParty\primaryElectronicAddress
    select firstonly logisticsElectronicAddress
        exists join dirPartyTable
            where dirPartyTable.PrimaryContactPhone == logisticsElectronicAddress.RecId
            && dirPartyTable.RecId == custTable.Party;
    
    info(strFmt("Alternative method\t%1 [Primary == %2]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary));

Here are all of the snippets in one large job you can copy & paste and run (after changing to your own customer)

static void FindPhoneExample(Args _args)
{
    CustTable                           custTable = CustTable::find('100013'); // TODO - Change to one of your customers
    DirPartyTable                       dirPartyTable = DirPartyTable::findRec(custTable.Party);
    DirPartyLocation                    dirPartyLocation;
    LogisticsElectronicAddress          logisticsElectronicAddress;
    LogisticsElectronicAddressRole      logisticsElectronicAddressRole;
    LogisticsLocationRole               locationRole;
    container                           purposes;

    
    info(strFmt("%1", custTable.phone()));
    setPrefix("Finding Phone Numbers");
    
    // Find all of the customer's phone numbers
    while select dirPartyLocation
        where dirPartyLocation.Party            == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress  == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc    
        where logisticsElectronicAddress.Location == dirPartyLocation.Location
    {
        info(strFmt("All Numbers\t%1 [Primary == %2]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary));
    }
    
    // Find all of a customer's phone numbers with the purpose of "Business"
    while select dirPartyLocation
        where dirPartyLocation.Party                            == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress                  == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc
        where logisticsElectronicAddress.Location               == dirPartyLocation.Location
    join logisticsElectronicAddressRole
        where logisticsElectronicAddressRole.ElectronicAddress  == logisticsElectronicAddress.RecId
    join locationRole
        where logisticsElectronicAddressRole.LocationRole       == locationRole.RecId       &&
              locationRole.Type                                 == LogisticsLocationRoleType::Business
    {
        info(strFmt("All Business Numbers Only\t%1 [Primary == %2, Role == %3]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary, locationRole.Name));
    }
    
    // Find all of a customer's phone numbers and all of the marked purposes for each number
    while select dirPartyLocation
        where dirPartyLocation.Party                    == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress          == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc
        where logisticsElectronicAddress.Location                == dirPartyLocation.Location
    {
        purposes = conNull();
        
        while select logisticsElectronicAddressRole
            where logisticsElectronicAddressRole.ElectronicAddress   == logisticsElectronicAddress.RecId
        join locationRole
            where logisticsElectronicAddressRole.LocationRole        == locationRole.RecId
     
        {       
            purposes += locationRole.Name;
        }
        
        info(strFmt("All Numbers and all roles\t%1 [Primary == %2, Roles == %3]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary, con2Str(purposes)));
    }
    
    // Alternative method for finding the primary locator contact
    // Can see used here \Classes\DirParty\primaryElectronicAddress
    select firstonly logisticsElectronicAddress
        exists join dirPartyTable
            where dirPartyTable.PrimaryContactPhone == logisticsElectronicAddress.RecId
            && dirPartyTable.RecId == custTable.Party;
    
    info(strFmt("Alternative method\t%1 [Primary == %2]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary));
    
    info("Done");
}

Wednesday, February 25, 2015

Are you sure that you want to cancel this operation? Keeps popping up fix


If "Are you sure that you want to cancel this operation?" keeps popping up over and over when you open AX, the fix is, when the prompt is up, press Ctrl+Pause, then click "No".

This happens to me all the time because Ctrl+Alt+Pause is a shortcut to make a remote desktop window full screen...and if AX is open and catches some of the keystrokes, it puts it in some sort of weird loop.

Monday, February 2, 2015

How to refresh AX WSDL configuration from a command prompt

My build/release process is almost entirely automated, except for one step, where we refresh the WSDL/WCF configuration.  So far, I've only been able to do it conventionally with the mouse.


After getting pointed in the right direction by Martin DrĂ¡b, I wrote a little command line tool in C# that you can incorporate into your build scripts that should refresh your WCF configuration in your AXC file automatically.

It has one dependency on the AX client configuration tool obviously, which is located at:
C:\Program Files\Microsoft Dynamics AX\60\BusinessConnector\Bin\AxCliCfg.exe

Usage: RefreshAXCConfig.exe <axc file> <aos name> <WSDL Port>
Example: RefreshAXCConfig.exe C:\CUS.axc DevAOS 8101

The only caveat that I'm now realizing as of typing this up, is it uses RegEx to find/replace in the AXC file, so it requires you to have refreshed your WCF at least once, otherwise the RegEx won't find what to replace.

The C# code is simple below, and make sure to add the reference to AxCliCfg.exe.  Happy DAX'ing and hopefully this helps someone.  I've also saved the ZIP'd executable to my OneDrive.  You will need to copy the AxCliCfg to the same local directory in order for it to work.

Link to compiled zipped executable for those who don't feel like typing up themselves.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Xml;
using Microsoft.Dynamics.Configuration;

namespace RefreshAXCConfig
{
    class Program
    {
        static void Main(string[] args)
        {
            Tuple<Guid, string> result;
            string axcFile;
            string strAOS;
            int intWSDLPort;
            
            if (args.Length == 0)
            {
                Console.WriteLine("Usage: RefreshAXCConfig.exe <axc file> <aos name> <WSDL Port>");
                Console.WriteLine("Example: RefreshAXCConfig.exe CUS.axc Dev-vwaos05 8101");
                return;
            }

            try
            {
                axcFile = args[0];
                strAOS = args[1];
                if (int.TryParse(args[2], out intWSDLPort) == true)
                {
                    result = FormRegenerateWcfDialog.GetConfigurationAsString(strAOS, intWSDLPort);

                    XmlDocument xmlDoc = new XmlDocument();
                    xmlDoc.LoadXml(result.Item2);

                    File.WriteAllText(axcFile, Regex.Replace(File.ReadAllText(axcFile), @"<\?xml.*\</configuration\>", xmlDoc.InnerXml));
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Error encountered: {0}", e.Message);
            }

            return;
        }
    }
}