Friday 30 October 2015

Multi select lookup on form field

We have ready-to-use lookups in system.
Unfortunately we can choose only one item from such lookup.
What if we need to select more than one element?

We have to use class SysLookupMultiSelectCtrl

I've prepared an example how to use this on AOT form, but there is no issue to use it on dialog field from SysOperation Framework.
You must remember that it can be used when dialog is already build, so on SysOperation framework it can be used on method postBuild or postRun.

It's not so easy to use it on RunBase classes or when you're trying to build, because you have to overload a method lookup for that dialog field. Overloading method can be done only once, and this SysLookupMultiSelectCtrl is overloading it once again, it cause runtime errors.
If you really need to have such multilookup on Runbase dialog you have to copy some pieces of code from SysLookupMultiSelectCtrl and overload lookup method.

So....this example is for form fields, and last line from init method can be transfered to postBuild() method on SysOperation framework to UIBuilder class.

I've created very simple form with one field ItemId



I've changed two properties on that field
Autodeclaration: Yes
ExtendedDataType: ItemId

You don't have to put EDT on this form control. You'll receive multilookup anyway.
I've used EDT to have proper label, help text and size of that control.

Remember that EDT ItemId has limitation of lenght, so it may be not long enough to store more than 1-2 item Ids! It's just an example

You have to modify three method on form

classDeclaration




init



close



Instead of modifying close method, you can add some button on our form.
I'm using close method to catch value from our ItemId control and display this value.

This is how our lookup looks like


When we press OK on lookup, we will get chosen values to our field



After form closing, we will receive a message with chosen values.




Tuesday 25 August 2015

Unretrieved fields

Sometimes there might be a problem with new field just added to a table.

There is no possibility to put a value to such field on form, neither in Table Browser.
Unretrieved value is visible, and such field is grayed out.

There are few steps which may help, but the bad news is that, that all those steps may not help and at the end you'll have to restart AOS (it worked always for me).
So, if you have this issue with "Unretrieved" fields on your DEV or TEST machine and you can restart it.....just do it.

