Tuesday, November 30, 2010

Change default layers when comparing code

During the integration of system patches or third-party layers I found it annoying to continually select a certain layer when using the compare tool.  Multiply the time it takes by 400 or more nodes and you get an unhappy developer.
Fixing this problem is very easy.  Just add the following code to the end of the \Classes\SysCompare.initContext() method.


    if (comboBox1.getText(comboBox1.selection()) == comboBox2.getText(comboBox2.selection()) && comboBox2.items() > comboBox2.selection() + 1 )
    {
        comboBox2.selection(comboBox2.selection()+1);

        // 2010.11.30  Change default selectons for code comparison
        if (comboBox1.items() == comboBox2.items() - 1)
        {
            if (comboBox1.items() > 2 && Global::strEndsWith(comboBox2.getText(comboBox2.items() - 1), ' (Washed)')) // english only
            {
                // Compare last two layers (not selecting the washed version)
                comboBox1.selection(comboBox1.items() - 2); // Set first drop down to second-to-last possible option
                comboBox2.selection(comboBox2.items() - 2); // Set second drop down to second-to-last possible option
            }
        }
    }
    // 2010.11.30  Change default selections for code comparison when importing
    else if (comboBox1.items() > 1 && comboBox2.items() == 1 && Global::strEndsWith(comboBox2.getText(1 - 1), ' (xpo)'))
    {
        // Compare last layer to the imported XPO
        comboBox1.selection(comboBox1.items() - 1); // Set first drop down to last possible option
    }

Monday, November 15, 2010

Convert Axapta Time in SQL

AX 4.0 stores time in the database using the seconds since midnight.  So in order to view the time (military format) we must divide.  Here is an example:
SELECT TOP 20 StartTime AS [Seconds since midnight],
      CAST(StartTime/60/60 AS VARCHAR(2)) + ':' + RIGHT('0' + CAST(FLOOR((StartTime/60.0/60.0 %1)*60) AS VARCHAR(2)), 2) AS [Start Time]
FROM Batch
WHERE [Status] = 1

Monday, November 1, 2010

Disable users who are not active in Active Directory

Occasionally when auditors come by I like to disable all user accounts in AX which have been disabled in Active Directory.  Even though AD will not let them login auditors have a hard time understanding it, so I disable the users.  Many times we do not get notification that someone has left the company, or sometimes it does not reach the right people in charge of AX security.  So I made the job below which disables users in AX because they are disabled in Active Directory.  The job takes a little while to run.


static void disableUsersMissingInAD(Args _args)
{
    UserInfo                userInfoUpdate;
    xAxaptaUserManager      xAxaptaUserManager;
    xAxaptaUserDetails      xAxaptaUserDetails;
    #Guest
    
    xAxaptaUserManager = new xAxaptaUserManager();

    Global::startLengthyOperation();
    ttsbegin;

    while select forUpdate userInfoUpdate
    order by networkAlias
    where userInfoUpdate.Id != #GuestUser
       && userInfoUpdate.enable == 1
    {
        // Get the single user's details from the kernel class
        xAxaptaUserDetails = xAxaptaUserManager.getDomainUser(userInfoUpdate.NetworkDomain, userInfoUpdate.NetworkAlias);

        // Only show users who are enabled in Active Directory
        if (xAxaptaUserDetails == null || xAxaptaUserDetails.getUserCount() == 0 || !xAxaptaUserDetails.isUserEnabled(0))
        {
            userInfoUpdate.enable = 0;
            userInfoUpdate.update();
        }
    }

    ttscommit;
    Global::endLengthyOperation();
}

Monday, October 25, 2010

Line ###-Offset voucher does not exist in account ______.


If a vendor transaction is reversed but an AP check has been printed (and the payment journal not posted) you may get this error.  Essentially the record which was marked for settlement by the check has been deleted so it does not exist (VendTransOpen was deleted when the invoice was reversed).  If you go to the line in the accounts payable payment journal and click Inquiries > view marked transactions you will find it blank.  AX has lost the relationship of what invoices/credits were settled as part of the payment.  There are really two tables involved SpecTrans and VendTransOpen.

First thing we need to know is which transactions were settled by that payment.  The check will have the transactions shown in the Bank module.  Bank > Checks…select the check…Invoices button.  It lists all the transactions paid.  Go to the vendor transaction which was paid, click the Open button and remember the RecID of the VendTransOpen record for that transaction.

