Monday, July 30, 2012

Checking Country-Specific Feature Settings from Code

Last week I posted about the country-specific features in AX 2012. We looked at creating fields that are country-specific, and how those relate to either your legal entity, or another party entity specified on another field in the table.

What prompted me to write about this feature was actually the opposite problem. I had some generic code that was looping over tables and fields, but users complained the code was showing tables and fields they had not seen before. Turned out those were country-specific fields and tables. When checking configuration keys, there are easy functions to test those (isConfigurationkeyEnabled). Unfortunately, there is no such functional to test for country-specific features... so, time to code!

The metadata classes DictTable and DictField have been updated to give you the two properties we've set in our previous article, namely "CountryRegionCodes" (listing the countries) and "CountryRegionContextField" (optionally listing a field for the region context).

First things first. Remembering the setup we did, the field or table specifies a list of country ISO codes. The country/region of the "context" is considered as a RecId of a DirParty (global address book) entity record. So, we'll need a function that finds the ISO code of the primary address of a DirParty record...

public static AddressCountryRegionISOCode getEntityRegionISO(DirPartyRecId _recId)
{
    DirParty party;
    LogisticsLocationEntity location;
    AddressCountryRegionId regionId;

    party = DirParty::constructFromPartyRecId(_recId);

    if (party)
        location = party.getPrimaryPostalAddressLocation();

    regionId = location ? location.getPostalAddress().CountryRegionId : '';

    return LogisticsAddressCountryRegion::find(regionId).ISOcode;
}

This will return an empty string if the party, the party's primary address, or the ISO code cannot be found. You could place this method on the Global class to make it a global function.

Next, we'll create a method to check if a table is in fact enabled in our current context (this means checking the regular configuration key as well as the country-specific settings, if they are used). Since there is potential for the function needing the actual record (in case of a table specifying a context field), we require an actual table buffer as input to our function. Note that the "getCountryRegionCodes()" method of the DictTable class doesn't return a comma-separated list as specified in the properties, but parses it out into a container of ISO codes.

public static boolean isTableCountryFeatureEnabled(Common _record)
{
    DictTable dictTable = new DictTable(_record.TableId);
    boolean isEnabled = false;
    DirPartyRecId partyRecId;
    // First make sure we found the table and it's enabled in the configuration
    isEnabled = dictTable != null && isConfigurationkeyEnabled(dictTable.configurationKeyId());
        
    // Additionally, check if there are country region codes specified
    if (isEnabled && conLen(dictTable.getCountryRegionCodes()) > 0)
    {
        // If a context field is specified
        if (dictTable.getCountryRegionContextField() != 0)
        {
            // Get the Party Rec Id Value from that field
            // TODO: could do some defensive coding and make sure the field id is valid
            partyRecId = _record.(dictTable.getCountryRegionContextField());
        }
        // If no context field is specified
        else
        {
            // Get the Rec Id from the legal entity
            partyRecId = CompanyInfo::find().RecId;
        }
        
        // Call our getEntityRegionISO function to find the ISO code for the entity        
        // and check if it is found in the container of region codes
        isEnabled = conFind(dictTable.getCountryRegionCodes(), getEntityRegionISO(partyRecId)) > 0;
    }

    return isEnabled;
}

The function will return true if the table is enabled in the current context or if the table is not bound to a country/region. Note that this code uses the "getEntityRegionISO" function created earlier. If you are not adding these to the Global class, you will have to reference the class where that method resides (or add it as an inline function inside the method).
Finally, we want a similar function that tests an individual field.

public static boolean isFieldCountryFeatureEnabled(Common _record, FieldName _fieldName)
{
    DictField dictField = new DictField(_record.TableId, fieldName2id(_record.TableId, _fieldName));
    boolean isEnabled = false;
    DirPartyRecId partyRecId;

    // First make sure the table is enabled
    isEnabled = isTableCountryFeatureEnabled(_record);
    // If that checks out, make sure we found the field and that is enbled in the configuration
    isEnabled = isEnabled && dictField != null && isConfigurationkeyEnabled(dictField.configurationKeyId());
        
    // Additionally, check if there are country region codes specified
    if (isEnabled && conLen(dictField.getCountryRegionCodes()) > 0)
    {
        // If a context field is specified
        if (dictField.getCountryRegionContextField() != 0)
        {
            // Get the Party Rec Id Value from that field
            // TODO: could do some defensive coding and make sure the field id is valid
            partyRecId = _record.(dictField.getCountryRegionContextField());
        }
        // If no context field is specified
        else
        {
            // Get the Rec Id from the legal entity
            partyRecId = CompanyInfo::find().RecId;
        }
        
        // Call our getEntityRegionISO function to find the ISO code for the entity        
        // and check if it is found in the container of region codes
        isEnabled = conFind(dictField.getCountryRegionCodes(), getEntityRegionISO(partyRecId)) > 0;
    }

    return isEnabled;
}

