Con la llegada de la versión 2012 de Microsoft Dynamics AX hubo un importante cambio en la forma en la que el ERP gestiona las Dimensiones Financieras. El modelo de datos cambió significativamente y con él, el modo en que nosotros, como desarrolladores, getionábamos y trabajábamos con ellas a través de X++. Dynamics 365 for Finance and Operations ha sido continuista en este aspecto, y la forma de trabajar con ellas es prácticamente la misma.

La principal diferencia que encontramos con este cambio fue que, mientras que en la versión 2009 o anterior teníamos limitado el número de dimensiones que podíamos utilizar en 10, en AX 2012 y MSDyn365FO ese límite desaparece. A nivel técnico, antes trabajábamos con un enumerado (SysDimension) para controlar las distintas dimensiones, y un EDT de tipo Array para almacenarlas, de forma que, si queríamos acceder a ellas, solo teníamos que acceder a los elementos de ese array para obtener los valores. Ahora, estas dimensiones vienen creadas directamente en base de datos, y la forma en la que se almacenan nos podrían recordar más a las dimensiones de inventario que ha utilizado históricamente el ERP, de forma que, lo que hacemos es almacenar posibles combinaciones de los distintos valores de las dimensiones, y utilizar un valor único (RecId) de esa combinación para asignar las dimensiones al registro correspondiente.

Este cambio, obviamente, abre muchísimo el abanico de posibilidades para explotar la información financiera a través de las dimensiones, pero, para los técnicos, supuso un pequeño dolor de cabeza el aprender a gestionar esta nueva estructura de dimensiones, es por ello, que quiero compartir con vosotros unos cuantos métodos que utilizo en todos los proyectos en los que participo, y que hacen que me olvide de esta gestión, facilitando mucho el uso de estas dimensiones. Podéis descargar esta clase directamente desde mi cuenta de GitHub y a continuación paso a explicaros su funcionamiento.

Como decía, antes gestionábamos las dimensiones a través del enumerado SysDimension, pero ahora, trabajamos con estas dimensiones por medio de su nombre, que viene dado por la persona que las crea o modifica, por lo que, lo primero que hacemos, es crear una serie de variables constantes estáticas que nos permitirán utilizar los nombres de las dimensiones en cualquier punto de la aplicación.
Importante: Si en algún momento se añade o modifica el nombre de alguna de las dimensiones es imprescindibles venir a esta clase y cambiar su nombre. Lo bueno es que estará centralizado y el cambio será mínimo. ¡¡¡No utilicéis el literal del nombre a discreción en todos los sitios!!!

JATDimensionUtils
class JATDimensionUtils
{
    // Dimension names
    public static const DimensionRefFieldName CostCenterName	= "CentroCoste";
    public static const DimensionRefFieldName BusinessUnitName	= "UnidadNegocio";
    public static const DimensionRefFieldName DepartmentName	= "Departamento";
    public static const DimensionRefFieldName ProjectName	    = "Proyecto";
    public static const DimensionRefFieldName CustomerName	    = "Cliente";
    public static const DimensionRefFieldName VendorName	    = "Proveedor";
    public static const DimensionRefFieldName WorkerName	    = "Trabajador";
    public static const DimensionRefFieldName CustomDimName	    = "Personalizada";
}

De este modo, cada vez que quiera utilizar el nombre de una dimensión solo tendré que utilizar la siguiente sintaxis:

JATDimensionUtils::CostCenterName

Ahora os voy a mostrar los distintos métodos que tengo dentro de esta misma clase y que me facilitan la vida.

GetDimensionAttributeValueSetId

Este método nos permitirá obtener el DefaultDimension dado un container con las dimensiones que queremos asociar al registro. Nos sirve por ejemplo para asociar dimensiones a la cuenta o cuenta de contrapartida de una línea de diario cuando son de tipo Vend, Cust, Bank…
Parámetro: Container con el total de dimensiones, seguido del nombre y valor de la dimensión.
Retorno: RecId asociado a la combinación de dimensiones obtenidas por parámetros.