So, there are few steps which may help.
1. Sychronize this table
2. Restore form where is this field. (right click on form from AOT and then -> Restore)
2. Clean all cache (Dev enviromnent, Tools\Caches\...)
3. Close AX client and delete all .auc files (C:\Users\YOURUSER\AppData\Local\Microsoft\Dynamics Ax\VSAssemblies{xxxxxxxxxxx\


There is one known additional issue regarding this subject. 
This new field may be visible on form and usable, but all select statement will return blank/null or (even worse) default value of Enum from that field, if that field is a type of Enum.
This last thing is the worst scenario in my opinion, because it's very hard to find why select statement is fetching wrong data.

Friday 14 August 2015

FilePath folder lookup does not work. Why?

There is a new field on our table. Field bases on Extended Data Type FilePath.

This fields has been added to a form. We can see an automatic lookup button. It looks like a folder button.



The issue is, that this folder button does not work. We can click on it, but there is no new window when we can choose a folder to store files.

The "magic trick" is, that we have to add to method node of our form new method
Metod name is filePathLookupTitle

This method returns some string.

Example

str filePathLookupTitle()
{
    // Archive directory
    return "@SYS26605";
}

The good example is on system form DocuType


Tuesday 11 August 2015

New SSRS instance. How to deploy all reports

We may have more then one SSRS instance on our server.

Every time when we will create such instance we have to create new configuration in our AX and create new report folder (also from AX)

System administration\Setup\Business intelligence\Reporting Services\Report servers
There is a button for creating a new report folder.

This folder will be empty.

We can deploy every report from AOT one by one: right click on report and position Deploy

It's much faster to use AX PowerShell for that purpose.
It has to be run from server where Reporting Services are installed.
There should be also installed a part of Dynamics AX, to have AX PowerShell and Dynamics AX Configuration. PowerShell will use default config set in AX Configuration tool.

We have to export AX configuration to have .axc file with stored information about that config. This file .axc has to be renamed to Microsoft.Dynamics.AX.ReportConfiguration.axc
When name of that file will be changes, we have to copy that file to folder with our new SSRS instance.
Default folder is usually:
C:\Program Files\Microsoft SQL Server\OurInstanceName\Reporting Services\ReportServer\bin

After that we have to run Microsoft Dynamics AX 2012 Management Shell (our Powershell with AX extensions)

command to use Publish-AXReport -ReportName *

Instead of '*' we can put a name of report which we want to deploy, or we can put for example S* - it will deploy all reports with names starting from S (SalesInvoice for example).

Such deploy process started from Shell is much faster then deploying from AOT.

Friday 31 July 2015

How to get Cust account or Vend account from ledgerJournalTrans?

LedgerJournalTrans may keep information about few types of account.
Two of them may be information about customer and vendor account.

There are two ways to find a CustTable record or VendTable record from ledgerJournalTrans.

First option:
Harder but it will show how does it work.

At the beginning it's good to know that this information is stored in LedgerJournalTrans in field LedgerDimension

So before you'll start searching vendor or customer, it would be good to check what type of account is stored in LedgerJournalTrans.
You will now that from field AccountType.

LedgerDimension stores an info about record of table DimensionAttributeValueCombination
And this last table has a field which may be interesting for us: DisplayValue

So, let's make a job which will give us a record from CustTable or from VendTable

static void accountHardWay(Args _args)
{
    LedgerJournalTrans                      ledgerJournalTrans;
    DimensionAttributeValueCombination      combination;
    CustTable                               custTable;
    VendTable                               vendTable;
    
    //I assume that we already have a record 
    //from ledgerJournalTrans
    
    combination = DimensionAttributeValueCombination::find(ledgerJournalTrans.LedgerDimension);
    
    if (combination)
    {
        if (ledgerJournalTrans.AccountType == LedgerJournalACType::Cust)    
        {
            custTable = CustTable::find(combination.DisplayValue);    
        }
        else
        {
            if (ledgerJournalTrans.AccountType == LedgerJournalACType::Cust)    
            {
                vendTable = VendTable::find(combination.DisplayValue);    
            }                
        }
    }

}



Second Option:
For dummies :)

ledgerJournalTrans has a method accountDisplay()

static void accountEasyWay(Args _args)
{
    LedgerJournalTrans                      ledgerJournalTrans;
    CustTable                               custTable;
    VendTable                               vendTable;
    
    //I assume that we already have a record 
    //from ledgerJournalTrans

    if (ledgerJournalTrans.AccountType == LedgerJournalACType::Cust)    
    {
        custTable = CustTable::find(ledgerJournalTrans.accountDisplay());    
    }
    else
    {
        if (ledgerJournalTrans.AccountType == LedgerJournalACType::Cust)    
        {
            vendTable = VendTable::find(ledgerJournalTrans.accountDisplay());    
        }                
    }

}

Monday 27 July 2015

Run as different user. Strange behaviour

Dynamics AX shortcut may start different AX session for your currently user, and for user which has been chosen from menu "Run as different user"



This is because Dynamics AX is starting with default configuration which has been set in Dynamics AX Configuration utility. 
This configuration is set separately for every single user from that Windows system when Dynamics AX Client is installed.

So, when you're starting Dynamics AX for different user, but from your user session (Run as different user) it takes AX configuration from that different user, not from yours.

May be obvious, but sometimes it can be a little surprise.

Tuesday 14 July 2015

Catch insert, update or delete. Database log

Sometimes there is a need to find out, which class or other element in system is inserting, updating or deleting a record in a table which is interesting for us.

The easiest way is just to put a breakpoint on insert(), update() or delete() method of that table.
If there is no such method, it has to be added of course.
When some piece of code will call a method on table, we will see a full stack, and probably a reason why this table has been modified and where.

An example of insert() method on SalesTable and a breakpoint at the beginning.


The problem is, when a record is inserted/updated/deleted using doInsert(), doUpdate(), doDelete().
In that case, such breakpoint on table method will not work.

To find out what changes data in our table, we have to put a breakpoint in class Application
There are many methods, but for us, there are three interesting: logDelete(), logInsert() and logUpdate().

And in one of them we should put a breakpoint.

An example of breakpoint on logInsert()


But it's not enough just to put a breakpoint.

We have to go to 'System administration\Database\Database log setup'.
There will be a form with listed tables for which system is saving a log of changes and a type of those changes (Insert/Update/Delete).

Let's say that we want to find when a record is inserted to a table SalesTable using doInsert() method.

We have to press "New" button on Database log setup form
It will start a Wizard.
Just press Next.
It will take a while to build a table structure.
In my opinion it would be much more useful, just to see a list of tables with their AOT names and without any structure. Much faster, that's for sure, because this window with structure built for the first time appears after about 10 minutes! 

Please remember, that you will see a list of tables sorted by modules, and you will see their printed names. It's important because I had two AX, both in English language, but one was in UK English, and one in US English. And of course those tables had different printed names.

So, I need to catch a doInsert on SalesTable, so I have to choose a table "Sales orders"

Press Next

On next window please tick a checkbox "Track new transactions" for row with table name "Sales orders". There may be only one row, but on older working environments there may be more



Press Next, and on next (last) window of that Wizzard, please press Finish.

There was a breakpoint few lines above on that blog on logInsert() method of Application class.

So, after this database log setup has been made for SalesTable, a breakpoint will stop a trace, and it will be ease to find out what process has inserted a record to our table.


Monday 13 July 2015

Infolog levels. Setprefix and other tricks.

Sometimes we have to display some kind of treeview in info message.
We need to make somehow sub-level of infolog.

To do that we can use a setPrefix() build-in function.

As an example:

setPrefix ('A new node of infolog');
info('A sub node of infolog');

Instead of using setPrefix(), we can just use a tab '\t' to format our message

info(strFmt('A new node of infolog%1A sub node of infolog', '\t'));

Result in both cases will be the same

To have a possibility to go back to level 1, we have to use another methods, sub methods or loop statements.

So, before calling our method or loop we have an infolog level X, and after this method or statement we will have exactly the same level X of our infolog.

Please have a look in that code below. This will produce few levels of our message, and sometimes it will get back to previous level or dig deeper.

I'll use sub method in job and for loop to show how does it work.


static void Job201(Args _args)
{
    Counter i, k;
    
    void sendMessage(str _message, str _prefix = '')
    {
        if (_prefix)
        {
            setPrefix(_prefix);    
        }
        
        info(_message);
    }
    
    
    //A prefix on the very top of infolog window
    setPrefix('Build a tree');
    
    //Two lines on first level of infolog. 
    //Just to show that two of them will be on the same level
    info('Just a message on first level');
    info('Another message on the same level');    
    
    //Sub method of that job used to display sub node of 
    //that infolog and display a message 
    //under that subnode
    sendMessage('This is message on sub level 2', 'Sub level 1 prefix');
    
    //When trace went out from submethod 'sendMessage' 
    //infolog level is again on level one!!
    //Be careful. In next line there will be 
    //a little trick to display another message 
    //on sub level 2from 'sendMessage' submethod. 
    //so this line above will be displayed after 
    //this all sublevel 2. 
    //Infolog will change an order of those lines
    info('This should be a message on level 1. Where it has been displayed? :)');    
    
    //You can use a tab '\t' instead of setPrefix().
    //When you'll place exactly the same string 
    //as a prefix as it has been previously used
    //an Infolog will put not create new node, 
    //but will just put a message to previous prefix node 
    //with the same name
    info(strfmt('Sub level 1 prefix%1This is another message on sub level 2', '\t'));
    
    //and again, another sublevel 2 node, 
    //but different prefix
    //so it will be displayed as separate subnode.
    info(strfmt('Another sub level 1 prefix%1This is another message on sub level 2', '\t'));    
    
    //and third level of infolog. 
    //Just used a tab '\t' to dig there.
    info(strfmt('Another sub level 1 prefix%1This is prefix message on sub level 2%1and message on sublevel 3!', '\t', '\t'));      
    
    //and we are getting back to level 1.
    info('This should be a message on level 1');
    
    //Let's dig one level deeper.    
    setPrefix('Well, level 2 before loop');
    
    //we can use a loop instead of subMethod.
    //It will do the same for us, just close 
    //a prefix level in one statement
    //and outside of that statement infolog level will 
    //go back to level before loop.    
    
    for (i = 1;i <= 2;i++)
    {
        //next level of prefix. This will be level 3   
        setPrefix('Level 3 inside of loop');
        
        for(k = 1; k <= 2; k++)
        {
            info('Message on level 4');    
        }
    }
    
    //and after a loop, Infolog will display 
    //this message on level 3
    //because before a loop there was a setPrefix statement
    info('This should on level 3');
}

This is how it will look like after that



Thursday 9 July 2015

COM error: invalid number of parameters

Well, I've met last days a strange error message when I wanted to import data from Excel.
I'm not sure (haven't tested that) but the same problem may occur with other applications connected by COM (Outlook for example).