Find the reversal transaction and revoke the reversal (click the Reverse button when you have the reversal transaction selected).  The invoice should now have a balance like any other open invoice.
At this point if the SpecTrans record still exists you can fix it otherwise you must create it.  First open the SpecTrans table in the table browser and filter on SpecRecID = the journal line’s RecID.  If you find one then just fix the RefRecID to be the record ID of the VendTransOpen record.  Otherwise create the missing SpecTrans record by using a job.

static void Job1(Args _args)
{
    SpecTrans       specTrans;
    ;
    SpecTrans.clear();
    SpecTrans.initValue();
    SpecTrans.SpecTableId       = tablenum(LedgerJournalTrans);
    SpecTrans.SpecRecId         = 5638151884; // Record ID of the ledger journal line
    SpecTrans.LineNum           = 1.00;
    SpecTrans.Code              = "USD";
    SpecTrans.Balance01         = -78680.92; // Amount of the invoice which was paid
    SpecTrans.RefTableId        = tablenum(VendTransOpen);
    SpecTrans.RefRecId          = 5637622098; // Record ID of the open transaction
    SpecTrans.Payment           = NoYes::No;
    SpecTrans.PaymentStatus     = CustVendPaymStatus::Sent;
    SpecTrans.ErrorCodePayment  = "";
    SpecTrans.FullSettlement    = NoYes::No;
    SpecTrans.insert();
}

Saturday, October 9, 2010

Get the next unique file name

Sometimes you are saving a temporary file so you don't want to delete or overwrite anything that already exists in a directory...there is a nice function to find the next unique file name.

fileNameTemp = Global::fileNameNext(fileOriginalsPath + fileName);

Wednesday, October 6, 2010

Monday, October 4, 2010

Rename items quickly

Here is a little script to rename items quickly.  Just provide a CSV file with old and new item number.  I found that making the CSV file was faster than making a job to intelligently rename items because I could use Excel functions and review the new item numbers faster.

