Wednesday, February 4, 2015

Dynamics Ax Internals: Default dimension storage in Ax 2012


X++ code to retrieve default dimensions (via individual selects)
Obviously this isn't a particularly efficient approach - it's expanded out like this for the sake of demonstration. In picture-form it may look similar to the following. Note the main tables involved, and the relationships between them:
 
 
That's a lot of tables! Whereas before we would just reference the elements of the Dimension array, we now have to go through multiple joins to get the same information. The reason for this is the way dimensions are defined and structured in Ax2012. Previously we had a fixed number of dimensions, and a fixed source (the dimension code table), but now we can define an attribute that points to pretty much anything (customers, item groups, warehouses, etc).
I'll be interested in seeing how this affects reporting that works off direct SQL queries or cubes, as we now have to dynamically link tables based on the underlying source table (identified by DimensionAttribute.BackingEntityType). It could make things a bit tricky, and I suspect we'll have to rely more on generating datasets from within Ax, using the new data provider framework for SSRS.
So an overview of the main tables involved is:
Table

Description
DimensionAttributeValueSet

A unique combination of values used for default dimensions. This acts as a
container for a list of DimensionAttributeValueSetItem records, which link off
to the specific attribute and attribute value records.
This is similar in concept to the InventDim table in Ax2009, which stores unique combination of inventory dimension values. It uses a field called Hash, which stores a hash-code for all of the attached values. This is used by Ax when checking whether it needs to create a new entry, or use an existing one. (NB the dimension controllers rely heavily on server-side caching - If you're doing any investigation into the code it may help to disable this via code. Just make sure it's left as-is for production and testing environments).

DimensionAttributeValueSetItem

This stores the individual attribute items (I would describe them more as the 'segments'), that make up a value set. This relates to the RecID of the
DimensionAttributeValueSet via the field of the same name.
Note that this table doesn't store the actual value. It points to an instance of DimensionAttributeValue (see below), which in-turn links back to the dimension value entitiy (eg Customer table).
DimensionAttributeValue

This is a link between an attribute and a value.
The field EntityInstance points to the RecID of the underlying table/view. NB
the structure of this is normally that you create a view pointing to the table or tables you want to use for the dimension values. The view can be structured as normal with joins, relations, etc, but will typically only return three fields:
  • Key - RecID of primary table
  • Value - 'Code', such as customer account, item number, etc.
  • Name - The description/name, eg The name on the customer address book entry.
