Thursday, 29 September 2011

Breakdown of a list page (Sales orders)

List pages are a new concept in Ax2012. Enterprise Portal in Ax2009 had them to some extent but now they're used as the main entry point for a lot of functional areas in Ax, like customers, sales orders, etc. Generally they'll be displayed as a list (surprise!), with action buttons at the top, and related/child information displayed in so-called 'parts'.

There are few things to keep in mind when developing list page forms in Ax. This post aims to go through some of the key points. We'll start by looking at the sales order form (AOT name SalesTableListPage).

Overview



Sales order list page

The name of this form is SalesTableListPage, and is broken down as follows:

  • Section 1 is the main list, implemented as a normal grid control.
  • Section 2 contains several Parts, like 'Related information'
  • Section 3 is an ActionPane, which contains button groups and buttons.
  • Section 4 is also a 'Part'. The reason it's down at the bottom of the page and not on the right-hand side like the other is that it's PartLocation property is set to Preview.

Also note some of the general properties of the form:

  • The FormTemplate property on the form is set to ListPage. Note that this adds special behavior and imposes restrictions on other properties within the form, such as not being able to add methods (see comment below).
  • The query SalesTableListPage is used as the primary datasource on the form. When a query is added as the datasource it will automatically add the associated tables to the form. This structure is then fixed - ie tables cannot be removed or their join properties modified.  
  • The InteractionClass property is set to SalesTableListPageInteraction. This is a controller class that handles the majority of form logic. This is in place of coding up logic directly on active/write/init methods, and is a key part of how list pages are structured.
A nice feature of list pages is that they can be deployed to Enterprise Portal at pretty much the click of a button. This is the main reason that there is effectively no code attached directly to the form object - It all happens through the interaction classes and so can be shared between the rich client and web UI's. If you're interested, OpenERP does this with all it's forms - you structure it once, and it automatically deploys to a form and to the web (on Windows and Unix). This is an Ax blog though so I'll press on...


SysListPageInteractionBase


As mentioned, most if not all of the form logic for list pages is handled through an instance of SysListPageInteractionPageBase (selected via a form-property). The sales order form uses derived class SalesTableListPageInteraction. Some of the key methods you should know about include:

Method Description
initializing Called when the form is initializing - Similar to the form init method
intializeQuery Also called when the form is initializing - Similar to the datasource init method
selectionChanged Called when the active record changes - Similar to the datasource active method.
setButtonEnabled Should be overridden to dynamically enable/disable buttons based on the current selection. This is called from the selectionChanged method.
setButtonVisibility Should be overridden to show/hide buttons when the form first opens. This is used more to do a one-off layout adjustment based on system configuration/parameters, as well as the menu-item used to open the form. eg If you have a menu-item that opens a form based on status, you may want to hide the relevant 'status' field to reduce clutter.

Some of the key methods used in the list page interaction classes

These are just a handful - Have a look through SysListPageInteractionBase and other examples (like SalesTableListPageInteraction) to get a better idea of what's available.

You'll probably have realised that since we've now moved the logic off the form and into a separate class, we can now no longer access controls directly by name (using the AutoDeclaration property). This also applies to the datasources - Where we would've previously just referenced "SalesTable" to get the currently active sales order record, we now have to find another way.
It's a standard convention in Ax that controller classes obtain a reference to controls of interest when they're created. An example of this is the class LedgerJournalFormTrans. If you look at the class declaration you'll see member-variables that point to buttons and data controls, which are set when the class is instantiated. I've always found this a fairly tedious, if necessary, pattern - Fortunately the list page interaction classes provide helper functions for getting easier access to controls and data-context.
If you look at the setButtonSalesOrder method in SalesTableListPageInteraction (which is called form setButtonEnabled), you'll see the code:

protected void setButtonSalesOrder()
{
    this.listPage().actionPaneControlEnabled(formControlStr(SalesTableListPage, SalesCopyAllHeader), !salesTableInteractionHelper.parmReturnItem());
    this.listPage().actionPaneControlEnabled(formControlStr(SalesTableListPage, SalesCopyJournalHeader), !salesTableInteractionHelper.parmReturnItem());
}

