diff --git a/src/Layers/W1/BaseApp/Purchases/Document/PurchLineReserve.Codeunit.al b/src/Layers/W1/BaseApp/Purchases/Document/PurchLineReserve.Codeunit.al index d9c1d74f3cd..b979d066332 100644 --- a/src/Layers/W1/BaseApp/Purchases/Document/PurchLineReserve.Codeunit.al +++ b/src/Layers/W1/BaseApp/Purchases/Document/PurchLineReserve.Codeunit.al @@ -311,6 +311,7 @@ codeunit 99000834 "Purch. Line-Reserve" var OldReservationEntry: Record "Reservation Entry"; OppositeReservationEntry: Record "Reservation Entry"; + ApplyToItemEntryNo: Integer; NotFullyReserved: Boolean; IsHandled: Boolean; begin @@ -353,16 +354,23 @@ codeunit 99000834 "Purch. Line-Reserve" OldReservationEntry.TestItemFields(PurchaseLine."No.", PurchaseLine."Variant Code", PurchaseLine."Location Code"); if CheckApplToItemEntry then begin + ApplyToItemEntryNo := OldReservationEntry."Appl.-to Item Entry"; if OldReservationEntry."Reservation Status" = OldReservationEntry."Reservation Status"::Reservation then begin OppositeReservationEntry.Get(OldReservationEntry."Entry No.", not OldReservationEntry.Positive); if OppositeReservationEntry."Source Type" <> Database::"Item Ledger Entry" then - NotFullyReserved := true; + NotFullyReserved := true + else + // Line is fully reserved against an item ledger entry, so the exact cost reversing + // link can be derived from that entry when it was not set on the reservation entry. + if ApplyToItemEntryNo = 0 then + ApplyToItemEntryNo := OppositeReservationEntry."Source Ref. No."; end else NotFullyReserved := true; if OldReservationEntry."Item Tracking" <> OldReservationEntry."Item Tracking"::None then begin - OldReservationEntry.TestField("Appl.-to Item Entry"); - CreateReservEntry.SetApplyToEntryNo(OldReservationEntry."Appl.-to Item Entry"); + if ApplyToItemEntryNo = 0 then + OldReservationEntry.TestField("Appl.-to Item Entry"); + CreateReservEntry.SetApplyToEntryNo(ApplyToItemEntryNo); CheckApplToItemEntry := false; end; end; diff --git a/src/Layers/W1/Tests/SCM/SCMCostingBatch.Codeunit.al b/src/Layers/W1/Tests/SCM/SCMCostingBatch.Codeunit.al index e7f42cc33bb..63a1f21d546 100644 --- a/src/Layers/W1/Tests/SCM/SCMCostingBatch.Codeunit.al +++ b/src/Layers/W1/Tests/SCM/SCMCostingBatch.Codeunit.al @@ -929,7 +929,7 @@ codeunit 137402 "SCM Costing Batch" end; [Test] - [HandlerFunctions('StatisticsMessageHandler')] + [HandlerFunctions('AdjustCostItemEntriesHandler,StatisticsMessageHandler')] [Scope('OnPrem')] procedure CapacityCostPostedToGLForTwoItemsInOneProductionOrder() var @@ -1075,7 +1075,7 @@ codeunit 137402 "SCM Costing Batch" end; [Test] - [HandlerFunctions('StatisticsMessageHandler')] + [HandlerFunctions('AdjustCostItemEntriesHandler,StatisticsMessageHandler')] [Scope('OnPrem')] procedure PurchasePostingDoesNotPropagateACYToSourceCurrencyOnGLEntry() var diff --git a/src/Layers/W1/Tests/SCM/SCMCostingPurchReturnsII.Codeunit.al b/src/Layers/W1/Tests/SCM/SCMCostingPurchReturnsII.Codeunit.al index 8fddc6252a8..2e810d6a25e 100644 --- a/src/Layers/W1/Tests/SCM/SCMCostingPurchReturnsII.Codeunit.al +++ b/src/Layers/W1/Tests/SCM/SCMCostingPurchReturnsII.Codeunit.al @@ -14,6 +14,9 @@ codeunit 137032 "SCM Costing Purch Returns II" LibraryPurchase: Codeunit "Library - Purchase"; LibraryInventory: Codeunit "Library - Inventory"; LibraryCosting: Codeunit "Library - Costing"; + LibraryItemTracking: Codeunit "Library - Item Tracking"; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; + LibraryUtility: Codeunit "Library - Utility"; Assert: Codeunit Assert; LibraryTestInitialize: Codeunit "Library - Test Initialize"; isInitialized: Boolean; @@ -187,6 +190,68 @@ codeunit 137032 "SCM Costing Purch Returns II" UpdatePurchasesPayablesSetup(BaseExactCostReversingMand, BaseExactCostReversingMand); end; + [Test] + [HandlerFunctions('ItemTrackingLinesAssignLotPageHandler,PostedPurchaseDocumentLinesModalHandler,ReservationReserveFromCurrentLinePageHandler,ItemTrackingListPageHandler,ConfirmHandler')] + [Scope('OnPrem')] + procedure PurchReturnReserveSpecificLotExactCostReversing() + var + Item: Record Item; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + ItemLedgerEntry: Record "Item Ledger Entry"; + BaseExactCostReversingMand: Boolean; + VendorNo: Code[20]; + LotNo: Code[50]; + Quantity: Decimal; + begin + // [SCENARIO] A purchase return created via Get Posted Document Lines to Reverse and then reserved from + // the current line against the receipt item ledger entry (specific lot) posts with Exact Cost Reversing Mandatory. + // [BUG] The Reserve action dropped "Appl.-to Item Entry" on the reservation entry, so posting failed with + // "Appl.-to Item Entry must have a value in Reservation Entry..." and the manual workaround then failed with + // "Applies-to Entry must not be filled out when reservations exist...". + + // [GIVEN] Exact Cost Reversing Mandatory and a lot-tracked item. + Initialize(); + UpdatePurchasesPayablesSetup(BaseExactCostReversingMand, true); + LibraryItemTracking.CreateLotItem(Item); + Quantity := 1; + LotNo := CopyStr(LibraryUtility.GenerateGUID(), 1, MaxStrLen(LotNo)); + + // [GIVEN] A posted purchase order (receive + invoice) with a specific lot assigned. + VendorNo := CreateAndPostPurchOrderWithLot(Item."No.", LotNo, Quantity); + + // [GIVEN] A purchase return order populated via Get Posted Document Lines to Reverse. + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::"Return Order", VendorNo); + UpdatePurchaseHeader(PurchaseHeader); + PurchaseHeader.GetPstdDocLinesToReverse(); + + // [GIVEN] The return line is reserved from the current line against the receipt entry (specific lot). + PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type"); + PurchaseLine.SetRange("Document No.", PurchaseHeader."No."); + PurchaseLine.SetRange(Type, PurchaseLine.Type::Item); + PurchaseLine.FindFirst(); + PurchaseLine.ShowReservation(); + + // [WHEN] Posting the purchase return order (ship + invoice). + LibraryPurchase.PostPurchaseDocument(PurchaseHeader, true, true); + + // [THEN] The inbound receipt entry for the lot is fully reversed (exact cost reversing applied to the correct entry). + ItemLedgerEntry.SetRange("Item No.", Item."No."); + ItemLedgerEntry.SetRange("Entry Type", ItemLedgerEntry."Entry Type"::Purchase); + ItemLedgerEntry.SetRange("Lot No.", LotNo); + ItemLedgerEntry.SetRange(Positive, true); + ItemLedgerEntry.FindFirst(); + ItemLedgerEntry.TestField("Remaining Quantity", 0); + + // [THEN] A return (outbound) entry exists for the same lot. + ItemLedgerEntry.SetRange(Positive, false); + Assert.IsFalse(ItemLedgerEntry.IsEmpty(), 'Expected an outbound return entry for the reserved lot.'); + + // Tear Down. + UpdatePurchasesPayablesSetup(BaseExactCostReversingMand, BaseExactCostReversingMand); + LibraryVariableStorage.AssertEmpty(); + end; + [Test] [Scope('OnPrem')] procedure PurchReturnItemAVGFIFO() @@ -881,6 +946,22 @@ codeunit 137032 "SCM Costing Purch Returns II" begin end; + local procedure CreateAndPostPurchOrderWithLot(ItemNo: Code[20]; LotNo: Code[50]; Quantity: Decimal): Code[20] + var + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + begin + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, ''); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, ItemNo, Quantity); + PurchaseLine.Validate("Direct Unit Cost", LibraryRandom.RandDec(100, 2) + 50); + PurchaseLine.Modify(true); + LibraryVariableStorage.Enqueue(LotNo); // Enqueue value for ItemTrackingLinesAssignLotPageHandler. + LibraryVariableStorage.Enqueue(Quantity); // Enqueue value for ItemTrackingLinesAssignLotPageHandler. + PurchaseLine.OpenItemTrackingLines(); + LibraryPurchase.PostPurchaseDocument(PurchaseHeader, true, true); + exit(PurchaseHeader."Buy-from Vendor No."); + end; + [ModalPageHandler] [Scope('OnPrem')] procedure PostedPurchaseDocumentLinesModalHandler(var PostedPurchaseDocumentLinesPage: TestPage "Posted Purchase Document Lines") @@ -895,5 +976,29 @@ codeunit 137032 "SCM Costing Purch Returns II" begin Reply := true; end; + + [ModalPageHandler] + [Scope('OnPrem')] + procedure ItemTrackingLinesAssignLotPageHandler(var ItemTrackingLines: TestPage "Item Tracking Lines") + begin + ItemTrackingLines."Lot No.".SetValue(LibraryVariableStorage.DequeueText()); + ItemTrackingLines."Quantity (Base)".SetValue(LibraryVariableStorage.DequeueDecimal()); + ItemTrackingLines.OK().Invoke(); + end; + + [ModalPageHandler] + [Scope('OnPrem')] + procedure ReservationReserveFromCurrentLinePageHandler(var Reservation: TestPage Reservation) + begin + Reservation."Reserve from Current Line".Invoke(); + Reservation.OK().Invoke(); + end; + + [ModalPageHandler] + [Scope('OnPrem')] + procedure ItemTrackingListPageHandler(var ItemTrackingList: TestPage "Item Tracking List") + begin + ItemTrackingList.OK().Invoke(); + end; }