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
1 2 3 4 5 6 7 8 9 10 11 12 |
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:
1 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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; } |
1 2 3 4 |
// 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
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(); } |
1 2 3 4 |
// 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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; } |
1 2 3 4 |
// 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.
1 2 3 4 5 6 7 8 9 10 |
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; } |
1 2 3 4 |
// 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
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; } |
1 2 |
// 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!