static void renameFromCSV(Args _args)
{
    #File
    CommaIO                 file;
    Container               values;
    Dialog                  dialog;
    DialogField             dfFileName;
    LineNum                 lineNum;
    ItemID                  itemIDfrom;
    ItemID                  itemIDto;
    InventTable             inventTable;
    ;
    setPrefix(funcName());
    dialog      = new Dialog("Rename items");
    dialog.filenameLookupFilter(["@SYS39829", #AllFilesName + #csv]);
    dfFileName  = dialog.addField(typeid(FileNameOpen));
    if (!dialog.run() || !dfFileName.value())
        return;

    if (!WinAPI::fileExists(dfFileName.value()))
        throw error(strfmt("@SYS18678", dfFileName.value()));

    file = new CommaIO(dfFileName.value(), #io_read);
    if (!file)
        throw error("@SYS74299");
    values = file.read();

    while (values != connull())
    {
        setPrefix(strfmt("Row %1 for %2", lineNum, conpeek(values, 1)));
        if (conlen(values) != 2)
            throw error('Bad row');

        itemIDfrom  = conpeek(values, 1);
        itemIDto    = conpeek(values, 2);

        if (InventTable::exist(itemIDfrom))
        {
            if (!InventTable::exist(itemIDto))
            {
                ttsbegin;
                inventTable = InventTable::find(itemIDfrom, true);
                inventTable.ItemId = itemIDto;
                inventTable.renamePrimaryKey();
                ttscommit;
                
                info(strfmt("%1 to %2", itemIDfrom, itemIDto));
            }
            else
                error(strfmt("@SYS93964", itemIDto));
        }
        else
            error(strfmt("@SYS94719", itemIDfrom));

        lineNum++;
        values = file.read();
    }
    info("@SYS78985");
}

Thursday, September 30, 2010

Modifying tab order

In AX to change tab order normally you change the order of the controls in the form.  You can set the "Skip" property on a control to Yes and when the user tabs it will skip that control.  Certain forms where there are many groups or process flow should be different than the default way require a different approach.  There is a way to specify the tab order for an entire form.

First set AutoDeclaration to Yes on all your controls.
Second override the init method and provide it an array of control IDs in the order that you want to tab.

void init()
{
    Array tabOrder = new Array(Types::Integer);
    ;
    super();

    tabOrder.value(1, Control1.id());
    tabOrder.value(2, Control2.id());
    tabOrder.value(3, Control3.id());
    tabOrder.value(4, Control4.id());
    tabOrder.value(5, Control5.id());
    tabOrder.value(6, Control6.id());
    tabOrder.value(7, Control7.id());
    tabOrder.value(8, Control8.id());
    tabOrder.value(9, Control9.id());
    tabOrder.value(10, Control10.id());
    tabOrder.value(11, Control11.id());
    tabOrder.value(12, Control12.id());

    element.tabOrder(tabOrder);
}

Sunday, September 26, 2010

Nagios: Automatically restart IIS after a CRITICAL alert

You can execute code when a service or host changes state.  These are called event handlers.

  1. Go to the Windows server and create a local user
  2. Create the following script called iisreset.sh in your Nagios plugins directory replacing the username and password in the three locations below



    #!/bin/sh
    #
    # Event handler script for restarting IIS on windows server
    #    Requires a local user on the server with the username and password below
    
    if [ $# -eq 3 ]
    then
            if [ "$1" = "CRITICAL" ]
            then
                    if [ "$2" = "HARD" ]
                    then
                            # Start the service
                            /usr/bin/net rpc service stop W3SVC -I $3 -U User%password >/dev/null
                            sleep 4
                            /usr/bin/net rpc service start W3SVC -I $3 -U User%password >/dev/null 2>&1
                            sleep 4
                            /usr/bin/net rpc service status W3SVC -I $3 -U User%password | grep 'W3SVC'
                            exit 0
                    fi
            fi
    else
            echo "Invalid number of arguments"
            exit 1
    fi
    

  3. Edit your commands.cfg file in the Nagios configuration directory



    define command {
            command_name    iisreset
            command_line    $USER1$/iisreset.sh $SERVICESTATE$ $SERVICESTATETYPE$ $HOSTADDRESS$
            }
    

  4. Add the event handler command to an existing service definition



    define service{
                    host_name                         somehost
                    service_description        HTTP
                    event_handler                  iisreset
                    ...
                    }
    

  5. The script will be called like this.  You can put this in a command line to confirm it will work. /usr/lib/nagios/plugins/iisreset.sh CRITICAL HARD 10.10.0.2
  6. Reload or start Nagios
    /etc/init.d/nagios3 reload

Saturday, September 11, 2010

Tuesday, September 7, 2010

Deleting all transactions in a company

Sometimes when creating a new playground for testing you need to delete all transactions in a company.  There is a nice utility included in AX for this purpose.  The SysDatabaseTransDelete class will delete all tables by checking the TableGroup property on the table.  All WorksheetHeader, WorksheetLine, and Transaction tables will be deleted.  There are a few exceptions so please review the class before running, and NEVER run in the live environment (I shouldn't have to say that).

Sunday, September 5, 2010

How to modify the permissions of the Admin group

After creating a new security key you will need to modify the permissions of the Admin group, but the User group permissions form does not allow it.  This small AX job will grant the Admin group permissions to the new security key.

static void grantSafetyAccess(Args _args)
{
    #Admin
    SecurityKeySet  securitySet;
    ;
    setPrefix(funcName());
 
    securitySet = new SecurityKeySet();
    securitySet.loadGroupRights(#AdminUserGroup, '');
 
    securitySet.access(securitykeynum(MyNewSecurityKey), AccessType::Delete);
 
    xAccessRightsList::saveSecurityRights(securitySet.pack(), #AdminUserGroup, '');
}

Friday, September 3, 2010

Reports being used

I was asked recently which reports were being run by users so that some of the menus could be simplified.  This SQL will list users and the AX report name for every report that has been run by anyone.  It is a good start, but it cannot get us the last date or time that they ran the report.

SELECT UserInfo.[Name], ElementName AS [Report Name], UserInfo.[Enable] AS [Active Employee]
INTO #temp
FROM SysLastValue WITH(NOLOCK)
INNER JOIN UserInfo WITH(NOLOCK)
                ON UserInfo.ID = SysLastValue.UserID
WHERE UserID NOT IN ('Admin', '')
                AND RecordType = 18 /* Report */

SELECT t.[Report Name],
                (SELECT COUNT(DISTINCT [Name])
                                FROM #temp sub
                                WHERE sub.[Active Employee] = 1
                                                AND sub.[End User] = 1
                                                AND sub.[Report Name] = t.[Report Name]) AS [# of Active Employees Who Ran The Report]
FROM #temp t
GROUP BY [Report Name]
ORDER BY [Report Name]

DROP TABLE #temp
}

This AX job returns the label and location of all reports in the menu structure.  We can then add this information with the last used information to come up with a fairly good estimation of which reports are being used.

// Lists where all reports are located in the menu structure
// Does not include reports which are called by classes, or auto printed by a scheduled job
static void listMenuStructure(Args _args)
{
    #AOT
    #Properties
    TreeNode                treeNode;
 
    void traverse(TreeNode tn)
    {
        while (tn)
        {
            switch (tn.AOTgetNodeType())
            {
                case 205: // Folder
                    traverse(tn.AOTfirstChild());
                    break;
                case 331: // Menu reference
                    if (tn.AOTgetProperty(#PropertyName))
                    {
                        treeNode = TreeNode::findNode(#MenusPath + #AOTDelimiter + any2str(tn.AOTgetProperty(#PropertyName)) + #AOTDelimiter + 'Reports');
                        if (treeNode)
                            traverse(treeNode.AOTfirstChild());
                    }
                    break;
                case 307: // Menu item
                    setPrefix(tn.treeNodePath());
                    info(tn.AOTgetProperty(#PropertyName));
                    info(tn.AOTgetProperty(#PropertyMenuitemname));
                    break;
            }
            tn = tn.AOTnextSibling();
        }
    }
    ;
    setPrefix(funcName());
    treeNode = TreeNode::findNode(#MenusPath + #AOTDelimiter + 'MainMenu');
    treeNode = treeNode.AOTfirstChild();
    traverse(treeNode);
}

Saturday, July 17, 2010

Inventory Closing and Adjustment

After writing a report which shows inventory transaction cost as of a specific date we had a small issue where very occasionally certain transactions were showing double cost.  After some poking around I found this post:

http://fedotenko.info/?page_id=31

The developer goes into depth about the procedure conceptually, technically, and differences between AX 4.0 and 2009 costing.  It is the very best explanation of the inventory costing feature in AX and saves many many hours of reading and interpreting code to understand.

Friday, July 16, 2010

Physical remaining quantity and total quantity must have the same sign

When posting a production report as finished journal you may receive this error,
Physical remaining quantity and total quantity must have the same sign.

It is because the remaining quantity on the production order is incorrect.



The quantity should never be negative.  I think it happens after running estimation incorrectly.  Run this job and it will fix the quantity.

static void fixProdNegQty(Args _args)
{
    ProdTable               prodTable;
    ;
    ttsbegin;

    select firstOnly forUpdate prodTable
    where prodTable.ProdId == "W0024381"
       && prodTable.RemainInventPhysical < 0;

    if (prodTable.RecId)
    {
        prodTable.RemainInventPhysical = 0;
        prodTable.modifiedField(fieldnum(ProdTable, RemainInventPhysical));
        prodTable.update();

        info("Updated");
    }

    ttscommit;
}

Thursday, July 15, 2010

You can't have unallocated cost on a planning item

When using the functionality provided by Process Manufacturing by ToIncrease occasionally we get the following error when running inventory close and adjustments.
You can't have unallocated cost on a planning item
The error always occurs when it is trying to pass an adjustment through a production order ("Batch order").  Every other month we encounter this issue so we customized it to tell us the production order number.  In \Classes\InventCostItemDim.addFormulaKeyAdjustment() add "info(_inventTrans.TransRefId);" just before "throw error("@PRO997");".




Through experience we have found that either the co/by product allocation does not total to 100% and the ProdBOM.pmfAllocationPct needs updating or that the received or purchased transactions for the production order were somehow set to value closed when they should still be open.  Running a SQL statement similar to this will set the value to open, allowing the adjustment to pass to those transactions.


update InventTrans
set ValueOpen = 1
where dataAreaId = '510'
and TransRefID = 'W0018583'
and StatusReceipt = 1

Monday, July 12, 2010

Offset voucher does not exist in account ____.

When posting a ledger journal you may receive:

Whenever open accounts payable or receivable transactions are selected a corresponding record in SpecTrans is created, which just holds the ledger journal line record ID and the open transaction record ID.

  1. Open the ledger journal
  2. Select the ledger journal line with the issue
  3. Click Inquiries > Marked for settlement
    This screen shows the items marked to be settled (paid) by the current line.
  4. Use the record ID from your ledger journal line with the error to filter on SpecRecID in the table browser.
  5. Go through each transaction and compare to the Marked for settlement list.  The record which is missing is pointing to a VendTransOpen or a CustTransOpen record which somehow does not exist anymore (RefTableID and RefRecID).  At this point, you can either correct the RefRecID to point to the correct transaction if it still exists, or you can delete the transaction.


Tuesday, July 6, 2010

Writing Quick Jobs/Exports

After getting many requests for lists, reports, exports I wrote a quick add-in script to give me a template for jobs.  With a little code you can add another template to the IDE which will allow you to easily write an export to CSV (Excel) with a dialog asking for parameters as well as a progress bar.



Go to /Classes/xppSource
Add a new method:

Source jobExport(
    SysElementName          name,
    boolean                 progressBar,
    boolean                 dialog
    )
{
    ;
    source += strfmt('static void %1(Args _args)', name) + #newLine;
    source += #methodStart;
    indentLevel = #defaultIndentColumns;
    source += this.indent() + '#File' + #newline;
    source += this.indent() + '#WinAPI' + #newline;
    source += this.indent() + 'CommaIO                 file;' + #newline;
    source += this.indent() + #string + '                     fileName = strfmt("%1\\\\%2_%3.csv", WinAPI::getFolderPath(#CSIDL_DESKTOP), funcname(), curExt());' + #newline;

    if (progressBar)
        source += this.indent() + 'SysOperationProgress    progress;' + #newline;
    if (dialog)
    {
        source += this.indent() + #newline;
        source += this.indent() + 'Dialog                  dialog;' + #newline;
        source += this.indent() + 'DialogField             dfStartDate, dfEndDate;' + #newline;
        source += this.indent() + 'StartDate               startDate;' + #newline;
        source += this.indent() + 'EndDate                 endDate;' + #newline;
    }

    source += this.indent() + ';' + #newline;
    source += this.indent() + 'setPrefix(funcName());' + #newline;
    source += this.indent() + #newline;

    if (dialog)
    {
        source += this.indent() + 'dialog = new Dialog(funcname());' + #newline;
        source += this.indent() + 'dfStartDate = dialog.addField(typeid(StartDate));' + #newline;
        source += this.indent() + 'dfEndDate   = dialog.addFieldValue(typeid(EndDate), systemDateGet());' + #newline;
        source += this.indent() + 'if (!dialog.run() || !dfStartDate.value() || !dfEndDate.value())' + #newline;
        source += this.indent() + strrep(' ', #defaultIndentColumns) + 'return;' + #newline;
        source += this.indent() + '    startDate = dfStartDate.value();' + #newline;
        source += this.indent() + '    endDate = dfEndDate.value();' + #newline;
    }

    // Create and initalized the file
    source += this.indent() + 'new FileIOPermission(fileName, #io_Write).assert();' + #newline;
    source += this.indent() + 'file = new CommaIO(fileName, #io_write);' + #newline;
    source += this.indent() + 'if (!file)' + #newline;
    source += this.indent() + strrep(' ', #defaultIndentColumns) + 'throw error("@SYS74299");' + #newline;
    source += this.indent() + #newline;
    source += this.indent() + '// File headings' + #newline;
    source += this.indent() + 'file.write(funcName());' + #newline;
    source += this.indent() + 'file.write(\' \');' + #newline;
    source += this.indent() + 'file.write("","",""); // Column headings' + #newline;
    source += this.indent() + #newline;

    // Progress indicator
    if (progressBar)
    {
        source += this.indent() + '// Initialize progress indicator' + #newline;
        source += this.indent() + 'progress = new SysOperationProgress();' + #newline;
        source += this.indent() + 'progress.setTotal((select count(RecId) from common).RecId);' + #newline;
        source += this.indent() + 'progress.setCaption(funcName());' + #newline;
        source += this.indent() + #newline;
    }

    // Loop
    source += this.indent() + 'while select common' + #newline;
    this.beginBlock();
    if (progressBar)
        source += this.indent() + 'progress.setText(common.caption());' + #newline;
    source += this.indent() + 'file.write(common.RecId);' + #newline;
    if (progressBar)
        source += this.indent() + 'progress.incCount();' + #newline;
    this.endBlock();

    if (progressBar)
        source += this.indent() + 'progress.kill();' + #newline;
    source += this.indent() + 'WinAPI::shellExecute("\\\"" + fileName + "\\\"");' + #newline;
    source += this.indent() + 'info(fileName);' + #newline;
    source += #methodEnd;
    return source;
}

Go to /Classes/EditorScripts
Add a new method:

void template_method_exportJob(Editor editor)
{
    xppSource       xppSource       = new xppSource();
    Dialog          dialog          = new Dialog("@SYS69534");
    DialogField     dlgName         = dialog.addField(typeid(SysElementName),"Job name");
    DialogField     dlgDialog       = dialog.addField(typeid(NoYesId),"@SYS24736");
    DialogField     dlgProgress     = dialog.addField(typeid(NoYesId),"@SYS94935", "@SYS89247");
    Source          template;
    ;
    if (dialog.run())
    {
        template = xppSource.jobExport(dlgName.value(), dlgProgress.value(), dlgDialog.value());

        editor.insertLines(template);
    }
}

Security Rights

We all know that AX never came with enough Security reports.  Security is complex in AX, if someone asks a functional question, "Who has access to view fixed assets?" I've found it is not as simple as looking...instead you find yourself answering a dozen questions to answer just the one simple one...Who belongs to which groups?  Which groups have access?  What type of access - View, Edit, Delete?  Do they have access to the form as well as the table?  Has someone else in my team modified security recently?

This is a job which answers the question from above by iterating through groups and users.

static void listSecurityRights_AssetTable(Args _args)
{
    TableId             tableId;
    DictTable           dictTable;
    DictField           dictField;
    DictSecurityKey     dictSecurityKey;
    SecurityKeySet      securitySet;
    UserGroupInfo       userGroupInfo;
    UserInfo            userInfo;
    AccessType          access;
    AccessType          menuAccess;
    ;
    setPrefix(funcname());

    info('AssetTable - Access to Fixed Assets');

    while select id, name from userGroupInfo
    {
        setPrefix("@SYS26106");

        securitySet = new SecurityKeySet();
        securitySet.loadGroupRights(userGroupInfo.id, '');

        access = securitySet.tableAccess(tablenum(AssetTable));
        menuAccess = securitySet.secureNodeAccess(menuItemDisplayStr(AssetTable), UtilElementType::DisplayTool);

        if (menuAccess < access)
            access = menuAccess;

        if (access > AccessType::NoAccess)
            info(strfmt("Group %2 - %1 can %3", userGroupInfo.name, userGroupInfo.id, access));
    }

    while select id, name from userInfo
    where userInfo.enable == true
    {
        setPrefix("@SYS25412");

        securitySet = new SecurityKeySet();
        securitySet.loadUserRights(userInfo.id, '');
        access = securitySet.tableAccess(tablenum(AssetTable));
        menuAccess = securitySet.secureNodeAccess(menuItemDisplayStr(AssetTable), UtilElementType::DisplayTool);

        if (menuAccess < access)
            access = menuAccess;

        if (access > AccessType::NoAccess)
            info(strfmt("User %2 - %1 can %3",  userInfo.name, userInfo.id, access));
    }
}

What?  You tried the job and it took forever to run.  Well yeah!  It uses the same method of accessing security as the slow User group permissions form.  How do we solve that?  Another faster job accessing tables which are, well largely undocumented.  The documentation describes the AccessRightsList table:
"Use of this table might lead to an Elevation of Privileges attack or a Denial of Service attack. The Application Object Server (AOS) authorizes each create, update, or delete action on the table by confirming that the current user has permission to perform the requested operation on that table. If the user who initiates the operation is not authorized to perform the operation, an exception will be thrown."
Well it won't hurt to take a peek!  This code actually does the same thing within seconds.

static void listSecurityRights_ElementName_aaaaaaa(Args _args)
{
    AccessRightsList AccessRightsList;
    ;
    while select accessRightsList
    where accessRightsList.elementName == 'CustTable'
    {
        if (accessRightsList.accessType > accessType::NoAccess)
            info(accessRightsList.groupId + ' ' + enum2str(accessRightsList.accessType));
    }
}

Bon appetit!

List AOT Objects

There is a very easy way to get a list of AOT objects using Microsoft Dynamics AX 4.0 without having to write a job iterating over project nodes.

Tools > Development tools > Application objects > Application objects

This form has a dump of everything...it is essentially the UtilElements "table" that the cross reference tables (xRef*) reference. Except this is not a table existing in SQL Server, instead it exists in the memory of the AOS.  The documentation declares "This method enables you to create, read, update, and delete X++ code and metadata." - although the table is not a method this does provide a little insight.  This table is queried by methods such as hasSecuritykeyAccess.

Take some caution when using the form...it acts like a temporary table and if you add too many filters AX will cease up.  I commonly use the form to list projects in one environment and compare them to the list in another by filtering on layer = cu?,us? and record type = SharedProject.

This article only applies to Microsoft Dynamics AX 4.0.

Parameters - When are those records created?

AX seems to create parameters either when the data area is created or when the parameter's form is opened.  So what happens when the parameter table has no form and AX has not created the record...or maybe you had some ambitious person delete the parameter records from the table?  The situation can be remedied using a simple job.  For example this one creates the alert parameter.

static void createEventParameter(Args _args)
{
    EventParameters parameter;
    ;
    Company::createParameter(parameter);
}

Table & Field Numbers - SQLDICTIONARY

Ever wondered how to lookup a table ID using a SQL statement or how tableid2str works?  The hidden table SQLDICTIONARY has the field and table numbers from the shown in the AOT.  It also includes the size and type of the columns.

For the anal retentive you can update data in this table to reorder the columns in the AOT.  They are ordered by table ID and field ID.  Or even better, just create the fields in the order you want them and don't bother modifying a hidden system table.

Seldom when copying data from one environment to another, but not moving the code table IDs and/or field IDs (sometimes added fields were added in different order) were different.  Synchronization fails for those tables.  I have found three ways to remedy it.  Other times you may see a SQL error message when opening the form for the table.

Case A: Similar Columns with different IDs - Offhours
AX is confused because although field MyNewField exists as a date in SQL Server the field with that ID in the SQLDICTIONARY table is a different name, or even a different field type.  The error message is slightly different but both can be solved this way...

  1. Go to SQL and rename the table, i.e. CustTable_OLD
  2. In AX perform compile and synchronize on just the table, i.e. CustTable.  If you do a full compile you run a small risk that the renamed table in SQL will be deleted
  3. Write an INSERT INTO...FROM statement to bring the data back into the table (RecIDs and all) - it will not cause an issue with record IDs.
  4. Delete the previous table, i.e. CustTable_OLD
Case B: Similar Columns with Different IDs - Online
Sometimes you need to fix the table quick and without any delay.  I have in the past been able to change the field or table IDs by modifying the records in the SQLDICTIONARY table.  Just make sure that they are unique and agree with the layer rules, for example IDs coming from the CUS and CUP layers begin at 40000.  This should not require you to restart - just recompile and synchronize the table.

Case C: Development
Delete the table!  you have lost all the data but it is the easiest and fastest way to fix the issue.

Copying AX Environments


After making so many environment copies I like to think I have it down to a science. I have a program which runs SQL scripts from a directory. Here are a few things that I do:
  • Disable all users
  • Clear the user login history
  • Clear the pending outgoing emails
  • Remove all alerts
  • Delete batches
  • Change paths of templates and document handling
  • Set developer compilation options
  • Delete print archive
Add-ons:
  • Clear or secure payroll data and social security numbers
  • Change BottomLine Create!form report parameters

What are the things you do to "initialize" a copied environment?

User is not authorized to insert a record in table SYSSETUPLOG


While doing a routine upgrade of a third-party add-on for Dynamics AX 4.0 I got the following error:

User '____' is not authorized to insert a record in table 'SYSSETUPLOG'. Request denied.

The SysSetupLog table, labeled "Installation information," stores application and kernel version information as well as the status of the System Upgrade Checklist. Therefore it is vital that the AOS service account as well as certain administrators have access to the table. When updating a third-party add-on the version number was changed (a key in that table) thereby triggering the system upgrade checklist. Unfortunately nobody had access to update the checklist items.

Although the cause can not be ascertained this job will allow anyone in the built-in administrators group to update the table.
static void updateAdminGroup_AllowChangeChecklist(Args _args)
{
#Admin
SecurityKeySet securitySet;
UserGroupInfo userGroupInfo;
UserInfo userInfo;
;
setPrefix(funcName());

securitySet = new SecurityKeySet();
securitySet.loadGroupRights(#AdminUserGroup, '');
securitySet.tableAccess(tablenum(SysSetupLog), AccessType::Delete);
securitySet.tableAccess(tablenum(SysSecurityFormTable), AccessType::Delete);
xAccessRightsList::saveSecurityRights(securitySet.pack(), userGroupInfo.id, '');
}