diff --git a/src/Layers/RU/Tests/SCM-Reservation/SCMItemTracking.Codeunit.al b/src/Layers/RU/Tests/SCM-Reservation/SCMItemTracking.Codeunit.al index e4556baf006..a37f279ebe6 100644 --- a/src/Layers/RU/Tests/SCM-Reservation/SCMItemTracking.Codeunit.al +++ b/src/Layers/RU/Tests/SCM-Reservation/SCMItemTracking.Codeunit.al @@ -5738,6 +5738,276 @@ codeunit 137405 "SCM Item Tracking" exit(ItemLedgerEntry.Quantity); end; + [Test] + [HandlerFunctions('ItemTrackingLinesLotSNQtyModalPageHandler')] + [Scope('OnPrem')] + procedure GetAvailableLotQtyExcludesUnregisteredWhsePickAllocation() + var + Item: Record Item; + Location: Record Location; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + TrackingSpecification: Record "Tracking Specification"; + ItemTrackingDataCollection: Codeunit "Item Tracking Data Collection"; + LotNo: Code[50]; + Qty: Integer; + begin + // [Bug 638344] Available lot quantity must exclude quantities committed to an unregistered warehouse pick + // [SCENARIO] A lot fully allocated to an unregistered warehouse-pick Take line is not reported as available for a different demand source + Initialize(); + + // [GIVEN] Lot-tracked item with lot warehouse tracking, in stock at a location for lot "L" with qty "Q" + Qty := LibraryRandom.RandIntInRange(20, 50); + LotNo := LibraryUtility.GenerateGUID(); + CreateLotTrackedItemAtLocation(Item, Location); + CreateAndPostLotStockForPick(Item."No.", Location.Code, LotNo, Qty); + + // [GIVEN] An unregistered warehouse pick Take line reserves the full lot quantity for a different sales line + CreateUnregisteredWhsePickTakeLine( + WarehouseActivityHeader, WarehouseActivityLine, Item."No.", Location.Code, LotNo, Qty, + Database::"Sales Line", 1, LibraryUtility.GenerateGUID(), 10000); + + // [WHEN] Available lot quantity is retrieved for a new demand on the same lot + SetTrackingSpecItemLotLocation(TrackingSpecification, Item."No.", Location.Code, LotNo); + + // [THEN] Available quantity is zero because the on-hand lot is fully committed to the unregistered pick + Assert.AreEqual( + 0, ItemTrackingDataCollection.GetAvailableLotQty(TrackingSpecification), + 'Available lot quantity must exclude quantities allocated to unregistered warehouse picks.'); + + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('ItemTrackingLinesLotSNQtyModalPageHandler')] + [Scope('OnPrem')] + procedure GetAvailableLotQtyExcludesUnregisteredInvtPickAllocation() + var + Item: Record Item; + Location: Record Location; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + TrackingSpecification: Record "Tracking Specification"; + ItemTrackingDataCollection: Codeunit "Item Tracking Data Collection"; + LotNo: Code[50]; + Qty: Integer; + begin + // [Bug 638344] Available lot quantity must exclude quantities committed to an unregistered inventory pick + // [SCENARIO] A lot fully allocated to an unregistered Invt. Pick line (blank Action Type) is not reported as available for a different demand source + Initialize(); + + // [GIVEN] Lot-tracked item with lot warehouse tracking, in stock at a location for lot "L" with qty "Q" + Qty := LibraryRandom.RandIntInRange(20, 50); + LotNo := LibraryUtility.GenerateGUID(); + CreateLotTrackedItemAtLocation(Item, Location); + CreateAndPostLotStockForPick(Item."No.", Location.Code, LotNo, Qty); + + // [GIVEN] An unregistered inventory pick line reserves the full lot quantity for a different sales line + CreateUnregisteredInvtPickLine( + WarehouseActivityHeader, WarehouseActivityLine, Item."No.", Location.Code, LotNo, Qty, + Database::"Sales Line", 1, LibraryUtility.GenerateGUID(), 10000); + + // [WHEN] Available lot quantity is retrieved for a new demand on the same lot + SetTrackingSpecItemLotLocation(TrackingSpecification, Item."No.", Location.Code, LotNo); + + // [THEN] Available quantity is zero because the on-hand lot is fully committed to the unregistered inventory pick + Assert.AreEqual( + 0, ItemTrackingDataCollection.GetAvailableLotQty(TrackingSpecification), + 'Available lot quantity must exclude quantities allocated to unregistered inventory picks.'); + + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('ItemTrackingLinesLotSNQtyModalPageHandler')] + [Scope('OnPrem')] + procedure GetAvailableLotQtyDoesNotDoubleCountPickWithSourceLineTracking() + var + Item: Record Item; + Location: Record Location; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + TrackingSpecification: Record "Tracking Specification"; + ItemTrackingDataCollection: Codeunit "Item Tracking Data Collection"; + LotNo: Code[50]; + SalesOrderNo: Code[20]; + OnHandQty: Integer; + PickQty: Integer; + begin + // [Bug 638344] An unregistered pick whose source line already carries item-tracking reservation must not be counted twice + // [SCENARIO] Production flow assigns source-line tracking (reservation entry) and then creates a pick for the same lot; the pick and reservation are one allocation + Initialize(); + + // [GIVEN] Lot-tracked item with lot warehouse tracking, on hand qty "Q" for lot "L" + OnHandQty := LibraryRandom.RandIntInRange(40, 60); + PickQty := LibraryRandom.RandIntInRange(10, 20); + LotNo := LibraryUtility.GenerateGUID(); + SalesOrderNo := LibraryUtility.GenerateGUID(); + CreateLotTrackedItemAtLocation(Item, Location); + CreateAndPostLotStockForPick(Item."No.", Location.Code, LotNo, OnHandQty); + + // [GIVEN] Source-line item tracking on a sales line reserves "P" of the lot (as production flow assigns tracking) + CreateOutboundLotReservationForSalesLine(Item."No.", Location.Code, LotNo, SalesOrderNo, 10000, PickQty); + + // [GIVEN] An unregistered warehouse pick Take line for the SAME sales line and lot moves the same "P" + CreateUnregisteredWhsePickTakeLine( + WarehouseActivityHeader, WarehouseActivityLine, Item."No.", Location.Code, LotNo, PickQty, + Database::"Sales Line", 1, SalesOrderNo, 10000); + + // [WHEN] Available lot quantity is retrieved for a new demand on the same lot + SetTrackingSpecItemLotLocation(TrackingSpecification, Item."No.", Location.Code, LotNo); + + // [THEN] Available equals on hand minus the single allocation, not minus both the reservation and the pick + Assert.AreEqual( + OnHandQty - PickQty, ItemTrackingDataCollection.GetAvailableLotQty(TrackingSpecification), + 'Available lot quantity must not double-count a pick already represented by a source-line reservation.'); + + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('ItemTrackingLinesLotSNQtyModalPageHandler')] + [Scope('OnPrem')] + procedure GetAvailableLotQtyNetsSourceReservationOnceAcrossSplitPickTakeLines() + var + Item: Record Item; + Location: Record Location; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + TrackingSpecification: Record "Tracking Specification"; + ItemTrackingDataCollection: Codeunit "Item Tracking Data Collection"; + LotNo: Code[50]; + SalesOrderNo: Code[20]; + OnHandQty: Integer; + ReservedQty: Integer; + FirstTakeQty: Integer; + SecondTakeQty: Integer; + begin + // [Bug 638344] A partial source reservation must be netted only once against the aggregate of split Take lines + // [SCENARIO] A pick has two Take lines (e.g. after SplitLine or when taken from multiple bins) for the same source and lot, with a smaller source reservation; the full picked quantity must be committed + Initialize(); + + // [GIVEN] Lot-tracked item with lot warehouse tracking, on hand qty "Q" for lot "L" + // [GIVEN] Take lines of 7 and 3 with a partial source reservation of 5 (the exact case that used to overstate availability by 3) + OnHandQty := LibraryRandom.RandIntInRange(40, 60); + FirstTakeQty := 7; + SecondTakeQty := 3; + ReservedQty := 5; + LotNo := LibraryUtility.GenerateGUID(); + SalesOrderNo := LibraryUtility.GenerateGUID(); + CreateLotTrackedItemAtLocation(Item, Location); + CreateAndPostLotStockForPick(Item."No.", Location.Code, LotNo, OnHandQty); + + // [GIVEN] Source-line item tracking on the sales line reserves only part of the lot ("R" < total picked) + CreateOutboundLotReservationForSalesLine(Item."No.", Location.Code, LotNo, SalesOrderNo, 10000, ReservedQty); + + // [GIVEN] Two unregistered warehouse pick Take lines for the SAME sales line and lot (split pick) + CreateUnregisteredWhsePickTakeLine( + WarehouseActivityHeader, WarehouseActivityLine, Item."No.", Location.Code, LotNo, FirstTakeQty, + Database::"Sales Line", 1, SalesOrderNo, 10000); + AddWhsePickTakeLine( + WarehouseActivityHeader, Item."No.", Location.Code, LotNo, SecondTakeQty, + Database::"Sales Line", 1, SalesOrderNo, 10000, 20000); + + // [WHEN] Available lot quantity is retrieved for a new demand on the same lot + SetTrackingSpecItemLotLocation(TrackingSpecification, Item."No.", Location.Code, LotNo); + + // [THEN] Available equals on hand minus the full picked quantity; the reservation is netted only once, not once per Take line + Assert.AreEqual( + OnHandQty - (FirstTakeQty + SecondTakeQty), ItemTrackingDataCollection.GetAvailableLotQty(TrackingSpecification), + 'A partial source reservation must be netted only once against the aggregate of split Take lines.'); + + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('ItemTrackingLinesLotSNQtyModalPageHandler')] + [Scope('OnPrem')] + procedure GetAvailableLotQtyNetsProductionComponentReservationOnce() + var + Item: Record Item; + Location: Record Location; + ReservationEntry: Record "Reservation Entry"; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + TrackingSpecification: Record "Tracking Specification"; + ItemTrackingDataCollection: Codeunit "Item Tracking Data Collection"; + LotNo: Code[50]; + ProductionOrderNo: Code[20]; + OnHandQty: Integer; + PickQty: Integer; + ProductionOrderLineNo: Integer; + ComponentLineNo: Integer; + NextEntryNo: Integer; + begin + // [Bug 638344] Production component source pointers must net the pick reservation once + // [SCENARIO] A production component reservation and its unregistered pick represent one allocation + Initialize(); + + OnHandQty := LibraryRandom.RandIntInRange(40, 60); + PickQty := LibraryRandom.RandIntInRange(10, 20); + LotNo := LibraryUtility.GenerateGUID(); + ProductionOrderNo := LibraryUtility.GenerateGUID(); + ProductionOrderLineNo := 10000; + ComponentLineNo := 20000; + CreateLotTrackedItemAtLocation(Item, Location); + CreateAndPostLotStockForPick(Item."No.", Location.Code, LotNo, OnHandQty); + + ReservationEntry.LockTable(); + if ReservationEntry.FindLast() then + NextEntryNo := ReservationEntry."Entry No." + 1 + else + NextEntryNo := 1; + ReservationEntry.Init(); + ReservationEntry."Entry No." := NextEntryNo; + ReservationEntry.Positive := false; + ReservationEntry."Reservation Status" := ReservationEntry."Reservation Status"::Surplus; + ReservationEntry."Item No." := Item."No."; + ReservationEntry."Location Code" := Location.Code; + ReservationEntry."Quantity (Base)" := -PickQty; + ReservationEntry.Quantity := -PickQty; + ReservationEntry."Qty. to Handle (Base)" := -PickQty; + ReservationEntry."Source Type" := Database::"Prod. Order Component"; + ReservationEntry."Source Subtype" := 1; + ReservationEntry."Source ID" := ProductionOrderNo; + ReservationEntry."Source Prod. Order Line" := ProductionOrderLineNo; + ReservationEntry."Source Ref. No." := ComponentLineNo; + ReservationEntry."Lot No." := LotNo; + ReservationEntry."Item Tracking" := ReservationEntry."Item Tracking"::"Lot No."; + ReservationEntry.Insert(false); + + WarehouseActivityHeader.Init(); + WarehouseActivityHeader.Type := WarehouseActivityHeader.Type::Pick; + WarehouseActivityHeader."No." := LibraryUtility.GenerateGUID(); + WarehouseActivityHeader."Location Code" := Location.Code; + WarehouseActivityHeader.Insert(false); + WarehouseActivityLine.Init(); + WarehouseActivityLine."Activity Type" := WarehouseActivityLine."Activity Type"::Pick; + WarehouseActivityLine."No." := WarehouseActivityHeader."No."; + WarehouseActivityLine."Line No." := 10000; + WarehouseActivityLine."Action Type" := WarehouseActivityLine."Action Type"::Take; + WarehouseActivityLine."Item No." := Item."No."; + WarehouseActivityLine."Location Code" := Location.Code; + WarehouseActivityLine."Lot No." := LotNo; + WarehouseActivityLine."Qty. Outstanding" := PickQty; + WarehouseActivityLine."Qty. Outstanding (Base)" := PickQty; + WarehouseActivityLine."Breakbulk No." := 0; + WarehouseActivityLine."Source Type" := Database::"Prod. Order Component"; + WarehouseActivityLine."Source Subtype" := 1; + WarehouseActivityLine."Source No." := ProductionOrderNo; + WarehouseActivityLine."Source Line No." := ProductionOrderLineNo; + WarehouseActivityLine."Source Subline No." := ComponentLineNo; + WarehouseActivityLine.Insert(false); + + SetTrackingSpecItemLotLocation(TrackingSpecification, Item."No.", Location.Code, LotNo); + + Assert.AreEqual( + OnHandQty - PickQty, ItemTrackingDataCollection.GetAvailableLotQty(TrackingSpecification), + 'A production component reservation and its unregistered pick must be counted as one allocation.'); + + LibraryVariableStorage.AssertEmpty(); + end; + local procedure Initialize() var LibraryERMCountryData: Codeunit "Library - ERM Country Data"; @@ -7608,6 +7878,134 @@ codeunit 137405 "SCM Item Tracking" Item.Modify(true); end; + local procedure CreateOutboundLotReservationForSalesLine(ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]; SourceNo: Code[20]; SourceLineNo: Integer; Qty: Decimal) + var + ReservationEntry: Record "Reservation Entry"; + NextEntryNo: Integer; + begin + ReservationEntry.LockTable(); + if ReservationEntry.FindLast() then + NextEntryNo := ReservationEntry."Entry No." + 1 + else + NextEntryNo := 1; + + ReservationEntry.Init(); + ReservationEntry."Entry No." := NextEntryNo; + ReservationEntry.Positive := false; + ReservationEntry."Reservation Status" := ReservationEntry."Reservation Status"::Surplus; + ReservationEntry."Item No." := ItemNo; + ReservationEntry."Location Code" := LocationCode; + ReservationEntry."Quantity (Base)" := -Qty; + ReservationEntry.Quantity := -Qty; + ReservationEntry."Qty. to Handle (Base)" := -Qty; + ReservationEntry."Source Type" := Database::"Sales Line"; + ReservationEntry."Source Subtype" := 1; + ReservationEntry."Source ID" := SourceNo; + ReservationEntry."Source Ref. No." := SourceLineNo; + ReservationEntry."Lot No." := LotNo; + ReservationEntry."Item Tracking" := ReservationEntry."Item Tracking"::"Lot No."; + ReservationEntry.Insert(false); + end; + + local procedure CreateLotTrackedItemAtLocation(var Item: Record Item; var Location: Record Location) + var + InventoryPostingSetup: Record "Inventory Posting Setup"; + ItemTrackingCodeCode: Code[10]; + begin + LibraryWarehouse.CreateLocationWithInventoryPostingSetup(Location); + Location.Validate("Require Pick", true); + Location.Validate("Require Shipment", true); + Location.Modify(true); + + ItemTrackingCodeCode := CreateItemTrackingCodeLotSpecificWhseTracking(true); + CreateItem(Item, ItemTrackingCodeCode, '', LibraryUtility.GetGlobalNoSeriesCode()); + + if not InventoryPostingSetup.Get(Location.Code, Item."Inventory Posting Group") then + LibraryInventory.CreateInventoryPostingSetup(InventoryPostingSetup, Location.Code, Item."Inventory Posting Group"); + end; + + local procedure SetTrackingSpecItemLotLocation(var TrackingSpecification: Record "Tracking Specification"; ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]) + begin + Clear(TrackingSpecification); + TrackingSpecification."Item No." := ItemNo; + TrackingSpecification."Location Code" := LocationCode; + TrackingSpecification."Lot No." := LotNo; + end; + + local procedure CreateUnregisteredWhsePickTakeLine(var WarehouseActivityHeader: Record "Warehouse Activity Header"; var WarehouseActivityLine: Record "Warehouse Activity Line"; ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]; Qty: Decimal; SourceType: Integer; SourceSubtype: Integer; SourceNo: Code[20]; SourceLineNo: Integer) + begin + WarehouseActivityHeader.Init(); + WarehouseActivityHeader.Type := WarehouseActivityHeader.Type::Pick; + WarehouseActivityHeader."No." := LibraryUtility.GenerateGUID(); + WarehouseActivityHeader."Location Code" := LocationCode; + WarehouseActivityHeader.Insert(false); + + WarehouseActivityLine.Init(); + WarehouseActivityLine."Activity Type" := WarehouseActivityLine."Activity Type"::Pick; + WarehouseActivityLine."No." := WarehouseActivityHeader."No."; + WarehouseActivityLine."Line No." := 10000; + WarehouseActivityLine."Action Type" := WarehouseActivityLine."Action Type"::Take; + WarehouseActivityLine."Item No." := ItemNo; + WarehouseActivityLine."Location Code" := LocationCode; + WarehouseActivityLine."Lot No." := LotNo; + WarehouseActivityLine."Qty. Outstanding" := Qty; + WarehouseActivityLine."Qty. Outstanding (Base)" := Qty; + WarehouseActivityLine."Breakbulk No." := 0; + WarehouseActivityLine."Source Type" := SourceType; + WarehouseActivityLine."Source Subtype" := SourceSubtype; + WarehouseActivityLine."Source No." := SourceNo; + WarehouseActivityLine."Source Line No." := SourceLineNo; + WarehouseActivityLine.Insert(false); + end; + + local procedure AddWhsePickTakeLine(WarehouseActivityHeader: Record "Warehouse Activity Header"; ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]; Qty: Decimal; SourceType: Integer; SourceSubtype: Integer; SourceNo: Code[20]; SourceLineNo: Integer; LineNo: Integer) + var + WarehouseActivityLine: Record "Warehouse Activity Line"; + begin + WarehouseActivityLine.Init(); + WarehouseActivityLine."Activity Type" := WarehouseActivityLine."Activity Type"::Pick; + WarehouseActivityLine."No." := WarehouseActivityHeader."No."; + WarehouseActivityLine."Line No." := LineNo; + WarehouseActivityLine."Action Type" := WarehouseActivityLine."Action Type"::Take; + WarehouseActivityLine."Item No." := ItemNo; + WarehouseActivityLine."Location Code" := LocationCode; + WarehouseActivityLine."Lot No." := LotNo; + WarehouseActivityLine."Qty. Outstanding" := Qty; + WarehouseActivityLine."Qty. Outstanding (Base)" := Qty; + WarehouseActivityLine."Breakbulk No." := 0; + WarehouseActivityLine."Source Type" := SourceType; + WarehouseActivityLine."Source Subtype" := SourceSubtype; + WarehouseActivityLine."Source No." := SourceNo; + WarehouseActivityLine."Source Line No." := SourceLineNo; + WarehouseActivityLine.Insert(false); + end; + + local procedure CreateUnregisteredInvtPickLine(var WarehouseActivityHeader: Record "Warehouse Activity Header"; var WarehouseActivityLine: Record "Warehouse Activity Line"; ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]; Qty: Decimal; SourceType: Integer; SourceSubtype: Integer; SourceNo: Code[20]; SourceLineNo: Integer) + begin + WarehouseActivityHeader.Init(); + WarehouseActivityHeader.Type := WarehouseActivityHeader.Type::"Invt. Pick"; + WarehouseActivityHeader."No." := LibraryUtility.GenerateGUID(); + WarehouseActivityHeader."Location Code" := LocationCode; + WarehouseActivityHeader.Insert(false); + + WarehouseActivityLine.Init(); + WarehouseActivityLine."Activity Type" := WarehouseActivityLine."Activity Type"::"Invt. Pick"; + WarehouseActivityLine."No." := WarehouseActivityHeader."No."; + WarehouseActivityLine."Line No." := 10000; + WarehouseActivityLine."Action Type" := WarehouseActivityLine."Action Type"::" "; + WarehouseActivityLine."Item No." := ItemNo; + WarehouseActivityLine."Location Code" := LocationCode; + WarehouseActivityLine."Lot No." := LotNo; + WarehouseActivityLine."Qty. Outstanding" := Qty; + WarehouseActivityLine."Qty. Outstanding (Base)" := Qty; + WarehouseActivityLine."Breakbulk No." := 0; + WarehouseActivityLine."Source Type" := SourceType; + WarehouseActivityLine."Source Subtype" := SourceSubtype; + WarehouseActivityLine."Source No." := SourceNo; + WarehouseActivityLine."Source Line No." := SourceLineNo; + WarehouseActivityLine.Insert(false); + end; + [ModalPageHandler] [Scope('OnPrem')] procedure ItemTrackingLinesPageHandlerTrackingOption(var ItemTrackingLines: TestPage "Item Tracking Lines") diff --git a/src/Layers/W1/BaseApp/Inventory/Tracking/ItemTrackingDataCollection.Codeunit.al b/src/Layers/W1/BaseApp/Inventory/Tracking/ItemTrackingDataCollection.Codeunit.al index 940022cd9b1..16dfc06b684 100644 --- a/src/Layers/W1/BaseApp/Inventory/Tracking/ItemTrackingDataCollection.Codeunit.al +++ b/src/Layers/W1/BaseApp/Inventory/Tracking/ItemTrackingDataCollection.Codeunit.al @@ -7,10 +7,12 @@ namespace Microsoft.Inventory.Tracking; using Microsoft.Inventory.Item; using Microsoft.Inventory.Journal; using Microsoft.Inventory.Ledger; +using Microsoft.Inventory.Location; using Microsoft.Projects.Project.Journal; using Microsoft.Projects.Project.Planning; using Microsoft.Purchases.Document; using Microsoft.Sales.Document; +using Microsoft.Warehouse.Activity; using Microsoft.Warehouse.Ledger; codeunit 6501 "Item Tracking Data Collection" @@ -455,6 +457,8 @@ codeunit 6501 "Item Tracking Data Collection" end; until TempTrackingSpecification.Next() = 0; + TransferUnregisteredPicksToTempRec(TempTrackingSpecification2); + OnRetrieveLookupDataOnAfterTransferToTempRec(TempGlobalEntrySummary, TempTrackingSpecification, ItemLedgEntry, LastSummaryEntryNo); TempGlobalEntrySummary.Reset(); @@ -539,6 +543,168 @@ codeunit 6501 "Item Tracking Data Collection" until TempReservEntry.Next() = 0; end; + local procedure TransferUnregisteredPicksToTempRec(var TrackingSpecification: Record "Tracking Specification" temporary) + var + WhseActivLine: Record "Warehouse Activity Line"; + TempWhseActivLine: Record "Warehouse Activity Line" temporary; + IsHandled: Boolean; + begin + IsHandled := false; + OnBeforeTransferUnregisteredPicksToTempRec(TrackingSpecification, TempGlobalReservEntry, IsHandled); + if IsHandled then + exit; + + if TrackingSpecification.Positive then + exit; + if TrackingSpecification."Item No." = '' then + exit; + if TrackingSpecification."Location Code" = '' then + exit; + if not LocationRequiresPick(TrackingSpecification."Location Code") then + exit; + + WhseActivLine.SetCurrentKey("Item No.", "Location Code", "Activity Type", "Bin Type Code", + "Unit of Measure Code", "Variant Code", "Breakbulk No.", "Action Type"); + WhseActivLine.SetLoadFields("Item No.", "Variant Code", "Location Code", "Activity Type", "No.", "Line No.", + "Source Type", "Source Subtype", "Source No.", "Source Line No.", "Source Subline No.", + "Lot No.", "Serial No.", "Package No.", "Qty. Outstanding (Base)"); + WhseActivLine.SetRange("Item No.", TrackingSpecification."Item No."); + WhseActivLine.SetRange("Variant Code", TrackingSpecification."Variant Code"); + WhseActivLine.SetRange("Location Code", TrackingSpecification."Location Code"); + WhseActivLine.SetFilter("Activity Type", '%1|%2', WhseActivLine."Activity Type"::Pick, WhseActivLine."Activity Type"::"Invt. Pick"); + // Blank Action Type covers Inventory Pick lines (single-step pick with no Take/Place split). + WhseActivLine.SetFilter("Action Type", '%1|%2', WhseActivLine."Action Type"::Take, WhseActivLine."Action Type"::" "); + WhseActivLine.SetRange("Breakbulk No.", 0); + WhseActivLine.SetFilter("Qty. Outstanding (Base)", '>%1', 0); + OnTransferUnregisteredPicksToTempRecOnAfterSetFilters(WhseActivLine, TrackingSpecification); + + if WhseActivLine.FindSet() then + repeat + if (WhseActivLine."Lot No." <> '') or (WhseActivLine."Serial No." <> '') or (WhseActivLine."Package No." <> '') then + if not PickBelongsToCurrentSource(WhseActivLine, TrackingSpecification) then + AggregateUnregisteredPick(TempWhseActivLine, WhseActivLine); + until WhseActivLine.Next() = 0; + + if TempWhseActivLine.FindSet() then + repeat + AddUnregisteredPickToTempRec(TempWhseActivLine, TrackingSpecification); + until TempWhseActivLine.Next() = 0; + + OnAfterTransferUnregisteredPicksToTempRec(TrackingSpecification, TempGlobalReservEntry); + end; + + local procedure AggregateUnregisteredPick(var TempWhseActivLine: Record "Warehouse Activity Line" temporary; var WhseActivLine: Record "Warehouse Activity Line") + begin + // Split Take lines (e.g. after SplitLine or when taken from multiple bins) can repeat the same source and + // tracking identity. Group their outstanding quantity so the source reservation is netted only once against + // the aggregate, instead of subtracting it from every line and overstating availability. + TempWhseActivLine.Reset(); + TempWhseActivLine.SetRange("Source Type", WhseActivLine."Source Type"); + TempWhseActivLine.SetRange("Source Subtype", WhseActivLine."Source Subtype"); + TempWhseActivLine.SetRange("Source No.", WhseActivLine."Source No."); + TempWhseActivLine.SetRange("Source Line No.", WhseActivLine."Source Line No."); + TempWhseActivLine.SetRange("Source Subline No.", WhseActivLine."Source Subline No."); + TempWhseActivLine.SetRange("Item No.", WhseActivLine."Item No."); + TempWhseActivLine.SetRange("Variant Code", WhseActivLine."Variant Code"); + TempWhseActivLine.SetRange("Location Code", WhseActivLine."Location Code"); + TempWhseActivLine.SetRange("Serial No.", WhseActivLine."Serial No."); + TempWhseActivLine.SetRange("Lot No.", WhseActivLine."Lot No."); + TempWhseActivLine.SetRange("Package No.", WhseActivLine."Package No."); + if TempWhseActivLine.FindFirst() then begin + TempWhseActivLine."Qty. Outstanding (Base)" += WhseActivLine."Qty. Outstanding (Base)"; + TempWhseActivLine.Modify(); + end else begin + TempWhseActivLine.Reset(); + TempWhseActivLine := WhseActivLine; + TempWhseActivLine.Insert(); + end; + TempWhseActivLine.Reset(); + end; + + local procedure AddUnregisteredPickToTempRec(var WhseActivLine: Record "Warehouse Activity Line"; var TrackingSpecification: Record "Tracking Specification" temporary) + var + QtyToAddBase: Decimal; + begin + // A pick tied to another source line may already be represented by that line's transferred item-tracking + // reservation. Net it out so the same physical allocation is not counted twice. + QtyToAddBase := WhseActivLine."Qty. Outstanding (Base)" - ReservedQtyForPickSource(WhseActivLine); + if QtyToAddBase <= 0 then + exit; + + LastReservEntryNo -= 1; + TempGlobalReservEntry.Init(); + TempGlobalReservEntry."Entry No." := LastReservEntryNo; + TempGlobalReservEntry."Reservation Status" := TempGlobalReservEntry."Reservation Status"::Prospect; + TempGlobalReservEntry.Positive := false; + TempGlobalReservEntry."Item No." := WhseActivLine."Item No."; + TempGlobalReservEntry."Variant Code" := WhseActivLine."Variant Code"; + TempGlobalReservEntry."Location Code" := WhseActivLine."Location Code"; + TempGlobalReservEntry."Quantity (Base)" := -QtyToAddBase; + TempGlobalReservEntry."Qty. to Handle (Base)" := -QtyToAddBase; + TempGlobalReservEntry."Source Type" := Database::"Warehouse Activity Line"; + TempGlobalReservEntry."Source Subtype" := WhseActivLine."Activity Type".AsInteger(); + TempGlobalReservEntry."Source ID" := WhseActivLine."No."; + if WhseActivLine."Source Type" = 5407 then begin + TempGlobalReservEntry."Source Prod. Order Line" := WhseActivLine."Source Line No."; + TempGlobalReservEntry."Source Ref. No." := WhseActivLine."Source Subline No."; + end else + TempGlobalReservEntry."Source Ref. No." := WhseActivLine."Line No."; + TempGlobalReservEntry."Serial No." := WhseActivLine."Serial No."; + TempGlobalReservEntry."Lot No." := WhseActivLine."Lot No."; + TempGlobalReservEntry."Package No." := WhseActivLine."Package No."; + TempGlobalReservEntry."Shipment Date" := DMY2Date(31, 12, 9999); + TempGlobalReservEntry.Insert(); + CreateEntrySummary(TrackingSpecification, TempGlobalReservEntry); + end; + + local procedure ReservedQtyForPickSource(var WhseActivLine: Record "Warehouse Activity Line") ReservedQtyBase: Decimal + begin + // Sum the item-tracking reservation already transferred for the pick's own source line and tracking. + TempGlobalReservEntry.Reset(); + TempGlobalReservEntry.SetRange("Source Type", WhseActivLine."Source Type"); + TempGlobalReservEntry.SetRange("Source Subtype", WhseActivLine."Source Subtype"); + TempGlobalReservEntry.SetRange("Source ID", WhseActivLine."Source No."); + if WhseActivLine."Source Type" = 5407 then begin + TempGlobalReservEntry.SetRange("Source Prod. Order Line", WhseActivLine."Source Line No."); + TempGlobalReservEntry.SetRange("Source Ref. No.", WhseActivLine."Source Subline No."); + end else + TempGlobalReservEntry.SetRange("Source Ref. No.", WhseActivLine."Source Line No."); + TempGlobalReservEntry.SetRange("Item No.", WhseActivLine."Item No."); + TempGlobalReservEntry.SetRange("Variant Code", WhseActivLine."Variant Code"); + TempGlobalReservEntry.SetRange("Location Code", WhseActivLine."Location Code"); + TempGlobalReservEntry.SetRange("Serial No.", WhseActivLine."Serial No."); + TempGlobalReservEntry.SetRange("Lot No.", WhseActivLine."Lot No."); + TempGlobalReservEntry.SetRange("Package No.", WhseActivLine."Package No."); + if TempGlobalReservEntry.FindSet() then + repeat + ReservedQtyBase += Abs(TempGlobalReservEntry."Quantity (Base)"); + until TempGlobalReservEntry.Next() = 0; + TempGlobalReservEntry.Reset(); + end; + + local procedure PickBelongsToCurrentSource(var WhseActivLine: Record "Warehouse Activity Line"; var TrackingSpecification: Record "Tracking Specification" temporary): Boolean + begin + if (WhseActivLine."Source Type" <> TrackingSpecification."Source Type") or (WhseActivLine."Source Subtype" <> TrackingSpecification."Source Subtype") or + (WhseActivLine."Source No." <> TrackingSpecification."Source ID") then + exit(false); + + if WhseActivLine."Source Type" = 5407 then + exit((WhseActivLine."Source Line No." = TrackingSpecification."Source Prod. Order Line") and + (WhseActivLine."Source Subline No." = TrackingSpecification."Source Ref. No.")); + + exit(WhseActivLine."Source Line No." = TrackingSpecification."Source Ref. No."); + end; + + local procedure LocationRequiresPick(LocationCode: Code[10]): Boolean + var + Location: Record Location; + begin + if not Location.Get(LocationCode) then + exit(false); + + exit(Location."Require Pick"); + end; + local procedure CreateEntrySummary(TrackingSpecification: Record "Tracking Specification" temporary; TempReservEntry: Record "Reservation Entry" temporary) begin CreateEntrySummary2(TrackingSpecification, TempReservEntry, true); @@ -1537,6 +1703,20 @@ codeunit 6501 "Item Tracking Data Collection" begin end; + [IntegrationEvent(false, false)] + local procedure OnBeforeTransferUnregisteredPicksToTempRec(var TrackingSpecification: Record "Tracking Specification" temporary; var TempGlobalReservEntry: Record "Reservation Entry" temporary; var IsHandled: Boolean) begin + end; + + [IntegrationEvent(false, false)] + local procedure OnTransferUnregisteredPicksToTempRecOnAfterSetFilters(var WhseActivLine: Record "Warehouse Activity Line"; TrackingSpecification: Record "Tracking Specification") + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnAfterTransferUnregisteredPicksToTempRec(var TrackingSpecification: Record "Tracking Specification" temporary; var TempGlobalReservEntry: Record "Reservation Entry" temporary) + begin + end; + [IntegrationEvent(false, false)] local procedure OnAssistEditTrackingNoOnBeforeSetSources(var TempTrackingSpecification: Record "Tracking Specification" temporary; var TempGlobalEntrySummary: Record "Entry Summary" temporary; var MaxQuantity: Decimal); begin diff --git a/src/Layers/W1/Tests/SCM-Reservation/SCMItemTracking.Codeunit.al b/src/Layers/W1/Tests/SCM-Reservation/SCMItemTracking.Codeunit.al index ae22552282a..6a29d98f6a1 100644 --- a/src/Layers/W1/Tests/SCM-Reservation/SCMItemTracking.Codeunit.al +++ b/src/Layers/W1/Tests/SCM-Reservation/SCMItemTracking.Codeunit.al @@ -5732,6 +5732,276 @@ codeunit 137405 "SCM Item Tracking" exit(ItemLedgerEntry.Quantity); end; + [Test] + [HandlerFunctions('ItemTrackingLinesLotSNQtyModalPageHandler')] + [Scope('OnPrem')] + procedure GetAvailableLotQtyExcludesUnregisteredWhsePickAllocation() + var + Item: Record Item; + Location: Record Location; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + TrackingSpecification: Record "Tracking Specification"; + ItemTrackingDataCollection: Codeunit "Item Tracking Data Collection"; + LotNo: Code[50]; + Qty: Integer; + begin + // [Bug 638344] Available lot quantity must exclude quantities committed to an unregistered warehouse pick + // [SCENARIO] A lot fully allocated to an unregistered warehouse-pick Take line is not reported as available for a different demand source + Initialize(); + + // [GIVEN] Lot-tracked item with lot warehouse tracking, in stock at a location for lot "L" with qty "Q" + Qty := LibraryRandom.RandIntInRange(20, 50); + LotNo := LibraryUtility.GenerateGUID(); + CreateLotTrackedItemAtLocation(Item, Location); + CreateAndPostLotStockForPick(Item."No.", Location.Code, LotNo, Qty); + + // [GIVEN] An unregistered warehouse pick Take line reserves the full lot quantity for a different sales line + CreateUnregisteredWhsePickTakeLine( + WarehouseActivityHeader, WarehouseActivityLine, Item."No.", Location.Code, LotNo, Qty, + Database::"Sales Line", 1, LibraryUtility.GenerateGUID(), 10000); + + // [WHEN] Available lot quantity is retrieved for a new demand on the same lot + SetTrackingSpecItemLotLocation(TrackingSpecification, Item."No.", Location.Code, LotNo); + + // [THEN] Available quantity is zero because the on-hand lot is fully committed to the unregistered pick + Assert.AreEqual( + 0, ItemTrackingDataCollection.GetAvailableLotQty(TrackingSpecification), + 'Available lot quantity must exclude quantities allocated to unregistered warehouse picks.'); + + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('ItemTrackingLinesLotSNQtyModalPageHandler')] + [Scope('OnPrem')] + procedure GetAvailableLotQtyExcludesUnregisteredInvtPickAllocation() + var + Item: Record Item; + Location: Record Location; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + TrackingSpecification: Record "Tracking Specification"; + ItemTrackingDataCollection: Codeunit "Item Tracking Data Collection"; + LotNo: Code[50]; + Qty: Integer; + begin + // [Bug 638344] Available lot quantity must exclude quantities committed to an unregistered inventory pick + // [SCENARIO] A lot fully allocated to an unregistered Invt. Pick line (blank Action Type) is not reported as available for a different demand source + Initialize(); + + // [GIVEN] Lot-tracked item with lot warehouse tracking, in stock at a location for lot "L" with qty "Q" + Qty := LibraryRandom.RandIntInRange(20, 50); + LotNo := LibraryUtility.GenerateGUID(); + CreateLotTrackedItemAtLocation(Item, Location); + CreateAndPostLotStockForPick(Item."No.", Location.Code, LotNo, Qty); + + // [GIVEN] An unregistered inventory pick line reserves the full lot quantity for a different sales line + CreateUnregisteredInvtPickLine( + WarehouseActivityHeader, WarehouseActivityLine, Item."No.", Location.Code, LotNo, Qty, + Database::"Sales Line", 1, LibraryUtility.GenerateGUID(), 10000); + + // [WHEN] Available lot quantity is retrieved for a new demand on the same lot + SetTrackingSpecItemLotLocation(TrackingSpecification, Item."No.", Location.Code, LotNo); + + // [THEN] Available quantity is zero because the on-hand lot is fully committed to the unregistered inventory pick + Assert.AreEqual( + 0, ItemTrackingDataCollection.GetAvailableLotQty(TrackingSpecification), + 'Available lot quantity must exclude quantities allocated to unregistered inventory picks.'); + + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('ItemTrackingLinesLotSNQtyModalPageHandler')] + [Scope('OnPrem')] + procedure GetAvailableLotQtyDoesNotDoubleCountPickWithSourceLineTracking() + var + Item: Record Item; + Location: Record Location; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + TrackingSpecification: Record "Tracking Specification"; + ItemTrackingDataCollection: Codeunit "Item Tracking Data Collection"; + LotNo: Code[50]; + SalesOrderNo: Code[20]; + OnHandQty: Integer; + PickQty: Integer; + begin + // [Bug 638344] An unregistered pick whose source line already carries item-tracking reservation must not be counted twice + // [SCENARIO] Production flow assigns source-line tracking (reservation entry) and then creates a pick for the same lot; the pick and reservation are one allocation + Initialize(); + + // [GIVEN] Lot-tracked item with lot warehouse tracking, on hand qty "Q" for lot "L" + OnHandQty := LibraryRandom.RandIntInRange(40, 60); + PickQty := LibraryRandom.RandIntInRange(10, 20); + LotNo := LibraryUtility.GenerateGUID(); + SalesOrderNo := LibraryUtility.GenerateGUID(); + CreateLotTrackedItemAtLocation(Item, Location); + CreateAndPostLotStockForPick(Item."No.", Location.Code, LotNo, OnHandQty); + + // [GIVEN] Source-line item tracking on a sales line reserves "P" of the lot (as production flow assigns tracking) + CreateOutboundLotReservationForSalesLine(Item."No.", Location.Code, LotNo, SalesOrderNo, 10000, PickQty); + + // [GIVEN] An unregistered warehouse pick Take line for the SAME sales line and lot moves the same "P" + CreateUnregisteredWhsePickTakeLine( + WarehouseActivityHeader, WarehouseActivityLine, Item."No.", Location.Code, LotNo, PickQty, + Database::"Sales Line", 1, SalesOrderNo, 10000); + + // [WHEN] Available lot quantity is retrieved for a new demand on the same lot + SetTrackingSpecItemLotLocation(TrackingSpecification, Item."No.", Location.Code, LotNo); + + // [THEN] Available equals on hand minus the single allocation, not minus both the reservation and the pick + Assert.AreEqual( + OnHandQty - PickQty, ItemTrackingDataCollection.GetAvailableLotQty(TrackingSpecification), + 'Available lot quantity must not double-count a pick already represented by a source-line reservation.'); + + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('ItemTrackingLinesLotSNQtyModalPageHandler')] + [Scope('OnPrem')] + procedure GetAvailableLotQtyNetsSourceReservationOnceAcrossSplitPickTakeLines() + var + Item: Record Item; + Location: Record Location; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + TrackingSpecification: Record "Tracking Specification"; + ItemTrackingDataCollection: Codeunit "Item Tracking Data Collection"; + LotNo: Code[50]; + SalesOrderNo: Code[20]; + OnHandQty: Integer; + ReservedQty: Integer; + FirstTakeQty: Integer; + SecondTakeQty: Integer; + begin + // [Bug 638344] A partial source reservation must be netted only once against the aggregate of split Take lines + // [SCENARIO] A pick has two Take lines (e.g. after SplitLine or when taken from multiple bins) for the same source and lot, with a smaller source reservation; the full picked quantity must be committed + Initialize(); + + // [GIVEN] Lot-tracked item with lot warehouse tracking, on hand qty "Q" for lot "L" + // [GIVEN] Take lines of 7 and 3 with a partial source reservation of 5 (the exact case that used to overstate availability by 3) + OnHandQty := LibraryRandom.RandIntInRange(40, 60); + FirstTakeQty := 7; + SecondTakeQty := 3; + ReservedQty := 5; + LotNo := LibraryUtility.GenerateGUID(); + SalesOrderNo := LibraryUtility.GenerateGUID(); + CreateLotTrackedItemAtLocation(Item, Location); + CreateAndPostLotStockForPick(Item."No.", Location.Code, LotNo, OnHandQty); + + // [GIVEN] Source-line item tracking on the sales line reserves only part of the lot ("R" < total picked) + CreateOutboundLotReservationForSalesLine(Item."No.", Location.Code, LotNo, SalesOrderNo, 10000, ReservedQty); + + // [GIVEN] Two unregistered warehouse pick Take lines for the SAME sales line and lot (split pick) + CreateUnregisteredWhsePickTakeLine( + WarehouseActivityHeader, WarehouseActivityLine, Item."No.", Location.Code, LotNo, FirstTakeQty, + Database::"Sales Line", 1, SalesOrderNo, 10000); + AddWhsePickTakeLine( + WarehouseActivityHeader, Item."No.", Location.Code, LotNo, SecondTakeQty, + Database::"Sales Line", 1, SalesOrderNo, 10000, 20000); + + // [WHEN] Available lot quantity is retrieved for a new demand on the same lot + SetTrackingSpecItemLotLocation(TrackingSpecification, Item."No.", Location.Code, LotNo); + + // [THEN] Available equals on hand minus the full picked quantity; the reservation is netted only once, not once per Take line + Assert.AreEqual( + OnHandQty - (FirstTakeQty + SecondTakeQty), ItemTrackingDataCollection.GetAvailableLotQty(TrackingSpecification), + 'A partial source reservation must be netted only once against the aggregate of split Take lines.'); + + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('ItemTrackingLinesLotSNQtyModalPageHandler')] + [Scope('OnPrem')] + procedure GetAvailableLotQtyNetsProductionComponentReservationOnce() + var + Item: Record Item; + Location: Record Location; + ReservationEntry: Record "Reservation Entry"; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + TrackingSpecification: Record "Tracking Specification"; + ItemTrackingDataCollection: Codeunit "Item Tracking Data Collection"; + LotNo: Code[50]; + ProductionOrderNo: Code[20]; + OnHandQty: Integer; + PickQty: Integer; + ProductionOrderLineNo: Integer; + ComponentLineNo: Integer; + NextEntryNo: Integer; + begin + // [Bug 638344] Production component source pointers must net the pick reservation once + // [SCENARIO] A production component reservation and its unregistered pick represent one allocation + Initialize(); + + OnHandQty := LibraryRandom.RandIntInRange(40, 60); + PickQty := LibraryRandom.RandIntInRange(10, 20); + LotNo := LibraryUtility.GenerateGUID(); + ProductionOrderNo := LibraryUtility.GenerateGUID(); + ProductionOrderLineNo := 10000; + ComponentLineNo := 20000; + CreateLotTrackedItemAtLocation(Item, Location); + CreateAndPostLotStockForPick(Item."No.", Location.Code, LotNo, OnHandQty); + + ReservationEntry.LockTable(); + if ReservationEntry.FindLast() then + NextEntryNo := ReservationEntry."Entry No." + 1 + else + NextEntryNo := 1; + ReservationEntry.Init(); + ReservationEntry."Entry No." := NextEntryNo; + ReservationEntry.Positive := false; + ReservationEntry."Reservation Status" := ReservationEntry."Reservation Status"::Surplus; + ReservationEntry."Item No." := Item."No."; + ReservationEntry."Location Code" := Location.Code; + ReservationEntry."Quantity (Base)" := -PickQty; + ReservationEntry.Quantity := -PickQty; + ReservationEntry."Qty. to Handle (Base)" := -PickQty; + ReservationEntry."Source Type" := Database::"Prod. Order Component"; + ReservationEntry."Source Subtype" := 1; + ReservationEntry."Source ID" := ProductionOrderNo; + ReservationEntry."Source Prod. Order Line" := ProductionOrderLineNo; + ReservationEntry."Source Ref. No." := ComponentLineNo; + ReservationEntry."Lot No." := LotNo; + ReservationEntry."Item Tracking" := ReservationEntry."Item Tracking"::"Lot No."; + ReservationEntry.Insert(false); + + WarehouseActivityHeader.Init(); + WarehouseActivityHeader.Type := WarehouseActivityHeader.Type::Pick; + WarehouseActivityHeader."No." := LibraryUtility.GenerateGUID(); + WarehouseActivityHeader."Location Code" := Location.Code; + WarehouseActivityHeader.Insert(false); + WarehouseActivityLine.Init(); + WarehouseActivityLine."Activity Type" := WarehouseActivityLine."Activity Type"::Pick; + WarehouseActivityLine."No." := WarehouseActivityHeader."No."; + WarehouseActivityLine."Line No." := 10000; + WarehouseActivityLine."Action Type" := WarehouseActivityLine."Action Type"::Take; + WarehouseActivityLine."Item No." := Item."No."; + WarehouseActivityLine."Location Code" := Location.Code; + WarehouseActivityLine."Lot No." := LotNo; + WarehouseActivityLine."Qty. Outstanding" := PickQty; + WarehouseActivityLine."Qty. Outstanding (Base)" := PickQty; + WarehouseActivityLine."Breakbulk No." := 0; + WarehouseActivityLine."Source Type" := Database::"Prod. Order Component"; + WarehouseActivityLine."Source Subtype" := 1; + WarehouseActivityLine."Source No." := ProductionOrderNo; + WarehouseActivityLine."Source Line No." := ProductionOrderLineNo; + WarehouseActivityLine."Source Subline No." := ComponentLineNo; + WarehouseActivityLine.Insert(false); + + SetTrackingSpecItemLotLocation(TrackingSpecification, Item."No.", Location.Code, LotNo); + + Assert.AreEqual( + OnHandQty - PickQty, ItemTrackingDataCollection.GetAvailableLotQty(TrackingSpecification), + 'A production component reservation and its unregistered pick must be counted as one allocation.'); + + LibraryVariableStorage.AssertEmpty(); + end; + local procedure Initialize() var LibraryERMCountryData: Codeunit "Library - ERM Country Data"; @@ -7592,6 +7862,134 @@ codeunit 137405 "SCM Item Tracking" Item.Modify(true); end; + local procedure CreateOutboundLotReservationForSalesLine(ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]; SourceNo: Code[20]; SourceLineNo: Integer; Qty: Decimal) + var + ReservationEntry: Record "Reservation Entry"; + NextEntryNo: Integer; + begin + ReservationEntry.LockTable(); + if ReservationEntry.FindLast() then + NextEntryNo := ReservationEntry."Entry No." + 1 + else + NextEntryNo := 1; + + ReservationEntry.Init(); + ReservationEntry."Entry No." := NextEntryNo; + ReservationEntry.Positive := false; + ReservationEntry."Reservation Status" := ReservationEntry."Reservation Status"::Surplus; + ReservationEntry."Item No." := ItemNo; + ReservationEntry."Location Code" := LocationCode; + ReservationEntry."Quantity (Base)" := -Qty; + ReservationEntry.Quantity := -Qty; + ReservationEntry."Qty. to Handle (Base)" := -Qty; + ReservationEntry."Source Type" := Database::"Sales Line"; + ReservationEntry."Source Subtype" := 1; + ReservationEntry."Source ID" := SourceNo; + ReservationEntry."Source Ref. No." := SourceLineNo; + ReservationEntry."Lot No." := LotNo; + ReservationEntry."Item Tracking" := ReservationEntry."Item Tracking"::"Lot No."; + ReservationEntry.Insert(false); + end; + + local procedure CreateLotTrackedItemAtLocation(var Item: Record Item; var Location: Record Location) + var + InventoryPostingSetup: Record "Inventory Posting Setup"; + ItemTrackingCodeCode: Code[10]; + begin + LibraryWarehouse.CreateLocationWithInventoryPostingSetup(Location); + Location.Validate("Require Pick", true); + Location.Validate("Require Shipment", true); + Location.Modify(true); + + ItemTrackingCodeCode := CreateItemTrackingCodeLotSpecificWhseTracking(true); + CreateItem(Item, ItemTrackingCodeCode, '', LibraryUtility.GetGlobalNoSeriesCode()); + + if not InventoryPostingSetup.Get(Location.Code, Item."Inventory Posting Group") then + LibraryInventory.CreateInventoryPostingSetup(InventoryPostingSetup, Location.Code, Item."Inventory Posting Group"); + end; + + local procedure SetTrackingSpecItemLotLocation(var TrackingSpecification: Record "Tracking Specification"; ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]) + begin + Clear(TrackingSpecification); + TrackingSpecification."Item No." := ItemNo; + TrackingSpecification."Location Code" := LocationCode; + TrackingSpecification."Lot No." := LotNo; + end; + + local procedure CreateUnregisteredWhsePickTakeLine(var WarehouseActivityHeader: Record "Warehouse Activity Header"; var WarehouseActivityLine: Record "Warehouse Activity Line"; ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]; Qty: Decimal; SourceType: Integer; SourceSubtype: Integer; SourceNo: Code[20]; SourceLineNo: Integer) + begin + WarehouseActivityHeader.Init(); + WarehouseActivityHeader.Type := WarehouseActivityHeader.Type::Pick; + WarehouseActivityHeader."No." := LibraryUtility.GenerateGUID(); + WarehouseActivityHeader."Location Code" := LocationCode; + WarehouseActivityHeader.Insert(false); + + WarehouseActivityLine.Init(); + WarehouseActivityLine."Activity Type" := WarehouseActivityLine."Activity Type"::Pick; + WarehouseActivityLine."No." := WarehouseActivityHeader."No."; + WarehouseActivityLine."Line No." := 10000; + WarehouseActivityLine."Action Type" := WarehouseActivityLine."Action Type"::Take; + WarehouseActivityLine."Item No." := ItemNo; + WarehouseActivityLine."Location Code" := LocationCode; + WarehouseActivityLine."Lot No." := LotNo; + WarehouseActivityLine."Qty. Outstanding" := Qty; + WarehouseActivityLine."Qty. Outstanding (Base)" := Qty; + WarehouseActivityLine."Breakbulk No." := 0; + WarehouseActivityLine."Source Type" := SourceType; + WarehouseActivityLine."Source Subtype" := SourceSubtype; + WarehouseActivityLine."Source No." := SourceNo; + WarehouseActivityLine."Source Line No." := SourceLineNo; + WarehouseActivityLine.Insert(false); + end; + + local procedure AddWhsePickTakeLine(WarehouseActivityHeader: Record "Warehouse Activity Header"; ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]; Qty: Decimal; SourceType: Integer; SourceSubtype: Integer; SourceNo: Code[20]; SourceLineNo: Integer; LineNo: Integer) + var + WarehouseActivityLine: Record "Warehouse Activity Line"; + begin + WarehouseActivityLine.Init(); + WarehouseActivityLine."Activity Type" := WarehouseActivityLine."Activity Type"::Pick; + WarehouseActivityLine."No." := WarehouseActivityHeader."No."; + WarehouseActivityLine."Line No." := LineNo; + WarehouseActivityLine."Action Type" := WarehouseActivityLine."Action Type"::Take; + WarehouseActivityLine."Item No." := ItemNo; + WarehouseActivityLine."Location Code" := LocationCode; + WarehouseActivityLine."Lot No." := LotNo; + WarehouseActivityLine."Qty. Outstanding" := Qty; + WarehouseActivityLine."Qty. Outstanding (Base)" := Qty; + WarehouseActivityLine."Breakbulk No." := 0; + WarehouseActivityLine."Source Type" := SourceType; + WarehouseActivityLine."Source Subtype" := SourceSubtype; + WarehouseActivityLine."Source No." := SourceNo; + WarehouseActivityLine."Source Line No." := SourceLineNo; + WarehouseActivityLine.Insert(false); + end; + + local procedure CreateUnregisteredInvtPickLine(var WarehouseActivityHeader: Record "Warehouse Activity Header"; var WarehouseActivityLine: Record "Warehouse Activity Line"; ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]; Qty: Decimal; SourceType: Integer; SourceSubtype: Integer; SourceNo: Code[20]; SourceLineNo: Integer) + begin + WarehouseActivityHeader.Init(); + WarehouseActivityHeader.Type := WarehouseActivityHeader.Type::"Invt. Pick"; + WarehouseActivityHeader."No." := LibraryUtility.GenerateGUID(); + WarehouseActivityHeader."Location Code" := LocationCode; + WarehouseActivityHeader.Insert(false); + + WarehouseActivityLine.Init(); + WarehouseActivityLine."Activity Type" := WarehouseActivityLine."Activity Type"::"Invt. Pick"; + WarehouseActivityLine."No." := WarehouseActivityHeader."No."; + WarehouseActivityLine."Line No." := 10000; + WarehouseActivityLine."Action Type" := WarehouseActivityLine."Action Type"::" "; + WarehouseActivityLine."Item No." := ItemNo; + WarehouseActivityLine."Location Code" := LocationCode; + WarehouseActivityLine."Lot No." := LotNo; + WarehouseActivityLine."Qty. Outstanding" := Qty; + WarehouseActivityLine."Qty. Outstanding (Base)" := Qty; + WarehouseActivityLine."Breakbulk No." := 0; + WarehouseActivityLine."Source Type" := SourceType; + WarehouseActivityLine."Source Subtype" := SourceSubtype; + WarehouseActivityLine."Source No." := SourceNo; + WarehouseActivityLine."Source Line No." := SourceLineNo; + WarehouseActivityLine.Insert(false); + end; + [ModalPageHandler] [Scope('OnPrem')] procedure ItemTrackingLinesPageHandlerTrackingOption(var ItemTrackingLines: TestPage "Item Tracking Lines")