Tuesday, August 22, 2017

Job to export AX 2012 label file in Dynamics 365 FOEE format for easy movement

I am maintaining an ISV solution across AX 2012 and Dynamics 365 for Finance and Operations, Enterprise Edition, and when there is development in one, porting those changes to the other can be a hassle.

If I create a label in Dynamics AX 2012, exactly creating it in D365 isn't straight forward/simple.

I quickly wrote this simple job to export a label to a format that can be easily imported into Dynamics 365. I simply looked at how D365 label text is stored and inferred this, so I haven't done extensive testing, but it seems to work fine for several hundred labels when comparing via WinMerge what my job outputs vs what D365 contains.

static void AlexOnDaxExportLabelToD365(Args _args)
    str                         labelFileId         = 'QUA';
    str                         language            = 'en-us';
    Filename                    labelFilenameD365   = @'C:\Temp\AlexLabel.label.txt';
    LabelId                     labelId;
    LabelString                 labelString;
    LabelDescription            labelDescription;
    Set                         setLabelIds;
    SetEnumerator               se;
    SysLabelFileReader          labelFileReader;
    TextIo                      textIo;
    SysLabelFile                labelFile = SysLabelFile::newLanguageModule(language, labelFileId);
    if (!Label::flush(labelFileId, language))
        throw error(strFmt("Unable to flush label %1 in language %2", labelFileId, language));
    new FileIOPermission(labelFilenameD365, 'W').assert();
    // This just create the file if it doesn't exist
    textIo = new TextIo(labelFilenameD365, #IO_Write, #utf8Format);
    textIo = null;

    // We output the file somewhere  
    if (labelFile.toFile(labelFilenameD365, true))
        labelFileReader = SysLabelFileReader::newFileClient(labelFilenameD365);

        if (labelFileReader)
            setLabelIds = labelFileReader.labelIds();
    if (!(setLabelIds && labelFileReader))
        throw error("Unable to get label");
    textIo = new TextIo(labelFilenameD365, #IO_Write, #utf8Format);
    se = setLabelIds.getEnumerator();
    while (se.moveNext())
        labelId             = se.current();
        labelString         = labelFileReader.labelText(labelId);
        labelDescription    = labelFileReader.labelDescription(labelId);
        // There must be a value
        if (!labelString)
            labelString = ' ';
        if (labelDescription)
            textIo.write(labelId + '=' + labelString + '\n' + ' ;' + labelDescription);
            textIo.write(labelId + '=' + labelString);
    textIo.write(''); // Write ending CR
    textIo = null;
    info(strFmt("Finished converting %1 to Dynamics 365 for Operations label file", labelFilenameD365));

Wednesday, August 16, 2017

How to find previously used addresses or other older records in Valid Time State Tables for Date Effective Data in Microsoft Dynamics 365 or AX

In Dynamics (AX/365) there is a concept of "Valid Time State Tables" that contain date effective data.  In this post, you'll find code to list all the previous addresses tied to an individual record.

If you are not familiar with this type of data, the simplest example to wrap your head around is postal addresses. If you are, just skip the next paragraph.

Let's say you send an order/invoice to a customer at 123 South St on 1/1/2017. Then a few months later, the customer moves locations to 456 North Ave and you update their address in Dynamics. All is good so far. Then for some reason you need to reprint the 1/1/2017 invoice...well that address record for 123 South St still exists in the database, but it's "ValidTo" is in the past. This way you have the historical addresses.

This sample job below takes an address and loops through the older versions of that address. This method can be used to seek any data in valid time state tables though.

static void AlexOnDAXFindOldAddresses(Args _args)
    CustTable                       custTable = CustTable::find("ZZZZ");
    LogisticsPostalAddress          logisticsPostalAddress;
    LogisticsPostalAddress          logisticsPostalAddressOld;
    utcDateTime                     utcMinVal = DateTimeUtil::minValue();
    utcDateTime                     utcMaxVal = DateTimeUtil::maxValue();
    // Active address
    logisticsPostalAddress = CustTable.postalAddress();
    info("Current: " + logisticsPostalAddress.Address);
    setPrefix("Previous Addresses");
    // This will loop through the inactive addresses in order
    while select validTimeState(utcMinVal, utcMaxVal) logisticsPostalAddressOld
        order by ValidTo desc
        where logisticsPostalAddressOld.Location        == logisticsPostalAddress.Location      &&
              logisticsPostalAddressOld.RecId           != logisticsPostalAddress.RecId
        info(strFmt("%1 [%2-%3]",