Skip to main content

Extending Dual Unit of Measure

This page is a technical reference for developers who extend or integrate with the Dual Unit of Measure app. It covers topics that are important to be aware of to avoid common pitfalls. This page will be updated over time as we add more guidance.

Units Fields on Ledger Entries

The following section covers correct and incorrect usage patterns for the units fields the app adds to the Item Ledger Entry, Warehouse Entry, and Value Entry tables.

These fields are NOT being removed

The Units_DU_TSL field on Item Ledger Entry, Warehouse Entry, and Value Entry is marked ObsoleteState = Pending not because it will be removed, but to draw attention to important usage constraints described on this page. The ObsoleteState acts as a built-in code review prompt.

Background

The Dual Unit of Measure app extends three core Business Central tables with a units field:

Table ExtensionFieldCompanion Aggregation Table
ItemLedgerEntry_DU_TSLUnits_DU_TSLItemLedgerEntryUnits_DU_TSL
WarehouseEntry_DU_TSLUnits_DU_TSLWarehouseEntryUnits_DU_TSL
ValueEntry_DU_TSLItem Ledger Units_DU_TSL(none — see below)

The units field is always populated at the time an entry is posted. Its value always equals the units value for that entry at the time of posting.

The companion aggregation tables (ItemLedgerEntryUnits_DU_TSL and WarehouseEntryUnits_DU_TSL) exist specifically to support efficient aggregation. They are fully indexed and designed to be used with CalcSums and as the basis for FlowFields.

Value Entry — no companion table

Value Entry does not have a companion aggregation table. Summing units across value entries is not a supported scenario — the units on a value entry are there for reference only (they reflect the units of the associated Item Ledger Entry). If you find yourself needing to aggregate units at the value entry level, reconsider the design: aggregate at the Item Ledger Entry level using ItemLedgerEntryUnits_DU_TSL instead.


Usage Rules

✅ Safe: Direct access on an existing record

Reading Units_DU_TSL directly from a record you already have in context is safe and perfectly correct. Because the field is always populated at posting time, you can rely on its value.

// ✅ CORRECT - direct access on a record you already hold
ItemLedgerEntry.FindFirst();
Units := ItemLedgerEntry.Units_DU_TSL;

When you access the field this way, suppress the obsolescence warning with a #pragma and include a brief comment explaining the reason. We strongly recommend this practice even though it may seem like extra ceremony — here is why:

The performance risk with these fields is easy to introduce accidentally and easy to miss. A CalcSums across a handful of records in a test environment will complete instantly and raise no alarms. The problem only surfaces in production environments with large data volumes, often long after the code was written. By requiring a pragma with an explanation at every usage site, you create a clear, searchable record of every place the field is accessed directly. Any future reviewer — or your future self — can immediately see that the usage was a deliberate and considered decision, not an oversight.

#pragma warning disable AL0432
// Accessing Units_DU_TSL directly - safe here as we already have the ILE record and not summing
Units := ItemLedgerEntry.Units_DU_TSL;
#pragma warning restore AL0432

❌ Unsafe: CalcSums on Units_DU_TSL

CalcSums on a table extension field that is not part of a SumIndexField key causes a full table scan. On the Item Ledger Entry table, which can contain millions of rows in a production environment, this leads to severe performance degradation. The same applies to Warehouse Entry and Value Entry.

Do not CalcSums on Value Entry units

There is no companion aggregation table for Value Entry. If you need aggregate unit figures, use ItemLedgerEntryUnits_DU_TSL — do not attempt to sum Item Ledger Units_DU_TSL on Value Entry.

// ❌ WRONG - do not do this
ItemLedgerEntry.SetRange("Item No.", ItemNo);
ItemLedgerEntry.CalcSums(Units_DU_TSL);
TotalUnits := ItemLedgerEntry.Units_DU_TSL;

❌ Unsafe: FlowFields based on Units_DU_TSL

For the same reason, do not create FlowFields that sum Units_DU_TSL from Item Ledger Entry, Warehouse Entry, or Value Entry. Without a SumIndexField, the aggregation is unindexed.

// ❌ WRONG - do not define a FlowField like this
field(50000; "Total Units"; Decimal)
{
FieldClass = FlowField;
CalcFormula = sum(ItemLedgerEntry.Units_DU_TSL where("Item No." = field("No.")));
}

✅ Correct: Aggregations using the companion tables

The companion tables ItemLedgerEntryUnits_DU_TSL and WarehouseEntryUnits_DU_TSL are fully indexed and exist specifically for this purpose. Use CalcSums against these tables, or base FlowFields on them.

// ✅ CORRECT - use the indexed companion table for aggregations
ILEUnits.SetRange("Item No.", ItemNo);
ILEUnits.SetRange("Posting Date", StartDate, EndDate);
ILEUnits.CalcSums(Units_DU_TSL);
TotalUnits := ILEUnits.Units_DU_TSL;
// ✅ CORRECT - FlowField based on the indexed companion table
field(50000; "Total Units"; Decimal)
{
FieldClass = FlowField;
CalcFormula = sum(ItemLedgerEntryUnits_DU_TSL.Units_DU_TSL where("Item No." = field("No.")));
}