The convention is that any table used for dimension values is exposed as a view (prefixed with "DimAttribute"). Have a look at the existing DimAttributexxx views in the standard application for plenty of examples.
DimensionAttribute
The main attribute table. This will have an entry for 'department', 'cost centre', 'purpose', etc, as well as any other dimensions you define. Each DimensionAttribute points to a 'backing entity' type, which is the table/view id of the underlying data-source.
For 'custom value' dimensions (ie those that don't point to an existing table), this points indirectly to table DimensionFinancialTag.
Table overview
* There's a slight caveat here. If the dimension points to a table like CustTable, how does Ax make sure that there is a corresponding entry in DimensionAttributeValue? The answer is that whenever the dimension value is referenced (for example by selecting it on a form), the system checks whether the entry exists, and if not, it's created. This occurs at:
Data DictionaryTablesDimensionAttributeValueMethodsinsert

5
Data DictionaryTablesDimensionAttributeValueMethodsfindByDimensionAttributeAndEntityInst

50
FormsDimensionDefaultingLookupMethodscloseSelect

17
And in addition, what if we're referencing the customer dimension, but the underlying customer record is deleted? If you look at CustTable.delete, you'll see a call to DimensionAttributeValue::updateForEntityValueDelete. This goes through any existing references to the corresponding DimensionAttributeValue and clears them. I suspect (at least I'd hope), that if any GL postings have already been made, you won't be able to remove the underlying record.

Forms

The class DimensionDefaultingController is used throughout the application to handle the display of default dimensions on master records (customer, suppliers, etc). If you look at the code in the following stack-trace, you'll see query logic similar to the sample at the beginning of this post.

The DimensionDefaultingController is created on the form, accepting the datasource and field (which in most cases will be DimensionDefault). On the datasource 'active' event, the controller iterates through the relevant dimension value set, and updates the controls. There's a lot more to cover with respect to how dimensions are displayed/updated from the UI - Look out for a future post.
Dynamics Ax Internals: Default dimension storage in Ax 2012

Useful X++ code for ledger dimensions

http://dynamics-resources.com/financial-dimensions-using-x/

Create Ledger Dimension through string values:
Converts string values to ledgerDimension after validating it against chart of account:
Creates DefaultDimension from string values:
Convert LedgerDimension into string value:

Create Purchase invoice journal and lines through code X++ in Ax2012.

//Working code and tested in same instance..
static void createVendorInvoiceJournal(Args args)
{
    LedgerJournalCheckPost                  jourCheckPost;
    LedgerJournalTable                          jourTable;
    InventTable                                       inventTable;
    DimensionAttributeValueSet            dimAttrValueSet;
    DimensionAttributeValueSetItem     dimAttrValueSetItem;
    DimensionAttributeValue                 dimAttrValue;
    DimensionAttribute                          dimAttr;
    Common                                           dimensionValueEntity;
    AxLedgerJournalTable header = new AxLedgerJournalTable();
    AxLedgerJournalTrans trans = new AxLedgerJournalTrans();
    AxLedgerJournalTrans trans1 = new AxLedgerJournalTrans();
    container            offsetDim;
    str 30      accNo,departmentDim,businessUnitDim,costCenterDim,itemGroupDim;
    str 30      businessUnitDimValue,costCenterDimValue,itemGroupDimValue,departmentDimValue;
    int           indexcount;
     
    LedgerJournalNameId ledgerJournalNameId = "APInvoice";
    DimensionAttributeValueCombination davc;

    header.parmJournalName(ledgerJournalNameId);
    header.parmJournalType(LedgerJournalType::VendInvoiceRegister);
    header.save();

    trans.parmAccountType(LedgerJournalACType::Vend);
    trans.parmJournalNum(header.ledgerJournalTable().JournalNum);

    select firstonly RecId from davc where davc.DisplayValue == "CN-001";    
    trans.parmLedgerDimension(davc.RecId);
    trans.parmAmountCurCredit(99.15);
    trans.parmTaxItemGroup("UB");
    trans.save();

    trans1.parmOffsetAccountType(LedgerJournalACType::Ledger);
    //dimAttrValueSet = DimensionAttributeValueSet::find(inventTable.DefaultDimension);
    dimAttrValueSet = DimensionAttributeValueSet::find(22565462243);

    while select dimAttrValueSetItem where dimAttrValueSetItem.DimensionAttributeValueSet ==  
     dimAttrValueSet.RecId
    {
        dimAttrValue = DimensionAttributeValue::find 
                                                            (dimAttrValueSetItem.DimensionAttributeValue);
        dimAttr = DimensionAttribute::find(dimAttrValue.DimensionAttribute);
        dimensionValueEntity = DimensionDefaultingControllerBase::findBackingEntityInstance
         (curext(),
        dimAttr, dimAttrValue.EntityInstance);

        if(dimAttr.Name == "Department")
        {
            departmentDim = dimAttr.Name;
            departmentDimValue = dimAttrValue.getValue();
        }
        if(dimAttr.Name == "BusinessUnit")
        {
            businessUnitDim = dimAttr.Name;
            businessUnitDimValue = dimAttrValue.getValue();
        }
        if(dimAttr.Name == "CostCenter")
        {
            costCenterDim = dimAttr.Name;
            costCenterDimValue = dimAttrValue.getValue();
        }
        if(dimAttr.Name == "ItemGroup")
        {
            itemGroupDim = dimAttr.Name;
            itemGroupDimValue = dimAttrValue.getValue();
        }
  }
    accNo = DimensionAttributeValueCombination::getDisplayValue(WNXParameters::find().VendorLedgerDimension);
    indexcount = 2; trans1.parmJournalNum(header.ledgerJournalTable().JournalNum);
    //First is  Display value, followed by Main Account and then dimensions.
    offsetDim =
  [accNo,accNo,indexcount,businessUnitDim,businessUnitDimValue,departmentDim,departmentDimValue];
    //offsetDim = ["110180", "110180", 2, "BusinessUnit", "001", "Department", "022"];
    //Manual input
    //offsetDim = ["112000", "112000", 0]; //Manual input
    trans1.parmLedgerDimension(AxdDimensionUtil::getLedgerAccountId(offsetDim));
    trans1.parmAmountCurDebit(99.15);
    trans1.parmTaxItemGroup("UB");
    trans1.save(); jourTable = header.ledgerJournalTable();
    if (jourTable.RecId > 0)
    {
        jourCheckPost = ledgerJournalCheckPost::newLedgerJournalTable(jourTable, NoYes::Yes, NoYes::Yes);
        // Post only if there is succesful validation.
        if (jourCheckPost.validate())
        {
            jourCheckPost.run();
        }
    }
}

Vender Creation in Ax2012.

static void createVendorRecord(Args _args)
{
    VendTable       vendTable, vendTableRef;
    DirPartyTable dirPartyTable;

    ttsBegin;      
    VendTable.AccountNum = NumberSeq::newGetNum(VendParameters::numRefVendAccount()).num();
    vendTableRef = VendTable::find(WNXParameters::find().VendorAccount);
    vendTable.initValue(); vendTable.VendGroup = vendTableRef.VendGroup;
    vendTable.Currency = vendTableRef.Currency;
   dirPartyTable = DirPartyTable::findRec(22565449577);//Customer Master PartyId
    if(dirPartyTable)
    {
        vendTable.Party = dirPartyTable.RecId;
        vendTable.insert();
    }
    else
    {
        info("vendor not created");
    }
    ttsCommit;
}