Pages

Tuesday, April 24, 2018

How to convert a decimal to a fraction in X++ with Dynamics AX or Dynamics 365 FO

I had a customer who needed to convert a decimal to fraction, so I just translated this StackOverflow post where all credit is due. I'm sure someone else will find it useful at some point.

The two main things in the job below (details taken from StackOverflow) are "_real", which is the value you want to convert, and "_accuracy", which specifies the max relative error; not the max absolute error.

So _accuracy = 0.01 would find a fraction within 1% of the value.

I also quickly threw this job together, so I didn't test for extremely large integers or any edge cases.

Here's a simple job that demonstrates how it works.


static void KW_DecToFrac(Args _args)
{
    // Create a function that accepts these two parameters
    real                _real           = 0.45;
    real                _accuracy       = 0.01;
    
    int                 sign = _real < 0 ? -1 : (_real == 0 ? 0 : 1);
    real                maxError;
    System.Decimal      d;
    
    int n;
    int lower_n = 0;
    int lower_d = 1;
    int middle_n;
    int middle_d;
    int upper_n = 1;
    int upper_d = 1;
    int z;
    
    void f(int _N, int _D)
    {
        info(strFmt("%1/%2", _N, _D));
    }
    
    _real = abs(_real);
    
    maxError = sign == 0 ? _accuracy : _real * _accuracy;
    
    d = System.Math::Floor(_real);
    n = System.Decimal::ToInt32(d);
    
    _real -= n;
    
    if (_real < maxError)
    {
        f(sign * n, 1);
        return;
    }
    
    if (1 - maxError < _real)
    {
        f(sign * (n+1), 1);
        return;
    }
    
    while (true)
    {
        z++;
        middle_n = lower_n + upper_n;
        middle_d = lower_d + upper_d;
        
        if (middle_d * (_real + maxError) < middle_n)
        {
            upper_n = middle_n;
            upper_d = middle_d;
        }
        else if (middle_n < (_real - maxError) * middle_d)
        {
            lower_n = middle_n;
            lower_d = middle_d;
        }
        else          
        {
            f((n * middle_d + middle_n) * sign, middle_d);
            return;
        }        
    }
    
    info("Done");
}

Wednesday, April 4, 2018

[#MsDyn365FO] How to adjust your DeployablePackages cleanup duration

Microsoft automatically cleans up the "DeployablePackages" folder for data older than 30 days on your Dynamics 365 for Finance and Operations, Enterprise Edition machine if you are using the LCS servicing flows.

If you prefer to keep less data, such as 10 days, you can add the following registry key to HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment :



Or copy & paste the below into a *.reg file, and you can add it that way:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Dynamics\Deployment]
"CutoffDaysForCleanup"="10"


See Yammer thread here for Microsoft's comments.

Tuesday, January 2, 2018

How to see when AOS service was started/stopped with PowerShell for AX with 1 line of code.

I often need to see when an AOS was stopped for various reasons, and scanning the event viewer can be a hassle. Running this simple 1-line PowerShell script will give you that information.

You can also use this script as a guide to run other PowerShell scripts to search the event log for specific text.

Get-WinEvent -ErrorAction SilentlyContinue -FilterHashtable @{logname='system'; StartTime=(((get-date).AddDays(-30).Date)); EndTime=(((get-date).AddDays(1).Date)); id=7036} -MaxEvents 10000 | ? {$_.Message -like '*Microsoft Dynamics AX Object Server*'}

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)
{  
    #File
    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.write('');
    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);
        else
            textIo.write(labelId + '=' + labelString);
    }
    
    textIo.write(''); // Write ending CR
    textIo = null;
        
    CodeAccessPermission::revertAssert(); 
    
    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]",
                    logisticsPostalAddressOld.Address,
                    logisticsPostalAddressOld.ValidFrom,
                    logisticsPostalAddressOld.ValidTo));
    }
}



Friday, May 26, 2017

How to rename a D365 virtual machine, link it to LCS, and optionally join it to a domain - Dynamics 365 for Finance and Operations, Enterprise edition

Whenever I get a new D365 virtual machine, I need to rename it, link it to LCS, and join it to our domain. I kept forgetting what I did, so I wrote it down here to share.

I rename it because there are multiple copies of the VM on the same network, and joining to a domain should be obvious all of the benefits there.

