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:

 

Friday, March 22, 2013

Mandatory fields and table hierarchies

If you have a table hierarchy, and there is a mandatory field in the child table, your data may become inconsistent in some cases.

For example, there are 2 tables (TestDog extends TestAnimal):

 
 
Let's make TestDog.OwnerName field mandatory and create a record in TestDog with blank OwnerName:
 
 
A warning is shown, which is correct. Now let's close the form. Normally, you would expect a dialog "Changes have been made in the form. Save changes? Yes/No". But in our case, the records are saved in both tables, with a blank mandatory field in TestDog.

Tuesday, January 8, 2013

Marking from code: part 2

In this post, I provided a code sample that manipulated InventMarking form from code to complete an ad-hoc task.

Today I had to implement marking from X++ code again, but this time it should have been run on the server side on a daily basis, so I did have to use
\Data Dictionary\Tables\TmpInventTransMark\Methods\updateTmpMark
method instead of the form.

As code samples I found in the Internet were outdated, I decided to post here code that did compile on my box (DAX 2012 CU3):

static void testXppMarking(Args _args)
{
    InventTrans issueInventTrans;
    TmpInventTransMark tmpInventTransMask;
    Map mapMarkNow;
    container con;
    real qty;
    Map mapTmp;
    MapEnumerator mapEnumerator;
 
    InventTransOriginId issueInventTransOriginId = 
        InventTransOrigin::findByInventTransId('Issue lot ID').RecId;
 
    InventTransOriginId receiptInventTransOriginId = 
        InventTransOrigin::findByInventTransId('Receipt lot ID').RecId;    
 
    InventQty qtyToMark = 11;
 
    ttsBegin;
 
    issueInventTrans = InventTrans::findByInventTransOrigin(
        issueInventTransOriginId);
 
    [con, qty] = TmpInventTransMark::packTmpMark(
        InventTransOrigin::find(issueInventTransOriginId),
        issueInventTrans.inventDim(), 
        issueInventTrans.Qty);
 
    mapTmp = Map::create(con);
    mapEnumerator = mapTmp.getEnumerator();
    while (mapEnumerator.moveNext())
    {
        tmpInventTransMask = mapEnumerator.currentValue();
 
        if (tmpInventTransMask.InventTransOrigin == receiptInventTransOriginId)
        {
            tmpInventTransMask.QtyMarkNow = qtyToMark;
            tmpInventTransMask.QtyRemain -= tmpInventTransMask.QtyMarkNow;
            mapMarkNow = new Map(Types::Int64, Types::Record);
            mapMarkNow.insert(tmpInventTransMask.RecId, tmpInventTransMask);
 
            TmpInventTransMark::updateTmpMark(
                issueInventTransOriginId, 
                issueInventTrans.inventDim(), 
                -qtyToMark,
                mapMarkNow.pack());
 
            break;
        }
    }
 
    ttsCommit;
}