Error executing code: The method has been called with an invalid number of parameters

The problem was, that there was good number of parameters.

Method looked like that


    SysExcelApplication excelApp;
    SysExcelWorkbooks   excelWorkbooks;
    SysExcelWorksheets  excelWorksheets;
    SysExcelCells       excelCells;
    SysExcelWorksheet   excelWorksheet;
    ComVariant          cellValue;

....

    //line calling error
    cellValue   = excelCells.item(row, col).value();


....


I found workaround of that problem, which is just a....workaround, and nothing more. The problem is still there, and probably has to be fixed by Microsoft.


The main issue is, that this problem is not possible to replicate every time. Sometimes it is, and sometimes not.

I've added a line just before this line which is responsible for that error

     infolog.yield();

Yes, that's it!

There is another workaround for that which works, but I think that it does not give a guarantee that it will work - how can you know that 50 (or another number) will be enough?
There might a coincidence that it will work after 51st iteration.

Just add an error counter with maximum value, let's say 50.
After that, run this problematic line in try catch statement, iterating as many times as code will reach a limit gave as maximum (hardcoded)

Something like that

  Counter limitCounter;

  try
  {
    cellValue   = excelCells.item(row, col).value();
  }
  catch(Exception::Error)
  { 
     limitCounter++;

     if(limitCounter <= 50) //hardcoded!!
        retry;
     else
        throw(Exception::Error);
  }