Summary

ScenarioRecommended Approach
Read units from an ILE/WE/VE record you already haveUse the units field directly. Suppress the warning with a pragma and document why.
Sum units across many ILE recordsUse ItemLedgerEntryUnits_DU_TSL with CalcSums.
Sum units across many WE recordsUse WarehouseEntryUnits_DU_TSL with CalcSums.
Sum units across Value EntriesNot supported — aggregate at ILE level using ItemLedgerEntryUnits_DU_TSL.
Define a FlowField that sums unitsBase the FlowField on ItemLedgerEntryUnits_DU_TSL or WarehouseEntryUnits_DU_TSL.
Avoid the warning without direct needRevisit whether you need the field at all, or restructure to use the companion table.

Integration Events

The app publishes the following integration events. Subscribe to these events in your own extension to extend or override app behaviour without modifying the app itself.

Inventory

EventRaised whenParameters
OnBeforeFillSourceQuantityArrayForCustomItem tracking quantities are being resolved for a source document type that is not natively handled (e.g. a custom document). Raised after all standard source types (sales, purchase, transfer, production) have been evaluated.SourceQuantityArray — quantity array being built; TrackingSpecification — the tracking spec record; NewSourceUnitsArray — units array to populate
OnBeforeValidateResidualsValidation is about to check that residual quantity or units are not left behind when the opposing measure reaches zero (e.g. units remain but base quantity is zero). Set IsHandled := true to skip the default residual check.ItemNo, CurrentQtyBase, NewQtyBase, CurrentUnits, NewUnits, RecordVariant, IsHandled
OnBeforeOpenPage (ItemLedgerEntryUnits page)The Item Ledger Entry Units page is opening, before any filter logic runs.(none)

Item Tracking

EventRaised whenParameters
OnBeforeOnAfterRegisterItemTrackingLinesItem tracking lines have just been registered. Raised before the app's own post-registration logic runs. Set IsHandled := true to suppress the default behaviour.TrackingSpecification, TempTrackingSpecification, CurrTrackingSpecification, AvailabilityDate, IsCorrection, IsHandled
OnBeforeCheckUnitsOnSetupSplitJnlLineOnBeforeCalcNonDistrQuantityAn item journal line is being split across tracking specifications and the app is about to validate that the units balance across the split. Set IsHandled := true to override the default check.TempTrackingSpecification, ItemJournalLine, SignFactor, IsHandled

Sales

EventRaised whenParameters
OnBeforeCheckBlanketOrderQuantityAndUnitsA sales blanket order is being converted to a sales order and the app is about to verify that the units on the order do not exceed the units outstanding on the blanket line. Set IsHandled := true to override the default check.SalesLine, IsHandled

Warehousing — Receipts

EventRaised whenParameters
OnAfterInitUnitsToReceive_DU_TSLUnits to Receive on a warehouse receipt line has been initialised (set to Units Outstanding). Raised after all default initialisation logic has completed.CurrentFieldNo — the field that triggered the initialisation
OnAfterCopyFromPurchaseLine_DU_TSL (Receipt)A warehouse receipt line has been populated from a purchase line. Raised after all Dual UOM fields have been copied.PurchaseLine
OnAfterCopyFromSalesLine_DU_TSL (Receipt)A warehouse receipt line has been populated from a sales return line. Raised after all Dual UOM fields have been copied.SalesLine
OnAfterCopyFromTransferLine_DU_TSL (Receipt)A warehouse receipt line has been populated from a transfer line (inbound). Raised after all Dual UOM fields have been copied.TransferLine

Warehousing — Shipments

EventRaised whenParameters
OnAfterCopyFromSalesLine_DU_TSL (Shipment)A warehouse shipment line has been populated from a sales line. Raised after all Dual UOM fields have been copied.SalesLine
OnAfterCopyFromPurchaseLine_DU_TSL (Shipment)A warehouse shipment line has been populated from a purchase return line. Raised after all Dual UOM fields have been copied.PurchaseLine
OnAfterCopyFromTransferLine_DU_TSL (Shipment)A warehouse shipment line has been populated from a transfer line (outbound). Raised after all Dual UOM fields have been copied.TransferLine
OnBeforeSyncWhseShipmentLineUnitsToSourceDocument_DU_TSLA registered pick is about to update the Units Picked and Units to Ship on the warehouse shipment line and synchronise the over/under shipment back to the source document. Set IsHandled := true to skip the entire synchronisation.WarehouseShipmentLine, WarehouseActivityLine, TotalUnits, IsHandled, NewIsUndoPostingDelayValidate
OnAfterSyncWhseShipmentLineUnitsToSourceDocument_DU_TSLUnits Picked and Units to Ship have been updated on the warehouse shipment line and the over/under shipment has been synchronised to the source document. Raised after all synchronisation is complete.WarehouseShipmentLine, WarehouseActivityLine

Warehousing — Journal

EventRaised whenParameters
OnBeforeValidateUnitsCalculated_DU_TSLThe Units_DU_TSL field on a warehouse journal line is being validated and the app is about to calculate the units value automatically. Set IsHandled := true to provide a custom units value and prevent the default calculation.WarehouseJournalLine, IsHandled