Financial Dimensions in D365 Finance: How They Really Work
A purchase line shows Department 022 and Cost center CC010. The voucher posts to the main account and the department is blank. The form looked right. The report reads the voucher. The tag never made it into the ledger dimension. That gap is the whole model: a default dimension is a set of tags a master or a line wants to inherit. A ledger dimension is a main account plus those tags, stored as one accounting string, and that is what financial reporting sees.
Cost center, department, business unit, project — they tag every transaction. Then a developer opens DimensionAttributeValueCombination joined to four other Dimension* tables and backs away. The path through that join is merge at posting, then validation.
Two RecIds, two jobs
There are exactly two flavours, and confusing them causes most dimension bugs.
- Default dimensions are a set of dimension values attached to a master record or line — a vendor, a fixed asset, a purchase line. They answer "what tags should transactions inherit from here?"
- Ledger dimensions are a main account plus its dimensions, combined into one accounting string. They answer "exactly which account did this voucher hit?"
A purchase order line carries a default dimension. When it posts, the framework merges that set with the main account to produce the ledger dimension on the voucher. Same values, different purpose, different storage. Copying DefaultDimension into a LedgerDimension field is how you post a combination with no main account.
The LedgerDimension and DefaultDimension fields on tables are RecId pointers into this structure. That is why you cannot read a cost center with a simple field access.
Where the values actually live
Every unique combination of dimension values is stored once in DimensionAttributeValueCombination (DAVC) and referenced by RecId. The supporting cast:
| Table | Holds |
|---|---|
DimensionAttribute | The dimension definitions (Department, CostCenter…) |
DimensionAttributeValue | A single value for one attribute (CostCenter "CC010") |
DimensionAttributeValueCombination | A full combination (the accounting string) |
DimensionAttributeValueSet | A set of values for default dimensions |
DimensionAttributeLevelValue | The exploded per-attribute rows for a combination |
DimensionAttributeValueSetItem is the per-attribute row on the default side. DimensionAttributeLevelValue is the exploded row on the ledger side. Reading the wrong child table for the RecId you have returns blank on a voucher that clearly posted.
Reading a default dimension
To get a human-readable value out of a default dimension, walk the value set:
public static str getDimensionValue(
DimensionDefault _defaultDimension, str _dimensionName)
{
DimensionAttributeValueSetItem setItem;
DimensionAttributeValue dimValue;
DimensionAttribute dimAttr;
select firstonly Value from setItem
where setItem.DimensionAttributeValueSet == _defaultDimension
join RecId from dimValue
where dimValue.RecId == setItem.DimensionAttributeValue
join Name from dimAttr
where dimAttr.RecId == dimValue.DimensionAttribute
&& dimAttr.Name == _dimensionName;
return setItem.DisplayValue;
}
For a ledger dimension, start from DimensionAttributeValueCombination where RecId is the LedgerDimension, and read DisplayValue for the accounting string. The exploded tags are DimensionAttributeLevelValue rows for that combination. If the string looks right and a report column is blank, the attribute was not in the combination. The form was looking at a default dimension the report does not read.
Building a default dimension
The supported way to create a combination is the DimensionDefaultingService / DimensionAttributeValueSetStorage helper, never hand-inserting into DAVC:
public static DimensionDefault buildDefaultDimension(
container _attrNames, container _values)
{
DimensionAttributeValueSetStorage storage =
new DimensionAttributeValueSetStorage();
for (int i = 1; i <= conLen(_attrNames); i++)
{
DimensionAttribute attr =
DimensionAttribute::findByName(conPeek(_attrNames, i));
if (attr.RecId)
{
DimensionAttributeValue dimValue =
DimensionAttributeValue::findByDimensionAttributeAndValue(
attr, conPeek(_values, i), false, true);
storage.addItem(dimValue);
}
}
return storage.save();
}
addItem plus save() deduplicates against existing combinations. findByDimensionAttributeAndValue with the create flag set will insert a DimensionAttributeValue if the value does not exist. That is a master-data write. Do not flip that flag on a posting path: a typo should not invent Cost center "CC01O".
Merge, then ledger
Posting does not pick one default dimension. It merges them. Line, header, vendor, main account: each can supply a tag. Empty on the more specific source is filled from the next. A value already set is not overwritten. DimensionDefaultingService::serviceMergeDefaultDimensions implements that. First argument wins on conflict; the second fills blanks.
DimensionDefault merged = DimensionDefaultingService::serviceMergeDefaultDimensions(
purchLine.DefaultDimension,
purchTable.DefaultDimension);
merged = DimensionDefaultingService::serviceMergeDefaultDimensions(
merged,
vendTable.DefaultDimension);
Order matters. Merge the line first, then the header, then the vendor, if line should win. Reverse that and a vendor default stamps over a line the clerk just set. Replay the same order the framework uses for that document, or the voucher will not match the form.
The ledger dimension is that merged set plus a main account. DimensionDefaultingService::serviceCreateLedgerDimension combines them: an account-only ledger dimension plus the default that carries the tags. The result is a RecId in DimensionAttributeValueCombination whose DisplayValue looks like 6000-022-CC010. Do not concatenate that string yourself and insert a DAVC row. Missing DimensionAttributeLevelValue rows show up in reporting, not at insert().
Validate after you build. Account structures and advanced rules, under General ledger > Chart of accounts > Structures, decide which shapes are legal. An account structure says "for main accounts 6000–6999, Department and CostCenter are required, BusinessUnit is blank." Advanced rules layer conditions on top — "if Department is Sales, also require SalesRegion."
That is why posting fails with "the dimension combination is not valid": the values exist, but the structure does not permit that shape for that main account. Building the RecId is not acceptance. Validate against the active structure before you insert a journal line, or catch the posting error and name the structure, the main account, and the tags you sent.
Gotchas that look like data bugs
- Suspended values.
DimensionAttributeValuecan exist and still be invalid as of the posting date. Check active dates, not justfindByDimensionAttributeAndValue. - Name vs label.
DimensionAttribute.Nameis the development name.findByNameon the user label returns nothing. - Blank is a value. Clearing a dimension on a line is not unspecified. An explicit blank blocks the vendor default from filling in, which is usually what the clerk wanted.
- Wrong child table. Default RecId into
DimensionAttributeLevelValue, or ledger RecId intoDimensionAttributeValueSetItem, returns no rows. Both RecId types areint64. - Reports read the voucher. Blank in reporting, fine on the source document: the default never merged into the ledger dimension. Financial reporting and Power BI read
DimensionAttributeLevelValueon the posted combination.
When not to build dimensions in code
Do not hand-build combinations in a job that "fixes" vouchers. Reverse and repost. Rewriting DAVC RecIds on posted transactions is how two reports disagree about the same voucher.
Do not bypass account structures for a migration. Import the values, then post through the journal so validation runs.
If the form already defaults correctly, do not add serviceMergeDefaultDimensions in insert. You will double-apply. Extend defaulting only when you have a source the framework does not know about.
Checklist before a custom posting path ships
- Identify every default dimension that should participate, and the merge order (line beats header beats master).
- Build defaults with
DimensionAttributeValueSetStorage.save(), not with inserts into DAVC. - Create the ledger dimension from main account plus merged default through
DimensionDefaultingService, then validate against the active account structure. - Persist the ledger RecId on the voucher line, not the default RecId.
- Post a journal where the structure requires a tag the vendor default does not supply. Confirm a structured error, not a posted blank.
- Compare voucher
DisplayValueto the source document, and confirm the report column that readsDimensionAttributeLevelValueis populated.
Remaining bugs are merge order, a structure the form did not enforce yet, and a report looking at the wrong RecId.
Keep reading
update_recordset: SQL Ran, and update() Never Did
update_recordset, insert_recordset, and delete_from send SQL now. They skip table methods, CoC, and events. Stamp the columns the method would have stamped, or stay on the row path.
FormDataSource.research: The Range You Added Never Ran
research reruns the live QueryRun. A range on query() waits for executeQuery. reread reloads only the current RecId, so an insert stays off the grid.
CacheLookup in X++: Found Serves the Stale Row Inside ttsbegin
Found serves the same CustTable inside ttsbegin. NotInTTS goes to SQL. EntireTable is for parameters. No unique index means the cache never hits.
crossCompany in X++: The Query That Mixes Legal Entities
A normal select stays in the current company. crossCompany returns every legal entity the user may read. How the company container, changeCompany, and OData differ.
Budget Control in Finance 10.0.49: Closed Periods, Reservations, and Broken Tracking
The budgeting features in Dynamics 365 Finance 10.0.49 — accounting-date advancement, reservation posting, encumbrance reporting, and a diagnostic for corrupt budget tracking.
Bank and Cash in Finance 10.0.49: Delayed Settlement, Match Preview, and Bridge Selection
What Dynamics 365 Finance 10.0.49 changes in payment journals and bank reconciliation, which pieces are still preview, and which bank behaviors you can no longer disable.
Newsletter
New posts, straight to your inbox
One email when a new post goes up. Double opt-in, unsubscribe anytime. Not a daily digest.
Comments
- No comments yet. Be the first.