So here are the steps that I perform to quickly do this, and if anyone has any other steps to add/modify, please let me know and I'll update the post. Follow carefully as each step is important.


  1. Connect to the VM, provision yourself (as usual)
  2. Go to Control Panel>System and Security>Security, click "Change Settings", and rename the machine to something unique.
  3. Restart VM
  4. Open Reporting Services Configuration Manager and connect to "localhost" or whatever you named your machine, and change the Database server so that it connects
  5. Open SQL Management Studio by doing "Run As Administrator", as the Local Admins security group is added but not the local administrator user.  Connect to SQL.
  6. Run these commands to get the name SQL thinks it is and the name you've used
    select @@SERVERNAME
    Select serverproperty('ServerName')
  7. Copy the old machine name and new machine name and replace it in these SQL commands and execute them
    --Run this with the updated names
    sp_dropserver 'MININT-S45GUTR'; --Old Name
    GO

    sp_addserver 'D365QDEVMAIN',LOCAL; --New Name
    GO
  8. Restart the machine
  9. In SQL (run as admin), run the steps in Step 6 again to verify both return the same new machine name result
  10. (Optional) The following steps are the optional LCS/Domain steps. Create a project in LCS that your D365 for Operations (or whatever it's called at the time of reading this) VM will connect to.
  11. (Optional) Download the LCS diagnostic installer under the "System Diagnostic" tile
  12. (Optional) Extract and run Setup. Choose "Create a new certificate" and enter whatever into the prefix. I usually just do the machine name.
  13. (Optional) This creates a certificate in the same directory as the installer, upload the certificate back into LCS under the same tile, then continue with the installer after it has been uploaded.
  14. (Optional) For the account/password I put in MachineName\Administrator and the administrator password. If you choose to use the local admin, it's important that you type the newly created machine name you chose or it doesn't always work.
  15. (Optional) There should now be a new icon on your desktop to launch the LCS diagnostic utility. Run this.
  16. (Optional) Put in the environment name and server name with your newly chosen name and put "AxDB" for the database name. Then click the buttons at the bottom of the tool in sequence.
  17. (Optional) When you get to step #3, your window may white-out and clear. Just choose the drop-down at the top under "Environment Name" and re-select your environment
  18. (Optional) Click "Generate Command" to get a copy/paste command that you can set up a scheduled task for so that LCS gets regular data. Copy the command and close the window. Mine was:

    "C:\Program Files\Microsoft Dynamics Lifecycle Services System Diagnostic Service\LCSDiagFXCollector.exe" "-Collect" "D365QDEVMAIN" "1"
  19. (Optional) Open "Task Scheduler" and create a new task and choose "Run whether user is logged on or not" and I choose "Run with highest privileges"
  20. (Optional) Choose the Triggers tab and setup a daily trigger (or whatever you want)
  21. (Optional) Choose the "Actions" tab, click New, and paste the entire command in the program/script window and click ok. It will prompt you to automatically fix it so just click yes.
  22. (Optional) Click OK to save the task and enter your user/password so that it can store it to run the task
  23. (Optional) Go back to LCS and under the same "System Diagnostic" tile, click on the "Environments" tab and verify your new environment is there and data is uploaded
  24. (Optional) Finally join it to your domain and restart.

And you are done!

Thursday, May 18, 2017

How to programmatically add menu items to favorites menu via X++ in Dynamics AX

The favorites menu is a bit unusual in AX in the way you need to add to it. Most of the way it works appears to be Kernel level, but through some TreeNode usage you can add items to it.

This will add the "SalesTable" menu item to your favorites.


static void JobAddToFavorites(Args _args)
{
    TreeNode                treeNode;
    TreeNode                menuToAdd = TreeNode::findNode(@"\Menu Items\Display\SalesTable");
    TreeNodeIterator        iterator;
    UserMenuList            userMenu;
    Menu                    menuNode;


    treeNode = infolog.userNode();
    iterator = treeNode.AOTiterator();
    treeNode = iterator.next();
    if (treeNode)
    {
        userMenu = treeNode;

        // find 'My Favorites' user menu; 
        treeNode = userMenu.AOTfindChild("@SYS95713");

        // Note menuNode is a different object than userMenu
        menuNode = treeNode;

        menuNode.addMenuitem(menuToAdd);
    }    
}

Another table to take note of is SysPersonalization if you're looking at doing this for multiple users. I haven't dug into this deeply, but this code snippet should get you started with what you may want to do.