Thursday, February 20, 2014

con2buf crashing AOS

Interesting kernel issue was found today.

Developer added a new string field to the SalesTable table, which already had some custom fields created for other features. Later, a new build was released to test. In the test environment, AOS crashed each time user had to create sales order lines.

User reset his usage data, and that fixed the issue, but we wanted to avoid resetting all usage data for all users in production environment.

Thanks to the Trace Parcer, the real source of the problem was found. Somewhere deep in the sales line creation logic,  SalesTable2LineUpdatePrompt.getLast() method was called. The crash happened when unpacking a sales table buffer from a container with a con2buf function. The fix was to remove all records for this class from the SysLastValue table:

static void fixAosCrash(Args _args)
{
    sysLastValue sysLastValue;
 
    delete_from sysLastValue
        where sysLastValue.elementName == classStr(SalesTable2LineUpdatePrompt);
}

I have no explanation why this happened, and I don't know how to repro that in the demo AX virtual PC, so just be aware of that issue.

Monday, January 27, 2014

Dynamics AX 2012: orig() method fails on derived tables

Just found a bug in the table inheritance feature (kernel 6.0.1108.5781).

If you have a base table A with field B, and a derived table C with field D, then C.orig().B will return a strange value (or nil, depending on the field type).

In order to get the correct value, you will need to downcast C to A, and then call A.orig().B.

This is the job that reproes the issue:

static void reproOrigBug(Args _args)
{
    CompanyInfo     companyInfo;
    DirPartyTable   dirPartyTable;
 
    select firstOnly companyInfo;
 
    info(companyInfo.orig().DataArea);
    info(companyInfo.orig().Name);
 
    dirPartyTable = companyInfo as DirPartyTable;
    info(dirPartyTable.orig().Name);
}

And this is the output:


I will report the issue to MS Support.

Thursday, January 16, 2014

Bug in the source document framework (AX 2012 CU5)

There was an issue in SouceDocumentLineItem class, that I had to fix recently.

The failing scenario looked something like this:
  • Create a direct delivery SO with inter-company chain, with one order line
  • Set the line sales qty to 10
  • Set the line property Completed to No, so it may be partially delivered
  • After the IC chain is there, go to the IC SO and post a packing slip for qty 5
  • Verify that the IC PO is also partially delivered with the same qty
  • Go to the original SO and change the line qty from 10 to 7 (in our customization, this is done in the code)
  • Check the IC PO accounting distributions
In our case, there was one distribution line broken (click to enlarge the picture):


Apparently, the main account for the tax amount was not found.

Because of the broken accounting distribution, the PO could not be processed, and the following error message popped up:

  • One or more accounting distribution is missing a ledger account or contains a ledger account that is not valid. Use the Accounting distribution form or the Posting profile to update the ledger account.
  • The state of the source document or source document line could not be updated.
Pressing Reset button did help, but our customer was not very happy about that workaround, as there were may POs broken like this.

After some hours of debugging, I found that everything starts in this method:


Here, we have a "select forupdate", that loops through TaxUncommitted table records and calls submitSourceDocumentLine instance method:


The taxUncommitted.submitSourceDocumentLine call ends up here:

In this constructor method, the sourceDocumentLineItem class instance is first initialized from the _sourceDocumentLineImplementation parameter (which is actually a TaxUncommitted record buffer), and then put to cache (see SysTransactionScopeCache::set call).

Let's look closer at the sourceDocumentLineItem.initialize method in the debugger. The  sourceDocumentLineItem variable (which is actually TaxSourceDocSublineItem class instance), has a member-variable of Map type, which is initialized with the TaxUncommitted record buffer (selected for update in the while-loop, as we have seen before!):


The problem is that by the time the sourceDocumentLineItem class instance is fetched from the cache later in the business logic, the TaxUncommitted cursor is null, so the taxMap member-variable in the cached instance is empty and things like tax code cannot be determined anymore.

The fix was to use xRecord.data() call to separate the map from the original table buffer:


I will report the issue to MS Support.

