diff --git a/src/Apps/W1/Quality Management/app/src/Dispositions/PutAway/QltyDispInternalPutAway.Codeunit.al b/src/Apps/W1/Quality Management/app/src/Dispositions/PutAway/QltyDispInternalPutAway.Codeunit.al index c9b2f02bfc..486929ac99 100644 --- a/src/Apps/W1/Quality Management/app/src/Dispositions/PutAway/QltyDispInternalPutAway.Codeunit.al +++ b/src/Apps/W1/Quality Management/app/src/Dispositions/PutAway/QltyDispInternalPutAway.Codeunit.al @@ -72,6 +72,11 @@ codeunit 20447 "Qlty. Disp. Internal Put-away" implements "Qlty. Disposition" exit; end; + repeat + QltyInventoryAvailability.ErrorIfFromBinIsReceiveBin(QltyInspectionHeader, TempQuantityToActQltyDispositionBuffer.GetFromLocationCode(), TempQuantityToActQltyDispositionBuffer.GetFromBinCode()); + until TempQuantityToActQltyDispositionBuffer.Next() = 0; + + TempQuantityToActQltyDispositionBuffer.FindSet(); repeat if (WhseInternalPutAwayHeader."Location Code" <> TempQuantityToActQltyDispositionBuffer."Location Filter") or (WhseInternalPutAwayHeader."From Bin Code" <> TempQuantityToActQltyDispositionBuffer."Bin Filter") then begin CreateInternalPutawayHeader(WhseInternalPutAwayHeader, TempQuantityToActQltyDispositionBuffer.GetFromLocationCode(), TempQuantityToActQltyDispositionBuffer.GetFromBinCode()); diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/QltyInventoryAvailability.Codeunit.al b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/QltyInventoryAvailability.Codeunit.al index e8e13f8888..28d0412dde 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/QltyInventoryAvailability.Codeunit.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/QltyInventoryAvailability.Codeunit.al @@ -24,6 +24,7 @@ codeunit 20445 "Qlty. Inventory Availability" NoSamplesToMoveErr: Label 'No samples meet the condition specified.', Locked = true; SerialQuantityGreaterThanOneErr: Label '%1 (%2) cannot be greater than 1 when New Serial No. is requested.', Comment = '%1=quantity behavior, %2=quantity'; ZeroQuantityErr: Label 'Unable to use the disposition %1 on the inspection %2 for the item %3 because the quantity is zero.', Comment = '%1=the inspection, %2=the inspection, %3=the item'; + InsufficientInventoryToAllocateErr: Label 'Unable to move the entire requested quantity for inspection %1 for item %2. There is not enough inventory available in eligible bins (short by %3). Put the remaining inventory away or reduce the quantity, and then try again.', Comment = '%1=the inspection, %2=the item, %3=the missing quantity'; SupplyFromLocationCodeNameLbl: Label 'Supply-from Location Code', Locked = true; FromLocationCodeNameLbl: Label 'From Location Code', Locked = true; LocationCodeNameLbl: Label 'Location Code', Locked = true; @@ -162,19 +163,20 @@ codeunit 20445 "Qlty. Inventory Availability" if RecordRefToSearch.FindFirst() then begin LocationCode := QltyInspectionHeader."Location Code"; GetFromLocationAndBinBasedOnNamingConventions(RecordRefToSearch, LocationCode, BinCode, QuantityBaseValue); - if LocationCode <> '' then begin - TempToMoveBinContent.Reset(); - TempToMoveBinContent.SetRange("Location Code", LocationCode); - if BinCode <> '' then - TempToMoveBinContent.SetRange("Bin Code", BinCode); - if not TempToMoveBinContent.FindFirst() then begin - TempToMoveBinContent.Init(); - TempToMoveBinContent."Location Code" := LocationCode; - TempToMoveBinContent."Bin Code" := BinCode; - TempToMoveBinContent."Min. Qty." := QuantityBaseValue; - TempToMoveBinContent.Insert(false); + if LocationCode <> '' then + if not ResolveBinsFromBinContent(QltyInspectionHeader, LocationCode, TempToMoveBinContent) then begin + TempToMoveBinContent.Reset(); + TempToMoveBinContent.SetRange("Location Code", LocationCode); + if BinCode <> '' then + TempToMoveBinContent.SetRange("Bin Code", BinCode); + if not TempToMoveBinContent.FindFirst() then begin + TempToMoveBinContent.Init(); + TempToMoveBinContent."Location Code" := LocationCode; + TempToMoveBinContent."Bin Code" := BinCode; + TempToMoveBinContent."Min. Qty." := QuantityBaseValue; + TempToMoveBinContent.Insert(false); + end; end; - end; end; end; @@ -187,6 +189,48 @@ codeunit 20445 "Qlty. Inventory Availability" end; end; + local procedure ResolveBinsFromBinContent(QltyInspectionHeader: Record "Qlty. Inspection Header"; LocationCode: Code[10]; var TempToMoveBinContent: Record "Bin Content" temporary): Boolean + var + Location: Record Location; + BinContent: Record "Bin Content"; + Added: Boolean; + begin + if (LocationCode = '') or (QltyInspectionHeader."Source Item No." = '') then + exit(false); + if not Location.Get(LocationCode) then + exit(false); + if not Location."Bin Mandatory" then + exit(false); + + BinContent.SetRange("Location Code", LocationCode); + BinContent.SetRange("Item No.", QltyInspectionHeader."Source Item No."); + BinContent.SetRange("Variant Code", QltyInspectionHeader."Source Variant Code"); + if QltyInspectionHeader."Source Lot No." <> '' then + BinContent.SetRange("Lot No. Filter", QltyInspectionHeader."Source Lot No."); + if QltyInspectionHeader."Source Serial No." <> '' then + BinContent.SetRange("Serial No. Filter", QltyInspectionHeader."Source Serial No."); + if QltyInspectionHeader."Source Package No." <> '' then + BinContent.SetRange("Package No. Filter", QltyInspectionHeader."Source Package No."); + if Location."Adjustment Bin Code" <> '' then + BinContent.SetFilter("Bin Code", '<>%1', Location."Adjustment Bin Code"); + BinContent.SetFilter("Quantity (Base)", '>%1', 0); + BinContent.SetAutoCalcFields("Quantity (Base)"); + if BinContent.FindSet() then + repeat + if not IsReceiveBin(BinContent."Location Code", BinContent."Bin Code") then begin + Added := true; + if not TempToMoveBinContent.Get(BinContent."Location Code", BinContent."Bin Code", BinContent."Item No.", BinContent."Variant Code", BinContent."Unit of Measure Code") then begin + TempToMoveBinContent.Init(); + TempToMoveBinContent := BinContent; + TempToMoveBinContent."Min. Qty." := BinContent."Quantity (Base)"; + TempToMoveBinContent.Insert(false); + end; + end; + until BinContent.Next() = 0; + + exit(Added); + end; + local procedure GetFromLocationAndBinBasedOnNamingConventions(var RecordRef: RecordRef; var LocationCode: Code[10]; var BinCode: Code[20]; var QuantityBase: Decimal) var Location: Record Location; @@ -386,7 +430,10 @@ codeunit 20445 "Qlty. Inventory Availability" MultipleBins: Boolean; SkipBinContent: Boolean; IsHandled: Boolean; + AllocateAcrossBins: Boolean; BufferEntryCounter: Integer; + RemainingQuantityToAllocate: Decimal; + QuantityToHandle: Decimal; begin TempQuantityQltyDispositionBuffer.Reset(); TempQuantityQltyDispositionBuffer.DeleteAll(); @@ -417,6 +464,18 @@ codeunit 20445 "Qlty. Inventory Availability" TempExistingInventoryBinContent.FindSet(); end; + AllocateAcrossBins := MultipleBins and (TempInstructionQltyDispositionBuffer."Quantity Behavior" in [ + TempInstructionQltyDispositionBuffer."Quantity Behavior"::"Specific Quantity", + TempInstructionQltyDispositionBuffer."Quantity Behavior"::"Sample Quantity", + TempInstructionQltyDispositionBuffer."Quantity Behavior"::"Failed Quantity", + TempInstructionQltyDispositionBuffer."Quantity Behavior"::"Passed Quantity"]); + if AllocateAcrossBins then + RemainingQuantityToAllocate := GetQuantityToHandleFromInspection( + QltyInspectionHeader, + TempInstructionQltyDispositionBuffer."Quantity Behavior", + TempInstructionQltyDispositionBuffer."Qty. To Handle (Base)", + TempExistingInventoryBinContent); + repeat SkipBinContent := false; if TempExistingInventoryBinContent."Location Code" <> '' then @@ -431,17 +490,30 @@ codeunit 20445 "Qlty. Inventory Availability" Location."Bin Mandatory"); end; + if not SkipBinContent then + if AllocateAcrossBins then begin + if TempExistingInventoryBinContent."Min. Qty." <= 0 then + QuantityToHandle := 0 + else + if TempExistingInventoryBinContent."Min. Qty." < RemainingQuantityToAllocate then + QuantityToHandle := TempExistingInventoryBinContent."Min. Qty." + else + QuantityToHandle := RemainingQuantityToAllocate; + SkipBinContent := QuantityToHandle <= 0; + end else + QuantityToHandle := GetQuantityToHandleFromInspection( + QltyInspectionHeader, + TempInstructionQltyDispositionBuffer."Quantity Behavior", + TempInstructionQltyDispositionBuffer."Qty. To Handle (Base)", + TempExistingInventoryBinContent); + if not SkipBinContent then begin BufferEntryCounter += 1; TempQuantityQltyDispositionBuffer := TempInstructionQltyDispositionBuffer; TempQuantityQltyDispositionBuffer."Buffer Entry No." := BufferEntryCounter; TempQuantityQltyDispositionBuffer."Location Filter" := TempExistingInventoryBinContent."Location Code"; TempQuantityQltyDispositionBuffer."Bin Filter" := TempExistingInventoryBinContent."Bin Code"; - TempQuantityQltyDispositionBuffer."Qty. To Handle (Base)" := GetQuantityToHandleFromInspection( - QltyInspectionHeader, - TempInstructionQltyDispositionBuffer."Quantity Behavior", - TempInstructionQltyDispositionBuffer."Qty. To Handle (Base)", - TempExistingInventoryBinContent); + TempQuantityQltyDispositionBuffer."Qty. To Handle (Base)" := QuantityToHandle; if TempQuantityQltyDispositionBuffer."Qty. To Handle (Base)" = 0 then Error(ZeroQuantityErr, TempInstructionQltyDispositionBuffer."Disposition Action", QltyInspectionHeader."No.", QltyInspectionHeader."Source Item No."); @@ -449,9 +521,15 @@ codeunit 20445 "Qlty. Inventory Availability" if (TempQuantityQltyDispositionBuffer."New Serial No." <> '') and (TempInstructionQltyDispositionBuffer."Qty. To Handle (Base)" > 1) then Error(SerialQuantityGreaterThanOneErr, TempInstructionQltyDispositionBuffer."Entry Behavior", TempInstructionQltyDispositionBuffer."Qty. To Handle (Base)"); + if AllocateAcrossBins then + RemainingQuantityToAllocate -= TempQuantityQltyDispositionBuffer."Qty. To Handle (Base)"; + TempQuantityQltyDispositionBuffer.Insert(false); end; until TempExistingInventoryBinContent.Next() = 0; + + if AllocateAcrossBins and (RemainingQuantityToAllocate > 0) then + Error(InsufficientInventoryToAllocateErr, QltyInspectionHeader."No.", QltyInspectionHeader."Source Item No.", RemainingQuantityToAllocate); end; OnAfterPopulateBinContentBuffer(QltyInspectionHeader, TempInstructionQltyDispositionBuffer, TempQuantityQltyDispositionBuffer, TempExistingInventoryBinContent); @@ -464,18 +542,23 @@ codeunit 20445 "Qlty. Inventory Availability" /// The location code the inventory is being moved from. /// The bin code the inventory is being moved from. internal procedure ErrorIfFromBinIsReceiveBin(QltyInspectionHeader: Record "Qlty. Inspection Header"; FromLocationCode: Code[10]; FromBinCode: Code[20]) + begin + if IsReceiveBin(FromLocationCode, FromBinCode) then + Error(CannotMoveFromReceiveBinErr, QltyInspectionHeader.GetFriendlyIdentifier(), FromBinCode, FromLocationCode); + end; + + local procedure IsReceiveBin(FromLocationCode: Code[10]; FromBinCode: Code[20]): Boolean var FromBin: Record Bin; BinType: Record "Bin Type"; begin if (FromLocationCode = '') or (FromBinCode = '') then - exit; + exit(false); if not FromBin.Get(FromLocationCode, FromBinCode) then - exit; + exit(false); if not BinType.Get(FromBin."Bin Type Code") then - exit; - if BinType.Receive then - Error(CannotMoveFromReceiveBinErr, QltyInspectionHeader.GetFriendlyIdentifier(), FromBinCode, FromLocationCode); + exit(false); + exit(BinType.Receive); end; [IntegrationEvent(false, false)] diff --git a/src/Apps/W1/Quality Management/app/src/Utilities/QltyNotificationMgmt.Codeunit.al b/src/Apps/W1/Quality Management/app/src/Utilities/QltyNotificationMgmt.Codeunit.al index 706110f3e1..fe9d46e200 100644 --- a/src/Apps/W1/Quality Management/app/src/Utilities/QltyNotificationMgmt.Codeunit.al +++ b/src/Apps/W1/Quality Management/app/src/Utilities/QltyNotificationMgmt.Codeunit.al @@ -270,7 +270,7 @@ codeunit 20437 "Qlty. Notification Mgmt." DocumentNotAbleToBeCreatedAnMsg, DocumentType, QltyInspectionHeader."No.", - TempInstructionQltyDispositionBuffer."Qty. To Handle (Base)", + GetDisplayQuantityForFailure(QltyInspectionHeader, TempInstructionQltyDispositionBuffer), GetSourceSummaryText(QltyInspectionHeader), OptionalAdditionalMessageContext, Item."Base Unit of Measure") @@ -279,7 +279,7 @@ codeunit 20437 "Qlty. Notification Mgmt." DocumentNotAbleToBeCreatedAMsg, DocumentType, QltyInspectionHeader."No.", - TempInstructionQltyDispositionBuffer."Qty. To Handle (Base)", + GetDisplayQuantityForFailure(QltyInspectionHeader, TempInstructionQltyDispositionBuffer), GetSourceSummaryText(QltyInspectionHeader), OptionalAdditionalMessageContext, Item."Base Unit of Measure"); @@ -294,6 +294,29 @@ codeunit 20437 "Qlty. Notification Mgmt." CreateActionNotification(DocumentCreationFailedNotification, CurrentMessage, AvailableOptions); end; + /// + /// Gets the quantity to show in the document creation failed message, falling back to the quantity implied by the quantity behavior when the instruction quantity is zero. + /// + /// The inspection the failed document relates to. + /// The attempted instruction. + /// The quantity to display. + local procedure GetDisplayQuantityForFailure(QltyInspectionHeader: Record "Qlty. Inspection Header"; var TempInstructionQltyDispositionBuffer: Record "Qlty. Disposition Buffer" temporary): Decimal + begin + if TempInstructionQltyDispositionBuffer."Qty. To Handle (Base)" <> 0 then + exit(TempInstructionQltyDispositionBuffer."Qty. To Handle (Base)"); + + case TempInstructionQltyDispositionBuffer."Quantity Behavior" of + TempInstructionQltyDispositionBuffer."Quantity Behavior"::"Failed Quantity": + exit(QltyInspectionHeader."Fail Quantity"); + TempInstructionQltyDispositionBuffer."Quantity Behavior"::"Passed Quantity": + exit(QltyInspectionHeader."Pass Quantity"); + TempInstructionQltyDispositionBuffer."Quantity Behavior"::"Sample Quantity": + exit(QltyInspectionHeader."Sample Size"); + else + exit(QltyInspectionHeader."Source Quantity (Base)"); + end; + end; + /// /// Creates a notification that an item tracking change occurred. /// diff --git a/src/Apps/W1/Quality Management/test/src/QltyTestsDispositions.Codeunit.al b/src/Apps/W1/Quality Management/test/src/QltyTestsDispositions.Codeunit.al index 866d0afe64..767c0485ab 100644 --- a/src/Apps/W1/Quality Management/test/src/QltyTestsDispositions.Codeunit.al +++ b/src/Apps/W1/Quality Management/test/src/QltyTestsDispositions.Codeunit.al @@ -6960,6 +6960,75 @@ codeunit 139960 "Qlty. Tests - Dispositions" LibraryAssert.AreEqual(WhseInternalPutAwayHeader.Status::Released, WhseInternalPutAwayHeader.Status, 'Should have released internal putaway'); end; + [Test] + procedure InternalPutaway_FailedQuantity_SourcedFromStorageBinNotReceive() + var + QltyInspectionTemplateHdr: Record "Qlty. Inspection Template Hdr."; + QltyInspectionGenRule: Record "Qlty. Inspection Gen. Rule"; + Location: Record Location; + Item: Record Item; + Bin: Record Bin; + BinType: Record "Bin Type"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + WarehouseEntry: Record "Warehouse Entry"; + QltyInspectionHeader: Record "Qlty. Inspection Header"; + WhseInternalPutAwayLine: Record "Whse. Internal Put-away Line"; + WarehouseSetup: Record "Warehouse Setup"; + FailedQuantity: Decimal; + TotalPutAwayQuantity: Decimal; + begin + // [SCENARIO] Creating an internal put-away for the failed quantity sources the inventory from the storage bin the goods were put away to, never from the receive bin, and moves exactly the failed quantity + + // [GIVEN] A directed warehouse location with warehouse number series configured + QltyInspectionUtility.EnsureSetupExists(); + QltyInspectionUtility.CreatePrioritizedRule(QltyInspectionTemplateHdr, Database::"Warehouse Entry", QltyInspectionGenRule); + LibraryWarehouse.NoSeriesSetup(WarehouseSetup); + LibraryWarehouse.CreateFullWMSLocation(Location, 3); + + // [GIVEN] Current user is set as warehouse employee for the location + QltyInspectionUtility.SetCurrLocationWhseEmployee(Location.Code); + + // [GIVEN] An untracked item is purchased, received and put away at the directed location + LibraryInventory.CreateItem(Item); + QltyPurOrderGenerator.CreatePurchaseOrder(100, Location, Item, PurchaseHeader, PurchaseLine); + LibraryPurchase.ReleasePurchaseDocument(PurchaseHeader); + QltyPurOrderGenerator.ReceivePurchaseOrder(Location, PurchaseHeader, PurchaseLine); + + // [GIVEN] A quality inspection is created for the put away warehouse entry (a storage bin, not the receive bin) + WarehouseEntry.SetRange("Entry Type", WarehouseEntry."Entry Type"::Movement); + WarehouseEntry.SetRange("Location Code", Location.Code); + WarehouseEntry.SetRange("Item No.", Item."No."); + WarehouseEntry.SetFilter("Zone Code", '<>%1', 'RECEIVE'); + WarehouseEntry.FindFirst(); + QltyInspectionUtility.CreateInspectionWithWarehouseEntry(WarehouseEntry, QltyInspectionHeader); + + // [GIVEN] The inspection has 5 samples with 3 failures and 2 passes + FailedQuantity := 3; + QltyInspectionHeader."Sample Size" := 5; + QltyInspectionHeader."Fail Quantity" := FailedQuantity; + QltyInspectionHeader."Pass Quantity" := 2; + QltyInspectionHeader.Modify(); + + // [WHEN] Perform disposition with internal put-away for the failed quantity + LibraryAssert.IsTrue( + QltyInspectionUtility.PerformInternalPutAwayDisposition(QltyInspectionHeader, 0, '', '', true, Enum::"Qlty. Quantity Behavior"::"Failed Quantity"), + 'Should have created an internal put-away for the failed quantity'); + + // [THEN] Every created put-away line is sourced from a non-receive bin + WhseInternalPutAwayLine.SetRange("Item No.", Item."No."); + LibraryAssert.IsTrue(WhseInternalPutAwayLine.FindSet(), 'Expected internal put-away lines to be created'); + repeat + Bin.Get(WhseInternalPutAwayLine."Location Code", WhseInternalPutAwayLine."From Bin Code"); + if BinType.Get(Bin."Bin Type Code") then + LibraryAssert.IsFalse(BinType.Receive, 'Internal put-away must not be sourced from a receive bin'); + TotalPutAwayQuantity += WhseInternalPutAwayLine.Quantity; + until WhseInternalPutAwayLine.Next() = 0; + + // [THEN] The total put-away quantity equals the failed quantity + LibraryAssert.AreEqual(FailedQuantity, TotalPutAwayQuantity, 'Total internal put-away quantity should equal the failed quantity'); + end; + [Test] procedure WarehousePutaway_PerformDisposition() var