7 min readRishi

Financial Dimensions in D365 Finance: How They Really Work

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:

TableHolds
DimensionAttributeThe dimension definitions (Department, CostCenter…)
DimensionAttributeValueA single value for one attribute (CostCenter "CC010")
DimensionAttributeValueCombinationA full combination (the accounting string)
DimensionAttributeValueSetA set of values for default dimensions
DimensionAttributeLevelValueThe 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. DimensionAttributeValue can exist and still be invalid as of the posting date. Check active dates, not just findByDimensionAttributeAndValue.
  • Name vs label. DimensionAttribute.Name is the development name. findByName on 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 into DimensionAttributeValueSetItem, returns no rows. Both RecId types are int64.
  • 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 DimensionAttributeLevelValue on 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

  1. Identify every default dimension that should participate, and the merge order (line beats header beats master).
  2. Build defaults with DimensionAttributeValueSetStorage.save(), not with inserts into DAVC.
  3. Create the ledger dimension from main account plus merged default through DimensionDefaultingService, then validate against the active account structure.
  4. Persist the ledger RecId on the voucher line, not the default RecId.
  5. Post a journal where the structure requires a tag the vendor default does not supply. Confirm a structured error, not a posted blank.
  6. Compare voucher DisplayValue to the source document, and confirm the report column that reads DimensionAttributeLevelValue is populated.

Remaining bugs are merge order, a structure the form did not enforce yet, and a report looking at the wrong RecId.

Keep reading

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.