Wednesday, December 18, 2013

Table inheritance and collection objects


Be aware that there is an issue with storing child table records in collection objects like List.

If you have a table hierarchy and add a child table record to a List and then try to get it back, information from parent tables is lost, along with InstanceRelationType field value.

The following job reproes the issue:

static void tableInheritanceAndListBug(Args _args)
{
    CompanyInfo     companyInfo;
    List            companyInfoList;
    ListEnumerator  companyInfoEnumerator;
 
    companyInfoList = new List(Types::Record);
 
    select firstOnly companyInfo;
 
    info(strFmt(
        "Orig: RecId: %1, Name: %2, InstanceRelationType: %3",
        companyInfo.RecId,
        companyInfo.Name,
        companyInfo.InstanceRelationType));
 
    companyInfoList.addEnd(companyInfo);
 
    companyInfoEnumerator = companyInfoList.getEnumerator();
    if (companyInfoEnumerator.moveNext())
    {
        companyInfo = companyInfoEnumerator.current();
        info(strFmt(
            "List: RecId: %1, Name: %2, InstanceRelationType: %3",
            companyInfo.RecId,
            companyInfo.Name,
            companyInfo.InstanceRelationType));
    }
}

Output:
Orig: RecId: 5637151316, Name: Contoso Entertainment Systems - E&G Division, InstanceRelationType: 41
List: RecId: 5637151316, Name: , InstanceRelationType: 0


The workaround is to use buf2con function to convert the table buffer to a container, save the container in the list and finally use con2buf when fetching the value with enumerator.

static void tableInheritanceAndListBugWorkaround(Args _args)
{
    CompanyInfo     companyInfo;
    List            companyInfoList;
    ListEnumerator  companyInfoEnumerator;
 
    companyInfoList = new List(Types::Container);
 
    select firstOnly companyInfo;
 
    info(strFmt(
        "Orig: RecId: %1, Name: %2, InstanceRelationType: %3",
        companyInfo.RecId,
        companyInfo.Name,
        companyInfo.InstanceRelationType));
 
    companyInfoList.addEnd(buf2Con(companyInfo));
 
    companyInfoEnumerator = companyInfoList.getEnumerator();
    if (companyInfoEnumerator.moveNext())
    {
        companyInfo = con2Buf(companyInfoEnumerator.current());
        info(strFmt(
            "List: RecId: %1, Name: %2, InstanceRelationType: %3",
            companyInfo.RecId,
            companyInfo.Name,
            companyInfo.InstanceRelationType));
    }
}

Output:
Orig: RecId: 5637151316, Name: Contoso Entertainment Systems - E&G Division, InstanceRelationType: 41
List: RecId: 5637151316, Name: Contoso Entertainment Systems - E&G Division, InstanceRelationType: 41


UPDATE: the bugs is reported to MS Support

Wednesday, October 23, 2013

Debugging update conflicts with X++

Let's say, you need to figure out why an update conflict happens, but there is no way to find that with cross-references (e.g., if doUpdate() method is called somewhere).

Normally, you could use SQL Server Management Studio, while debugging the main logic. Simply set transaction isolation level to "read uncommitted" and periodically check RecVersion field value in the "problematic" record .

However, if there is no access to the SQL Server management Studio, try the following approach:

1. Open another AX client and development workspace.
2. Create a class and add main-method to it.
3. Paste the following code to the main method:
 
public static server void main(Args _args)
 {
     Connection      connection;
     Statement       statement;
     str             query;
     Resultset       resultSet;
 
     connection = new Connection();
     statement = connection.createStatement();
 
     query  = "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;";
 
     query += @"SELECT RecVersion FROM PurchLine                "; 
     query += @"    WHERE PurchLine.InventTransId = N'11199055' ";
     query += @"      AND PurchLine.dataAreaId    = N'CEU'      ";
 
     new SqlStatementExecutePermission(query).assert();
 
     resultSet = statement.executeQuery(query);
 
     while (resultSet.next())
     {
         info(resultSet.getString(1));
     }
 
     CodeAccessPermission::revertAssert();
 }