public static RecId getDimensionAttributeValueSetId(container _dimensionValue, dataAreaId _dataAreaId = curext())
{
    RecId                               dimensionId;
    DimensionAttributeValueSetStorage   storage;
    DimensionAttribute                  dimensionAttribute;
    DimensionAttributeValue             dimensionAttributeValue;
    int                                 attributeCount, attributeIndex;
    str                                 attributeName, attributeValue;
    int                                 containerElementIndex;
    changecompany(_dataAreaId)
    {
        containerElementIndex = 1;
        storage = new DimensionAttributeValueSetStorage();
        // Get attribute count
        attributeCount = conPeek(_dimensionValue, containerElementIndex);
        containerElementIndex++;
        // Get attributes
        for (attributeIndex = 1; attributeIndex <= attributeCount; attributeIndex++)
        {
            // Get attribute name
            attributeName = conPeek(_dimensionValue, containerElementIndex);
            containerElementIndex++;
            // Validate the Financial Dimenion that was passed in.
            dimensionAttribute = DimensionAttribute::findByName(attributeName);
            // Get attribute value
            attributeValue = conPeek(_dimensionValue, containerElementIndex);
            containerElementIndex++;
            // Validate the Financial Dimenion Value that was passed in.
            dimensionAttributeValue = DimensionAttributeValue::findByDimensionAttributeAndValue(dimensionAttribute, attributeValue);
            // Add attribute
            storage.addItem(dimensionAttributeValue);
        }
        dimensionId = storage.save();
    }
    return dimensionId;
}
// Uso de getDimensionAttributeValueSetId
Container   conDimension    = [2, JATDimensionUtils::CostCenterName, "CC1", JATDimensionUtils::DepartmentName, "DEP01"];

ledgerJournalTrans.DefaultDimension = JATDimensionUtils::getDimensionAttributeValueSetId(conDimension);
GetLedgerDimensionId

Este método nos permitirá obtener el LedgerDimension dado un container con la cuenta contable (MainAccount) y las dimensiones que queremos asociar al registro. Nos sirve por ejemplo para asociar dimensiones a la cuenta o cuenta de contrapartida de una línea de diario cuando son de tipo Ledger.
Parámetro: Container con el MainAccount, seguido del nombre y valor de la dimensión.
Retorno: RecId asociado a la combinación de cuenta y dimensiones obtenidas por parámetros.

public static DimensionDynamicAccount getLedgerDimensionId(container _dimensionValue)
{
    DimensionStorage        dimensionStorage	= DimensionStorage::construct(0, LedgerDimensionType::Account);
    DimensionAttributeValue dimAttributeValue;
    DimensionStorageSegment dimensionStorageSegment;
    DimensionHierarchyLevel	dimHierarchyLevel;
    MainAccount		        mainAccount;
    recid			        dimHierarchyId;
    recid			        mainAccountRecId;
    DimensionValue          dimensionValue;
    container		        dimensions;
    
    for(int i = 1; i <= conLen(_dimensionValue); i++)
    {
        try
        {
            dimensionValue = conPeek(_dimensionValue, i);

            if(i == 1) // MainAccount
            {
                dimAttributeValue	= DimensionAttributeValue::findByDimensionAttributeAndValueNoError(DimensionAttribute::findByName("MainAccount"), dimensionValue, false, true);
                mainAccountRecId	= DimensionAttributeValue::find(dimAttributeValue.RecId).EntityInstance;
                dimHierarchyId	    = DimensionHierarchy::getAccountStructure(mainAccountRecId);
                dimensionStorage.addHierarchy(dimHierarchyId);

                while select DimensionAttribute, Level
                    from dimHierarchyLevel order by Level
                    where dimHierarchyLevel.DimensionHierarchy == dimHierarchyId
                {
                    dimensions		+= DimensionAttribute::find(dimHierarchyLevel.DimensionAttribute).Name;
                }

                dimensionStorageSegment	= DimensionStorageSegment::constructFromValue(dimAttributeValue.CachedDisplayValue,
                                                                                    dimAttributeValue);
                dimensionStorage.setSegment(i, dimensionStorageSegment);
            }
            else
            {
                // Resto de dimensiones
                if(dimensionValue)
                {
                    dimAttributeValue = DimensionAttributeValue::findByDimensionAttributeAndValueNoError(DimensionAttribute::findByName(conPeek(dimensions, i)),
                                                                                                        dimensionValue,
                                                                                                        false,
                                                                                                        true);
                    if (!dimAttributeValue)
                    {
                        // @JAT:DimensionNotFound = The value '%1' of the dimension '%2' does not exist.
                        throw error(strFmt("@JAT:DimensionNotFound", dimensionValue, conPeek(dimensions, i)));
                    }

                    dimensionStorageSegment = DimensionStorageSegment::constructFromValue(dimAttributeValue.CachedDisplayValue, dimAttributeValue);
                    dimensionStorage.setSegment(i, dimensionStorageSegment);
                }
                else
                {
                    dimensionStorageSegment = DimensionStorageSegment::emptySegment();
                    dimensionStorage.setSegment(i, dimensionStorageSegment);
                }
            }
        }
        catch
        {
            return 0;
        }
    }
    
    return dimensionStorage.save();
}
// Uso de getLedgerDimensionId
Container   conDimension    = ["705001", JATDimensionUtils::CostCenterName, "CC1", JATDimensionUtils::DepartmentName, "DEP01"];

ledgerJournalTrans.LedgerDimension  = JATDimensionUtils::getLedgerDimensionId(conDimension);
ChangeDimensionValue

Este método nos permitirá modificar los valores de algunas de las dimensiones que ya tiene asociadas el registro.
Parámetro: RecId de las dimensiones que ya tiene asociadas el registro y Container con los pares Nombre y Valor de las dimensiones que se quieren modificar.
Retorno: RecId de la combinación de dimensiones antiguas y nuevos valores asociados al registro.

public static RecId changeDimensionValue(DimensionDefault _defaultDimension, container _dimensionValue) 
{
    DimensionAttributeValueSetStorage	dimensionAttributeValueSetStorage;
    DimensionAttribute			        dimensionAttribute;
    DimensionValue				        oldDimensionValue;
    DimensionValue				        newDimensionValue;
    DimensionDefault			        newDimensionDefault;
    DimensionRefFieldName			    dimensionName;
    int					                i = 1;

    while (i <= conLen(_dimensionValue))
    {
        dimensionName	= conPeek(_dimensionValue, i);
        i++;
        newDimensionValue	= conPeek(_dimensionValue, i);
        i++;
        // Get current value
        oldDimensionValue = JATDimensionUtils::getDimensionValue(_defaultDimension, dimensionName);
        // Build DimensionAttributeValueSetStorage
        dimensionAttributeValueSetStorage = DimensionAttributeValueSetStorage::find(_defaultDimension);

        // Remove old dimension value
        dimensionAttribute = DimensionAttribute::findByName(dimensionName);
        dimensionAttributeValueSetStorage.removeDimensionAttributeValue(DimensionAttributeValue::findByDimensionAttributeAndValue(dimensionAttribute, oldDimensionValue).RecId);

        // Set new dimension value
        if(newDimensionValue != "")
        {
            dimensionAttribute = DimensionAttribute::findByName(dimensionName);
            dimensionAttributeValueSetStorage.addItem(DimensionAttributeValue::findByDimensionAttributeAndValue(dimensionAttribute, newDimensionValue));
        }

        _defaultDimension = dimensionAttributeValueSetStorage.save();
    }
    return _defaultDimension;
}
// Uso de changeDimensionValue
Container   conDimension    = [JATDimensionUtils::CostCenterName, "CC1", JATDimensionUtils::DepartmentName, "DEP01"];

projTable.DefaultDimension  = JATDimensionUtils::changeDimensionValue(projTable.DefaultDimension, conDimension);
GetDimensionValue

Este método nos permitirá obtener el valor de una dimensión concreta asociada al registro.
Parámetro: RecId de la combinación de dimensiones asociadas al registro y nombre de la dimensión que se quiere obtener.
Retorno: Valor de la dimensión requerida.

public static DimensionValue getDimensionValue(DimensionDefault _dimensionDefault, DimensionRefFieldName _dimensionName)
{
    DefaultDimensionView defaultDimensionView;
    
    select firstonly DisplayValue
        from defaultDimensionView
        where defaultDimensionView.Name			                == _dimensionName
            && defaultDimensionView.DefaultDimension	== _dimensionDefault;
    return defaultDimensionView.DisplayValue;
}
// Uso de getDimensionValue
DimensionValue  dimValue;

dimValue    = JATDimensionUtils::getDimensionValue(custTable.DefaultDimension, JATDimensionUtils::CostCenterName);
CheckDimensionValue

Con este método seremos capaces de validar si un valor concreto existe en una dimensión concreta, independientemente de si estas dimensiones son estándar o personalizadas.
Parámetro: Nombre y valor de la dimensión que queremos validar.
Retorno: Booleano que indicará si este valor existe o no en la dimensión indicada.

