diff --git a/src/Layers/IT/BaseApp/Manufacturing/Document/ProdOrderComponent.Table.al b/src/Layers/IT/BaseApp/Manufacturing/Document/ProdOrderComponent.Table.al index b040e90ba4..fab5445ad9 100644 --- a/src/Layers/IT/BaseApp/Manufacturing/Document/ProdOrderComponent.Table.al +++ b/src/Layers/IT/BaseApp/Manufacturing/Document/ProdOrderComponent.Table.al @@ -190,6 +190,9 @@ table 5407 "Prod. Order Component" begin UpdateExpectedQuantity(); + if IsTemporary() then + exit; + ProdOrderLine.Get(Status, "Prod. Order No.", "Prod. Order Line No."); IsHandled := false; @@ -1286,7 +1289,13 @@ table 5407 "Prod. Order Component" ProdOrderLine: Record "Prod. Order Line"; ProdOrderRtngLine: Record "Prod. Order Routing Line"; NeededQty: Decimal; + IsHandled: Boolean; begin + IsHandled := IsTemporary(); + OnBeforeGetProdOrderNeeds(Rec, ProdOrderLine, IsHandled); + if IsHandled then + exit; + ProdOrderLine.Get(Status, "Prod. Order No.", "Prod. Order Line No."); if "Due Date" = 0D then begin @@ -1522,7 +1531,7 @@ table 5407 "Prod. Order Component" ProdOrderLine: Record "Prod. Order Line"; IsHandled: Boolean; begin - IsHandled := false; + IsHandled := IsTemporary(); OnBeforeCreateDim(Rec, DefaultDimSource, CurrFieldNo, IsHandled); if IsHandled then exit; @@ -1747,10 +1756,16 @@ table 5407 "Prod. Order Component" var ProdOrderLine: Record "Prod. Order Line"; WMSManagement: Codeunit "WMS Management"; + IsHandled: Boolean; begin - OnBeforeGetDefaultConsumptionBin(Rec, ProdOrderRtngLine, BinCode); + IsHandled := IsTemporary(); + OnBeforeGetDefaultConsumptionBin(Rec, ProdOrderRtngLine, BinCode, ProdOrderLine, IsHandled); - ProdOrderLine.Get(Status, "Prod. Order No.", "Prod. Order Line No."); + if IsHandled then + exit; + + if not IsTemporary() then + ProdOrderLine.Get(Status, "Prod. Order No.", "Prod. Order Line No."); if "Location Code" = ProdOrderLine."Location Code" then if FindFirstRtngLine(ProdOrderRtngLine, ProdOrderLine) then BinCode := GetBinCodeFromRtngLine(ProdOrderRtngLine); @@ -1882,7 +1897,7 @@ table 5407 "Prod. Order Component" ProdOrderComp2: Record "Prod. Order Component"; OverwriteBinCode, IsHandled : Boolean; begin - IsHandled := false; + IsHandled := IsTemporary(); OnBeforeUpdateBin(ProdOrderComp, FieldNo, FieldCaption, IsHandled); if IsHandled then exit; @@ -2194,6 +2209,8 @@ table 5407 "Prod. Order Component" var DefaultDimSource: List of [Dictionary of [Integer, Code[20]]]; begin + if IsTemporary() then + exit; InitDefaultDimensionSources(DefaultDimSource); CreateDim(DefaultDimSource); end; @@ -2460,7 +2477,7 @@ table 5407 "Prod. Order Component" end; [IntegrationEvent(false, false)] - local procedure OnBeforeGetDefaultConsumptionBin(var ProdOrderComponent: Record "Prod. Order Component"; var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var BinCode: Code[20]) + local procedure OnBeforeGetDefaultConsumptionBin(var ProdOrderComponent: Record "Prod. Order Component"; var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var BinCode: Code[20]; var ProdOrderLine: Record "Prod. Order Line"; var IsHandled: Boolean) begin end; @@ -2639,5 +2656,9 @@ table 5407 "Prod. Order Component" local procedure OnBeforeCreateDim(var ProdOrderComponent: Record "Prod. Order Component"; var DefaultDimSource: List of [Dictionary of [Integer, Code[20]]]; CurrentFieldNo: Integer; var IsHandled: Boolean) begin end; -} + [IntegrationEvent(false, false)] + local procedure OnBeforeGetProdOrderNeeds(var ProdOrderComponent: Record "Prod. Order Component"; var ProdOrderLine: Record "Prod. Order Line"; var IsHandled: Boolean) + begin + end; +} \ No newline at end of file diff --git a/src/Layers/IT/BaseApp/Manufacturing/Document/ProdOrderRoutingLine.Table.al b/src/Layers/IT/BaseApp/Manufacturing/Document/ProdOrderRoutingLine.Table.al index 252cbe42ba..cc057398e6 100644 --- a/src/Layers/IT/BaseApp/Manufacturing/Document/ProdOrderRoutingLine.Table.al +++ b/src/Layers/IT/BaseApp/Manufacturing/Document/ProdOrderRoutingLine.Table.al @@ -1114,9 +1114,13 @@ table 5409 "Prod. Order Routing Line" local procedure GetProdOrderLine() begin + OnBeforeGetProdOrderLine(Rec, ProdOrderLine, ProdOrderLineRead); if ProdOrderLineRead then exit; + if Rec.IsTemporary() then + exit; + ProdOrderLine.SetRange(Status, Status); ProdOrderLine.SetRange("Prod. Order No.", "Prod. Order No."); ProdOrderLine.SetRange("Routing No.", "Routing No."); @@ -1406,6 +1410,9 @@ table 5409 "Prod. Order Routing Line" if IsHandled then exit; + if IsTemporary() then + exit; + if "Routing Status" = "Routing Status"::Finished then FieldError("Routing Status"); @@ -1824,6 +1831,9 @@ table 5409 "Prod. Order Routing Line" TempProdOrderRoutingLine: Record "Prod. Order Routing Line" temporary; ProdOrderRoutingLine: Record "Prod. Order Routing Line"; begin + if Rec.IsTemporary() then + exit; + if SkipUpdateOfCompBinCodes then exit; @@ -2052,6 +2062,30 @@ table 5409 "Prod. Order Routing Line" exit("Next Operation No." <> ''); end; + internal procedure CheckPreviousAndNextForTemp() + var + TempAffectedRoutingLine: Record "Prod. Order Routing Line" temporary; + PrevOperNo: Code[30]; + NextOperNo: Code[30]; + begin + PrevOperNo := Rec."Previous Operation No."; + NextOperNo := Rec."Next Operation No."; + + TempAffectedRoutingLine.Copy(Rec, true); + + if PrevOperNo <> '' then + if TempAffectedRoutingLine.Get(Rec.Status, Rec."Prod. Order No.", Rec."Routing Reference No.", Rec."Routing No.", PrevOperNo) then begin + TempAffectedRoutingLine."Next Operation No." := NextOperNo; + TempAffectedRoutingLine.Modify(); + end; + + if NextOperNo <> '' then + if TempAffectedRoutingLine.Get(Rec.Status, Rec."Prod. Order No.", Rec."Routing Reference No.", Rec."Routing No.", NextOperNo) then begin + TempAffectedRoutingLine."Previous Operation No." := PrevOperNo; + TempAffectedRoutingLine.Modify(); + end; + end; + [IntegrationEvent(false, false)] local procedure OnAfterCalcStartingEndingDates(var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var xProdOrderRoutingLine: Record "Prod. Order Routing Line"; var ProdOrderLine: Record "Prod. Order Line"; CallingFieldNo: Integer) begin @@ -2243,5 +2277,9 @@ table 5409 "Prod. Order Routing Line" local procedure OnCheckPreviousAndNextOnBeforeInsertTempRemainingProdOrderRtngLine(ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var ProdOrderRoutingLineFiltered: Record "Prod. Order Routing Line"; var TempDeletedProdOrderRoutingLine: Record "Prod. Order Routing Line" temporary; var ErrorOnNext: Boolean; var ErrorOnPrevious: Boolean) begin end; -} + [IntegrationEvent(false, false)] + local procedure OnBeforeGetProdOrderLine(ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var ProdOrderLine: Record "Prod. Order Line"; var ProdOrderLineRead: Boolean) + begin + end; +} \ No newline at end of file diff --git a/src/Layers/IT/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al b/src/Layers/IT/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al index 57036f69d2..7952126db1 100644 --- a/src/Layers/IT/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al +++ b/src/Layers/IT/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al @@ -245,6 +245,75 @@ page 99000768 "Manufacturing Setup" } } #endif + group(ProductionDefinitionWizard) + { + Caption = 'Production Definition Wizard'; + + group(BOMRoutingDisplayGroup) + { + Caption = 'BOM/Routing Step Display'; + + field("Show Rtng BOM Select Both"; Rec."Show Rtng BOM Select Both") + { + ApplicationArea = Manufacturing; + } + field("Show Rtng BOM Select Partial"; Rec."Show Rtng BOM Select Partial") + { + ApplicationArea = Manufacturing; + } + field("Show Rtng BOM Select Nothing"; Rec."Show Rtng BOM Select Nothing") + { + ApplicationArea = Manufacturing; + } + } + group(ProdCompDisplayGroup) + { + Caption = 'Components/Routing Preview Step Display'; + + field("Show Prod Comp Select Both"; Rec."Show Prod Comp Select Both") + { + ApplicationArea = Manufacturing; + } + field("Show Prod Comp Select Partial"; Rec."Show Prod Comp Select Partial") + { + ApplicationArea = Manufacturing; + } + field("Show Prod Comp Select Nothing"; Rec."Show Prod Comp Select Nothing") + { + ApplicationArea = Manufacturing; + } + } + group(GeneralWizardOptions) + { + Caption = 'General Options'; + + field("Always Save Modified Versions"; Rec."Always Save Modified Versions") + { + ApplicationArea = Manufacturing; + } + field("Allow Edit UI Selection"; Rec."Allow Edit UI Selection") + { + ApplicationArea = Manufacturing; + } + } + group(WizardDefaults) + { + Caption = 'Defaults'; + + field("Def. Wiz. Work Center No."; Rec."Def. Wiz. Work Center No.") + { + ApplicationArea = Manufacturing; + } + field("Def. Wiz. Comp Item No."; Rec."Def. Wiz. Comp Item No.") + { + ApplicationArea = Manufacturing; + } + field("Def. Wiz. Flushing Method"; Rec."Def. Wiz. Flushing Method") + { + ApplicationArea = Manufacturing; + } + } + } } area(factboxes) { @@ -349,4 +418,13 @@ page 99000768 "Manufacturing Setup" ManufacturingSetupNotif.ShowPlanningFieldsMoveNotification(); end; +<<<<<<< src/Layers/IT/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al +} +||||||| Base: src/Layers/W1/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al + +} + +======= + } +>>>>>>> src/Layers/W1/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al \ No newline at end of file diff --git a/src/Layers/IT/BaseApp/Manufacturing/Setup/ManufacturingSetup.Table.al b/src/Layers/IT/BaseApp/Manufacturing/Setup/ManufacturingSetup.Table.al index 31701bc59d..ba2d534282 100644 --- a/src/Layers/IT/BaseApp/Manufacturing/Setup/ManufacturingSetup.Table.al +++ b/src/Layers/IT/BaseApp/Manufacturing/Setup/ManufacturingSetup.Table.al @@ -10,6 +10,7 @@ using Microsoft.Foundation.Calendar; #endif using Microsoft.Foundation.NoSeries; using Microsoft.Inventory.BOM.Tree; +using Microsoft.Inventory.Item; using Microsoft.Inventory.Location; using Microsoft.Inventory.Planning; #if not CLEAN27 @@ -19,6 +20,8 @@ using Microsoft.Manufacturing.Capacity; using Microsoft.Manufacturing.Forecast; using Microsoft.Manufacturing.MachineCenter; using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Manufacturing.WorkCenter; using System.Telemetry; using System.Utilities; @@ -336,6 +339,80 @@ table 99000765 "Manufacturing Setup" InitValue = "Pick + Manual"; ToolTip = 'Specifies default flushing method assigned to new items. A different flushing method on item cards will override this default.'; } + field(301; "Show Rtng BOM Select Both"; Enum "Prod. Definition Display") + { + Caption = 'Show BOM/Routing (Both Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the BOM/Routing selection step is shown when both a BOM and a Routing exist for the item. This setting only applies in Create Production Order mode. In Define Item Structure mode, the BOM and Routing steps are always shown in Edit mode.'; + } + field(302; "Show Rtng BOM Select Partial"; Enum "Prod. Definition Display") + { + Caption = 'Show BOM/Routing (Partially Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the BOM/Routing selection step is shown when only one of BOM or Routing exists. This setting only applies in Create Production Order mode. In Define Item Structure mode, the BOM and Routing steps are always shown in Edit mode.'; + } + field(303; "Show Rtng BOM Select Nothing"; Enum "Prod. Definition Display") + { + Caption = 'Show BOM/Routing (None Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the BOM/Routing selection step is shown when neither a BOM nor a Routing exists. This setting only applies in Create Production Order mode. In Define Item Structure mode, the BOM and Routing steps are always shown in Edit mode.'; + } + field(304; "Show Prod Comp Select Both"; Enum "Prod. Definition Display") + { + Caption = 'Show Prod. Components (Both Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the components/routing preview steps are shown when both a BOM and a Routing exist. This setting only applies in Create Production Order mode. In Define Item Structure mode, the components and production routing steps are never shown.'; + } + field(305; "Show Prod Comp Select Partial"; Enum "Prod. Definition Display") + { + Caption = 'Show Prod. Components (Partially Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the components/routing preview steps are shown when only one of BOM or Routing exists. This setting only applies in Create Production Order mode. In Define Item Structure mode, the components and production routing steps are never shown.'; + } + field(306; "Show Prod Comp Select Nothing"; Enum "Prod. Definition Display") + { + Caption = 'Show Prod. Components (None Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the components/routing preview steps are shown when neither BOM nor Routing exists. This setting only applies in Create Production Order mode. In Define Item Structure mode, the components and production routing steps are never shown.'; + } + field(307; "Always Save Modified Versions"; Boolean) + { + Caption = 'Always Save Modified Versions'; + DataClassification = CustomerContent; + ToolTip = 'Specifies that newly created BOM/Routing versions are always kept even when the Save option is set to Empty when using the production order creation wizard.'; + } + field(308; "Allow Edit UI Selection"; Boolean) + { + Caption = 'Allow Edit UI Selection'; + DataClassification = CustomerContent; + ToolTip = 'Specifies that users can override the step visibility settings (Show/Edit/Hide) on the wizard introduction step.'; + } + field(309; "Def. Wiz. Work Center No."; Code[20]) + { + Caption = 'Default Prod. Wiz. Work Center No.'; + DataClassification = CustomerContent; + TableRelation = "Work Center"; + ToolTip = 'Specifies the fallback work center to use when creating new routing lines without an explicit work center.'; + } + field(310; "Def. Wiz. Comp Item No."; Code[20]) + { + Caption = 'Default Prod. Wiz. Component Item No.'; + DataClassification = CustomerContent; + TableRelation = Item; + ToolTip = 'Specifies the default item to use when initializing a new BOM from scratch through the production order creation wizard.'; + } + field(311; "Def. Wiz. Flushing Method"; Enum "Flushing Method") + { + Caption = 'Default Prod. Wiz. Flushing Method'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the default flushing method applied to Production Order Components when creating a new production BOM from scratch through the production order creation wizard. This is only used for temporary BOM creation; existing BOMs retain their original flushing method.'; + } field(5500; "Preset Output Quantity"; Option) { Caption = 'Preset Output Quantity'; @@ -439,4 +516,4 @@ table 99000765 "Manufacturing Setup" begin exit(ManufacturingSetupFeatureTelemetryNameLbl); end; -} +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Document/CreateOrderFromSales.Page.al b/src/Layers/W1/BaseApp/Manufacturing/Document/CreateOrderFromSales.Page.al index 45b396331b..f8a5df44fd 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Document/CreateOrderFromSales.Page.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Document/CreateOrderFromSales.Page.al @@ -36,6 +36,18 @@ page 99000884 "Create Order From Sales" { ApplicationArea = Basic, Suite; Caption = 'Order Type'; + + trigger OnValidate() + begin + UpdateWizardEditable(); + end; + } + field(UseProductDefinitionWizard; UseProductDefinitionWizard) + { + ApplicationArea = Manufacturing; + Caption = 'Use Production Definition Wizard'; + Editable = UseWizardEditable; + ToolTip = 'Prepare to create a production order for the selected sales demand. Optionally use the Production Definition Wizard to customize the order for a single line.'; } } } @@ -50,10 +62,16 @@ page 99000884 "Create Order From Sales" OrderStatus := CreateStatus; end; + protected var + OrderType: Enum "Create Production Order Type"; + var OrderStatus: Enum "Production Order Status"; CreateStatus: Enum "Create Production Order Status"; - OrderType: Enum "Create Production Order Type"; + UseProductDefinitionWizard: Boolean; + SingleLineSelected: Boolean; + UseWizardEditable: Boolean; + procedure GetParameters(var NewStatus: Enum "Production Order Status"; var NewOrderType: Enum "Create Production Order Type") begin @@ -67,5 +85,29 @@ page 99000884 "Create Order From Sales" CreateStatus := OrderStatus; OrderType := NewOrderType; end; -} + /// + /// Returns whether the Production Definition Wizard should be used for order creation. + /// + internal procedure GetUseProductDefinitionWizard(): Boolean + begin + exit(UseProductDefinitionWizard and SingleLineSelected); + end; + + /// + /// Sets whether exactly one sales planning line is selected on the parent page. + /// Controls editability of the UseProductDefinitionWizard toggle. + /// + internal procedure SetSingleLineSelected(IsSingleLine: Boolean) + begin + SingleLineSelected := IsSingleLine; + UpdateWizardEditable(); + end; + + local procedure UpdateWizardEditable() + begin + UseWizardEditable := SingleLineSelected and (OrderType = "Create Production Order Type"::ItemOrder); + if not UseWizardEditable then + UseProductDefinitionWizard := false; + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderComponent.Table.al b/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderComponent.Table.al index b1e63de287..e771465ebb 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderComponent.Table.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderComponent.Table.al @@ -175,6 +175,9 @@ table 5407 "Prod. Order Component" begin UpdateExpectedQuantity(); + if IsTemporary() then + exit; + ProdOrderLine.Get(Status, "Prod. Order No.", "Prod. Order Line No."); IsHandled := false; @@ -1150,8 +1153,14 @@ table 5407 "Prod. Order Component" ProdOrderLine: Record "Prod. Order Line"; ProdOrderRtngLine: Record "Prod. Order Routing Line"; NeededQty: Decimal; + IsHandled: Boolean; begin - ProdOrderLine.Get(Status, "Prod. Order No.", "Prod. Order Line No."); + OnBeforeGetProdOrderNeeds(Rec, ProdOrderLine, IsHandled); + if IsHandled then + exit; + + if not IsTemporary() then + ProdOrderLine.Get(Status, "Prod. Order No.", "Prod. Order Line No."); if "Due Date" = 0D then begin "Due Date" := ProdOrderLine."Starting Date"; @@ -1386,7 +1395,7 @@ table 5407 "Prod. Order Component" ProdOrderLine: Record "Prod. Order Line"; IsHandled: Boolean; begin - IsHandled := false; + IsHandled := IsTemporary(); OnBeforeCreateDim(Rec, DefaultDimSource, CurrFieldNo, IsHandled); if IsHandled then exit; @@ -1611,10 +1620,16 @@ table 5407 "Prod. Order Component" var ProdOrderLine: Record "Prod. Order Line"; WMSManagement: Codeunit "WMS Management"; + IsHandled: Boolean; begin - OnBeforeGetDefaultConsumptionBin(Rec, ProdOrderRtngLine, BinCode); + IsHandled := IsTemporary(); + OnBeforeGetDefaultConsumptionBin(Rec, ProdOrderRtngLine, BinCode, ProdOrderLine, IsHandled); - ProdOrderLine.Get(Status, "Prod. Order No.", "Prod. Order Line No."); + if IsHandled then + exit; + + if not IsTemporary() then + ProdOrderLine.Get(Status, "Prod. Order No.", "Prod. Order Line No."); if "Location Code" = ProdOrderLine."Location Code" then if FindFirstRtngLine(ProdOrderRtngLine, ProdOrderLine) then BinCode := GetBinCodeFromRtngLine(ProdOrderRtngLine); @@ -1746,7 +1761,7 @@ table 5407 "Prod. Order Component" ProdOrderComp2: Record "Prod. Order Component"; OverwriteBinCode, IsHandled : Boolean; begin - IsHandled := false; + IsHandled := IsTemporary(); OnBeforeUpdateBin(ProdOrderComp, FieldNo, FieldCaption, IsHandled); if IsHandled then exit; @@ -2058,6 +2073,8 @@ table 5407 "Prod. Order Component" var DefaultDimSource: List of [Dictionary of [Integer, Code[20]]]; begin + if IsTemporary() then + exit; InitDefaultDimensionSources(DefaultDimSource); CreateDim(DefaultDimSource); end; @@ -2324,7 +2341,7 @@ table 5407 "Prod. Order Component" end; [IntegrationEvent(false, false)] - local procedure OnBeforeGetDefaultConsumptionBin(var ProdOrderComponent: Record "Prod. Order Component"; var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var BinCode: Code[20]) + local procedure OnBeforeGetDefaultConsumptionBin(var ProdOrderComponent: Record "Prod. Order Component"; var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var BinCode: Code[20]; var ProdOrderLine: Record "Prod. Order Line"; var IsHandled: Boolean) begin end; @@ -2487,5 +2504,9 @@ table 5407 "Prod. Order Component" local procedure OnBeforeCreateDim(var ProdOrderComponent: Record "Prod. Order Component"; var DefaultDimSource: List of [Dictionary of [Integer, Code[20]]]; CurrentFieldNo: Integer; var IsHandled: Boolean) begin end; -} + [IntegrationEvent(false, false)] + local procedure OnBeforeGetProdOrderNeeds(var ProdOrderComponent: Record "Prod. Order Component"; var ProdOrderLine: Record "Prod. Order Line"; var IsHandled: Boolean) + begin + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderLine.Table.al b/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderLine.Table.al index 56e3d7433f..3465fa0b44 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderLine.Table.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderLine.Table.al @@ -64,7 +64,8 @@ table 5406 "Prod. Order Line" IsHandled := false; OnValidateItemNoOnBeforeCheckReservations(Rec, xRec, CurrFieldNo, IsHandled); if not IsHandled then begin - ProdOrderLineReserve.VerifyChange(Rec, xRec); + if not IsTemporary() then + ProdOrderLineReserve.VerifyChange(Rec, xRec); TestField("Finished Quantity", 0); CalcFields("Reserved Quantity"); TestField("Reserved Quantity", 0); @@ -77,7 +78,9 @@ table 5406 "Prod. Order Line" if "Item No." = '' then Init() else begin - ProdOrder.Get(Status, "Prod. Order No."); + OnBeforeGetProdOrder(Rec, ProdOrder); + if not Rec.IsTemporary() then + ProdOrder.Get(Status, "Prod. Order No."); "Starting Date" := ProdOrder."Starting Date"; "Starting Time" := ProdOrder."Starting Time"; "Ending Date" := ProdOrder."Ending Date"; @@ -142,7 +145,8 @@ table 5406 "Prod. Order Line" var ItemVariant: Record "Item Variant"; begin - ProdOrderLineReserve.VerifyChange(Rec, xRec); + if not IsTemporary() then + ProdOrderLineReserve.VerifyChange(Rec, xRec); TestField("Finished Quantity", 0); CalcFields("Reserved Quantity"); TestField("Reserved Quantity", 0); @@ -184,10 +188,14 @@ table 5406 "Prod. Order Line" ProductionOrder: Record "Production Order"; begin ProductionOrder.SetLoadFields(Status, "No.", "Location Code"); - ProductionOrder.Get(Rec.Status, Rec."Prod. Order No."); - ProdOrderLineReserve.VerifyChange(Rec, xRec); - ProdOrderWarehouseMgt.ProdOrderLineVerifyChange(Rec, xRec); - ProdOrderWarehouseMgt.CompareProdOrderWithProdOrderLinesForLocation(ProductionOrder, Rec); + if not Rec.IsTemporary() then begin + ProductionOrder.Get(Rec.Status, Rec."Prod. Order No."); + ProdOrderLineReserve.VerifyChange(Rec, xRec); + + ProdOrderWarehouseMgt.ProdOrderLineVerifyChange(Rec, xRec); + + ProdOrderWarehouseMgt.CompareProdOrderWithProdOrderLinesForLocation(ProductionOrder, Rec); + end; ProdOrderWarehouseMgt.ValidateWarehousePutAwayLocation(Rec); GetUpdateFromSKU(); GetDefaultBin(); @@ -302,7 +310,8 @@ table 5406 "Prod. Order Line" if "Remaining Quantity" < 0 then "Remaining Quantity" := 0; "Remaining Qty. (Base)" := CalcBaseQty("Remaining Quantity", FieldCaption("Remaining Quantity"), FieldCaption("Remaining Qty. (Base)")); - ProdOrderLineReserve.VerifyQuantity(Rec, xRec); + if not IsTemporary() then + ProdOrderLineReserve.VerifyQuantity(Rec, xRec); ProdOrderWarehouseMgt.ProdOrderLineVerifyChange(Rec, xRec); UpdateProdOrderComp(xRec."Qty. per Unit of Measure"); @@ -485,7 +494,7 @@ table 5406 "Prod. Order Line" ModifyRecord: Boolean; IsHandled: Boolean; begin - IsHandled := false; + IsHandled := IsTemporary(); OnBeforeValidateRoutingNo(Rec, xRec, CurrFieldNo, IsHandled); if IsHandled then exit; @@ -1154,6 +1163,9 @@ table 5406 "Prod. Order Line" begin OnBeforeDeleteRelations(Rec); + if IsTemporary() then + exit; + ProdOrderLine.SetRange(Status, Status); ProdOrderLine.SetRange("Prod. Order No.", "Prod. Order No."); ProdOrderLine.SetRange("Routing No.", "Routing No."); @@ -1790,6 +1802,9 @@ table 5406 "Prod. Order Line" var DefaultDimSource: List of [Dictionary of [Integer, Code[20]]]; begin + if Rec.IsTemporary() then + exit; + InitDefaultDimensionSources(DefaultDimSource); CreateDim(DefaultDimSource); end; @@ -2111,4 +2126,9 @@ table 5406 "Prod. Order Line" local procedure OnBeforeOnValidateUnitCost(var ProdOrderLine: Record "Prod. Order Line"; var SkipInventoryValueZeroCheck: Boolean) begin end; -} + + [IntegrationEvent(false, false)] + local procedure OnBeforeGetProdOrder(var ProdOrderLine: Record "Prod. Order Line"; var ProdOrder: Record "Production Order") + begin + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderLineReserve.Codeunit.al b/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderLineReserve.Codeunit.al index 67eebad572..786d53e5ac 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderLineReserve.Codeunit.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderLineReserve.Codeunit.al @@ -409,6 +409,10 @@ codeunit 99000837 "Prod. Order Line-Reserve" if ProdOrderLine.Status = ProdOrderLine.Status::Simulated then exit; + + if ProdOrderLine.IsTemporary() then + exit; + if ProdOrderLine."Item No." <> '' then PlanningAssignment.ChkAssignOne(ProdOrderLine."Item No.", ProdOrderLine."Variant Code", ProdOrderLine."Location Code", WorkDate()); end; diff --git a/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderRoutingLine.Table.al b/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderRoutingLine.Table.al index 062a961803..a4fc217f5f 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderRoutingLine.Table.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Document/ProdOrderRoutingLine.Table.al @@ -951,9 +951,13 @@ table 5409 "Prod. Order Routing Line" local procedure GetProdOrderLine() begin + OnBeforeGetProdOrderLine(Rec, ProdOrderLine, ProdOrderLineRead); if ProdOrderLineRead then exit; + if Rec.IsTemporary() then + exit; + ProdOrderLine.SetRange(Status, Status); ProdOrderLine.SetRange("Prod. Order No.", "Prod. Order No."); ProdOrderLine.SetRange("Routing No.", "Routing No."); @@ -1228,6 +1232,9 @@ table 5409 "Prod. Order Routing Line" if IsHandled then exit; + if IsTemporary() then + exit; + if "Routing Status" = "Routing Status"::Finished then FieldError("Routing Status"); @@ -1609,6 +1616,9 @@ table 5409 "Prod. Order Routing Line" TempProdOrderRoutingLine: Record "Prod. Order Routing Line" temporary; ProdOrderRoutingLine: Record "Prod. Order Routing Line"; begin + if Rec.IsTemporary() then + exit; + if SkipUpdateOfCompBinCodes then exit; @@ -1837,6 +1847,30 @@ table 5409 "Prod. Order Routing Line" exit("Next Operation No." <> ''); end; + internal procedure CheckPreviousAndNextForTemp() + var + TempAffectedRoutingLine: Record "Prod. Order Routing Line" temporary; + PrevOperNo: Code[30]; + NextOperNo: Code[30]; + begin + PrevOperNo := Rec."Previous Operation No."; + NextOperNo := Rec."Next Operation No."; + + TempAffectedRoutingLine.Copy(Rec, true); + + if PrevOperNo <> '' then + if TempAffectedRoutingLine.Get(Rec.Status, Rec."Prod. Order No.", Rec."Routing Reference No.", Rec."Routing No.", PrevOperNo) then begin + TempAffectedRoutingLine."Next Operation No." := NextOperNo; + TempAffectedRoutingLine.Modify(); + end; + + if NextOperNo <> '' then + if TempAffectedRoutingLine.Get(Rec.Status, Rec."Prod. Order No.", Rec."Routing Reference No.", Rec."Routing No.", NextOperNo) then begin + TempAffectedRoutingLine."Previous Operation No." := PrevOperNo; + TempAffectedRoutingLine.Modify(); + end; + end; + [IntegrationEvent(false, false)] local procedure OnAfterCalcStartingEndingDates(var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var xProdOrderRoutingLine: Record "Prod. Order Routing Line"; var ProdOrderLine: Record "Prod. Order Line"; CallingFieldNo: Integer) begin @@ -2026,5 +2060,9 @@ table 5409 "Prod. Order Routing Line" local procedure OnCheckPreviousAndNextOnBeforeInsertTempRemainingProdOrderRtngLine(ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var ProdOrderRoutingLineFiltered: Record "Prod. Order Routing Line"; var TempDeletedProdOrderRoutingLine: Record "Prod. Order Routing Line" temporary; var ErrorOnNext: Boolean; var ErrorOnPrevious: Boolean) begin end; -} + [IntegrationEvent(false, false)] + local procedure OnBeforeGetProdOrderLine(ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var ProdOrderLine: Record "Prod. Order Line"; var ProdOrderLineRead: Boolean) + begin + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Document/ProductionOrder.Table.al b/src/Layers/W1/BaseApp/Manufacturing/Document/ProductionOrder.Table.al index d32fe79a5a..fadd79714c 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Document/ProductionOrder.Table.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Document/ProductionOrder.Table.al @@ -1606,6 +1606,9 @@ table 5405 "Production Order" var DefaultDimSource: List of [Dictionary of [Integer, Code[20]]]; begin + if Rec.IsTemporary() then + exit; + InitDefaultDimensionSources(DefaultDimSource); CreateDim(DefaultDimSource); end; @@ -1893,4 +1896,4 @@ table 5405 "Production Order" local procedure OnCreatePickOnBeforeRunCreatePickFromWhseSource(var ProdOrderComponent: Record "Prod. Order Component") begin end; -} +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Inventory/Item/MfgItemCard.PageExt.al b/src/Layers/W1/BaseApp/Manufacturing/Inventory/Item/MfgItemCard.PageExt.al index 07b187d809..eb6a645654 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Inventory/Item/MfgItemCard.PageExt.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Inventory/Item/MfgItemCard.PageExt.al @@ -6,6 +6,7 @@ namespace Microsoft.Inventory.Item; using Microsoft.Manufacturing.ProductionBOM; using Microsoft.Manufacturing.StandardCost; +using Microsoft.Manufacturing.Wizard; pageextension 99000750 "Mfg. Item Card" extends "Item Card" { @@ -163,5 +164,22 @@ pageextension 99000750 "Mfg. Item Card" extends "Item Card" end; } } + addafter("&Create Stockkeeping Unit") + { + action(RunProdDefinition) + { + ApplicationArea = Manufacturing; + Caption = 'Production Definition'; + Image = ProductionSetup; + ToolTip = 'Define or review the bill of materials and routing for this item using the Production Definition Wizard.'; + + trigger OnAction() + var + ProductionDefinitionManager: Codeunit "Production Definition Manager"; + begin + ProductionDefinitionManager.RunForSource(Rec, "Prod. Definition Mode"::DefineItemStructure); + end; + } + } } -} +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Inventory/Item/MfgItemList.PageExt.al b/src/Layers/W1/BaseApp/Manufacturing/Inventory/Item/MfgItemList.PageExt.al index b5acded45e..0afaad1be7 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Inventory/Item/MfgItemList.PageExt.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Inventory/Item/MfgItemList.PageExt.al @@ -10,6 +10,7 @@ using Microsoft.Inventory.Reports; using Microsoft.Manufacturing.ProductionBOM; using Microsoft.Manufacturing.Reports; using Microsoft.Manufacturing.StandardCost; +using Microsoft.Manufacturing.Wizard; pageextension 99000751 "Mfg. Item List" extends "Item List" { @@ -178,6 +179,23 @@ pageextension 99000751 "Mfg. Item List" extends "Item List" RunObject = Report "Compare Production Cost Shares"; } } + addafter("&Create Stockkeeping Unit") + { + action(RunProdDefinition) + { + ApplicationArea = Manufacturing; + Caption = 'Production Definition'; + Image = ProductionSetup; + ToolTip = 'Define or review the bill of materials and routing for this item using the Production Definition Wizard.'; + + trigger OnAction() + var + ProductionDefinitionManager: Codeunit "Production Definition Manager"; + begin + ProductionDefinitionManager.RunForSource(Rec, "Prod. Definition Mode"::DefineItemStructure); + end; + } + } } procedure SelectActiveItemsForProductionBOM(): Text @@ -187,4 +205,4 @@ pageextension 99000751 "Mfg. Item List" extends "Item List" Item.SetFilter(Type, '<>%1', Item.Type::Service); exit(SelectInItemList(Item)); end; -} +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Inventory/Location/MfgStockkeepingUnitCard.PageExt.al b/src/Layers/W1/BaseApp/Manufacturing/Inventory/Location/MfgStockkeepingUnitCard.PageExt.al index 06f174a73b..144ca9d226 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Inventory/Location/MfgStockkeepingUnitCard.PageExt.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Inventory/Location/MfgStockkeepingUnitCard.PageExt.al @@ -4,6 +4,8 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Inventory.Location; +using Microsoft.Manufacturing.Wizard; + pageextension 99000754 "Mfg. Stockkeeping Unit Card" extends "Stockkeeping Unit Card" { layout @@ -89,5 +91,22 @@ pageextension 99000754 "Mfg. Stockkeeping Unit Card" extends "Stockkeeping Unit end; } } + addlast("F&unctions") + { + action(RunProdDefinition) + { + ApplicationArea = Manufacturing; + Caption = 'Production Definition'; + Image = ProductionSetup; + ToolTip = 'Define or review the bill of materials and routing for this item using the Production Definition Wizard.'; + + trigger OnAction() + var + ProductionDefinitionManager: Codeunit "Production Definition Manager"; + begin + ProductionDefinitionManager.RunForSource(Rec, "Prod. Definition Mode"::DefineItemStructure); + end; + } + } } } \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Inventory/Location/MfgStockkeepingUnitList.PageExt.al b/src/Layers/W1/BaseApp/Manufacturing/Inventory/Location/MfgStockkeepingUnitList.PageExt.al index c21525bcc6..c6e2c7197d 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Inventory/Location/MfgStockkeepingUnitList.PageExt.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Inventory/Location/MfgStockkeepingUnitList.PageExt.al @@ -4,6 +4,8 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Inventory.Location; +using Microsoft.Manufacturing.Wizard; + pageextension 99000755 "Mfg. Stockkeeping Unit List" extends "Stockkeeping Unit List" { actions @@ -40,5 +42,22 @@ pageextension 99000755 "Mfg. Stockkeeping Unit List" extends "Stockkeeping Unit } } } + addlast("F&unctions") + { + action(RunProdDefinition) + { + ApplicationArea = Manufacturing; + Caption = 'Production Definition'; + Image = ProductionSetup; + ToolTip = 'Define or review the bill of materials and routing for this item using the Production Definition Wizard.'; + + trigger OnAction() + var + ProductionDefinitionManager: Codeunit "Production Definition Manager"; + begin + ProductionDefinitionManager.RunForSource(Rec, "Prod. Definition Mode"::DefineItemStructure); + end; + } + } } } \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/ProductionBOM/ProductionBOMLine.Table.al b/src/Layers/W1/BaseApp/Manufacturing/ProductionBOM/ProductionBOMLine.Table.al index 534c8c0274..9fae918741 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/ProductionBOM/ProductionBOMLine.Table.al +++ b/src/Layers/W1/BaseApp/Manufacturing/ProductionBOM/ProductionBOMLine.Table.al @@ -410,7 +410,7 @@ table 99000772 "Production BOM Line" if IsHandled then exit; - if IsTemporary then + if IsTemporary() then exit; if "Version Code" = '' then begin @@ -622,4 +622,3 @@ table 99000772 "Production BOM Line" begin end; } - diff --git a/src/Layers/W1/BaseApp/Manufacturing/ProductionBOM/ProductionBOMLines.Page.al b/src/Layers/W1/BaseApp/Manufacturing/ProductionBOM/ProductionBOMLines.Page.al index e17b116f15..745aa97e73 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/ProductionBOM/ProductionBOMLines.Page.al +++ b/src/Layers/W1/BaseApp/Manufacturing/ProductionBOM/ProductionBOMLines.Page.al @@ -277,5 +277,4 @@ page 99000788 "Production BOM Lines" VariantCodeMandatory: Boolean; IsSaaSExcelAddinEnabled: Boolean; ExcelFileNameTxt: Label 'Production BOM Lines - %1 - %2', Comment = '%1 = Production BOM No., %2 = Version Code'; -} - +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Routing/RoutingLine.Table.al b/src/Layers/W1/BaseApp/Manufacturing/Routing/RoutingLine.Table.al index 09b8b670fe..938945be89 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Routing/RoutingLine.Table.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Routing/RoutingLine.Table.al @@ -615,6 +615,30 @@ table 99000764 "Routing Line" until Next() = 0; end; + internal procedure CheckPreviousAndNextForTemp() + var + TempAffectedRoutingLine: Record "Routing Line" temporary; + PrevOperNo: Code[30]; + NextOperNo: Code[30]; + begin + PrevOperNo := Rec."Previous Operation No."; + NextOperNo := Rec."Next Operation No."; + + TempAffectedRoutingLine.Copy(Rec, true); + + if PrevOperNo <> '' then + if TempAffectedRoutingLine.Get(Rec."Routing No.", Rec."Version Code", PrevOperNo) then begin + TempAffectedRoutingLine."Next Operation No." := NextOperNo; + TempAffectedRoutingLine.Modify(); + end; + + if NextOperNo <> '' then + if TempAffectedRoutingLine.Get(Rec."Routing No.", Rec."Version Code", NextOperNo) then begin + TempAffectedRoutingLine."Previous Operation No." := PrevOperNo; + TempAffectedRoutingLine.Modify(); + end; + end; + [IntegrationEvent(false, false)] local procedure OnAfterDeleteRelations(RoutingLine: Record "Routing Line") begin @@ -674,5 +698,4 @@ table 99000764 "Routing Line" local procedure OnBeforeValidateStandardTaskCode(var RoutingLine: Record "Routing Line"; xRoutingLine: Record "Routing Line"; var IsHandled: Boolean) begin end; -} - +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Sales/Document/MfgSalesOrderPlanning.PageExt.al b/src/Layers/W1/BaseApp/Manufacturing/Sales/Document/MfgSalesOrderPlanning.PageExt.al index 2a5eceb7cb..2a607b07a1 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Sales/Document/MfgSalesOrderPlanning.PageExt.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Sales/Document/MfgSalesOrderPlanning.PageExt.al @@ -5,6 +5,7 @@ namespace Microsoft.Sales.Document; using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.Wizard; pageextension 99000883 "Mfg. Sales Order Planning" extends "Sales Order Planning" { @@ -37,34 +38,43 @@ pageextension 99000883 "Mfg. Sales Order Planning" extends "Sales Order Planning var NewStatus: Enum "Production Order Status"; NewOrderType: Enum "Create Production Order Type"; + UseWizard: Boolean; Text001: Label 'There is nothing to plan.'; procedure CreateProdOrder() var CreateOrderFromSales: Page "Create Order From Sales"; + TempSalesPlanningLine: Record "Sales Planning Line" temporary; NewOrderTypeOption: Option; ShowCreateOrderForm: Boolean; IsHandled: Boolean; + SalesPlanningCount: Integer; begin + UseWizard := false; ShowCreateOrderForm := true; IsHandled := false; NewOrderTypeOption := NewOrderType.AsInteger(); - OnBeforeCreateProdOrder(Rec, NewStatus, NewOrderTypeOption, ShowCreateOrderForm, IsHandled); + TempSalesPlanningLine.Copy(Rec, true); + CurrPage.SetSelectionFilter(TempSalesPlanningLine); + + OnBeforeCreateProdOrder(TempSalesPlanningLine, NewStatus, NewOrderTypeOption, ShowCreateOrderForm, IsHandled); NewOrderType := "Create Production Order Type".FromInteger(NewOrderTypeOption); if IsHandled then exit; if ShowCreateOrderForm then begin + CreateOrderFromSales.SetSingleLineSelected(TempSalesPlanningLine.Count() = 1); if CreateOrderFromSales.RunModal() <> ACTION::Yes then exit; CreateOrderFromSales.GetParameters(NewStatus, NewOrderType); - OnCreateProdOrderOnAfterGetParameters(Rec, NewStatus, NewOrderType); + UseWizard := CreateOrderFromSales.GetUseProductDefinitionWizard(); + OnCreateProdOrderOnAfterGetParameters(TempSalesPlanningLine, NewStatus, NewOrderType); Clear(CreateOrderFromSales); end; - if not CreateOrders() then + if not CreateOrders(TempSalesPlanningLine) then Message(Text001); Rec.SetRange("Planning Status"); @@ -76,7 +86,7 @@ pageextension 99000883 "Mfg. Sales Order Planning" extends "Sales Order Planning CurrPage.Update(false); end; - local procedure CreateOrders() OrdersCreated: Boolean + local procedure CreateOrders(var TempSalesPlanningLine: Record "Sales Planning Line" temporary) OrdersCreated: Boolean var xSalesPlanLine: Record "Sales Planning Line"; SalesLine: Record "Sales Line"; @@ -86,24 +96,24 @@ pageextension 99000883 "Mfg. Sales Order Planning" extends "Sales Order Planning IsHandled: Boolean; ProcessOrder: Boolean; begin - xSalesPlanLine := Rec; + xSalesPlanLine := TempSalesPlanningLine; OrdersCreated := false; - OnCreateOrdersOnBeforeFindSet(Rec, IsHandled, OrdersCreated); + OnCreateOrdersOnBeforeFindSet(TempSalesPlanningLine, IsHandled, OrdersCreated); if IsHandled then exit; - if not Rec.FindSet() then + if not TempSalesPlanningLine.FindSet() then exit; repeat - SalesLine.Get(SalesLine."Document Type"::Order, Rec."Sales Order No.", Rec."Sales Order Line No."); + SalesLine.Get(SalesLine."Document Type"::Order, TempSalesPlanningLine."Sales Order No.", TempSalesPlanningLine."Sales Order Line No."); SalesLine.TestField("Shipment Date"); SalesLine.CalcFields("Reserved Qty. (Base)"); IsHandled := false; ProcessOrder := true; - OnCreateOrdersOnBeforeCreateProdOrder(Rec, SalesLine, IsHandled, ProcessOrder, OrdersCreated, EndLoop); + OnCreateOrdersOnBeforeCreateProdOrder(TempSalesPlanningLine, SalesLine, IsHandled, ProcessOrder, OrdersCreated, EndLoop); if IsHandled then exit; @@ -111,27 +121,33 @@ pageextension 99000883 "Mfg. Sales Order Planning" extends "Sales Order Planning if SalesLine."Outstanding Qty. (Base)" > SalesLine."Reserved Qty. (Base)" then begin DoCreateProdOrder := CreateProdOrderLines.CheckReplenishmentSystemProdOrderAndNotProductionBlocked(SalesLine."No.", SalesLine."Variant Code", SalesLine."Location Code"); - CreateOrder(DoCreateProdOrder, SalesLine, EndLoop, OrdersCreated); + CreateOrder(TempSalesPlanningLine, DoCreateProdOrder, SalesLine, EndLoop, OrdersCreated); end; - until (Rec.Next() = 0) or EndLoop; + until (TempSalesPlanningLine.Next() = 0) or EndLoop; - Rec := xSalesPlanLine; + TempSalesPlanningLine := xSalesPlanLine; end; - local procedure CreateOrder(DoCreateProdOrder: Boolean; var SalesLine: Record "Sales Line"; var EndLoop: Boolean; var OrdersCreated: Boolean) + local procedure CreateOrder(var TempSalesPlanningLine: Record "Sales Planning Line" temporary; DoCreateProdOrder: Boolean; var SalesLine: Record "Sales Line"; var EndLoop: Boolean; var OrdersCreated: Boolean) var + ProductionDefinitionManager: Codeunit "Production Definition Manager"; CreateProdOrderFromSale: Codeunit "Create Prod. Order from Sale"; HideValidationDialog: Boolean; begin HideValidationDialog := false; - OnBeforeCreateOrder(Rec, SalesLine, DoCreateProdOrder, HideValidationDialog); + OnBeforeCreateOrder(TempSalesPlanningLine, SalesLine, DoCreateProdOrder, HideValidationDialog); if DoCreateProdOrder then begin - OrdersCreated := true; - CreateProdOrderFromSale.SetHideValidationDialog(HideValidationDialog); - CreateProdOrderFromSale.CreateProductionOrder(SalesLine, NewStatus, NewOrderType); - if NewOrderType = NewOrderType::ProjectOrder then - EndLoop := true; + if UseWizard then begin + if ProductionDefinitionManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder, NewStatus) then + OrdersCreated := true; + end else begin + OrdersCreated := true; + CreateProdOrderFromSale.SetHideValidationDialog(HideValidationDialog); + CreateProdOrderFromSale.CreateProductionOrder(SalesLine, NewStatus, NewOrderType); + if NewOrderType = NewOrderType::ProjectOrder then + EndLoop := true; + end; end; end; diff --git a/src/Layers/W1/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al b/src/Layers/W1/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al index fc014ab57d..9c632e4a84 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Setup/ManufacturingSetup.Page.al @@ -213,6 +213,75 @@ page 99000768 "Manufacturing Setup" ApplicationArea = Manufacturing; } } + group(ProductionDefinitionWizard) + { + Caption = 'Production Definition Wizard'; + + group(BOMRoutingDisplayGroup) + { + Caption = 'BOM/Routing Step Display'; + + field("Show Rtng BOM Select Both"; Rec."Show Rtng BOM Select Both") + { + ApplicationArea = Manufacturing; + } + field("Show Rtng BOM Select Partial"; Rec."Show Rtng BOM Select Partial") + { + ApplicationArea = Manufacturing; + } + field("Show Rtng BOM Select Nothing"; Rec."Show Rtng BOM Select Nothing") + { + ApplicationArea = Manufacturing; + } + } + group(ProdCompDisplayGroup) + { + Caption = 'Components/Routing Preview Step Display'; + + field("Show Prod Comp Select Both"; Rec."Show Prod Comp Select Both") + { + ApplicationArea = Manufacturing; + } + field("Show Prod Comp Select Partial"; Rec."Show Prod Comp Select Partial") + { + ApplicationArea = Manufacturing; + } + field("Show Prod Comp Select Nothing"; Rec."Show Prod Comp Select Nothing") + { + ApplicationArea = Manufacturing; + } + } + group(GeneralWizardOptions) + { + Caption = 'General Options'; + + field("Always Save Modified Versions"; Rec."Always Save Modified Versions") + { + ApplicationArea = Manufacturing; + } + field("Allow Edit UI Selection"; Rec."Allow Edit UI Selection") + { + ApplicationArea = Manufacturing; + } + } + group(WizardDefaults) + { + Caption = 'Defaults'; + + field("Def. Wiz. Work Center No."; Rec."Def. Wiz. Work Center No.") + { + ApplicationArea = Manufacturing; + } + field("Def. Wiz. Comp Item No."; Rec."Def. Wiz. Comp Item No.") + { + ApplicationArea = Manufacturing; + } + field("Def. Wiz. Flushing Method"; Rec."Def. Wiz. Flushing Method") + { + ApplicationArea = Manufacturing; + } + } + } } area(factboxes) { @@ -246,5 +315,4 @@ page 99000768 "Manufacturing Setup" ManufacturingSetupNotif.ShowPlanningFieldsMoveNotification(); end; -} - +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Setup/ManufacturingSetup.Table.al b/src/Layers/W1/BaseApp/Manufacturing/Setup/ManufacturingSetup.Table.al index b14d26fa75..f569a26629 100644 --- a/src/Layers/W1/BaseApp/Manufacturing/Setup/ManufacturingSetup.Table.al +++ b/src/Layers/W1/BaseApp/Manufacturing/Setup/ManufacturingSetup.Table.al @@ -10,12 +10,15 @@ using Microsoft.Foundation.Calendar; #endif using Microsoft.Foundation.NoSeries; using Microsoft.Inventory.BOM.Tree; +using Microsoft.Inventory.Item; using Microsoft.Inventory.Location; using Microsoft.Inventory.Planning; using Microsoft.Manufacturing.Capacity; using Microsoft.Manufacturing.Forecast; using Microsoft.Manufacturing.MachineCenter; using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Manufacturing.WorkCenter; using System.Telemetry; using System.Utilities; @@ -333,6 +336,80 @@ table 99000765 "Manufacturing Setup" InitValue = "Pick + Manual"; ToolTip = 'Specifies default flushing method assigned to new items. A different flushing method on item cards will override this default.'; } + field(301; "Show Rtng BOM Select Both"; Enum "Prod. Definition Display") + { + Caption = 'Show BOM/Routing (Both Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the BOM/Routing selection step is shown when both a BOM and a Routing exist for the item. This setting only applies in Create Production Order mode. In Define Item Structure mode, the BOM and Routing steps are always shown in Edit mode.'; + } + field(302; "Show Rtng BOM Select Partial"; Enum "Prod. Definition Display") + { + Caption = 'Show BOM/Routing (Partially Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the BOM/Routing selection step is shown when only one of BOM or Routing exists. This setting only applies in Create Production Order mode. In Define Item Structure mode, the BOM and Routing steps are always shown in Edit mode.'; + } + field(303; "Show Rtng BOM Select Nothing"; Enum "Prod. Definition Display") + { + Caption = 'Show BOM/Routing (None Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the BOM/Routing selection step is shown when neither a BOM nor a Routing exists. This setting only applies in Create Production Order mode. In Define Item Structure mode, the BOM and Routing steps are always shown in Edit mode.'; + } + field(304; "Show Prod Comp Select Both"; Enum "Prod. Definition Display") + { + Caption = 'Show Prod. Components (Both Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the components/routing preview steps are shown when both a BOM and a Routing exist. This setting only applies in Create Production Order mode. In Define Item Structure mode, the components and production routing steps are never shown.'; + } + field(305; "Show Prod Comp Select Partial"; Enum "Prod. Definition Display") + { + Caption = 'Show Prod. Components (Partially Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the components/routing preview steps are shown when only one of BOM or Routing exists. This setting only applies in Create Production Order mode. In Define Item Structure mode, the components and production routing steps are never shown.'; + } + field(306; "Show Prod Comp Select Nothing"; Enum "Prod. Definition Display") + { + Caption = 'Show Prod. Components (None Available)'; + DataClassification = CustomerContent; + InitValue = Edit; + ToolTip = 'Specifies whether the components/routing preview steps are shown when neither BOM nor Routing exists. This setting only applies in Create Production Order mode. In Define Item Structure mode, the components and production routing steps are never shown.'; + } + field(307; "Always Save Modified Versions"; Boolean) + { + Caption = 'Always Save Modified Versions'; + DataClassification = CustomerContent; + ToolTip = 'Specifies that newly created BOM/Routing versions are always kept even when the Save option is set to Empty when using the production order creation wizard.'; + } + field(308; "Allow Edit UI Selection"; Boolean) + { + Caption = 'Allow Edit UI Selection'; + DataClassification = CustomerContent; + ToolTip = 'Specifies that users can override the step visibility settings (Show/Edit/Hide) on the wizard introduction step.'; + } + field(309; "Def. Wiz. Work Center No."; Code[20]) + { + Caption = 'Default Prod. Wiz. Work Center No.'; + DataClassification = CustomerContent; + TableRelation = "Work Center"; + ToolTip = 'Specifies the fallback work center to use when creating new routing lines without an explicit work center.'; + } + field(310; "Def. Wiz. Comp Item No."; Code[20]) + { + Caption = 'Default Prod. Wiz. Component Item No.'; + DataClassification = CustomerContent; + TableRelation = Item; + ToolTip = 'Specifies the default item to use when initializing a new BOM from scratch through the production order creation wizard.'; + } + field(311; "Def. Wiz. Flushing Method"; Enum "Flushing Method") + { + Caption = 'Default Prod. Wiz. Flushing Method'; + DataClassification = CustomerContent; + ToolTip = 'Specifies the default flushing method applied to Production Order Components when creating a new production BOM from scratch through the production order creation wizard. This is only used for temporary BOM creation; existing BOMs retain their original flushing method.'; + } field(5500; "Preset Output Quantity"; Option) { Caption = 'Preset Output Quantity'; @@ -404,4 +481,4 @@ table 99000765 "Manufacturing Setup" begin exit(ManufacturingSetupFeatureTelemetryNameLbl); end; -} +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefProdOrdLineBind.Codeunit.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefProdOrdLineBind.Codeunit.al new file mode 100644 index 0000000000..48ce9e774f --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefProdOrdLineBind.Codeunit.al @@ -0,0 +1,97 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Manufacturing.Document; +codeunit 99001023 "Prod. Def. ProdOrdLine Bind" +{ + EventSubscriberInstance = Manual; + + var + StoredProdOrder: Record "Production Order"; + StoredProdOrderLine: Record "Prod. Order Line"; + + /// + /// Stores the production order to return for temporary production order lines. + /// Must be called before BindSubscription. + /// + internal procedure SetProdOrder(ProdOrder: Record "Production Order") + begin + StoredProdOrder := ProdOrder; + end; + + /// + /// Stores the production order line to return for temporary routing lines. + /// Must be called before BindSubscription. + /// + internal procedure SetProdOrderLine(ProdOrderLine: Record "Prod. Order Line") + begin + StoredProdOrderLine := ProdOrderLine; + end; + + [EventSubscriber(ObjectType::Table, Database::"Prod. Order Line", 'OnBeforeGetProdOrder', '', false, false)] + local procedure OnBeforeGetProdOrderForProdOrderLine(var ProdOrderLine: Record "Prod. Order Line"; var ProdOrder: Record "Production Order") + begin + if StoredProdOrder."No." = '' then + exit; + if not ProdOrderLine.IsTemporary() then + exit; + if ProdOrderLine.Status <> StoredProdOrder.Status then + exit; + if ProdOrderLine."Prod. Order No." <> StoredProdOrder."No." then + exit; + ProdOrder := StoredProdOrder; + end; + + [EventSubscriber(ObjectType::Table, Database::"Prod. Order Component", 'OnBeforeGetDefaultConsumptionBin', '', false, false)] + local procedure OnBeforeGetDefaultConsumptionBinForProdOrderComp(var ProdOrderComponent: Record "Prod. Order Component"; var ProdOrderLine: Record "Prod. Order Line"; var IsHandled: Boolean) + begin + if not ProdOrderComponent.IsTemporary() then + exit; + if StoredProdOrderLine."Prod. Order No." = '' then + exit; + if ProdOrderComponent.Status <> StoredProdOrderLine.Status then + exit; + if ProdOrderComponent."Prod. Order No." <> StoredProdOrderLine."Prod. Order No." then + exit; + if ProdOrderComponent."Prod. Order Line No." <> StoredProdOrderLine."Line No." then + exit; + IsHandled := false; + if ProdOrderLine.Get(ProdOrderComponent.Status, ProdOrderComponent."Prod. Order No.", ProdOrderComponent."Prod. Order Line No.") then + exit; + ProdOrderLine := StoredProdOrderLine; + end; + + [EventSubscriber(ObjectType::Table, Database::"Prod. Order Component", 'OnBeforeGetProdOrderNeeds', '', false, false)] + local procedure OnBeforeGetProdOrderNeedsForProdOrderComp(var ProdOrderComponent: Record "Prod. Order Component"; var ProdOrderLine: Record "Prod. Order Line"; var IsHandled: Boolean) + begin + if StoredProdOrderLine."Prod. Order No." = '' then + exit; + if not ProdOrderComponent.IsTemporary() then + exit; + if ProdOrderComponent.Status <> StoredProdOrderLine.Status then + exit; + if ProdOrderComponent."Prod. Order No." <> StoredProdOrderLine."Prod. Order No." then + exit; + if ProdOrderComponent."Prod. Order Line No." <> StoredProdOrderLine."Line No." then + exit; + ProdOrderLine := StoredProdOrderLine; + end; + + [EventSubscriber(ObjectType::Table, Database::"Prod. Order Routing Line", 'OnBeforeGetProdOrderLine', '', false, false)] + local procedure OnBeforeGetProdOrderLineForRoutingLine(ProdOrderRoutingLine: Record "Prod. Order Routing Line"; var ProdOrderLine: Record "Prod. Order Line"; var ProdOrderLineRead: Boolean) + begin + if StoredProdOrderLine."Prod. Order No." = '' then + exit; + if ProdOrderRoutingLine."Prod. Order No." <> StoredProdOrderLine."Prod. Order No." then + exit; + if ProdOrderRoutingLine.Status <> StoredProdOrderLine.Status then + exit; + if ProdOrderRoutingLine."Routing Reference No." <> StoredProdOrderLine."Line No." then + exit; + ProdOrderLine := StoredProdOrderLine; + ProdOrderLineRead := true; + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefSourceInitializer.Codeunit.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefSourceInitializer.Codeunit.al new file mode 100644 index 0000000000..148e682e23 --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefSourceInitializer.Codeunit.al @@ -0,0 +1,87 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Location; +using Microsoft.Sales.Document; +using System.Reflection; + +codeunit 99001022 "Prod. Def. Source Initializer" +{ + var + UnknownSourceTypeErr: Label 'No initialization handler found for source type: %1.', Comment = '%1 = source type description'; + + /// + /// Dispatches initialization of the temporary production definition data based on the type of the source record. + /// Built-in handling is provided for Item, Stockkeeping Unit, and Sales Line. For other source types (such as Purchase Line in the + /// Subcontracting app), subscribe to OnInitializeFromSource and set IsHandled to true. + /// + /// The temporary data codeunit to initialize. + /// The source record variant. Supported built-in types: Item, Stockkeeping Unit, Sales Line. + internal procedure InitializeFromSource(var TempData: Codeunit "Prod. Definition Temp Data"; Source: Variant) + var + Item: Record Item; + SKU: Record "Stockkeeping Unit"; + SalesLine: Record "Sales Line"; + DataTypeManagement: Codeunit "Data Type Management"; + SourceRecRef: RecordRef; + IsHandled: Boolean; + begin + IsHandled := false; + OnBeforeInitializeFromSource(TempData, Source, IsHandled); + if IsHandled then + exit; + + if not DataTypeManagement.GetRecordRef(Source, SourceRecRef) then begin + OnInitializeFromSource(TempData, Source, IsHandled); + if not IsHandled then + Error(UnknownSourceTypeErr, Format(Source)); + exit; + end; + + case SourceRecRef.Number of + Database::Item: + begin + SourceRecRef.SetTable(Item); + TempData.InitializeFromItem(Item); + end; + Database::"Sales Line": + begin + SourceRecRef.SetTable(SalesLine); + TempData.InitializeFromSalesLine(SalesLine); + end; + Database::"Stockkeeping Unit": + begin + SourceRecRef.SetTable(SKU); + TempData.InitializeFromSKU(SKU); + end; + else begin + OnInitializeFromSource(TempData, Source, IsHandled); + if not IsHandled then + Error(UnknownSourceTypeErr, Format(Source)); + end; + end; + end; + + /// + /// Raised before built-in dispatch. Set IsHandled to true to skip default Item/SalesLine handling + /// and to prevent OnInitializeFromSource from firing as the fallback. + /// + [IntegrationEvent(false, false)] + local procedure OnBeforeInitializeFromSource(var TempData: Codeunit "Prod. Definition Temp Data"; Source: Variant; var IsHandled: Boolean) + begin + end; + + /// + /// Raised when the source type is not handled natively (i.e. not Item, Stockkeeping Unit, or Sales Line). + /// Subscribe here to support additional source types such as Purchase Line. + /// Set IsHandled to true once the initialization is complete. + /// + [IntegrationEvent(false, false)] + local procedure OnInitializeFromSource(var TempData: Codeunit "Prod. Definition Temp Data"; Source: Variant; var IsHandled: Boolean) + begin + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionDisplay.Enum.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionDisplay.Enum.al new file mode 100644 index 0000000000..a67f0a6690 --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionDisplay.Enum.al @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +enum 99001010 "Prod. Definition Display" +{ + Extensible = true; + AssignmentCompatibility = true; + + value(0; Hide) + { + Caption = 'Hide'; + } + value(1; Show) + { + Caption = 'Show'; + } + value(2; Edit) + { + Caption = 'Edit'; + } +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionMode.Enum.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionMode.Enum.al new file mode 100644 index 0000000000..ddbf7bd77c --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionMode.Enum.al @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +enum 99001011 "Prod. Definition Mode" +{ + Extensible = true; + + value(0; DefineItemStructure) + { + Caption = 'Define Item Structure'; + } + value(1; CreateProductionOrder) + { + Caption = 'Create Production Order'; + } +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionSaveTarget.Enum.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionSaveTarget.Enum.al new file mode 100644 index 0000000000..bfb1879f3c --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionSaveTarget.Enum.al @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +enum 99001031 "Prod. Definition Save Target" +{ + Extensible = true; + AssignmentCompatibility = true; + + value(0; Empty) + { + Caption = ' ', Locked = true; + } + value(1; Item) + { + Caption = 'Item'; + } + value(2; StockkeepingUnit) + { + Caption = 'Stockkeeping Unit'; + } +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionScenario.Enum.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionScenario.Enum.al new file mode 100644 index 0000000000..64892d192f --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionScenario.Enum.al @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +enum 99001028 "Prod. Definition Scenario" +{ + Extensible = true; + value(0; NothingAvailable) + { + Caption = 'Nothing Available'; + } + value(1; PartiallyAvailable) + { + Caption = 'Partially Available'; + } + value(2; BothAvailable) + { + Caption = 'Both Available'; + } +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionSource.Enum.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionSource.Enum.al new file mode 100644 index 0000000000..8c9998cb22 --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionSource.Enum.al @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +enum 99001012 "Prod. Definition Source" +{ + Extensible = true; + AssignmentCompatibility = true; + + value(0; Empty) + { + Caption = ' ', Locked = true; + } + value(1; Item) + { + Caption = 'Item'; + } + value(2; StockkeepingUnit) + { + Caption = 'Stockkeeping Unit'; + } + value(3; SalesLine) + { + Caption = 'Sales Line'; + } + value(4; PurchaseLine) + { + Caption = 'Purchase Line'; + } +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionTempData.Codeunit.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionTempData.Codeunit.al new file mode 100644 index 0000000000..dcf6e1d65e --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionTempData.Codeunit.al @@ -0,0 +1,921 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Location; +using Microsoft.Manufacturing.Capacity; +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; +using Microsoft.Manufacturing.Setup; +using Microsoft.Purchases.Document; +using Microsoft.Sales.Document; + +codeunit 99001016 "Prod. Definition Temp Data" +{ + var + TempProdOrderComponent: Record "Prod. Order Component" temporary; + TempProdOrderLine: Record "Prod. Order Line" temporary; + TempProdOrderRoutingLine: Record "Prod. Order Routing Line" temporary; + TempBOMHeader: Record "Production BOM Header" temporary; + TempBOMLine: Record "Production BOM Line" temporary; + TempProdOrder: Record "Production Order" temporary; + TempPurchLine: Record "Purchase Line" temporary; + TempRoutingHeader: Record "Routing Header" temporary; + TempRoutingLine: Record "Routing Line" temporary; + TempSalesLine: Record "Sales Line" temporary; + TempSKU: Record "Stockkeeping Unit" temporary; + ManufacturingSetup: Record "Manufacturing Setup"; + ProdDefinitionVersionMgmt: Codeunit "Prod. Definition Version Mgmt."; + ProdOrdLineBind: Codeunit "Prod. Def. ProdOrdLine Bind"; + GlobalItemNo: Code[20]; + GlobalItemDescription: Text[100]; + GlobalBaseUOM: Code[10]; + ManufacturingSetupRead: Boolean; + GlobalSourceType: Enum "Prod. Definition Source"; + RoutingBOMSourceType: Enum "Prod. Definition Source"; + ProdOrderStatus: Enum "Production Order Status"; + + /// + /// Initializes the temporary data from an item record, setting item details and clearing production tables. + /// + /// The item to initialize the temporary production data from. + internal procedure InitializeFromItem(Item: Record Item) + begin + GlobalItemNo := Item."No."; + GlobalItemDescription := Item.Description; + GlobalBaseUOM := Item."Base Unit of Measure"; + ClearSourceContext(); + GlobalSourceType := "Prod. Definition Source"::Item; + ClearTemporaryProductionTables(); + end; + + /// + /// Initializes the temporary data from a sales line, creating a temporary production order and clearing production tables. + /// + /// The sales line to base the temporary production data on. + internal procedure InitializeFromSalesLine(SalesLine: Record "Sales Line") + var + Item: Record Item; + Location: Record Location; + begin + ClearSourceContext(); + GlobalSourceType := "Prod. Definition Source"::SalesLine; + TempSalesLine := SalesLine; + TempSalesLine.Insert(); + + GlobalItemNo := SalesLine."No."; + GlobalItemDescription := SalesLine.Description; + Item.SetLoadFields("Base Unit of Measure"); + if Item.Get(SalesLine."No.") then + GlobalBaseUOM := Item."Base Unit of Measure"; + + CreateTemporaryProductionOrderFromSalesLine(SalesLine); + CreateTemporaryProdOrderLine(); + + TempProdOrderLine.Description := SalesLine.Description; + TempProdOrderLine."Description 2" := SalesLine."Description 2"; + if Location.Get(TempProdOrderLine."Location Code") and not Location."Require Pick" and (SalesLine."Bin Code" <> '') then + TempProdOrderLine."Bin Code" := SalesLine."Bin Code"; + TempProdOrderLine."Shortcut Dimension 1 Code" := SalesLine."Shortcut Dimension 1 Code"; + TempProdOrderLine."Shortcut Dimension 2 Code" := SalesLine."Shortcut Dimension 2 Code"; + TempProdOrderLine."Dimension Set ID" := SalesLine."Dimension Set ID"; + TempProdOrderLine.Modify(); + + ClearTemporaryProductionTables(); + end; + + /// + /// Initializes the temporary data from a stockkeeping unit. The parent item's description and base unit of measure + /// are resolved and stored alongside the SKU's location and variant codes for downstream BOM/routing resolution. + /// + /// The stockkeeping unit to initialize the temporary production data from. + internal procedure InitializeFromSKU(SKU: Record "Stockkeeping Unit") + var + Item: Record Item; + begin + ClearSourceContext(); + GlobalSourceType := "Prod. Definition Source"::StockkeepingUnit; + TempSKU := SKU; + TempSKU.Insert(); + + Item.SetLoadFields(Description, "Base Unit of Measure"); + Item.Get(SKU."Item No."); + GlobalItemNo := SKU."Item No."; + GlobalItemDescription := Item.Description; + GlobalBaseUOM := Item."Base Unit of Measure"; + ClearTemporaryProductionTables(); + end; + + local procedure CreateTemporaryProductionOrderFromSalesLine(SalesLine: Record "Sales Line") + var + TempProdOrderNoLbl: Label 'TEMP-%1', Locked = true, MaxLength = 20; + ProductionOrderQtyZeroOrNegativeErr: Label 'Cannot create a production order from Sales Line %1 line %2: the calculated quantity (%3) is zero or negative because the line is fully or over-reserved.', Comment = '%1 = Document No., %2 = Line No., %3 = Quantity'; + begin + TempProdOrder.Reset(); + TempProdOrder.DeleteAll(); + TempProdOrder.Init(); + TempProdOrder.Status := ProdOrderStatus; + TempProdOrder."No." := StrSubstNo(TempProdOrderNoLbl, CopyStr(Format(CreateGuid()), 2, 10)); + TempProdOrder."Starting Date" := WorkDate(); + TempProdOrder."Creation Date" := WorkDate(); + TempProdOrder."Due Date" := SalesLine."Shipment Date"; + TempProdOrder."Source Type" := "Prod. Order Source Type"::Item; + TempProdOrder."Location Code" := SalesLine."Location Code"; + TempProdOrder.Validate("Source No.", SalesLine."No."); + TempProdOrder.Validate("Variant Code", SalesLine."Variant Code"); + SalesLine.CalcFields("Reserved Qty. (Base)"); + TempProdOrder.Quantity := SalesLine."Outstanding Qty. (Base)" - SalesLine."Reserved Qty. (Base)"; + if TempProdOrder.Quantity <= 0 then + Error(ProductionOrderQtyZeroOrNegativeErr, SalesLine."Document No.", SalesLine."Line No.", TempProdOrder.Quantity); + TempProdOrder.InitRecord(); + TempProdOrder.Insert(); + end; + + /// + /// Creates and inserts a temporary production order line from the current internal temporary production order. + /// Call SetNewProdOrder first to ensure the production order context is set correctly. + /// + internal procedure CreateTemporaryProdOrderLine() + var + Item: Record Item; + begin + TempProdOrderLine.Reset(); + TempProdOrderLine.DeleteAll(); + TempProdOrderLine.Init(); + TempProdOrderLine.Status := TempProdOrder.Status; + TempProdOrderLine."Prod. Order No." := TempProdOrder."No."; + TempProdOrderLine."Line No." := 10000; + TempProdOrderLine."Routing Reference No." := TempProdOrderLine."Line No."; + TempProdOrderLine.Validate("Item No.", TempProdOrder."Source No."); + TempProdOrderLine."Location Code" := TempProdOrder."Location Code"; + TempProdOrderLine.Validate("Variant Code", TempProdOrder."Variant Code"); + TempProdOrderLine.Validate(Quantity, TempProdOrder.Quantity); + TempProdOrderLine."Due Date" := TempProdOrder."Due Date"; + + Item.SetLoadFields("Scrap %", "Inventory Posting Group"); + if Item.Get(TempProdOrder."Source No.") then begin + TempProdOrderLine."Scrap %" := Item."Scrap %"; + TempProdOrderLine."Inventory Posting Group" := Item."Inventory Posting Group"; + end; + + TempProdOrderLine.Insert(); + BindProdOrderSubscriber(); + end; + + /// + /// Clears the temporary production order component and routing line tables. + /// Call this after SetNewProdOrder and CreateTemporaryProdOrderLine to reset the component/routing state. + /// + internal procedure ClearTemporaryProductionTables() + begin + TempProdOrderComponent.Reset(); + TempProdOrderComponent.DeleteAll(); + TempProdOrderRoutingLine.Reset(); + TempProdOrderRoutingLine.DeleteAll(); + end; + + local procedure ClearSourceContext() + begin + GlobalSourceType := "Prod. Definition Source"::Empty; + TempSalesLine.Reset(); + TempSalesLine.DeleteAll(); + TempPurchLine.Reset(); + TempPurchLine.DeleteAll(); + TempSKU.Reset(); + TempSKU.DeleteAll(); + end; + + /// + /// Creates a new blank temporary BOM header and an optional default component line for editing in the wizard. + /// + /// The item number the new BOM is being created for. + /// The item description used to name the BOM. + /// The base unit of measure code for the BOM header. + internal procedure InitializeNewTemporaryBOMInformation(ItemNo: Code[20]; ItemDescription: Text[100]; BaseUOMCode: Code[10]) + var + BOMNo: Code[20]; + BOMForLbl: Label 'BOM for %1'; + TempBOMNoLbl: Label 'TEMP-BOM-%1', Locked = true, MaxLength = 20; + begin + ClearBOMTables(); + + GetManufacturingSetup(); + ManufacturingSetup.TestField("Production BOM Nos."); + + TempBOMHeader.Init(); + BOMNo := StrSubstNo(TempBOMNoLbl, CopyStr(Format(CreateGuid()), 2, 10)); + TempBOMHeader."No." := BOMNo; + TempBOMHeader.Description := CopyStr(StrSubstNo(BOMForLbl, ItemDescription), 1, MaxStrLen(TempBOMHeader.Description)); + TempBOMHeader."Unit of Measure Code" := BaseUOMCode; + TempBOMHeader.Insert(); + + InitializeDefaultTemporaryBOMLine(BOMNo, ManufacturingSetup."Def. Wiz. Comp Item No."); + UpdateProdOrderBOMInfo(BOMNo, ''); + end; + + /// + /// Creates a new blank temporary routing header for editing in the wizard, and adds a default operation 10 if a common work center is configured. + /// + /// The item number the new routing is being created for. + /// The item description used to name the routing. + internal procedure InitializeNewTemporaryRoutingInformation(ItemNo: Code[20]; ItemDescription: Text[100]) + var + RoutingNo: Code[20]; + RoutingForLbl: Label 'Routing for %1'; + TempRoutingNoLbl: Label 'TEMP-RTNG-%1', Locked = true, MaxLength = 20; + begin + ClearRoutingTables(); + + GetManufacturingSetup(); + ManufacturingSetup.TestField("Routing Nos."); + + TempRoutingHeader.Init(); + RoutingNo := StrSubstNo(TempRoutingNoLbl, CopyStr(Format(CreateGuid()), 2, 10)); + TempRoutingHeader."No." := RoutingNo; + TempRoutingHeader.Description := CopyStr(StrSubstNo(RoutingForLbl, ItemDescription), 1, MaxStrLen(TempRoutingHeader.Description)); + TempRoutingHeader.Insert(); + + AddDefaultRoutingOperation(RoutingNo, ManufacturingSetup."Def. Wiz. Work Center No."); + UpdateProdOrderRoutingInfo(RoutingNo, ''); + OnAfterInitializeNewTemporaryRoutingInformation(TempRoutingHeader, TempRoutingLine, ItemNo); + end; + + local procedure InitializeDefaultTemporaryBOMLine(BOMNo: Code[20]; DefaultComponentItemNo: Code[20]) + begin + if DefaultComponentItemNo = '' then + exit; + + TempBOMLine.Init(); + TempBOMLine."Production BOM No." := BOMNo; + TempBOMLine."Line No." := 10000; + TempBOMLine.Type := "Production BOM Line Type"::Item; + TempBOMLine.Validate("No.", DefaultComponentItemNo); + TempBOMLine.Validate("Quantity per", 1); + OnBeforeInsertDefaultTemporaryBOMLine(TempBOMLine, DefaultComponentItemNo); + TempBOMLine.Insert(); + end; + + local procedure AddDefaultRoutingOperation(RoutingNo: Code[20]; DefaultWorkCenterNo: Code[20]) + begin + if DefaultWorkCenterNo = '' then + exit; + + TempRoutingLine.Init(); + TempRoutingLine."Routing No." := RoutingNo; + TempRoutingLine."Operation No." := '10'; + TempRoutingLine.Type := "Capacity Type Routing"::"Work Center"; + TempRoutingLine.Validate("No.", DefaultWorkCenterNo); + TempRoutingLine.Validate("Work Center No.", DefaultWorkCenterNo); + OnBeforeInsertDefaultRoutingOperation(TempRoutingLine); + TempRoutingLine.Insert(); + end; + + /// + /// Builds the temporary production order component and routing line structures from the current temporary BOM and routing data. + /// + internal procedure BuildTemporaryStructureFromBOMRouting() + begin + BuildTemporaryRoutingLines(); + BuildTemporaryComponents(); + end; + + local procedure BuildTemporaryComponents() + var + TempProductionBOMLine: Record "Production BOM Line" temporary; + LineNo: Integer; + begin + TempProdOrderComponent.Reset(); + TempProdOrderComponent.DeleteAll(); + + GetGlobalBOMLines(TempProductionBOMLine); + if TempProductionBOMLine.FindSet() then begin + LineNo := 0; + repeat + CreateTemporaryComponentFromBOMLine(TempProductionBOMLine, 1, LineNo); + until TempProductionBOMLine.Next() = 0; + end; + end; + + local procedure BuildTemporaryRoutingLines() + var + TempRoutingLineCopy: Record "Routing Line" temporary; + begin + TempProdOrderRoutingLine.Reset(); + TempProdOrderRoutingLine.DeleteAll(); + GetGlobalRoutingLines(TempRoutingLineCopy); + if TempRoutingLineCopy.FindSet() then + repeat + CreateTemporaryProdOrderRoutingLineFromRouting(TempRoutingLineCopy); + until TempRoutingLineCopy.Next() = 0; + end; + + local procedure CreateTemporaryComponentFromBOMLine(ProductionBOMLine: Record "Production BOM Line"; BOMQuantity: Decimal; var LineNo: Integer) + var + ProductionBOMLineNextLevel: Record "Production BOM Line"; + TempProdOrderRoutingLineCopy: Record "Prod. Order Routing Line" temporary; + Item: Record Item; + begin + GetManufacturingSetup(); + + case ProductionBOMLine.Type of + "Production BOM Line Type"::Item: + begin + LineNo += 10000; + TempProdOrderComponent.Init(); + TempProdOrderComponent.Status := TempProdOrderLine.Status; + TempProdOrderComponent."Prod. Order No." := TempProdOrderLine."Prod. Order No."; + TempProdOrderComponent."Prod. Order Line No." := TempProdOrderLine."Line No."; + TempProdOrderComponent."Line No." := LineNo; + TempProdOrderComponent."Qty. per Unit of Measure" := 1; + TempProdOrderComponent.Validate("Item No.", ProductionBOMLine."No."); + TempProdOrderComponent.Validate("Variant Code", ProductionBOMLine."Variant Code"); + TempProdOrderComponent.Description := ProductionBOMLine.Description; + TempProdOrderComponent."Description 2" := ProductionBOMLine."Description 2"; + TempProdOrderComponent.Validate("Quantity per", ProductionBOMLine."Quantity per" * BOMQuantity); + if ProductionBOMLine."Unit of Measure Code" <> '' then + TempProdOrderComponent.Validate("Unit of Measure Code", ProductionBOMLine."Unit of Measure Code"); + TempProdOrderComponent.Length := ProductionBOMLine.Length; + TempProdOrderComponent.Width := ProductionBOMLine.Width; + TempProdOrderComponent.Weight := ProductionBOMLine.Weight; + TempProdOrderComponent.Depth := ProductionBOMLine.Depth; + TempProdOrderComponent.Position := ProductionBOMLine.Position; + TempProdOrderComponent."Position 2" := ProductionBOMLine."Position 2"; + TempProdOrderComponent."Position 3" := ProductionBOMLine."Position 3"; + TempProdOrderComponent."Lead-Time Offset" := ProductionBOMLine."Lead-Time Offset"; + TempProdOrderComponent.Validate("Scrap %", ProductionBOMLine."Scrap %"); + TempProdOrderComponent.Validate("Calculation Formula", ProductionBOMLine."Calculation Formula"); + TempProdOrderComponent."Routing Link Code" := ProductionBOMLine."Routing Link Code"; + if TempProdOrderLine."Location Code" <> '' then + TempProdOrderComponent.Validate("Location Code", TempProdOrderLine."Location Code"); + + Item.SetLoadFields(Type); + if Item.Get(TempProdOrderComponent."Item No.") and Item.IsInventoriableType() then begin + TempProdOrderRoutingLineCopy.Copy(TempProdOrderRoutingLine, true); + TempProdOrderComponent."Bin Code" := TempProdOrderComponent.GetDefaultConsumptionBin(TempProdOrderRoutingLineCopy); + end; + + if not ProdDefinitionVersionMgmt.CheckBOMExists(ProductionBOMLine."Production BOM No.", '') then + TempProdOrderComponent."Flushing Method" := ManufacturingSetup."Def. Wiz. Flushing Method"; + TempProdOrderComponent.Insert(); + OnAfterCreateTemporaryComponentFromBOMLine(TempProdOrderComponent, ProductionBOMLine); + end; + "Production BOM Line Type"::"Production BOM": + begin + ProductionBOMLineNextLevel.SetRange("Production BOM No.", ProductionBOMLine."No."); + ProductionBOMLineNextLevel.SetRange("Version Code", ProdDefinitionVersionMgmt.GetDefaultBOMVersion(ProductionBOMLine."No.")); + if ProductionBOMLineNextLevel.FindSet() then + repeat + CreateTemporaryComponentFromBOMLine(ProductionBOMLineNextLevel, BOMQuantity * ProductionBOMLine."Quantity per", LineNo); + until ProductionBOMLineNextLevel.Next() = 0; + end; + end; + end; + + local procedure CreateTemporaryProdOrderRoutingLineFromRouting(RoutingLine: Record "Routing Line") + var + WorkCenterNo: Code[20]; + begin + TempProdOrderRoutingLine.Init(); + TempProdOrderRoutingLine.Status := TempProdOrderLine.Status; + TempProdOrderRoutingLine."Prod. Order No." := TempProdOrderLine."Prod. Order No."; + TempProdOrderRoutingLine."Routing No." := RoutingLine."Routing No."; + TempProdOrderRoutingLine.Validate("Routing Reference No.", TempProdOrderLine."Line No."); + TempProdOrderRoutingLine.Validate("Operation No.", RoutingLine."Operation No."); + TempProdOrderRoutingLine.Insert(); + + TempProdOrderRoutingLine.Validate(Type, RoutingLine.Type); + + WorkCenterNo := RoutingLine."Work Center No."; + if WorkCenterNo = '' then begin + GetManufacturingSetup(); + WorkCenterNo := ManufacturingSetup."Def. Wiz. Work Center No."; + end; + + if (RoutingLine.Type = RoutingLine.Type::"Work Center") and (RoutingLine."No." = '') and (WorkCenterNo <> '') then + TempProdOrderRoutingLine.Validate("No.", WorkCenterNo) + else + TempProdOrderRoutingLine.Validate("No.", RoutingLine."No."); + + TempProdOrderRoutingLine.Validate("Work Center No.", WorkCenterNo); + TempProdOrderRoutingLine.Description := RoutingLine.Description; + TempProdOrderRoutingLine."Description 2" := RoutingLine."Description 2"; + TempProdOrderRoutingLine.Validate("Setup Time", RoutingLine."Setup Time"); + TempProdOrderRoutingLine.Validate("Run Time", RoutingLine."Run Time"); + TempProdOrderRoutingLine.Validate("Wait Time", RoutingLine."Wait Time"); + TempProdOrderRoutingLine.Validate("Move Time", RoutingLine."Move Time"); + if TempProdOrderLine."Ending Date" <> 0D then + TempProdOrderRoutingLine.Validate("Ending Date", TempProdOrderLine."Ending Date"); + if TempProdOrderLine."Ending Time" <> 0T then + TempProdOrderRoutingLine.Validate("Ending Time", TempProdOrderLine."Ending Time"); + TempProdOrderRoutingLine."Routing Link Code" := RoutingLine."Routing Link Code"; + TempProdOrderRoutingLine."Previous Operation No." := RoutingLine."Previous Operation No."; + TempProdOrderRoutingLine."Next Operation No." := RoutingLine."Next Operation No."; + TempProdOrderRoutingLine."Setup Time Unit of Meas. Code" := RoutingLine."Setup Time Unit of Meas. Code"; + TempProdOrderRoutingLine."Run Time Unit of Meas. Code" := RoutingLine."Run Time Unit of Meas. Code"; + TempProdOrderRoutingLine."Wait Time Unit of Meas. Code" := RoutingLine."Wait Time Unit of Meas. Code"; + TempProdOrderRoutingLine."Move Time Unit of Meas. Code" := RoutingLine."Move Time Unit of Meas. Code"; + TempProdOrderRoutingLine."Fixed Scrap Quantity" := RoutingLine."Fixed Scrap Quantity"; + TempProdOrderRoutingLine."Scrap Factor %" := RoutingLine."Scrap Factor %"; + TempProdOrderRoutingLine."Send-Ahead Quantity" := RoutingLine."Send-Ahead Quantity"; + TempProdOrderRoutingLine."Concurrent Capacities" := RoutingLine."Concurrent Capacities"; + TempProdOrderRoutingLine."Lot Size" := RoutingLine."Lot Size"; + TempProdOrderRoutingLine."Unit Cost per" := RoutingLine."Unit Cost per"; + TempProdOrderRoutingLine.FillDefaultLocationAndBins(); + TempProdOrderRoutingLine.Modify(); + OnAfterCreateTemporaryProdOrderRoutingLineFromRouting(TempProdOrderRoutingLine, RoutingLine); + end; + + /// + /// Loads production BOM lines from the database into the internal temporary BOM line table. + /// + /// The production BOM number to load lines from. + /// The BOM version code to load. Use empty string for the base version. + internal procedure LoadBOMLines(BOMNo: Code[20]; VersionCode: Code[20]) + var + ProductionBOMLine: Record "Production BOM Line"; + begin + ClearBOMTables(); + UpdateProdOrderBOMInfo(BOMNo, VersionCode); + + if BOMNo = '' then + exit; + SetBOMLineFilters(ProductionBOMLine, BOMNo, VersionCode); + CopyBOMLinesToTemporary(ProductionBOMLine); + end; + + local procedure SetBOMLineFilters(var ProductionBOMLine: Record "Production BOM Line"; BOMNo: Code[20]; VersionCode: Code[20]) + begin + if VersionCode <> '' then begin + ProductionBOMLine.SetRange("Production BOM No.", BOMNo); + ProductionBOMLine.SetRange("Version Code", VersionCode); + end else begin + ProductionBOMLine.SetRange("Production BOM No.", BOMNo); + ProductionBOMLine.SetRange("Version Code", ''); + end; + end; + + local procedure CopyBOMLinesToTemporary(var ProductionBOMLine: Record "Production BOM Line") + begin + if ProductionBOMLine.FindSet() then + repeat + TempBOMLine := ProductionBOMLine; + TempBOMLine.Insert(); + until ProductionBOMLine.Next() = 0; + end; + + /// + /// Loads routing lines from the database into the internal temporary routing line table. + /// + /// The routing number to load lines from. + /// The routing version code to load. If empty, the default active version is used. + internal procedure LoadRoutingLines(RoutingNo: Code[20]; VersionCode: Code[20]) + var + RoutingLine: Record "Routing Line"; + begin + ClearRoutingTables(); + UpdateProdOrderRoutingInfo(RoutingNo, VersionCode); + + if RoutingNo = '' then + exit; + + if VersionCode = '' then + VersionCode := ProdDefinitionVersionMgmt.GetDefaultRoutingVersion(RoutingNo); + + SetRoutingLineFilters(RoutingLine, RoutingNo, VersionCode); + CopyRoutingLinesToTemporary(RoutingLine); + end; + + local procedure SetRoutingLineFilters(var RoutingLine: Record "Routing Line"; RoutingNo: Code[20]; VersionCode: Code[20]) + begin + if VersionCode <> '' then begin + RoutingLine.SetRange("Routing No.", RoutingNo); + RoutingLine.SetRange("Version Code", VersionCode); + end else begin + RoutingLine.SetRange("Routing No.", RoutingNo); + RoutingLine.SetRange("Version Code", ''); + end; + end; + + local procedure CopyRoutingLinesToTemporary(var RoutingLine: Record "Routing Line") + begin + if RoutingLine.FindSet() then + repeat + TempRoutingLine := RoutingLine; + TempRoutingLine.Insert(); + until RoutingLine.Next() = 0; + end; + + /// + /// Updates the version code on all temporary routing lines to the specified new version code. + /// + /// The new version code to assign to all temporary routing lines. + internal procedure UpdateRoutingVersionCode(NewVersionCode: Code[20]) + var + TempRoutingLineCopy: Record "Routing Line" temporary; + TempRoutingLineToInsert: Record "Routing Line" temporary; + begin + TempRoutingLineCopy.Copy(TempRoutingLine, true); + TempRoutingLineToInsert.Copy(TempRoutingLineCopy, true); + + TempRoutingLineCopy.SetFilter("Version Code", '<>%1', NewVersionCode); + if TempRoutingLineCopy.FindSet() then + repeat + TempRoutingLineToInsert := TempRoutingLineCopy; + TempRoutingLineToInsert."Version Code" := NewVersionCode; + TempRoutingLineToInsert.Insert(); + until TempRoutingLineCopy.Next() = 0; + + TempRoutingLineCopy.DeleteAll(); + end; + + /// + /// Updates the version code on all temporary BOM lines to the specified new version code. + /// + /// The new version code to assign to all temporary BOM lines. + internal procedure UpdateBOMVersionCode(NewVersionCode: Code[20]) + var + TempBOMLineCopy: Record "Production BOM Line" temporary; + TempBOMLineToInsert: Record "Production BOM Line" temporary; + begin + TempBOMLineCopy.Copy(TempBOMLine, true); + TempBOMLineToInsert.Copy(TempBOMLineCopy, true); + + TempBOMLineCopy.SetFilter("Version Code", '<>%1', NewVersionCode); + if TempBOMLineCopy.FindSet(true) then + repeat + TempBOMLineToInsert := TempBOMLineCopy; + TempBOMLineToInsert."Version Code" := NewVersionCode; + TempBOMLineToInsert.Insert(); + until TempBOMLineCopy.Next() = 0; + + TempBOMLineCopy.DeleteAll(); + end; + + local procedure ClearBOMTables() + begin + TempBOMHeader.Reset(); + TempBOMHeader.DeleteAll(); + TempBOMLine.Reset(); + TempBOMLine.DeleteAll(); + end; + + local procedure ClearRoutingTables() + begin + TempRoutingHeader.Reset(); + TempRoutingHeader.DeleteAll(); + TempRoutingLine.Reset(); + TempRoutingLine.DeleteAll(); + end; + + /// + /// Replaces the internal temporary BOM data with the provided BOM header and lines. + /// + /// The temporary BOM header to store. + /// The temporary BOM lines to store. + internal procedure SetNewBOMInformation(var TempBOMHeader2: Record "Production BOM Header" temporary; var TempBOMLines2: Record "Production BOM Line" temporary) + begin + ClearBOMTables(); + TempBOMHeader.Copy(TempBOMHeader2, true); + TempBOMLine.Copy(TempBOMLines2, true); + end; + + /// + /// Replaces the internal temporary routing data with the provided routing header and lines. + /// + /// The temporary routing header to store. + /// The temporary routing lines to store. + internal procedure SetNewRoutingInformation(var TempRoutingHeader2: Record "Routing Header" temporary; var TempRoutingLines2: Record "Routing Line" temporary) + begin + ClearRoutingTables(); + TempRoutingHeader.Copy(TempRoutingHeader2, true); + TempRoutingLine.Copy(TempRoutingLines2, true); + end; + + /// + /// Replaces the internal temporary production order components with the provided records. + /// + /// The temporary production order component records to store. + internal procedure SetNewProdOrderComponent(var TempProdOrderComponent2: Record "Prod. Order Component" temporary) + begin + TempProdOrderComponent.Reset(); + TempProdOrderComponent.DeleteAll(); + TempProdOrderComponent.Copy(TempProdOrderComponent2, true); + end; + + /// + /// Replaces the internal temporary production order routing lines with the provided records. + /// + /// The temporary production order routing line records to store. + internal procedure SetNewProdOrderRoutingLine(var TempProdOrderRoutingLine2: Record "Prod. Order Routing Line" temporary) + begin + TempProdOrderRoutingLine.Reset(); + TempProdOrderRoutingLine.DeleteAll(); + TempProdOrderRoutingLine.Copy(TempProdOrderRoutingLine2, true); + end; + + /// + /// Replaces the internal temporary production order with the provided record. + /// + /// The temporary production order record to store. + internal procedure SetNewProdOrder(var TempProdOrder2: Record "Production Order" temporary) + begin + TempProdOrder.Reset(); + TempProdOrder.DeleteAll(); + TempProdOrder.Copy(TempProdOrder2, true); + end; + + /// + /// Sets the global item context (item number and description) used throughout the wizard. + /// This is normally set automatically by InitializeFromItem or InitializeFromSalesLine. + /// Call this before SetNewProdOrder and CreateTemporaryProdOrderLine when initializing from a custom source type. + /// + /// The item number to set as the global item context. + /// The item description to set as the global item context. + internal procedure SetGlobalItemInfo(ItemNo: Code[20]; ItemDescription: Text[100]) + begin + GlobalItemNo := ItemNo; + GlobalItemDescription := ItemDescription; + end; + + /// + /// Stores a copy of the given purchase line as the internal purchase line context. + /// This is used by downstream logic (e.g. BOM/routing source resolution) that reads back the stored purchase line. + /// + /// The purchase line to store. + internal procedure SetNewPurchLine(PurchLine: Record "Purchase Line") + begin + TempPurchLine.Reset(); + TempPurchLine.DeleteAll(); + TempPurchLine := PurchLine; + TempPurchLine.Insert(); + end; + + /// + /// Sets the source type indicating whether the BOM/routing originates from an existing record or is newly defined. + /// + /// The source type to set. + internal procedure SetRtngBOMSourceType(SourceType: Enum "Prod. Definition Source") + begin + RoutingBOMSourceType := SourceType; + end; + + /// + /// Returns the internal temporary BOM lines by copying them into the provided variable. + /// + /// The variable to receive the temporary BOM lines. + internal procedure GetGlobalBOMLines(var TempBOMLines: Record "Production BOM Line" temporary) + begin + TempBOMLines.Copy(TempBOMLine, true); + end; + + /// + /// Returns the internal temporary routing lines by copying them into the provided variable. + /// + /// The variable to receive the temporary routing lines. + internal procedure GetGlobalRoutingLines(var TempRoutingLines: Record "Routing Line" temporary) + begin + TempRoutingLines.Copy(TempRoutingLine, true); + end; + + /// + /// Returns the internal temporary BOM header by copying it into the provided variable. + /// + /// The variable to receive the temporary BOM header. + internal procedure GetGlobalBOMHeader(var TempBOMHeaderOut: Record "Production BOM Header" temporary) + begin + TempBOMHeaderOut.Copy(TempBOMHeader, true); + end; + + /// + /// Returns the internal temporary routing header by copying it into the provided variable. + /// + /// The variable to receive the temporary routing header. + internal procedure GetGlobalRoutingHeader(var TempRoutingHeaderOut: Record "Routing Header" temporary) + begin + TempRoutingHeaderOut.Copy(TempRoutingHeader, true); + end; + + /// + /// Returns the internal temporary production order components by copying them into the provided variable. + /// + /// The variable to receive the temporary production order components. + internal procedure GetGlobalProdOrderComponent(var TempProdOrderComponentOut: Record "Prod. Order Component" temporary) + begin + TempProdOrderComponentOut.Copy(TempProdOrderComponent, true); + end; + + /// + /// Returns the internal temporary production order routing lines by copying them into the provided variable. + /// + /// The variable to receive the temporary production order routing lines. + internal procedure GetGlobalProdOrderRoutingLine(var TempProdOrderRoutingLineOut: Record "Prod. Order Routing Line" temporary) + begin + TempProdOrderRoutingLineOut.Copy(TempProdOrderRoutingLine, true); + end; + + /// + /// Returns the internal temporary production order by copying it into the provided variable. + /// + /// The variable to receive the temporary production order. + internal procedure GetGlobalProdOrder(var TempProductionOrder: Record "Production Order" temporary) + begin + TempProductionOrder.Copy(TempProdOrder, true); + end; + + /// + /// Returns the internal temporary production order line by copying it into the provided variable. + /// + /// The variable to receive the temporary production order line. + internal procedure GetGlobalProdOrderLine(var TempProductionOrderLine: Record "Prod. Order Line" temporary) + begin + TempProductionOrderLine.Copy(TempProdOrderLine, true); + end; + + /// + /// Sets the Routing No. on the temporary production order line. + /// If the Prod. Order Routing Line already has a routing assigned, this will overwrite the existing routing reference with the new one. + /// + /// The routing number to assign. + /// The routing version code to assign. + internal procedure UpdateProdOrderRoutingInfo(RoutingNo: Code[20]; VersionCode: Code[20]) + begin + if TempProdOrderLine."Prod. Order No." = '' then + exit; + if not TempProdOrderLine.FindFirst() then + exit; + TempProdOrderLine."Routing No." := RoutingNo; + TempProdOrderLine."Routing Version Code" := VersionCode; + TempProdOrderLine.Modify(); + + UpdateProdOrderRoutingLineRoutingNo(RoutingNo); + end; + + local procedure UpdateProdOrderRoutingLineRoutingNo(NewRoutingNo: Code[20]) + var + TempProdOrderRoutingLineCopy: Record "Prod. Order Routing Line" temporary; + TempProdOrderRoutingLineToInsert: Record "Prod. Order Routing Line" temporary; + begin + TempProdOrderRoutingLineCopy.Copy(TempProdOrderRoutingLine, true); + TempProdOrderRoutingLineToInsert.Copy(TempProdOrderRoutingLine, true); + + TempProdOrderRoutingLineCopy.SetFilter("Routing No.", '<>%1', NewRoutingNo); + if TempProdOrderRoutingLineCopy.FindSet(true) then + repeat + TempProdOrderRoutingLineToInsert := TempProdOrderRoutingLineCopy; + TempProdOrderRoutingLineToInsert."Routing No." := NewRoutingNo; + TempProdOrderRoutingLineToInsert.Insert(); + until TempProdOrderRoutingLineCopy.Next() = 0; + + TempProdOrderRoutingLineCopy.DeleteAll(); + end; + + /// + /// Sets the Production BOM No. and Version Code on the temporary production order line. + /// + /// The production BOM number to assign. + /// The production BOM version code to assign. + internal procedure UpdateProdOrderBOMInfo(BOMNo: Code[20]; VersionCode: Code[20]) + begin + if TempProdOrderLine."Prod. Order No." = '' then + exit; + if not TempProdOrderLine.FindFirst() then + exit; + TempProdOrderLine."Production BOM No." := BOMNo; + TempProdOrderLine."Production BOM Version Code" := VersionCode; + TempProdOrderLine.Modify(); + end; + + /// + /// Returns the currently stored BOM/routing source type. + /// + /// The source type indicating whether BOM/routing data originates from an existing record or is newly defined. + internal procedure GetRtngBOMSourceType(): Enum "Prod. Definition Source" + begin + exit(RoutingBOMSourceType); + end; + + /// + /// Returns the stored global purchase line by copying it into the provided variable. + /// + /// The variable to receive the temporary purchase line. + internal procedure GetGlobalPurchLine(var TempPurchaseLine: Record "Purchase Line" temporary) + begin + TempPurchaseLine.Copy(TempPurchLine, true); + end; + + /// + /// Returns the stored global sales line by copying it into the provided variable. + /// + /// The variable to receive the temporary sales line. + internal procedure GetGlobalSalesLine(var TempSalesLineOut: Record "Sales Line" temporary) + begin + TempSalesLineOut.Copy(TempSalesLine, true); + end; + + /// + /// Returns the stored global stockkeeping unit by copying it into the provided variable. + /// + /// The variable to receive the temporary stockkeeping unit. + internal procedure GetGlobalSKU(var TempSKUOut: Record "Stockkeeping Unit" temporary) + begin + TempSKUOut.Copy(TempSKU, true); + end; + + /// + /// Returns the item number that was used to initialize this temporary data instance. + /// + /// The global item number. + internal procedure GetGlobalItemNo(): Code[20] + begin + exit(GlobalItemNo); + end; + + /// + /// Returns the source record type that was used to initialize this temporary data instance, + /// e.g. Item, StockkeepingUnit, SalesLine, or PurchaseLine. + /// + /// The source type enum value. + internal procedure GetGlobalSourceType(): Enum "Prod. Definition Source" + begin + exit(GlobalSourceType); + end; + + /// + /// Sets the source record type. Use this when initializing TempData from an extension-handled source + /// (e.g. in a subscriber to Prod. Def. Source Initializer.OnInitializeFromSource) so the source type + /// is available to callers via GetGlobalSourceType. + /// + /// The source type to set. + internal procedure SetGlobalSourceType(SourceType: Enum "Prod. Definition Source") + begin + GlobalSourceType := SourceType; + end; + + /// + /// Sets the production order status on the temporary production order line. + /// + /// The production order status to set. + internal procedure SetProdOrderStatus(NewStatus: Enum "Production Order Status") + begin + ProdOrderStatus := NewStatus; + end; + + /// + /// Get the production order status. + /// + /// The production order status. + internal procedure GetProdOrderStatus(): Enum "Production Order Status" + begin + exit(ProdOrderStatus); + end; + + local procedure GetManufacturingSetup() + begin + if ManufacturingSetupRead then + exit; + + ManufacturingSetup.Get(); + ManufacturingSetupRead := true; + end; + + local procedure BindProdOrderSubscriber() + begin + UnbindSubscription(ProdOrdLineBind); + ProdOrdLineBind.SetProdOrder(TempProdOrder); + ProdOrdLineBind.SetProdOrderLine(TempProdOrderLine); + BindSubscription(ProdOrdLineBind); + end; + + + + [IntegrationEvent(false, false)] + local procedure OnAfterCreateTemporaryComponentFromBOMLine(var TempProdOrderComponent: Record "Prod. Order Component" temporary; ProductionBOMLine: Record "Production BOM Line") + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnAfterCreateTemporaryProdOrderRoutingLineFromRouting(var TempProdOrderRoutingLine: Record "Prod. Order Routing Line" temporary; RoutingLine: Record "Routing Line") + begin + end; + + + + [IntegrationEvent(false, false)] + local procedure OnAfterInitializeNewTemporaryRoutingInformation(var TempRoutingHeader: Record "Routing Header" temporary; var TempRoutingLine: Record "Routing Line" temporary; ItemNo: Code[20]) + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeInsertDefaultRoutingOperation(var TempRoutingLine: Record "Routing Line" temporary) + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeInsertDefaultTemporaryBOMLine(var TempBOMLine: Record "Production BOM Line" temporary; DefaultComponentItemNo: Code[20]) + begin + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionVersionMgmt.Codeunit.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionVersionMgmt.Codeunit.al new file mode 100644 index 0000000000..6339aa8de0 --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdDefinitionVersionMgmt.Codeunit.al @@ -0,0 +1,274 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; + +codeunit 99001015 "Prod. Definition Version Mgmt." +{ + var + VersionManagement: Codeunit VersionManagement; + + /// + /// Opens a lookup page for certified Production BOM versions and returns the selected version code. + /// + /// The production BOM number to filter versions for. + /// Returns the version code selected by the user. + /// True if the user selected a version; false if cancelled or no BOM number provided. + internal procedure ShowBOMVersionSelection(ProductionBOMNo: Code[20]; var SelectedVersion: Code[20]): Boolean + var + ProductionBOMVersion: Record "Production BOM Version"; + ProductionBOMVersions: Page "Prod. BOM Version List"; + begin + if ProductionBOMNo = '' then + exit(false); + + ProductionBOMVersion.SetRange("Production BOM No.", ProductionBOMNo); + ProductionBOMVersion.SetRange(Status, "BOM Status"::Certified); + ProductionBOMVersions.SetTableView(ProductionBOMVersion); + ProductionBOMVersions.LookupMode(true); + + if ProductionBOMVersions.RunModal() = Action::LookupOK then begin + ProductionBOMVersions.GetRecord(ProductionBOMVersion); + SelectedVersion := ProductionBOMVersion."Version Code"; + exit(true); + end; + + exit(false); + end; + + /// + /// Opens a lookup page for certified routing versions and returns the selected version code. + /// + /// The routing number to filter versions for. + /// Returns the version code selected by the user. + /// True if the user selected a version; false if cancelled or no routing number provided. + internal procedure ShowRoutingVersionSelection(RoutingNo: Code[20]; var SelectedVersion: Code[20]): Boolean + var + RoutingVersion: Record "Routing Version"; + RoutingVersions: Page "Routing Version List"; + begin + if RoutingNo = '' then + exit(false); + + RoutingVersion.SetRange("Routing No.", RoutingNo); + RoutingVersion.SetRange(Status, "Routing Status"::Certified); + RoutingVersions.SetTableView(RoutingVersion); + RoutingVersions.LookupMode(true); + + if RoutingVersions.RunModal() = Action::LookupOK then begin + RoutingVersions.GetRecord(RoutingVersion); + SelectedVersion := RoutingVersion."Version Code"; + exit(true); + end; + + exit(false); + end; + + /// + /// Returns the default active BOM version code for a given production BOM as of the work date. + /// + /// The production BOM number to retrieve the default version for. + /// The default version code, or empty if none exists. + internal procedure GetDefaultBOMVersion(ProductionBOMNo: Code[20]): Code[20] + begin + exit(VersionManagement.GetBOMVersion(ProductionBOMNo, WorkDate(), true)); + end; + + /// + /// Returns the default active routing version code for a given routing as of the work date. + /// + /// The routing number to retrieve the default version for. + /// The default version code, or empty if none exists. + internal procedure GetDefaultRoutingVersion(RoutingNo: Code[20]): Code[20] + begin + exit(VersionManagement.GetRtngVersion(RoutingNo, WorkDate(), true)); + end; + + /// + /// Opens a lookup page for certified production BOMs and returns the selected BOM number. + /// + /// Returns the BOM number selected by the user. + /// True if the user selected a BOM; false if cancelled. + internal procedure ShowBOMSelection(var SelectedBOMNo: Code[20]): Boolean + var + ProductionBOMHeader: Record "Production BOM Header"; + ProductionBOMList: Page "Production BOM List"; + begin + ProductionBOMHeader.SetRange(Status, "BOM Status"::Certified); + ProductionBOMList.SetTableView(ProductionBOMHeader); + ProductionBOMList.LookupMode(true); + + if ProductionBOMList.RunModal() = Action::LookupOK then begin + ProductionBOMList.GetRecord(ProductionBOMHeader); + SelectedBOMNo := ProductionBOMHeader."No."; + exit(true); + end; + + exit(false); + end; + + /// + /// Opens a lookup page for certified routings and returns the selected routing number. + /// + /// Returns the routing number selected by the user. + /// True if the user selected a routing; false if cancelled. + internal procedure ShowRoutingSelection(var SelectedRoutingNo: Code[20]): Boolean + var + RoutingHeader: Record "Routing Header"; + RoutingList: Page "Routing List"; + begin + RoutingHeader.SetRange(Status, "Routing Status"::Certified); + RoutingList.SetTableView(RoutingHeader); + RoutingList.LookupMode(true); + + if RoutingList.RunModal() = Action::LookupOK then begin + RoutingList.GetRecord(RoutingHeader); + SelectedRoutingNo := RoutingHeader."No."; + exit(true); + end; + + exit(false); + end; + + /// + /// Checks whether a routing (or a specific routing version) exists in the database. + /// + /// The routing number to check. + /// The version code to check. If empty, checks the base routing header. + /// True if the routing or routing version record exists; otherwise false. + internal procedure CheckRoutingExists(RoutingNo: Code[20]; RoutingVersionCode: Code[20]): Boolean + var + RoutingHeader: Record "Routing Header"; + RoutingVersion: Record "Routing Version"; + begin + if RoutingVersionCode <> '' then begin + RoutingVersion.SetLoadFields(SystemId); + exit(RoutingVersion.Get(RoutingNo, RoutingVersionCode)); + end; + + RoutingHeader.SetLoadFields(SystemId); + exit(RoutingHeader.Get(RoutingNo)); + end; + + /// + /// Validates that the specified routing (or routing version) has Certified status. Throws an error if not certified. + /// + /// The routing number to validate. + /// The version code to validate. If empty, the base routing header is validated. + internal procedure TestRoutingCertified(RoutingNo: Code[20]; RoutingVersionCode: Code[20]) + var + RoutingHeader: Record "Routing Header"; + RoutingVersion: Record "Routing Version"; + begin + if RoutingNo = '' then + exit; + + if RoutingVersionCode <> '' then begin + RoutingVersion.SetLoadFields(Status); + RoutingVersion.Get(RoutingNo, RoutingVersionCode); + RoutingVersion.TestField(Status, "Routing Status"::Certified); + exit; + end; + + RoutingHeader.SetLoadFields(Status); + RoutingHeader.Get(RoutingNo); + RoutingHeader.TestField(Status, "Routing Status"::Certified); + end; + + /// + /// Checks whether a production BOM (or a specific BOM version) exists in the database. + /// + /// The production BOM number to check. + /// The version code to check. If empty, checks the base BOM header. + /// True if the BOM or BOM version record exists; otherwise false. + internal procedure CheckBOMExists(ProductionBOMNo: Code[20]; BOMVersionCode: Code[20]): Boolean + var + ProductionBOMHeader: Record "Production BOM Header"; + ProductionBOMVersion: Record "Production BOM Version"; + begin + if BOMVersionCode <> '' then begin + ProductionBOMVersion.SetLoadFields(SystemId); + exit(ProductionBOMVersion.Get(ProductionBOMNo, BOMVersionCode)); + end; + + ProductionBOMHeader.SetLoadFields(SystemId); + exit(ProductionBOMHeader.Get(ProductionBOMNo)); + end; + + /// + /// Validates that the specified production BOM (or BOM version) has Certified status. Throws an error if not certified. + /// + /// The production BOM number to validate. + /// The version code to validate. If empty, the base BOM header is validated. + internal procedure TestBOMCertified(ProductionBOMNo: Code[20]; BOMVersionCode: Code[20]) + var + ProductionBOMHeader: Record "Production BOM Header"; + ProductionBOMVersion: Record "Production BOM Version"; + begin + if ProductionBOMNo = '' then + exit; + + if BOMVersionCode <> '' then begin + ProductionBOMVersion.SetLoadFields(Status); + ProductionBOMVersion.Get(ProductionBOMNo, BOMVersionCode); + ProductionBOMVersion.TestField(Status, "BOM Status"::Certified); + exit; + end; + + ProductionBOMHeader.SetLoadFields(Status); + ProductionBOMHeader.Get(ProductionBOMNo); + ProductionBOMHeader.TestField(Status, "BOM Status"::Certified); + end; + + /// + /// Returns the number series code used for BOM versions of the specified production BOM. + /// + /// The production BOM number to retrieve the version number series for. + /// The version number series code from the production BOM header. + internal procedure GetBOMVersionNoSeries(ProductionBOMNo: Code[20]): Code[20] + var + ProductionBOM: Record "Production BOM Header"; + begin + ProductionBOM.SetLoadFields("Version Nos."); + ProductionBOM.Get(ProductionBOMNo); + ProductionBOM.TestField("Version Nos."); + exit(ProductionBOM."Version Nos."); + end; + + /// + /// Returns the number series code used for routing versions of the specified routing. + /// + /// The routing number to retrieve the version number series for. + /// The version number series code from the routing header. + internal procedure GetRoutingVersionNoSeries(RoutingNo: Code[20]): Code[20] + var + Routing: Record "Routing Header"; + begin + Routing.SetLoadFields("Version Nos."); + Routing.Get(RoutingNo); + Routing.TestField("Version Nos."); + exit(Routing."Version Nos."); + end; + + /// + /// Validates that the production BOM has a version number series configured. Throws an error if not set. + /// + /// The production BOM number to validate the version number series for. + internal procedure ValidateBOMVersionNoSeries(BOMNo: Code[20]) + begin + GetBOMVersionNoSeries(BOMNo); + end; + + /// + /// Validates that the routing has a version number series configured. Throws an error if not set. + /// + /// The routing number to validate the version number series for. + internal procedure ValidateRoutingVersionNoSeries(RoutingNo: Code[20]) + begin + GetRoutingVersionNoSeries(RoutingNo); + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdOrderDirectCreator.Codeunit.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdOrderDirectCreator.Codeunit.al new file mode 100644 index 0000000000..a9397a2e89 --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProdOrderDirectCreator.Codeunit.al @@ -0,0 +1,270 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; + +codeunit 99001018 "Prod. Order Direct Creator" +{ + var + ProdOrderCompRoutingCreated: Boolean; + TempRecordNotFoundErr: Label 'No temporary %1 exists for source type %2. The wizard source must provide a %1 context before a production order can be created.', Comment = '%1 = record type (Production Order or Production Order Line), %2 = source type'; + + /// + /// Creates a Production Order from the temporary data held in TempData with the specified status. + /// Returns the created production order. + /// + [CommitBehavior(CommitBehavior::Ignore)] + internal procedure CreateProductionOrderFromTempData(var TempData: Codeunit "Prod. Definition Temp Data"; var ProdOrder: Record "Production Order") + var + ProdOrderLine: Record "Prod. Order Line"; + TempProdOrderLine: Record "Prod. Order Line" temporary; + begin + CreateProductionOrderFromTemp(ProdOrder, TempData); + + TempData.GetGlobalProdOrderLine(TempProdOrderLine); + if not TempProdOrderLine.FindFirst() then + Error(TempRecordNotFoundErr, TempProdOrderLine.TableCaption(), TempData.GetGlobalSourceType()); + + CreateProdOrderLineFromTemp(ProdOrderLine, ProdOrder, TempProdOrderLine); + TransferComponentsAndRoutingLines(ProdOrderLine, TempData); + end; + + local procedure CreateProductionOrderFromTemp(var ProdOrder: Record "Production Order"; var TempData: Codeunit "Prod. Definition Temp Data") + var + TempProdOrder: Record "Production Order" temporary; + begin + TempData.GetGlobalProdOrder(TempProdOrder); + if not TempProdOrder.FindFirst() then + Error(TempRecordNotFoundErr, TempProdOrder.TableCaption(), TempData.GetGlobalSourceType()); + + Clear(ProdOrder); + ProdOrder.Init(); + ProdOrder.Validate(Status, TempProdOrder.Status); + ProdOrder.Insert(true); + ProdOrderCompRoutingCreated := false; + + ProdOrder."Due Date" := TempProdOrder."Due Date"; + ProdOrder."Starting Date" := TempProdOrder."Due Date"; + ProdOrder."Source Type" := TempProdOrder."Source Type"; + ProdOrder."Location Code" := TempProdOrder."Location Code"; + ProdOrder.Validate("Source No.", TempProdOrder."Source No."); + ProdOrder.Validate("Variant Code", TempProdOrder."Variant Code"); + ProdOrder.Validate(Quantity, TempProdOrder.Quantity); + OnConfigureProductionOrderFromTempOnBeforeModify(ProdOrder, TempProdOrder); + ProdOrder.Modify(true); + end; + + local procedure CreateProdOrderLineFromTemp(var ProdOrderLine: Record "Prod. Order Line"; ProdOrder: Record "Production Order"; TempProdOrderLine: Record "Prod. Order Line" temporary) + var + RecordFound: Boolean; + begin + ProdOrderLine.SetRange(Status, ProdOrder.Status); + ProdOrderLine.SetRange("Prod. Order No.", ProdOrder."No."); + RecordFound := ProdOrderLine.FindFirst(); + if not RecordFound then begin + ProdOrderLine.Init(); + ProdOrderLine.Status := ProdOrder.Status; + ProdOrderLine."Prod. Order No." := ProdOrder."No."; + ProdOrderLine."Line No." := 10000; + ProdOrderLine."Routing Reference No." := ProdOrderLine."Line No."; + end; + + ProdOrderLine.Validate("Item No.", TempProdOrderLine."Item No."); + ProdOrderLine."Location Code" := TempProdOrderLine."Location Code"; + ProdOrderLine.Validate("Variant Code", TempProdOrderLine."Variant Code"); + ProdOrderLine.Description := TempProdOrderLine.Description; + ProdOrderLine."Description 2" := TempProdOrderLine."Description 2"; + ProdOrderLine."Shortcut Dimension 1 Code" := TempProdOrderLine."Shortcut Dimension 1 Code"; + ProdOrderLine."Shortcut Dimension 2 Code" := TempProdOrderLine."Shortcut Dimension 2 Code"; + ProdOrderLine."Dimension Set ID" := TempProdOrderLine."Dimension Set ID"; + ProdOrderLine.Validate(Quantity, TempProdOrderLine.Quantity); + ProdOrderLine."Due Date" := TempProdOrderLine."Due Date"; + ProdOrderLine."Starting Date" := ProdOrder."Starting Date"; + ProdOrderLine."Starting Time" := ProdOrder."Starting Time"; + ProdOrderLine."Ending Date" := ProdOrder."Ending Date"; + ProdOrderLine."Ending Time" := ProdOrder."Ending Time"; + ProdOrderLine."Bin Code" := TempProdOrderLine."Bin Code"; + ProdOrderLine.UpdateDatetime(); + if TempProdOrderLine."Production BOM No." <> '' then + ProdOrderLine."Production BOM No." := TempProdOrderLine."Production BOM No."; + if TempProdOrderLine."Production BOM Version Code" <> '' then + ProdOrderLine."Production BOM Version Code" := TempProdOrderLine."Production BOM Version Code"; + if TempProdOrderLine."Routing No." <> '' then + ProdOrderLine."Routing No." := TempProdOrderLine."Routing No."; + if TempProdOrderLine."Routing Version Code" <> '' then + ProdOrderLine."Routing Version Code" := TempProdOrderLine."Routing Version Code"; + ProdOrderLine.Validate("Unit Cost"); + + if RecordFound then + ProdOrderLine.Modify(true) + else + ProdOrderLine.Insert(true); + end; + + local procedure TransferComponentsAndRoutingLines(var ProdOrderLine: Record "Prod. Order Line"; var TempData: Codeunit "Prod. Definition Temp Data") + var + TempBOMLine: Record "Production BOM Line" temporary; + TempRoutingLine: Record "Routing Line" temporary; + begin + TempData.GetGlobalBOMLines(TempBOMLine); + TempData.GetGlobalRoutingLines(TempRoutingLine); + + if TempBOMLine.IsEmpty() and TempRoutingLine.IsEmpty() then + exit; + + TransferProductionOrderComponents(ProdOrderLine, TempData); + TransferProductionOrderRoutingLines(ProdOrderLine, TempData); + + ProdOrderCompRoutingCreated := true; + end; + + local procedure TransferProductionOrderComponents(var ProdOrderLine: Record "Prod. Order Line"; var TempData: Codeunit "Prod. Definition Temp Data") + var + ProdOrderComponent: Record "Prod. Order Component"; + TempProdOrderComponent: Record "Prod. Order Component" temporary; + begin + TempData.GetGlobalProdOrderComponent(TempProdOrderComponent); + if TempProdOrderComponent.FindSet() then + repeat + CreateProdOrderComponentFromTemp(ProdOrderComponent, TempProdOrderComponent, ProdOrderLine); + until TempProdOrderComponent.Next() = 0; + end; + + local procedure CreateProdOrderComponentFromTemp(var ProdOrderComponent: Record "Prod. Order Component"; TempProdOrderComponent: Record "Prod. Order Component" temporary; ProdOrderLine: Record "Prod. Order Line") + begin + ProdOrderComponent.Init(); + ProdOrderComponent.Status := ProdOrderLine.Status; + ProdOrderComponent."Prod. Order No." := ProdOrderLine."Prod. Order No."; + ProdOrderComponent."Prod. Order Line No." := ProdOrderLine."Line No."; + ProdOrderComponent."Line No." := TempProdOrderComponent."Line No."; + ProdOrderComponent.Validate("Item No.", TempProdOrderComponent."Item No."); + ProdOrderComponent.Validate("Unit of Measure Code", TempProdOrderComponent."Unit of Measure Code"); + ProdOrderComponent.Validate("Quantity per", TempProdOrderComponent."Quantity per"); + ProdOrderComponent.Validate("Routing Link Code", TempProdOrderComponent."Routing Link Code"); + ProdOrderComponent.Validate("Location Code", TempProdOrderComponent."Location Code"); + ProdOrderComponent.Validate("Variant Code", TempProdOrderComponent."Variant Code"); + ProdOrderComponent.Validate("Bin Code", TempProdOrderComponent."Bin Code"); + ProdOrderComponent.Validate("Flushing Method", TempProdOrderComponent."Flushing Method"); + ProdOrderComponent.Validate("Scrap %", TempProdOrderComponent."Scrap %"); + ProdOrderComponent.Validate("Calculation Formula", TempProdOrderComponent."Calculation Formula"); + ProdOrderComponent.Length := TempProdOrderComponent.Length; + ProdOrderComponent.Width := TempProdOrderComponent.Width; + ProdOrderComponent.Weight := TempProdOrderComponent.Weight; + ProdOrderComponent.Depth := TempProdOrderComponent.Depth; + ProdOrderComponent.Position := TempProdOrderComponent.Position; + ProdOrderComponent."Position 2" := TempProdOrderComponent."Position 2"; + ProdOrderComponent."Position 3" := TempProdOrderComponent."Position 3"; + ProdOrderComponent."Lead-Time Offset" := TempProdOrderComponent."Lead-Time Offset"; + ProdOrderComponent.Description := TempProdOrderComponent.Description; + ProdOrderComponent."Description 2" := TempProdOrderComponent."Description 2"; + OnBeforeInsertProdOrderComponentFromTemp(ProdOrderComponent, TempProdOrderComponent, ProdOrderLine); + ProdOrderComponent.Insert(true); + end; + + local procedure TransferProductionOrderRoutingLines(var ProdOrderLine: Record "Prod. Order Line"; var TempData: Codeunit "Prod. Definition Temp Data") + var + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + TempProdOrderRoutingLine: Record "Prod. Order Routing Line" temporary; + begin + TempData.GetGlobalProdOrderRoutingLine(TempProdOrderRoutingLine); + if TempProdOrderRoutingLine.FindSet() then + repeat + CreateProdOrderRoutingLineFromTemp(ProdOrderRoutingLine, TempProdOrderRoutingLine, ProdOrderLine); + until TempProdOrderRoutingLine.Next() = 0; + end; + + local procedure CreateProdOrderRoutingLineFromTemp(var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; TempProdOrderRoutingLine: Record "Prod. Order Routing Line" temporary; ProdOrderLine: Record "Prod. Order Line") + begin + ProdOrderRoutingLine.Init(); + ProdOrderRoutingLine.Status := ProdOrderLine.Status; + ProdOrderRoutingLine."Prod. Order No." := ProdOrderLine."Prod. Order No."; + ProdOrderRoutingLine."Routing No." := TempProdOrderRoutingLine."Routing No."; + ProdOrderRoutingLine.Validate("Routing Reference No.", ProdOrderLine."Line No."); + ProdOrderRoutingLine.Validate("Operation No.", TempProdOrderRoutingLine."Operation No."); + ProdOrderRoutingLine."Starting Time" := ProdOrderLine."Starting Time"; + ProdOrderRoutingLine."Starting Date" := ProdOrderLine."Starting Date"; + ProdOrderRoutingLine."Ending Time" := ProdOrderLine."Ending Time"; + ProdOrderRoutingLine."Ending Date" := ProdOrderLine."Ending Date"; + ProdOrderRoutingLine.Insert(true); + ProdOrderRoutingLine.Validate(Type, TempProdOrderRoutingLine.Type); + ProdOrderRoutingLine.Validate("No.", TempProdOrderRoutingLine."No."); + ProdOrderRoutingLine.Validate("Work Center No.", TempProdOrderRoutingLine."Work Center No."); + ProdOrderRoutingLine.Description := TempProdOrderRoutingLine.Description; + ProdOrderRoutingLine."Description 2" := TempProdOrderRoutingLine."Description 2"; + ProdOrderRoutingLine."Setup Time" := TempProdOrderRoutingLine."Setup Time"; + ProdOrderRoutingLine."Run Time" := TempProdOrderRoutingLine."Run Time"; + ProdOrderRoutingLine."Wait Time" := TempProdOrderRoutingLine."Wait Time"; + ProdOrderRoutingLine."Move Time" := TempProdOrderRoutingLine."Move Time"; + ProdOrderRoutingLine."Routing Link Code" := TempProdOrderRoutingLine."Routing Link Code"; + ProdOrderRoutingLine."Previous Operation No." := TempProdOrderRoutingLine."Previous Operation No."; + ProdOrderRoutingLine."Next Operation No." := TempProdOrderRoutingLine."Next Operation No."; + ProdOrderRoutingLine."Flushing Method" := TempProdOrderRoutingLine."Flushing Method"; + ProdOrderRoutingLine."Schedule Manually" := TempProdOrderRoutingLine."Schedule Manually"; + ProdOrderRoutingLine."Setup Time Unit of Meas. Code" := TempProdOrderRoutingLine."Setup Time Unit of Meas. Code"; + ProdOrderRoutingLine."Run Time Unit of Meas. Code" := TempProdOrderRoutingLine."Run Time Unit of Meas. Code"; + ProdOrderRoutingLine."Wait Time Unit of Meas. Code" := TempProdOrderRoutingLine."Wait Time Unit of Meas. Code"; + ProdOrderRoutingLine."Move Time Unit of Meas. Code" := TempProdOrderRoutingLine."Move Time Unit of Meas. Code"; + ProdOrderRoutingLine."Fixed Scrap Quantity" := TempProdOrderRoutingLine."Fixed Scrap Quantity"; + ProdOrderRoutingLine."Scrap Factor %" := TempProdOrderRoutingLine."Scrap Factor %"; + ProdOrderRoutingLine."Send-Ahead Quantity" := TempProdOrderRoutingLine."Send-Ahead Quantity"; + ProdOrderRoutingLine."Concurrent Capacities" := TempProdOrderRoutingLine."Concurrent Capacities"; + ProdOrderRoutingLine."Lot Size" := TempProdOrderRoutingLine."Lot Size"; + ProdOrderRoutingLine."Unit Cost per" := TempProdOrderRoutingLine."Unit Cost per"; + ProdOrderRoutingLine.Validate("Location Code", TempProdOrderRoutingLine."Location Code"); + ProdOrderRoutingLine.Validate("From-Production Bin Code", TempProdOrderRoutingLine."From-Production Bin Code"); + ProdOrderRoutingLine.Validate("To-Production Bin Code", TempProdOrderRoutingLine."To-Production Bin Code"); + ProdOrderRoutingLine.Validate("Open Shop Floor Bin Code", TempProdOrderRoutingLine."Open Shop Floor Bin Code"); + OnBeforeModifyProdOrderRoutingLineFromTemp(ProdOrderRoutingLine, TempProdOrderRoutingLine, ProdOrderLine); + ProdOrderRoutingLine.Modify(true); + end; + /// + /// Refreshes the production order. + /// When components and routing lines were directly transferred from temporary data, only dates are + /// recalculated (CalcRouting/CalcComponents = false). When no temporary transfer occurred, a full + /// backwards explosion is performed using the item's saved BOM and routing (CalcRouting/CalcComponents = true), + /// mirroring the standard production order creation path. + /// + internal procedure RefreshProductionOrder(var ProductionOrder: Record "Production Order") + var + ProdOrderLine: Record "Prod. Order Line"; + CalculateProdOrder: Codeunit "Calculate Prod. Order"; + Direction: Option Forward,Backward; + begin + ProdOrderLine.SetRange("Prod. Order No.", ProductionOrder."No."); + ProdOrderLine.SetRange(Status, ProductionOrder.Status); + if not ProdOrderLine.FindFirst() then + exit; + if ProdOrderCompRoutingCreated then + CalculateProdOrder.Calculate(ProdOrderLine, Direction::Backward, false, false, false, true) + else + CalculateProdOrder.Calculate(ProdOrderLine, Direction::Backward, true, true, true, true); + end; + /// + /// Returns whether production order components and routing lines were created during the last direct creation run. + /// + /// True if components and routing lines were created; otherwise false. + internal procedure GetProdOrderCompRoutingCreated(): Boolean + begin + exit(ProdOrderCompRoutingCreated); + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeInsertProdOrderComponentFromTemp(var ProdOrderComponent: Record "Prod. Order Component"; TempProdOrderComponent: Record "Prod. Order Component"; ProdOrderLine: Record "Prod. Order Line") + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeModifyProdOrderRoutingLineFromTemp(var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; TempProdOrderRoutingLine: Record "Prod. Order Routing Line"; ProdOrderLine: Record "Prod. Order Line") + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnConfigureProductionOrderFromTempOnBeforeModify(var ProdOrder: Record "Production Order"; TempProdOrder: Record "Production Order" temporary) + begin + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProductionDefinitionManager.Codeunit.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProductionDefinitionManager.Codeunit.al new file mode 100644 index 0000000000..9b14abf691 --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProductionDefinitionManager.Codeunit.al @@ -0,0 +1,636 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Foundation.NoSeries; +using Microsoft.Foundation.UOM; +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Location; +using Microsoft.Inventory.Tracking; +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; +using Microsoft.Manufacturing.Setup; +using Microsoft.Purchases.Document; +using Microsoft.Sales.Document; +using Microsoft.Utilities; + +codeunit 99001017 "Production Definition Manager" +{ + var + TempData: Codeunit "Prod. Definition Temp Data"; + ProdDefVersionMgmt: Codeunit "Prod. Definition Version Mgmt."; + ProdDefSourceInitializer: Codeunit "Prod. Def. Source Initializer"; + BomRtngSaveTarget: Enum "Prod. Definition Save Target"; + WizardMode: Enum "Prod. Definition Mode"; + ProdOrderNotificationIdTok: Label '{5A3C5A1F-3B2E-4C8D-9F0A-1B2C3D4E5F6A}', Locked = true; + + /// + /// Opens the Production Definition Wizard for any source record type using the default Released production order status. + /// + /// The source record (Item, Stockkeeping Unit, Sales Line, or any other supported type). + /// Determines the wizard behavior. + /// True if the wizard was completed and confirmed; false if the user cancelled. + internal procedure RunForSource(Source: Variant; Mode: Enum "Prod. Definition Mode"): Boolean + begin + exit(RunForSource(Source, Mode, "Production Order Status"::Released)); + end; + + /// + /// Opens the Production Definition Wizard for any source record type. The source is dispatched + /// through Prod. Def. Source Initializer, which handles Item, Stockkeeping Unit, and Sales Line natively. + /// For other source types such as Purchase Line, subscribe to OnInitializeFromSource on that codeunit. + /// BOM and routing are resolved from the best matching SKU or item. Depending on the wizard mode, + /// a production order may also be created directly from the resulting definition. + /// + /// The source record (Item, Stockkeeping Unit, Sales Line, Purchase Line, or any other supported type). + /// Determines the wizard behavior. + /// The status to use when creating the production order. Defaults to Released. + /// True if the wizard was completed and confirmed; false if the user cancelled. + + internal procedure RunForSource(Source: Variant; Mode: Enum "Prod. Definition Mode"; ProdOrderStatus: Enum "Production Order Status"): Boolean + var + Item: Record Item; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + VariantCode: Code[10]; + LocationCode: Code[10]; + SourceType: Enum "Prod. Definition Source"; + ScenarioType: Enum "Prod. Definition Scenario"; + begin + WizardMode := Mode; + TempData.SetProdOrderStatus(ProdOrderStatus); + + ProdDefSourceInitializer.InitializeFromSource(TempData, Source); + ValidateSourceTypeForMode(TempData.GetGlobalSourceType(), Mode); + + ItemNo := TempData.GetGlobalItemNo(); + Item.SetLoadFields(Description, "Base Unit of Measure", "Production BOM No.", "Routing No."); + Item.Get(ItemNo); + + GetLocationAndVariantFromTempData(LocationCode, VariantCode); + GetBOMAndRoutingFromBestSource(ItemNo, VariantCode, LocationCode, BOMNo, RoutingNo, SourceType); + + TempData.SetRtngBOMSourceType(SourceType); + ScenarioType := GetScenarioType(BOMNo, RoutingNo); + PrepareBOMAndRoutingDataForScenario(BOMNo, RoutingNo, Item.Description, Item."Base Unit of Measure"); + + if ExecuteWizardProcess(Item, ScenarioType) then begin + PostWizardProcessing(ItemNo); + exit(true); + end; + exit(false); + end; + + local procedure ValidateSourceTypeForMode(SourceType: Enum "Prod. Definition Source"; Mode: Enum "Prod. Definition Mode") + var + CreateProdOrderNotSupportedErr: Label 'Creating a production order directly is not supported when the wizard is called from an Item or Stockkeeping Unit. Use the Sales Line source instead.'; + begin + if Mode <> Mode::CreateProductionOrder then + exit; + if SourceType in ["Prod. Definition Source"::Item, "Prod. Definition Source"::StockkeepingUnit] then + Error(CreateProdOrderNotSupportedErr); + end; + + local procedure GetScenarioType(BOMNo: Code[20]; RoutingNo: Code[20]): Enum "Prod. Definition Scenario" + var + ScenarioType: Enum "Prod. Definition Scenario"; + begin + if (BOMNo <> '') and (RoutingNo <> '') then + exit(ScenarioType::BothAvailable); + if (BOMNo <> '') or (RoutingNo <> '') then + exit(ScenarioType::PartiallyAvailable); + exit(ScenarioType::NothingAvailable); + end; + + local procedure PrepareBOMAndRoutingDataForScenario(BOMNo: Code[20]; RoutingNo: Code[20]; ItemDescription: Text[100]; BaseUOMCode: Code[10]) + var + BOMVersionCode: Code[20]; + RoutingVersionCode: Code[20]; + begin + TempData.InitializeNewTemporaryBOMInformation(TempData.GetGlobalItemNo(), ItemDescription, BaseUOMCode); + if BOMNo <> '' then begin + BOMVersionCode := ProdDefVersionMgmt.GetDefaultBOMVersion(BOMNo); + TempData.LoadBOMLines(BOMNo, BOMVersionCode); + end; + + TempData.InitializeNewTemporaryRoutingInformation(TempData.GetGlobalItemNo(), ItemDescription); + if RoutingNo <> '' then begin + RoutingVersionCode := ProdDefVersionMgmt.GetDefaultRoutingVersion(RoutingNo); + TempData.LoadRoutingLines(RoutingNo, RoutingVersionCode); + end; + end; + + local procedure ExecuteWizardProcess(Item: Record Item; ScenarioType: Enum "Prod. Definition Scenario"): Boolean + var + BOMRoutingDisplay: Enum "Prod. Definition Display"; + ProdCompDisplay: Enum "Prod. Definition Display"; + begin + GetShowEditTypesForScenario(ScenarioType, BOMRoutingDisplay, ProdCompDisplay); + + if ShouldSkipUserInteraction(BOMRoutingDisplay, ProdCompDisplay) then begin + TempData.BuildTemporaryStructureFromBOMRouting(); + exit(true); + end; + + exit(ExecuteWizardPageWithTemporaryData(Item, BOMRoutingDisplay, ProdCompDisplay)); + end; + + local procedure GetShowEditTypesForScenario(ScenarioType: Enum "Prod. Definition Scenario"; var BOMRoutingDisplay: Enum "Prod. Definition Display"; var ProdCompDisplay: Enum "Prod. Definition Display") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + + ManufacturingSetup.SetLoadFields("Show Rtng BOM Select Both", "Show Prod Comp Select Both", "Show Rtng BOM Select Partial", "Show Prod Comp Select Partial", "Show Rtng BOM Select Nothing", "Show Prod Comp Select Nothing"); + ManufacturingSetup.Get(); + case ScenarioType of + ScenarioType::BothAvailable: + begin + BOMRoutingDisplay := ManufacturingSetup."Show Rtng BOM Select Both"; + ProdCompDisplay := ManufacturingSetup."Show Prod Comp Select Both"; + end; + ScenarioType::PartiallyAvailable: + begin + BOMRoutingDisplay := ManufacturingSetup."Show Rtng BOM Select Partial"; + ProdCompDisplay := ManufacturingSetup."Show Prod Comp Select Partial"; + end; + ScenarioType::NothingAvailable: + begin + BOMRoutingDisplay := ManufacturingSetup."Show Rtng BOM Select Nothing"; + ProdCompDisplay := ManufacturingSetup."Show Prod Comp Select Nothing"; + end; + end; + + if WizardMode = WizardMode::DefineItemStructure then begin + BOMRoutingDisplay := BOMRoutingDisplay::Edit; + ProdCompDisplay := ProdCompDisplay::Hide; + end; + end; + + local procedure ShouldSkipUserInteraction(BOMRoutingDisplay: Enum "Prod. Definition Display"; ProdCompDisplay: Enum "Prod. Definition Display"): Boolean + begin + exit((BOMRoutingDisplay = BOMRoutingDisplay::Hide) and (ProdCompDisplay = ProdCompDisplay::Hide)); + end; + + local procedure ExecuteWizardPageWithTemporaryData(Item: Record Item; BOMRoutingDisplay: Enum "Prod. Definition Display"; ProdCompDisplay: Enum "Prod. Definition Display"): Boolean + var + ProductionDefinitionWizard: Page "Production Definition Wizard"; + begin + ProductionDefinitionWizard.SetItem(Item); + ProductionDefinitionWizard.SetBOMRoutingDisplay(BOMRoutingDisplay); + ProductionDefinitionWizard.SetProdCompDisplay(ProdCompDisplay); + ProductionDefinitionWizard.SetTempData(TempData); + ProductionDefinitionWizard.SetWizardMode(WizardMode); + ProductionDefinitionWizard.RunModal(); + + if ProductionDefinitionWizard.GetFinished() then begin + BomRtngSaveTarget := ProductionDefinitionWizard.GetBomRtngSaveTarget(); + exit(true); + end; + + exit(false); + end; + + /// + /// Persists the temporary production BOM and routing data collected during the wizard run. + /// If no existing BOM or routing is found for the item, new records are created using the + /// configured number series and certified immediately. If a version code is present, a new + /// BOM or routing version is created instead. After saving, the resolved BOM and routing + /// numbers are written back to the source item or stockkeeping unit. + /// + /// The item number whose production BOM and routing records are to be created or versioned. + internal procedure ProcessBOMAndRoutingData(ItemNo: Code[20]) + var + TempBOMHeader: Record "Production BOM Header" temporary; + TempBOMLine: Record "Production BOM Line" temporary; + TempRoutingHeader: Record "Routing Header" temporary; + TempRoutingLine: Record "Routing Line" temporary; + BOMNo: Code[20]; + BOMVersionCode: Code[20]; + RoutingNo: Code[20]; + RoutingVersionCode: Code[20]; + begin + TempData.GetGlobalBOMHeader(TempBOMHeader); + TempData.GetGlobalBOMLines(TempBOMLine); + TempData.GetGlobalRoutingHeader(TempRoutingHeader); + TempData.GetGlobalRoutingLines(TempRoutingLine); + + if TempBOMLine.FindFirst() then begin + BOMNo := TempBOMLine."Production BOM No."; + BOMVersionCode := TempBOMLine."Version Code"; + if BOMVersionCode = '' then + CreateBOMIfNotExists(TempBOMHeader, TempBOMLine, BOMNo) + else + SaveBOMVersionIfRequired(TempBOMHeader, TempBOMLine, BOMNo, BOMVersionCode); + end; + if TempRoutingLine.FindFirst() then begin + RoutingNo := TempRoutingLine."Routing No."; + RoutingVersionCode := TempRoutingLine."Version Code"; + if RoutingVersionCode = '' then + CreateRoutingIfNotExists(TempRoutingHeader, TempRoutingLine, RoutingNo) + else + SaveRoutingVersionIfRequired(TempRoutingHeader, TempRoutingLine, RoutingNo, RoutingVersionCode); + end; + + UpdateSourceWithBOMRoutingNumbers(ItemNo, BOMNo, RoutingNo); + + TempData.UpdateProdOrderRoutingInfo(RoutingNo, RoutingVersionCode); + TempData.UpdateProdOrderBOMInfo(BOMNo, BOMVersionCode); + end; + + local procedure CreateBOMIfNotExists(var TempBOMHeader: Record "Production BOM Header" temporary; var TempBOMLine: Record "Production BOM Line" temporary; var BOMNo: Code[20]) + var + ManufacturingSetup: Record "Manufacturing Setup"; + BOMHeader: Record "Production BOM Header"; + BOMLine: Record "Production BOM Line"; + NoSeries: Codeunit "No. Series"; + begin + if TempBOMLine.IsEmpty() then + exit; + if ProdDefVersionMgmt.CheckBOMExists(BOMNo, '') then + exit; + + ManufacturingSetup.Get(); + BOMNo := NoSeries.GetNextNo(ManufacturingSetup."Production BOM Nos."); + + BOMHeader.Init(); + BOMHeader."No." := BOMNo; + BOMHeader.Description := TempBOMHeader.Description; + BOMHeader.Validate("Unit of Measure Code", TempBOMHeader."Unit of Measure Code"); + BOMHeader.Insert(true); + + if TempBOMLine.FindSet() then + repeat + BOMLine := TempBOMLine; + BOMLine."Production BOM No." := BOMNo; + BOMLine."Version Code" := ''; + BOMLine.Insert(true); + until TempBOMLine.Next() = 0; + + BOMHeader.Validate(Status, "BOM Status"::Certified); + BOMHeader.Modify(true); + + TempData.LoadBOMLines(BOMNo, ''); + end; + + local procedure CreateRoutingIfNotExists(var TempRoutingHeader: Record "Routing Header" temporary; var TempRoutingLine: Record "Routing Line" temporary; var RoutingNo: Code[20]) + var + ManufacturingSetup: Record "Manufacturing Setup"; + RoutingHeader: Record "Routing Header"; + RoutingLine: Record "Routing Line"; + NoSeries: Codeunit "No. Series"; + begin + if TempRoutingLine.IsEmpty() then + exit; + if not TempRoutingHeader.FindFirst() then + exit; + + if ProdDefVersionMgmt.CheckRoutingExists(RoutingNo, '') then + exit; + + ManufacturingSetup.Get(); + RoutingNo := NoSeries.GetNextNo(ManufacturingSetup."Routing Nos."); + + RoutingHeader.Init(); + RoutingHeader."No." := RoutingNo; + RoutingHeader.Description := TempRoutingHeader.Description; + RoutingHeader.Insert(true); + + if TempRoutingLine.FindSet() then + repeat + RoutingLine := TempRoutingLine; + RoutingLine."Routing No." := RoutingNo; + RoutingLine."Version Code" := ''; + RoutingLine.Insert(true); + until TempRoutingLine.Next() = 0; + + RoutingHeader.Validate(Status, "Routing Status"::Certified); + RoutingHeader.Modify(true); + + TempData.LoadRoutingLines(RoutingNo, ''); + end; + + local procedure SaveBOMVersionIfRequired(var TempBOMHeader: Record "Production BOM Header" temporary; var TempBOMLine: Record "Production BOM Line" temporary; BOMNo: Code[20]; BOMVersionCode: Code[20]) + var + BOMHeader: Record "Production BOM Header"; + BOMLine: Record "Production BOM Line"; + BOMVersion: Record "Production BOM Version"; + NoSeries: Codeunit "No. Series"; + NewBOMVersionCode: Code[20]; + begin + if ProdDefVersionMgmt.CheckBOMExists(BOMNo, BOMVersionCode) then begin + TempData.UpdateBOMVersionCode(BOMVersionCode); + exit; + end; + + NewBOMVersionCode := NoSeries.GetNextNo(ProdDefVersionMgmt.GetBOMVersionNoSeries(BOMNo)); + TempData.UpdateBOMVersionCode(NewBOMVersionCode); + + BOMHeader.SetLoadFields(Description, "Unit of Measure Code"); + BOMHeader.Get(BOMNo); + + BOMVersion.Init(); + BOMVersion."Production BOM No." := BOMNo; + BOMVersion."Version Code" := NewBOMVersionCode; + BOMVersion.Description := BOMHeader.Description; + BOMVersion."Unit of Measure Code" := BOMHeader."Unit of Measure Code"; + BOMVersion."Starting Date" := WorkDate(); + BOMVersion.Insert(true); + + if TempBOMLine.FindSet() then + repeat + BOMLine := TempBOMLine; + BOMLine."Production BOM No." := BOMNo; + BOMLine."Version Code" := NewBOMVersionCode; + BOMLine.Insert(true); + until TempBOMLine.Next() = 0; + + BOMVersion.Validate(Status, "BOM Status"::Certified); + BOMVersion.Modify(true); + end; + + local procedure SaveRoutingVersionIfRequired(var TempRoutingHeader: Record "Routing Header" temporary; var TempRoutingLine: Record "Routing Line" temporary; RoutingNo: Code[20]; RoutingVersionCode: Code[20]) + var + RoutingHeader: Record "Routing Header"; + RoutingLine: Record "Routing Line"; + RoutingVersion: Record "Routing Version"; + NoSeries: Codeunit "No. Series"; + NewRoutingVersionCode: Code[20]; + begin + if ProdDefVersionMgmt.CheckRoutingExists(RoutingNo, RoutingVersionCode) then begin + TempData.UpdateRoutingVersionCode(RoutingVersionCode); + exit; + end; + + NewRoutingVersionCode := NoSeries.GetNextNo(ProdDefVersionMgmt.GetRoutingVersionNoSeries(RoutingNo)); + TempData.UpdateRoutingVersionCode(NewRoutingVersionCode); + + + RoutingHeader.SetLoadFields(Description, Type); + RoutingHeader.Get(RoutingNo); + + RoutingVersion.Init(); + RoutingVersion."Routing No." := RoutingNo; + RoutingVersion."Version Code" := NewRoutingVersionCode; + RoutingVersion.Description := RoutingHeader.Description; + RoutingVersion.Type := RoutingHeader.Type; + RoutingVersion."Starting Date" := WorkDate(); + RoutingVersion.Insert(true); + + if TempRoutingLine.FindSet() then + repeat + RoutingLine := TempRoutingLine; + RoutingLine."Routing No." := RoutingNo; + RoutingLine."Version Code" := NewRoutingVersionCode; + RoutingLine.Insert(true); + until TempRoutingLine.Next() = 0; + + RoutingVersion.Validate(Status, "Routing Status"::Certified); + RoutingVersion.Modify(true); + end; + + local procedure UpdateSourceWithBOMRoutingNumbers(ItemNo: Code[20]; BOMNo: Code[20]; RoutingNo: Code[20]) + var + Item: Record Item; + StockkeepingUnit: Record "Stockkeeping Unit"; + LocationCode: Code[10]; + VariantCode: Code[10]; + begin + case BomRtngSaveTarget of + BomRtngSaveTarget::Item: + if Item.Get(ItemNo) then + if (Item."Production BOM No." <> BOMNo) or (Item."Routing No." <> RoutingNo) then begin + Item.Validate("Production BOM No.", BOMNo); + Item.Validate("Routing No.", RoutingNo); + Item.Modify(true); + end; + BomRtngSaveTarget::StockkeepingUnit: + begin + GetLocationAndVariantFromTempData(LocationCode, VariantCode); + if StockkeepingUnit.Get(LocationCode, ItemNo, VariantCode) then begin + if (StockkeepingUnit."Production BOM No." <> BOMNo) or (StockkeepingUnit."Routing No." <> RoutingNo) then begin + StockkeepingUnit.Validate("Production BOM No.", BOMNo); + StockkeepingUnit.Validate("Routing No.", RoutingNo); + StockkeepingUnit.Modify(true); + end; + end else begin + StockkeepingUnit.Init(); + StockkeepingUnit."Location Code" := LocationCode; + StockkeepingUnit."Item No." := ItemNo; + StockkeepingUnit."Variant Code" := VariantCode; + StockkeepingUnit.Validate("Production BOM No.", BOMNo); + StockkeepingUnit.Validate("Routing No.", RoutingNo); + StockkeepingUnit.Insert(true); + end; + end; + end; + end; + + local procedure GetLocationAndVariantFromTempData(var LocationCode: Code[10]; var VariantCode: Code[10]) + var + TempSKU: Record "Stockkeeping Unit" temporary; + TempPurchLine: Record "Purchase Line" temporary; + TempSalesLine: Record "Sales Line" temporary; + SourceType: Enum "Prod. Definition Source"; + begin + SourceType := TempData.GetGlobalSourceType(); + case SourceType of + SourceType::StockkeepingUnit: + begin + TempData.GetGlobalSKU(TempSKU); + if TempSKU.FindFirst() then begin + LocationCode := TempSKU."Location Code"; + VariantCode := TempSKU."Variant Code"; + end; + end; + SourceType::SalesLine: + begin + TempData.GetGlobalSalesLine(TempSalesLine); + if TempSalesLine.FindFirst() then begin + LocationCode := TempSalesLine."Location Code"; + VariantCode := TempSalesLine."Variant Code"; + end; + end; + SourceType::PurchaseLine: + begin + TempData.GetGlobalPurchLine(TempPurchLine); + if TempPurchLine.FindFirst() then begin + LocationCode := TempPurchLine."Location Code"; + VariantCode := TempPurchLine."Variant Code"; + end; + end; + end; + end; + + /// + /// Returns the internal temporary data codeunit that holds the BOM, routing, and source + /// document data collected during or after a wizard run. Use this to inspect or pass the + /// wizard results to other codeunits without re-running the wizard. + /// + /// Receives the temporary data codeunit instance used by this manager. + internal procedure GetTempData(var OutTempData: Codeunit "Prod. Definition Temp Data") + begin + OutTempData := TempData; + end; + + local procedure PostWizardProcessing(ItemNo: Code[20]) + var + ManufacturingSetup: Record "Manufacturing Setup"; + ProdOrderDirectCreator: Codeunit "Prod. Order Direct Creator"; + ProdOrder: Record "Production Order"; + begin + ManufacturingSetup.SetLoadFields("Always Save Modified Versions"); + ManufacturingSetup.Get(); + + if (BomRtngSaveTarget <> BomRtngSaveTarget::Empty) or ManufacturingSetup."Always Save Modified Versions" then + ProcessBOMAndRoutingData(ItemNo); + + if WizardMode = WizardMode::CreateProductionOrder then begin + ProdOrderDirectCreator.CreateProductionOrderFromTempData(TempData, ProdOrder); + ProdOrderDirectCreator.RefreshProductionOrder(ProdOrder); + CreateReservationFromSalesLine(ProdOrder); + FlushReleasedProdOrderForSalesSource(ProdOrder); + SendProdOrderCreatedNotification(ProdOrder); + end; + + OnAfterPostWizardProcessing(TempData, ProdOrder); + end; + + local procedure GetBOMAndRoutingFromBestSource(ItemNo: Code[20]; VariantCode: Code[10]; LocationCode: Code[10]; var BOMNo: Code[20]; var RoutingNo: Code[20]; var SourceType: Enum "Prod. Definition Source") + var + StockkeepingUnit: Record "Stockkeeping Unit"; + Item: Record Item; + begin + Item.SetLoadFields("Production BOM No.", "Routing No."); + Item.Get(ItemNo); + + BOMNo := Item."Production BOM No."; + RoutingNo := Item."Routing No."; + if (BOMNo <> '') or (RoutingNo <> '') then + SourceType := SourceType::Item; + + if StockkeepingUnit.Get(LocationCode, ItemNo, VariantCode) then begin + if StockkeepingUnit."Production BOM No." <> '' then + BOMNo := StockkeepingUnit."Production BOM No."; + if StockkeepingUnit."Routing No." <> '' then + RoutingNo := StockkeepingUnit."Routing No."; + if (StockkeepingUnit."Production BOM No." <> '') or (StockkeepingUnit."Routing No." <> '') then + SourceType := SourceType::StockkeepingUnit; + end; + end; + + local procedure FlushReleasedProdOrderForSalesSource(ProdOrder: Record "Production Order") + var + TempSalesLine: Record "Sales Line" temporary; + ProdOrderStatusMgt: Codeunit "Prod. Order Status Management"; + begin + if ProdOrder.Status <> ProdOrder.Status::Released then + exit; + TempData.GetGlobalSalesLine(TempSalesLine); + if not TempSalesLine.FindFirst() then + exit; + ProdOrderStatusMgt.FlushProdOrder(ProdOrder, ProdOrder.Status, WorkDate()); + end; + + local procedure CreateReservationFromSalesLine(ProdOrder: Record "Production Order") + var + TempSalesLine: Record "Sales Line" temporary; + SalesLine: Record "Sales Line"; + ProdOrderLine: Record "Prod. Order Line"; + TrackingSpecification: Record "Tracking Specification"; + SalesLineReserve: Codeunit "Sales Line-Reserve"; + ItemTrackingMgt: Codeunit "Item Tracking Management"; + UOMMgt: Codeunit "Unit of Measure Management"; + ProdOrderRowID: Text[250]; + ReservQty: Decimal; + ReservQtyBase: Decimal; + begin + TempData.GetGlobalSalesLine(TempSalesLine); + if not TempSalesLine.FindFirst() then + exit; + + if not SalesLine.Get(TempSalesLine."Document Type", TempSalesLine."Document No.", TempSalesLine."Line No.") then + exit; + + ProdOrderLine.SetRange(Status, ProdOrder.Status); + ProdOrderLine.SetRange("Prod. Order No.", ProdOrder."No."); + if not ProdOrderLine.FindFirst() then + exit; + + ProdOrderRowID := ItemTrackingMgt.ComposeRowID( + Database::"Prod. Order Line", ProdOrderLine.Status.AsInteger(), + ProdOrderLine."Prod. Order No.", '', ProdOrderLine."Line No.", 0); + ItemTrackingMgt.CopyItemTracking(SalesLine.RowID1(), ProdOrderRowID, true, true); + + SalesLine.CalcFields("Reserved Quantity", "Reserved Qty. (Base)"); + if ProdOrderLine."Remaining Qty. (Base)" > (SalesLine."Outstanding Qty. (Base)" - SalesLine."Reserved Qty. (Base)") then begin + ReservQty := SalesLine."Outstanding Quantity" - SalesLine."Reserved Quantity"; + ReservQtyBase := SalesLine."Outstanding Qty. (Base)" - SalesLine."Reserved Qty. (Base)"; + end else begin + ReservQty := Round(ProdOrderLine."Remaining Qty. (Base)" / SalesLine."Qty. per Unit of Measure", UOMMgt.QtyRndPrecision()); + ReservQtyBase := ProdOrderLine."Remaining Qty. (Base)"; + end; + + TrackingSpecification.InitTrackingSpecification( + Database::"Prod. Order Line", ProdOrderLine.Status.AsInteger(), ProdOrderLine."Prod. Order No.", '', ProdOrderLine."Line No.", 0, + ProdOrderLine."Variant Code", ProdOrderLine."Location Code", ProdOrderLine."Qty. per Unit of Measure"); + SalesLineReserve.BindToTracking( + SalesLine, TrackingSpecification, ProdOrderLine.Description, ProdOrderLine."Ending Date", ReservQty, ReservQtyBase); + + if SalesLine.Reserve = SalesLine.Reserve::Never then begin + SalesLine.Reserve := SalesLine.Reserve::Optional; + SalesLine.Modify(); + end; + ProdOrderLine.Modify(); + end; + + local procedure SendProdOrderCreatedNotification(ProdOrder: Record "Production Order") + var + ProdOrderNotification: Notification; + ProdOrderCreatedMsg: Label 'Production Order %1 has been created.', Comment = '%1 = Production Order No.'; + OpenProdOrderActionLbl: Label 'Open Production Order'; + begin + ProdOrderNotification.Id(GetProdOrderCreatedNotificationId()); + ProdOrderNotification.Message(StrSubstNo(ProdOrderCreatedMsg, ProdOrder."No.")); + ProdOrderNotification.SetData('Status', Format(ProdOrder.Status.AsInteger())); + ProdOrderNotification.SetData('No', ProdOrder."No."); + ProdOrderNotification.AddAction(OpenProdOrderActionLbl, Codeunit::"Production Definition Manager", 'OpenCreatedProductionOrder'); + ProdOrderNotification.Send(); + end; + + local procedure GetProdOrderCreatedNotificationId(): Guid + begin + exit(ProdOrderNotificationIdTok); + end; + + /// + /// Opens the production order card page that corresponds to the status stored in the notification. + /// This procedure is the action callback for the notification sent after a production order is created + /// by the Production Definition Wizard. + /// + /// The notification that carries the production order status and number as data entries. + internal procedure OpenCreatedProductionOrder(ProdOrderNotification: Notification) + var + ProdOrder: Record "Production Order"; + PageManagement: Codeunit "Page Management"; + StatusInt: Integer; + Status: Enum "Production Order Status"; + ProdOrderNo: Code[20]; + begin + Evaluate(StatusInt, ProdOrderNotification.GetData('Status')); + Status := Enum::"Production Order Status".FromInteger(StatusInt); + ProdOrderNo := CopyStr(ProdOrderNotification.GetData('No'), 1, MaxStrLen(ProdOrder."No.")); + ProdOrder.Get(Status, ProdOrderNo); + PageManagement.PageRun(ProdOrder) + end; + + [IntegrationEvent(false, false)] + local procedure OnAfterPostWizardProcessing(TempData: Codeunit "Prod. Definition Temp Data"; var ProdOrder: Record "Production Order") + begin + end; + + +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProductionDefinitionWizard.Page.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProductionDefinitionWizard.Page.al new file mode 100644 index 0000000000..300a540ced --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/ProductionDefinitionWizard.Page.al @@ -0,0 +1,776 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Inventory.Item; +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; +using Microsoft.Manufacturing.Setup; +using System.Environment; +using System.Utilities; + +page 99001021 "Production Definition Wizard" +{ + ApplicationArea = Manufacturing; + Caption = 'Production Definition Wizard'; + PageType = NavigatePage; + SourceTable = Item; + SourceTableTemporary = true; + UsageCategory = None; + + layout + { + area(Content) + { + group(StandardBanner) + { + Editable = false; + ShowCaption = false; + Visible = TopBannerVisible; + field(MediaResourcesStandardBanner; MediaResourcesStandard."Media Reference") + { + Editable = false; + ShowCaption = false; + } + } + // Step 1: Introduction + group(IntroStep) + { + ShowCaption = false; + Visible = IntroStepVisible; + + group(WelcomeGroup) + { + Caption = 'Welcome to the Production Definition Wizard'; + group(IntroductionGroup) + { + InstructionalText = 'This wizard helps you select and configure the BOM and Routing for your production. You can choose which BOM and Routing to use, and preview the components and operations before finishing.'; + ShowCaption = false; + } + } + group(GeneralGroup) + { + Caption = 'General'; + field("No."; Rec."No.") + { + Caption = 'Item No.'; + Editable = false; + ToolTip = 'Specifies the item number for which production is being defined.'; + } + field(Description; Rec.Description) + { + Caption = 'Description'; + Editable = false; + ToolTip = 'Specifies the description of the item.'; + } + group(BomRoutingTransferGroup) + { + Caption = 'BOM / Routing Details'; + field(BOMRtngFromSourceField; BOMRoutingFromSourceTxt) + { + Caption = 'Source'; + Editable = false; + ToolTip = 'Specifies the source from which BOM and Routing data was retrieved.'; + } + field(SaveBOMRoutingField; SaveBOMRouting) + { + Caption = 'Save'; + ToolTip = 'Specifies whether to apply the selected BOM and Routing back to the source record.'; + + trigger OnValidate() + begin + if not SaveBOMRouting then begin + Clear(BOMRoutingSaveTarget); + exit; + end; + + SetBOMRoutingSourceDependingOnSourceType(); + if BOMRoutingSaveTarget = BOMRoutingSaveTarget::Empty then + if BOMRoutingFromSource = BOMRoutingFromSource::StockkeepingUnit then + BOMRoutingSaveTarget := BOMRoutingSaveTarget::StockkeepingUnit + else + BOMRoutingSaveTarget := BOMRoutingSaveTarget::Item; + end; + } + field(SaveBOMRtngToSourceField; BOMRoutingSaveTarget) + { + Caption = 'Save in'; + Editable = SaveBOMRouting; + ToolTip = 'Specifies where to apply the BOM and Routing changes (Item or Stockkeeping Unit).'; + + trigger OnValidate() + var + GlobalSource: Enum "Prod. Definition Source"; + begin + if SaveBOMRouting and (BOMRoutingSaveTarget = BOMRoutingSaveTarget::Empty) then + Error(SaveBOMRtngSourceNotEmptyErr); + GlobalSource := TempData.GetGlobalSourceType(); + if (GlobalSource = BOMRoutingFromSource::Item) and (BOMRoutingSaveTarget = BOMRoutingSaveTarget::StockkeepingUnit) then + Error(SaveBOMRtngSKUNotAllowedErr); + end; + } + } + } + group(ShowEditOptionsGroup) + { + Caption = 'Step Configuration'; + Enabled = ShowEditOptionsEnabled; + + field(BOMRoutingDisplayField; BOMRoutingDisplay) + { + Caption = 'BOM/Routing'; + ToolTip = 'Specifies the display and editing behavior for BOM and Routing selection steps. Hide: Skip steps. Show: View only. Edit: Full editing.'; + + trigger OnValidate() + begin + SetBOMRoutingEditable(); + end; + } + field(ProdCompDisplayField; ProdComponentDisplay) + { + Caption = 'Prod. Components/Prod. Operations'; + ToolTip = 'Specifies the display and editing behavior for Production Order Components and Routing preview steps. Hide: Skip steps. Show: View only. Edit: Full editing.'; + } + } + } + // Step 2: BOM Selection + group(BOMStep) + { + ShowCaption = false; + Visible = BOMStepVisible; + + group(BOMSelectionGroup) + { + Caption = 'Production BOM Selection'; + InstructionalText = 'Select the Production BOM and version to use.'; + + group(BOMCreationGroup) + { + ShowCaption = false; + Visible = CreateBOMVersionVisible; + field(BOMIntroductionText; NewVersionIntroductionLbl) + { + Editable = false; + MultiLine = true; + ShowCaption = false; + Style = StandardAccent; + } + field(CreateBOMVersionField; CreateBOMVersion) + { + Caption = 'Create New BOM Version'; + ToolTip = 'Specifies whether to create a new version of the Production BOM with an automatically generated version number.'; + + trigger OnValidate() + begin + if CreateBOMVersion then begin + SelectedBOMVersion := 'TEMP-VERSION-' + CopyStr(Format(CreateGuid()), 2, 7); + ProdDefinitionVersionMgmt.ValidateBOMVersionNoSeries(SelectedBOMNo); + end else begin + SelectedBOMVersion := ''; + LoadBOMLines(); + end; + SetBOMRoutingEditable(); + TempData.UpdateBOMVersionCode(SelectedBOMVersion); + end; + } + } + field(ProductionBOMNoField; SelectedBOMNo) + { + Caption = 'Production BOM No.'; + Editable = false; + ToolTip = 'Specifies the Production BOM to use.'; + + trigger OnAssistEdit() + var + NewBOMNo: Code[20]; + begin + NewBOMNo := SelectedBOMNo; + if ProdDefinitionVersionMgmt.ShowBOMSelection(NewBOMNo) then + if NewBOMNo <> SelectedBOMNo then begin + SelectedBOMNo := NewBOMNo; + SelectedBOMVersion := ''; + CreateBOMVersion := false; + LoadBOMLines(); + SetBOMRoutingEditable(); + end; + end; + } + field(SelectedBOMVersionField; SelectedBOMVersion) + { + Caption = 'Selected BOM Version'; + Editable = false; + ToolTip = 'Specifies the version of the Production BOM to use.'; + + trigger OnAssistEdit() + begin + if ProdDefinitionVersionMgmt.ShowBOMVersionSelection(SelectedBOMNo, SelectedBOMVersion) then begin + CreateBOMVersion := false; + SetBOMRoutingEditable(); + TempData.UpdateBOMVersionCode(SelectedBOMVersion); + LoadBOMLines(); + end; + end; + } + part(BOMLinesPart; "Temp BOM Lines") + { + Caption = 'Production BOM Lines'; + Enabled = EditBOMLines; + Editable = EditBOMLines; + } + } + } + // Step 3: Routing Selection + group(RoutingStep) + { + ShowCaption = false; + Visible = RoutingStepVisible; + + group(RoutingSelectionGroup) + { + Caption = 'Routing Selection'; + InstructionalText = 'Select the Routing and version to use.'; + + group(RoutingCreationGroup) + { + ShowCaption = false; + Visible = CreateRoutingVersionVisible; + field(RoutingIntroductionText; NewVersionIntroductionLbl) + { + Editable = false; + MultiLine = true; + ShowCaption = false; + Style = StandardAccent; + } + field(CreateRoutingVersionField; CreateRoutingVersion) + { + Caption = 'Create New Routing Version'; + ToolTip = 'Specifies whether to create a new version of the Routing with an automatically generated version number.'; + + trigger OnValidate() + begin + if CreateRoutingVersion then begin + SelectedRoutingVersion := 'TEMP-VER-' + CopyStr(Format(CreateGuid()), 2, 7); + ProdDefinitionVersionMgmt.ValidateRoutingVersionNoSeries(SelectedRoutingNo); + end else begin + SelectedRoutingVersion := ''; + LoadRoutingLines(); + end; + SetBOMRoutingEditable(); + TempData.UpdateRoutingVersionCode(SelectedRoutingVersion); + end; + } + } + field(RoutingNoField; SelectedRoutingNo) + { + Caption = 'Routing No.'; + Editable = false; + ToolTip = 'Specifies the Routing to use.'; + + trigger OnAssistEdit() + var + NewRoutingNo: Code[20]; + begin + NewRoutingNo := SelectedRoutingNo; + if ProdDefinitionVersionMgmt.ShowRoutingSelection(NewRoutingNo) then + if NewRoutingNo <> SelectedRoutingNo then begin + SelectedRoutingNo := NewRoutingNo; + SelectedRoutingVersion := ''; + CreateRoutingVersion := false; + LoadRoutingLines(); + SetBOMRoutingEditable(); + end; + end; + } + field(SelectedRoutingVersionField; SelectedRoutingVersion) + { + Caption = 'Selected Routing Version'; + Editable = false; + ToolTip = 'Specifies the version of the Routing to use.'; + + trigger OnAssistEdit() + begin + if ProdDefinitionVersionMgmt.ShowRoutingVersionSelection(SelectedRoutingNo, SelectedRoutingVersion) then + LoadRoutingLines(); + end; + } + part(RoutingLinesPart; "Temp Routing Lines") + { + Caption = 'Routing Lines'; + Enabled = EditRoutingLines; + Editable = EditRoutingLines; + } + } + } + // Step 4: Components Preview (CreateProductionOrder mode only) + group(ComponentsStep) + { + ShowCaption = false; + Visible = ComponentsStepVisible; + + group(ComponentsPreviewGroup) + { + Caption = 'Components Preview'; + InstructionalText = 'Review and edit the components that will be created for the production order.'; + + part(ComponentsPart; "Temp Prod. Order Comp. List") + { + Caption = 'Components'; + Enabled = ProdComponentDisplay = ProdComponentDisplay::Edit; + Editable = ProdComponentDisplay = ProdComponentDisplay::Edit; + } + } + } + // Step 5: Production Routing Preview (CreateProductionOrder mode only) + group(ProdRoutingStep) + { + ShowCaption = false; + Visible = ProdRoutingStepVisible; + + group(ProdRoutingPreviewGroup) + { + Caption = 'Production Order Routing Preview'; + InstructionalText = 'Review and edit the routing operations that will be created for the production order.'; + + part(ProdOrderRoutingPart; "Temp Prod. Ord. Rtng List") + { + Caption = 'Prod. Order Routing Lines'; + Enabled = ProdComponentDisplay = ProdComponentDisplay::Edit; + Editable = ProdComponentDisplay = ProdComponentDisplay::Edit; + } + } + } + } + } + + actions + { + area(Processing) + { + action(ActionBack) + { + Caption = 'Back'; + Enabled = BackActionEnabled; + Image = PreviousRecord; + InFooterBar = true; + + trigger OnAction() + begin + GoBack(); + end; + } + action(ActionNext) + { + Caption = 'Next'; + Enabled = NextActionEnabled; + Image = NextRecord; + InFooterBar = true; + + trigger OnAction() + begin + GoForward(); + end; + } + action(ActionFinish) + { + Caption = 'Finish'; + Enabled = FinishActionEnabled; + Image = Approve; + InFooterBar = true; + + trigger OnAction() + begin + FinishAction(); + end; + } + } + } + + protected var + BackActionEnabled: Boolean; + FinishActionEnabled: Boolean; + NextActionEnabled: Boolean; + ShowEditOptionsEnabled: Boolean; + BOMStepVisible: Boolean; + ComponentsStepVisible: Boolean; + IntroStepVisible: Boolean; + ProdRoutingStepVisible: Boolean; + RoutingStepVisible: Boolean; + TopBannerVisible: Boolean; + CreateBOMVersion: Boolean; + CreateRoutingVersion: Boolean; + CreateBOMVersionVisible: Boolean; + CreateRoutingVersionVisible: Boolean; + EditBOMLines: Boolean; + EditRoutingLines: Boolean; + SaveBOMRouting: Boolean; + SelectedBOMNo: Code[20]; + SelectedBOMVersion: Code[20]; + SelectedRoutingNo: Code[20]; + SelectedRoutingVersion: Code[20]; + BOMRoutingSaveTarget: Enum "Prod. Definition Save Target"; + BOMRoutingDisplay: Enum "Prod. Definition Display"; + ProdComponentDisplay: Enum "Prod. Definition Display"; + Step: Option Intro,BOM,Routing,Components,ProdRouting; + + var + MediaRepositoryStandard: Record "Media Repository"; + MediaResourcesStandard: Record "Media Resources"; + TempData: Codeunit "Prod. Definition Temp Data"; + ProdDefinitionVersionMgmt: Codeunit "Prod. Definition Version Mgmt."; + Finished: Boolean; + BOMRoutingFromSource: Enum "Prod. Definition Source"; + BOMRoutingFromSourceTxt: Text; + WizardMode: Enum "Prod. Definition Mode"; + NewVersionIntroductionLbl: Label 'Turn on "Create New Version" to make the lines below editable. Note that existing versions cannot be modified — the wizard always creates a new version. It assigns a temporary version code and lets you modify the lines freely. When you finish the wizard and the changes are saved, the temporary code is replaced by a number from the version number series and the version is certified automatically.'; + SaveBOMRtngSourceNotEmptyErr: Label 'Please select a valid source for saving BOM and Routing changes.'; + SaveBOMRtngSKUNotAllowedErr: Label 'Stockkeeping Unit is not allowed when the source is Item.'; + + trigger OnInit() + begin + LoadTopBanners(); + end; + + trigger OnOpenPage() + begin + SetBOMDataReference(); + SetRoutingDataReference(); + SetProdOrderDataReference(); + InitBomRoutingSource(); + + Step := Step::Intro; + EnableControls(); + SetBOMRoutingEditable(); + SetShowEditOptionsEnabled(); + + if Rec.FindFirst() then; + end; + + /// + /// Sets the item record that the wizard operates on. + /// + /// The item to bind the wizard page to. + internal procedure SetItem(Item: Record Item) + begin + Rec.Reset(); + Rec.DeleteAll(); + Rec := Item; + Rec.Insert(); + end; + + /// + /// Sets the display mode for the BOM and routing section of the wizard. + /// + /// The display type controlling which BOM/routing tab is shown. + internal procedure SetBOMRoutingDisplay(DisplayType: Enum "Prod. Definition Display") + begin + BOMRoutingDisplay := DisplayType; + end; + + /// + /// Sets the display mode for the production order components section of the wizard. + /// + /// The display type controlling which component tab is shown. + internal procedure SetProdCompDisplay(DisplayType: Enum "Prod. Definition Display") + begin + ProdComponentDisplay := DisplayType; + end; + + /// + /// Injects the temporary data codeunit that the wizard uses for BOM, routing, and production order data. + /// + /// The temporary data codeunit to use. + internal procedure SetTempData(var NewTempData: Codeunit "Prod. Definition Temp Data") + begin + TempData := NewTempData; + end; + + /// + /// Sets the operating mode of the wizard (e.g., define item structure or create production order). + /// + /// The wizard mode to apply. + internal procedure SetWizardMode(Mode: Enum "Prod. Definition Mode") + begin + WizardMode := Mode; + end; + + /// + /// Returns the user's choice for where to apply the BOM and routing changes. + /// + /// The source type selected by the user indicating where to save BOM/routing changes. + internal procedure GetBOMRtngSaveTarget(): Enum "Prod. Definition Save Target" + begin + exit(BOMRoutingSaveTarget); + end; + + /// + /// Returns whether the wizard has been completed by the user. + /// + /// True if the user finished the wizard; otherwise false. + internal procedure GetFinished(): Boolean + begin + exit(Finished); + end; + + local procedure LoadBOMLines() + begin + if SelectedBOMNo = '' then + exit; + if not ProdDefinitionVersionMgmt.CheckBOMExists(SelectedBOMNo, SelectedBOMVersion) then + exit; + TempData.LoadBOMLines(SelectedBOMNo, SelectedBOMVersion); + end; + + local procedure LoadRoutingLines() + begin + if SelectedRoutingNo = '' then + exit; + if not ProdDefinitionVersionMgmt.CheckRoutingExists(SelectedRoutingNo, SelectedRoutingVersion) then + exit; + TempData.LoadRoutingLines(SelectedRoutingNo, SelectedRoutingVersion); + end; + + local procedure LoadTopBanners() + begin + if MediaRepositoryStandard.Get('AssistedSetup-NoText-400px.png', Format(CurrentClientType())) then + if MediaResourcesStandard.Get(MediaRepositoryStandard."Media Resources Ref") then + TopBannerVisible := MediaResourcesStandard."Media Reference".HasValue(); + end; + + local procedure SetBOMDataReference() + var + TempBOMLines: Record "Production BOM Line" temporary; + TempBOMHeader: Record "Production BOM Header" temporary; + begin + TempData.GetGlobalBOMLines(TempBOMLines); + CurrPage.BOMLinesPart.Page.SetTemporaryRecords(TempBOMLines); + if TempBOMLines.FindFirst() then begin + SelectedBOMNo := TempBOMLines."Production BOM No."; + SelectedBOMVersion := TempBOMLines."Version Code"; + end else begin + TempData.GetGlobalBOMHeader(TempBOMHeader); + if TempBOMHeader.FindFirst() then begin + SelectedBOMNo := TempBOMHeader."No."; + SelectedBOMVersion := ''; + end; + end; + end; + + local procedure SetRoutingDataReference() + var + TempRoutingLine: Record "Routing Line" temporary; + TempRoutingHeader: Record "Routing Header" temporary; + begin + TempData.GetGlobalRoutingLines(TempRoutingLine); + CurrPage.RoutingLinesPart.Page.SetTemporaryRecords(TempRoutingLine); + if TempRoutingLine.FindFirst() then begin + SelectedRoutingNo := TempRoutingLine."Routing No."; + SelectedRoutingVersion := TempRoutingLine."Version Code"; + end else begin + TempData.GetGlobalRoutingHeader(TempRoutingHeader); + if TempRoutingHeader.FindFirst() then begin + SelectedRoutingNo := TempRoutingHeader."No."; + SelectedRoutingVersion := ''; + end; + end; + end; + + local procedure SetProdOrderDataReference() + var + TempProdOrderComponent: Record "Prod. Order Component" temporary; + TempProdOrderRoutingLine: Record "Prod. Order Routing Line" temporary; + begin + TempData.GetGlobalProdOrderComponent(TempProdOrderComponent); + CurrPage.ComponentsPart.Page.SetTempProdOrderComponent(TempProdOrderComponent); + TempData.GetGlobalProdOrderRoutingLine(TempProdOrderRoutingLine); + CurrPage.ProdOrderRoutingPart.Page.SetTempProdOrdRtngLine(TempProdOrderRoutingLine); + end; + + local procedure SetBOMRoutingEditable() + begin + EditBOMLines := not ProdDefinitionVersionMgmt.CheckBOMExists(SelectedBOMNo, SelectedBOMVersion) and + (BOMRoutingDisplay = BOMRoutingDisplay::Edit); + EditRoutingLines := not ProdDefinitionVersionMgmt.CheckRoutingExists(SelectedRoutingNo, SelectedRoutingVersion) and + (BOMRoutingDisplay = BOMRoutingDisplay::Edit); + CreateBOMVersionVisible := ProdDefinitionVersionMgmt.CheckBOMExists(SelectedBOMNo, '') and + (BOMRoutingDisplay = BOMRoutingDisplay::Edit); + CreateRoutingVersionVisible := ProdDefinitionVersionMgmt.CheckRoutingExists(SelectedRoutingNo, '') and + (BOMRoutingDisplay = BOMRoutingDisplay::Edit); + end; + + local procedure SetShowEditOptionsEnabled() + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.SetLoadFields("Allow Edit UI Selection"); + ManufacturingSetup.Get(); + ShowEditOptionsEnabled := ManufacturingSetup."Allow Edit UI Selection"; + end; + + local procedure IsCreateProductionOrderMode(): Boolean + begin + exit(WizardMode = WizardMode::CreateProductionOrder); + end; + + local procedure GoBack() + begin + NextStep(true); + end; + + local procedure GoForward() + begin + NextStep(false); + end; + + local procedure NextStep(Backwards: Boolean) + begin + repeat + if Backwards then + Step := Step - 1 + else + Step := Step + 1; + + if Step < Step::Intro then begin + Step := Step::Intro; + break; + end; + if Step > Step::ProdRouting then begin + Step := Step::ProdRouting; + break; + end; + until not IsStepHidden(Step); + + if not Backwards and (Step = Step::Components) then + TempData.BuildTemporaryStructureFromBOMRouting(); + + EnableControls(); + end; + + local procedure IsStepHidden(CurrentStep: Option): Boolean + begin + case CurrentStep of + Step::BOM, Step::Routing: + exit(BOMRoutingDisplay = BOMRoutingDisplay::Hide); + Step::Components, Step::ProdRouting: + exit((ProdComponentDisplay = ProdComponentDisplay::Hide) or not IsCreateProductionOrderMode()); + end; + exit(false); + end; + + local procedure EnableControls() + begin + ResetControls(); + case Step of + Step::Intro: + ShowIntroStep(); + Step::BOM: + ShowBOMStep(); + Step::Routing: + ShowRoutingStep(); + Step::Components: + ShowComponentsStep(); + Step::ProdRouting: + ShowProdRoutingStep(); + end; + end; + + local procedure ResetControls() + begin + IntroStepVisible := false; + BOMStepVisible := false; + RoutingStepVisible := false; + ComponentsStepVisible := false; + ProdRoutingStepVisible := false; + BackActionEnabled := true; + NextActionEnabled := true; + FinishActionEnabled := false; + end; + + local procedure ShowIntroStep() + begin + IntroStepVisible := true; + BackActionEnabled := false; + if (BOMRoutingDisplay = BOMRoutingDisplay::Hide) and + (not IsCreateProductionOrderMode() or (ProdComponentDisplay = ProdComponentDisplay::Hide)) then begin + NextActionEnabled := false; + FinishActionEnabled := true; + end; + end; + + local procedure ShowBOMStep() + begin + BOMStepVisible := true; + end; + + local procedure ShowRoutingStep() + begin + RoutingStepVisible := true; + if not IsCreateProductionOrderMode() or (ProdComponentDisplay = ProdComponentDisplay::Hide) then begin + NextActionEnabled := false; + FinishActionEnabled := true; + end; + end; + + local procedure ShowComponentsStep() + begin + ComponentsStepVisible := true; + end; + + local procedure ShowProdRoutingStep() + begin + ProdRoutingStepVisible := true; + NextActionEnabled := false; + FinishActionEnabled := true; + end; + + local procedure FinishAction() + var + SaveTargetRequiredErr: Label 'Please select a target for saving BOM and Routing changes (Item or Stockkeeping Unit) before finishing.'; + begin + if SaveBOMRouting and (BOMRoutingSaveTarget = BOMRoutingSaveTarget::Empty) then + Error(SaveTargetRequiredErr); + Finished := true; + CurrPage.Close(); + end; + + local procedure InitBomRoutingSource() + var + ManufacturingSetup: Record "Manufacturing Setup"; + GlobalSource: Enum "Prod. Definition Source"; + begin + BOMRoutingFromSource := TempData.GetRtngBOMSourceType(); + if BOMRoutingFromSource = BOMRoutingFromSource::Empty then + BOMRoutingFromSourceTxt := ManufacturingSetup.TableCaption() + else + BOMRoutingFromSourceTxt := Format(BOMRoutingFromSource); + + GlobalSource := TempData.GetGlobalSourceType(); + case GlobalSource of + GlobalSource::Item: + begin + SaveBOMRouting := true; + BOMRoutingSaveTarget := BOMRoutingSaveTarget::Item; + end; + GlobalSource::StockkeepingUnit: + begin + SaveBOMRouting := true; + BOMRoutingSaveTarget := BOMRoutingSaveTarget::StockkeepingUnit; + end; + end; + end; + + local procedure SetBOMRoutingSourceDependingOnSourceType() + var + GlobalSource: Enum "Prod. Definition Source"; + begin + GlobalSource := TempData.GetGlobalSourceType(); + case GlobalSource of + GlobalSource::Item: + BOMRoutingSaveTarget := BOMRoutingSaveTarget::Item; + GlobalSource::StockkeepingUnit: + BOMRoutingSaveTarget := BOMRoutingSaveTarget::StockkeepingUnit; + end; + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempBOMLines.Page.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempBOMLines.Page.al new file mode 100644 index 0000000000..68489889a2 --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempBOMLines.Page.al @@ -0,0 +1,179 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Manufacturing.ProductionBOM; +page 99001029 "Temp BOM Lines" +{ + PageType = ListPart; + SourceTable = "Production BOM Line"; + SourceTableTemporary = true; + Caption = 'Production BOM Lines'; + AutoSplitKey = true; + DelayedInsert = true; + + layout + { + area(Content) + { + repeater(Lines) + { + field(Type; Rec.Type) + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the type of the BOM line (Item or Production BOM).'; + } + field("No."; Rec."No.") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the number of the component item or sub-assembly.'; + } + field("Variant Code"; Rec."Variant Code") + { + ApplicationArea = Planning; + Visible = false; + ToolTip = 'Specifies the variant of the item on the BOM line.'; + } + field(Description; Rec.Description) + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies a description of the BOM line.'; + } + field("Description 2"; Rec."Description 2") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies additional description of the BOM line.'; + } + field("Calculation Formula"; Rec."Calculation Formula") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies how to calculate the Quantity per field.'; + } + field(Length; Rec.Length) + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the length of the component.'; + } + field(Width; Rec.Width) + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the width of the component.'; + } + field(Depth; Rec.Depth) + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the depth of the component.'; + } + field(Weight; Rec.Weight) + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the weight of the component.'; + } + field("Quantity per"; Rec."Quantity per") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies how many units of the component are required per unit of the parent item.'; + } + field("Unit of Measure Code"; Rec."Unit of Measure Code") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the unit of measure for the BOM line.'; + } + field("Scrap %"; Rec."Scrap %") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the scrap percentage for the component.'; + } + field("Routing Link Code"; Rec."Routing Link Code") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the routing link code to link this component to a routing operation.'; + } + field(Position; Rec.Position) + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the position of the component.'; + } + field("Position 2"; Rec."Position 2") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the second position reference of the component.'; + } + field("Position 3"; Rec."Position 3") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the third position reference of the component.'; + } + field("Lead-Time Offset"; Rec."Lead-Time Offset") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the lead-time offset for the component.'; + } + field("Starting Date"; Rec."Starting Date") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the date from which the BOM line is valid.'; + } + field("Ending Date"; Rec."Ending Date") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the last date on which the BOM line is valid.'; + } + } + } + } + + var + LinesChanged: Boolean; + + trigger OnInsertRecord(BelowxRec: Boolean): Boolean + begin + LinesChanged := true; + exit(true); + end; + + trigger OnModifyRecord(): Boolean + begin + LinesChanged := true; + exit(true); + end; + + trigger OnDeleteRecord(): Boolean + begin + LinesChanged := true; + exit(true); + end; + + /// + /// Binds the page list to the provided temporary BOM line records. + /// + /// The temporary BOM lines to display and edit on the page. + internal procedure SetTemporaryRecords(var TempBOMLine: Record "Production BOM Line" temporary) + begin + Rec.Copy(TempBOMLine, true); + CurrPage.Update(false); + end; + + /// + /// Returns the current temporary BOM line records from the page by copying them into the provided variable. + /// + /// The variable to receive the temporary BOM lines. + internal procedure GetTemporaryRecords(var TempBOMLine: Record "Production BOM Line" temporary) + begin + TempBOMLine.Copy(Rec, true); + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempProdOrdRtngList.Page.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempProdOrdRtngList.Page.al new file mode 100644 index 0000000000..6f43bfc9c6 --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempProdOrdRtngList.Page.al @@ -0,0 +1,341 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.Setup; + +page 99001020 "Temp Prod. Ord. Rtng List" +{ + PageType = ListPart; + SourceTable = "Prod. Order Routing Line"; + SourceTableTemporary = true; + Caption = 'Production Order Routing Lines'; + DelayedInsert = true; + + layout + { + area(Content) + { + repeater(Lines) + { + field(Status; Rec.Status) + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the status of the production order to which the routing list belongs.'; + Editable = false; + Visible = false; + } + field("Prod. Order No."; Rec."Prod. Order No.") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the production order number.'; + Editable = false; + Visible = false; + } + field("Routing No."; Rec."Routing No.") + { + ApplicationArea = Manufacturing; + Editable = false; + Visible = false; + ToolTip = 'Specifies the routing number.'; + } + field("Routing Reference No."; Rec."Routing Reference No.") + { + ApplicationArea = Manufacturing; + Editable = false; + Visible = false; + ToolTip = 'Specifies the routing reference number.'; + } + field("Schedule Manually"; Rec."Schedule Manually") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies whether to schedule the operation manually.'; + } + field("Operation No."; Rec."Operation No.") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the operation number for the routing line.'; + } + field("Previous Operation No."; Rec."Previous Operation No.") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the previous operation number in the routing.'; + } + field("Next Operation No."; Rec."Next Operation No.") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the next operation number in the routing.'; + } + field(Type; Rec.Type) + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the type of the routing operation (Work Center or Machine Center).'; + } + field("No."; Rec."No.") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the number of the work center or machine center.'; + } + field(Description; Rec.Description) + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies a description of the routing operation.'; + } + field("Description 2"; Rec."Description 2") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies additional description of the routing operation.'; + } + field("Flushing Method"; Rec."Flushing Method") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies how consumption is flushed for this operation.'; + } + field("Starting Date-Time"; Rec."Starting Date-Time") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the starting date and time of the operation.'; + } + field("Starting Time"; Rec."Starting Time") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the starting time of the operation.'; + } + field("Starting Date"; Rec."Starting Date") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the starting date of the operation.'; + } + field("Ending Date-Time"; Rec."Ending Date-Time") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the ending date and time of the operation.'; + } + field("Ending Time"; Rec."Ending Time") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the ending time of the operation.'; + } + field("Ending Date"; Rec."Ending Date") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the ending date of the operation.'; + } + field("Setup Time"; Rec."Setup Time") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the setup time for the operation.'; + } + field("Setup Time Unit of Meas. Code"; Rec."Setup Time Unit of Meas. Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the unit of measure code that applies to the setup time of the operation.'; + } + field("Run Time"; Rec."Run Time") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the run time per unit for the operation.'; + } + field("Run Time Unit of Meas. Code"; Rec."Run Time Unit of Meas. Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the unit of measure code that applies to the run time of the operation.'; + } + field("Wait Time"; Rec."Wait Time") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the wait time after the operation.'; + } + field("Wait Time Unit of Meas. Code"; Rec."Wait Time Unit of Meas. Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the unit of measure code for the wait time.'; + } + field("Move Time"; Rec."Move Time") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the move time after the operation.'; + } + field("Move Time Unit of Meas. Code"; Rec."Move Time Unit of Meas. Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the unit of measure code for the move time.'; + } + field("Fixed Scrap Quantity"; Rec."Fixed Scrap Quantity") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the fixed scrap quantity for the operation.'; + } + field("Routing Link Code"; Rec."Routing Link Code") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the routing link code to connect this operation to BOM components.'; + } + field("Scrap Factor %"; Rec."Scrap Factor %") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the scrap factor percentage for the operation.'; + } + field("Send-Ahead Quantity"; Rec."Send-Ahead Quantity") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the send-ahead quantity for the operation.'; + } + field("Concurrent Capacities"; Rec."Concurrent Capacities") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the number of concurrent capacities for the operation.'; + } + field("Unit Cost per"; Rec."Unit Cost per") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the unit cost per for the operation.'; + } + field("Lot Size"; Rec."Lot Size") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the number of items that are included in the same operation at the same time.'; + } + field("Expected Operation Cost Amt."; Rec."Expected Operation Cost Amt.") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the expected operation cost amount.'; + } + field("Expected Capacity Ovhd. Cost"; Rec."Expected Capacity Ovhd. Cost") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the expected capacity overhead cost.'; + } + field("Expected Capacity Need"; Rec."Expected Capacity Need") + { + ApplicationArea = Manufacturing; + AutoFormatType = 0; + DecimalPlaces = 0 : 5; + Visible = false; + ToolTip = 'Specifies the expected capacity need for the production order.'; + } + field("Routing Status"; Rec."Routing Status") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the routing status of the operation.'; + } + field("Location Code"; Rec."Location Code") + { + ApplicationArea = Location; + Visible = false; + ToolTip = 'Specifies the location code for the operation.'; + } + field("Open Shop Floor Bin Code"; Rec."Open Shop Floor Bin Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the open shop floor bin code.'; + } + field("To-Production Bin Code"; Rec."To-Production Bin Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the to-production bin code.'; + } + field("From-Production Bin Code"; Rec."From-Production Bin Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the from-production bin code.'; + } + field("Posted Output Quantity"; Rec."Posted Output Quantity") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the posted output quantity for the operation.'; + } + field("Posted Scrap Quantity"; Rec."Posted Scrap Quantity") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the posted scrap quantity for the operation.'; + } + field("Posted Run Time"; Rec."Posted Run Time") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the posted run time for the operation.'; + } + field("Posted Setup Time"; Rec."Posted Setup Time") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the posted setup time for the operation.'; + } + } + } + } + trigger OnDeleteRecord(): Boolean + begin + Rec.CheckPreviousAndNextForTemp(); + end; + + trigger OnNewRecord(BelowxRec: Boolean) + begin + Rec."Prod. Order No." := xRec."Prod. Order No."; + Rec."Routing Reference No." := xRec."Routing Reference No."; + Rec."Routing No." := xRec."Routing No."; + Rec.Status := xRec.Status; + + GetManufacturingSetup(); + Rec."Flushing Method" := ManufacturingSetup."Def. Wiz. Flushing Method"; + end; + + trigger OnOpenPage() + begin + if Rec.FindFirst() then; + end; + + var + ManufacturingSetup: Record "Manufacturing Setup"; + ManufacturingSetupRead: Boolean; + + /// + /// Binds the page list to the provided temporary production order routing line records. + /// + /// The temporary production order routing lines to display and edit on the page. + internal procedure SetTempProdOrdRtngLine(var TempProdOrdRtngLine: Record "Prod. Order Routing Line" temporary) + begin + Rec.Copy(TempProdOrdRtngLine, true); + CurrPage.Update(false); + end; + + local procedure GetManufacturingSetup() + begin + if not ManufacturingSetupRead then begin + ManufacturingSetup.SetLoadFields("Def. Wiz. Flushing method"); + ManufacturingSetup.Get(); + ManufacturingSetupRead := true; + end; + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempProdOrderCompList.Page.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempProdOrderCompList.Page.al new file mode 100644 index 0000000000..c9b9527320 --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempProdOrderCompList.Page.al @@ -0,0 +1,159 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Manufacturing.Document; + +page 99001019 "Temp Prod. Order Comp. List" +{ + PageType = ListPart; + SourceTable = "Prod. Order Component"; + SourceTableTemporary = true; + Caption = 'Production Order Components'; + AutoSplitKey = true; + DelayedInsert = true; + + layout + { + area(Content) + { + repeater(Lines) + { + field("Item No."; Rec."Item No.") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the item number of the component.'; + } + field("Variant Code"; Rec."Variant Code") + { + ApplicationArea = Planning; + Visible = false; + ToolTip = 'Specifies the variant of the component item.'; + } + field("Due Date-Time"; Rec."Due Date-Time") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the due date and time for the component.'; + } + field("Due Date"; Rec."Due Date") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the due date for the component.'; + } + field(Description; Rec.Description) + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies a description of the component.'; + } + field("Description 2"; Rec."Description 2") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies additional description of the component.'; + } + field("Scrap %"; Rec."Scrap %") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the scrap percentage for the component.'; + } + field("Calculation Formula"; Rec."Calculation Formula") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies how to calculate the Quantity per field.'; + } + field(Length; Rec.Length) + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the length of the component.'; + } + field(Width; Rec.Width) + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the width of the component.'; + } + field(Weight; Rec.Weight) + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the weight of the component.'; + } + field(Depth; Rec.Depth) + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the depth of the component.'; + } + field("Quantity per"; Rec."Quantity per") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies how many units of the component are required to produce one unit of the parent item.'; + } + field("Reserved Quantity"; Rec."Reserved Quantity") + { + ApplicationArea = Reservation; + Visible = false; + ToolTip = 'Specifies the quantity reserved for the component.'; + } + field("Unit of Measure Code"; Rec."Unit of Measure Code") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the unit of measure code for the component.'; + } + field("Flushing Method"; Rec."Flushing Method") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies how consumption of the component is calculated and handled.'; + } + field("Expected Quantity"; Rec."Expected Quantity") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the expected quantity of the component.'; + } + field("Remaining Quantity"; Rec."Remaining Quantity") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the remaining quantity of the component.'; + } + field("Routing Link Code"; Rec."Routing Link Code") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies a routing link code to link the component with a specific operation.'; + } + field("Location Code"; Rec."Location Code") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the location from which components are consumed.'; + } + } + } + } + trigger OnNewRecord(BelowxRec: Boolean) + begin + Rec."Prod. Order No." := xRec."Prod. Order No."; + Rec."Prod. Order Line No." := xRec."Prod. Order Line No."; + Rec.Status := xRec.Status; + Rec."Location Code" := xRec."Location Code"; + end; + + trigger OnOpenPage() + begin + if Rec.FindFirst() then; + end; + + /// + /// Binds the page list to the provided temporary production order component records. + /// + /// The temporary production order components to display and edit on the page. + internal procedure SetTempProdOrderComponent(var TempProdOrderComponent: Record "Prod. Order Component" temporary) + begin + Rec.Copy(TempProdOrderComponent, true); + CurrPage.Update(false); + end; +} \ No newline at end of file diff --git a/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempRoutingLines.Page.al b/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempRoutingLines.Page.al new file mode 100644 index 0000000000..aef9c2fb7d --- /dev/null +++ b/src/Layers/W1/BaseApp/Manufacturing/Wizard/TempRoutingLines.Page.al @@ -0,0 +1,191 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Wizard; + +using Microsoft.Manufacturing.Routing; + +page 99001030 "Temp Routing Lines" +{ + PageType = ListPart; + SourceTable = "Routing Line"; + SourceTableTemporary = true; + Caption = 'Routing Lines'; + DelayedInsert = true; + + layout + { + area(Content) + { + repeater(Lines) + { + field("Operation No."; Rec."Operation No.") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the operation number for the routing line.'; + } + field("Previous Operation No."; Rec."Previous Operation No.") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the previous operation number in the routing.'; + } + field("Next Operation No."; Rec."Next Operation No.") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the next operation number in the routing.'; + } + field(Type; Rec.Type) + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the type of the routing operation (Work Center or Machine Center).'; + } + field("No."; Rec."No.") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the number of the work center or machine center.'; + } + field("Standard Task Code"; Rec."Standard Task Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the standard task code for the operation.'; + } + field("Routing Link Code"; Rec."Routing Link Code") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the routing link code to connect this operation to BOM components.'; + } + field(Description; Rec.Description) + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies a description of the routing operation.'; + } + field("Description 2"; Rec."Description 2") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies additional description of the routing operation.'; + } + field("Setup Time"; Rec."Setup Time") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the setup time for the operation.'; + } + field("Setup Time Unit of Meas. Code"; Rec."Setup Time Unit of Meas. Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the unit of measure for the setup time.'; + } + field("Run Time"; Rec."Run Time") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the run time per unit for the operation.'; + } + field("Run Time Unit of Meas. Code"; Rec."Run Time Unit of Meas. Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the unit of measure for the run time.'; + } + field("Wait Time"; Rec."Wait Time") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the wait time after the operation.'; + } + field("Wait Time Unit of Meas. Code"; Rec."Wait Time Unit of Meas. Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the unit of measure for the wait time.'; + } + field("Move Time"; Rec."Move Time") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the move time after the operation.'; + } + field("Move Time Unit of Meas. Code"; Rec."Move Time Unit of Meas. Code") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the unit of measure for the move time.'; + } + field("Fixed Scrap Quantity"; Rec."Fixed Scrap Quantity") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the fixed scrap quantity for the operation.'; + } + field("Scrap Factor %"; Rec."Scrap Factor %") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the scrap factor percentage for the operation.'; + } + field("Minimum Process Time"; Rec."Minimum Process Time") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the minimum process time for the operation.'; + } + field("Maximum Process Time"; Rec."Maximum Process Time") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the maximum process time for the operation.'; + } + field("Concurrent Capacities"; Rec."Concurrent Capacities") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the number of concurrent capacities for the operation.'; + } + field("Send-Ahead Quantity"; Rec."Send-Ahead Quantity") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the send-ahead quantity for the operation.'; + } + field("Unit Cost per"; Rec."Unit Cost per") + { + ApplicationArea = Manufacturing; + ToolTip = 'Specifies the unit cost per for the operation.'; + } + field("Lot Size"; Rec."Lot Size") + { + ApplicationArea = Manufacturing; + Visible = false; + ToolTip = 'Specifies the lot size for the operation.'; + } + } + } + } + trigger OnDeleteRecord(): Boolean + begin + Rec.CheckPreviousAndNextForTemp(); + end; + + trigger OnNewRecord(BelowxRec: Boolean) + begin + Rec."Routing No." := xRec."Routing No."; + Rec."Version Code" := xRec."Version Code"; + end; + + /// + /// Binds the page list to the provided temporary routing line records. + /// + /// The temporary routing lines to display and edit on the page. + internal procedure SetTemporaryRecords(var TempRoutingLine: Record "Routing Line" temporary) + begin + Rec.Copy(TempRoutingLine, true); + CurrPage.Update(false); + end; + + /// + /// Returns the current temporary routing line records from the page by copying them into the provided variable. + /// + /// The variable to receive the temporary routing lines. + internal procedure GetTemporaryRecords(var TempRoutingLine: Record "Routing Line" temporary) + begin + TempRoutingLine.Copy(Rec, true); + end; +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/ProdDefWizCheckLib.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/ProdDefWizCheckLib.Codeunit.al new file mode 100644 index 0000000000..febb16dd02 --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/ProdDefWizCheckLib.Codeunit.al @@ -0,0 +1,550 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Location; +using Microsoft.Inventory.Tracking; +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.Setup; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; +using Microsoft.Sales.Document; + +codeunit 137422 "Prod. Def. Wiz. Check Lib." +{ + var + Assert: Codeunit Assert; + ItemShouldHaveBOMLbl: Label 'Item %1 should have BOM No. = %2', Locked = true; + ItemShouldHaveRoutingLbl: Label 'Item %1 should have Routing No. = %2', Locked = true; + SKUShouldHaveBOMLbl: Label 'SKU (%1/%2/%3) should have BOM No. = %4', Locked = true; + ProdOrderShouldExistLbl: Label 'Production Order should exist for item %1', Locked = true; + ProdOrderShouldNotExistLbl: Label 'No Production Order should exist for item %1', Locked = true; + ProdOrderFieldMismatchLbl: Label 'Production Order field %1: expected %2, got %3', Locked = true; + ComponentCountMismatchLbl: Label 'Expected %1 Prod. Order components but found %2', Locked = true; + RoutingLineCountMismatchLbl: Label 'Expected %1 Prod. Order routing lines but found %2', Locked = true; + BOMVersionShouldBeCertifiedLbl: Label 'BOM %1 version %2 should be certified', Locked = true; + BOMVersionShouldNotExistLbl: Label 'BOM %1 version %2 should not exist', Locked = true; + RoutingVersionShouldBeCertifiedLbl: Label 'Routing %1 version %2 should be certified', Locked = true; + RoutingVersionShouldNotExistLbl: Label 'Routing Version should not exist for Routing %1', Locked = true; + BOMVersionShouldBeNewLbl: Label 'BOM %1 should have a new version after the wizard; previous: %2, last is still: %3', Locked = true; + RoutingVersionShouldBeNewLbl: Label 'Routing %1 should have a new version after the wizard; previous: %2, last is still: %3', Locked = true; + ReservationShouldExistLbl: Label 'Reservation Entry should exist for Sales Line %1/%2', Locked = true; + ReservationShouldNotExistLbl: Label 'Reservation Entry should NOT exist for Sales Line %1/%2', Locked = true; + ReservationShouldLinkProdOrderLbl: Label 'Reservation Entry should exist linking Sales Line %1/%2 to Production Order %3', Locked = true; + + procedure VerifyItemHasBOM(ItemNo: Code[20]; ExpectedBOMNo: Code[20]) + var + Item: Record Item; + begin + Item.Get(ItemNo); + Assert.AreEqual(ExpectedBOMNo, Item."Production BOM No.", + StrSubstNo(ItemShouldHaveBOMLbl, ItemNo, ExpectedBOMNo)); + end; + + procedure VerifyItemHasRouting(ItemNo: Code[20]; ExpectedRoutingNo: Code[20]) + var + Item: Record Item; + begin + Item.Get(ItemNo); + Assert.AreEqual(ExpectedRoutingNo, Item."Routing No.", + StrSubstNo(ItemShouldHaveRoutingLbl, ItemNo, ExpectedRoutingNo)); + end; + + procedure VerifyItemBOMUnchanged(ItemNo: Code[20]; OriginalBOMNo: Code[20]) + var + Item: Record Item; + begin + Item.Get(ItemNo); + Assert.AreEqual(OriginalBOMNo, Item."Production BOM No.", + StrSubstNo(ItemShouldHaveBOMLbl, ItemNo, OriginalBOMNo)); + end; + + procedure VerifyItemRoutingUnchanged(ItemNo: Code[20]; OriginalRoutingNo: Code[20]) + var + Item: Record Item; + begin + Item.Get(ItemNo); + Assert.AreEqual(OriginalRoutingNo, Item."Routing No.", + StrSubstNo(ItemShouldHaveRoutingLbl, ItemNo, OriginalRoutingNo)); + end; + + procedure VerifyItemHasAnyBOM(ItemNo: Code[20]) + var + Item: Record Item; + begin + Item.Get(ItemNo); + Assert.AreNotEqual('', Item."Production BOM No.", + StrSubstNo(ItemShouldHaveBOMLbl, ItemNo, '')); + end; + + procedure VerifyItemHasAnyRouting(ItemNo: Code[20]) + var + Item: Record Item; + begin + Item.Get(ItemNo); + Assert.AreNotEqual('', Item."Routing No.", + StrSubstNo(ItemShouldHaveRoutingLbl, ItemNo, '')); + end; + + procedure VerifySKUHasBOM(ItemNo: Code[20]; LocationCode: Code[10]; VariantCode: Code[10]; ExpectedBOMNo: Code[20]) + var + SKU: Record "Stockkeeping Unit"; + begin + SKU.Get(LocationCode, ItemNo, VariantCode); + Assert.AreEqual(ExpectedBOMNo, SKU."Production BOM No.", + StrSubstNo(SKUShouldHaveBOMLbl, ItemNo, LocationCode, VariantCode, ExpectedBOMNo)); + end; + + procedure VerifyNoProdOrderForItem(ItemNo: Code[20]) + var + ProdOrderLine: Record "Prod. Order Line"; + begin + ProdOrderLine.SetRange("Item No.", ItemNo); + Assert.IsTrue(ProdOrderLine.IsEmpty(), + StrSubstNo(ProdOrderShouldNotExistLbl, ItemNo)); + end; + + procedure VerifyProdOrderExists(ItemNo: Code[20]; var ProdOrder: Record "Production Order") + var + ProdOrderLine: Record "Prod. Order Line"; + begin + ProdOrderLine.SetRange("Item No.", ItemNo); + Assert.IsTrue(ProdOrderLine.FindFirst(), + StrSubstNo(ProdOrderShouldExistLbl, ItemNo)); + ProdOrder.Get(ProdOrderLine.Status, ProdOrderLine."Prod. Order No."); + end; + + procedure VerifyProdOrderFields(ProdOrder: Record "Production Order"; ItemNo: Code[20]; Qty: Decimal; DueDate: Date; LocationCode: Code[10]; VariantCode: Code[10]) + var + ProdOrderLine: Record "Prod. Order Line"; + begin + ProdOrderLine.SetRange(Status, ProdOrder.Status); + ProdOrderLine.SetRange("Prod. Order No.", ProdOrder."No."); + ProdOrderLine.SetRange("Item No.", ItemNo); + Assert.IsTrue(ProdOrderLine.FindFirst(), + StrSubstNo(ProdOrderShouldExistLbl, ItemNo)); + + Assert.AreEqual(ItemNo, ProdOrderLine."Item No.", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Item No.', ItemNo, ProdOrderLine."Item No.")); + Assert.AreNearlyEqual(Qty, ProdOrderLine.Quantity, 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Quantity', Qty, ProdOrderLine.Quantity)); + if DueDate <> 0D then + Assert.AreEqual(DueDate, ProdOrderLine."Due Date", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Due Date', DueDate, ProdOrderLine."Due Date")); + Assert.AreEqual(LocationCode, ProdOrderLine."Location Code", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Location Code', LocationCode, ProdOrderLine."Location Code")); + Assert.AreEqual(VariantCode, ProdOrderLine."Variant Code", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Variant Code', VariantCode, ProdOrderLine."Variant Code")); + end; + + procedure VerifyProdOrderHasComponentCount(ProdOrder: Record "Production Order"; ExpectedCount: Integer) + var + ProdOrderComponent: Record "Prod. Order Component"; + ActualCount: Integer; + begin + ProdOrderComponent.SetRange(Status, ProdOrder.Status); + ProdOrderComponent.SetRange("Prod. Order No.", ProdOrder."No."); + ActualCount := ProdOrderComponent.Count(); + Assert.AreEqual(ExpectedCount, ActualCount, + StrSubstNo(ComponentCountMismatchLbl, ExpectedCount, ActualCount)); + end; + + procedure VerifyProdOrderHasRoutingLineCount(ProdOrder: Record "Production Order"; ExpectedCount: Integer) + var + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + ActualCount: Integer; + begin + ProdOrderRoutingLine.SetRange(Status, ProdOrder.Status); + ProdOrderRoutingLine.SetRange("Prod. Order No.", ProdOrder."No."); + ActualCount := ProdOrderRoutingLine.Count(); + Assert.AreEqual(ExpectedCount, ActualCount, + StrSubstNo(RoutingLineCountMismatchLbl, ExpectedCount, ActualCount)); + end; + + procedure VerifyNoBOMVersionExists(BOMNo: Code[20]) + var + ProductionBOMVersion: Record "Production BOM Version"; + begin + ProductionBOMVersion.SetRange("Production BOM No.", BOMNo); + Assert.IsTrue(ProductionBOMVersion.IsEmpty(), + StrSubstNo(BOMVersionShouldNotExistLbl, BOMNo, '')); + end; + + procedure VerifyNoRoutingVersionExists(RoutingNo: Code[20]) + var + RoutingVersion: Record "Routing Version"; + begin + RoutingVersion.SetRange("Routing No.", RoutingNo); + Assert.IsTrue(RoutingVersion.IsEmpty(), + StrSubstNo(RoutingVersionShouldNotExistLbl, RoutingNo)); + end; + + procedure GetLastBOMVersionCode(BOMNo: Code[20]): Code[20] + var + ProductionBOMVersion: Record "Production BOM Version"; + begin + ProductionBOMVersion.SetRange("Production BOM No.", BOMNo); + if ProductionBOMVersion.FindLast() then + exit(ProductionBOMVersion."Version Code"); + exit(''); + end; + + procedure GetLastRoutingVersionCode(RoutingNo: Code[20]): Code[20] + var + RoutingVersion: Record "Routing Version"; + begin + RoutingVersion.SetRange("Routing No.", RoutingNo); + if RoutingVersion.FindLast() then + exit(RoutingVersion."Version Code"); + exit(''); + end; + + procedure VerifyNewLastBOMVersionCertified(BOMNo: Code[20]; PreviousVersionCode: Code[20]) + var + ProductionBOMVersion: Record "Production BOM Version"; + begin + ProductionBOMVersion.SetRange("Production BOM No.", BOMNo); + Assert.IsTrue(ProductionBOMVersion.FindLast(), + StrSubstNo(BOMVersionShouldBeCertifiedLbl, BOMNo, '')); + Assert.AreNotEqual(PreviousVersionCode, ProductionBOMVersion."Version Code", + StrSubstNo(BOMVersionShouldBeNewLbl, BOMNo, PreviousVersionCode, ProductionBOMVersion."Version Code")); + Assert.AreEqual(ProductionBOMVersion.Status::Certified, ProductionBOMVersion.Status, + StrSubstNo(BOMVersionShouldBeCertifiedLbl, BOMNo, ProductionBOMVersion."Version Code")); + end; + + procedure VerifyNewLastRoutingVersionCertified(RoutingNo: Code[20]; PreviousVersionCode: Code[20]) + var + RoutingVersion: Record "Routing Version"; + begin + RoutingVersion.SetRange("Routing No.", RoutingNo); + Assert.IsTrue(RoutingVersion.FindLast(), + StrSubstNo(RoutingVersionShouldBeCertifiedLbl, RoutingNo, '')); + Assert.AreNotEqual(PreviousVersionCode, RoutingVersion."Version Code", + StrSubstNo(RoutingVersionShouldBeNewLbl, RoutingNo, PreviousVersionCode, RoutingVersion."Version Code")); + Assert.AreEqual(RoutingVersion.Status::Certified, RoutingVersion.Status, + StrSubstNo(RoutingVersionShouldBeCertifiedLbl, RoutingNo, RoutingVersion."Version Code")); + end; + + procedure VerifyReservationExistsForSalesLine(SalesLine: Record "Sales Line") + var + ReservationEntry: Record "Reservation Entry"; + begin + ReservationEntry.SetRange("Source Type", Database::"Sales Line"); + ReservationEntry.SetRange("Source Subtype", SalesLine."Document Type".AsInteger()); + ReservationEntry.SetRange("Source ID", SalesLine."Document No."); + ReservationEntry.SetRange("Source Ref. No.", SalesLine."Line No."); + Assert.IsFalse(ReservationEntry.IsEmpty(), + StrSubstNo(ReservationShouldExistLbl, SalesLine."Document No.", SalesLine."Line No.")); + end; + + procedure VerifyReservationLinksToProductionOrder(SalesLine: Record "Sales Line"; ProdOrder: Record "Production Order") + var + ReservationEntry: Record "Reservation Entry"; + begin + // Verify the supply side of the reservation points to the specific Production Order + ReservationEntry.SetRange("Source Type", Database::"Prod. Order Line"); + ReservationEntry.SetRange("Source Subtype", ProdOrder.Status.AsInteger()); + ReservationEntry.SetRange("Source ID", ProdOrder."No."); + ReservationEntry.SetRange("Reservation Status", ReservationEntry."Reservation Status"::Reservation); + Assert.IsFalse(ReservationEntry.IsEmpty(), + StrSubstNo(ReservationShouldLinkProdOrderLbl, + SalesLine."Document No.", SalesLine."Line No.", ProdOrder."No.")); + end; + + procedure VerifyProdOrderStatus(ProdOrder: Record "Production Order"; ExpectedStatus: Enum "Production Order Status") + begin + Assert.AreEqual(ExpectedStatus, ProdOrder.Status, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Status', Format(ExpectedStatus), Format(ProdOrder.Status))); + end; + + procedure VerifyProdOrderLineHasBOM(ItemNo: Code[20]; ExpectedBOMNo: Code[20]) + var + ProdOrderLine: Record "Prod. Order Line"; + begin + ProdOrderLine.SetRange("Item No.", ItemNo); + Assert.IsTrue(ProdOrderLine.FindFirst(), + StrSubstNo(ProdOrderShouldExistLbl, ItemNo)); + Assert.AreEqual(ExpectedBOMNo, ProdOrderLine."Production BOM No.", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Production BOM No.', ExpectedBOMNo, ProdOrderLine."Production BOM No.")); + end; + + procedure VerifyProdOrderLineHasRouting(ItemNo: Code[20]; ExpectedRoutingNo: Code[20]) + var + ProdOrderLine: Record "Prod. Order Line"; + begin + ProdOrderLine.SetRange("Item No.", ItemNo); + Assert.IsTrue(ProdOrderLine.FindFirst(), + StrSubstNo(ProdOrderShouldExistLbl, ItemNo)); + Assert.AreEqual(ExpectedRoutingNo, ProdOrderLine."Routing No.", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Routing No.', ExpectedRoutingNo, ProdOrderLine."Routing No.")); + end; + + procedure VerifyProdOrderLineHasBOMVersion(ItemNo: Code[20]; ExpectedVersionCode: Code[20]) + var + ProdOrderLine: Record "Prod. Order Line"; + begin + ProdOrderLine.SetRange("Item No.", ItemNo); + Assert.IsTrue(ProdOrderLine.FindFirst(), + StrSubstNo(ProdOrderShouldExistLbl, ItemNo)); + Assert.AreEqual(ExpectedVersionCode, ProdOrderLine."Production BOM Version Code", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Production BOM Version Code', ExpectedVersionCode, ProdOrderLine."Production BOM Version Code")); + end; + + procedure VerifyProdOrderLineHasRoutingVersion(ItemNo: Code[20]; ExpectedVersionCode: Code[20]) + var + ProdOrderLine: Record "Prod. Order Line"; + begin + ProdOrderLine.SetRange("Item No.", ItemNo); + Assert.IsTrue(ProdOrderLine.FindFirst(), + StrSubstNo(ProdOrderShouldExistLbl, ItemNo)); + Assert.AreEqual(ExpectedVersionCode, ProdOrderLine."Routing Version Code", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Routing Version Code', ExpectedVersionCode, ProdOrderLine."Routing Version Code")); + end; + + procedure VerifyProdOrderLineScrapPct(ItemNo: Code[20]; ExpectedScrapPct: Decimal) + var + ProdOrderLine: Record "Prod. Order Line"; + begin + ProdOrderLine.SetRange("Item No.", ItemNo); + Assert.IsTrue(ProdOrderLine.FindFirst(), + StrSubstNo(ProdOrderShouldExistLbl, ItemNo)); + Assert.AreNearlyEqual(ExpectedScrapPct, ProdOrderLine."Scrap %", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Scrap %', Format(ExpectedScrapPct), Format(ProdOrderLine."Scrap %"))); + end; + + procedure VerifyProdOrderComponentHasQtyPerForFirstComponent(ProdOrder: Record "Production Order"; ExpectedQtyPer: Decimal) + var + ProdOrderComponent: Record "Prod. Order Component"; + begin + ProdOrderComponent.SetRange(Status, ProdOrder.Status); + ProdOrderComponent.SetRange("Prod. Order No.", ProdOrder."No."); + Assert.IsTrue(ProdOrderComponent.FindFirst(), + StrSubstNo(ComponentCountMismatchLbl, 1, 0)); + Assert.AreNearlyEqual(ExpectedQtyPer, ProdOrderComponent."Quantity per", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Quantity per', Format(ExpectedQtyPer), Format(ProdOrderComponent."Quantity per"))); + end; + + procedure VerifyProdOrderRoutingLineRunTime(ProdOrder: Record "Production Order"; OperationNo: Code[10]; ExpectedRunTime: Decimal) + var + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + begin + ProdOrderRoutingLine.SetRange(Status, ProdOrder.Status); + ProdOrderRoutingLine.SetRange("Prod. Order No.", ProdOrder."No."); + ProdOrderRoutingLine.SetRange("Operation No.", OperationNo); + Assert.IsTrue(ProdOrderRoutingLine.FindFirst(), + StrSubstNo(RoutingLineCountMismatchLbl, 1, 0)); + Assert.AreNearlyEqual(ExpectedRunTime, ProdOrderRoutingLine."Run Time", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Run Time', Format(ExpectedRunTime), Format(ProdOrderRoutingLine."Run Time"))); + end; + + procedure VerifyProdOrderComponentHasVariantCode(ProdOrder: Record "Production Order"; ExpectedVariantCode: Code[10]) + var + ProdOrderComponent: Record "Prod. Order Component"; + begin + ProdOrderComponent.SetRange(Status, ProdOrder.Status); + ProdOrderComponent.SetRange("Prod. Order No.", ProdOrder."No."); + Assert.IsTrue(ProdOrderComponent.FindFirst(), + StrSubstNo(ComponentCountMismatchLbl, 1, 0)); + Assert.AreEqual(ExpectedVariantCode, ProdOrderComponent."Variant Code", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Variant Code (Component)', ExpectedVariantCode, ProdOrderComponent."Variant Code")); + end; + + procedure VerifyProdOrderComponentHasDescription2(ProdOrder: Record "Production Order"; ExpectedDesc2: Text[50]) + var + ProdOrderComponent: Record "Prod. Order Component"; + begin + ProdOrderComponent.SetRange(Status, ProdOrder.Status); + ProdOrderComponent.SetRange("Prod. Order No.", ProdOrder."No."); + Assert.IsTrue(ProdOrderComponent.FindFirst(), + StrSubstNo(ComponentCountMismatchLbl, 1, 0)); + Assert.AreEqual(ExpectedDesc2, ProdOrderComponent."Description 2", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Description 2', ExpectedDesc2, ProdOrderComponent."Description 2")); + end; + + procedure VerifyProdOrderRoutingLineHasDescription2(ProdOrder: Record "Production Order"; ExpectedDesc2: Text[50]) + var + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + begin + ProdOrderRoutingLine.SetRange(Status, ProdOrder.Status); + ProdOrderRoutingLine.SetRange("Prod. Order No.", ProdOrder."No."); + Assert.IsTrue(ProdOrderRoutingLine.FindFirst(), + StrSubstNo(RoutingLineCountMismatchLbl, 1, 0)); + Assert.AreEqual(ExpectedDesc2, ProdOrderRoutingLine."Description 2", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Description 2', ExpectedDesc2, ProdOrderRoutingLine."Description 2")); + end; + + procedure VerifyProdOrderComponentFlushingMethod(ProdOrder: Record "Production Order"; ExpectedFlushingMethod: Enum "Flushing Method") + var + ProdOrderComponent: Record "Prod. Order Component"; + begin + ProdOrderComponent.SetRange(Status, ProdOrder.Status); + ProdOrderComponent.SetRange("Prod. Order No.", ProdOrder."No."); + if ProdOrderComponent.FindSet() then + repeat + Assert.AreEqual(ExpectedFlushingMethod, ProdOrderComponent."Flushing Method", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Flushing Method', + Format(ExpectedFlushingMethod), Format(ProdOrderComponent."Flushing Method"))); + until ProdOrderComponent.Next() = 0; + end; + + procedure VerifyReservationBaseQtyMatchesProdOrderRemainingBase(SalesLine: Record "Sales Line"; ItemNo: Code[20]) + var + ReservationEntry: Record "Reservation Entry"; + ProdOrderLine: Record "Prod. Order Line"; + begin + ProdOrderLine.SetRange("Item No.", ItemNo); + Assert.IsTrue(ProdOrderLine.FindFirst(), + StrSubstNo(ProdOrderShouldExistLbl, ItemNo)); + ReservationEntry.SetRange("Source Type", Database::"Sales Line"); + ReservationEntry.SetRange("Source Subtype", SalesLine."Document Type".AsInteger()); + ReservationEntry.SetRange("Source ID", SalesLine."Document No."); + ReservationEntry.SetRange("Source Ref. No.", SalesLine."Line No."); + Assert.IsTrue(ReservationEntry.FindFirst(), + StrSubstNo(ReservationShouldExistLbl, SalesLine."Document No.", SalesLine."Line No.")); + Assert.IsTrue(Abs(ReservationEntry."Quantity (Base)") > 0, + 'Reservation base quantity should be greater than zero'); + Assert.AreNearlyEqual(ProdOrderLine."Remaining Qty. (Base)", Abs(ReservationEntry."Quantity (Base)"), 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, + 'Reservation Quantity (Base)', + Format(ProdOrderLine."Remaining Qty. (Base)"), + Format(Abs(ReservationEntry."Quantity (Base)")))); + end; + + procedure VerifyTrackingSpecExistsForProdOrderLine(ItemNo: Code[20]; ExpectedLotNo: Code[50]) + var + ProdOrderLine: Record "Prod. Order Line"; + ReservationEntry: Record "Reservation Entry"; + begin + ProdOrderLine.SetRange("Item No.", ItemNo); + Assert.IsTrue(ProdOrderLine.FindFirst(), + StrSubstNo(ProdOrderShouldExistLbl, ItemNo)); + ReservationEntry.SetRange("Source Type", Database::"Prod. Order Line"); + ReservationEntry.SetRange("Source Subtype", ProdOrderLine.Status.AsInteger()); + ReservationEntry.SetRange("Source ID", ProdOrderLine."Prod. Order No."); + ReservationEntry.SetRange("Source Prod. Order Line", ProdOrderLine."Line No."); + Assert.IsTrue(ReservationEntry.FindFirst(), + 'Reservation Entry should exist for Prod. Order Line of item ' + ItemNo); + Assert.AreEqual(ExpectedLotNo, ReservationEntry."Lot No.", + 'Lot No. on Reservation Entry should match expected: ' + ExpectedLotNo); + end; + + procedure VerifyBOMVersionLineCount(BOMNo: Code[20]; VersionCode: Code[20]; ExpectedCount: Integer) + var + ProductionBOMLine: Record "Production BOM Line"; + ActualCount: Integer; + begin + ProductionBOMLine.SetRange("Production BOM No.", BOMNo); + ProductionBOMLine.SetRange("Version Code", VersionCode); + ActualCount := ProductionBOMLine.Count(); + Assert.AreEqual(ExpectedCount, ActualCount, + StrSubstNo('BOM %1 version %2: expected %3 lines but found %4', + BOMNo, VersionCode, ExpectedCount, ActualCount)); + end; + + procedure VerifyRoutingVersionLineCount(RoutingNo: Code[20]; VersionCode: Code[20]; ExpectedCount: Integer) + var + RoutingLine: Record "Routing Line"; + ActualCount: Integer; + begin + RoutingLine.SetRange("Routing No.", RoutingNo); + RoutingLine.SetRange("Version Code", VersionCode); + ActualCount := RoutingLine.Count(); + Assert.AreEqual(ExpectedCount, ActualCount, + StrSubstNo('Routing %1 version %2: expected %3 lines but found %4', + RoutingNo, VersionCode, ExpectedCount, ActualCount)); + end; + + procedure GetLastProductionBOMNo(): Code[20] + var + ProductionBOMHeader: Record "Production BOM Header"; + begin + ProductionBOMHeader.SetLoadFields("No."); + if ProductionBOMHeader.FindLast() then + exit(ProductionBOMHeader."No."); + exit(''); + end; + + procedure GetLastRoutingNo(): Code[20] + var + RoutingHeader: Record "Routing Header"; + begin + RoutingHeader.SetLoadFields("No."); + if RoutingHeader.FindLast() then + exit(RoutingHeader."No."); + exit(''); + end; + + procedure VerifyProdOrderComponentBOMFields(ProdOrder: Record "Production Order"; ComponentItemNo: Code[20]; ExpectedScrapPct: Decimal; ExpectedLength: Decimal; ExpectedWidth: Decimal; ExpectedWeight: Decimal; ExpectedDepth: Decimal) + var + ProdOrderComponent: Record "Prod. Order Component"; + begin + ProdOrderComponent.SetRange(Status, ProdOrder.Status); + ProdOrderComponent.SetRange("Prod. Order No.", ProdOrder."No."); + ProdOrderComponent.SetRange("Item No.", ComponentItemNo); + Assert.IsTrue(ProdOrderComponent.FindFirst(), + StrSubstNo(ComponentCountMismatchLbl, 1, 0)); + Assert.AreNearlyEqual(ExpectedScrapPct, ProdOrderComponent."Scrap %", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Scrap % (Component)', Format(ExpectedScrapPct), Format(ProdOrderComponent."Scrap %"))); + Assert.AreNearlyEqual(ExpectedLength, ProdOrderComponent."Length", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Length (Component)', Format(ExpectedLength), Format(ProdOrderComponent."Length"))); + Assert.AreNearlyEqual(ExpectedWidth, ProdOrderComponent."Width", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Width (Component)', Format(ExpectedWidth), Format(ProdOrderComponent."Width"))); + Assert.AreNearlyEqual(ExpectedWeight, ProdOrderComponent."Weight", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Weight (Component)', Format(ExpectedWeight), Format(ProdOrderComponent."Weight"))); + Assert.AreNearlyEqual(ExpectedDepth, ProdOrderComponent."Depth", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Depth (Component)', Format(ExpectedDepth), Format(ProdOrderComponent."Depth"))); + end; + + procedure VerifyProdOrderComponentCalcFormula(ProdOrder: Record "Production Order"; ComponentItemNo: Code[20]) + var + ProdOrderComponent: Record "Prod. Order Component"; + begin + ProdOrderComponent.SetRange(Status, ProdOrder.Status); + ProdOrderComponent.SetRange("Prod. Order No.", ProdOrder."No."); + ProdOrderComponent.SetRange("Item No.", ComponentItemNo); + Assert.IsTrue(ProdOrderComponent.FindFirst(), + StrSubstNo(ComponentCountMismatchLbl, 1, 0)); + Assert.AreEqual( + ProdOrderComponent."Calculation Formula"::"Fixed Quantity", + ProdOrderComponent."Calculation Formula", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Calculation Formula (Component)', + Format(ProdOrderComponent."Calculation Formula"::"Fixed Quantity"), + Format(ProdOrderComponent."Calculation Formula"))); + end; + + procedure VerifyProdOrderRoutingLineExtendedFields(ProdOrder: Record "Production Order"; OperationNo: Code[10]; ExpectedPrevOpNo: Code[30]; ExpectedNextOpNo: Code[30]; ExpectedSetupUOM: Code[10]; ExpectedRunUOM: Code[10]; ExpectedWaitUOM: Code[10]; ExpectedMoveUOM: Code[10]; ExpectedFixedScrapQty: Decimal; ExpectedScrapFactorPct: Decimal; ExpectedSendAheadQty: Decimal; ExpectedConcurrentCap: Decimal; ExpectedLotSize: Decimal) + var + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + begin + ProdOrderRoutingLine.SetRange(Status, ProdOrder.Status); + ProdOrderRoutingLine.SetRange("Prod. Order No.", ProdOrder."No."); + ProdOrderRoutingLine.SetRange("Operation No.", OperationNo); + Assert.IsTrue(ProdOrderRoutingLine.FindFirst(), + StrSubstNo(RoutingLineCountMismatchLbl, 1, 0)); + Assert.AreEqual(ExpectedPrevOpNo, ProdOrderRoutingLine."Previous Operation No.", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Previous Operation No.', ExpectedPrevOpNo, ProdOrderRoutingLine."Previous Operation No.")); + Assert.AreEqual(ExpectedNextOpNo, ProdOrderRoutingLine."Next Operation No.", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Next Operation No.', ExpectedNextOpNo, ProdOrderRoutingLine."Next Operation No.")); + Assert.AreEqual(ExpectedSetupUOM, ProdOrderRoutingLine."Setup Time Unit of Meas. Code", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Setup Time Unit of Meas. Code', ExpectedSetupUOM, ProdOrderRoutingLine."Setup Time Unit of Meas. Code")); + Assert.AreEqual(ExpectedRunUOM, ProdOrderRoutingLine."Run Time Unit of Meas. Code", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Run Time Unit of Meas. Code', ExpectedRunUOM, ProdOrderRoutingLine."Run Time Unit of Meas. Code")); + Assert.AreEqual(ExpectedWaitUOM, ProdOrderRoutingLine."Wait Time Unit of Meas. Code", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Wait Time Unit of Meas. Code', ExpectedWaitUOM, ProdOrderRoutingLine."Wait Time Unit of Meas. Code")); + Assert.AreEqual(ExpectedMoveUOM, ProdOrderRoutingLine."Move Time Unit of Meas. Code", + StrSubstNo(ProdOrderFieldMismatchLbl, 'Move Time Unit of Meas. Code', ExpectedMoveUOM, ProdOrderRoutingLine."Move Time Unit of Meas. Code")); + Assert.AreNearlyEqual(ExpectedFixedScrapQty, ProdOrderRoutingLine."Fixed Scrap Quantity", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Fixed Scrap Quantity', Format(ExpectedFixedScrapQty), Format(ProdOrderRoutingLine."Fixed Scrap Quantity"))); + Assert.AreNearlyEqual(ExpectedScrapFactorPct, ProdOrderRoutingLine."Scrap Factor %", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Scrap Factor %', Format(ExpectedScrapFactorPct), Format(ProdOrderRoutingLine."Scrap Factor %"))); + Assert.AreNearlyEqual(ExpectedSendAheadQty, ProdOrderRoutingLine."Send-Ahead Quantity", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Send-Ahead Quantity', Format(ExpectedSendAheadQty), Format(ProdOrderRoutingLine."Send-Ahead Quantity"))); + Assert.AreNearlyEqual(ExpectedConcurrentCap, ProdOrderRoutingLine."Concurrent Capacities", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Concurrent Capacities', Format(ExpectedConcurrentCap), Format(ProdOrderRoutingLine."Concurrent Capacities"))); + Assert.AreNearlyEqual(ExpectedLotSize, ProdOrderRoutingLine."Lot Size", 0.01, + StrSubstNo(ProdOrderFieldMismatchLbl, 'Lot Size', Format(ExpectedLotSize), Format(ProdOrderRoutingLine."Lot Size"))); + end; +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/ProdDefWizLibrary.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/ProdDefWizLibrary.Codeunit.al new file mode 100644 index 0000000000..7d980fca51 --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/ProdDefWizLibrary.Codeunit.al @@ -0,0 +1,458 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Foundation.UOM; +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Ledger; +using Microsoft.Inventory.Location; +using Microsoft.Inventory.Tracking; +using Microsoft.Manufacturing.Capacity; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; +using Microsoft.Manufacturing.WorkCenter; +using Microsoft.Sales.Document; +using Microsoft.Sales.Customer; + +codeunit 137420 "Prod. Def. Wiz. Library" +{ + var + LibraryERM: Codeunit "Library - ERM"; + LibraryInventory: Codeunit "Library - Inventory"; + LibraryManufacturing: Codeunit "Library - Manufacturing"; + LibrarySales: Codeunit "Library - Sales"; + LibraryWarehouse: Codeunit "Library - Warehouse"; + LibraryRandom: Codeunit "Library - Random"; + + procedure CreateItemWithBOMAndRouting(BOMNo: Code[20]; RoutingNo: Code[20]): Code[20] + var + Item: Record Item; + begin + LibraryInventory.CreateItem(Item); + Item.Validate("Replenishment System", Item."Replenishment System"::"Prod. Order"); + Item.Validate("Production BOM No.", BOMNo); + Item.Validate("Routing No.", RoutingNo); + Item.Modify(true); + exit(Item."No."); + end; + + procedure CreateBOM(NumberOfLines: Integer): Code[20] + var + ComponentItem: Record Item; + ProductionBOMHeader: Record "Production BOM Header"; + ProductionBOMLine: Record "Production BOM Line"; + i: Integer; + begin + LibraryInventory.CreateItem(ComponentItem); + LibraryManufacturing.CreateProductionBOMHeader(ProductionBOMHeader, ComponentItem."Base Unit of Measure"); + for i := 1 to NumberOfLines do begin + LibraryInventory.CreateItem(ComponentItem); + LibraryManufacturing.CreateProductionBOMLine( + ProductionBOMHeader, ProductionBOMLine, '', ProductionBOMLine.Type::Item, ComponentItem."No.", i); + end; + ProductionBOMHeader.Validate("Version Nos.", LibraryERM.CreateNoSeriesCode()); + ProductionBOMHeader.Validate(Status, ProductionBOMHeader.Status::Certified); + ProductionBOMHeader.Modify(true); + exit(ProductionBOMHeader."No."); + end; + + procedure CreateBOMVersionAndCertify(BOMNo: Code[20]; VersionCode: Code[20]; StartingDate: Date) + var + ComponentItem: Record Item; + ProductionBOMHeader: Record "Production BOM Header"; + ProductionBOMLine: Record "Production BOM Line"; + ProductionBOMVersion: Record "Production BOM Version"; + begin + ProductionBOMHeader.Get(BOMNo); + LibraryInventory.CreateItem(ComponentItem); + LibraryManufacturing.CreateProductionBOMVersion( + ProductionBOMVersion, BOMNo, VersionCode, ComponentItem."Base Unit of Measure"); + ProductionBOMVersion."Starting Date" := StartingDate; + ProductionBOMVersion.Modify(); + + LibraryManufacturing.CreateProductionBOMLine( + ProductionBOMHeader, ProductionBOMLine, VersionCode, ProductionBOMLine.Type::Item, ComponentItem."No.", 1); + + ProductionBOMVersion.Validate(Status, ProductionBOMVersion.Status::Certified); + ProductionBOMVersion.Modify(true); + end; + + procedure CreateBOMWithoutVersionNos(): Code[20] + var + ComponentItem: Record Item; + ProductionBOMHeader: Record "Production BOM Header"; + ProductionBOMLine: Record "Production BOM Line"; + begin + LibraryInventory.CreateItem(ComponentItem); + LibraryManufacturing.CreateProductionBOMHeader(ProductionBOMHeader, ComponentItem."Base Unit of Measure"); + LibraryManufacturing.CreateProductionBOMLine( + ProductionBOMHeader, ProductionBOMLine, '', ProductionBOMLine.Type::Item, ComponentItem."No.", 1); + // Intentionally leave Version Nos. blank + ProductionBOMHeader."Version Nos." := ''; + ProductionBOMHeader.Validate(Status, ProductionBOMHeader.Status::Certified); + ProductionBOMHeader.Modify(true); + exit(ProductionBOMHeader."No."); + end; + + procedure CreateRoutingWithoutVersionNos(): Code[20] + var + RoutingHeader: Record "Routing Header"; + RoutingLine: Record "Routing Line"; + WorkCenter: Record "Work Center"; + begin + CreateAndCalculateWorkCenter(WorkCenter); + LibraryManufacturing.CreateRoutingHeader(RoutingHeader, RoutingHeader.Type::Serial); + LibraryManufacturing.CreateRoutingLine( + RoutingHeader, RoutingLine, '', '10', RoutingLine.Type::"Work Center", WorkCenter."No."); + RoutingLine.Validate("Run Time", 5); + RoutingLine.Modify(true); + // Intentionally leave Version Nos. blank + RoutingHeader."Version Nos." := ''; + RoutingHeader.Validate(Status, RoutingHeader.Status::Certified); + RoutingHeader.Modify(true); + exit(RoutingHeader."No."); + end; + + procedure CreateAndCalculateWorkCenter(var WorkCenter: Record "Work Center") + var + CapacityUnitOfMeasure: Record "Capacity Unit of Measure"; + ShopCalendarCode: Code[10]; + begin + LibraryManufacturing.CreateCapacityUnitOfMeasure(CapacityUnitOfMeasure, "Capacity Unit of Measure"::Minutes); + ShopCalendarCode := LibraryManufacturing.UpdateShopCalendarWorkingDays(); + LibraryManufacturing.CreateWorkCenter(WorkCenter); + WorkCenter.Validate("Unit of Measure Code", CapacityUnitOfMeasure.Code); + WorkCenter.Validate("Shop Calendar Code", ShopCalendarCode); + WorkCenter.Validate("Direct Unit Cost", LibraryRandom.RandDec(10, 2)); + WorkCenter.Modify(true); + LibraryManufacturing.CalculateWorkCenterCalendar(WorkCenter, CalcDate('<-CY-1Y>', WorkDate()), CalcDate('', WorkDate())); + end; + + procedure CreateRoutingWithSingleLine(var WorkCenterNo: Code[20]): Code[20] + var + RoutingHeader: Record "Routing Header"; + RoutingLine: Record "Routing Line"; + WorkCenter: Record "Work Center"; + begin + CreateAndCalculateWorkCenter(WorkCenter); + WorkCenterNo := WorkCenter."No."; + + LibraryManufacturing.CreateRoutingHeader(RoutingHeader, RoutingHeader.Type::Serial); + LibraryManufacturing.CreateRoutingLine( + RoutingHeader, RoutingLine, '', '10', RoutingLine.Type::"Work Center", WorkCenter."No."); + RoutingLine.Validate("Setup Time", 10); + RoutingLine.Validate("Run Time", 5); + RoutingLine.Modify(true); + + RoutingHeader.Validate("Version Nos.", LibraryERM.CreateNoSeriesCode()); + RoutingHeader.Validate(Status, RoutingHeader.Status::Certified); + RoutingHeader.Modify(true); + exit(RoutingHeader."No."); + end; + + procedure CreateRoutingWithTwoLines(var WorkCenter1No: Code[20]; var WorkCenter2No: Code[20]): Code[20] + var + RoutingHeader: Record "Routing Header"; + RoutingLine: Record "Routing Line"; + WorkCenter1: Record "Work Center"; + WorkCenter2: Record "Work Center"; + begin + CreateAndCalculateWorkCenter(WorkCenter1); + CreateAndCalculateWorkCenter(WorkCenter2); + WorkCenter1No := WorkCenter1."No."; + WorkCenter2No := WorkCenter2."No."; + + LibraryManufacturing.CreateRoutingHeader(RoutingHeader, RoutingHeader.Type::Serial); + + LibraryManufacturing.CreateRoutingLine( + RoutingHeader, RoutingLine, '', '10', RoutingLine.Type::"Work Center", WorkCenter1."No."); + RoutingLine.Validate("Setup Time", 10); + RoutingLine.Validate("Run Time", 5); + RoutingLine.Modify(true); + + LibraryManufacturing.CreateRoutingLine( + RoutingHeader, RoutingLine, '', '20', RoutingLine.Type::"Work Center", WorkCenter2."No."); + RoutingLine.Validate("Setup Time", 15); + RoutingLine.Validate("Run Time", 8); + RoutingLine.Modify(true); + + RoutingHeader.Validate("Version Nos.", LibraryERM.CreateNoSeriesCode()); + RoutingHeader.Validate(Status, RoutingHeader.Status::Certified); + RoutingHeader.Modify(true); + exit(RoutingHeader."No."); + end; + + procedure CreateRoutingVersionAndCertify(RoutingNo: Code[20]; VersionCode: Code[20]; StartingDate: Date) + var + RoutingHeader: Record "Routing Header"; + RoutingLine: Record "Routing Line"; + RoutingVersion: Record "Routing Version"; + WorkCenter: Record "Work Center"; + begin + RoutingHeader.Get(RoutingNo); + CreateAndCalculateWorkCenter(WorkCenter); + LibraryManufacturing.CreateRoutingVersion(RoutingVersion, RoutingNo, VersionCode); + RoutingVersion."Starting Date" := StartingDate; + RoutingVersion.Modify(); + + LibraryManufacturing.CreateRoutingLine( + RoutingHeader, RoutingLine, VersionCode, '10', RoutingLine.Type::"Work Center", WorkCenter."No."); + RoutingLine.Validate("Setup Time", 10); + RoutingLine.Validate("Run Time", 5); + RoutingLine.Modify(true); + + RoutingVersion.Validate(Status, RoutingVersion.Status::Certified); + RoutingVersion.Modify(true); + end; + + procedure CreateLocationCode(): Code[10] + var + Location: Record Location; + begin + LibraryWarehouse.CreateLocation(Location); + exit(Location.Code); + end; + + procedure CreateStockkeepingUnitWithBOMAndRouting(var SKU: Record "Stockkeeping Unit"; ItemNo: Code[20]; LocationCode: Code[10]; VariantCode: Code[10]; BOMNo: Code[20]; RoutingNo: Code[20]) + begin + SKU.Init(); + SKU."Item No." := ItemNo; + SKU."Location Code" := LocationCode; + SKU."Variant Code" := VariantCode; + SKU.Insert(true); + SKU.Validate("Production BOM No.", BOMNo); + SKU.Validate("Routing No.", RoutingNo); + SKU.Modify(true); + end; + + procedure CreateVariantForItem(ItemNo: Code[20]): Code[10] + var + ItemVariant: Record "Item Variant"; + begin + LibraryInventory.CreateItemVariant(ItemVariant, ItemNo); + exit(ItemVariant.Code); + end; + + procedure CreateSalesLine(var SalesLine: Record "Sales Line"; ItemNo: Code[20]; Qty: Decimal; LocationCode: Code[10]; VariantCode: Code[10]; ShipmentDate: Date) + var + Customer: Record Customer; + SalesHeader: Record "Sales Header"; + begin + LibrarySales.CreateCustomer(Customer); + LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No."); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, ItemNo, Qty); + SalesLine.Validate("Location Code", LocationCode); + if VariantCode <> '' then + SalesLine.Validate("Variant Code", VariantCode); + if ShipmentDate <> 0D then + SalesLine.Validate("Shipment Date", ShipmentDate); + SalesLine.Modify(true); + end; + + procedure CreatePartialReservationForSalesLine(var SalesLine: Record "Sales Line"; QtyToReserve: Decimal) + var + ReservEntry: Record "Reservation Entry"; + NextEntryNo: Integer; + begin + if ReservEntry.FindLast() then + NextEntryNo := ReservEntry."Entry No." + 1 + else + NextEntryNo := 1; + + // Supply side (positive) – linked to a fictitious item ledger entry + ReservEntry.Init(); + ReservEntry."Entry No." := NextEntryNo; + ReservEntry."Item No." := SalesLine."No."; + ReservEntry."Location Code" := SalesLine."Location Code"; + ReservEntry.Quantity := QtyToReserve; + ReservEntry."Quantity (Base)" := QtyToReserve; + ReservEntry.Positive := true; + ReservEntry."Reservation Status" := ReservEntry."Reservation Status"::Reservation; + ReservEntry."Source Type" := Database::"Item Ledger Entry"; + ReservEntry."Source Subtype" := 0; + ReservEntry."Source ID" := ''; + ReservEntry."Source Ref. No." := 0; + ReservEntry.Insert(); + + // Demand side (negative) – linked to the sales line + ReservEntry.Init(); + ReservEntry."Entry No." := NextEntryNo + 1; + ReservEntry."Item No." := SalesLine."No."; + ReservEntry."Location Code" := SalesLine."Location Code"; + ReservEntry.Quantity := -QtyToReserve; + ReservEntry."Quantity (Base)" := -QtyToReserve; + ReservEntry.Positive := false; + ReservEntry."Reservation Status" := ReservEntry."Reservation Status"::Reservation; + ReservEntry."Source Type" := Database::"Sales Line"; + ReservEntry."Source Subtype" := SalesLine."Document Type".AsInteger(); + ReservEntry."Source ID" := SalesLine."Document No."; + ReservEntry."Source Ref. No." := SalesLine."Line No."; + ReservEntry.Insert(); + end; + + procedure CreateBOMWithComponentAndDescription2(var ComponentItemNo: Code[20]; Desc2: Text[50]): Code[20] + var + ComponentItem: Record Item; + ProductionBOMHeader: Record "Production BOM Header"; + ProductionBOMLine: Record "Production BOM Line"; + begin + LibraryInventory.CreateItem(ComponentItem); + ComponentItemNo := ComponentItem."No."; + LibraryManufacturing.CreateProductionBOMHeader(ProductionBOMHeader, ComponentItem."Base Unit of Measure"); + LibraryManufacturing.CreateProductionBOMLine( + ProductionBOMHeader, ProductionBOMLine, '', ProductionBOMLine.Type::Item, ComponentItem."No.", 1); + ProductionBOMLine.Validate("Description 2", Desc2); + ProductionBOMLine.Modify(true); + ProductionBOMHeader.Validate("Version Nos.", LibraryERM.CreateNoSeriesCode()); + ProductionBOMHeader.Validate(Status, ProductionBOMHeader.Status::Certified); + ProductionBOMHeader.Modify(true); + exit(ProductionBOMHeader."No."); + end; + + procedure CreateRoutingWithSingleLineAndDescription2(var WorkCenterNo: Code[20]; Desc2: Text[50]): Code[20] + var + RoutingHeader: Record "Routing Header"; + RoutingLine: Record "Routing Line"; + WorkCenter: Record "Work Center"; + begin + CreateAndCalculateWorkCenter(WorkCenter); + WorkCenterNo := WorkCenter."No."; + LibraryManufacturing.CreateRoutingHeader(RoutingHeader, RoutingHeader.Type::Serial); + LibraryManufacturing.CreateRoutingLine( + RoutingHeader, RoutingLine, '', '10', RoutingLine.Type::"Work Center", WorkCenter."No."); + RoutingLine.Validate("Setup Time", 10); + RoutingLine.Validate("Run Time", 5); + RoutingLine.Validate("Description 2", CopyStr(Desc2, 1, MaxStrLen(RoutingLine."Description 2"))); + RoutingLine.Modify(true); + RoutingHeader.Validate("Version Nos.", LibraryERM.CreateNoSeriesCode()); + RoutingHeader.Validate(Status, RoutingHeader.Status::Certified); + RoutingHeader.Modify(true); + exit(RoutingHeader."No."); + end; + + procedure CreateItemWithSalesUOMMultiplier(BOMNo: Code[20]; RoutingNo: Code[20]; AltUOMMultiplier: Decimal; var ItemNo: Code[20]; var AltUOMCode: Code[10]) + var + Item: Record Item; + ItemUnitOfMeasure: Record "Item Unit of Measure"; + UnitOfMeasure: Record "Unit of Measure"; + begin + LibraryInventory.CreateUnitOfMeasureCode(UnitOfMeasure); + ItemNo := CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + LibraryInventory.CreateItemUnitOfMeasure(ItemUnitOfMeasure, ItemNo, UnitOfMeasure.Code, AltUOMMultiplier); + AltUOMCode := ItemUnitOfMeasure.Code; + Item.Validate("Sales Unit of Measure", AltUOMCode); + Item.Modify(true); + end; + + procedure CreateItemWithLotTracking(BOMNo: Code[20]; RoutingNo: Code[20]): Code[20] + var + Item: Record Item; + ItemTrackingCode: Record "Item Tracking Code"; + begin + ItemTrackingCode.Init(); + ItemTrackingCode.Code := CopyStr('LOT' + Format(LibraryRandom.RandIntInRange(1000, 9999)), 1, 10); + ItemTrackingCode."Lot Sales Outbound Tracking" := true; + ItemTrackingCode."Lot Manuf. Inbound Tracking" := true; + ItemTrackingCode.Insert(true); + Item.Get(CreateItemWithBOMAndRouting(BOMNo, RoutingNo)); + Item.Validate("Item Tracking Code", ItemTrackingCode.Code); + Item.Modify(true); + exit(Item."No."); + end; + + procedure CreateBOMWithComponentVariant(var ComponentItemNo: Code[20]; var ComponentVariantCode: Code[10]): Code[20] + var + ComponentItem: Record Item; + ComponentVariant: Record "Item Variant"; + ProductionBOMHeader: Record "Production BOM Header"; + ProductionBOMLine: Record "Production BOM Line"; + begin + LibraryInventory.CreateItem(ComponentItem); + ComponentItemNo := ComponentItem."No."; + LibraryInventory.CreateItemVariant(ComponentVariant, ComponentItemNo); + ComponentVariantCode := ComponentVariant.Code; + + LibraryManufacturing.CreateProductionBOMHeader(ProductionBOMHeader, ComponentItem."Base Unit of Measure"); + LibraryManufacturing.CreateProductionBOMLine( + ProductionBOMHeader, ProductionBOMLine, '', ProductionBOMLine.Type::Item, ComponentItemNo, 1); + ProductionBOMLine.Validate("Variant Code", ComponentVariantCode); + ProductionBOMLine.Modify(true); + + ProductionBOMHeader.Validate("Version Nos.", LibraryERM.CreateNoSeriesCode()); + ProductionBOMHeader.Validate(Status, ProductionBOMHeader.Status::Certified); + ProductionBOMHeader.Modify(true); + exit(ProductionBOMHeader."No."); + end; + + procedure CreateBOMWithDimensionalFields(var ComponentItemNo: Code[20]): Code[20] + var + ComponentItem: Record Item; + ProductionBOMHeader: Record "Production BOM Header"; + ProductionBOMLine: Record "Production BOM Line"; + begin + LibraryInventory.CreateItem(ComponentItem); + ComponentItemNo := ComponentItem."No."; + LibraryManufacturing.CreateProductionBOMHeader(ProductionBOMHeader, ComponentItem."Base Unit of Measure"); + LibraryManufacturing.CreateProductionBOMLine( + ProductionBOMHeader, ProductionBOMLine, '', ProductionBOMLine.Type::Item, ComponentItemNo, 1); + ProductionBOMLine.Validate("Calculation Formula", ProductionBOMLine."Calculation Formula"::"Fixed Quantity"); + ProductionBOMLine.Validate("Scrap %", 5); + ProductionBOMLine."Length" := 2; + ProductionBOMLine."Width" := 3; + ProductionBOMLine."Weight" := 4; + ProductionBOMLine."Depth" := 1; + ProductionBOMLine.Modify(true); + ProductionBOMHeader.Validate("Version Nos.", LibraryERM.CreateNoSeriesCode()); + ProductionBOMHeader.Validate(Status, ProductionBOMHeader.Status::Certified); + ProductionBOMHeader.Modify(true); + exit(ProductionBOMHeader."No."); + end; + + procedure CreateRoutingWithTwoLinesAndExtendedFields(var WorkCenter1No: Code[20]; var WorkCenter2No: Code[20]; var CapUOMCode: Code[10]): Code[20] + var + CapacityUnitOfMeasure: Record "Capacity Unit of Measure"; + RoutingHeader: Record "Routing Header"; + RoutingLine: Record "Routing Line"; + WorkCenter1: Record "Work Center"; + WorkCenter2: Record "Work Center"; + begin + CreateAndCalculateWorkCenter(WorkCenter1); + CreateAndCalculateWorkCenter(WorkCenter2); + WorkCenter1No := WorkCenter1."No."; + WorkCenter2No := WorkCenter2."No."; + LibraryManufacturing.CreateCapacityUnitOfMeasure(CapacityUnitOfMeasure, "Capacity Unit of Measure"::Minutes); + CapUOMCode := CapacityUnitOfMeasure.Code; + + LibraryManufacturing.CreateRoutingHeader(RoutingHeader, RoutingHeader.Type::Serial); + + LibraryManufacturing.CreateRoutingLine( + RoutingHeader, RoutingLine, '', '10', RoutingLine.Type::"Work Center", WorkCenter1."No."); + RoutingLine.Validate("Setup Time", 10); + RoutingLine.Validate("Run Time", 5); + RoutingLine.Validate("Setup Time Unit of Meas. Code", CapUOMCode); + RoutingLine.Validate("Run Time Unit of Meas. Code", CapUOMCode); + RoutingLine.Validate("Wait Time Unit of Meas. Code", CapUOMCode); + RoutingLine.Validate("Move Time Unit of Meas. Code", CapUOMCode); + RoutingLine."Fixed Scrap Quantity" := 3; + RoutingLine."Scrap Factor %" := 10; + RoutingLine."Send-Ahead Quantity" := 5; + RoutingLine."Concurrent Capacities" := 2; + RoutingLine."Lot Size" := 1; + RoutingLine.Modify(true); + + LibraryManufacturing.CreateRoutingLine( + RoutingHeader, RoutingLine, '', '20', RoutingLine.Type::"Work Center", WorkCenter2."No."); + RoutingLine.Validate("Setup Time", 15); + RoutingLine.Validate("Run Time", 8); + RoutingLine.Modify(true); + + RoutingHeader.Validate("Version Nos.", LibraryERM.CreateNoSeriesCode()); + RoutingHeader.Validate(Status, RoutingHeader.Status::Certified); + RoutingHeader.Modify(true); + exit(RoutingHeader."No."); + end; + +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/ProdDefWizSetupLib.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/ProdDefWizSetupLib.Codeunit.al new file mode 100644 index 0000000000..66072e6b79 --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/ProdDefWizSetupLib.Codeunit.al @@ -0,0 +1,198 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Manufacturing.Setup; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Manufacturing.WorkCenter; + +codeunit 137421 "Prod. Def. Wiz. Setup Lib." +{ + var + LibraryERM: Codeunit "Library - ERM"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + LibraryInventory: Codeunit "Library - Inventory"; + + procedure InitializeBasicSetup() + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + if ManufacturingSetup."Production BOM Nos." = '' then begin + ManufacturingSetup.Validate("Production BOM Nos.", LibraryERM.CreateNoSeriesCode()); + ManufacturingSetup.Modify(true); + end; + if ManufacturingSetup."Routing Nos." = '' then begin + ManufacturingSetup.Validate("Routing Nos.", LibraryERM.CreateNoSeriesCode()); + ManufacturingSetup.Modify(true); + end; + if ManufacturingSetup."Def. Wiz. Work Center No." = '' then + CreateDefaultWorkCenterAndSetInSetup(); + if ManufacturingSetup."Def. Wiz. Comp Item No." = '' then + CreateDefaultComponentItemAndSetInSetup(); + end; + + procedure SetBOMRoutingDisplayForBothAvailable(Display: Enum "Prod. Definition Display") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Show Rtng BOM Select Both", Display); + ManufacturingSetup.Modify(true); + end; + + procedure SetBOMRoutingDisplayForPartiallyAvailable(Display: Enum "Prod. Definition Display") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Show Rtng BOM Select Partial", Display); + ManufacturingSetup.Modify(true); + end; + + procedure SetBOMRoutingDisplayForNothingAvailable(Display: Enum "Prod. Definition Display") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Show Rtng BOM Select Nothing", Display); + ManufacturingSetup.Modify(true); + end; + + procedure SetProdCompDisplayForBothAvailable(Display: Enum "Prod. Definition Display") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Show Prod Comp Select Both", Display); + ManufacturingSetup.Modify(true); + end; + + procedure SetProdCompDisplayForPartiallyAvailable(Display: Enum "Prod. Definition Display") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Show Prod Comp Select Partial", Display); + ManufacturingSetup.Modify(true); + end; + + procedure SetProdCompDisplayForNothingAvailable(Display: Enum "Prod. Definition Display") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Show Prod Comp Select Nothing", Display); + ManufacturingSetup.Modify(true); + end; + + procedure ConfigureForBothAvailable(BOMRtngDisplay: Enum "Prod. Definition Display"; ProdCompDisplay: Enum "Prod. Definition Display") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Show Rtng BOM Select Both", BOMRtngDisplay); + ManufacturingSetup.Validate("Show Prod Comp Select Both", ProdCompDisplay); + ManufacturingSetup.Modify(true); + end; + + procedure ConfigureForPartiallyAvailable(BOMRtngDisplay: Enum "Prod. Definition Display"; ProdCompDisplay: Enum "Prod. Definition Display") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Show Rtng BOM Select Partial", BOMRtngDisplay); + ManufacturingSetup.Validate("Show Prod Comp Select Partial", ProdCompDisplay); + ManufacturingSetup.Modify(true); + end; + + procedure ConfigureForNothingAvailable(BOMRtngDisplay: Enum "Prod. Definition Display"; ProdCompDisplay: Enum "Prod. Definition Display") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Show Rtng BOM Select Nothing", BOMRtngDisplay); + ManufacturingSetup.Validate("Show Prod Comp Select Nothing", ProdCompDisplay); + ManufacturingSetup.Modify(true); + end; + + procedure SetAllowEditUISelection(Allow: Boolean) + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Allow Edit UI Selection", Allow); + ManufacturingSetup.Modify(true); + end; + + procedure SetAlwaysSaveModifiedVersions(AlwaysSave: Boolean) + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Always Save Modified Versions", AlwaysSave); + ManufacturingSetup.Modify(true); + end; + + procedure GetAlwaysSaveModifiedVersions(): Boolean + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + exit(ManufacturingSetup."Always Save Modified Versions"); + end; + + procedure SetDefWizFlushingMethod(FlushingMethod: Enum "Flushing Method") + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Def. Wiz. Flushing Method", FlushingMethod); + ManufacturingSetup.Modify(true); + end; + + procedure SetDefaultWorkCenter(WorkCenterNo: Code[20]) + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Def. Wiz. Work Center No.", WorkCenterNo); + ManufacturingSetup.Modify(true); + end; + + procedure SetDefaultComponentItem(ItemNo: Code[20]) + var + ManufacturingSetup: Record "Manufacturing Setup"; + begin + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Def. Wiz. Comp Item No.", ItemNo); + ManufacturingSetup.Modify(true); + end; + + procedure CreateDefaultWorkCenterAndSetInSetup(): Code[20] + var + ManufacturingSetup: Record "Manufacturing Setup"; + WorkCenter: Record "Work Center"; + begin + ProdDefWizLibrary.CreateAndCalculateWorkCenter(WorkCenter); + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Def. Wiz. Work Center No.", WorkCenter."No."); + ManufacturingSetup.Modify(true); + exit(WorkCenter."No."); + end; + + procedure CreateDefaultComponentItemAndSetInSetup(): Code[20] + var + Item: Record Item; + ManufacturingSetup: Record "Manufacturing Setup"; + begin + LibraryInventory.CreateItem(Item); + ManufacturingSetup.Get(); + ManufacturingSetup.Validate("Def. Wiz. Comp Item No.", Item."No."); + ManufacturingSetup.Modify(true); + exit(Item."No."); + end; +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/TempProdOrdCompListExt.PageExt.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/TempProdOrdCompListExt.PageExt.al new file mode 100644 index 0000000000..a764af1635 --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/TempProdOrdCompListExt.PageExt.al @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Manufacturing.Wizard; + +pageextension 137435 "Temp ProdOrdCompList Ext." extends "Temp Prod. Order Comp. List" +{ + actions + { + addlast(processing) + { + action(TestDelete) + { + ApplicationArea = All; + Caption = 'Test Delete'; + + trigger OnAction() + begin + Rec.Delete(true); + end; + } + } + } +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/TempProdOrdRtngListExt.PageExt.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/TempProdOrdRtngListExt.PageExt.al new file mode 100644 index 0000000000..faebe08196 --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/Libraries/TempProdOrdRtngListExt.PageExt.al @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Manufacturing.Wizard; + +pageextension 137434 "Temp ProdOrdRtngList Ext." extends "Temp Prod. Ord. Rtng List" +{ + actions + { + addlast(processing) + { + action(TestDelete) + { + ApplicationArea = All; + Caption = 'Test Delete'; + + trigger OnAction() + begin + Rec.Delete(true); + Rec.CheckPreviousAndNextForTemp(); + end; + } + } + } +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizBOMRtngTest.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizBOMRtngTest.Codeunit.al new file mode 100644 index 0000000000..6cc71905d5 --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizBOMRtngTest.Codeunit.al @@ -0,0 +1,694 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Sales.Document; + +codeunit 137426 "Prod. Def. Wiz. BOM Rtng Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + + trigger OnRun() + begin + // [FEATURE] Production Definition Wizard - BOM & Routing Step Behavior + IsInitialized := false; + end; + + var + Assert: Codeunit Assert; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + ProdDefWizSetupLib: Codeunit "Prod. Def. Wiz. Setup Lib."; + ProdDefWizCheckLib: Codeunit "Prod. Def. Wiz. Check Lib."; + IsInitialized: Boolean; + WizardFinished: Boolean; + // Handler state + ActualBOMLineItemNos: List of [Code[20]]; + ActualRoutingLineWCNos: List of [Code[20]]; + ActualComponentItemNos: List of [Code[20]]; + ActualProdRoutingWCNos: List of [Code[20]]; + ActualSelectedBOMVersion: Text; + ActualSelectedRoutingVersion: Text; + TargetBOMNoForLookup: Code[20]; + TargetRoutingNoForLookup: Code[20]; + + + [Test] + [HandlerFunctions('HandleWizardCaptureBOMLineNos')] + procedure TestD1_BOMLoadedFromItemBOM_TwoLines() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ProductionBOMLine: Record "Production BOM Line"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + ActualItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO D1] BOM lines loaded from Item's Production BOM on open + Initialize(); + + // [GIVEN] Item has certified BOM with 2 lines + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard opens, user navigates to Step 2 + Clear(ActualBOMLineItemNos); + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] BOMLinesPart displays exactly 2 lines, each a component of the BOM + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProductionBOMLine.SetRange("Production BOM No.", BOMNo); + ActualBOMLineItemNos.Remove(''); + Assert.AreEqual(ProductionBOMLine.Count(), ActualBOMLineItemNos.Count(), 'BOMLinesPart should display exactly 2 lines from the certified BOM'); + foreach ActualItemNo in ActualBOMLineItemNos do begin + ProductionBOMLine.SetRange("No.", ActualItemNo); + Assert.IsTrue(ProductionBOMLine.FindFirst(), StrSubstNo('BOM component %1 should belong to BOM %2', ActualItemNo, BOMNo)); + end; + + // [THEN] Each BOM line has Quantity per > 0 (lines created with Qty = line index 1, 2, ...) + ProductionBOMLine.Reset(); + ProductionBOMLine.SetRange("Production BOM No.", BOMNo); + ProductionBOMLine.SetRange("Version Code", ''); + if ProductionBOMLine.FindSet() then + repeat + Assert.IsTrue(ProductionBOMLine."Quantity per" > 0, + StrSubstNo('BOM line for component %1 should have Quantity per > 0', ProductionBOMLine."No.")); + until ProductionBOMLine.Next() = 0; + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureRoutingLineNos')] + procedure TestD2_RoutingLoadedFromItemRouting_TwoLines() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + RoutingLine: Record "Routing Line"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + ActualWCNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO D2] Routing lines loaded from Item's Routing on open + Initialize(); + + // [GIVEN] Item has certified Routing with 2 operation lines + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard opens, user navigates to Step 3 + Clear(ActualRoutingLineWCNos); + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] RoutingLinesPart displays exactly 2 lines, each a work center of the Routing + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + RoutingLine.SetRange("Routing No.", RoutingNo); + ActualRoutingLineWCNos.Remove(''); + Assert.AreEqual(RoutingLine.Count(), ActualRoutingLineWCNos.Count(), 'RoutingLinesPart should display exactly 2 lines from the certified Routing'); + foreach ActualWCNo in ActualRoutingLineWCNos do begin + RoutingLine.SetRange("No.", ActualWCNo); + Assert.IsTrue(RoutingLine.FindFirst(), StrSubstNo('Routing work center %1 should belong to Routing %2', ActualWCNo, RoutingNo)); + end; + + // [THEN] Routing lines carry the expected Setup Time and Run Time values + // Line '10': Setup=10, Run=5; Line '20': Setup=15, Run=8 (per CreateRoutingWithTwoLines) + RoutingLine.Reset(); + RoutingLine.SetRange("Routing No.", RoutingNo); + RoutingLine.SetRange("Version Code", ''); + RoutingLine.SetRange("Operation No.", '10'); + Assert.IsTrue(RoutingLine.FindFirst(), 'Routing line operation 10 must exist'); + Assert.AreEqual(10, RoutingLine."Setup Time", 'Routing line 10 Setup Time should be 10'); + Assert.AreEqual(5, RoutingLine."Run Time", 'Routing line 10 Run Time should be 5'); + RoutingLine.SetRange("Operation No.", '20'); + Assert.IsTrue(RoutingLine.FindFirst(), 'Routing line operation 20 must exist'); + Assert.AreEqual(15, RoutingLine."Setup Time", 'Routing line 20 Setup Time should be 15'); + Assert.AreEqual(8, RoutingLine."Run Time", 'Routing line 20 Run Time should be 8'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureSelectedBOMVersion')] + procedure TestD3_ActiveBOMVersionSelectedByDefault() + var + Item: Record Item; + ProductionBOMVersion: Record "Production BOM Version"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO D3] BOM version is selected as default active version + Initialize(); + + // [GIVEN] Item's Production BOM has two certified versions; V2 is active as of WorkDate + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ProdDefWizLibrary.CreateBOMVersionAndCertify(BOMNo, 'V1', CalcDate('<-1Y>', WorkDate())); + ProdDefWizLibrary.CreateBOMVersionAndCertify(BOMNo, 'V2', WorkDate()); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard opens and navigates to Step 2 + ActualSelectedBOMVersion := ''; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] SelectedBOMVersion shows V2 (most recent active version) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual('V2', ActualSelectedBOMVersion, 'SelectedBOMVersion should be V2 (active version as of WorkDate)'); + + // [THEN] BOM version V2 is certified and has the correct Starting Date + ProductionBOMVersion.Get(BOMNo, 'V2'); + Assert.AreEqual(ProductionBOMVersion.Status::Certified, ProductionBOMVersion.Status, + 'BOM version V2 should be Certified'); + Assert.AreEqual(WorkDate(), ProductionBOMVersion."Starting Date", + 'BOM version V2 Starting Date should equal WorkDate()'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureSelectedRoutingVersion')] + procedure TestD4_ActiveRoutingVersionSelectedByDefault() + var + Item: Record Item; + RoutingVersion: Record "Routing Version"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO D4] Routing version is selected as default active version + Initialize(); + + // [GIVEN] Item's Routing has two certified versions; V2 is active as of WorkDate + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ProdDefWizLibrary.CreateRoutingVersionAndCertify(RoutingNo, 'V1', CalcDate('<-1Y>', WorkDate())); + ProdDefWizLibrary.CreateRoutingVersionAndCertify(RoutingNo, 'V2', WorkDate()); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard opens and navigates to Step 3 + ActualSelectedRoutingVersion := ''; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] SelectedRoutingVersion shows V2 (most recent active version as of WorkDate) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual('V2', ActualSelectedRoutingVersion, 'SelectedRoutingVersion should be V2 (active version as of WorkDate)'); + // [THEN] Routing version V2 is certified and has the correct Starting Date + RoutingVersion.Get(RoutingNo, 'V2'); + Assert.AreEqual(RoutingVersion.Status::Certified, RoutingVersion.Status, + 'Routing version V2 should be Certified'); + Assert.AreEqual(WorkDate(), RoutingVersion."Starting Date", + 'Routing version V2 Starting Date should equal WorkDate()'); + end; + + [Test] + [HandlerFunctions('HandleWizardBOMAssistEditAndCaptureLines,HandleProductionBOMListSelect')] + procedure TestD5_AssistEditBOMNo_ReloadsBOMLines() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ProductionBOMLine: Record "Production BOM Line"; + BOMBNo: Code[20]; + OriginalBOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + ActualItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO D5] AssistEdit on BOM No. selects a different BOM → BOM lines reload from the new BOM + Initialize(); + + // [GIVEN] Item has BOM-B (3 lines); display = Edit + BOMBNo := ProdDefWizLibrary.CreateBOM(3); // BOM-B to switch to + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + OriginalBOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(OriginalBOMNo, RoutingNo); + Item.Get(ItemNo); + TargetBOMNoForLookup := BOMBNo; + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User uses AssistEdit on BOM No. to select BOM-B + Clear(ActualBOMLineItemNos); + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] BOMLinesPart now shows 3 lines from BOM-B, each a component of BOM-B + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProductionBOMLine.SetRange("Production BOM No.", BOMBNo); + ActualBOMLineItemNos.Remove(''); + Assert.AreEqual(ProductionBOMLine.Count(), ActualBOMLineItemNos.Count(), 'BOMLinesPart should display 3 lines from the newly selected BOM-B'); + foreach ActualItemNo in ActualBOMLineItemNos do begin + ProductionBOMLine.SetRange("No.", ActualItemNo); + Assert.IsTrue(ProductionBOMLine.FindFirst(), StrSubstNo('BOM component %1 should belong to BOM-B %2', ActualItemNo, BOMBNo)); + end; + + // [THEN] Item BOM No. is changed to TargetBOMNoForLookup in the database (Save=true → new BOM retained) + Item.Get(ItemNo); + Assert.AreEqual(TargetBOMNoForLookup, Item."Production BOM No.", + 'Item Production BOM No. should be updated to the new BOM after wizard when Save=true'); + end; + + [Test] + [HandlerFunctions('HandleWizardRoutingAssistEditAndCaptureLines,HandleRoutingListSelect')] + procedure TestD6_AssistEditRoutingNo_ReloadsRoutingLines() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + RoutingLine: Record "Routing Line"; + BOMNo: Code[20]; + RoutingBNo: Code[20]; + OriginalRoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + WC3No: Code[20]; + ActualWCNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO D6] AssistEdit on Routing No. selects a different Routing → Routing lines reload + Initialize(); + + // [GIVEN] Item has Routing-A (1 line); Routing-B (2 lines) also exists; display = Edit + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingBNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); // Routing-B to switch to + OriginalRoutingNo := ProdDefWizLibrary.CreateRoutingWithSingleLine(WC3No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, OriginalRoutingNo); + Item.Get(ItemNo); + TargetRoutingNoForLookup := RoutingBNo; + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User uses AssistEdit on Routing No. to select Routing-B + Clear(ActualRoutingLineWCNos); + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] RoutingLinesPart now shows 2 lines from Routing-B, each a work center of Routing-B + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + RoutingLine.SetRange("Routing No.", RoutingBNo); + ActualRoutingLineWCNos.Remove(''); + Assert.AreEqual(RoutingLine.Count(), ActualRoutingLineWCNos.Count(), 'RoutingLinesPart should display 2 lines from the newly selected Routing-B'); + foreach ActualWCNo in ActualRoutingLineWCNos do begin + RoutingLine.SetRange("No.", ActualWCNo); + Assert.IsTrue(RoutingLine.FindFirst(), StrSubstNo('Routing work center %1 should belong to Routing-B %2', ActualWCNo, RoutingBNo)); + end; + + // [THEN] Item Routing No. is updated in the database (Save=true → new Routing retained) + Item.Get(ItemNo); + Assert.AreEqual(TargetRoutingNoForLookup, Item."Routing No.", + 'Item Routing No. should be updated to the new Routing after wizard when Save=true'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureCompAndRoutingNos')] + procedure TestD7_ComponentsAndRoutingPreview_CorrectFromBOMAndRouting() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + ProdOrderComponent: Record "Prod. Order Component"; + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + LocationCode: Code[10]; + ActualItemNo: Code[20]; + ActualNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO D7] Components Preview (Step 4) and Routing Preview (Step 5) show correct lines + Initialize(); + + // [GIVEN] Sales Line for item with BOM (2 components) and Routing (2 operations); ProdComponentDisplay = Show + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Show, "Prod. Definition Display"::Show); + + // [WHEN] User navigates to Steps 4 and 5 and finishes (creates a Production Order) + Clear(ActualComponentItemNos); + Clear(ActualProdRoutingWCNos); + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] ComponentsPart shows items matching the created Prod. Order components; + // ProdOrderRoutingPart shows work centers matching the created Prod. Order routing lines + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdOrderComponent.SetRange(Status, ProdOrder.Status); + ProdOrderComponent.SetRange("Prod. Order No.", ProdOrder."No."); + ActualComponentItemNos.Remove(''); + Assert.AreEqual(ProdOrderComponent.Count(), ActualComponentItemNos.Count(), 'ComponentsPart should list 2 lines matching the Prod. Order components'); + foreach ActualItemNo in ActualComponentItemNos do begin + ProdOrderComponent.SetRange("Item No.", ActualItemNo); + Assert.IsTrue(ProdOrderComponent.FindFirst(), StrSubstNo('Component item %1 should exist in the created Prod. Order', ActualItemNo)); + end; + ProdOrderRoutingLine.SetRange(Status, ProdOrder.Status); + ProdOrderRoutingLine.SetRange("Prod. Order No.", ProdOrder."No."); + ActualProdRoutingWCNos.Remove(''); + Assert.AreEqual(ProdOrderRoutingLine.Count(), ActualProdRoutingWCNos.Count(), 'ProdOrderRoutingPart should list 2 lines matching the Prod. Order routing lines'); + foreach ActualNo in ActualProdRoutingWCNos do begin + ProdOrderRoutingLine.SetRange("No.", ActualNo); + Assert.IsTrue(ProdOrderRoutingLine.FindFirst(), StrSubstNo('Work center %1 should exist in the created Prod. Order routing', ActualNo)); + end; + + // [THEN] Prod. Order fields: Qty = 5 (from SalesLine), Due Date = WorkDate() +1, Location matches SalesLine + ProdDefWizCheckLib.VerifyProdOrderFields(ProdOrder, ItemNo, 5, WorkDate(), LocationCode, ''); + // [THEN] First component has Quantity per = 1 (BOM created with Qty = line index starting at 1) + ProdDefWizCheckLib.VerifyProdOrderComponentHasQtyPerForFirstComponent(ProdOrder, 1); + end; + + + [ModalPageHandler] + procedure HandleWizardCaptureBOMLineNos(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM step) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Collect BOM line component item Nos + Clear(ActualBOMLineItemNos); + if Wizard.BOMLinesPart.First() then + repeat + ActualBOMLineItemNos.Add(CopyStr(Wizard.BOMLinesPart."No.".Value(), 1, 20)); + until not Wizard.BOMLinesPart.Next(); + // Continue to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCaptureRoutingLineNos(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Collect Routing line work center Nos + Clear(ActualRoutingLineWCNos); + if Wizard.RoutingLinesPart.First() then + repeat + ActualRoutingLineWCNos.Add(CopyStr(Wizard.RoutingLinesPart."No.".Value(), 1, 20)); + until not Wizard.RoutingLinesPart.Next(); + // Continue to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCaptureSelectedBOMVersion(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM step) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Capture selected BOM version + ActualSelectedBOMVersion := Wizard.SelectedBOMVersionField.Value(); + // Continue to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCaptureSelectedRoutingVersion(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Capture selected Routing version + ActualSelectedRoutingVersion := Wizard.SelectedRoutingVersionField.Value(); + // Continue to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardBOMAssistEditAndCaptureLines(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM step) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Trigger AssistEdit on BOM No. field → opens Production BOM List (handled by HandleProductionBOMListSelect) + Wizard.ProductionBOMNoField.AssistEdit(); + // Collect BOM line item Nos after the new BOM was selected + Clear(ActualBOMLineItemNos); + if Wizard.BOMLinesPart.First() then + repeat + ActualBOMLineItemNos.Add(CopyStr(Wizard.BOMLinesPart."No.".Value(), 1, 20)); + until not Wizard.BOMLinesPart.Next(); + // Continue to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleProductionBOMListSelect(var ProductionBOMList: TestPage "Production BOM List") + begin + ProductionBOMList.Filter.SetFilter("No.", TargetBOMNoForLookup); + ProductionBOMList.First(); + ProductionBOMList.OK.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardRoutingAssistEditAndCaptureLines(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing step) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Trigger AssistEdit on Routing No. field → opens Routing List (handled by HandleRoutingListSelect) + Wizard.RoutingNoField.AssistEdit(); + // Collect Routing line work center Nos after the new Routing was selected + Clear(ActualRoutingLineWCNos); + if Wizard.RoutingLinesPart.First() then + repeat + ActualRoutingLineWCNos.Add(CopyStr(Wizard.RoutingLinesPart."No.".Value(), 1, 20)); + until not Wizard.RoutingLinesPart.Next(); + // Continue to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleRoutingListSelect(var RoutingList: TestPage "Routing List") + begin + RoutingList.Filter.SetFilter("No.", TargetRoutingNoForLookup); + RoutingList.First(); + RoutingList.OK.Invoke(); + end; + + + [ModalPageHandler] + procedure HandleWizardCaptureCompAndRoutingNos(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate through BOM and Routing steps + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); // Step 2 BOM + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); // Step 3 Routing + // Step 4: Components — collect component item Nos + if Wizard.ActionNext.Enabled() then begin + Wizard.ActionNext.Invoke(); + Clear(ActualComponentItemNos); + if Wizard.ComponentsPart.First() then + repeat + ActualComponentItemNos.Add(CopyStr(Wizard.ComponentsPart."Item No.".Value(), 1, 20)); + until not Wizard.ComponentsPart.Next(); + end; + // Step 5: Prod. Routing — collect work center Nos + if Wizard.ActionNext.Enabled() then begin + Wizard.ActionNext.Invoke(); + Clear(ActualProdRoutingWCNos); + if Wizard.ProdOrderRoutingPart.First() then + repeat + ActualProdRoutingWCNos.Add(CopyStr(Wizard.ProdOrderRoutingPart."No.".Value(), 1, 20)); + until not Wizard.ProdOrderRoutingPart.Next(); + end; + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + + [Test] + [HandlerFunctions('HandleWizardCaptureCompAndRoutingNos')] + procedure TestD8_BOMLineFields_TransferredToProdOrderComponent() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + ComponentItemNo: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO D8] BOM line fields (Length, Width, Weight, Depth, Scrap %, Calculation Formula) are transferred to Prod. Order Component on Production Order creation + Initialize(); + + // [GIVEN] BOM with one component line having dimensional fields, Scrap % = 5 and Calculation Formula = Fixed Quantity + BOMNo := ProdDefWizLibrary.CreateBOMWithDimensionalFields(ComponentItemNo); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithSingleLine(WC1No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 1, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Show, "Prod. Definition Display"::Show); + + // [WHEN] Wizard creates a Production Order from the Sales Line + Clear(ActualComponentItemNos); + Clear(ActualProdRoutingWCNos); + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Prod. Order Component has Scrap % = 5, Length = 2, Width = 3, Weight = 4, Depth = 1 from BOM line + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderComponentBOMFields(ProdOrder, ComponentItemNo, 5, 2, 3, 4, 1); + // [THEN] Prod. Order Component has Calculation Formula = Fixed Quantity from BOM line + ProdDefWizCheckLib.VerifyProdOrderComponentCalcFormula(ProdOrder, ComponentItemNo); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureCompAndRoutingNos')] + procedure TestD9_RoutingLineFields_TransferredToProdOrderRoutingLine() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + CapUOMCode: Code[10]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO D9] Routing line fields (UOM codes, Fixed Scrap Qty, Scrap Factor %, Send-Ahead Qty, Concurrent Capacities, Lot Size, Prev/Next Operation No.) are transferred to Prod. Order Routing Line on Production Order creation + Initialize(); + + // [GIVEN] Routing with two lines where operation '10' has all extended capacity and scrap fields set; after certification Prev/Next Op No. are auto-populated + BOMNo := ProdDefWizLibrary.CreateBOM(1); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLinesAndExtendedFields(WC1No, WC2No, CapUOMCode); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 1, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Show, "Prod. Definition Display"::Show); + + // [WHEN] Wizard creates a Production Order from the Sales Line + Clear(ActualComponentItemNos); + Clear(ActualProdRoutingWCNos); + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Prod. Order Routing Line '10' has all extended fields transferred from Routing Line + // Previous Op No. = '' (first in serial routing), Next Op No. = '20' (auto-populated on certification) + // All time UOM codes match the created Capacity UOM; Fixed Scrap Qty = 3, Scrap Factor % = 10, + // Send-Ahead Qty = 5, Concurrent Capacities = 2, Lot Size = 1 + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderRoutingLineExtendedFields( + ProdOrder, '10', '', '20', CapUOMCode, CapUOMCode, CapUOMCode, CapUOMCode, 3, 10, 5, 2, 1); + end; + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Prod. Def. Wiz. BOM Rtng Test"); + LibrarySetupStorage.Restore(); + WizardFinished := false; + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. BOM Rtng Test"); + LibraryERMCountryData.CreateVATData(); + LibraryERMCountryData.UpdateGeneralPostingSetup(); + ProdDefWizSetupLib.InitializeBasicSetup(); + + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + IsInitialized := true; + Commit(); + LibrarySetupStorage.SaveManufacturingSetup(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. BOM Rtng Test"); + end; +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizChangeTest.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizChangeTest.Codeunit.al new file mode 100644 index 0000000000..95d10081f1 --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizChangeTest.Codeunit.al @@ -0,0 +1,449 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Sales.Document; + +codeunit 137429 "Prod. Def. Wiz. Change Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + + trigger OnRun() + begin + // [FEATURE] Production Definition Wizard - Change / Re-selection During Wizard + IsInitialized := false; + end; + + var + Assert: Codeunit Assert; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + ProdDefWizSetupLib: Codeunit "Prod. Def. Wiz. Setup Lib."; + ProdDefWizCheckLib: Codeunit "Prod. Def. Wiz. Check Lib."; + IsInitialized: Boolean; + WizardFinished: Boolean; + // Handler state + ActualBOMNoAfterBackForward: Text; + ActualEditBOMAfterOverride: Boolean; + ActualBOMStepVisible: Boolean; + ActualBOMNoAfterRoutingChange: Text; + TargetRoutingNoForH2: Code[20]; + ActualCreateBOMVersionAfterVersionSelect: Boolean; + ActualEditBOMAfterVersionSelect: Boolean; + ActualSelectedBOMVersionAfterAssistEdit: Text; + TargetBOMVersionForH5: Code[20]; + ActualComponentCountAfterBackForward: Integer; + + + [Test] + [HandlerFunctions('HandleWizardChangeBOMNavigateBackForward')] + procedure TestH1_ChangeBOMOnStep2_NavigateBackForward_BOMRetained() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + OriginalBOMNo: Code[20]; + ItemNo: Code[20]; + RoutingNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO H1] Change BOM No. on Step 2, step back to Step 1 and forward again → new BOM retained + Initialize(); + + // [GIVEN] Wizard on Step 2; user has item with BOM + OriginalBOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(OriginalBOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User navigates Back to Step 1 and then Next again + ActualBOMNoAfterBackForward := ''; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] SelectedBOMNo is retained; BOM lines match selected BOM + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + // The actual verification is done inside the handler: + // after Back and Forward, SelectedBOMNo should equal OriginalBOMNo (or whatever was set on Step 2) + Assert.AreEqual(OriginalBOMNo, ActualBOMNoAfterBackForward, 'SelectedBOMNo should be retained after Back/Forward navigation'); + // Confirm Item still carries the correct BOM after wizard finishes + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, OriginalBOMNo); + ProdDefWizCheckLib.VerifyItemHasRouting(ItemNo, RoutingNo); + end; + + [Test] + [HandlerFunctions('HandleWizardChangeRoutingAndCaptureBOM,HandleRoutingListSelectForH2')] + procedure TestH2_ChangeRoutingOnStep3_BOMSelectionOnStep2Unchanged() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + OriginalBOMNo: Code[20]; + ItemNo: Code[20]; + RoutingANo: Code[20]; + RoutingBNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + WC3No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO H2] Change Routing No. on Step 3, go back to Step 2 → BOM selection is unchanged + Initialize(); + + // [GIVEN] Item with BOM-A and Routing-A; second Routing-B also exists + OriginalBOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingANo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + RoutingBNo := ProdDefWizLibrary.CreateRoutingWithSingleLine(WC3No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(OriginalBOMNo, RoutingANo); + Item.Get(ItemNo); + TargetRoutingNoForH2 := RoutingBNo; + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User navigates to Step 3, changes Routing to Routing-B, then goes back to Step 2 + ActualBOMNoAfterRoutingChange := ''; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] SelectedBOMNo on Step 2 is still OriginalBOMNo (routing change did not affect BOM) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual(OriginalBOMNo, ActualBOMNoAfterRoutingChange, + 'SelectedBOMNo should remain unchanged after Routing change on Step 3'); + // Confirm Item BOM is unchanged and Routing is updated to Routing-B after wizard completes + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, OriginalBOMNo); + ProdDefWizCheckLib.VerifyItemHasRouting(ItemNo, RoutingBNo); + end; + + [Test] + [HandlerFunctions('HandleWizardOverrideBOMRoutingDisplayToEdit')] + procedure TestH3_ChangeBOMRoutingDisplayToEdit_BOMStepEditable() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO H3] Changing BOMRoutingDisplay on Step 1 from Show to Edit → BOM step becomes editable + Initialize(); + + // [GIVEN] Allow Edit UI Selection = true; default BOMRoutingDisplay = Show + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Show, "Prod. Definition Display"::Show); + ProdDefWizSetupLib.SetAllowEditUISelection(true); + + // [WHEN] User changes BOMRoutingDisplay to Edit on Step 1, then navigates to Step 2 + ActualEditBOMAfterOverride := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] EditBOMLines = true; Item BOM and Routing unchanged (Save = false) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.IsTrue(ActualEditBOMAfterOverride, 'BOM lines should be editable after user overrides BOMRoutingDisplay to Edit'); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyItemHasRouting(ItemNo, RoutingNo); + end; + + [Test] + [HandlerFunctions('HandleWizardOverrideBOMRoutingDisplayToHide')] + procedure TestH4_ChangeBOMRoutingDisplayToHide_BOMStepSkipped() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO H4] Changing BOMRoutingDisplay to Hide on Step 1 → BOM and Routing steps are skipped + Initialize(); + + // [GIVEN] Allow Edit UI Selection = true; user changes BOMRoutingDisplay to Hide on Step 1 + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + ProdDefWizSetupLib.SetAllowEditUISelection(true); + + // [WHEN] User clicks Next + ActualBOMStepVisible := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] BOM step is not shown; Item BOM and Routing unchanged (Save = false) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.IsFalse(ActualBOMStepVisible, 'BOM step should not be visible after changing BOMRoutingDisplay to Hide'); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyItemHasRouting(ItemNo, RoutingNo); + end; + + [Test] + [HandlerFunctions('HandleWizardH5CreateBOMVersionThenSelectVersion,HandleBOMVersionListForH5')] + procedure TestH5_CreateBOMVersionOn_AssistEditSelectsExistingVersion_CreateBOMVersionCleared() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO H5] Create New BOM Version = true; user AssistEdits SelectedBOMVersion to pick existing certified version → CreateBOMVersion reset to false; BOM lines reload from existing version + Initialize(); + + // [GIVEN] Item with certified BOM; a certified BOM version V001 exists + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ProdDefWizLibrary.CreateBOMVersionAndCertify(BOMNo, 'V001', WorkDate()); + TargetBOMVersionForH5 := 'V001'; + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User enables CreateBOMVersion then AssistEdits to pick V001 + ActualCreateBOMVersionAfterVersionSelect := true; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] CreateBOMVersion = false; BOM lines not editable (existing certified version); Item BOM unchanged (Save = false) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.IsFalse(ActualCreateBOMVersionAfterVersionSelect, 'CreateBOMVersion should be reset to false after selecting existing certified version'); + Assert.IsFalse(ActualEditBOMAfterVersionSelect, 'BOM lines should not be editable when existing certified version is selected'); + Assert.AreEqual('V001', ActualSelectedBOMVersionAfterAssistEdit, 'SelectedBOMVersion should be V001 after AssistEdit selects the existing certified version'); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, BOMNo); + // [THEN] BOM version V001 has exactly 1 line (from CreateBOMVersionAndCertify); verifies BOM lines were reloaded from the correct version + ProdDefWizCheckLib.VerifyBOMVersionLineCount(BOMNo, 'V001', 1); + end; + + + [ModalPageHandler] + procedure HandleWizardChangeBOMNavigateBackForward(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 + Wizard.ActionNext.Invoke(); + // Capture SelectedBOMNo on Step 2 + // (no change — just note what was selected) + // Navigate Back to Step 1 + Wizard.ActionBack.Invoke(); + // Navigate Next to Step 2 again + Wizard.ActionNext.Invoke(); + // Re-capture SelectedBOMNo — should be unchanged + ActualBOMNoAfterBackForward := Wizard.ProductionBOMNoField.Value(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardOverrideBOMRoutingDisplayToEdit(var Wizard: TestPage "Production Definition Wizard") + begin + // On Step 1, override BOMRoutingDisplay to Edit + Wizard.BOMRoutingDisplayField.SetValue('Edit'); + // Navigate to Step 2 + Wizard.ActionNext.Invoke(); + // Capture editability of BOM lines + if Wizard.CreateBOMVersionField.Enabled() then + Wizard.CreateBOMVersionField.SetValue(true); // ensure CreateBOMVersion does not affect editability + ActualEditBOMAfterOverride := Wizard.BOMLinesPart.Enabled(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardOverrideBOMRoutingDisplayToHide(var Wizard: TestPage "Production Definition Wizard") + begin + // On Step 1, override BOMRoutingDisplay to Hide + Wizard.BOMRoutingDisplayField.SetValue('Hide'); + // Navigate through all steps; use OR-pattern so any accidental BOM visibility is caught + if Wizard.BOMLinesPart.Visible() then + ActualBOMStepVisible := true; + while Wizard.ActionNext.Enabled() do begin + Wizard.ActionNext.Invoke(); + if Wizard.BOMLinesPart.Visible() then + ActualBOMStepVisible := true; + end; + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardChangeRoutingAndCaptureBOM(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) + Wizard.ActionNext.Invoke(); + // Change Routing No. via AssistEdit → opens Routing List (handled by HandleRoutingListSelectForH2) + Wizard.RoutingNoField.AssistEdit(); + // Navigate back to Step 2 + Wizard.ActionBack.Invoke(); + // Capture SelectedBOMNo — should be unchanged + ActualBOMNoAfterRoutingChange := Wizard.ProductionBOMNoField.Value(); + // Finish the wizard + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleRoutingListSelectForH2(var RoutingList: TestPage "Routing List") + begin + RoutingList.Filter.SetFilter("No.", TargetRoutingNoForH2); + RoutingList.First(); + RoutingList.OK.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardH5CreateBOMVersionThenSelectVersion(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Enable "Create New BOM Version" + Wizard.CreateBOMVersionField.SetValue(true); + // Use AssistEdit on SelectedBOMVersionField to pick an existing certified version + // (opens "Prod. BOM Version List" — handled by HandleBOMVersionListForH5) + Wizard.SelectedBOMVersionField.AssistEdit(); + // Capture SelectedBOMVersion — should be 'V001' after selecting the existing version + ActualSelectedBOMVersionAfterAssistEdit := Wizard.SelectedBOMVersionField.Value(); + // Capture CreateBOMVersion — should be false after selecting existing version + Evaluate(ActualCreateBOMVersionAfterVersionSelect, Wizard.CreateBOMVersionField.Value()); + // Capture BOM lines editability — should be false (existing version is not editable) + ActualEditBOMAfterVersionSelect := Wizard.BOMLinesPart.Enabled(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleBOMVersionListForH5(var BOMVersionList: TestPage "Prod. BOM Version List") + begin + BOMVersionList.Filter.SetFilter("Version Code", TargetBOMVersionForH5); + BOMVersionList.First(); + BOMVersionList.OK.Invoke(); + end; + + [Test] + [HandlerFunctions('HandleWizardBackFromComponentsAndCount')] + procedure TestH6_BackNavigateFromComponentsStep_ComponentCountStableAfterReEntry() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO H6] User navigates to Step 4 (Components), Back to Step 3, Forward to Step 4 again. + // The Production Order created on Finish must have exactly 2 components + // (not 4 due to stale BuildTemporaryStructureFromBOMRouting accumulation). + Initialize(); + + // [GIVEN] SalesLine for item with BOM of 2 lines and Routing; ProdComponentDisplay = Edit + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User navigates to Step 4 (Components), Back to Step 3 (Routing), Forward to Step 4 again, then Finish + ActualComponentCountAfterBackForward := 0; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Wizard finished; ProdOrder has exactly 2 components (not 4 from stale temp-data) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasComponentCount(ProdOrder, 2); + end; + + [ModalPageHandler] + procedure HandleWizardBackFromComponentsAndCount(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) + Wizard.ActionNext.Invoke(); + // Navigate to Step 4 (Components) + Wizard.ActionNext.Invoke(); + // Navigate BACK to Step 3 (triggers stale-accumulation bug on re-entry to Step 4) + Wizard.ActionBack.Invoke(); + // Navigate FORWARD to Step 4 again + Wizard.ActionNext.Invoke(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Prod. Def. Wiz. Change Test"); + LibrarySetupStorage.Restore(); + WizardFinished := false; + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Change Test"); + LibraryERMCountryData.CreateVATData(); + LibraryERMCountryData.UpdateGeneralPostingSetup(); + ProdDefWizSetupLib.InitializeBasicSetup(); + + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + IsInitialized := true; + Commit(); + LibrarySetupStorage.SaveManufacturingSetup(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Change Test"); + end; +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizConfigTest.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizConfigTest.Codeunit.al new file mode 100644 index 0000000000..f2c026278d --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizConfigTest.Codeunit.al @@ -0,0 +1,505 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Sales.Document; + +codeunit 137425 "Prod. Def. Wiz. Config Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + + trigger OnRun() + begin + // [FEATURE] Production Definition Wizard - Configuration / Display Settings + IsInitialized := false; + end; + + var + Assert: Codeunit Assert; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + ProdDefWizSetupLib: Codeunit "Prod. Def. Wiz. Setup Lib."; + IsInitialized: Boolean; + WizardFinished: Boolean; + // Handler state + ActualEditBOMLines: Boolean; + CreateBOMVersionFieldEnabled: Boolean; + ActualShowEditOptionsEnabled: Boolean; + ActualBOMStepVisible: Boolean; + ActualRoutingStepVisible: Boolean; + ActualComponentsStepVisible: Boolean; + ActualProdRoutingStepVisible: Boolean; + ActualProdCompDisplayValue: Text; + ActualBOMRoutingDisplayValue: Text; + + + [Test] + [HandlerFunctions('HandleWizardTrackStepVisibility')] + procedure TestC1_BOMRoutingDisplayHide_DefineItemStructureMode_ForcedToEdit() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO C1] In DefineItemStructure mode, BOMRoutingDisplay is always forced to Edit — setup Hide is ignored + Initialize(); + + // [GIVEN] Manufacturing Setup: ShowRtngBOMSelect_Both = Hide; item has both BOM and Routing + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Hide, "Prod. Definition Display"::Hide); + + // [WHEN] Wizard is opened in DefineItemStructure mode + ActualBOMStepVisible := false; + ActualRoutingStepVisible := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Step 2 (BOM) and Step 3 (Routing) are visible because DefineItemStructure always forces Edit + Assert.IsTrue(ActualBOMStepVisible, 'BOM step (Step 2) should be visible in DefineItemStructure mode regardless of setup'); + Assert.IsTrue(ActualRoutingStepVisible, 'Routing step (Step 3) should be visible in DefineItemStructure mode regardless of setup'); + end; + + [Test] + [HandlerFunctions('HandleWizardNavigateToStep2AndCaptureEditability')] + procedure TestC2_BOMRoutingDisplayShow_DefineItemStructureMode_ForcedToEdit() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO C2] In DefineItemStructure mode, BOMRoutingDisplay is always forced to Edit — setup Show is overridden + Initialize(); + + // [GIVEN] Manufacturing Setup: ShowRtngBOMSelect_Both = Show + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Show, "Prod. Definition Display"::Show); + + // [WHEN] Wizard opens on Step 2 + ActualEditBOMLines := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] BOMLinesPart.Enabled = true because DefineItemStructure mode forces Edit regardless of Show setup + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.IsFalse(ActualEditBOMLines, 'BOM lines should be editable in DefineItemStructure mode regardless of Show setting'); + Assert.IsTrue(CreateBOMVersionFieldEnabled, 'CreateBOMVersionField should be enabled in DefineItemStructure mode regardless of Show setting'); + end; + + [Test] + [HandlerFunctions('HandleWizardTrackStepVisibility')] + procedure TestC3_ProdCompDisplayHide_Steps4And5Skipped() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO C3] ProdComponentDisplay = Hide → Steps 4 and 5 are skipped + Initialize(); + + // [GIVEN] Mode = CreateProductionOrder; ShowProdCompSelect_Both = Hide + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Show, "Prod. Definition Display"::Hide); + + // [WHEN] User navigates from Step 3 + ActualComponentsStepVisible := false; + ActualProdRoutingStepVisible := false; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Steps 4 and 5 are not visible + Assert.IsFalse(ActualComponentsStepVisible, 'Components step (Step 4) should not be visible when ProdComp = Hide'); + Assert.IsFalse(ActualProdRoutingStepVisible, 'Prod. Routing step (Step 5) should not be visible when ProdComp = Hide'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureProdCompDisplay')] + procedure TestC4_ProdCompDisplayShow_StepVisibleAndDisplayIsHide() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO C4] ProdCompDisplay = Show → Components step is not visible; display mode is Hide (read-only), because of definition mode = DefineItemStructure + Initialize(); + + // [GIVEN] Mode = CreateProductionOrder; ProdComponentDisplay = Show + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Show); + + // [WHEN] Wizard opens and user navigates to Step 4 + ActualProdCompDisplayValue := ''; + ActualComponentsStepVisible := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Components step is not visible; ProdCompDisplayField = 'Hide' + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.IsFalse(ActualComponentsStepVisible, 'Components step (Step 4) should not be visible when ProdComp = Show'); + Assert.AreEqual('Hide', ActualProdCompDisplayValue, 'ProdCompDisplayField should be Hide'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureShowEditOptionsEnabled')] + procedure TestC5_AllowEditUISelectionTrue_GroupEnabled() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO C5] Allow Edit UI Selection = true → Show/Edit options group is enabled on Step 1 + Initialize(); + + // [GIVEN] Manufacturing Setup: Allow Edit UI Selection = true + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.SetAllowEditUISelection(true); + + // [WHEN] Wizard opens on Step 1 + ActualShowEditOptionsEnabled := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] ShowEditOptionsGroup.Enabled = true + Assert.IsTrue(ActualShowEditOptionsEnabled, 'Show/Edit options group should be enabled when Allow Edit UI Selection = true'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureShowEditOptionsEnabled')] + procedure TestC6_AllowEditUISelectionFalse_GroupDisabled() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO C6] Allow Edit UI Selection = false → Show/Edit options group is disabled on Step 1 + Initialize(); + + // [GIVEN] Manufacturing Setup: Allow Edit UI Selection = false + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.SetAllowEditUISelection(false); + + // [WHEN] Wizard opens on Step 1 + ActualShowEditOptionsEnabled := true; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] ShowEditOptionsGroup.Enabled = false + Assert.IsFalse(ActualShowEditOptionsEnabled, 'Show/Edit options group should be disabled when Allow Edit UI Selection = false'); + end; + + + [ModalPageHandler] + procedure HandleWizardTrackStepVisibility(var Wizard: TestPage "Production Definition Wizard") + begin + // Record which steps are visible at any point during navigation + if Wizard.BOMLinesPart.Visible() then + ActualBOMStepVisible := true; + if Wizard.RoutingLinesPart.Visible() then + ActualRoutingStepVisible := true; + if Wizard.ComponentsPart.Visible() then + ActualComponentsStepVisible := true; + if Wizard.ProdOrderRoutingPart.Visible() then + ActualProdRoutingStepVisible := true; + + while Wizard.ActionNext.Enabled() do begin + Wizard.ActionNext.Invoke(); + if Wizard.BOMLinesPart.Visible() then + ActualBOMStepVisible := true; + if Wizard.RoutingLinesPart.Visible() then + ActualRoutingStepVisible := true; + if Wizard.ComponentsPart.Visible() then + ActualComponentsStepVisible := true; + if Wizard.ProdOrderRoutingPart.Visible() then + ActualProdRoutingStepVisible := true; + end; + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardNavigateToStep2AndCaptureEditability(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM step) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Capture editability + ActualEditBOMLines := Wizard.BOMLinesPart.Enabled(); + CreateBOMVersionFieldEnabled := Wizard.CreateBOMVersionField.Enabled(); + + // Continue to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCaptureShowEditOptionsEnabled(var Wizard: TestPage "Production Definition Wizard") + begin + // On Step 1, capture ShowEditOptionsEnabled + ActualShowEditOptionsEnabled := Wizard.BOMRoutingDisplayField.Enabled(); + // Finish the wizard + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCaptureProdCompDisplay(var Wizard: TestPage "Production Definition Wizard") + begin + // On Step 1, capture ProdCompDisplayField value + ActualProdCompDisplayValue := Wizard.ProdCompDisplayField.Value(); + // Navigate through steps; capture ComponentsStepVisible when on that step + while Wizard.ActionNext.Enabled() do begin + Wizard.ActionNext.Invoke(); + if Wizard.ComponentsPart.Visible() then + ActualComponentsStepVisible := true; + end; + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + + [Test] + [HandlerFunctions('HandleWizardCaptureDisplayFieldValues')] + procedure TestC7_ScenarioNothingAvailable_DisplayFieldsFromNothingSetupFields() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO C7] NothingAvailable scenario → BOMRoutingDisplay and ProdCompDisplay resolved from ShowRtngBOMSelect_Nothing / ShowProdCompSelect_Nothing + Initialize(); + + // [GIVEN] Sales Line for item with no BOM and no Routing; Nothing = Show, Both = Edit, Partial = Hide + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Hide, "Prod. Definition Display"::Hide); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Show, "Prod. Definition Display"::Show); + + // [WHEN] Wizard is open + ActualBOMRoutingDisplayValue := ''; + ActualProdCompDisplayValue := ''; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Display fields come from Nothing scenario settings + Assert.AreEqual('Show', ActualBOMRoutingDisplayValue, 'BOMRoutingDisplay should come from ShowRtngBOMSelect_Nothing = Show'); + Assert.AreEqual('Show', ActualProdCompDisplayValue, 'ProdCompDisplay should come from ShowProdCompSelect_Nothing = Show'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureDisplayFieldValues')] + procedure TestC8_ScenarioPartiallyAvailable_DisplayFieldsFromPartialSetupFields() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO C8] PartiallyAvailable scenario → BOMRoutingDisplay and ProdCompDisplay resolved from ShowRtngBOMSelect_Partial / ShowProdCompSelect_Partial + Initialize(); + + // [GIVEN] Sales Line for item with BOM only (no Routing); Partial = Edit, Both = Show, Nothing = Hide + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Show, "Prod. Definition Display"::Show); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Hide, "Prod. Definition Display"::Hide); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is open + ActualBOMRoutingDisplayValue := ''; + ActualProdCompDisplayValue := ''; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Display fields come from Partial scenario settings + Assert.AreEqual('Edit', ActualBOMRoutingDisplayValue, 'BOMRoutingDisplay should come from ShowRtngBOMSelect_Partial = Edit'); + Assert.AreEqual('Edit', ActualProdCompDisplayValue, 'ProdCompDisplay should come from ShowProdCompSelect_Partial = Edit'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureDisplayFieldValues')] + procedure TestC9_ScenarioBothAvailable_DisplayFieldsFromBothSetupFields() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO C9] BothAvailable scenario → BOMRoutingDisplay and ProdCompDisplay resolved from ShowRtngBOMSelect_Both / ShowProdCompSelect_Both + Initialize(); + + // [GIVEN] Sales Line for item with both BOM and Routing; Both = Show, Partial = Edit, Nothing = Edit + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Show, "Prod. Definition Display"::Show); + + // [WHEN] Wizard is open + ActualBOMRoutingDisplayValue := ''; + ActualProdCompDisplayValue := ''; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Display fields come from Both scenario settings + Assert.AreEqual('Show', ActualBOMRoutingDisplayValue, 'BOMRoutingDisplay should come from ShowRtngBOMSelect_Both = Show'); + Assert.AreEqual('Show', ActualProdCompDisplayValue, 'ProdCompDisplay should come from ShowProdCompSelect_Both = Show'); + end; + + [ModalPageHandler] + procedure HandleWizardCaptureDisplayFieldValues(var Wizard: TestPage "Production Definition Wizard") + begin + // On Step 1, capture the display field values resolved by the manager + ActualBOMRoutingDisplayValue := Wizard.BOMRoutingDisplayField.Value(); + ActualProdCompDisplayValue := Wizard.ProdCompDisplayField.Value(); + // Navigate through all steps and finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Prod. Def. Wiz. Config Test"); + LibrarySetupStorage.Restore(); + WizardFinished := false; + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Config Test"); + + LibraryERMCountryData.CreateVATData(); + LibraryERMCountryData.UpdateGeneralPostingSetup(); + ProdDefWizSetupLib.InitializeBasicSetup(); + + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + IsInitialized := true; + Commit(); + LibrarySetupStorage.SaveManufacturingSetup(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Config Test"); + end; + + [Test] + [HandlerFunctions('HandleWizardNavigateToStep2AndCaptureEditability')] + procedure TestC_BOMRoutingDisplayEdit_BOMStepEditable() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO C_Edit] BOMRoutingDisplay = Edit → BOM lines part is enabled (editable) + Initialize(); + + // [GIVEN] Manufacturing Setup: ShowRtngBOMSelect_Both = Edit + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard opens on Step 2 + ActualEditBOMLines := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] BOMLinesPart.Enabled = true (Display = Edit means editable) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.IsTrue(ActualEditBOMLines, 'BOM lines should be editable when Display = Edit'); + end; + +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizGeneralTest.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizGeneralTest.Codeunit.al new file mode 100644 index 0000000000..11c906931d --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizGeneralTest.Codeunit.al @@ -0,0 +1,380 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Location; +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Sales.Document; + +codeunit 137423 "Prod. Def. Wiz. General Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + + trigger OnRun() + begin + // [FEATURE] Production Definition Wizard - General: Open & Finish + IsInitialized := false; + end; + + var + Assert: Codeunit Assert; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + ProdDefWizSetupLib: Codeunit "Prod. Def. Wiz. Setup Lib."; + ProdDefWizCheckLib: Codeunit "Prod. Def. Wiz. Check Lib."; + IsInitialized: Boolean; + WizardFinished: Boolean; + // Handler state for step-checking tests + Step1BackWasDisabled: Boolean; + Step2BackWasEnabled: Boolean; + DefineItemStructure_Step4Visible: Boolean; + DefineItemStructure_Step5Visible: Boolean; + CreateProdOrder_Step2Visible: Boolean; + CreateProdOrder_Step3Visible: Boolean; + CreateProdOrder_Step4Visible: Boolean; + CreateProdOrder_Step5Visible: Boolean; + + + [Test] + [HandlerFunctions('HandleWizardSaveToItemAndCaptureBOM')] + procedure TestA1_OpenFromItemNoBOMRouting_OpensAndFinishes() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + PreWizardLastBOMNo: Code[20]; + PostWizardLastBOMNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO A1] Open wizard from Item with no BOM/Routing — wizard opens and finishes; BOM assigned + Initialize(); + + // [GIVEN] An item with no Production BOM and no Routing + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] The Production Definition action is invoked (wizard run in DefineItemStructure mode with Save) + WizardFinished := false; + PreWizardLastBOMNo := ProdDefWizCheckLib.GetLastProductionBOMNo(); + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Wizard opened and completed; Item now has a Production BOM assigned + PostWizardLastBOMNo := ProdDefWizCheckLib.GetLastProductionBOMNo(); + Assert.IsTrue(WizardFinished, 'Wizard should have finished successfully'); + Assert.AreNotEqual(PreWizardLastBOMNo, PostWizardLastBOMNo, 'Wizard should have created a new Production BOM'); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, PostWizardLastBOMNo); + end; + + [Test] + [HandlerFunctions('HandleWizardSaveToSKUAndCaptureBOM')] + procedure TestA2_OpenFromSKU_OpensAndFinishes() + var + SKU: Record "Stockkeeping Unit"; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + LocationCode: Code[10]; + PreWizardLastBOMNo: Code[20]; + PostWizardLastBOMNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO A2] Open wizard from SKU — wizard opens and finishes; BOM assigned to SKU + Initialize(); + + // [GIVEN] A Stockkeeping Unit for any item with no BOM/Routing + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateStockkeepingUnitWithBOMAndRouting(SKU, ItemNo, LocationCode, '', '', ''); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] The Production Definition action is invoked on the SKU (with Save) + WizardFinished := false; + PreWizardLastBOMNo := ProdDefWizCheckLib.GetLastProductionBOMNo(); + Commit(); + ProdDefManager.RunForSource(SKU, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Wizard opens and finishes without error; SKU now has a Production BOM assigned + PostWizardLastBOMNo := ProdDefWizCheckLib.GetLastProductionBOMNo(); + Assert.IsTrue(WizardFinished, 'Wizard should have finished successfully from SKU'); + Assert.AreNotEqual(PreWizardLastBOMNo, PostWizardLastBOMNo, 'Wizard should have created a new Production BOM for the SKU'); + ProdDefWizCheckLib.VerifySKUHasBOM(ItemNo, LocationCode, '', PostWizardLastBOMNo); + end; + + [Test] + [HandlerFunctions('HandleWizardCheckStep1BackDisabled')] + procedure TestA3_Step1_BackButtonDisabled() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO A3] Wizard step navigation: Back button on Step 1 is disabled + // [INFRASTRUCTURE TEST] Verifies wizard UI navigation state (Back button disabled on Step 1), not business logic. + Initialize(); + + // [GIVEN] Wizard is open on Step 1 (Introduction), BOMRoutingDisplay = Edit + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + + // [WHEN] Checking the state of the Back action on Step 1 + Step1BackWasDisabled := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] ActionBack.Enabled = false on Step 1 + Assert.IsTrue(Step1BackWasDisabled, 'Back button should be disabled on Step 1'); + end; + + [Test] + [HandlerFunctions('HandleWizardCheckStep2BackEnabled')] + procedure TestA4_Step2_BackButtonEnabled() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO A4] Wizard step navigation: Back button becomes enabled on Step 2 + // [INFRASTRUCTURE TEST] Verifies wizard UI navigation state (Back button enabled on Step 2), not business logic. + Initialize(); + + // [GIVEN] Wizard is open; BOMRoutingDisplay = Edit (DefineItemStructure mode) + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + + // [WHEN] Wizard moves to Step 2 + Step2BackWasEnabled := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] ActionBack.Enabled = true on Step 2 + Assert.IsTrue(Step2BackWasEnabled, 'Back button should be enabled on Step 2'); + end; + + [Test] + [HandlerFunctions('HandleWizardCheckDefineItemStructureSteps')] + procedure TestA5_DefineItemStructureMode_OnlySteps1To3() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO A5] DefineItemStructure mode: wizard has exactly Steps 1–3 + Initialize(); + + // [GIVEN] Wizard opened with Mode = DefineItemStructure; steps 4 and 5 are absent due to mode, not missing data + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + + // [WHEN] User navigates through all visible steps + DefineItemStructure_Step4Visible := false; // handler sets to true only if the step becomes visible + DefineItemStructure_Step5Visible := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Steps 4 (Components Preview) and 5 (Production Routing Preview) are never shown + Assert.IsFalse(DefineItemStructure_Step4Visible, 'Step 4 (Components) should not be visible in DefineItemStructure mode'); + Assert.IsFalse(DefineItemStructure_Step5Visible, 'Step 5 (Prod. Routing) should not be visible in DefineItemStructure mode'); + end; + + [Test] + [HandlerFunctions('HandleWizardCheckCreateProdOrderSteps')] + procedure TestA6_CreateProductionOrderMode_AllSteps1To5() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + RoutingNo: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO A6] CreateProductionOrder mode: wizard has Steps 1–5 + Initialize(); + + // [GIVEN] Sales Line for item with BOM and Routing; setup: BOMRoutingDisplay=Edit, ProdComponentDisplay=Edit + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, + "Prod. Definition Display"::Edit); + + // [WHEN] User navigates through all visible steps in CreateProductionOrder mode + CreateProdOrder_Step2Visible := false; + CreateProdOrder_Step3Visible := false; + CreateProdOrder_Step4Visible := false; + CreateProdOrder_Step5Visible := false; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Steps 2, 3, 4 and 5 are all shown + Assert.IsTrue(CreateProdOrder_Step2Visible, 'Step 2 (BOM) should be visible in CreateProductionOrder mode'); + Assert.IsTrue(CreateProdOrder_Step3Visible, 'Step 3 (Routing) should be visible in CreateProductionOrder mode'); + Assert.IsTrue(CreateProdOrder_Step4Visible, 'Step 4 (Components) should be visible in CreateProductionOrder mode'); + Assert.IsTrue(CreateProdOrder_Step5Visible, 'Step 5 (Prod. Routing) should be visible in CreateProductionOrder mode'); + // Verify a Production Order was actually created for the item after wizard completes + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasComponentCount(ProdOrder, 2); + ProdDefWizCheckLib.VerifyProdOrderFields(ProdOrder, ItemNo, 5, WorkDate(), LocationCode, ''); + ProdDefWizCheckLib.VerifyProdOrderHasRoutingLineCount(ProdOrder, 2); + end; + + + [ModalPageHandler] + procedure HandleWizardSimpleFinish(var Wizard: TestPage "Production Definition Wizard") + begin + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCheckStep1BackDisabled(var Wizard: TestPage "Production Definition Wizard") + begin + // On opening, wizard is on Step 1 — verify Back is disabled + Step1BackWasDisabled := not Wizard.ActionBack.Enabled(); + // Finish the wizard + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then + Wizard.ActionFinish.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardCheckStep2BackEnabled(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 + Wizard.ActionNext.Invoke(); + // Check Back is now enabled + Step2BackWasEnabled := Wizard.ActionBack.Enabled(); + // Finish the wizard + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then + Wizard.ActionFinish.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardCheckDefineItemStructureSteps(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate through all steps and track which are visible + // In DefineItemStructure mode, ComponentsStepVisible and ProdRoutingStepVisible should be false + DefineItemStructure_Step4Visible := Wizard.ComponentsPart.Visible(); + DefineItemStructure_Step5Visible := Wizard.ProdOrderRoutingPart.Visible(); + while Wizard.ActionNext.Enabled() do begin + Wizard.ActionNext.Invoke(); + // Re-check after each navigation step + if Wizard.ComponentsPart.Visible() then + DefineItemStructure_Step4Visible := true; + if Wizard.ProdOrderRoutingPart.Visible() then + DefineItemStructure_Step5Visible := true; + end; + if Wizard.ActionFinish.Enabled() then + Wizard.ActionFinish.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardCheckCreateProdOrderSteps(var Wizard: TestPage "Production Definition Wizard") + begin + // Check visibility at initial open + if Wizard.BOMLinesPart.Visible() then + CreateProdOrder_Step2Visible := true; + if Wizard.RoutingLinesPart.Visible() then + CreateProdOrder_Step3Visible := true; + if Wizard.ComponentsPart.Visible() then + CreateProdOrder_Step4Visible := true; + if Wizard.ProdOrderRoutingPart.Visible() then + CreateProdOrder_Step5Visible := true; + + // Navigate and re-check + while Wizard.ActionNext.Enabled() do begin + Wizard.ActionNext.Invoke(); + if Wizard.BOMLinesPart.Visible() then + CreateProdOrder_Step2Visible := true; + if Wizard.RoutingLinesPart.Visible() then + CreateProdOrder_Step3Visible := true; + if Wizard.ComponentsPart.Visible() then + CreateProdOrder_Step4Visible := true; + if Wizard.ProdOrderRoutingPart.Visible() then + CreateProdOrder_Step5Visible := true; + end; + if Wizard.ActionFinish.Enabled() then + Wizard.ActionFinish.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardSaveToItemAndCaptureBOM(var Wizard: TestPage "Production Definition Wizard") + begin + // Enable Save = Item so BOM is persisted to the item record + Wizard.SaveBOMRoutingField.SetValue(true); + // Navigate to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardSaveToSKUAndCaptureBOM(var Wizard: TestPage "Production Definition Wizard") + var + SaveBomRoutingToSource: Boolean; + begin + Evaluate(SaveBomRoutingToSource, Wizard.SaveBOMRoutingField.Value()); + Assert.IsTrue(SaveBomRoutingToSource, + 'SaveBOMRoutingToSource should be StockkeepingUnit when Save is enabled for SKU source'); + // Navigate to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Prod. Def. Wiz. General Test"); + LibrarySetupStorage.Restore(); + WizardFinished := false; + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. General Test"); + + LibraryERMCountryData.CreateVATData(); + LibraryERMCountryData.UpdateGeneralPostingSetup(); + ProdDefWizSetupLib.InitializeBasicSetup(); + + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + IsInitialized := true; + Commit(); + LibrarySetupStorage.SaveManufacturingSetup(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. General Test"); + end; +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizItemStrTest.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizItemStrTest.Codeunit.al new file mode 100644 index 0000000000..58e6a7675a --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizItemStrTest.Codeunit.al @@ -0,0 +1,211 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Wizard; + +codeunit 137432 "Prod. Def. Wiz. Item Str. Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + + trigger OnRun() + begin + // [FEATURE] Production Definition Wizard - DefineItemStructure Mode + IsInitialized := false; + end; + + var + Assert: Codeunit Assert; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + ProdDefWizSetupLib: Codeunit "Prod. Def. Wiz. Setup Lib."; + ProdDefWizCheckLib: Codeunit "Prod. Def. Wiz. Check Lib."; + IsInitialized: Boolean; + WizardFinished: Boolean; + WizardResult: Boolean; + // Handler state + TargetBOMNoForK1: Code[20]; + + + [Test] + [HandlerFunctions('HandleWizardK1SelectBOMNoSave,HandleProductionBOMListForK1')] + procedure TestK1_DefineItemStructure_SaveFalse_ItemBOMUnchanged() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + OriginalBOMNo: Code[20]; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO K1] Mode = DefineItemStructure; user selects a different BOM in the wizard; Save = false → Item BOM unchanged after finish + Initialize(); + + // [GIVEN] Item with BOM-A; a second BOM-B also exists + OriginalBOMNo := ProdDefWizLibrary.CreateBOM(2); + TargetBOMNoForK1 := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(OriginalBOMNo, ''); + Item.Get(ItemNo); + + // [WHEN] Wizard opens; user selects BOM-B on Step 2 but leaves Save = false and finishes + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Item."Production BOM No." still equals BOM-A (finish without save leaves Item unchanged) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyItemBOMUnchanged(ItemNo, OriginalBOMNo); + end; + + + [Test] + [HandlerFunctions('HandleWizardCancel')] + procedure TestK2_DefineItemStructure_UserCancels_ItemUnchanged_NoProdOrder() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO K2] DefineItemStructure mode: user cancels wizard → Item unchanged, no Production Order created + Initialize(); + + // [GIVEN] Item with an existing BOM + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User opens wizard in DefineItemStructure mode and cancels without finishing + WizardFinished := false; + Commit(); + WizardResult := ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] RunForSource returns false; Item BOM is unchanged; no Production Order exists + Assert.IsFalse(WizardResult, 'RunForSource should return false when wizard is cancelled'); + Assert.IsFalse(WizardFinished, 'WizardFinished should be false after cancel'); + ProdDefWizCheckLib.VerifyItemBOMUnchanged(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyNoProdOrderForItem(ItemNo); + end; + + [ModalPageHandler] + procedure HandleWizardCancel(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate one step then close without finishing (simulates cancel) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + end; + + + [ModalPageHandler] + procedure HandleWizardK1SelectBOMNoSave(var Wizard: TestPage "Production Definition Wizard") + begin + // Explicitly disable Save + Wizard.SaveBOMRoutingField.SetValue(false); + // Navigate to Step 2 (BOM) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Select BOM-B via AssistEdit on ProductionBOMNoField + // (opens Production BOM List — handled by HandleProductionBOMListForK4) + Wizard.ProductionBOMNoField.AssistEdit(); + // Navigate and finish without saving + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleProductionBOMListForK1(var ProductionBOMList: TestPage "Production BOM List") + begin + ProductionBOMList.Filter.SetFilter("No.", TargetBOMNoForK1); + ProductionBOMList.First(); + ProductionBOMList.OK.Invoke(); + end; + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Prod. Def. Wiz. Item Str. Test"); + LibrarySetupStorage.Restore(); + WizardFinished := false; + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Item Str. Test"); + LibraryERMCountryData.CreateVATData(); + LibraryERMCountryData.UpdateGeneralPostingSetup(); + ProdDefWizSetupLib.InitializeBasicSetup(); + + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + IsInitialized := true; + Commit(); + LibrarySetupStorage.SaveManufacturingSetup(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Item Str. Test"); + end; + + [Test] + [HandlerFunctions('HandleWizardK3SaveToItem')] + procedure TestK3_DefineItemStructure_SaveTrue_ItemBOMAndRoutingAssigned() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + PreWizardLastBOMNo: Code[20]; + PostWizardLastBOMNo: Code[20]; + PreWizardLastRoutingNo: Code[20]; + PostWizardLastRoutingNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO K3] Mode = DefineItemStructure; Save = Item → Item."Production BOM No." and "Routing No." are assigned after finish + Initialize(); + + // [GIVEN] Item with no BOM and no Routing; wizard configured for Nothing scenario + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User enables Save = Item and finishes the wizard + PreWizardLastBOMNo := ProdDefWizCheckLib.GetLastProductionBOMNo(); + PreWizardLastRoutingNo := ProdDefWizCheckLib.GetLastRoutingNo(); + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Item."Production BOM No." and "Routing No." are now assigned to the wizard-created BOM/Routing + PostWizardLastBOMNo := ProdDefWizCheckLib.GetLastProductionBOMNo(); + PostWizardLastRoutingNo := ProdDefWizCheckLib.GetLastRoutingNo(); + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreNotEqual(PreWizardLastBOMNo, PostWizardLastBOMNo, 'Wizard should have created a new Production BOM'); + Assert.AreNotEqual(PreWizardLastRoutingNo, PostWizardLastRoutingNo, 'Wizard should have created a new Routing'); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, PostWizardLastBOMNo); + ProdDefWizCheckLib.VerifyItemHasRouting(ItemNo, PostWizardLastRoutingNo); + end; + + [ModalPageHandler] + procedure HandleWizardK3SaveToItem(var Wizard: TestPage "Production Definition Wizard") + begin + // Enable Save = Item + Wizard.SaveBOMRoutingField.SetValue(true); + // Navigate to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizSalesTest.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizSalesTest.Codeunit.al new file mode 100644 index 0000000000..0670f79154 --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizSalesTest.Codeunit.al @@ -0,0 +1,1611 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Location; +using Microsoft.Inventory.Tracking; +using Microsoft.Manufacturing.Capacity; +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; +using Microsoft.Manufacturing.Setup; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Sales.Document; + +codeunit 137431 "Prod. Def. Wiz. Sales Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + + trigger OnRun() + begin + // [FEATURE] Production Definition Wizard - CreateProductionOrder Mode (Sales Line Entry) + IsInitialized := false; + end; + + var + Assert: Codeunit Assert; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + ProdDefWizSetupLib: Codeunit "Prod. Def. Wiz. Setup Lib."; + ProdDefWizCheckLib: Codeunit "Prod. Def. Wiz. Check Lib."; + LibraryItemTracking: Codeunit "Library - Item Tracking"; + IsInitialized: Boolean; + WizardFinished: Boolean; + WizardResult: Boolean; + CapturedNotificationMessage: Text; + CapturedExpectedQty: Decimal; + TargetBOMVersionCode: Code[20]; + TargetRoutingVersionCode: Code[20]; + + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ1_InitFromSalesLine_ProdOrderHeaderCorrect() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + VariantCode: Code[10]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + OrderQty: Decimal; + ShipmentDate: Date; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J1] InitializeFromSalesLine: Prod. Order header created with correct fields + Initialize(); + + // [GIVEN] Sales Line: Item=X, Qty=10, ShipmentDate=WorkDate+30, Location=EAST, Variant=RED-V + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + VariantCode := ProdDefWizLibrary.CreateVariantForItem(ItemNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + OrderQty := 10; + ShipmentDate := CalcDate('<+30D>', WorkDate()); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, OrderQty, LocationCode, VariantCode, ShipmentDate); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is initialized and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order: SourceNo=ItemNo, Qty=10, DueDate=ShipmentDate, Location=EAST, Variant=RED-V + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderFields(ProdOrder, ItemNo, OrderQty, ShipmentDate, LocationCode, VariantCode); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ2_InitFromSalesLine_ComponentsCreatedFromBOM() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J2] InitializeFromSalesLine: Prod. Order components created from BOM + Initialize(); + + // [GIVEN] Sales Line for item with BOM containing 2 components; Mode = CreateProductionOrder + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 10, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is initialized and user finishes without modifying components + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order Component lines exist (2 components from BOM) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasComponentCount(ProdOrder, 2); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ3_InitFromSalesLine_RoutingLinesCreatedFromRouting() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J3] InitializeFromSalesLine: Prod. Order routing lines created from Routing + Initialize(); + + // [GIVEN] Sales Line for item with Routing containing 2 operations; Mode = CreateProductionOrder + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is initialized and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order Routing Lines match the Routing operations (2 lines) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasRoutingLineCount(ProdOrder, 2); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ4_InitFromSalesLine_ReservationCreatedForProdOrder() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J4] CreateProductionOrder mode from Sales Line: reservation entry created linking Sales Line demand to Prod. Order + Initialize(); + + // [GIVEN] Sales Line for item with BOM + Routing; item is set to Reserve = Always + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + Item.Validate(Reserve, Item.Reserve::Always); + Item.Modify(true); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is initialized and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] A Reservation Entry exists linking the Sales Line demand to the created Production Order + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyReservationExistsForSalesLine(SalesLine); + ProdDefWizCheckLib.VerifyReservationLinksToProductionOrder(SalesLine, ProdOrder); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ5_InitFromSalesLine_EmptyBOMRouting_OrderStillCreated() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J5] CreateProductionOrder mode, BOM/Routing both empty: Production Order is still created + Initialize(); + + // [GIVEN] Sales Line for item with no BOM and no Routing + ProdDefWizSetupLib.SetDefWizFlushingMethod("Flushing Method"::Backward); + + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 3, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is initialized, user finishes (default placeholder lines active) + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order is created; references the item and quantity + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderFields(ProdOrder, ItemNo, 3, WorkDate(), LocationCode, ''); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ6_SalesLineWithSKUMatchingLocation_SKUBOMRoutingUsed() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + SKU: Record "Stockkeeping Unit"; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemBOMNo: Code[20]; + SKUBOMNo: Code[20]; + ItemRoutingNo: Code[20]; + SKURoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + WC3No: Code[20]; + WC4No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J6] Sales Line with SKU whose location matches the sales line location → SKU BOM/Routing used + Initialize(); + + // [GIVEN] Item with BOM-A (2 lines) + Routing-A; SKU for (ItemNo, LocationCode) with BOM-B (3 lines) + Routing-B + ItemBOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemRoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + SKUBOMNo := ProdDefWizLibrary.CreateBOM(3); + SKURoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC3No, WC4No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(ItemBOMNo, ItemRoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateStockkeepingUnitWithBOMAndRouting(SKU, ItemNo, LocationCode, '', SKUBOMNo, SKURoutingNo); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order components come from SKU BOM-B (3 lines, not 2) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasComponentCount(ProdOrder, 3); + end; + + [Test] + [HandlerFunctions('HandleWizardCancel')] + procedure TestJ7_UserCancelsWizard_NoProductionOrderCreated() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J7] User cancels the wizard → RunForSource returns false and no Production Order is created + Initialize(); + + // [GIVEN] Sales Line for item with BOM and Routing + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User cancels the wizard (handler closes without Finish) + WizardFinished := false; + Commit(); + WizardResult := ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] RunForSource returns false and no Production Order is created + Assert.IsFalse(WizardResult, 'RunForSource should return false when wizard is cancelled'); + Assert.IsFalse(WizardFinished, 'WizardFinished should be false after cancel'); + ProdDefWizCheckLib.VerifyNoProdOrderForItem(ItemNo); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ8_PlannedStatusVia3ArgOverload_ProdOrderHasPlannedStatus() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J8] RunForSource with ProdOrderStatus = Planned → Production Order created with Planned status + Initialize(); + + // [GIVEN] Sales Line for item with BOM and Routing + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs with Planned status via 3-argument overload + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder, "Production Order Status"::Planned); + + // [THEN] Created Production Order has Planned status + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderStatus(ProdOrder, "Production Order Status"::Planned); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ9_PartiallyReservedSalesLine_ProdOrderQtyEqualsOutstandingMinusReserved() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J9] Sales Line with partial reservation → Production Order qty = outstanding - reserved + Initialize(); + + // [GIVEN] Sales Line with Qty=10; a partial reservation of 3 exists + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 10, LocationCode, '', WorkDate()); + // NOTE: Direct insertion is intentional - no standard library supports partial + // reservation creation. This creates a minimal reservation state to test that + // the wizard handles existing reservations correctly, without triggering + // full reservation engine logic. + ProdDefWizLibrary.CreatePartialReservationForSalesLine(SalesLine, 3); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order quantity = 10 - 3 = 7 + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderFields(ProdOrder, ItemNo, 7, WorkDate(), LocationCode, ''); + end; + + [Test] + procedure TestJ10_BothHide_WizardSkipped_ProdOrderAutoCreated() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J10] Both ShowRtngBOMSelect and ShowProdCompSelect = Hide → wizard page skipped, ProdOrder auto-created + Initialize(); + + // [GIVEN] Both display settings = Hide + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Hide, "Prod. Definition Display"::Hide); + + // [WHEN] RunForSource is called (wizard page is never opened) + Commit(); + WizardResult := ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Returns true and Production Order was auto-created + Assert.IsTrue(WizardResult, 'RunForSource should return true when interaction is skipped'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + end; + + [Test] + [HandlerFunctions('HandleWizardSaveToItemFinish')] + procedure TestJ11_SaveItemInCreateProdOrderMode_ItemBOMRoutingUpdated() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J11] Save = true with SaveBOMRoutingToSource = Item in CreateProductionOrder mode → Item BOM/Routing updated + Initialize(); + + // [GIVEN] Item with no BOM/Routing; Sales Line for that item + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs with Save = Item and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Item BOM and Routing are updated AND Production Order is created + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyItemHasAnyBOM(ItemNo); + ProdDefWizCheckLib.VerifyItemHasAnyRouting(ItemNo); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ12_WizardFinishes_ProdOrderLineHasCorrectBOMAndRouting() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J12] Wizard finishes → Production Order line has correct Production BOM No. and Routing No. + Initialize(); + + // [GIVEN] Item with BOMNo + RoutingNo; Sales Line for that item + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes without changes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order line has Production BOM No. = BOMNo and Routing No. = RoutingNo + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderLineHasBOM(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyProdOrderLineHasRouting(ItemNo, RoutingNo); + end; + + [Test] + [HandlerFunctions('HandleWizardSelectBOMVersion,HandleBOMVersionListSelection')] + procedure TestJ13_UserSelectsBOMVersion_ProdOrderLineHasThatVersion() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J13] User selects BOM version V1 in Step 2 → ProdOrder line Production BOM Version Code = V1 + Initialize(); + + // [GIVEN] BOM with two versions V1 (older) and V2 (current active); item uses that BOM + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ProdDefWizLibrary.CreateBOMVersionAndCertify(BOMNo, 'V1', CalcDate('<-1Y>', WorkDate())); + ProdDefWizLibrary.CreateBOMVersionAndCertify(BOMNo, 'V2', WorkDate()); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + TargetBOMVersionCode := 'V1'; + + // [WHEN] User navigates to Step 2, selects V1 via AssistEdit, then finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order line Production BOM Version Code = V1 + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderLineHasBOMVersion(ItemNo, 'V1'); + end; + + [Test] + [HandlerFunctions('HandleWizardSelectRoutingVersion,HandleRoutingVersionListSelection')] + procedure TestJ14_UserSelectsRoutingVersion_ProdOrderLineHasThatVersion() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J14] User selects Routing version V1 in Step 3 → ProdOrder line Routing Version Code = V1 + Initialize(); + + // [GIVEN] Routing with two versions V1 (older) and V2 (current active); item uses that Routing + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ProdDefWizLibrary.CreateRoutingVersionAndCertify(RoutingNo, 'V1', CalcDate('<-1Y>', WorkDate())); + ProdDefWizLibrary.CreateRoutingVersionAndCertify(RoutingNo, 'V2', WorkDate()); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + TargetRoutingVersionCode := 'V1'; + + // [WHEN] User navigates to Step 3, selects V1 via AssistEdit, then finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order line Routing Version Code = V1 + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderLineHasRoutingVersion(ItemNo, 'V1'); + end; + + [Test] + [HandlerFunctions('HandleWizardModifyComponentQty')] + procedure TestJ15_UserModifiesComponentQtyInStep4_ProdOrderComponentHasModifiedQty() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J15] ProdCompDisplay = Edit; user modifies Qty per in Step 4 → ProdOrder component has modified qty + Initialize(); + + // [GIVEN] Item with BOM (2 comp) and Routing (2 ops); Sales Line; ProdCompDisplay = Edit + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User navigates to Step 4 and sets "Quantity per" of first component to 99 + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] First Production Order component has Quantity per = 99 + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderComponentHasQtyPerForFirstComponent(ProdOrder, 99); + end; + + [Test] + [HandlerFunctions('HandleWizardAddComponent')] + procedure TestJ16_UserAddsComponentInStep4_AddedComponentInProdOrder() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J16] ProdCompDisplay = Edit; user adds component in Step 4 → added component present in ProdOrder + Initialize(); + + // [GIVEN] Item with BOM (2 comp), Routing (2 ops); Sales Line + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User navigates to Step 4 and adds a new component line + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order has 3 components (2 original + 1 added) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasComponentCount(ProdOrder, 3); + end; + + [Test] + [HandlerFunctions('HandleWizardDeleteComponent')] + procedure TestJ17_UserDeletesComponentInStep4_DeletedComponentAbsentFromProdOrder() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J17] ProdCompDisplay = Edit; user deletes component in Step 4 → deleted component absent from ProdOrder + Initialize(); + + // [GIVEN] Item with BOM (2 comp), Routing (2 ops); Sales Line + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User navigates to Step 4 and deletes the first component + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order has 1 component (2 - 1 deleted) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasComponentCount(ProdOrder, 1); + end; + + [Test] + [HandlerFunctions('HandleWizardModifyRoutingRunTime')] + procedure TestJ18_UserModifiesRoutingRunTimeInStep5_ProdOrderRoutingHasModifiedRunTime() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J18] ProdCompDisplay = Edit; user modifies Run Time in Step 5 → ProdOrder routing line has modified run time + Initialize(); + + // [GIVEN] Item with BOM (2 comp) and Routing (2 ops); Sales Line + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User navigates to Step 5 and modifies Run Time of operation '10' to 999 + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order routing line for operation '10' has Run Time = 999 + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderRoutingLineRunTime(ProdOrder, '10', 999); + end; + + [Test] + [HandlerFunctions('HandleWizardAddRoutingOperation')] + procedure TestJ19_UserAddsRoutingOperationInStep5_AddedOperationPresentInProdOrder() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J19] ProdCompDisplay = Edit; user adds routing operation in Step 5 → added operation present in ProdOrder + Initialize(); + + // [GIVEN] Item with BOM (2 comp) and Routing (2 ops); Sales Line + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User navigates to Step 5 and adds a new routing operation + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order has 3 routing lines (2 original + 1 added) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasRoutingLineCount(ProdOrder, 3); + end; + + [Test] + [HandlerFunctions('HandleWizardDeleteRoutingOperation')] + procedure TestJ20_UserDeletesRoutingOperationInStep5_DeletedOperationAbsentFromProdOrder() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J20] ProdCompDisplay = Edit; user deletes routing operation in Step 5 → deleted operation absent from ProdOrder + Initialize(); + + // [GIVEN] Item with BOM (2 comp) and Routing (2 ops); Sales Line + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User navigates to Step 5 and deletes the first routing operation + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order has 1 routing line (2 - 1 deleted) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasRoutingLineCount(ProdOrder, 1); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish,HandleProdOrderCreatedNotification')] + procedure TestJ21_WizardCompletes_NotificationSentWithProdOrderNo() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J21] After wizard completes from Sales Line, a notification is sent containing the Production Order No. + Initialize(); + + // [GIVEN] Sales Line for item with BOM and Routing + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and finishes + CapturedNotificationMessage := ''; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] The captured notification message contains the created Production Order No. + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + Assert.IsTrue(CapturedNotificationMessage.Contains(ProdOrder."No."), + 'Notification message should contain the Production Order No.'); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ22_ProdOrderFromSalesLine_ExistingBOM_ComponentFlushingNotOverridden() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J22] When item has an existing Production BOM, the wizard does not overwrite the component flushing method with the setup default; components retain their original (Manual) flushing method + Initialize(); + + // [GIVEN] ManufacturingSetup."Def. Wiz. Flushing Method" = Backward; item with existing BOM + Routing (components have default Manual flushing method) + ProdDefWizSetupLib.SetDefWizFlushingMethod("Flushing Method"::Backward); + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes (Released status is the default) + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Wizard completes without error; Production Order exists with Released status; components retain their original Manual flushing method (not overridden by the Backward setup default) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderStatus(ProdOrder, "Production Order Status"::Released); + ProdDefWizCheckLib.VerifyProdOrderComponentFlushingMethod(ProdOrder, "Flushing Method"::"Pick + Manual"); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ23_BOMLineWithDescription2_ProdOrderComponentHasDescription2() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + ComponentItemNo: Code[20]; + ExpectedDesc2: Text[50]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J23] BOM line with Description 2 → Production Order component carries the same Description 2 + Initialize(); + + // [GIVEN] BOM with a component line that has Description 2; item uses that BOM + ExpectedDesc2 := 'BOM-DESC2-TEST'; + BOMNo := ProdDefWizLibrary.CreateBOMWithComponentAndDescription2(ComponentItemNo, ExpectedDesc2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order component carries Description 2 from the BOM line + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderComponentHasDescription2(ProdOrder, ExpectedDesc2); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ24_RoutingLineWithDescription2_ProdOrderRoutingLineHasDescription2() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WCNo: Code[20]; + ExpectedDesc2: Text[50]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J24] Routing line with Description 2 → Production Order routing line carries the same Description 2 + Initialize(); + + // [GIVEN] Routing with an operation line that has Description 2; item uses that Routing + ExpectedDesc2 := 'RTNG-DESC2-TEST'; + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithSingleLineAndDescription2(WCNo, ExpectedDesc2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order routing line carries Description 2 from the Routing line + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderRoutingLineHasDescription2(ProdOrder, ExpectedDesc2); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ25_SalesLineWithNoShipmentDate_WizardRunsWithoutError() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J25] Sales Line with no Shipment Date → wizard runs without error, Production Order created + Initialize(); + + // [GIVEN] Sales Line with ShipmentDate = 0D (no shipment date) + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 3, LocationCode, '', 0D); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes — no error should be raised + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Wizard finished without error; Production Order exists with empty Due Date + Assert.IsTrue(WizardFinished, 'Wizard should have finished without error'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ26_SalesLineWithBlankLocation_ProdOrderCreatedWithBlankLocation() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J26] Sales Line with blank Location Code → Production Order created with blank location + Initialize(); + + // [GIVEN] Sales Line with LocationCode = '' (no location) + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 3, '', '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order exists with blank Location Code + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderFields(ProdOrder, ItemNo, 3, WorkDate(), '', ''); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ27_SalesLineWithAlternateUOM_ReservationBaseQtyMatchesProdOrderRemainingBase() + var + SalesLine: Record "Sales Line"; + Item: Record Item; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + AltUOMCode: Code[10]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J27] Sales Line whose item has Qty. per Unit of Measure ≠ 1 → reservation base quantity is correctly calculated using UOM conversion + Initialize(); + + // [GIVEN] Item with BOM + Routing; alternate UOM BOX = 10 PCS; Sales UOM = BOX; Sales Line qty = 2 BOX (base = 20) + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ProdDefWizLibrary.CreateItemWithSalesUOMMultiplier(BOMNo, RoutingNo, 10, ItemNo, AltUOMCode); + Item.Get(ItemNo); + Item.Validate(Reserve, Item.Reserve::Always); + Item.Modify(true); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 2, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Reservation exists for the Sales Line and the reservation base quantity matches the Production Order remaining base quantity + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyReservationExistsForSalesLine(SalesLine); + ProdDefWizCheckLib.VerifyReservationBaseQtyMatchesProdOrderRemainingBase(SalesLine, ItemNo); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ28_SalesLineWithLotTracking_TrackingCopiedToProdOrderLine() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ReservEntry: Record "Reservation Entry"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + LotNo: Code[50]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J28] Sales Line with item-tracking lot number entries → CopyItemTracking transfers tracking to Production Order line after wizard completes + Initialize(); + + // [GIVEN] Item with BOM + Routing and lot tracking; Sales Line with a lot number assigned via Tracking Specification + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithLotTracking(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + LotNo := 'LOT-TEST-001'; + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + LibraryItemTracking.CreateSalesOrderItemTracking(ReservEntry, SalesLine, '', LotNo, 5); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Wizard completes; tracking is transferred: a reservation entry with the lot number exists for the Production Order line + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyTrackingSpecExistsForProdOrderLine(ItemNo, LotNo); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ29_ItemWithScrapPct_ProdOrderLineHasScrapPct() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + ScrapPct: Decimal; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J29] Item with Scrap % = 10 → Production Order line created by wizard has Scrap % = 10 + Initialize(); + + // [GIVEN] Item with Scrap % = 10; Sales Line for that item + ScrapPct := 10; + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + Item.Validate("Scrap %", ScrapPct); + Item.Modify(true); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order line has Scrap % = 10 + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderLineScrapPct(ItemNo, ScrapPct); + end; + + [Test] + [HandlerFunctions('HandleWizardSimpleFinish')] + procedure TestJ30_SalesLineWithVariantAndMatchingSKU_SKUBOMRoutingUsed() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + SKU: Record "Stockkeeping Unit"; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemBOMNo: Code[20]; + SKUBOMNo: Code[20]; + ItemRoutingNo: Code[20]; + SKURoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + VariantCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + WC3No: Code[20]; + WC4No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J30] Sales Line with Variant+Location matching a SKU → SKU BOM/Routing used over item-level + Initialize(); + + // [GIVEN] Item with BOM-A (2 lines); SKU for (ItemNo, LocationCode, VariantCode) with BOM-B (3 lines) + ItemBOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemRoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + SKUBOMNo := ProdDefWizLibrary.CreateBOM(3); + SKURoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC3No, WC4No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(ItemBOMNo, ItemRoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + VariantCode := ProdDefWizLibrary.CreateVariantForItem(ItemNo); + ProdDefWizLibrary.CreateStockkeepingUnitWithBOMAndRouting(SKU, ItemNo, LocationCode, VariantCode, SKUBOMNo, SKURoutingNo); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, VariantCode, WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order has 3 components from SKU BOM-B (not 2 from item BOM-A) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasComponentCount(ProdOrder, 3); + end; + + [Test] + [HandlerFunctions('HandleWizardAddComponent')] + procedure TestJ31_ProdOrderFromSalesLine_NoBOM_TemporaryComponentGetsSetupFlushingMethod() + var + ProdOrderComponent: Record "Prod. Order Component"; + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J31] When item has no Production BOM (temporary creation case), the wizard applies the setup default flushing method to the manually added component + Initialize(); + + // [GIVEN] ManufacturingSetup."Def. Wiz. Flushing Method" = Backward; item with no BOM and no Routing + ProdDefWizSetupLib.SetDefWizFlushingMethod("Flushing Method"::Backward); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 3, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs; user adds one component (no existing BOM to load from) and finishes + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order exists; the added component has the Backward flushing method from the item default (temporary creation case), setup component flushing method is applied to the first component, and the last component has flushing method from item + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + + ProdOrderComponent.SetRange(Status, ProdOrder.Status); + ProdOrderComponent.SetRange("Prod. Order No.", ProdOrder."No."); + ProdOrderComponent.FindFirst(); + Assert.AreEqual("Flushing Method"::Backward, ProdOrderComponent."Flushing Method", 'Flushing Method of the first component should match the setup default (Backward)'); + ProdOrderComponent.FindLast(); + Assert.AreEqual("Flushing Method"::"Pick + Manual", ProdOrderComponent."Flushing Method", 'Flushing Method of the last component should match the setup default (Backward)'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureComponentExpectedQty')] + procedure TestJ32_TempComponentsPreview_ExpectedQuantityNonZero() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + SalesQty: Decimal; + ComponentQtyPer: Decimal; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J32] In Step 4 (component preview), temporary components display non-zero Expected Quantity after Quantity per is set + Initialize(); + + // [GIVEN] Item with a BOM (1 component, Qty per = 2) and Routing; Sales Line qty = 5 + SalesQty := 5; + ComponentQtyPer := 2; // first BOM line qty = 1, second = 2; CreateBOM sets qty = line index + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, SalesQty, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard runs and user navigates to Step 4 (component preview) + CapturedExpectedQty := 0; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] The second temporary component's Expected Quantity is non-zero (= SalesQty * ComponentQtyPer = 10) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.IsTrue(CapturedExpectedQty > 0, + 'Temporary component Expected Quantity must be non-zero after Quantity per validation'); + Assert.AreNearlyEqual(SalesQty * ComponentQtyPer, CapturedExpectedQty, 0.01, + 'Expected Quantity should equal Sales Qty × Quantity per'); + end; + + [ModalPageHandler] + procedure HandleWizardCaptureComponentExpectedQty(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 4 (Components) via Steps 2 and 3 + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + if Wizard.ActionNext.Enabled() then begin + Wizard.ActionNext.Invoke(); + // Find the last component (BOM line 2 has Qty per = 2) + if Wizard.ComponentsPart.Last() then + CapturedExpectedQty := Wizard.ComponentsPart."Expected Quantity".AsDecimal(); + end; + // Continue and finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardSimpleFinish(var Wizard: TestPage "Production Definition Wizard") + begin + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCancel(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate one step then close without finishing (simulates cancel) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardSaveToItemFinish(var Wizard: TestPage "Production Definition Wizard") + begin + Wizard.SaveBOMRoutingField.SetValue(true); + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardSelectBOMVersion(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM selection) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Trigger version selection AssistEdit (opens "Prod. BOM Version List") + Wizard.SelectedBOMVersionField.AssistEdit(); + // Continue and finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleBOMVersionListSelection(var BOMVersionList: TestPage "Prod. BOM Version List") + begin + BOMVersionList.Filter.SetFilter("Version Code", TargetBOMVersionCode); + BOMVersionList.First(); + BOMVersionList.OK.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardSelectRoutingVersion(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Trigger version selection AssistEdit (opens "Routing Version List") + Wizard.SelectedRoutingVersionField.AssistEdit(); + // Continue and finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleRoutingVersionListSelection(var RoutingVersionList: TestPage "Routing Version List") + begin + RoutingVersionList.Filter.SetFilter("Version Code", TargetRoutingVersionCode); + RoutingVersionList.First(); + RoutingVersionList.OK.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardModifyComponentQty(var Wizard: TestPage "Production Definition Wizard") + var + ModifiedQtyPer: Decimal; + begin + ModifiedQtyPer := 99; + // Steps 2 and 3 + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Step 4 - Components + if Wizard.ActionNext.Enabled() then begin + Wizard.ActionNext.Invoke(); + if Wizard.ComponentsPart.First() then + Wizard.ComponentsPart."Quantity per".SetValue(ModifiedQtyPer); + end; + // Step 5 and finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardAddComponent(var Wizard: TestPage "Production Definition Wizard") + var + ManufacturingSetup: Record "Manufacturing Setup"; + DefaultCompItemNo: Code[20]; + begin + ManufacturingSetup.Get(); + DefaultCompItemNo := ManufacturingSetup."Def. Wiz. Comp Item No."; + // Steps 2 and 3 + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Step 4 - Components; add a new line + if Wizard.ActionNext.Enabled() then begin + Wizard.ActionNext.Invoke(); + Wizard.ComponentsPart.New(); + Wizard.ComponentsPart."Item No.".SetValue(DefaultCompItemNo); + Wizard.ComponentsPart."Quantity per".SetValue(1); + end; + // Step 5 and finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardDeleteComponent(var Wizard: TestPage "Production Definition Wizard") + begin + // Steps 2 and 3 + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Step 4 - Components; delete the first line + if Wizard.ActionNext.Enabled() then begin + Wizard.ActionNext.Invoke(); + if Wizard.ComponentsPart.First() then + Wizard.ComponentsPart.TestDelete.Invoke(); + end; + // Step 5 and finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardModifyRoutingRunTime(var Wizard: TestPage "Production Definition Wizard") + var + ModifiedRunTime: Decimal; + begin + ModifiedRunTime := 999; + // Steps 2 and 3 + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Step 4 + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Step 5 - Routing; modify Run Time on first row + if Wizard.ActionNext.Enabled() then begin + Wizard.ActionNext.Invoke(); + if Wizard.ProdOrderRoutingPart.First() then + Wizard.ProdOrderRoutingPart."Run Time".SetValue(ModifiedRunTime); + end; + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardAddRoutingOperation(var Wizard: TestPage "Production Definition Wizard") + var + ManufacturingSetup: Record "Manufacturing Setup"; + DefaultWCNo: Code[20]; + NewOperationNo: Code[10]; + begin + ManufacturingSetup.Get(); + DefaultWCNo := ManufacturingSetup."Def. Wiz. Work Center No."; + NewOperationNo := '30'; + // Steps 2 and 3 + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Step 4 + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Step 5 - Routing; add a new operation + if Wizard.ActionNext.Enabled() then begin + Wizard.ActionNext.Invoke(); + Wizard.ProdOrderRoutingPart.New(); + Wizard.ProdOrderRoutingPart."Operation No.".SetValue(NewOperationNo); + Wizard.ProdOrderRoutingPart.Type.SetValue(Format("Capacity Type Routing"::"Work Center")); + Wizard.ProdOrderRoutingPart."No.".SetValue(DefaultWCNo); + end; + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardDeleteRoutingOperation(var Wizard: TestPage "Production Definition Wizard") + begin + // Steps 2 and 3 + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Step 4 + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + // Step 5 - Routing; delete the first operation + if Wizard.ActionNext.Enabled() then begin + Wizard.ActionNext.Invoke(); + if Wizard.ProdOrderRoutingPart.First() then + Wizard.ProdOrderRoutingPart.TestDelete.Invoke(); + end; + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [SendNotificationHandler] + procedure HandleProdOrderCreatedNotification(var Notification: Notification): Boolean + begin + CapturedNotificationMessage := Notification.Message(); + exit(true); + end; + + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Prod. Def. Wiz. Sales Test"); + LibrarySetupStorage.Restore(); + WizardFinished := false; + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Sales Test"); + LibraryERMCountryData.CreateVATData(); + LibraryERMCountryData.UpdateGeneralPostingSetup(); + ProdDefWizSetupLib.InitializeBasicSetup(); + + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + IsInitialized := true; + Commit(); + LibrarySetupStorage.SaveManufacturingSetup(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Sales Test"); + end; + + [Test] + procedure TestJ_FullyReservedSalesLine_ErrorOnRun() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + RoutingNo: Code[20]; + ProductionOrderQtyZeroOrNegativeErr: Label 'Cannot create a production order from Sales Line %1 line %2: the calculated quantity (%3) is zero or negative because the line is fully or over-reserved.', Comment = '%1 = Document No., %2 = Line No., %3 = Quantity'; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO J_FullyReserved] Sales Line fully reserved (Outstanding = Reserved) → wizard raises an error + // (regression test for BUG-05 guard) + Initialize(); + + // [GIVEN] Sales Line for 5 units, fully reserved (reservation = 5) + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, '', '', WorkDate()); + ProdDefWizLibrary.CreatePartialReservationForSalesLine(SalesLine, 5); // fully reserved + + // [WHEN] Wizard is launched from the fully-reserved Sales Line + // [THEN] An error is raised (quantity = Outstanding - Reserved = 5 - 5 = 0) + asserterror ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + Assert.ExpectedError(StrSubstNo(ProductionOrderQtyZeroOrNegativeErr, + SalesLine."Document No.", SalesLine."Line No.", 0)); + end; + +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizSaveTest.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizSaveTest.Codeunit.al new file mode 100644 index 0000000000..097f5021fa --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizSaveTest.Codeunit.al @@ -0,0 +1,327 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Location; +using Microsoft.Manufacturing.Wizard; + +codeunit 137428 "Prod. Def. Wiz. Save Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + + trigger OnRun() + begin + // [FEATURE] Production Definition Wizard - Save Behavior + IsInitialized := false; + end; + + var + Assert: Codeunit Assert; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + ProdDefWizSetupLib: Codeunit "Prod. Def. Wiz. Setup Lib."; + ProdDefWizCheckLib: Codeunit "Prod. Def. Wiz. Check Lib."; + IsInitialized: Boolean; + WizardFinished: Boolean; + // Handler state + ActualSaveTargetAfterToggleOff: Text; + ActualSKUNotAllowedErrorRaised: Boolean; + + + [Test] + [HandlerFunctions('HandleWizardNoSave')] + procedure TestF1_SaveOff_ItemBOMRoutingUnchanged() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO F1] Save toggle OFF → Item Card BOM/Routing unchanged after finish + Initialize(); + + // [GIVEN] Item with BOM-A and Routing-A; wizard opened with Save = false (default) + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User finishes the wizard without enabling Save + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Item."Production BOM No." and Item."Routing No." remain unchanged + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyItemBOMUnchanged(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyItemRoutingUnchanged(ItemNo, RoutingNo); + end; + + [Test] + [HandlerFunctions('HandleWizardSaveToItem')] + procedure TestF2_SaveToItem_ItemBOMRoutingUpdated() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + PreWizardLastBOMNo: Code[20]; + PostWizardLastBOMNo: Code[20]; + PreWizardLastRoutingNo: Code[20]; + PostWizardLastRoutingNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO F2] Save = Item, source was Item → Item BOM/Routing updated to selected values + Initialize(); + + // [GIVEN] Item with no BOM and no Routing; wizard opened from Item + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User enables Save = Item and finishes the wizard + PreWizardLastBOMNo := ProdDefWizCheckLib.GetLastProductionBOMNo(); + PreWizardLastRoutingNo := ProdDefWizCheckLib.GetLastRoutingNo(); + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Item."Production BOM No." is populated with the wizard-selected BOM; Item."Routing No." with the wizard-selected Routing + PostWizardLastBOMNo := ProdDefWizCheckLib.GetLastProductionBOMNo(); + PostWizardLastRoutingNo := ProdDefWizCheckLib.GetLastRoutingNo(); + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreNotEqual(PreWizardLastBOMNo, PostWizardLastBOMNo, 'Wizard should have created a new Production BOM'); + Assert.AreNotEqual(PreWizardLastRoutingNo, PostWizardLastRoutingNo, 'Wizard should have created a new Routing'); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, PostWizardLastBOMNo); + ProdDefWizCheckLib.VerifyItemHasRouting(ItemNo, PostWizardLastRoutingNo); + end; + + [Test] + [HandlerFunctions('HandleWizardSaveToSKU')] + procedure TestF3_SaveToSKU_SKUUpdated_ItemUnchanged() + var + SKU: Record "Stockkeeping Unit"; + ProdDefManager: Codeunit "Production Definition Manager"; + OriginalItemBOMNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO F3] Save = StockkeepingUnit, source was SKU → SKU BOM/Routing updated + Initialize(); + + // [GIVEN] SKU with no BOM/Routing; Item has BOM-A + OriginalItemBOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(OriginalItemBOMNo, ''); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateStockkeepingUnitWithBOMAndRouting(SKU, ItemNo, LocationCode, '', '', ''); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User opens wizard from SKU Card, enables Save = StockkeepingUnit, finishes + Commit(); + ProdDefManager.RunForSource(SKU, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] SKU."Production BOM No." is set to the wizard-selected BOM; Item."Production BOM No." still = BOM-A + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifySKUHasBOM(ItemNo, LocationCode, '', OriginalItemBOMNo); + ProdDefWizCheckLib.VerifyItemBOMUnchanged(ItemNo, OriginalItemBOMNo); + end; + + [Test] + [HandlerFunctions('HandleWizardTrySaveToSKUFromItem')] + procedure TestF4_SaveSKU_SourceIsItem_Error() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + SaveBOMRtngSKUNotAllowedErr: Label 'Stockkeeping Unit is not allowed when the source is Item.'; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO F4] Save = StockkeepingUnit not allowed when source was Item + Initialize(); + + // [GIVEN] Wizard opened from Item Card (Source = Item) + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User attempts to set SaveBOMRoutingToSource = StockkeepingUnit + ActualSKUNotAllowedErrorRaised := false; + Commit(); + asserterror ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] An error is raised + Assert.ExpectedError(SaveBOMRtngSKUNotAllowedErr); + end; + + [Test] + [HandlerFunctions('HandleWizardSaveOnThenOff')] + procedure TestF5_SaveOnThenOff_SaveTargetCleared() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO F5] Save toggle ON and then OFF: SaveBOMRoutingToSource is cleared + Initialize(); + + // [GIVEN] Wizard open on Step 1 + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User sets Save = true (SaveBOMRoutingToSource auto-set to Item), then sets Save = false + ActualSaveTargetAfterToggleOff := 'NOT-EMPTY'; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] SaveBOMRoutingToSource = Empty + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual(' ', ActualSaveTargetAfterToggleOff, 'SaveBOMRoutingToSource should be cleared (Empty) after Save toggle off'); + end; + + [Test] + [HandlerFunctions('HandleWizardNoSave')] + procedure TestF6_DefineItemStructure_NoSave_NoProdOrderCreated() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO F6] DefineItemStructure mode: finish without Save does not create any production order + Initialize(); + + // [GIVEN] Mode = DefineItemStructure; item with BOM and Routing; Save = false + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + + // [WHEN] User finishes the wizard + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] No Production Order record exists for this item + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyNoProdOrderForItem(ItemNo); + end; + + + [ModalPageHandler] + procedure HandleWizardNoSave(var Wizard: TestPage "Production Definition Wizard") + begin + // Do not change SaveBOMRouting (leave as false) + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardSaveToItem(var Wizard: TestPage "Production Definition Wizard") + begin + // Enable Save (auto-sets SaveBOMRoutingToSource = Item for Item source) + Wizard.SaveBOMRoutingField.SetValue(true); + // Navigate to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardSaveToSKU(var Wizard: TestPage "Production Definition Wizard") + begin + Wizard.SaveBOMRoutingField.SetValue(true); + // Navigate to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardTrySaveToSKUFromItem(var Wizard: TestPage "Production Definition Wizard") + begin + // Enable Save first to activate SaveBomRtngToSourceField + Wizard.SaveBOMRoutingField.SetValue(true); + // Attempt to set StockkeepingUnit — should raise an error since source is Item + Wizard.SaveBOMRtngToSourceField.SetValue("Prod. Definition Save Target"::StockkeepingUnit); + ActualSKUNotAllowedErrorRaised := true; + // Finish (if possible) + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardSaveOnThenOff(var Wizard: TestPage "Production Definition Wizard") + begin + // Set Save = true + Wizard.SaveBOMRoutingField.SetValue(true); + // Set Save = false + Wizard.SaveBOMRoutingField.SetValue(false); + // Capture SaveBomRtngToSourceField value (should be ' ' = Empty) + ActualSaveTargetAfterToggleOff := Wizard.SaveBOMRtngToSourceField.Value(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Prod. Def. Wiz. Save Test"); + LibrarySetupStorage.Restore(); + WizardFinished := false; + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Save Test"); + LibraryERMCountryData.CreateVATData(); + LibraryERMCountryData.UpdateGeneralPostingSetup(); + ProdDefWizSetupLib.InitializeBasicSetup(); + + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + IsInitialized := true; + Commit(); + LibrarySetupStorage.SaveManufacturingSetup(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Save Test"); + end; +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizSourceTest.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizSourceTest.Codeunit.al new file mode 100644 index 0000000000..9bf57f3e2f --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizSourceTest.Codeunit.al @@ -0,0 +1,315 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Location; +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Sales.Document; + +codeunit 137424 "Prod. Def. Wiz. Source Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + + trigger OnRun() + begin + // [FEATURE] Production Definition Wizard - Source Prioritization & Scenario Resolution + IsInitialized := false; + end; + + var + Assert: Codeunit Assert; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + ProdDefWizSetupLib: Codeunit "Prod. Def. Wiz. Setup Lib."; + ProdDefWizCheckLib: Codeunit "Prod. Def. Wiz. Check Lib."; + IsInitialized: Boolean; + WizardFinished: Boolean; + // Handler state + ActualSourceText: Text; + ActualBOMNoFromWizardB5: Code[20]; + ActualRoutingNoFromWizardB5: Code[20]; + + [Test] + [HandlerFunctions('HandleWizardCaptureSourceWithSave')] + procedure TestB1_NeitherHasData_EmptySource_PlaceholderCreated() + var + Item: Record Item; + ProductionBOMLine: Record "Production BOM Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO B1] Neither SKU nor Item has BOM/Routing → Empty source, placeholder lines created + Initialize(); + + // [GIVEN] Item has no BOM and no Routing; no SKU for the item + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is initialized from the Item + ActualSourceText := ''; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Source = Empty; wizard saved a placeholder BOM with 1 line to the item + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual('Manufacturing Setup', ActualSourceText, 'Source should be empty when item has no BOM or Routing'); + Item.Get(ItemNo); + ProductionBOMLine.SetRange("Production BOM No.", Item."Production BOM No."); + Assert.AreEqual(1, ProductionBOMLine.Count(), 'Item BOM should have 1 placeholder line'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureSource')] + procedure TestB2_ItemHasBOMOnly_PartialScenario_RoutingPlaceholder() + var + Item: Record Item; + ProductionBOMLine: Record "Production BOM Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO B2] Item has only BOM (no Routing) → Partial scenario, BOM from Item, Routing placeholder + Initialize(); + + // [GIVEN] Item has BOM-A but no Routing + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is initialized from the Item + ActualSourceText := ''; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Source = Item; BOM-A has 2 lines in the database + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual('Item', ActualSourceText, 'Source should be Item'); + ProductionBOMLine.SetRange("Production BOM No.", BOMNo); + ProductionBOMLine.SetRange("Version Code", ''); + Assert.AreEqual(2, ProductionBOMLine.Count(), 'BOM-A should have 2 lines'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureSourceWithSave')] + procedure TestB3_ItemHasRoutingOnly_PartialScenario_BOMPlaceholder() + var + Item: Record Item; + ProductionBOMLine: Record "Production BOM Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO B3] Item has only Routing (no BOM) → Partial scenario, Routing from Item, BOM placeholder + Initialize(); + + // [GIVEN] Item has Routing-A but no BOM + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is initialized from the Item + ActualSourceText := ''; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Source = Item; wizard saved a placeholder BOM with 1 line to the item + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual('Item', ActualSourceText, 'Source should be Item'); + Item.Get(ItemNo); + ProductionBOMLine.SetRange("Production BOM No.", Item."Production BOM No."); + Assert.AreEqual(1, ProductionBOMLine.Count(), 'Item BOM should have 1 placeholder line'); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureSource')] + procedure TestB4_InitFromSalesLine_NoSKUForLocation_ItemUsed() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO B4] InitializeFromSalesLine: item data resolved when no SKU for that location + Initialize(); + + // [GIVEN] Sales line for an item with BOM-A and Routing-A; sales line location has no SKU + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is launched from the Sales Line + ActualSourceText := ''; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Source = Item; prod order has 2 component lines from BOM-A; + // prod order quantity = 5, due date = WorkDate(), location = sales line location, variant = '' + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual('Item', ActualSourceText, 'Source should be Item (no SKU for location)'); + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasComponentCount(ProdOrder, 2); + ProdDefWizCheckLib.VerifyProdOrderFields(ProdOrder, ItemNo, 5, WorkDate(), LocationCode, ''); + end; + + [ModalPageHandler] + procedure HandleWizardCaptureSource(var Wizard: TestPage "Production Definition Wizard") + begin + // Capture source type text from Step 1 + ActualSourceText := Wizard.BOMRtngFromSourceField.Value(); + + // Navigate to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCaptureSourceWithSave(var Wizard: TestPage "Production Definition Wizard") + begin + // Capture source type text from Step 1 + ActualSourceText := Wizard.BOMRtngFromSourceField.Value(); + + // Enable Save so BOM/Routing changes are persisted to the item + Wizard.SaveBOMRoutingField.SetValue(true); + + // Navigate to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Prod. Def. Wiz. Source Test"); + LibrarySetupStorage.Restore(); + WizardFinished := false; + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Source Test"); + + LibraryERMCountryData.CreateVATData(); + LibraryERMCountryData.UpdateGeneralPostingSetup(); + ProdDefWizSetupLib.InitializeBasicSetup(); + + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + IsInitialized := true; + Commit(); + LibrarySetupStorage.SaveManufacturingSetup(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Source Test"); + end; + + // ----------------------------------------------------------------------------------------- + // CRITICAL #1 — Mixed-source BOM+Routing resolution + // Bug: GetBOMAndRoutingFromBestSource exits after finding SKU BOM, never retrieves Item Routing + // ----------------------------------------------------------------------------------------- + [Test] + [HandlerFunctions('HandleWizardCaptureBOMAndRoutingForB5')] + procedure TestB5_SKUHasBOMOnly_ItemHasRoutingOnly_BothResolved() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + SKU: Record "Stockkeeping Unit"; + ProdDefManager: Codeunit "Production Definition Manager"; + SKUBOMNo: Code[20]; + ItemRoutingNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO B5] SKU has BOM only (no Routing); Item has Routing only (no BOM). + // Wizard must resolve BOM from SKU AND Routing from Item — BothAvailable scenario. + // Detects BUG: GetBOMAndRoutingFromBestSource exits after SKU BOM found, + // never reading Item-level Routing; wizard shows PartiallyAvailable instead. + Initialize(); + + // [GIVEN] Item has Routing-A (no BOM); SKU at LocationCode has BOM-A (no Routing) + SKUBOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemRoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ItemRoutingNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateStockkeepingUnitWithBOMAndRouting(SKU, ItemNo, LocationCode, '', SKUBOMNo, ''); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 3, LocationCode, '', WorkDate()); + // Configure both display scenarios to Edit so all steps are visible regardless of resolved scenario + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is launched from the Sales Line (Sales Line matches the SKU at LocationCode) + ActualBOMNoFromWizardB5 := ''; + ActualRoutingNoFromWizardB5 := ''; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Wizard shows BOM from SKU + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual(SKUBOMNo, ActualBOMNoFromWizardB5, + 'BOM No. in wizard should be the SKU BOM (not empty); mixed-source resolution failed'); + // [THEN] Wizard shows Routing from Item (not empty) — key assertion for the critical bug + Assert.AreEqual(ItemRoutingNo, ActualRoutingNoFromWizardB5, + 'Routing No. in wizard should come from Item even though SKU has no Routing; GetBOMAndRoutingFromBestSource must not exit early'); + // [THEN] Production Order has 2 components (from SKU BOM) and 2 routing lines (from Item Routing) + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderHasComponentCount(ProdOrder, 2); + ProdDefWizCheckLib.VerifyProdOrderHasRoutingLineCount(ProdOrder, 2); + end; + + [ModalPageHandler] + procedure HandleWizardCaptureBOMAndRoutingForB5(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM step) and capture the resolved BOM No. + Wizard.ActionNext.Invoke(); + ActualBOMNoFromWizardB5 := CopyStr(Wizard.ProductionBOMNoField.Value(), 1, 20); + // Navigate to Step 3 (Routing step) and capture the resolved Routing No. + // If the bug is present and only PartiallyAvailable is resolved, Routing step may be empty. + Wizard.ActionNext.Invoke(); + ActualRoutingNoFromWizardB5 := CopyStr(Wizard.RoutingNoField.Value(), 1, 20); + // Navigate to finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizVariantTest.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizVariantTest.Codeunit.al new file mode 100644 index 0000000000..15ee1c28a1 --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizVariantTest.Codeunit.al @@ -0,0 +1,274 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Location; +using Microsoft.Manufacturing.Document; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Sales.Document; + +codeunit 137430 "Prod. Def. Wiz. Variant Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + + trigger OnRun() + begin + // [FEATURE] Production Definition Wizard - Variant Handling + IsInitialized := false; + end; + + var + Assert: Codeunit Assert; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + ProdDefWizSetupLib: Codeunit "Prod. Def. Wiz. Setup Lib."; + ProdDefWizCheckLib: Codeunit "Prod. Def. Wiz. Check Lib."; + IsInitialized: Boolean; + WizardFinished: Boolean; + // Handler state + ActualSourceText: Text; + ActualBOMLineItemNos: List of [Code[20]]; + + + [Test] + procedure TestI1_SalesLineWithVariant_ProdOrderHasVariantAndQuantity() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + ItemNo: Code[20]; + VariantCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO I1] Sales Line with variant code; wizard finishes → Production Order has matching variant and quantity + Initialize(); + + // [GIVEN] Item with variant; Sales Line for that item/variant with quantity 5; wizard UI skipped (Nothing + Hide) + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', ''); + VariantCode := ProdDefWizLibrary.CreateVariantForItem(ItemNo); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, '', VariantCode, WorkDate()); + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Hide, "Prod. Definition Display"::Hide); + + // [WHEN] Wizard runs from Sales Line (UI skipped — no modal handler needed) + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Production Order exists with Variant Code and Quantity = 5 + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderFields(ProdOrder, ItemNo, 5, WorkDate(), '', VariantCode); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureSourceAndBOMLines')] + procedure TestI2_SKUWithVariant_WizardInitializesFromCorrectSKU() + var + SKU: Record "Stockkeeping Unit"; + ProductionBOMLine: Record "Production BOM Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + ActualBOMLineItemNo: Code[20]; + ItemBOMNo: Code[20]; + SKUBOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + VariantCode: Code[10]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO I2] SKU with variant: wizard initializes from correct SKU (matching variant) + Initialize(); + + // [GIVEN] Item has SKU for location "EAST" + variant "BLUE" with BOM-B; Item itself has BOM-A + ItemBOMNo := ProdDefWizLibrary.CreateBOM(2); + SKUBOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(ItemBOMNo, RoutingNo); + VariantCode := ProdDefWizLibrary.CreateVariantForItem(ItemNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + ProdDefWizLibrary.CreateStockkeepingUnitWithBOMAndRouting(SKU, ItemNo, LocationCode, VariantCode, SKUBOMNo, RoutingNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard is initialized from that SKU (with variant) + ActualSourceText := ''; + Commit(); + ProdDefManager.RunForSource(SKU, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Source = StockkeepingUnit; BOM lines match BOM-B (not BOM-A from Item) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual('Stockkeeping Unit', ActualSourceText, 'Source should be Stockkeeping Unit (matched by variant)'); + // Verify wizard displayed lines from SKU BOM-B, not Item BOM-A + ProductionBOMLine.SetRange("Production BOM No.", SKUBOMNo); + ActualBOMLineItemNos.Remove(''); + Assert.IsTrue(ActualBOMLineItemNos.Count() > 0, 'Wizard should have shown BOM lines from SKU BOM-B'); + Assert.AreEqual(ProductionBOMLine.Count(), ActualBOMLineItemNos.Count(), + 'Wizard BOM line count should match SKU BOM-B line count (not Item BOM-A)'); + foreach ActualBOMLineItemNo in ActualBOMLineItemNos do + if ActualBOMLineItemNo <> '' then begin + ProductionBOMLine.SetRange("No.", ActualBOMLineItemNo); + Assert.IsTrue(ProductionBOMLine.FindFirst(), + StrSubstNo('BOM line item %1 should belong to SKU BOM-B, not Item BOM-A', ActualBOMLineItemNo)); + end; + // DefineItemStructure mode: no production order should have been created + ProdDefWizCheckLib.VerifyNoProdOrderForItem(ItemNo); + end; + + [Test] + [HandlerFunctions('HandleWizardCaptureSourceAndBOMLines')] + procedure TestI3_SKULookupConsidersLocationAndVariant_ExactVariantMatch() + var + SKU1: Record "Stockkeeping Unit"; + SKU2: Record "Stockkeeping Unit"; + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProductionBOMLine: Record "Production BOM Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + ActualBOMLineItemNo: Code[20]; + ItemBOMNo: Code[20]; + SKU1BOMNo: Code[20]; + SKU2BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + VariantCode: Code[10]; + LocationCode: Code[10]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO I3] SKU lookup considers both location and variant code — exact variant match wins + Initialize(); + + // [GIVEN] Two SKUs same item: SKU1(Loc=EAST, Var=''), SKU2(Loc=EAST, Var=RED); Sales Line Loc=EAST, Var=RED + ItemBOMNo := ProdDefWizLibrary.CreateBOM(2); + SKU1BOMNo := ProdDefWizLibrary.CreateBOM(2); + SKU2BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(ItemBOMNo, RoutingNo); + VariantCode := ProdDefWizLibrary.CreateVariantForItem(ItemNo); + LocationCode := ProdDefWizLibrary.CreateLocationCode(); + + // SKU1: no variant + ProdDefWizLibrary.CreateStockkeepingUnitWithBOMAndRouting(SKU1, ItemNo, LocationCode, '', SKU1BOMNo, RoutingNo); + + // SKU2: with variant — exact match + ProdDefWizLibrary.CreateStockkeepingUnitWithBOMAndRouting(SKU2, ItemNo, LocationCode, VariantCode, SKU2BOMNo, RoutingNo); + + // Sales Line with Loc=LocationCode, Var=VariantCode + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 3, LocationCode, VariantCode, WorkDate()); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] Wizard initialized from Sales Line + ActualSourceText := ''; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] SKU2 is selected (exact variant match); its BOM populates the wizard + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual('Stockkeeping Unit', ActualSourceText, 'Source should be StockkeepingUnit (exact variant match)'); + // CreateProductionOrder mode: a production order must exist for this item + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProductionBOMLine.SetRange("Production BOM No.", SKU2BOMNo); + // Verify the wizard showed BOM lines and that the count matches SKU2's BOM + ActualBOMLineItemNos.Remove(''); + Assert.IsTrue(ActualBOMLineItemNos.Count() > 0, 'Wizard should have shown at least one BOM line'); + Assert.AreEqual(ProductionBOMLine.Count(), ActualBOMLineItemNos.Count(), 'Wizard BOM line count should match SKU2''s BOM line count'); + foreach ActualBOMLineItemNo in ActualBOMLineItemNos do + if ActualBOMLineItemNo <> '' then begin + ProductionBOMLine.SetRange("No.", ActualBOMLineItemNo); + Assert.IsTrue(ProductionBOMLine.FindFirst(), + StrSubstNo('BOM line item %1 should belong to SKU2''s BOM (exact variant match), not SKU1''s', ActualBOMLineItemNo)); + end; + end; + + + [Test] + procedure TestI_ComponentVariantCodePropagated() + var + SalesLine: Record "Sales Line"; + ProdOrder: Record "Production Order"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + ComponentItemNo: Code[20]; + ComponentVariantCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO I_ComponentVariant] BOM line with a variant code → Prod. Order Component carries the same + // variant code after wizard finishes (regression test for BUG-03). + Initialize(); + + // [GIVEN] BOM with one component line that specifies a variant; Sales Line sources this item + BOMNo := ProdDefWizLibrary.CreateBOMWithComponentVariant(ComponentItemNo, ComponentVariantCode); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, '', '', WorkDate()); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Hide, "Prod. Definition Display"::Hide); + + // [WHEN] Wizard runs in CreateProductionOrder mode (UI skipped — no modal handler needed) + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::CreateProductionOrder); + + // [THEN] Prod. Order Component inherits the variant code from the BOM line + ProdDefWizCheckLib.VerifyProdOrderExists(ItemNo, ProdOrder); + ProdDefWizCheckLib.VerifyProdOrderComponentHasVariantCode(ProdOrder, ComponentVariantCode); + end; + + + [ModalPageHandler] + procedure HandleWizardCaptureSourceAndBOMLines(var Wizard: TestPage "Production Definition Wizard") + begin + // Capture source text on Step 1 + ActualSourceText := Wizard.BOMRtngFromSourceField.Value(); + // Navigate to Step 2 (BOM) + if Wizard.ActionNext.Enabled() then + Wizard.ActionNext.Invoke(); + + // Capture all BOM line item Nos to verify which BOM was loaded + Clear(ActualBOMLineItemNos); + if Wizard.BOMLinesPart.First() then + repeat + ActualBOMLineItemNos.Add(CopyStr(Wizard.BOMLinesPart."No.".Value(), 1, 20)); + until not Wizard.BOMLinesPart.Next(); + + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Prod. Def. Wiz. Variant Test"); + LibrarySetupStorage.Restore(); + WizardFinished := false; + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Variant Test"); + LibraryERMCountryData.CreateVATData(); + LibraryERMCountryData.UpdateGeneralPostingSetup(); + ProdDefWizSetupLib.InitializeBasicSetup(); + + ProdDefWizSetupLib.ConfigureForNothingAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + IsInitialized := true; + Commit(); + LibrarySetupStorage.SaveManufacturingSetup(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Variant Test"); + end; + +} \ No newline at end of file diff --git a/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizVersionTest.Codeunit.al b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizVersionTest.Codeunit.al new file mode 100644 index 0000000000..430a1f71e3 --- /dev/null +++ b/src/Layers/W1/Tests/SCM-Manufacturing/ProductionDefinitionWizard/ProdDefWizVersionTest.Codeunit.al @@ -0,0 +1,814 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Manufacturing.Test; + +using Microsoft.Inventory.Item; +using Microsoft.Manufacturing.ProductionBOM; +using Microsoft.Manufacturing.Routing; +using Microsoft.Manufacturing.Wizard; +using Microsoft.Sales.Document; + +codeunit 137427 "Prod. Def. Wiz. Version Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + + trigger OnRun() + begin + // [FEATURE] Production Definition Wizard - Version Management + IsInitialized := false; + end; + + var + Assert: Codeunit Assert; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + ProdDefWizLibrary: Codeunit "Prod. Def. Wiz. Library"; + ProdDefWizSetupLib: Codeunit "Prod. Def. Wiz. Setup Lib."; + ProdDefWizCheckLib: Codeunit "Prod. Def. Wiz. Check Lib."; + IsInitialized: Boolean; + WizardFinished: Boolean; + // Handler state + ActualEditBOMLines: Boolean; + ActualSelectedBOMVersionText: Text; + ActualCreateBOMVersionToggleRaisedError: Boolean; + ActualCreateBOMVersionErrorText: Text; + ActualEditRoutingLines: Boolean; + ActualSelectedRoutingVersionText: Text; + ActualCreateRoutingVersionToggleRaisedError: Boolean; + ActualCreateRoutingVersionErrorText: Text; + + + [Test] + [HandlerFunctions('HandleWizardToggleCreateBOMVersionOn')] + procedure TestE1_CreateNewBOMVersionToggleOn_BOMEditable() + var + SalesLine: Record "Sales Line"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + LocationCode: Code[10]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E1] "Create New BOM Version" toggle enabled → BOM lines become editable + Initialize(); + + // [GIVEN] Wizard is on Step 2; BOMRoutingDisplay = Edit; item has a certified BOM with Version Nos. series + BOMNo := ProdDefWizLibrary.CreateBOM(2); // already sets Version Nos. + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + ProdDefWizLibrary.CreateSalesLine(SalesLine, ItemNo, 5, LocationCode, '', WorkDate()); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User turns on "Create New BOM Version" + ActualEditBOMLines := false; + ActualSelectedBOMVersionText := ''; + Commit(); + ProdDefManager.RunForSource(SalesLine, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] EditBOMLines = true; SelectedBOMVersion starts with 'TEMP' + // Note: Save=false and AlwaysSaveModifiedVersions=false → the TEMP version is discarded at Finish. + // VerifyNoBOMVersionExists confirms the version was not inadvertently committed to the database. + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.IsTrue(ActualEditBOMLines, 'BOM lines should be editable after toggling Create New BOM Version'); + Assert.IsTrue(ActualSelectedBOMVersionText.StartsWith('TEMP'), 'SelectedBOMVersion should start with TEMP'); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyNoBOMVersionExists(BOMNo); + end; + + [Test] + [HandlerFunctions('HandleWizardToggleCreateBOMVersionOnThenOff')] + procedure TestE2_CreateNewBOMVersionToggleOff_LinesRevert() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E2] "Create New BOM Version" disabled (turned off again) → BOM lines revert to source lines + Initialize(); + + // [GIVEN] User had turned on "Create New BOM Version" on Step 2 + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User turns off "Create New BOM Version" + ActualSelectedBOMVersionText := 'NOT-EMPTY'; // will be reset by handler + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] SelectedBOMVersion is cleared (empty) + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual('', ActualSelectedBOMVersionText, 'SelectedBOMVersion should be cleared after turning off Create New BOM Version'); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyNoBOMVersionExists(BOMNo); + end; + + [Test] + [HandlerFunctions('HandleWizardToggleCreateRoutingVersionOnAndClose')] + procedure TestE3_CreateNewRoutingVersionToggleOn_RoutingEditable() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E3] "Create New Routing Version" toggle enabled → Routing lines become editable + Initialize(); + + // [GIVEN] Item has certified BOM and certified Routing with Version Nos. series set; display = Edit + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User navigates to the Routing step and turns on "Create New Routing Version" + ActualEditRoutingLines := false; + ActualSelectedRoutingVersionText := ''; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] EditRoutingLines = true; SelectedRoutingVersion starts with 'TEMP'; + // the TEMP version is discarded at closing + Assert.IsFalse(WizardFinished, 'Wizard should not have finished'); + Assert.IsTrue(ActualEditRoutingLines, 'Routing lines should be editable after toggling Create New Routing Version'); + Assert.IsTrue(ActualSelectedRoutingVersionText.StartsWith('TEMP'), 'SelectedRoutingVersion should start with TEMP'); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyItemHasRouting(ItemNo, RoutingNo); + ProdDefWizCheckLib.VerifyNoBOMVersionExists(BOMNo); + ProdDefWizCheckLib.VerifyNoRoutingVersionExists(RoutingNo); + end; + + [Test] + [HandlerFunctions('HandleWizardCreateNewBOMVersionAndSave')] + procedure TestE4_FinishWithNewBOMVersion_VersionCertified() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + LastBOMVersionBefore: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E4] "Create New BOM Version" on finish: new BOM version is created and certified + Initialize(); + + // [GIVEN] Wizard on Step 2 with "Create New BOM Version" = true; Save = Item + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + LastBOMVersionBefore := ProdDefWizCheckLib.GetLastBOMVersionCode(BOMNo); + + // [WHEN] User finishes the wizard with Create New BOM Version enabled + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] The item still has the same BOM; a new certified BOM version was created + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyNewLastBOMVersionCertified(BOMNo, LastBOMVersionBefore); + end; + + [Test] + [HandlerFunctions('HandleWizardCreateNewRoutingVersionAndSave')] + procedure TestE5_FinishWithNewRoutingVersion_VersionCertified() + var + Item: Record Item; + RoutingHeader: Record "Routing Header"; + RoutingVersion: Record "Routing Version"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + LastRoutingVersionBefore: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E5] "Create New Routing Version" on finish: new Routing version is created and certified + Initialize(); + + // [GIVEN] Item has certified BOM and certified Routing with Version Nos. series set; Save = Item + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + LastRoutingVersionBefore := ProdDefWizCheckLib.GetLastRoutingVersionCode(RoutingNo); + + // [WHEN] User enables Save and "Create New Routing Version", then finishes + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] The item still has the same Routing; a new certified Routing version was created + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyItemHasRouting(ItemNo, RoutingNo); + ProdDefWizCheckLib.VerifyNewLastRoutingVersionCertified(RoutingNo, LastRoutingVersionBefore); + + // [THEN] The new Routing version Type is not blank (must inherit Serial or Parallel from source Routing Header) + // Detects BUG: RoutingVersion.Type never set; defaults to blank (0) + RoutingHeader.Get(RoutingNo); + RoutingVersion.SetRange("Routing No.", RoutingNo); + RoutingVersion.FindLast(); + Assert.AreEqual(Format(RoutingHeader.Type), Format(RoutingVersion.Type), + 'New Routing version Type should match the source Routing Header Type (Serial/Parallel), not remain blank'); + + // [THEN] The new Routing version Starting Date = WorkDate() + Assert.AreEqual(WorkDate(), RoutingVersion."Starting Date", + 'New Routing version Starting Date should be WorkDate()'); + end; + + [Test] + [HandlerFunctions('HandleWizardToggleCreateBOMVersionError')] + procedure TestE6_BOMWithoutVersionNos_ToggleRaisesError() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E6] BOM without Version Nos. series → "Create New BOM Version" check raises error + Initialize(); + + // [GIVEN] Item has a certified BOM where "Version Nos." field is empty + BOMNo := ProdDefWizLibrary.CreateBOMWithoutVersionNos(); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User turns on "Create New BOM Version" on Step 2 + ActualCreateBOMVersionToggleRaisedError := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] An error is raised indicating version number series is required + Assert.IsTrue(ActualCreateBOMVersionToggleRaisedError, 'Turning on Create New BOM Version without Version Nos. should raise an error'); + Assert.AreNotEqual('', ActualCreateBOMVersionErrorText, 'Error text must be captured to confirm the correct error was raised'); + Assert.IsTrue(ActualCreateBOMVersionErrorText.Contains('Version Nos.'), + StrSubstNo('Expected error about ''Version Nos.'' but got: %1', ActualCreateBOMVersionErrorText)); + ProdDefWizCheckLib.VerifyItemHasBOM(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyNoBOMVersionExists(BOMNo); + end; + + [Test] + [HandlerFunctions('HandleWizardCreateNewBOMVersionAndSaveVersionOnly')] + procedure TestE7_AlwaysSaveModifiedVersions_True_VersionKept() + var + Item: Record Item; + ProductionBOMVersion: Record "Production BOM Version"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + LastBOMVersionBefore: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E7] Always Save Modified Versions = true → new version kept even when Save toggle is off + Initialize(); + + // [GIVEN] Manufacturing Setup: Always Save Modified Versions = true; user creates new BOM version but Save toggle = false + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + ProdDefWizSetupLib.SetAlwaysSaveModifiedVersions(true); + // Re-read Manufacturing Setup to confirm the flag was persisted before the wizard runs + Assert.IsTrue(ProdDefWizSetupLib.GetAlwaysSaveModifiedVersions(), 'AlwaysSaveModifiedVersions must be true in Manufacturing Setup before the wizard runs'); + LastBOMVersionBefore := ProdDefWizCheckLib.GetLastBOMVersionCode(BOMNo); + + // [WHEN] User finishes wizard with Create New BOM Version = true but SaveBOMRouting = false + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Item BOM unchanged; a new certified BOM version was saved + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyItemBOMUnchanged(ItemNo, BOMNo); + ProdDefWizCheckLib.VerifyNewLastBOMVersionCertified(BOMNo, LastBOMVersionBefore); + + // [THEN] The new BOM version has exactly 2 lines (source BOM has 2 lines) + // Detects stale-accumulation bug if line count is doubled + ProductionBOMVersion.SetRange("Production BOM No.", BOMNo); + ProductionBOMVersion.FindLast(); + ProdDefWizCheckLib.VerifyBOMVersionLineCount(BOMNo, ProductionBOMVersion."Version Code", 2); + end; + + [Test] + [HandlerFunctions('HandleWizardToggleCreateRoutingVersionError')] + procedure TestE9_RoutingWithoutVersionNos_ToggleRaisesError() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + RoutingNo: Code[20]; + ItemNo: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E9] Routing without Version Nos. series → "Create New Routing Version" check raises error + Initialize(); + + // [GIVEN] Item has a certified Routing where "Version Nos." field is empty + RoutingNo := ProdDefWizLibrary.CreateRoutingWithoutVersionNos(); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting('', RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User turns on "Create New Routing Version" on Step 3 + ActualCreateRoutingVersionToggleRaisedError := false; + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] An error is raised indicating version number series is required + Assert.IsTrue(ActualCreateRoutingVersionToggleRaisedError, 'Turning on Create New Routing Version without Version Nos. should raise an error'); + Assert.AreNotEqual('', ActualCreateRoutingVersionErrorText, 'Error text must be captured to confirm the correct error was raised'); + Assert.IsTrue(ActualCreateRoutingVersionErrorText.Contains('Version Nos.'), + StrSubstNo('Expected error about ''Version Nos.'' but got: %1', ActualCreateRoutingVersionErrorText)); + ProdDefWizCheckLib.VerifyItemHasRouting(ItemNo, RoutingNo); + ProdDefWizCheckLib.VerifyNoRoutingVersionExists(RoutingNo); + end; + + + [ModalPageHandler] + procedure HandleWizardToggleCreateBOMVersionOn(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Toggle on Create New BOM Version + Wizard.CreateBOMVersionField.SetValue(true); + // Capture state + ActualEditBOMLines := Wizard.BOMLinesPart.Enabled(); + ActualSelectedBOMVersionText := Wizard.SelectedBOMVersionField.Value(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardToggleCreateBOMVersionOnThenOff(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Toggle on, then off + Wizard.CreateBOMVersionField.SetValue(true); + Wizard.CreateBOMVersionField.SetValue(false); + // Capture SelectedBOMVersion (should be empty) + ActualSelectedBOMVersionText := Wizard.SelectedBOMVersionField.Value(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCreateNewBOMVersionAndSave(var Wizard: TestPage "Production Definition Wizard") + begin + // Enable Save to Item on Step 1 + Wizard.SaveBOMRoutingField.SetValue(true); + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Toggle on Create New BOM Version + Wizard.CreateBOMVersionField.SetValue(true); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardToggleCreateBOMVersionError(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Toggle on Create New BOM Version — expect error because no Version Nos. series + asserterror Wizard.CreateBOMVersionField.SetValue(true); + ActualCreateBOMVersionToggleRaisedError := true; + ActualCreateBOMVersionErrorText := GetLastErrorText(); + // Close wizard by invoking Finish (if still enabled) + if Wizard.ActionFinish.Enabled() then + Wizard.ActionFinish.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardCreateNewBOMVersionAndSaveVersionOnly(var Wizard: TestPage "Production Definition Wizard") + begin + // Do NOT set SaveBOMRouting (leave false) + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Toggle on Create New BOM Version + Wizard.CreateBOMVersionField.SetValue(true); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardToggleCreateRoutingVersionError(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) + Wizard.ActionNext.Invoke(); + // Toggle on Create New Routing Version — expect error because no Version Nos. series + asserterror Wizard.CreateRoutingVersionField.SetValue(true); + ActualCreateRoutingVersionToggleRaisedError := true; + ActualCreateRoutingVersionErrorText := GetLastErrorText(); + // Close wizard by invoking Finish (if still enabled) + if Wizard.ActionFinish.Enabled() then + Wizard.ActionFinish.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardToggleCreateRoutingVersionOnAndClose(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) + Wizard.ActionNext.Invoke(); + // Toggle on Create New Routing Version + Wizard.CreateRoutingVersionField.SetValue(true); + // Capture state + ActualEditRoutingLines := Wizard.RoutingLinesPart.Enabled(); + ActualSelectedRoutingVersionText := Wizard.SelectedRoutingVersionField.Value(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + end; + + [ModalPageHandler] + procedure HandleWizardToggleCreateRoutingVersionOnThenOff(var Wizard: TestPage "Production Definition Wizard") + begin + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) + Wizard.ActionNext.Invoke(); + // Toggle on, then off + Wizard.CreateRoutingVersionField.SetValue(true); + Wizard.CreateRoutingVersionField.SetValue(false); + // Capture SelectedRoutingVersion (should be empty after toggle-off) + ActualSelectedRoutingVersionText := Wizard.SelectedRoutingVersionField.Value(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCreateNewRoutingVersionAndSave(var Wizard: TestPage "Production Definition Wizard") + begin + // Enable Save to Item on Step 1 + Wizard.SaveBOMRoutingField.SetValue(true); + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) + Wizard.ActionNext.Invoke(); + // Toggle on Create New Routing Version + Wizard.CreateRoutingVersionField.SetValue(true); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [Test] + [HandlerFunctions('HandleWizardToggleCreateRoutingVersionOnThenOff')] + procedure TestE8_CreateNewRoutingVersionToggleOff_LinesRevert() + var + Item: Record Item; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E8] "Create New Routing Version" disabled (turned off again) → Routing lines revert to source lines + Initialize(); + + // [GIVEN] Item with certified BOM and Routing; user had turned on "Create New Routing Version" on Step 3 + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + // [WHEN] User turns off "Create New Routing Version" + ActualSelectedRoutingVersionText := 'NOT-EMPTY'; // will be reset by handler + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] SelectedRoutingVersion is cleared (empty); no routing version committed to the database + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + Assert.AreEqual('', ActualSelectedRoutingVersionText, 'SelectedRoutingVersion should be cleared after turning off Create New Routing Version'); + ProdDefWizCheckLib.VerifyItemHasRouting(ItemNo, RoutingNo); + ProdDefWizCheckLib.VerifyNoRoutingVersionExists(RoutingNo); + end; + + [Test] + [HandlerFunctions('HandleWizardCreateNewBOMVersionAndSave')] + procedure TestE_BOMVersionLinesHaveCorrectVersionCode() + var + Item: Record Item; + ProductionBOMLine: Record "Production BOM Line"; + ProductionBOMVersion: Record "Production BOM Version"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + LastBOMVersionBefore: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E_BOMVersionLines] After the wizard creates a new BOM version every BOM line carries + // the correct "Version Code" (regression test for BUG-01). + Initialize(); + + // [GIVEN] Item with certified BOM; wizard configured with Create New BOM Version + Save=Item + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + LastBOMVersionBefore := ProdDefWizCheckLib.GetLastBOMVersionCode(BOMNo); + + // [WHEN] Wizard creates and saves a new BOM version + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] A new certified BOM version exists + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyNewLastBOMVersionCertified(BOMNo, LastBOMVersionBefore); + + // [THEN] Every BOM line under the new version has "Version Code" = the new version code + ProductionBOMVersion.SetRange("Production BOM No.", BOMNo); + ProductionBOMVersion.FindLast(); + ProductionBOMLine.SetRange("Production BOM No.", BOMNo); + ProductionBOMLine.SetRange("Version Code", ProductionBOMVersion."Version Code"); + Assert.IsTrue(ProductionBOMLine.FindSet(), + StrSubstNo('New BOM version %1 must have at least one line', ProductionBOMVersion."Version Code")); + repeat + Assert.AreEqual(ProductionBOMVersion."Version Code", ProductionBOMLine."Version Code", + StrSubstNo('BOM line "Version Code" must equal the new version code; expected %1, got %2', + ProductionBOMVersion."Version Code", ProductionBOMLine."Version Code")); + until ProductionBOMLine.Next() = 0; + end; + + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Prod. Def. Wiz. Version Test"); + LibrarySetupStorage.Restore(); + WizardFinished := false; + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Version Test"); + LibraryERMCountryData.CreateVATData(); + LibraryERMCountryData.UpdateGeneralPostingSetup(); + ProdDefWizSetupLib.InitializeBasicSetup(); + + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + + IsInitialized := true; + Commit(); + LibrarySetupStorage.SaveManufacturingSetup(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Prod. Def. Wiz. Version Test"); + end; + + // ----------------------------------------------------------------------------------------- + // CRITICAL #2 — UpdateBOMVersionCode stale accumulation + // Bug: old TempBOMLine records not deleted before re-inserting updated copy; + // back-forward navigation doubles the BOM version line count. + // ----------------------------------------------------------------------------------------- + [Test] + [HandlerFunctions('HandleWizardCreateBOMVersionBackForwardThenSave')] + procedure TestE10_BackForwardNavigation_BOMVersionLineCountStable() + var + Item: Record Item; + ProductionBOMVersion: Record "Production BOM Version"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + ItemNo: Code[20]; + LastBOMVersionBefore: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E10] "Create New BOM Version" enabled; user navigates Back then Forward on the + // BOM step; the resulting BOM version must contain exactly the original number + // of BOM lines (2), not double (4) due to stale TempBOMLine accumulation. + Initialize(); + + // [GIVEN] Item with certified BOM of 2 lines; wizard configured with Create New BOM Version + Save = Item + BOMNo := ProdDefWizLibrary.CreateBOM(2); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, ''); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForPartiallyAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + LastBOMVersionBefore := ProdDefWizCheckLib.GetLastBOMVersionCode(BOMNo); + + // [WHEN] User enables CreateBOMVersion, navigates Back to Step 1, then Forward to Step 2 again, then finishes + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] A new certified BOM version was created + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyNewLastBOMVersionCertified(BOMNo, LastBOMVersionBefore); + + // [THEN] The new BOM version has exactly 2 lines — not 4 due to stale TempBOMLine records + ProductionBOMVersion.SetRange("Production BOM No.", BOMNo); + ProductionBOMVersion.FindLast(); + ProdDefWizCheckLib.VerifyBOMVersionLineCount(BOMNo, ProductionBOMVersion."Version Code", 2); + end; + + // ----------------------------------------------------------------------------------------- + // LOW #16 — AlwaysSaveModifiedVersions + Routing version (mirrors TestE7 for BOM) + // ----------------------------------------------------------------------------------------- + [Test] + [HandlerFunctions('HandleWizardCreateNewRoutingVersionNoSave')] + procedure TestE11_AlwaysSaveModifiedVersions_True_RoutingVersionKept() + var + Item: Record Item; + RoutingVersion: Record "Routing Version"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + LastRoutingVersionBefore: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E11] Always Save Modified Versions = true → new Routing version kept even when + // Save toggle is off. Mirrors TestE7 (BOM) for the Routing path. + Initialize(); + + // [GIVEN] Item with certified BOM and Routing; AlwaysSaveModifiedVersions = true; Save toggle = false + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + ProdDefWizSetupLib.SetAlwaysSaveModifiedVersions(true); + Assert.IsTrue(ProdDefWizSetupLib.GetAlwaysSaveModifiedVersions(), + 'AlwaysSaveModifiedVersions must be true in Manufacturing Setup before the wizard runs'); + LastRoutingVersionBefore := ProdDefWizCheckLib.GetLastRoutingVersionCode(RoutingNo); + + // [WHEN] User finishes wizard with Create New Routing Version = true but SaveBOMRouting = false + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] Item Routing unchanged; a new certified Routing version was saved + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyItemRoutingUnchanged(ItemNo, RoutingNo); + ProdDefWizCheckLib.VerifyNewLastRoutingVersionCertified(RoutingNo, LastRoutingVersionBefore); + + // [THEN] The new Routing version has exactly 2 lines (source Routing has 2 lines) + // Detects UpdateRoutingVersionCode stale-accumulation bug if line count is doubled + RoutingVersion.SetRange("Routing No.", RoutingNo); + RoutingVersion.FindLast(); + ProdDefWizCheckLib.VerifyRoutingVersionLineCount(RoutingNo, RoutingVersion."Version Code", 2); + end; + + [Test] + [HandlerFunctions('HandleWizardCreateRoutingVersionBackForwardThenSave')] + procedure TestE12_BackForwardNavigation_RoutingVersionLineCountStable() + var + Item: Record Item; + RoutingVersion: Record "Routing Version"; + ProdDefManager: Codeunit "Production Definition Manager"; + BOMNo: Code[20]; + RoutingNo: Code[20]; + ItemNo: Code[20]; + WC1No: Code[20]; + WC2No: Code[20]; + LastRoutingVersionBefore: Code[20]; + begin + // [FEATURE] Production Definition Wizard + // [SCENARIO E12] "Create New Routing Version" enabled; user navigates Back then Forward on the + // Routing step; the resulting Routing version must contain exactly the original + // number of lines (2), not double (4) due to stale TempRoutingLine accumulation. + Initialize(); + + // [GIVEN] Item with certified BOM and Routing of 2 lines; wizard configured with Create New Routing Version + Save = Item + BOMNo := ProdDefWizLibrary.CreateBOM(2); + RoutingNo := ProdDefWizLibrary.CreateRoutingWithTwoLines(WC1No, WC2No); + ItemNo := ProdDefWizLibrary.CreateItemWithBOMAndRouting(BOMNo, RoutingNo); + Item.Get(ItemNo); + ProdDefWizSetupLib.ConfigureForBothAvailable( + "Prod. Definition Display"::Edit, "Prod. Definition Display"::Edit); + LastRoutingVersionBefore := ProdDefWizCheckLib.GetLastRoutingVersionCode(RoutingNo); + + // [WHEN] User enables CreateRoutingVersion, navigates Back to Step 2, then Forward to Step 3 again, then finishes + Commit(); + ProdDefManager.RunForSource(Item, "Prod. Definition Mode"::DefineItemStructure); + + // [THEN] A new certified Routing version was created + Assert.IsTrue(WizardFinished, 'Wizard should have finished'); + ProdDefWizCheckLib.VerifyNewLastRoutingVersionCertified(RoutingNo, LastRoutingVersionBefore); + + // [THEN] The new Routing version has exactly 2 lines — not 4 due to stale TempRoutingLine records + // Detects BUG: UpdateRoutingVersionCode does not delete old temp records before re-insert + RoutingVersion.SetRange("Routing No.", RoutingNo); + RoutingVersion.FindLast(); + ProdDefWizCheckLib.VerifyRoutingVersionLineCount(RoutingNo, RoutingVersion."Version Code", 2); + end; + + [ModalPageHandler] + procedure HandleWizardCreateBOMVersionBackForwardThenSave(var Wizard: TestPage "Production Definition Wizard") + begin + // Enable Save to Item + Wizard.SaveBOMRoutingField.SetValue(true); + // Navigate to Step 2 (BOM) and enable Create New BOM Version + Wizard.ActionNext.Invoke(); + Wizard.CreateBOMVersionField.SetValue(true); + // Navigate BACK to Step 1 (triggers internal BOM version temp-data refresh) + Wizard.ActionBack.Invoke(); + // Navigate FORWARD to Step 2 again (re-triggers UpdateBOMVersionCode; bug = lines doubled) + Wizard.ActionNext.Invoke(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCreateRoutingVersionBackForwardThenSave(var Wizard: TestPage "Production Definition Wizard") + begin + // Enable Save to Item + Wizard.SaveBOMRoutingField.SetValue(true); + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) and enable Create New Routing Version + Wizard.ActionNext.Invoke(); + Wizard.CreateRoutingVersionField.SetValue(true); + // Navigate BACK to Step 2 (triggers internal Routing version temp-data refresh) + Wizard.ActionBack.Invoke(); + Wizard.ActionNext.Invoke(); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; + + [ModalPageHandler] + procedure HandleWizardCreateNewRoutingVersionNoSave(var Wizard: TestPage "Production Definition Wizard") + begin + // Do NOT set SaveBOMRouting (leave false) + // Navigate to Step 2 (BOM) + Wizard.ActionNext.Invoke(); + // Navigate to Step 3 (Routing) + Wizard.ActionNext.Invoke(); + // Toggle on Create New Routing Version + Wizard.CreateRoutingVersionField.SetValue(true); + // Finish + while Wizard.ActionNext.Enabled() do + Wizard.ActionNext.Invoke(); + if Wizard.ActionFinish.Enabled() then begin + Wizard.ActionFinish.Invoke(); + WizardFinished := true; + end; + end; +} \ No newline at end of file