The function behaves similarly to the table function, and in fact it calls the table function first. Note that this means the code depends on the "isTableCountryFeatureEnabled" and the "getEntityRegionISO" functions created earlier. If you are not adding these to the Global class, you will have to reference the class where that method resides (or add it as an inline function inside the method).

So now, feel free to test these methods on our previously created fields. Sure hope it works =)

Wednesday, July 25, 2012

Creating Country-Specific Features in AX 2012

One of the new features in Dynamics AX 2012 is the country-specific features. In previous releases, country-specific features were available through configuration keys. Configuration keys however are global, meaning when turned on those features appear in all companies in your Dynamics AX environment.

In Dynamics AX 2012 these configuration keys are still there, and you still can disable/enable these configuration keys. However, to avoid confusion in legal entities that are in other countries, a new feature will make the user interface "adapt" to the country your legal entity is in, or, depending on the setup, the country of the data you are working with.

Let's create some code to illustrate this functionality and how it works.
First, my setup here is using the Contoso Demo Data. I'm the "CEU" legal entity. Since the country features will look at that entity's primary address country by default, let's make sure we know what it is.
Go to Organization Administration > Setup > Organization > Legal Entities. In the Addresses FastTab, select the primary address and click More Options > Advanced.


On the Country/region field, right-click and select "View Details". The reason we go through all of this is that the country-specific features use the country ISO codes, so you want to make sure you capture the correct ISO code for the address. In my case here, the address uses "USA" as the country, but the ISO code is "US".


Let's dive into the code. We're going to create a table with 2 string-type fields. One regular field, one field that is tied to the country features. To shorten the exercise I won't bother with data types or labels, but of course you should always use those in your code! :-)


Right-click the Country_US field and select "Properties". On the properties window enter US in the CountryRegionCodes property. Note that you can specify a comma-separated list of ISO country codes in case you need the field in multiple regions.


Next, we'll create a form for this table. I just added a grid and put the two fields on it. I dragged my table onto the "data sources" node, and from there dragged the two fields onto the grid. You gotta love MorphX drag and drop.


Now when I open the screen in my US company, as expected, here's what it looks like:


However, when we open this screen in for example CEUF, which is a French demo company, here's what the screen looks like:


In this case, the field we added uses the legal entity's country ISO code to determine visibility of the field. However, we can make this a bit more dynamic by explicitly specifying what is called a "Context Field". By default, the context is the legal entity (and it's primary address' country) the form is running under, however, we can create a field on the table that will indicate the context on a record-by-record basis.
To demonstrate this, let's add a field indicating a "party" (yes, global address book!) the record is for, and a second country-specific field, this time for France.

First, add an Int64 (recid) field to indicate the "entity" (aka party) and a new field that will be France-specific.


On the Party field, select the extended data type "DirPartyRecId" (make sure your field is Int64 type). This will also prompt you to add the relation to your table. Say yes, as this will enable the alternate key lookup.


For the new Country_FR field, make sure to set the "CountryRegionCodes" property to "FR" and select our new Party field as the "CountryRegionContextField".


Next, change our original Country_US field to use the new CountryRegion field as the context, by setting the "CountryRegionContextField" property on that field to "Party".


Finally, we'll need to add both fields on our form. First, you need to refresh the form to pick up the newly added fields on the table. In case you don't know, the quickest and easiest way to accomplish that is to right-click the form and select "restore". This will also reload the table and make the new fields available. Open the Data Sources node and expand the DMCountryExample table... drag the new fields onto the grid in the design. By dragging the Party field, you will automatically get the ReferenceGroup added. If you are manually adding controls to the grid, make sure to add the reference group for the Party so you get the alternate key lookup correctly.



So now for the grand finale... Let's open the form and add two records, one with a US party, one with an FR party. You may have to go through your parties and find USA and FRA records, or perhaps add a primary address to one of the parties and set them so you know they work.

This is what that looks like in a US company:


If you would blank out the Name for one, both fields will show the n/a symbol.

And of course, in the French company, this screen looks exactly the same, as the fields are now tied to the party and no longer to the default of legal entity!