public static boolean checkDimensionValue(DimensionRefFieldName _dimensionName, DimensionValue _dimensionValue)
{
    DimensionAttributeDirCategory   dimAttributeDirCategory;
    DimensionAttribute		        dimensionAttribute;
    DimAttributeOMCostCenter	    dimAttributeOMCostCenter;
    DimAttributeOMBusinessUnit	    dimAttributeOMBusinessUnit;
    DimAttributeOMDepartment	    dimAttributeOMDepartment;
    DimensionFinancialTag		    dimensionFinancialTag;
    DimAttributeCustTable		    dimAttributeCustTable;
    DimAttributeVendTable		    dimAttributeVendTable;
    DimAttributeProjTable		    dimAttributeProjTable;
    DimAttributeHcmWorker		    dimAttributeHcmWorker;
    boolean				            ret = true;
    
    switch (_dimensionName)
    {
        case JATDimensionUtils::CustomDimName:
            dimensionAttribute	= DimensionAttribute::findByName(_dimensionName);
            select firstOnly RecId 
                from dimAttributeDirCategory
                    where dimAttributeDirCategory.DimensionAttribute == dimensionAttribute.RecId
                join dimensionFinancialTag
                    where dimensionFinancialTag.FinancialTagCategory == dimAttributeDirCategory.RecId
                        && dimensionFinancialTag.Value		 == _dimensionValue;

            if (!dimAttributeDirCategory.RecId)
            {
                ret = checkFailed(strFmt("@JAT:DimensionNotFound", _dimensionValue, _dimensionName)); 
            }
            break;

        case JATDimensionUtils::BusinessUnitName:
            select firstonly RecId
                from dimAttributeOMBusinessUnit
                where dimAttributeOMBusinessUnit.Value	== _dimensionValue;
            if (!dimAttributeOMBusinessUnit.RecId)
            {
                ret = checkFailed(strFmt("@JAT:DimensionNotFound", _dimensionValue, _dimensionName));
            }
            break;

        case JATDimensionUtils::CostCenterName:
            select firstonly RecId
                from dimAttributeOMCostCenter
                where dimAttributeOMCostCenter.Value	== _dimensionValue;
            if (!dimAttributeOMCostCenter.RecId)
            {
                ret = checkFailed(strFmt("@JAT:DimensionNotFound", _dimensionValue, _dimensionName));
            }
            break;

        case JATDimensionUtils::DepartmentName:
            select firstonly RecId
                from dimAttributeOMDepartment
                where dimAttributeOMDepartment.Value	== _dimensionValue;
            if (!dimAttributeOMDepartment.RecId)
            {
                ret = checkFailed(strFmt("@JAT:DimensionNotFound", _dimensionValue, _dimensionName));
            }
            break;

        case JATDimensionUtils::ProjectName:
            select firstonly RecId
                from dimAttributeProjTable
                where dimAttributeProjTable.Value	== _dimensionValue;
            if (!dimAttributeProjTable.RecId)
            {
                ret = checkFailed(strFmt("@JAT:DimensionNotFound", _dimensionValue, _dimensionName));
            }
            break;

        case JATDimensionUtils::CustomerName:
            select firstonly RecId
                from dimAttributeCustTable
                where dimAttributeCustTable.Value	== _dimensionValue;
            if (!dimAttributeCustTable.RecId)
            {
                ret = checkFailed(strFmt("@JAT:DimensionNotFound", _dimensionValue, _dimensionName));
            }
            break;

        case JATDimensionUtils::VendorName:
            select firstonly RecId
                from dimAttributeVendTable
                where dimAttributeVendTable.Value	== _dimensionValue;
            if (!dimAttributeVendTable.RecId)
            {
                ret = checkFailed(strFmt("@JAT:DimensionNotFound", _dimensionValue, _dimensionName));
            }
            break;

        case JATDimensionUtils::WorkerName:
            select firstonly RecId
                from dimAttributeHcmWorker
                where dimAttributeHcmWorker.Value	== _dimensionValue;
            if (!dimAttributeHcmWorker.RecId)
            {
                ret = checkFailed(strFmt("@JAT:DimensionNotFound", _dimensionValue, _dimensionName));
            }
            break;
            
        default:
            ret = checkFailed(strFmt("@JAT:DimensionNotFound", _dimensionValue, _dimensionName));
            break;
    }
    
    return ret;
}
// Uso de checkDimensionValue
ret = JATDimensionUtils::checkDimensionValue(JATDimensionUtils::CostCenterName, "CC01");

Y hasta aquí mi pequeña ayuda para trabajar con dimensiones. Cualquier corrección, aporte, idea o comentario será más que bienvenido. Saludos!