diff --git a/src/Apps/W1/Subcontracting/App/src/Process/Codeunits/SubcontractingManagement.Codeunit.al b/src/Apps/W1/Subcontracting/App/src/Process/Codeunits/SubcontractingManagement.Codeunit.al index 37142d717f..7ecc1cb068 100644 --- a/src/Apps/W1/Subcontracting/App/src/Process/Codeunits/SubcontractingManagement.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Process/Codeunits/SubcontractingManagement.Codeunit.al @@ -5,6 +5,7 @@ namespace Microsoft.Manufacturing.Subcontracting; using Microsoft.Foundation.Company; +using Microsoft.Foundation.UOM; using Microsoft.Inventory.Item; using Microsoft.Inventory.Ledger; using Microsoft.Inventory.Location; @@ -296,6 +297,10 @@ codeunit 99001505 "Subcontracting Management" TempForReservationEntry: Record "Reservation Entry" temporary; TempTrackingSpecification: Record "Tracking Specification" temporary; ProdOrderCompReserve: Codeunit "Prod. Order Comp.-Reserve"; + UnitOfMeasureManagement: Codeunit "Unit of Measure Management"; + QtyToReserve: Decimal; + QtyToReserveBase: Decimal; + AvailableToReserveBase: Decimal; begin if (TransferReceiptLine."Subc. Prod. Order No." = '') or (TransferReceiptLine."Subc. Operation No." = '') then exit; @@ -308,36 +313,59 @@ codeunit 99001505 "Subcontracting Management" ItemLedgerEntry.SetRange("Document No.", TransferReceiptLine."Document No."); ItemLedgerEntry.SetRange("Document Line No.", TransferReceiptLine."Line No."); ItemLedgerEntry.SetRange("Location Code", TransferReceiptLine."Transfer-to Code"); - ItemLedgerEntry.SetLoadFields("Serial No.", "Lot No.", "Package No.", "Variant Code", "Location Code", "Qty. per Unit of Measure", Quantity); + ItemLedgerEntry.SetLoadFields("Serial No.", "Lot No.", "Package No.", "Variant Code", "Location Code", Quantity); if not ItemLedgerEntry.IsEmpty() then begin ItemLedgerEntry.FindSet(); repeat if (ItemLedgerEntry."Lot No." <> '') or (ItemLedgerEntry."Serial No." <> '') or (ItemLedgerEntry."Package No." <> '') then begin - if not TempTrackingSpecification.IsEmpty() then - TempTrackingSpecification.DeleteAll(); - TempTrackingSpecification."Source Type" := Database::"Item Ledger Entry"; - TempTrackingSpecification."Source Subtype" := 0; - TempTrackingSpecification."Source ID" := ''; - TempTrackingSpecification."Source Batch Name" := ''; - TempTrackingSpecification."Source Prod. Order Line" := 0; - TempTrackingSpecification."Source Ref. No." := ItemLedgerEntry."Entry No."; - TempTrackingSpecification."Variant Code" := ItemLedgerEntry."Variant Code"; - TempTrackingSpecification."Location Code" := ItemLedgerEntry."Location Code"; - TempTrackingSpecification."Serial No." := ItemLedgerEntry."Serial No."; - TempTrackingSpecification."Lot No." := ItemLedgerEntry."Lot No."; - TempTrackingSpecification."Package No." := ItemLedgerEntry."Package No."; - TempTrackingSpecification."Qty. per Unit of Measure" := ItemLedgerEntry."Qty. per Unit of Measure"; - TempTrackingSpecification.Insert(); - - ProdOrderCompReserve.CreateReservationSetFrom(TempTrackingSpecification); - TempForReservationEntry.CopyTrackingFromSpec(TempTrackingSpecification); - ProdOrderCompReserve.CreateReservation( - ProdOrderComponent, - ProdOrderComponent.Description, - ProdOrderComponent."Due Date", - ItemLedgerEntry.Quantity, - ItemLedgerEntry.Quantity * ItemLedgerEntry."Qty. per Unit of Measure", - TempForReservationEntry); + // Only reserve up to the component's remaining need. Excess received quantity + // (e.g. when more was transferred to/from the subcontractor than the component requires) + // is left as free inventory instead of failing with "Reserved quantity cannot be greater than 0". + ProdOrderComponent.CalcFields("Reserved Qty. (Base)"); + AvailableToReserveBase := Abs(ProdOrderComponent."Remaining Qty. (Base)") - Abs(ProdOrderComponent."Reserved Qty. (Base)"); + + // Item ledger entry quantities are always stored in the base unit of measure. + QtyToReserveBase := ItemLedgerEntry.Quantity; + if QtyToReserveBase > AvailableToReserveBase then + // Serial-tracked entries are indivisible, so skip the entry entirely when it no longer + // fully fits. Lot- and package-tracked entries can be reserved partially. + if ItemLedgerEntry."Serial No." <> '' then + QtyToReserveBase := 0 + else + QtyToReserveBase := AvailableToReserveBase; + + if QtyToReserveBase > 0 then begin + if ProdOrderComponent."Qty. per Unit of Measure" <> 0 then + QtyToReserve := UnitOfMeasureManagement.CalcQtyFromBase(QtyToReserveBase, ProdOrderComponent."Qty. per Unit of Measure") + else + QtyToReserve := QtyToReserveBase; + + if not TempTrackingSpecification.IsEmpty() then + TempTrackingSpecification.DeleteAll(); + TempTrackingSpecification."Source Type" := Database::"Item Ledger Entry"; + TempTrackingSpecification."Source Subtype" := 0; + TempTrackingSpecification."Source ID" := ''; + TempTrackingSpecification."Source Batch Name" := ''; + TempTrackingSpecification."Source Prod. Order Line" := 0; + TempTrackingSpecification."Source Ref. No." := ItemLedgerEntry."Entry No."; + TempTrackingSpecification."Variant Code" := ItemLedgerEntry."Variant Code"; + TempTrackingSpecification."Location Code" := ItemLedgerEntry."Location Code"; + TempTrackingSpecification."Serial No." := ItemLedgerEntry."Serial No."; + TempTrackingSpecification."Lot No." := ItemLedgerEntry."Lot No."; + TempTrackingSpecification."Package No." := ItemLedgerEntry."Package No."; + TempTrackingSpecification."Qty. per Unit of Measure" := ProdOrderComponent."Qty. per Unit of Measure"; + TempTrackingSpecification.Insert(); + + ProdOrderCompReserve.CreateReservationSetFrom(TempTrackingSpecification); + TempForReservationEntry.CopyTrackingFromSpec(TempTrackingSpecification); + ProdOrderCompReserve.CreateReservation( + ProdOrderComponent, + ProdOrderComponent.Description, + ProdOrderComponent."Due Date", + QtyToReserve, + QtyToReserveBase, + TempForReservationEntry); + end; end; until ItemLedgerEntry.Next() = 0; end; diff --git a/src/Apps/W1/Subcontracting/Test/src/Codeunits/Tests/SubcTransOrdReservTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/src/Codeunits/Tests/SubcTransOrdReservTest.Codeunit.al index fbe602172f..d68d1d54e1 100644 --- a/src/Apps/W1/Subcontracting/Test/src/Codeunits/Tests/SubcTransOrdReservTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/src/Codeunits/Tests/SubcTransOrdReservTest.Codeunit.al @@ -6,6 +6,8 @@ namespace Microsoft.Manufacturing.Subcontracting.Test; using Microsoft.Foundation.NoSeries; using Microsoft.Inventory.Item; +using Microsoft.Inventory.Journal; +using Microsoft.Inventory.Ledger; using Microsoft.Inventory.Tracking; using Microsoft.Inventory.Transfer; using Microsoft.Manufacturing.Document; @@ -159,6 +161,98 @@ codeunit 149915 "Subc. TransOrd. Reserv. Test" Assert.AreEqual(18, TransferLine."Quantity (Base)", 'Transfer line base qty should reflect the reduced PO qty'); end; + [Test] + procedure ExcessLotQuantityReceiptReservesOnlyComponentNeed() + var + Item: Record Item; + ProdOrderComponent: Record "Prod. Order Component"; + ProductionOrder: Record "Production Order"; + TransferReceiptLine: Record "Transfer Receipt Line"; + ItemLedgerEntry: Record "Item Ledger Entry"; + WorkCenter: array[2] of Record "Work Center"; + MachineCenter: array[2] of Record "Machine Center"; + begin + // [SCENARIO 634465] Receiving a transfer back from the subcontractor with a lot quantity that exceeds the component need must reserve only the remaining need and leave the excess as free inventory instead of failing with "Reserved quantity cannot be greater than 0". + + // [GIVEN] A transfer-subcontracting released production order whose lot-tracked component needs 10 + Initialize(); + SetupTransferReservationScenario(Item, WorkCenter, MachineCenter, ProductionOrder, ProdOrderComponent, 10, 1, false); + EnableLotTrackingOnTransferComponent(Item); + + // [GIVEN] 15 lot-tracked units of the component were received at the component location (5 more than needed) + PostComponentInventoryAsLot(ProdOrderComponent."Item No.", ProdOrderComponent."Location Code", 'LOT01', 15); + FindPostedComponentItemLedgerEntry(ItemLedgerEntry, ProdOrderComponent."Item No.", ProdOrderComponent."Location Code"); + + // [WHEN] The posted transfer receipt line is reserved against the production order component + BuildTransferReceiptLineForComponent(TransferReceiptLine, ProdOrderComponent, ItemLedgerEntry); + SubcontractingManagement.TransferReservationEntryFromPstTransferLineToProdOrderComp(TransferReceiptLine); + + // [THEN] Only the component need (10) is reserved in a single capped reservation; the 5 excess units stay as free inventory + Assert.AreEqual(10, SubcontractingManagement.GetComponentReservedQtyBase(ProdOrderComponent), 'Only the component need must be reserved'); + Assert.AreEqual(1, CountProdOrderComponentReservations(ProdOrderComponent), 'A single capped lot reservation must be created on the component'); + end; + + [Test] + procedure ExcessSerialQuantityReceiptReservesOnlyComponentNeed() + var + Item: Record Item; + ProdOrderComponent: Record "Prod. Order Component"; + ProductionOrder: Record "Production Order"; + TransferReceiptLine: Record "Transfer Receipt Line"; + ItemLedgerEntry: Record "Item Ledger Entry"; + WorkCenter: array[2] of Record "Work Center"; + MachineCenter: array[2] of Record "Machine Center"; + begin + // [SCENARIO 636820] Receiving serial-tracked units in excess of the component need must reserve only the needed serials and skip the excess serials instead of failing with "Reserved quantity cannot be greater than 0". + + // [GIVEN] A transfer-subcontracting released production order whose serial-tracked component needs 3 + Initialize(); + SetupTransferReservationScenario(Item, WorkCenter, MachineCenter, ProductionOrder, ProdOrderComponent, 3, 1, true); + + // [GIVEN] 5 serial-tracked units of the component were received at the component location (2 more than needed) + PostComponentInventoryAsSerials(ProdOrderComponent."Item No.", ProdOrderComponent."Location Code", 5); + FindPostedComponentItemLedgerEntry(ItemLedgerEntry, ProdOrderComponent."Item No.", ProdOrderComponent."Location Code"); + + // [WHEN] The posted transfer receipt line is reserved against the production order component + BuildTransferReceiptLineForComponent(TransferReceiptLine, ProdOrderComponent, ItemLedgerEntry); + SubcontractingManagement.TransferReservationEntryFromPstTransferLineToProdOrderComp(TransferReceiptLine); + + // [THEN] Exactly three serials are reserved and the two excess serials stay as free inventory + Assert.AreEqual(3, SubcontractingManagement.GetComponentReservedQtyBase(ProdOrderComponent), 'Only the needed serials must be reserved'); + Assert.AreEqual(3, CountProdOrderComponentReservations(ProdOrderComponent), 'Exactly three serial reservations must be created on the component'); + end; + + [Test] + procedure ExcessPackageQuantityReceiptReservesOnlyComponentNeed() + var + Item: Record Item; + ProdOrderComponent: Record "Prod. Order Component"; + ProductionOrder: Record "Production Order"; + TransferReceiptLine: Record "Transfer Receipt Line"; + ItemLedgerEntry: Record "Item Ledger Entry"; + WorkCenter: array[2] of Record "Work Center"; + MachineCenter: array[2] of Record "Machine Center"; + begin + // [SCENARIO 634465] Package-tracked quantity is divisible like a lot, so receiving more than the component need must reserve only the remaining need and leave the excess as free inventory instead of failing with "Reserved quantity cannot be greater than 0". + + // [GIVEN] A transfer-subcontracting released production order whose package-tracked component needs 10 + Initialize(); + SetupTransferReservationScenario(Item, WorkCenter, MachineCenter, ProductionOrder, ProdOrderComponent, 10, 1, false); + EnablePackageTrackingOnTransferComponent(Item); + + // [GIVEN] 15 package-tracked units of the component were received at the component location (5 more than needed) + PostComponentInventoryAsPackage(ProdOrderComponent."Item No.", ProdOrderComponent."Location Code", 'PKG01', 15); + FindPostedComponentItemLedgerEntry(ItemLedgerEntry, ProdOrderComponent."Item No.", ProdOrderComponent."Location Code"); + + // [WHEN] The posted transfer receipt line is reserved against the production order component + BuildTransferReceiptLineForComponent(TransferReceiptLine, ProdOrderComponent, ItemLedgerEntry); + SubcontractingManagement.TransferReservationEntryFromPstTransferLineToProdOrderComp(TransferReceiptLine); + + // [THEN] Only the component need (10) is reserved in a single capped reservation; the 5 excess units stay as free inventory + Assert.AreEqual(10, SubcontractingManagement.GetComponentReservedQtyBase(ProdOrderComponent), 'Only the component need must be reserved'); + Assert.AreEqual(1, CountProdOrderComponentReservations(ProdOrderComponent), 'A single capped package reservation must be created on the component'); + end; + local procedure Initialize() begin LibraryTestInitialize.OnTestInitialize(Codeunit::"Subc. TransOrd. Reserv. Test"); @@ -248,6 +342,100 @@ codeunit 149915 "Subc. TransOrd. Reserv. Test" ComponentItem.Modify(true); end; + local procedure EnableLotTrackingOnTransferComponent(Item: Record Item) + var + ComponentItem: Record Item; + ProductionBOMLine: Record "Production BOM Line"; + ItemTrackingCode: Record "Item Tracking Code"; + LotNoSeries: Record "No. Series"; + LotNoSeriesLine: Record "No. Series Line"; + begin + ProductionBOMLine.SetRange("Production BOM No.", Item."Production BOM No."); + ProductionBOMLine.FindLast(); + ComponentItem.Get(ProductionBOMLine."No."); + + LibraryUtility.CreateNoSeries(LotNoSeries, true, true, false); + LibraryUtility.CreateNoSeriesLine(LotNoSeriesLine, LotNoSeries.Code, 'L0000000001', 'L0000000999'); + LibraryItemTracking.CreateItemTrackingCode(ItemTrackingCode, false, true, false); + + ComponentItem.Validate("Item Tracking Code", ItemTrackingCode.Code); + ComponentItem.Validate("Lot Nos.", LotNoSeries.Code); + ComponentItem.Modify(true); + end; + + local procedure PostComponentInventoryAsLot(ItemNo: Code[20]; LocationCode: Code[10]; LotNo: Code[50]; Qty: Decimal) + var + ItemJournalLine: Record "Item Journal Line"; + ReservationEntry: Record "Reservation Entry"; + begin + LibraryInventory.CreateItemJournalLineInItemTemplate(ItemJournalLine, ItemNo, LocationCode, '', Qty); + LibraryItemTracking.CreateItemJournalLineItemTracking(ReservationEntry, ItemJournalLine, '', LotNo, '', Qty); + LibraryInventory.PostItemJournalLine(ItemJournalLine."Journal Template Name", ItemJournalLine."Journal Batch Name"); + end; + + local procedure EnablePackageTrackingOnTransferComponent(Item: Record Item) + var + ComponentItem: Record Item; + ProductionBOMLine: Record "Production BOM Line"; + ItemTrackingCode: Record "Item Tracking Code"; + begin + ProductionBOMLine.SetRange("Production BOM No.", Item."Production BOM No."); + ProductionBOMLine.FindLast(); + ComponentItem.Get(ProductionBOMLine."No."); + + LibraryItemTracking.CreateItemTrackingCode(ItemTrackingCode, false, false, true); + + ComponentItem.Validate("Item Tracking Code", ItemTrackingCode.Code); + ComponentItem.Modify(true); + end; + + local procedure PostComponentInventoryAsPackage(ItemNo: Code[20]; LocationCode: Code[10]; PackageNo: Code[50]; Qty: Decimal) + var + ItemJournalLine: Record "Item Journal Line"; + ReservationEntry: Record "Reservation Entry"; + begin + LibraryInventory.CreateItemJournalLineInItemTemplate(ItemJournalLine, ItemNo, LocationCode, '', Qty); + LibraryItemTracking.CreateItemJournalLineItemTracking(ReservationEntry, ItemJournalLine, '', '', PackageNo, Qty); + LibraryInventory.PostItemJournalLine(ItemJournalLine."Journal Template Name", ItemJournalLine."Journal Batch Name"); + end; + + local procedure PostComponentInventoryAsSerials(ItemNo: Code[20]; LocationCode: Code[10]; Qty: Integer) + var + ItemJournalLine: Record "Item Journal Line"; + ReservationEntry: Record "Reservation Entry"; + i: Integer; + begin + // All serials are posted from a single journal line so the resulting item ledger entries + // share the same Document No. and Document Line No. (as a real transfer receipt would). + LibraryInventory.CreateItemJournalLineInItemTemplate(ItemJournalLine, ItemNo, LocationCode, '', Qty); + for i := 1 to Qty do + LibraryItemTracking.CreateItemJournalLineItemTracking( + ReservationEntry, ItemJournalLine, CopyStr(StrSubstNo(SerialNoTok, i), 1, 50), '', '', 1); + LibraryInventory.PostItemJournalLine(ItemJournalLine."Journal Template Name", ItemJournalLine."Journal Batch Name"); + end; + + local procedure FindPostedComponentItemLedgerEntry(var ItemLedgerEntry: Record "Item Ledger Entry"; ItemNo: Code[20]; LocationCode: Code[10]) + begin + ItemLedgerEntry.SetRange("Item No.", ItemNo); + ItemLedgerEntry.SetRange("Location Code", LocationCode); + ItemLedgerEntry.SetRange(Positive, true); + ItemLedgerEntry.FindFirst(); + end; + + local procedure BuildTransferReceiptLineForComponent(var TransferReceiptLine: Record "Transfer Receipt Line"; ProdOrderComponent: Record "Prod. Order Component"; ItemLedgerEntry: Record "Item Ledger Entry") + begin + // The reservation procedure only reads the transfer receipt line, so an in-memory record is sufficient. + TransferReceiptLine.Init(); + TransferReceiptLine."Document No." := ItemLedgerEntry."Document No."; + TransferReceiptLine."Line No." := ItemLedgerEntry."Document Line No."; + TransferReceiptLine."Item No." := ProdOrderComponent."Item No."; + TransferReceiptLine."Transfer-to Code" := ProdOrderComponent."Location Code"; + TransferReceiptLine."Subc. Prod. Order No." := ProdOrderComponent."Prod. Order No."; + TransferReceiptLine."Subc. Prod. Order Line No." := ProdOrderComponent."Prod. Order Line No."; + TransferReceiptLine."Subc. Prod. Ord. Comp Line No." := ProdOrderComponent."Line No."; + TransferReceiptLine."Subc. Operation No." := '10'; + end; + local procedure CreateSubcontractingPurchaseOrderAndReduceQuantity(Item: Record Item; WorkCenter: Record "Work Center"; ProductionOrder: Record "Production Order"; var PurchaseHeader: Record "Purchase Header"; var PurchaseLine: Record "Purchase Line"; ReducedQty: Decimal) begin SubcontractingMgmtLibrary.CreateSubcontractingOrderFromProdOrderRtngPage(Item."Routing No.", WorkCenter."No."); @@ -388,6 +576,7 @@ codeunit 149915 "Subc. TransOrd. Reserv. Test" var Assert: Codeunit Assert; LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibraryInventory: Codeunit "Library - Inventory"; LibraryItemTracking: Codeunit "Library - Item Tracking"; LibrarySetupStorage: Codeunit "Library - Setup Storage"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; @@ -395,6 +584,7 @@ codeunit 149915 "Subc. TransOrd. Reserv. Test" LibraryUtility: Codeunit "Library - Utility"; LibraryVariableStorage: Codeunit "Library - Variable Storage"; SubcontractingMgmtLibrary: Codeunit "Subc. Management Library"; + SubcontractingManagement: Codeunit "Subcontracting Management"; SubSetupLibrary: Codeunit "Subc. Setup Library"; SubcWarehouseLibrary: Codeunit "Subc. Warehouse Library"; IsInitialized: Boolean;