4. Adjust the query string as needed.
5. Go through the main logic in the debugger and periodically run this class in the second workspace, so you will know if the RecVersion value is still the same.

Wednesday, September 18, 2013

Why reading whitepapers may be useful

If you don't know how to find a list of values behind a DefaultDimension in AX 2012, Google will most likely get you to the following solution:

static void DEV_Dimension(Args _args)
{
    CustTable                         custTable = CustTable::find("1101");
    DimensionAttributeValueSetStorage dimStorage;
    Counter i;

    dimStorage = DimensionAttributeValueSetStorage::find(custTable.DefaultDimension);

    for (i=1 ; i<= dimStorage.elements() ; i++)
    {
        info(strFmt("%1 = %2", DimensionAttribute::find(dimStorage.getAttributeByIndex(i)).Name,       
                               dimStorage.getDisplayValueByIndex(i)));
    }
}


The solution above is copy-pasted all over the AX segment of the Internet.

However, there is a better way to do that:

static void ShowDimensionInOneSelect(Args _args)
{
    DefaultDimensionView defaultDimensionView;
    CustTable                       custTable;

    while select defaultDimensionView
        exists join custTable
            where custTable.AccountNum == "1101"
               && custTable.DefaultDimension == defaultDimensionView.DefaultDimension
    {
        info(strFmt("%1 = %2", defaultDimensionView.Name,
                               defaultDimensionView.DisplayValue));
    }
}

This and some other useful techniques are well described in Implementing the Account and Financial Dimensions Framework white paper.

Thursday, May 30, 2013

Generate a project with application objects from "used by" cross-references

Sometimes one needs to investigate where some field or method is used in the AOT. There may be a long list of AOT paths in the "Used by" form, and having a project with all those application elements may be a good starting point.

Add a button to xRefReferencesUsedByTypedTree form, set its Text property to "Create project" and MultiSelect to Yes:


Override its clicked method and use the following code:

void clicked()
{
    #TreeNodeSysNodeType
 
    XrefPaths xRefPathsLocal;
 
    Set treeNodeLocalPaths;
    SetEnumerator treeNodeLocalPathEnumerator;
 
    SysProjectFilterRunBase     project;
    UtilElements                utilElements;
    ProjectNode                 projectNode;
 
    Dialog                      dialog;
    DialogField                 dialogField;
 
    super();
 
    treeNodeLocalPaths = new Set(Types::String);
 
    for (xRefPathsLocal = xRefPaths_ds.getFirst(true) ?
        xRefPaths_ds.getFirst(true) : xRefPaths_ds.cursor();
        xRefPathsLocal;
        xRefPathsLocal = xRefPaths_ds.getNext())
    {
        treeNodeLocalPaths.add(xRefPathsLocal.Path);
    }
 
    if (treeNodeLocalPaths.elements() == 0)
    {
        info("There are no AOT objects to create the project from.");
        return;
    }
 
    dialog = new dialog("Create project");
    dialogField = dialog.addFieldValue(
        extendedTypeStr(ProjectName), 
        'UsedByProject');
 
    if (dialog.run())
    {
        projectNode = SysTreeNode::createProject(any2str(dialogField.value()));
 
        project = new SysProjectFilterRunBase();
        project.parmProjectNode(projectNode);
        project.grouping(SysProjectGrouping::AOT);
 
        treeNodeLocalPathEnumerator = treeNodeLocalPaths.getEnumerator();
        while (treeNodeLocalPathEnumerator.moveNext())
        {
            utilElements = xUtilElements::findTreeNode(
                treeNode::findNode(
                    SysTreeNode::applObjectPath(
                    treeNodeLocalPathEnumerator.current())),
                false);
 
            if (utilElements.RecId != 0)
            {
                project.doUtilElements(utilElements);
            }
        }
 
        project.write();
    }
}

Now, if you select one or more records in the "used by" grid and press the button, a new project will be created: