Showing posts with label AX 2012. Show all posts
Showing posts with label AX 2012. Show all posts

Wednesday, March 27, 2019

Common mistake, when creating Purchase Orders from X++

I would like to warn my fellow Axapta/AX/D365FO bloggers, that blind copy-pasting from each other seems innocent, but it fact may result in spreading bugs.

E.g., the following piece of code is very popular in "How to create Purchase Order from X++" posts.

numberSeq = NumberSeq::newGetNumFromCode(purchParameters::numRefPurchaseOrderId().NumberSequence,true);

Purchtable.PurchId = numberSeq.num();

The problem is numRefPurchaseOrderId returns the number sequence for purchase order confirmation number. For purchase order number, numRefPurchId should have been used. Using the wrong number sequence may result in duplicate key issues.

Tuesday, November 29, 2016

Monday, October 3, 2016

Bugs in the DateTimeUtil::getSystemDateTime()

After the current system date is changed, either from the user interface or via systemDateSet function, the DateTimeUtil::getSystemDateTime() goes out of control. Don't ever use this function for unique keys generation.

Unfortunately, they use it a lot in the DIXF.

static void printDateTimeJob(Args _args)
{
    void printDateTime()
    {
        info(strFmt('systemDateGet: %1 %2', 
            systemDateGet(), 
            time2StrHMS(timeNow())));
        info(strFmt('getSystemDateTime: %1', DateTimeUtil::getSystemDateTime()));
        info(strFmt('utcNow: %1', DateTimeUtil::utcNow()));
    }
 
    warning('Before date/time change');
 
    printDateTime();
 
    sleep(2000);
 
    info('...2 seconds later:');
 
    printDateTime();
 
    systemDateSet(systemDateGet() - 3);
 
    warning('System date changed:');
 
    printDateTime();
 
    sleep(2000);
 
    info('...2 seconds later:');
 
    printDateTime();
 
    systemDateSet(systemDateGet() + 3);
 
    warning('System date is back:');
 
    printDateTime();
 
    sleep(2000);
 
    info('...2 seconds later:');
 
    printDateTime();
}



And this is the result:


P. S.: Kernel version 6.3.4000.1745, Application version 6.3.3000.110

Monday, April 4, 2016

Error executing code: The field with ID '0' does not exist in table 'SysExtensionSerializerExtensionMap'.

If you ever get "Error executing code: The field with ID '0' does not exist in table 'SysExtensionSerializerExtensionMap'." error in CIL, it may happen it is the table extension framework + the standard buf2buf function to blame.


Somehow, if you pass a Map instead of a table to buf2buf function, the following line fails:

_to.(fieldId) = _from.(fieldId);


However, if you replace this line with:


fieldName = fieldId2name(_from.TableId, fieldId);
 _to.setFieldValue(fieldName, _from.getFieldValue(fieldName));


everything is fine both in X++ and in CIL.


P.S. I didn't dare to change the standard buf2buf method. Istead, I created another method and used it in the table extension framework logic, which failed because of the issue (\Data Dictionary\Maps\SysExtensionSerializerMap\Methods\copyExtensionTableData)


P.P.S. There are a couple of other SysExtensionSerializerMap methods that should be fixed, as they call buf2buf too.


Tuesday, March 8, 2016

DIXF: wrong field values after sales order import?

The more I work with the Data Import-Export Framework in AX 2012 R3, the more fun I have.

Never ever put an initValue() method call after any initFrom<some table name> method.


Thursday, December 10, 2015

New release of the CGI Code Review Tool for Dynamics AX

I am happy to announce the new release of the CGI Code Review Tool for Dynamics AX.

Only AX 2012 version is supported this time, but should you need an AX 2009 version, please let me know and I will prepare and upload an extra XPO.

In earlier releases of the tool, user might see that comparison tool didn't show the exact change for overlayered objects in some cases. This has been fixed now.

Enjoy.

Tuesday, October 27, 2015

When fields lists don't work as expected

Did you know that a field list in a single-record select-statement is not always beneficial in comparison to a select firstonly * from...  (or a static find-method)?

The thing is, when a where-clause matches a unique index (in AX 2009 – a primary index), the AOS may find the complete record in the cache and return it. And if this is the very first call, it will pull the whole record anyway, in order to be able to cache it. This may be proved by debugging.

So please use find-methods where possible, because they are easier to read and maintain. And remember: a while select with a nested find-method call may actually perform better than a join – thanks to caching. Therefore do measure performance to confirm that your select-statement is the most optimal one.

Tuesday, October 20, 2015

Table extension framework

There is a great article on how to use the table extension framework in AX 2012.

Keep in mind that if the parent table's primary key is not RecId-based, but the table has CreateRecIdIndex property set to Yes, then the relation on step #1 should be created based on "Single field AlternateKey based"; otherwise the new feld will potentially have wrong type, and it will not be possible to use it in the SysExtensionSerializerExtensionMap map.

And remember to add a DeleteAction to the parent table.

Tuesday, June 2, 2015

Which query range should I select?!

Customer asked me to figure out which "Customer account" should be selected in a query. There were three of them on the Customer invoice journal table:


Apparently, there were a couple of fields that used the default label, and I found them in the following way.

Selected the first "Customer account" range:


... right-clicked in the field value and selected Record Info:


... in the dialog, clicked on the Script button:


... pasted the clipboard to Notepad:


The extended field ID was 65540.

Then, I selected another range:


... and repeated the process. The second field ID was 81625. Finally, I selected the third Customer account range and found that it was based on the field ID 81626.

So, the three field IDs for CustInvoiceJour table were found. But what were the field names?

static void printFieldNames(Args _args)
{
    print fieldId2Name(tableNum(CustInvoiceJour), fieldExt2Id(65540));
    print fieldId2Name(tableNum(CustInvoiceJour), fieldExt2Id(81625));
    print fieldId2Name(tableNum(CustInvoiceJour), fieldExt2Id(81626));
    pause;
}

And the output was:

Somebody did not follow the Best Practices for Table Fields:






Friday, September 26, 2014

CGI Code Review Tool for Dynamics AX

I am happy to announce the first release of a code review tool developed by CGI Group Inc.
The tool has been installed and successfully used at multiple customer project, both AX 2009 and 2012.

It has been decided to share the tool with the rest of the community, so you are very welcome to get it at CodePlex.

Any feedback is more than welcome.

Friday, May 9, 2014

AX Content: Create your own PDF guides from TechNet content

TechNet has added a great feature that you can use to create custom PDF guides from topics.

Enjoy.

P. S. The first thing I have done after reading the article by AX Support was export Best Practices for Microsoft Dynamics AX Development [AX 2012] and create a reading plan. Reading them online was unbearable.

 

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:

 

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.