From de822a56e0d41773ab840db7afb225e1ebd47158 Mon Sep 17 00:00:00 2001 From: v-nehanawal Date: Thu, 2 Jul 2026 16:43:17 +0530 Subject: [PATCH 1/7] Initial Commit --- .../Tracking/InventoryProfileOffsetting.Codeunit.al | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al b/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al index fd6f30c69d..3377cdf66f 100644 --- a/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al +++ b/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al @@ -2911,6 +2911,12 @@ codeunit 99000854 "Inventory Profile Offsetting" ReqLine.Validate( Quantity, Round(SupplyInventoryProfile."Remaining Quantity (Base)" / SupplyInventoryProfile."Qty. per Unit of Measure", UOMMgt.QtyRndPrecision())); + + if Abs(ReqLine."Quantity (Base)" - SupplyInventoryProfile."Remaining Quantity (Base)") <= UOMMgt.QtyRndPrecision() then + if ReqLine."Quantity (Base)" <> SupplyInventoryProfile."Remaining Quantity (Base)" then begin + ReqLine."Remaining Qty. (Base)" += SupplyInventoryProfile."Remaining Quantity (Base)" - ReqLine."Quantity (Base)"; + ReqLine."Quantity (Base)" := SupplyInventoryProfile."Remaining Quantity (Base)"; + end; end; local procedure UpdateReqLineOriginalQuantity(var SupplyInventoryProfile: Record "Inventory Profile") From 80c20f3ca4ab086b3d6497387c15f3d783b39b07 Mon Sep 17 00:00:00 2001 From: v-nehanawal Date: Fri, 3 Jul 2026 17:56:35 +0530 Subject: [PATCH 2/7] test case --- .../Tests/SCM/SCMPlanningNTFtests.Codeunit.al | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al b/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al index cad9314d5b..894d35e927 100644 --- a/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al +++ b/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al @@ -299,6 +299,50 @@ codeunit 137021 "SCM Planning - NTF tests" LibraryJob.CreateJobTask(Job, JobTask); end; + local procedure CreateItemWithNonBasePurchUoM(var Item: Record Item; var ItemUnitOfMeasure: Record "Item Unit of Measure"; QtyPerUoM: Decimal) + begin + ItemSetup(Item, Item."Replenishment System"::Purchase, '<0D>'); + Item.Validate("Reordering Policy", Item."Reordering Policy"::"Lot-for-Lot"); + Item.Validate("Vendor No.", LibraryPurchase.CreateVendorNo()); + LibraryInventory.CreateItemUnitOfMeasureCode(ItemUnitOfMeasure, Item."No.", QtyPerUoM); + Item.Validate("Purch. Unit of Measure", ItemUnitOfMeasure.Code); + Item.Modify(true); + end; + + local procedure AcceptAndCarryOutActionMessage(ItemNo: Code[20]) + var + RequisitionLine: Record "Requisition Line"; + begin + RequisitionLine.SetRange(Type, RequisitionLine.Type::Item); + RequisitionLine.SetRange("No.", ItemNo); + RequisitionLine.FindSet(true); + repeat + RequisitionLine.Validate("Accept Action Message", true); + RequisitionLine.Modify(true); + until RequisitionLine.Next() = 0; + LibraryPlanning.CarryOutActionMsgPlanWksh(RequisitionLine); + end; + + local procedure FindPurchaseLineByItemNo(var PurchaseLine: Record "Purchase Line"; ItemNo: Code[20]) + begin + PurchaseLine.SetRange("Document Type", PurchaseLine."Document Type"::Order); + PurchaseLine.SetRange(Type, PurchaseLine.Type::Item); + PurchaseLine.SetRange("No.", ItemNo); + PurchaseLine.FindFirst(); + end; + + local procedure VerifyReservEntryQtyBase(SourceType: Integer; SourceSubtype: Integer; SourceID: Code[20]; ExpectedQtyBase: Decimal) + var + ReservationEntry: Record "Reservation Entry"; + begin + ReservationEntry.SetRange("Source Type", SourceType); + ReservationEntry.SetRange("Source Subtype", SourceSubtype); + ReservationEntry.SetRange("Source ID", SourceID); + ReservationEntry.FindFirst(); + Assert.AreEqual(ExpectedQtyBase, Abs(ReservationEntry."Quantity (Base)"), + 'Reservation Entry Quantity (Base) must match demand.'); + end; + local procedure CreateSaleDocType(var SalesHeader: Record "Sales Header"; DocumentType: Enum "Sales Document Type"; Item: Record Item; SalesQty: Integer; ShipmentDate: Date; LocationCode: Code[10]) var SalesLine: Record "Sales Line"; @@ -3647,6 +3691,44 @@ codeunit 137021 "SCM Planning - NTF tests" RequisitionLine."Ref. Order Type"::Transfer, LocationTwo.Code, 1); end; + [Test] + [Scope('OnPrem')] + procedure JobPlanLineNonBaseUoMCarryOutDeletePlanLine() + var + Item: Record Item; + ItemUnitOfMeasure: Record "Item Unit of Measure"; + JobTask: Record "Job Task"; + JobPlanningLine: Record "Job Planning Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + QtyPerUoM: Decimal; + JobQuantity: Decimal; + begin + // [SCENARIO 640455] Cannot delete Project Planning Line due to incorrect reservation entries + + Initialize(); + + // [GIVEN] Item "I" with non-base Purchase UoM "U" with random Qty per UoM, Reordering Policy = Lot-for-Lot, and Job Planning Line "J" with random quantity. + TestSetup(); + QtyPerUoM := LibraryRandom.RandIntInRange(2, 5); + JobQuantity := LibraryRandom.RandIntInRange(1, 5); + CreateItemWithNonBasePurchUoM(Item, ItemUnitOfMeasure, QtyPerUoM); + CreateJobAndPlanningLine(JobTask, JobPlanningLine, Item."No.", JobQuantity, true); + + // [WHEN] Calculate Regenerative Plan is run and Action Message is carried out to create a Purchase Order "P". + LibraryPlanning.CalcRegenPlanForPlanWksh(Item, WorkDate(), CalcDate(PlanningEndDate, WorkDate())); + AcceptAndCarryOutActionMessage(Item."No."); + + // [THEN] Reservation entries linked to "P" have correct Quantity (Base) matching "J" quantity, and deleting "P" and then "J" succeeds without error. + FindPurchaseLineByItemNo(PurchaseLine, Item."No."); + PurchaseHeader.Get(PurchaseHeader."Document Type"::Order, PurchaseLine."Document No."); + VerifyReservEntryQtyBase( + Database::"Purchase Line", PurchaseHeader."Document Type".AsInteger(), PurchaseHeader."No.", JobQuantity); + PurchaseHeader.Delete(true); + JobPlanningLine.Find(); + JobPlanningLine.Delete(true); + end; + [ModalPageHandler] [Scope('OnPrem')] procedure ItemTrackingPageHandler(var ItemTrackingLines: TestPage "Item Tracking Lines") From 5e46c27c147a384ed72e41acae509875b8f70bea Mon Sep 17 00:00:00 2001 From: v-nehanawal Date: Fri, 10 Jul 2026 15:52:49 +0530 Subject: [PATCH 3/7] Comments fixed --- .../InventoryProfileOffsetting.Codeunit.al | 4 +++- .../Tests/SCM/SCMPlanningNTFtests.Codeunit.al | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al b/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al index c20b78a0da..a005e1538b 100644 --- a/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al +++ b/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al @@ -2912,7 +2912,9 @@ codeunit 99000854 "Inventory Profile Offsetting" Quantity, Round(SupplyInventoryProfile."Remaining Quantity (Base)" / SupplyInventoryProfile."Qty. per Unit of Measure", UOMMgt.QtyRndPrecision())); - if Abs(ReqLine."Quantity (Base)" - SupplyInventoryProfile."Remaining Quantity (Base)") <= UOMMgt.QtyRndPrecision() then + // Rounding the quantity to the base unit of measure can differ from the demand by up to half a rounding step per unit of measure. + // When the difference is only caused by this conversion rounding, align the base quantity with the demand to avoid invalid reservation entries. + if Abs(ReqLine."Quantity (Base)" - SupplyInventoryProfile."Remaining Quantity (Base)") <= SupplyInventoryProfile."Qty. per Unit of Measure" * UOMMgt.QtyRndPrecision() then if ReqLine."Quantity (Base)" <> SupplyInventoryProfile."Remaining Quantity (Base)" then begin ReqLine."Remaining Qty. (Base)" += SupplyInventoryProfile."Remaining Quantity (Base)" - ReqLine."Quantity (Base)"; ReqLine."Quantity (Base)" := SupplyInventoryProfile."Remaining Quantity (Base)"; diff --git a/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al b/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al index 894d35e927..b15b6812b7 100644 --- a/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al +++ b/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al @@ -343,6 +343,14 @@ codeunit 137021 "SCM Planning - NTF tests" 'Reservation Entry Quantity (Base) must match demand.'); end; + local procedure VerifyNoReservEntryExists(ItemNo: Code[20]) + var + ReservationEntry: Record "Reservation Entry"; + begin + ReservationEntry.SetRange("Item No.", ItemNo); + Assert.RecordIsEmpty(ReservationEntry); + end; + local procedure CreateSaleDocType(var SalesHeader: Record "Sales Header"; DocumentType: Enum "Sales Document Type"; Item: Record Item; SalesQty: Integer; ShipmentDate: Date; LocationCode: Code[10]) var SalesLine: Record "Sales Line"; @@ -3708,10 +3716,11 @@ codeunit 137021 "SCM Planning - NTF tests" Initialize(); - // [GIVEN] Item "I" with non-base Purchase UoM "U" with random Qty per UoM, Reordering Policy = Lot-for-Lot, and Job Planning Line "J" with random quantity. + // [GIVEN] Item "I" with non-base Purchase UoM "U" with Qty per UoM = 12, Reordering Policy = Lot-for-Lot, and Job Planning Line "J" with quantity = 1. + // [GIVEN] Qty per UoM = 12 and quantity = 1 reproduce the base-quantity conversion rounding mismatch (1/12 rounded to 0.00001 precision converts back to 0.99996). TestSetup(); - QtyPerUoM := LibraryRandom.RandIntInRange(2, 5); - JobQuantity := LibraryRandom.RandIntInRange(1, 5); + QtyPerUoM := 12; + JobQuantity := 1; CreateItemWithNonBasePurchUoM(Item, ItemUnitOfMeasure, QtyPerUoM); CreateJobAndPlanningLine(JobTask, JobPlanningLine, Item."No.", JobQuantity, true); @@ -3719,7 +3728,7 @@ codeunit 137021 "SCM Planning - NTF tests" LibraryPlanning.CalcRegenPlanForPlanWksh(Item, WorkDate(), CalcDate(PlanningEndDate, WorkDate())); AcceptAndCarryOutActionMessage(Item."No."); - // [THEN] Reservation entries linked to "P" have correct Quantity (Base) matching "J" quantity, and deleting "P" and then "J" succeeds without error. + // [THEN] Reservation entries linked to "P" have correct Quantity (Base) matching "J" quantity, and deleting "P" and then "J" succeeds without leaving orphan reservation entries. FindPurchaseLineByItemNo(PurchaseLine, Item."No."); PurchaseHeader.Get(PurchaseHeader."Document Type"::Order, PurchaseLine."Document No."); VerifyReservEntryQtyBase( @@ -3727,6 +3736,7 @@ codeunit 137021 "SCM Planning - NTF tests" PurchaseHeader.Delete(true); JobPlanningLine.Find(); JobPlanningLine.Delete(true); + VerifyNoReservEntryExists(Item."No."); end; [ModalPageHandler] From e0a92bfc53bba688c1e7b277b82ad3979aa3c291 Mon Sep 17 00:00:00 2001 From: v-nehanawal Date: Tue, 14 Jul 2026 19:52:03 +0530 Subject: [PATCH 4/7] test desc fixed --- src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al b/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al index b15b6812b7..72a8370178 100644 --- a/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al +++ b/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al @@ -3712,7 +3712,7 @@ codeunit 137021 "SCM Planning - NTF tests" QtyPerUoM: Decimal; JobQuantity: Decimal; begin - // [SCENARIO 640455] Cannot delete Project Planning Line due to incorrect reservation entries + // [SCENARIO 640455] Job Planning Line with non-base Purchase UoM can be deleted after carrying out the planning Action Message, without orphan reservation entries. Initialize(); From bf968c5408d2fe98b79255658563fbbd0df7f8c9 Mon Sep 17 00:00:00 2001 From: v-nehanawal Date: Thu, 16 Jul 2026 10:07:54 +0530 Subject: [PATCH 5/7] resolved comments --- .../Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al b/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al index a005e1538b..8c2318c5c1 100644 --- a/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al +++ b/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al @@ -2914,7 +2914,7 @@ codeunit 99000854 "Inventory Profile Offsetting" // Rounding the quantity to the base unit of measure can differ from the demand by up to half a rounding step per unit of measure. // When the difference is only caused by this conversion rounding, align the base quantity with the demand to avoid invalid reservation entries. - if Abs(ReqLine."Quantity (Base)" - SupplyInventoryProfile."Remaining Quantity (Base)") <= SupplyInventoryProfile."Qty. per Unit of Measure" * UOMMgt.QtyRndPrecision() then + if Abs(ReqLine."Quantity (Base)" - SupplyInventoryProfile."Remaining Quantity (Base)") <= SupplyInventoryProfile."Qty. per Unit of Measure" * UOMMgt.QtyRndPrecision() / 2 then if ReqLine."Quantity (Base)" <> SupplyInventoryProfile."Remaining Quantity (Base)" then begin ReqLine."Remaining Qty. (Base)" += SupplyInventoryProfile."Remaining Quantity (Base)" - ReqLine."Quantity (Base)"; ReqLine."Quantity (Base)" := SupplyInventoryProfile."Remaining Quantity (Base)"; From f85a3a87bea94d2a834aff8ca5fe271d6418f610 Mon Sep 17 00:00:00 2001 From: v-nehanawal Date: Fri, 17 Jul 2026 17:43:14 +0530 Subject: [PATCH 6/7] suggested changes fix --- .../Tracking/InventoryProfileOffsetting.Codeunit.al | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al b/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al index 8c2318c5c1..fe913c984e 100644 --- a/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al +++ b/src/Layers/W1/BaseApp/Inventory/Tracking/InventoryProfileOffsetting.Codeunit.al @@ -2901,6 +2901,7 @@ codeunit 99000854 "Inventory Profile Offsetting" local procedure UpdateReqLineQuantity(var SupplyInventoryProfile: Record "Inventory Profile") var + QtyRoundingPrecisionBase: Decimal; IsHandled: Boolean; begin IsHandled := false; @@ -2912,9 +2913,12 @@ codeunit 99000854 "Inventory Profile Offsetting" Quantity, Round(SupplyInventoryProfile."Remaining Quantity (Base)" / SupplyInventoryProfile."Qty. per Unit of Measure", UOMMgt.QtyRndPrecision())); - // Rounding the quantity to the base unit of measure can differ from the demand by up to half a rounding step per unit of measure. - // When the difference is only caused by this conversion rounding, align the base quantity with the demand to avoid invalid reservation entries. - if Abs(ReqLine."Quantity (Base)" - SupplyInventoryProfile."Remaining Quantity (Base)") <= SupplyInventoryProfile."Qty. per Unit of Measure" * UOMMgt.QtyRndPrecision() / 2 then + QtyRoundingPrecisionBase := ReqLine."Qty. Rounding Precision (Base)"; + if QtyRoundingPrecisionBase = 0 then + QtyRoundingPrecisionBase := UOMMgt.QtyRndPrecision(); + if Abs(ReqLine."Quantity (Base)" - SupplyInventoryProfile."Remaining Quantity (Base)") <= + (SupplyInventoryProfile."Qty. per Unit of Measure" * UOMMgt.QtyRndPrecision() + QtyRoundingPrecisionBase) / 2 + then if ReqLine."Quantity (Base)" <> SupplyInventoryProfile."Remaining Quantity (Base)" then begin ReqLine."Remaining Qty. (Base)" += SupplyInventoryProfile."Remaining Quantity (Base)" - ReqLine."Quantity (Base)"; ReqLine."Quantity (Base)" := SupplyInventoryProfile."Remaining Quantity (Base)"; From c0919768fadbd0e7aaa81b87f7c3dd81b04a0cee Mon Sep 17 00:00:00 2001 From: v-nehanawal Date: Fri, 17 Jul 2026 17:44:37 +0530 Subject: [PATCH 7/7] test case fix --- .../W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al b/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al index 72a8370178..24b01d8300 100644 --- a/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al +++ b/src/Layers/W1/Tests/SCM/SCMPlanningNTFtests.Codeunit.al @@ -334,12 +334,16 @@ codeunit 137021 "SCM Planning - NTF tests" local procedure VerifyReservEntryQtyBase(SourceType: Integer; SourceSubtype: Integer; SourceID: Code[20]; ExpectedQtyBase: Decimal) var ReservationEntry: Record "Reservation Entry"; + TotalQtyBase: Decimal; begin ReservationEntry.SetRange("Source Type", SourceType); ReservationEntry.SetRange("Source Subtype", SourceSubtype); ReservationEntry.SetRange("Source ID", SourceID); - ReservationEntry.FindFirst(); - Assert.AreEqual(ExpectedQtyBase, Abs(ReservationEntry."Quantity (Base)"), + if ReservationEntry.FindSet() then + repeat + TotalQtyBase += Abs(ReservationEntry."Quantity (Base)"); + until ReservationEntry.Next() = 0; + Assert.AreEqual(ExpectedQtyBase, TotalQtyBase, 'Reservation Entry Quantity (Base) must match demand.'); end; @@ -3712,14 +3716,14 @@ codeunit 137021 "SCM Planning - NTF tests" QtyPerUoM: Decimal; JobQuantity: Decimal; begin - // [SCENARIO 640455] Job Planning Line with non-base Purchase UoM can be deleted after carrying out the planning Action Message, without orphan reservation entries. + // [SCENARIO 642150] Job Planning Line with non-base Purchase UoM can be deleted after carrying out the planning Action Message, without orphan reservation entries. Initialize(); - // [GIVEN] Item "I" with non-base Purchase UoM "U" with Qty per UoM = 12, Reordering Policy = Lot-for-Lot, and Job Planning Line "J" with quantity = 1. - // [GIVEN] Qty per UoM = 12 and quantity = 1 reproduce the base-quantity conversion rounding mismatch (1/12 rounded to 0.00001 precision converts back to 0.99996). + // [GIVEN] Item "I" with non-base Purchase UoM "U" with Qty per UoM = 1.5, Reordering Policy = Lot-for-Lot, and Job Planning Line "J" with quantity = 1. + // [GIVEN] Qty per UoM = 1.5 and quantity = 1 reproduce a base-quantity conversion rounding mismatch that also depends on the base rounding step (1/1.5 rounded to 0.00001 precision converts back to 1.00001). TestSetup(); - QtyPerUoM := 12; + QtyPerUoM := 1.5; JobQuantity := 1; CreateItemWithNonBasePurchUoM(Item, ItemUnitOfMeasure, QtyPerUoM); CreateJobAndPlanningLine(JobTask, JobPlanningLine, Item."No.", JobQuantity, true);