Describe the issue
Posting a transfer order for an item with lot + package + expiration tracking is quadratic in the number of packages of the same lot.
Item Jnl.-Post Line.CheckExpirationDate → GetExistingExpirationDateFromILE → Item Tracking Management.ExistingExpirationDateAndQty → FindLastItemLedgerEntry.
Two compounding defects:
1. FindLastItemLedgerEntry applies tracking filters as else-if, not AND
https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Inventory/Tracking/ItemTrackingManagement.Codeunit.al
if ItemTrackingSetup."Lot No." <> '' then
ItemLedgEntry.SetRange("Lot No.", ItemTrackingSetup."Lot No.")
else
if ItemTrackingSetup."Serial No." <> '' then
ItemLedgEntry.SetRange("Serial No.", ItemTrackingSetup."Serial No.")
else
if ItemTrackingSetup."Package No." <> '' then
ItemLedgEntry.SetRange("Package No.", ItemTrackingSetup."Package No.");
The key used on the same procedure already includes Package No.:
"Item No.", Open, "Variant Code", Positive, "Lot No.", "Serial No.", "Package No."
When Lot No. is filled, Package No. and Serial No. are ignored. ExistingExpirationDateAndQty then CalcSums("Remaining Quantity") over every positive ILE of the lot.
2. GetExistingExpirationDateFromILE looks up by New tracking, and the package fallback never runs when the lot exists
https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Inventory/Posting/ItemJnlPostLine.Codeunit.al
ItemTrackingSetup.CopyTrackingFromNewTrackingSpec(TempTrackingSpecification);
Transfer reservation / tracking lines often have Lot No. + Package No. filled and New Lot No. / New Package No. blank (same identity at destination). CopyTrackingFromNewTrackingSpec then supplies a blank package.
The fallback only runs when the first lookup found nothing:
if ((ExistingExpirationDate = 0D) and (SumOfEntries = 0)) and
((New Lot No. = Lot No.) and (New Package No. <> Package No.))
A lot-only lookup always finds the lot, so SumOfEntries is the remaining qty of the whole lot. The fallback that would CopyTrackingFromTrackingSpec (and include Package No.) never executes.
CheckExpirationDate then does:
if (SumOfEntries > 0) and
((SumOfEntries <> SumLot) or (New Lot No. <> Lot No.))
then
TempTrackingSpecification.TestField("New Expiration Date", ExistingExpirationDate);
SumLot is the current split (e.g. 1 kg). SumOfEntries is the lot (e.g. 2000 kg). They never match, so every package TestFields New Expiration Date. If that field is blank the post fails with New Expiration Date must be equal to '…'. Current value is ''. If it is filled, the post succeeds but each of N packages still does a lot-wide FindLast + CalcSums → O(N × lot size).
CPU profile on a ~2000-package transfer (same lot): ~420 s / 88% of posting in this SQL:
SELECT TOP (1) … Item Ledger Entry WHERE Item No_ + Positive + Variant + Lot No_ (no Package No.).
After an ISV stamped New Lot No. / New Package No. on the reservation entries, the same post dropped to ~71 s and this lookup to ~11 s. The product should not require that stamp.
Expected behavior
- FindLastItemLedgerEntry / ExistingExpirationDateAndQty apply every tracking dimension that is present (lot AND serial AND package).
- For a transfer that does not change lot/package, a blank New Lot No. / New Package No. is treated as “same as source”, so the ILE lookup is package-specific.
- Posting N packages of one lot is roughly linear in N, not quadratic in lot size.
- The New Expiration Date TestField is not raised solely because remaining qty of the lot differs from qty of this package.
Steps to reproduce
-
Item tracking code: Lot Specific Tracking = Yes, Package Tracking = Yes, Strict Expiration Posting = Yes (or Use Expiration Dates = Yes).
-
Post inbound inventory for one item, one lot, one expiration date, ~200–2000 packages (1 base unit each).
-
Create a transfer order (Direct Transfer = No is enough). Assign item tracking from existing ILE / Item Tracking Summary so each package is a tracking line. Leave New Lot No. / New Package No. / New Expiration Date blank (standard transfer: same identity).
-
Post shipment, then post receipt (or ship+receive).
-
Observe posting time growing with package count, and/or error:
New Expiration Date must be equal to '<lot expiration>'. Current value is ''.
Optional: In-client performance profiler on the Post action. The hot stack is CheckExpirationDate → GetExistingExpirationDateFromILE → ExistingExpirationDateAndQty → FindLastItemLedgerEntry.
Additional context
Proposed product fix (happy to PR after approval):
FindLastItemLedgerEntry: replace the else-if with independent filters (matches the key already set):
if ItemTrackingSetup."Lot No." <> '' then
ItemLedgEntry.SetRange("Lot No.", ItemTrackingSetup."Lot No.");
if ItemTrackingSetup."Serial No." <> '' then
ItemLedgEntry.SetRange("Serial No.", ItemTrackingSetup."Serial No.");
if ItemTrackingSetup."Package No." <> '' then
ItemLedgEntry.SetRange("Package No.", ItemTrackingSetup."Package No.");
-
GetExistingExpirationDateFromILE: before the lookup, if a New* tracking field is blank, inherit from the corresponding source field (transfer keeps the same lot/package). Then the existing package-change fallback can stay for true re-pack.
-
(Optional, smaller) OnBeforeExistingExpirationDateAndQty today only receives Lot No. and Serial No. Adding Package No. would let extenders handle this without replacing the whole procedure.
I will add an SCM test: transfer of several packages of one lot with expiration; New tracking fields blank; post must not error, and remaining-qty used for the New Expiration Date check must be the package qty not the lot qty.
This is still present on main (W1 ItemTrackingManagement / ItemJnlPostLine). Customer-impacting on current SaaS; we will also route via partner support for SLA. Opening here because we can contribute the AL change.
I will provide a fix for a bug
Describe the issue
Posting a transfer order for an item with lot + package + expiration tracking is quadratic in the number of packages of the same lot.
Item Jnl.-Post Line.CheckExpirationDate → GetExistingExpirationDateFromILE →Item Tracking Management.ExistingExpirationDateAndQty → FindLastItemLedgerEntry.Two compounding defects:
1. FindLastItemLedgerEntry applies tracking filters as else-if, not AND
https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Inventory/Tracking/ItemTrackingManagement.Codeunit.al
The key used on the same procedure already includes Package No.:
"Item No.", Open, "Variant Code", Positive, "Lot No.", "Serial No.", "Package No."When Lot No. is filled, Package No. and Serial No. are ignored. ExistingExpirationDateAndQty then CalcSums("Remaining Quantity") over every positive ILE of the lot.
2. GetExistingExpirationDateFromILE looks up by New tracking, and the package fallback never runs when the lot exists
https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Inventory/Posting/ItemJnlPostLine.Codeunit.al
Transfer reservation / tracking lines often have Lot No. + Package No. filled and New Lot No. / New Package No. blank (same identity at destination). CopyTrackingFromNewTrackingSpec then supplies a blank package.
The fallback only runs when the first lookup found nothing:
A lot-only lookup always finds the lot, so SumOfEntries is the remaining qty of the whole lot. The fallback that would CopyTrackingFromTrackingSpec (and include Package No.) never executes.
CheckExpirationDate then does:
SumLot is the current split (e.g. 1 kg). SumOfEntries is the lot (e.g. 2000 kg). They never match, so every package TestFields New Expiration Date. If that field is blank the post fails with New Expiration Date must be equal to '…'. Current value is ''. If it is filled, the post succeeds but each of N packages still does a lot-wide FindLast + CalcSums → O(N × lot size).
CPU profile on a ~2000-package transfer (same lot): ~420 s / 88% of posting in this SQL:
SELECT TOP (1) … Item Ledger Entry WHERE Item No_ + Positive + Variant + Lot No_(no Package No.).After an ISV stamped New Lot No. / New Package No. on the reservation entries, the same post dropped to ~71 s and this lookup to ~11 s. The product should not require that stamp.
Expected behavior
Steps to reproduce
Item tracking code: Lot Specific Tracking = Yes, Package Tracking = Yes, Strict Expiration Posting = Yes (or Use Expiration Dates = Yes).
Post inbound inventory for one item, one lot, one expiration date, ~200–2000 packages (1 base unit each).
Create a transfer order (Direct Transfer = No is enough). Assign item tracking from existing ILE / Item Tracking Summary so each package is a tracking line. Leave New Lot No. / New Package No. / New Expiration Date blank (standard transfer: same identity).
Post shipment, then post receipt (or ship+receive).
Observe posting time growing with package count, and/or error:
New Expiration Date must be equal to '<lot expiration>'. Current value is ''.Optional: In-client performance profiler on the Post action. The hot stack is CheckExpirationDate → GetExistingExpirationDateFromILE → ExistingExpirationDateAndQty → FindLastItemLedgerEntry.
Additional context
Proposed product fix (happy to PR after approval):
FindLastItemLedgerEntry: replace the else-if with independent filters (matches the key already set):GetExistingExpirationDateFromILE: before the lookup, if a New* tracking field is blank, inherit from the corresponding source field (transfer keeps the same lot/package). Then the existing package-change fallback can stay for true re-pack.(Optional, smaller)
OnBeforeExistingExpirationDateAndQtytoday only receives Lot No. and Serial No. Adding Package No. would let extenders handle this without replacing the whole procedure.I will add an SCM test: transfer of several packages of one lot with expiration; New tracking fields blank; post must not error, and remaining-qty used for the New Expiration Date check must be the package qty not the lot qty.
This is still present on
main(W1 ItemTrackingManagement / ItemJnlPostLine). Customer-impacting on current SaaS; we will also route via partner support for SLA. Opening here because we can contribute the AL change.I will provide a fix for a bug