domingo, 30 de octubre de 2011

The 'Washed' Layer

When opening up the Compare tool on a modified object we are able to compare the sys layer with our modifications in the var or usr layer, for example. 


There is another option available called the washed layer, as explained from Inside Microsoft Dynamics™ AX 4.0:

A few simple best practice issues can be resolved automatically by a best practice “wash.” Selecting the washed version shows you how your implementation differs from best practices. To get the full benefit of this, select the Case Sensitive check box on the Advanced tab.


Note that if it is an object that we created and is not part of the original framework then the Washed option will be the only one available!  We usually prepend the company name or other three character letters to all objects that we create in the AOT and so I was confused at first why my object had no sys layer.

martes, 25 de octubre de 2011

Google -> MSDN

If like me you are at times lost floating in the ocean of Ax you will find the Google/Bing search engines extremely useful for answering your doubts.  By prepending 'axapta msdn' to the queries (the 'ax' term isn't as good, imho) we can usually jump straight into the API we are looking for.  One of the frustrating things with this approach, however, is that you are probably using a different version of Ax than the page that we have landed on.  The class structure hasn't changed too much it's true but we have more confidence in reading the correct version of the documentation.  Searching for ReportSection for example we could land in any of the following:
My understanding is that AX 2012 is now 'released' and I think it's a pity that we can't just change to a 'aa621385(AX.60).aspx' to jump straight into AX 2012 should we want to...  A lot of the links I select from Google go straight to Axapta 4.0.

As a final note MSDN has this 'Language Filter' option which is useless as we are all X++ gurus.  I wonder if it would be far more useful to be able to select a filter on the Ax version that you work with, where a redirection occurs when we first arrive should the equivalent page exist.

lunes, 17 de octubre de 2011

Error 112


Amusingly I've just read someone else's blog post about the reason for error 112...  You can never find blog posts the second time around, can you?  So in the end it looks like at least two of us on the planet have just run out of hard disk space!  It could also be caused by having read-only access to the AOT directory.

viernes, 14 de octubre de 2011

localmacro insanity

Sometimes writing a piece of legible and easily understood code just isn't enough. We have to obfuscate it all in the name of reusability. We have the Business Relationship form, which is as permissive an entity as you can get, and needed to 'separate' it between two types of relationships - legal and people. The client will see two designs and consequently will require two sets of business rules such as obligatory mandatory fields. All this of course generated from the same form.

Creating mandatory form fields on-the-fly requires us to both set the mandatory field to true on the datasource as well as physically adding code to the validateWrite method (link). For our particular use case we did this at form level rather than at the smmBusRelTable entity as we are bulk importing over 150,000 rows and the form will be the only point of access to the data. Despite all of that we must always try to put the buisiness logic at the table level if at all possible.

First of all in the init method of the form we could write the following piece of code:
smmBusRelTable_ds.object(dt.fieldName2Id('Name')).mandatory(true);
We would then repeat the code for the other fields depending upon the relationship type. However... We could use an inner function which would then avoid some repetition and would not confuse unfamiliar developers to the code base:
    DictTable dt = new DictTable(smmBusRelTable.TableId);
    
    void mandatory(str fieldName)
    {
        smmBusRelTable_ds.object(dt.fieldName2Id(fieldName)).mandatory(true);
    }
    ;
    
    switch (personTypeParam)
    {
        case EVE_PersonType::Organization:
            mandatory('Name');
            mandatory('NameAlias');
            mandatory('Phone');
            mandatory('EVE_DocumentType');
            mandatory('EVE_DocumentId');
            mandatory('EVE_DUPCI');
        break;

        case EVE_PersonType::Person:
            // More fields
        break;
    }
That isn't actually too bad. However as I indicated before we now need to perform checks in the validateWrite part of the form's smmBusRelTable datasource. It's not a real problem in my case as there are only a few fields but what about using the power of macros?

The next piece of code is in my opinion less legible/readable and we won't be able to debug/step through it, but I do like it! Lets throw away the previous code and start over. Firstly we've declared the field list of obligatory mandatory in the ClassDeclaration:
    // EVE_PersonType::Organization list of mandatory fields
    #localmacro.Organization
        %1(Name)
        %1(NameAlias)
        %1(Phone)
        %1(EVE_DocumentType)
        %1(EVE_DocumentId)
        %1(EVE_DUPCI)
    #endmacro

    // EVE_PersonType::Person list of mandatory fields
    #localmacro.Person
        %1(EVE_Name)
        %1(EVE_MiddleName)
        %1(EVE_Surname)
        %1(EVE_BirthDate)
        %1(EVE_Gender)
        %1(EVE_DocumentType)
        %1(EVE_DocumentId)
        %1(EVE_DUPCI)
        %1(Phone)
    #endmacro
You will notice an additional %1 and parenthesis around the field names. We'll be calling each of those fields with a function name passed in as an argument. When the #Person macro appears in our code we will be repeatingly call '%1(fieldName)' in the code for each line.

The form's init method will now call the below:
void setObligatoryControls()
{
    DictTable dt = new DictTable(smmBusRelTable.TableId);

    // Mandatory function fragment
    #localmacro.mandatory
        smmBusRelTable_ds.object(dt.fieldName2Id('%1')).mandatory(true);
    #endmacro
    ;

    switch (personType)
    {
        case EVE_PersonType::Organization:
            #Organization(#mandatory)
        break;

        case EVE_PersonType::Person:
            #Person(#mandatory)
        break;
    }
}
It's a similar approach within the validateWrite method:
public boolean validateWrite()
{
    boolean         ret;
    DictTable       dt = new DictTable(smmBusRelTable.TableId);
    
    // Mandatory function fragment
    #localmacro.mandatory
        if (!smmBusRelTable.%1)
        {   //Se debe rellenar el campo %1.
            ret = checkFailed(strfmt("@SYS110217", dt.fieldObject(dt.fieldName2Id('%1')).label()));
        }
    #endmacro
    ;

    ret = super();

    if (ret)
    {
        switch (personType)
        {
            case EVE_PersonType::Organization:
                #Organization(#mandatory)
            break;

            case EVE_PersonType::Person:
                #Person(#mandatory)
            break;
        }
    }

    return ret;
}
To get your head around it all, try reading the article in Axaptapedia that gave me the idea.

lunes, 10 de octubre de 2011

SELECT DISTINCT fieldId FROM tableId ORDER BY fieldId

The DISTINCT clause doesn't happen in Ax 2009 and earlier, maybe niether in the 2012 version. There is however a trick to obtain this funcionality.

Query getDistinctQuery(TableId _tableId, FieldId _fieldId)
{
    Query                   query;
    QueryBuildDataSource    qbdsUtilElements;
    QueryBuildFieldList     qbdsFieldList;
    ;

    query = new Query();
    qbdsUtilElements = query.addDataSource(_tableId);
    qbdsUtilElements.update(false);
    //qbdsUtilElements.addGroupByField(_fieldId);
    qbdsUtilElements.addSortField(_fieldId);
    qbdsUtilElements.orderMode(orderMode::GroupBy);
    qbdsFieldList = qbdsUtilElements.fields();
    qbdsFieldList.dynamic(false);
    qbdsFieldList.clearFieldList();        // >> COMPLETE WIERDENESS
    qbdsUtilElements.addSelectionField(_fieldId, SelectionField::Max);
    return query;
}
The SelectionField::Database doesn't work (nor do we know what it does) and I'm not really sure why we have to add the SelectionField enum in the first place. Without that optional SelectionField enum second parameter for some reason when the query is run and all of the table fields appear to be readded to the sql and we don't get the desired result.

Obviously the above function can be changed to include more fields.


Edit: 23/02/2012 
I'm revisiting this with a second example that does not require the above addSelectionField statement.

Below is the SQL that the query example will generate:
SELECT JournalNum, AccountNum, Due, PaymMode, TransDate, CurrencyCode 
FROM LedgerJournalTrans 
WHERE JournalNum = N'000004_008'
GROUP BY LedgerJournalTrans.JournalNum, LedgerJournalTrans.AccountNum, LedgerJournalTrans.Due, LedgerJournalTrans.PaymMode, LedgerJournalTrans.TransDate, LedgerJournalTrans.CurrencyCode

And the query example code:
    Query                   qry;
    QueryBuildDataSource    qbr1;
    QueryBuildRange         range;
    QueryBuildFieldList     qbdsFieldList;
    ;
    
    qry = new Query();
    qbr1 = qry.addDataSource(tablenum(LedgerJournalTrans));
    qbr1.addGroupByField(fieldnum(LedgerJournalTrans, JournalNum));
    qbr1.addGroupByField(fieldnum(LedgerJournalTrans, AccountNum));
    qbr1.addGroupByField(fieldnum(LedgerJournalTrans, Due));
    qbr1.addGroupByField(fieldnum(LedgerJournalTrans, PaymMode));
    qbr1.addGroupByField(fieldnum(LedgerJournalTrans, TransDate));
    qbr1.addGroupByField(fieldnum(LedgerJournalTrans, CurrencyCode));

    range = qry.dataSourceTable(tablenum(LedgerJournalTrans)).findRange(fieldNum(LedgerJournalTrans, JournalNum));
    if (!range)
        range = qry.dataSourceTable(tablenum(LedgerJournalTrans)).addRange(fieldNum(LedgerJournalTrans, JournalNum));
    range.value('000004_08');

    qbdsFieldList = qbr1.fields();
    qbdsFieldList.dynamic(false);
    qbdsFieldList.clearFieldList();
    
    qbdsFieldList.addField(fieldNum(LedgerJournalTrans, JournalNum));
    qbdsFieldList.addField(fieldNum(LedgerJournalTrans, AccountNum));
    qbdsFieldList.addField(fieldNum(LedgerJournalTrans, Due));
    qbdsFieldList.addField(fieldNum(LedgerJournalTrans, PaymMode));
    qbdsFieldList.addField(fieldNum(LedgerJournalTrans, TransDate));
    qbdsFieldList.addField(fieldNum(LedgerJournalTrans, CurrencyCode));

    //element.query(qry);

    return qry;


Edit: 18/09/2014 
Finally let us not forget the quick-to-implement yet inefficient-to-run trick of using a collection class for holding unique values, a Set, to check if we have already treated the entity during a loop.

static void ACT_UniqueTest(Args _args)
{
    Set                 stAccountNums = new Set (Types::String);
    LedgerJournalTrans  ledgerJournalTrans;
    ;
    
    while select ledgerJournalTrans 
        where ledgerJournalTrans.AccountType == LedgerJournalACType::Cust
            && ledgerJournalTrans.JournalNum == '000002_017'
    {
        if (stAccountNums.in(ledgerJournalTrans.AccountNum))
            continue;
        stAccountNums.add(ledgerJournalTrans.AccountNum);

        info(strFmt('%1 - %2', 
            ledgerJournalTrans.AccountNum,
            CustTable::blocked(ledgerJournalTrans.AccountNum)));
    }
}

miércoles, 5 de octubre de 2011

I *really* wanted that 'two-column-50%' feeling

MorphX is great for throwing some controls on a form and and presenting the information from the database table.  If however the form is complex or will be one of the main forms used within the application it is well worth taking a few minutes out to present the information in a beter format...  Did I say a few minutes?


The first attempt had controls with their text cut off and all presented in 2 columns with a third effectively empty.  What I created was two Groups, their Frame set to None, and manually fill each one with controls or more sub-groups so that they balance out in total height of control content.  A little 'meh' but it will do.  Next I manually set their Width properties to 0 (changing the WidthMode, see here).  We will be hitting the X++ to next set their width values. I also set the General tab's ColumnSpace value to 10, and it's LeftMargin + RightMargin to a value of 5. The Columns value is set to 2 of course. Everything has it's AutoDeclaration set to Yes of course.


What I ended up doing was going to the land of WinAPI and attempt to get a handle on the General tab's rectangle dimensions - see the getWindowRect API and the RECT object it returns.  From there it was a case of some simple math, thus:

public void run()
{
    int            iWidth, x, y, k, l;
    ;

    SysListPageHelper::handleRunPreSuper(element, ctrlTabForm, 3, 2);
    super();
    
    [x, y, k, l]    = WinApi::getWindowRect(General.hWnd());
    iWidth          = (k - x
                        - 20 // General.ColumnSpace + General.LeftMargin + General.RightMargin
                        - 20 // Magic fudge factor
                        ) / 2;
    
    GeneralGrpCol1.width(iWidth);
    GeneralGrpCol2.width(iWidth);

    SysListPageHelper::handleRunPostSuper(element, ctrlTabForm);
}
It's not perfect, and I shall have to test it some more but it's far better than before!  As you can see from the above code there is a fudge factor which means that I'm missing something in the total width calculation - any suggestions?


Another example of the WinAPI::getWindowRect can be seen in a blog post capturing Ax form screenshots.  Finally, as we're in WinAPI land, I refer you to a useful post as to how to obtain screen sizes and the like.

All of this work simply because I was a web developer where everything on the page can be designed to 'float' around each other... In conclusion then I DON'T recommend this approach!

sábado, 1 de octubre de 2011

E-Learning suffrance

As part of my personal brain training with Ax 2009 I started off with reading the Microsoft Official Training Materials for Microsoft Dynamics® - more specifically the 80011* DEVeloper material they had. It was a great kick-off point by sitting down for 4 days and working through the first two or three modules that they offered. With copious amounts of head scratching, coffee and crying on the course tutor's shoulder we learnt a lot in a little time.

Now however after 6 months I'm an expert... And those of you who have been working in the ERP industry for last 20 will know how much of a joke that last sentence is. Therefore I applied myself to the Best Practices White Paper for Ax 2009. It's a popular resource related to developing with the product and well worth a perusal. It really should be done and dusted if you intend to write MorphX code as part of your career.

Next up was to broaden my horizons and get a better grasp of the individual modules available. The E-Learning catalogue provides an interesting offering of resources to the training materials available in pdf, and also in electronic plus interactive format. Furthermore you can see what modules you have completed and those still available. Best yet your boss can see how you are progressing with that fascinating multi-company, multi-currency Financials module. While having paper in my hand is always best I enjoyed sitting at my desk at work plodding through the catalogues available, sound off (the text is read out by a clear speaking American lady but I just can't handle having to listen to more than one woman telling me what to do in my life, thanks), and then click away at the Next button until the mini-tests arrive. My theory is that 50% of the difficulty of the exams vary more depending upon your own experience with ERP than the product itself.

And now it's all gone (for me). I had access via Customer Source but now I'm left with only my company's Partner Source but with no access to the E-Learning catalogue. I get the following message in a page with 240+ down votes:

You do not have unlimited organizational access to E-Learning for Microsoft Dynamics and related business products.

Enroll in a Partner Service Plan today to receive access!
Sadness.