Obtaining a reference to a form control from the interaction class

What's happening here is we're getting a reference to the current ListPage (representing the form), and from that a reference to the control, identified by name. To get the current record, you'll be following the pattern (from method currentSalesTable):

    return this.listPage().activeRecord(queryDataSourceStr(SalesTableListPage, SalesTable)) as SalesTable;

Obtaining current record from the interaction class
Again, this uses the listPage method to reference the form object itself, then uses the method activeRecord (accepting the datasource name) to return the currently selected/active record. activeRecord returns a generic record (instance of Common), so we need to cast it to the correct record type using the new 'as' keyword (familiar to you C# developers).

It would be a good idea to create wrapper methods for all of the record types you'll be referencing in the interaction handler. It's a shame we've lost the ability to reference the datasource directly, but it's a trade-off for getting one-click deployment to the web.

Info parts and context - Latest sales orders

The 'related information' boxes use InfoParts to display fact-boxes and preview information. In the sales order form this includes general customer information for the current order, as well as a summary of the lines.

Let's have a closer look at the "Latest sales orders" part, and how it's attached to the form.



The Parts section of the form contains a reference to the menu item SalesLatestOrdersPart, which in-turn points to InfoPart SalesLatestOrdersInfoPart, as follows:

Referencing a part on a form - Object links
There are a lot of places in Ax2012 where we have to reference things indirectly through menu-items where a direct reference to the underlying object would probably do. In my opinion this makes things unnecessarily difficult to maintain - Same complaint for setting up new workflow types. I think the main reason is because the new security model is geared more towards setting privileges based on menu-items, but it still feels like overkill. 
The context of the main form is passed through to the part via the Datasource property and optionally the DataSourceRelation property on the form part reference. In this instance, the datasource is set to SalesTable, and the DataSourceRelation is set to EDT.SalesTable.CustAccount. What this does is take the current sales order record, pick up the CustAccount field (order account), and use that as the primary filter on the underlying query/table in the part (SalesLatestOrdersPart/CustTable).

The options available to you for DataSourceRelation are determined by finding compatible relations between the selected DataSource, and the primary table that is used in the part query. In this case, it finds the following relationships:

  • SalesTable.InvoiceCustomer - Relation InvoiceCustomer defined on SalesTable
  • SalesTable.OrderCustomer - Relation OrderCustomer defined on SalesTable
  • EDT.SalesTable.CustAccount - Relation defined on extended data type CustAccount, used by field SalesTable.CustAccount.
  • EDT.SalesTable.InvoiceAccount - Relation defined on extended data type CustAccount, used by field SalesTable.InvoiceAccount

Info parts and context - Preview pane (Sales lines)


The preview pane at the bottom works in a similar way but has a couple of differences worth noting:

  • The PartLocation property is set to PreviewPane. This positions the part at the bottom of the page. The default setting of 'Auto' aligns it on the right. NB There is a deliberate convention for laying out list pages - The idea is to keep them all consistent across both the rich client and web.
  • The DataSourceRelation is set to EDT.SalesTable.SalesID. The query used on the preview part (SalesTableListPagePreviewPane) uses SalesTable as the primary datasource, and joins to SalesLine. Setting the datasource relation to SalesID passes through the current sales order number which filters the part context automatically.

In addition, if you look at the layout section of the part (SalesTableListPagePreviewPane), you'll see two sections:
  • SalesTable shows basic order header information.
  • SalesLine shows the line details as a grid. Note the property 'Repeating' is set to true - This displays all matching records in grid form. I think this one could have been named more intuitively!


Cue groups and context - Related customer information


The related information section is similar in appearance to the InfoPart references, but actually points to a cue group, as follows (NB this diagram flows all the way through to the resulting form specified on the cue CustUnpaidInvoices, which is contained within Cue group CustRelatedInfo).



Since the datasource on the part reference is set to SalesTable, this is passed all the way through to the resulting form query (defined on CustOpenInvoicesListPage). This filters correctly because the main datasource of the cue (CustTransOpen) has a relation against SalesTable, based on the account number.
In my opinion, this is not a good design choice. It works, but logically CustTransOpen does not relate to SalesTable by account number alone. It looks like this has been added in to satisfy the cue relations, even though it's not strictly correct.
 I think this is another area that is over-burdened with multiple relationships between objects through menu-items, and possibly a source of confusion when developing and maintaining cue references on forms. At this point I would probably lean towards using FormParts over cue groups for form layouts. As an example, the 'Open sales orders' cue seems to incorrectly filter on the currently selected order, making it a bit pointless. This looks like a side-effect of having to pass through too many objects and layers to propagate the context.

Modifying initial query through menu items


List pages can also be filtered automatically via properties on the menu-items. The form SalesTableListPage uses the query SalesTableListPage. The menu-item SalesTableListPage (ie "All sales orders") points to that form.

If you look at menu item SalesTableListPageJournal (ie "Sales orders of type journal"), you'll see that in addition it specifies query SalesTableListPageJournal. That query bases itself on the original query (SalesTableListPage) using the Composite query pattern, but specifies an additional range on the SalesType. This causes the list page to use that query instead of the default, and provides automatic filtering.

This is quite a handy way of doing providing different entry-points for similar views. In previous versions you would most likely have passed a parameter in via the menu item and updated the query in the datasource or form init methods.
Keep in mind that there are limitations when using Composite queries, like not being able to add additional joined tables. However you could get around this by updating the query in the initializeQuery method on the list page interaction class. 

Hopefully this post helps people get an idea of the new 'list page' structure. Feel free to comment or leave questions.

Basic Address Book structure in Ax2012

This is a quick overview of the main tables involved in the address book functionality of Ax2012.

In Ax2009, the integration between the address book and 'entities', like customers, suppliers, etc was a bit flaky. This has been tightened up in Ax2012 so that now the address book has a much more important role in maintaining basic data like customer names and addresses etc.

A few of the tables you'll need to know about are:


Table Description
DirPartyTable Global address book. This will contain entries for all people and organizations
you deal with, including customers, suppliers, employees, etc.


This information is maintained across the entire organization. NB the table structure often refers to address book entries as 'parties'. Generally other records (like customer, supplier, etc) will reference a record in this table by a field named Party.
LogisticsLocation This is a single 'location' that can be attached to one or more address book entries. This is similar in principle to the old 'Address' table from Ax2009, that no longer exists - The main difference now being that the location header always points to an address book entry, whereas in 2009 the Address table could point to anything.

Note that this is not an address - Physical address details are stored in
LogisticsPostalAddress
LogisticsPostalAddress A postal address, linked to a LogisticsLocation record via field Location.
LogisticsElectronicAddress 'Electronic' address details, such as email, phone, web address etc.

Each different type of address is represented as a separate record, delineated by 'Type'. This links to the location record.
DirPartyLocation This table links entries in the LogisticsLocation table to an address book entry (DirPartyTable).
LogisticsLocationRole This defines types of roles that an address are classified as, such as "Delivery", "Invoice", etc.
DirPartyLocationRole Links a location role type (LogisticsLocationRole) and an address book entry (DirPartyTable)
DirPartyPostalAddressView (view) This is a view that collates address book entries with their linked postal addresses

A few of the address book tables you should know about


The following code sample shows how we could obtain the postal addresses for a customer. NB You would preferably use a view or joined queries to get this information. I've expanded it out to demonstrate the relationships.


static void ShowCustomerAddressBookDetails(Args _args)
{
    CustTable               custTable;
    DirPartyTable           dirPartyTable;
    DirPartyLocation        partyLocation;
    LogisticsLocation       logisticsLocation;
    LogisticsPostalAddress  postalAddress;
    ;
    custTable       = custTable::find('2014');
    dirPartyTable   = dirPartyTable::findRec(custTable.Party);
    while select partyLocation
        where   partyLocation.Party     == dirPartyTable.RecId
    {
        logisticsLocation = logisticsLocation::find(partyLocation.Location);        
        if(logisticsLocation.IsPostalAddress)
        {
            postalAddress = LogisticsPostalAddress::findByLocation(logisticsLocation.RecId);            
            info(strFmt("%1 - %2",
                logisticsLocation.Description,
                postalAddress.CountryRegionId));
        }        
    }
}
Obtaining postal addresses from a customer

And to get the email addresses:

static void ShowCustomerEmailAddresses(Args _args)
{
    CustTable                   custTable;
    DirPartyTable               dirPartyTable;
    DirPartyLocation            partyLocation;
    LogisticsLocation           logisticsLocation;
    LogisticsElectronicAddress  electronicAddress;
    ;
    custTable       = custTable::find('2014');
    dirPartyTable   = dirPartyTable::findRec(custTable.Party);
    while select partyLocation
        where   partyLocation.Party     == dirPartyTable.RecId
    {
        logisticsLocation = logisticsLocation::find(partyLocation.Location);        
        while select electronicAddress
            where   electronicAddress.Location  == logisticsLocation.RecId
            &&      electronicAddress.Type      == LogisticsElectronicAddressMethodType::Email
        {            
            info(strFmt("%1",electronicAddress.Locator));
        }        
    }
}

Obtain customer email addresses


The following code sample retrieves the phone numbers attached to a warehouse. It was added in response to a question from adiso.

static void FindPhoneNumbersAttachedToWarehouse(Args _args)
{

    InventLocation                      inventLocation;
    LogisticsEntityPostalAddressView    postalAddressView;
    LogisticsElectronicAddress          elecAddress;
    LogisticsLocation                   contactLocation;
    
    inventLocation = inventLocation::find('11');
    
    if(inventLocation)
    {
        while select postalAddressView  
            where   postalAddressView.Entity            == inventLocation.RecId
            &&      postalAddressView.EntityType        == LogisticsLocationEntityType::Warehouse
        {                
            while select elecAddress                
                where   elecAddress.Type                == LogisticsElectronicAddressMethodType::Phone
            join contactLocation                                    
                where   contactLocation.ParentLocation  == postalAddressView.Location
                &&      contactLocation.RecId           == elecAddress.Location
            {            
                info(elecAddress.Locator);   
            }                   
        }
    }

}

Basic structure of ledger in Ax2012

If you've worked in Ax2009 or prior, you'll see some major changes to the way the ledger and chart of accounts are defined and structured in 2012, both from a functional point of view as well as behind the scenes. This post aims to cover off some of the basic details of how it's now implemented. The Microsoft course material covers off the functional areas well so it's worth checking that out.


Standard disclaimer is that this is all fairly new to me as well so if you spot any errors or omissions feel free to comment or email me!

Chart of accounts and structures (global)


In Ax2009, the chart of accounts (LedgerTable) was defined per company. In 2012, the charts of accounts are defined across the entire organization, then selected per-company via the company 'Ledger'.


Chart of accounts setup. Defined across entire organization

The above form is accessible from General Ledger / Setup / Chart of accounts / Chart of accounts.
  • The "Charts of accounts" (left section of above form) are defined in LedgerChartOfAccounts
  • The "Main accounts", which are similar in meaning to the Ledger accounts (LedgerTable) in Ax2009, are stored in table MainAccount
  • The "Account structures" (LedgerChartOfAccountsStructure) link a chart of accounts to a dimension structure (DimensionHierarchy), which defines the breakdown of how dimensions are entered (more on this later).


Ledger setup (per-company selection of chart of accounts)



Ledger setup - Defines per-company settings

The Ledger form above (General Ledger / Setup / Ledger) defines the per-company information. This is where you nominate the fiscal calendar and chart of accounts relevant to the company. The table that stores this information is Ledger, and is linked to a company via the field PrimaryForLegalEntity, which points to CompanyInfo.RecID.

Note that even though the Ledger table defines per-company information, it's still a 'shared' table (ie SaveDataPerCompany is "No"). It maintains a unique index on PrimaryForLegalEntity to effectively make it non-shared (ie per-company).

The following is a basic overview of the relationships between the main tables:



Basic structure of ledger and chart of accounts tables

The following code-snippet shows the relationship by displaying the name of the current company's selected chart of accounts:

static void ShowCurrentChartOfAccounts(Args _args)
{
    CompanyInfo             companyInfo;
    Ledger                  ledger;
    LedgerChartOfAccounts   ledgerCOA;
    ;
    
    companyInfo = companyInfo::find();
    ledger      = ledger::findByLegalEntity(companyInfo.RecId);
    ledgerCOA   = LedgerChartOfAccounts::find(ledger.ChartOfAccounts);
    
    info(ledgerCOA.Name);    
}

Code to retrieve the current chart of accounts

Dimension structure in a bit more detail

A chart of accounts can have one or more dimension structures attached to it (via LedherChartOfAccountsStructure as shown above).

The dimension structure includes several segments, which for example could include:
  • Main account
  • Department
  • Cost centre
  • Purpose
These are setup via the menu item General Ledger / Setup / Chart of accounts / Configure account structures, and can be attached via the Chart of accounts form.


Configure account structures form

Item (one) in the above diagram shows all of the available account structures, stored in table DimensionHierarchy.
NB this table is used for several purposes, like dimension structures, dimension sets (for reporting and financial statements), advanced rule structures etc. These are delineated by the 'StructureType' field, which in this instance is limited to 'Account structure'.
Item (two) moving across the screen shows the 'segments' of the structure. This starts with the 'main account' and optional filter, and can include one or more additional dimension attributes, such as department, purpose, customer, etc. These are stored in table DimensionHierarchyLevel.

Item (three) moving down shows the different 'constraint nodes' of the structure (in this picture we only have one). These are used to define validation rules specific to an account range. So, in the above screen the filter for main account is 0101..0106, then the filter for department is >5, and so on. This means when a posting is made for account 0105, it will validate that the department value is greater than 5.

If we had another level where the main account was ranged 0200..0210, and a posting was made for account 0205, then any additional segment ranges would be validated against that level, and so on.

The constraint nodes are stored in table DimensionConstraintNode, which is linked to the structure via table DimensionConstraintTree.

The following shows the structure in code-form, by selecting a specific account structure and dumping the criteria values:

static void ShowAccountStructureBreakdown(Args _args)
{
    DimensionHierarchy              dimHierarchy;    
    DimensionHierarchyLevel         dimHierarchyLevel;
    DimensionAttribute              dimAttribute;
    DimensionConstraintTree         conTree;   
    DimensionConstraintNode         conNode;
    DimensionConstraintNodeCriteria conNodeCriteria;
    ;   
    
    dimHierarchy    = DimensionHierarchy::find(5637146666);
    conTree         = DimensionConstraintTree::findByDimensionHierarchy(dimHierarchy.RecId);
    
    setPrefix('"' + dimHierarchy.Name + '"');
    
    while select dimHierarchyLevel
        order by Level
        where dimHierarchyLevel.DimensionHierarchy  == dimHierarchy.RecId
    {        
        
        dimAttribute = DimensionAttribute::find(dimHierarchyLevel.DimensionAttribute);
        info(strFmt("Attribute at level %1 is %2",dimHierarchyLevel.Level,dimAttribute.Name));
        
        while select conNode 
            order by Ordinal
            where   conNode.DimensionConstraintTree  == conTree.RecId
            &&      conNode.DimensionHierarchyLevel  == dimHierarchyLevel.RecId
        {            
            
            while select conNodeCriteria
                where   conNodeCriteria.DimensionConstraintNode == conNode.RecId
            {            
                if(conNodeCriteria.RangeFrom || conNodeCriteria.RangeTo)
                {
                    info(strFmt(" - Limited from %1 to %2",                    
                        conNodeCriteria.RangeFrom,
                        conNodeCriteria.RangeTo));   
                }
                else
                    info(" - [No restriction]");
            }            
        }
    }
}

View account structure table relationships in code

You probably wouldn't write code like this as there are APIs to achieve the same result, and in reality I wouldn't expect this logic to be changed during an implementation. The sample is just to demonstrate the tables and main relationships.
In diagram form, the basic relationship is:

Dimension constraint table relationships

The account structures can become quite complex with parent/child relationships between the levels and criteria, but for a simple example look at the following setup.


Here we have an account structure with 3 levels: Main account, Department, and Customer. Remember that a 'segment' is stored in the table DimensionHierarchyLevel.


The first segment (main account) is limited to the ranges 0101 through 0106, and 110101 through 399999. The second segment (department) is limited to values greater than 5. When running the above job on this we get the following output:

Info  "Account structure"  Attribute at level 1 is MainAccount
Info  "Account structure"   - Limited from 0101 to 0106
Info  "Account structure"   - Limited from 110101 to 399999
Info  "Account structure"  Attribute at level 2 is Department
Info  "Account structure"   - Limited from 5 to 
Info  "Account structure"  Attribute at level 3 is Customer
Info  "Account structure"   - [No restriction]


Output from dump of dimension levels and criteria. 

This shows our 3 levels (MainAccount, Department, Customer), and for each the set of constraint nodes (only one per level in this example), and criteria.

You may have noticed that the range for Department (>5) has a RangeFrom of 5 and a RangeTo of [undefined]. In Ax terms, this would normally be expressed as "5..", resulting in an inclusive range (ie 5 and greater) as opposed to 'Greater than 5'. This is handled by the additional flags IsFromOpen and IsToOpen on DimensionConstraintNodeCriteria, which indicate whether the range is inclusive/exclusive at either end.

This only scratches the surface of the structural changes in Ax2012 finance. I'm aiming to get more posts up soon.

Wednesday, 28 September 2011

Default dimension storage in Ax 2012

The Chart of accounts structure in Ax2012 has changed significantly from previous versions. In Ax2009, any table that stored dimensions would have a field called 'Dimension' (an n-element array). This applied to master data (customers, items, etc), as well as transactions, like entries in the GL (LedgerTrans). Typically, dimension values would flow from the master data all the way through the GL postings. eg. Values specified on a customer record would get copied onto a sales order header, to the sales lines, to the invoice/lines, then through to the GL.

In 2012, this concept still remains however the implementation is very different. We now have the concept of 'Default dimensions', which are stored on the master data, but not on the transactions. The default dimensions are similar to the dimension values propagated to the GL, but they are stored in a different structure.

Table structure


Let's have a look at a quick code sample that displays the default dimensions attached to a supplier. (This is working off the Ax2012 demo data).

static void ShowVendDefaultDimensions(Args _args)
{
    VendTable                       vendTable;
    DimensionAttributeValueSet      dimAttrValueSet;
    DimensionAttributeValueSetItem  dimAttrValueSetItem;
    DimensionAttributeValue         dimAttrValue;
    DimensionAttribute              dimAttr;
    Common                          dimensionValueEntity;
    ;
    
    // Find our supplier
    vendTable = VendTable::find('3008');
    
    // Find the dimension value set that the vendor points to (for specifying the 
    // 'default' dimensions). This table is used as a sort of 'header' that the 
    // value set items (DimensionAttributeValueSetItem) records belong to.
    dimAttrValueSet = DimensionAttributeValueSet::find(vendTable.DefaultDimension);

    // Find all of the 'value set items' linked against the 'value set'  
    while select dimAttrValueSetItem
        where   dimAttrValueSetItem.DimensionAttributeValueSet   == dimAttrValueSet.RecId
    {
        // Find the dimension 'value' (DimensionAttributeValue) that the set item points to.        
        dimAttrValue        = DimensionAttributeValue::find(dimAttrValueSetItem.DimensionAttributeValue);
        
        // Find the underlying attribute.
        dimAttr             = DimensionAttribute::find(dimAttrValue.DimensionAttribute);

        // Use the helper class to obtain a reference to the underlying entity (can be anything)              
        dimensionValueEntity = DimensionDefaultingControllerBase::findBackingEntityInstance(
            curext(),
            dimAttr,
            dimAttrValue.EntityInstance);
        
        info(dimAttr.Name + ' ' + dimAttrValue.getValue());               
    }
}
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:

Tables for default dimensions


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 Dictionary\Tables\DimensionAttributeValue\Methods\insert

5

\Data Dictionary\Tables\DimensionAttributeValue\Methods\findByDimensionAttributeAndEntityInst

50

\Forms\DimensionDefaultingLookup\Methods\closeSelect

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.


Stack trace for the update of default dimension controls on a form

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.