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;
}

Tuesday, December 11, 2012

When good habits matter

Ever wondered why

if (x == 1)
{
    y = 2;
}
 

is better than

if (x == 1)
    y = 2;

?

Find a bug in Dynamics AX 2012 RTM then:

Friday, November 30, 2012

Value cannot be null. Parameter name: x

Don't get confused, when trying to find where this error message was thrown: "Value cannot be null. Parameter name: x". Especially, when this is something thrown during AIF processing.

You might suggest that this is a label and you do find the label @SYS91439, but still you have no idea what is going on, e.g. no breakpoints with this label are hit.

So, it may well be something thrown by an arithmetic operation, which was passed a null value.


Another good reason to give meaningful names to variables and check for null values.

Sunday, November 4, 2012

Computed columns in Views by example

There is a bug in the standard AX, in the query named AxdPurchaseRequisition. The problem is here:


PurchLineAllVersions datasource is based on a View object, so PurchLineAllVersions.TableId contains table ID of the view itself, and not that of the underlying tables. As a result, no DocuRefTrans records will be found with this quiry.

The issue may be fixed by using computed columns in views - the new feature in AX 2012.

As you may already know, the PurchLineAllVersions view is based on the quiry PurchLineAllVersions, which, in turn, is a union of 2 other views: PurchLineArchivedVersions and PurchLineNotArchivedVersions.

First, add a method to the 2 views.

PurchLineNotArchivedVersions:

public static server str purchLineTableId()
{
    return SysComputedColumn::returnLiteral(tableNum(PurchLine));
} 

PurchLineArchivedVersions:

public static server str purchLineTableId()
{
    return SysComputedColumn::returnLiteral(tableNum(PurchLineHistory));
}

Add a column to the 2 views: right-click on the Fields, then "New -> Int Computed Column". Set the new fields' properties like that:


Then, add new PurchLineTableId fields to the PurchLineAllVersions query datasources and the PurchLineAllVersions view.

Finally, change the faulty link in the AxdPurchaseRequisition quiry, so that it will connect PurchLineAllVersions.PurchLineTableId and DocuRefTrans.RefTableId.

By the way, there is another, better example of how computed columns may be used: take a look at the InventValue* views, which are used by the Inventory Value report.

Thursday, November 1, 2012

xRecord.setTmp() and inactive configuration keys

Be carefull when using setTmp method on table buffers. If your table is assigned a configuration key, turning it off will make this line of code throw an error at runtime: "Table '<table name>' of type TempDB cannot be changed to table of type InMemory".

For example, \Classes\SysRecordTemplateEdit\setFormDataSourceRecord has this problem. It loops through form datasources and tries to set them all to InMemory withouth checking first if they are TempDB or not.

In order to avoid this error, you can use the following construction:

    if (!myTable.isTempDb())
    {
        myTable.setTmp();
    }

More about TempDB tables on MSDN.

Exporting to Excel with Microsoft.Dynamics.AX.Fim.Spreadsheets.* classes

While looking for a way to export to Excel in batch, I investigated what they do in financial statements (LedgerBalanceSheetDimPrintExcelEngine class).

Pros:
  • It is possible to use these .NET components in batch on the server side
  • Execution is way faster than that of the SysExcel* classes

Cons:
  • It looks like it is not possible to create more columns than there are letters in the English alphabet
  • Less flexible comparing to standard SysExcel classes, so manual formatting will likely be needed in the end. 
static void AnotherWayToExportToExcel(Args _args)
{
    #define.ReadWritePermission('RW')
    #define.FileName('c:\myFile.xlsx')
    #define.ExcelColumnWidth(15)
    #define.ExcelCellFontSize("Microsoft.Dynamics.AX.Fim.Spreadsheets.CellFontSize")
    #define.Size9("Size9")    
 
    CustTable custTable;
 
    Microsoft.Dynamics.AX.Fim.Spreadsheets.Spreadsheet spreadsheet;
    Microsoft.Dynamics.AX.Fim.Spreadsheets.CellProperties cellProperties;    
    Microsoft.Dynamics.AX.Fim.Spreadsheets.ColumnProperties columnProperties;    
 
    void addColumn(str _name)
    {
        columnProperties = new Microsoft.Dynamics.AX.Fim.Spreadsheets.ColumnProperties();
        columnProperties.set_Width(#ExcelColumnWidth);
        spreadSheet.InstantiateColumn(columnProperties);
 
        cellProperties = new Microsoft.Dynamics.AX.Fim.Spreadsheets.CellProperties();
        cellProperties.set_FontSize(CLRInterop::parseClrEnum(#ExcelCellFontSize, #Size9));
        cellProperties.set_Bold(true);
 
        spreadSheet.AddStringCellToWorkbook(_name, cellProperties);
    }    
 
    new FileIOPermission(#FileName, #ReadWritePermission).assert();
 
    spreadSheet = new Microsoft.Dynamics.AX.Fim.Spreadsheets.Spreadsheet();
 
    if (!spreadSheet.CreateSpreadsheet(#FileName))
    {
        throw error(strFmt("@SYS72245", #FileName));
    }
 
    addColumn("Customer name");
    addColumn("Balance");    
 
    while select custTable
    {
        spreadSheet.MoveToNextRowInWorkbook();        
 
        cellProperties = new Microsoft.Dynamics.AX.Fim.Spreadsheets.CellProperties();
        cellProperties.set_FontSize(CLRInterop::parseClrEnum(#ExcelCellFontSize, #Size9));        
 
        spreadSheet.AddStringCellToWorkbook(custTable.name(), cellProperties);            
        spreadSheet.AddNumberCellToWorkbook(real2double(custTable.openBalanceCur()), cellProperties);                    
    }
 
    spreadSheet.WriteFile();
    spreadSheet.Dispose();
 
    CodeAccessPermission::revertAssert();    
}

Output:

Wednesday, October 17, 2012

Manupulating forms from code - continued...

After publishing a post about manipulating the Marking form from code, I got some feedback (special thanks to Maxim Gorbunov).

I would like my readers to remember, that forms should never be used like that in business logic executed on a daily basis. If this is something that should be run more often than once, this logic should preferably be run on the server, and in the case with the Marking form, it should rather be TmpInventTransMark::updateTmpMark() method called directly. You will need to prepare parameters for it, though, so you will first need to figure out how the form does that.

In other words, please use out-of-the-box solutions you find on the Internet with care.