From 673cf9069a99562204f2dbd09ccb9339181f43f3 Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah Date: Wed, 9 Sep 2026 21:54:25 +0200 Subject: [PATCH] Bug 648535: Preserve subcontracting costs during scheduling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SubcReqWkshMakeOrd.Codeunit.al | 12 +- .../SubcCalculateSubcontracts.Report.al | 2 +- .../Purchase/SubcPriceManagement.Codeunit.al | 12 ++ .../Purchase/SubcPurchaseLineExt.Codeunit.al | 32 ++++- .../SubcPurchaseOrderCreator.Codeunit.al | 2 +- .../Test/Tests/SubcPricingTest.Codeunit.al | 128 ++++++++++++++++++ 6 files changed, 177 insertions(+), 11 deletions(-) diff --git a/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al b/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al index 4778b502c49..3eb30ad1b15 100644 --- a/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al @@ -29,10 +29,8 @@ codeunit 20516 "Subc. Req. Wksh. Make Ord." HandleSubcontractingAfterPurchOrderLineInsert(PurchOrderLine, NextLineNo, RequisitionLine); end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Req. Wksh.-Make Order", OnInsertPurchOrderLineOnAfterTransferFromReqLineToPurchLine, '', false, false)] - local procedure OnInsertPurchOrderLineOnAfterTransferFromReqLineToPurchLine(var PurchOrderLine: Record "Purchase Line"; RequisitionLine: Record "Requisition Line") - var - SubcPriceManagement: Codeunit "Subc. Price Management"; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Req. Wksh.-Make Order", OnBeforeCopyOrderDateFromPurchHeader, '', false, false)] + local procedure OnBeforeCopyOrderDateFromPurchHeader(var RequisitionLine: Record "Requisition Line"; PurchaseHeader: Record "Purchase Header"; PurchaseLine: Record "Purchase Line"; var IsHandled: Boolean) begin #if not CLEAN29 #pragma warning disable AL0432 @@ -40,10 +38,14 @@ codeunit 20516 "Subc. Req. Wksh. Make Ord." #pragma warning restore AL0432 exit; #endif + if PurchaseHeader."Document Type" <> PurchaseHeader."Document Type"::Order then + exit; + if PurchaseLine.Type <> PurchaseLine.Type::Item then + exit; if (RequisitionLine."Prod. Order No." = '') or (RequisitionLine."Operation No." = '') then exit; - SubcPriceManagement.GetSubcPriceForPurchLine(PurchOrderLine); + IsHandled := true; end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Req. Wksh.-Make Order", OnInsertPurchOrderLineOnAfterCheckInsertFinalizePurchaseOrderHeader, '', false, false)] diff --git a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcCalculateSubcontracts.Report.al b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcCalculateSubcontracts.Report.al index f313e44735e..d2e797ca71b 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcCalculateSubcontracts.Report.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcCalculateSubcontracts.Report.al @@ -195,7 +195,6 @@ report 20505 "Subc. Calculate Subcontracts" ReqLine."Qty. Rounding Precision (Base)" := ProdOrderLine."Qty. Rounding Precision (Base)"; ReqLine."Prod. Order No." := ProdOrderLine."Prod. Order No."; ReqLine."Prod. Order Line No." := ProdOrderLine."Line No."; - ReqLine."Due Date" := ProdOrderRoutingLine."Ending Date"; ReqLine."Requester ID" := CopyStr(UserId(), 1, 50); ReqLine."Location Code" := ProdOrderLine."Location Code"; ReqLine."Bin Code" := ProdOrderLine."Bin Code"; @@ -208,6 +207,7 @@ report 20505 "Subc. Calculate Subcontracts" ReqLine."Description 2" := ProdOrderRoutingLine."Description 2"; SetVendorItemNo(); OnAfterTransferProdOrderRoutingLine(ReqLine, ProdOrderRoutingLine); + ReqLine.Validate("Due Date", ProdOrderRoutingLine."Ending Date"); // If purchase order already exist we will change this if possible PurchLine.Reset(); PurchLine.SetCurrentKey("Prod. Order No.", "Prod. Order Line No.", "Routing No.", "Operation No."); diff --git a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al index d240a0167bb..26d2ba53bea 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al @@ -504,6 +504,16 @@ codeunit 20508 "Subc. Price Management" end; procedure GetSubcPriceForPurchLine(var PurchaseLine: Record "Purchase Line") + begin + ApplySubcPriceForPurchLine(PurchaseLine, true); + end; + + internal procedure RepriceSubcPurchLineForDateChange(var PurchaseLine: Record "Purchase Line") + begin + ApplySubcPriceForPurchLine(PurchaseLine, false); + end; + + local procedure ApplySubcPriceForPurchLine(var PurchaseLine: Record "Purchase Line"; UseRoutingCostFallback: Boolean) var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; SubcontractorPrice: Record "Subcontractor Price"; @@ -544,6 +554,8 @@ codeunit 20508 "Subc. Price Management" ConvertPriceToCurrency(PurchaseLine."Currency Code", SubcontractorPrice."Currency Code", PriceListCost, DirectCost) end; end else begin + if not UseRoutingCostFallback then + exit; GetUOMPrice(PurchaseLine."No.", PurchaseLine.GetQuantityBase(), SubcontractorPrice, PriceListUOM, PriceListQtyPerUOM, PriceListQty); ProdOrderRoutingLine.TestField(Type, "Capacity Type"::"Work Center"); DirectCost := ProdOrderRoutingLine."Direct Unit Cost"; diff --git a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPurchaseLineExt.Codeunit.al b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPurchaseLineExt.Codeunit.al index 8662e2be2af..b8907620e0c 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPurchaseLineExt.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPurchaseLineExt.Codeunit.al @@ -100,10 +100,10 @@ codeunit 20534 "Subc. Purchase Line Ext" if GetExecutionContext() = ExecutionContext::Upgrade then exit; - if Rec."Planned Receipt Date" = xRec."Planned Receipt Date" then + if Rec."Order Date" = xRec."Order Date" then exit; - GetSubcontractingPrice(Rec); + RepriceSubcontractingLineAfterDateChange(Rec); end; [EventSubscriber(ObjectType::Table, Database::"Purchase Line", OnAfterValidateEvent, "Order Date", false, false)] @@ -124,7 +124,7 @@ codeunit 20534 "Subc. Purchase Line Ext" if Rec."Order Date" = xRec."Order Date" then exit; - GetSubcontractingPrice(Rec); + RepriceSubcontractingLineAfterDateChange(Rec); end; [EventSubscriber(ObjectType::Table, Database::"Purchase Line", OnAfterValidateEvent, Quantity, false, false)] @@ -350,10 +350,34 @@ codeunit 20534 "Subc. Purchase Line Ext" var SubcPriceManagement: Codeunit "Subc. Price Management"; begin - if (PurchaseLine.Type = PurchaseLine.Type::Item) and (PurchaseLine."No." <> '') and (PurchaseLine."Prod. Order No." <> '') and (PurchaseLine."Operation No." <> '') then + if IsSubcontractingPurchaseLine(PurchaseLine) then SubcPriceManagement.GetSubcPriceForPurchLine(PurchaseLine); end; + local procedure RepriceSubcontractingLineAfterDateChange(var PurchaseLine: Record "Purchase Line") + var + PurchaseHeader: Record "Purchase Header"; + SubcPriceManagement: Codeunit "Subc. Price Management"; + begin + if not IsSubcontractingPurchaseLine(PurchaseLine) then + exit; + + PurchaseHeader := PurchaseLine.GetPurchHeader(); + if PurchaseHeader.Status <> PurchaseHeader.Status::Open then + exit; + + SubcPriceManagement.RepriceSubcPurchLineForDateChange(PurchaseLine); + end; + + local procedure IsSubcontractingPurchaseLine(PurchaseLine: Record "Purchase Line"): Boolean + begin + exit( + (PurchaseLine.Type = PurchaseLine.Type::Item) and + (PurchaseLine."No." <> '') and + (PurchaseLine."Prod. Order No." <> '') and + (PurchaseLine."Operation No." <> '')); + end; + [EventSubscriber(ObjectType::Table, Database::"Purchase Line", OnBeforeOpenItemTrackingLines, '', false, false)] local procedure OpenProdOrderLineItemTrackingOnBeforeOpenItemTrackingLines(PurchaseLine: Record "Purchase Line"; var IsHandled: Boolean) begin diff --git a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPurchaseOrderCreator.Codeunit.al b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPurchaseOrderCreator.Codeunit.al index c5354da902d..251e575e489 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPurchaseOrderCreator.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPurchaseOrderCreator.Codeunit.al @@ -701,7 +701,6 @@ codeunit 20557 "Subc. Purchase Order Creator" RequisitionLine."Qty. Rounding Precision (Base)" := ProdOrderLine."Qty. Rounding Precision (Base)"; RequisitionLine."Prod. Order No." := ProdOrderLine."Prod. Order No."; RequisitionLine."Prod. Order Line No." := ProdOrderLine."Line No."; - RequisitionLine."Due Date" := ProdOrderRoutingLine."Ending Date"; RequisitionLine."Requester ID" := CopyStr(UserId(), 1, MaxStrLen(RequisitionLine."Requester ID")); RequisitionLine."Location Code" := ProdOrderLine."Location Code"; @@ -718,6 +717,7 @@ codeunit 20557 "Subc. Purchase Order Creator" RequisitionLine.Description := ProdOrderRoutingLine.Description; RequisitionLine."Description 2" := ProdOrderRoutingLine."Description 2"; RequisitionLine.Validate("Subc. Standard Task Code", ProdOrderRoutingLine."Standard Task Code"); + RequisitionLine.Validate("Due Date", ProdOrderRoutingLine."Ending Date"); SetVendorItemNo(RequisitionLine); if PurchLineExists(PurchaseLine, ProdOrderLine, ProdOrderRoutingLine) then begin diff --git a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al index 5d3037e20c9..682185bed8f 100644 --- a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al @@ -126,6 +126,8 @@ codeunit 139982 "Subc. Pricing Test" CreateDateEffectiveSubcontractingScenario(Item, ProductionOrder, ProdOrderRoutingLine, EarlierPrice, LaterPrice); SubcontractingMgmtLibrary.CreateReqWkshTemplateAndName(ReqWkshTemplate, RequisitionWkshName); SubcontractingMgmtLibrary.CalculateSubcontractsAndFindReqLine(RequisitionWkshName, ProductionOrder."No.", RequisitionLine); + Assert.IsTrue(RequisitionLine."Order Date" < WorkDate(), 'The worksheet line order date must be scheduled before WorkDate.'); + Assert.AreEqual(EarlierPrice, RequisitionLine."Direct Unit Cost", 'The worksheet line must use the price valid on its scheduled order date.'); // [WHEN] The worksheet action is carried out for the subcontracting operation SubcontractingMgmtLibrary.CarryOutSubcontractingAction(RequisitionLine); @@ -136,6 +138,39 @@ codeunit 139982 "Subc. Pricing Test" Assert.AreEqual(EarlierPrice, PurchaseLine."Direct Unit Cost", 'The purchase line must use the subcontractor price valid on its order date.'); end; + [Test] + procedure ReqWkshCreatedSubcPurchLinePreservesManualCost() + var + Item: Record Item; + ProductionOrder: Record "Production Order"; + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + PurchaseLine: Record "Purchase Line"; + ReqWkshTemplate: Record "Req. Wksh. Template"; + RequisitionLine: Record "Requisition Line"; + RequisitionWkshName: Record "Requisition Wksh. Name"; + EarlierPrice: Decimal; + LaterPrice: Decimal; + ManualCost: Decimal; + begin + // [SCENARIO 648535] Carrying out a subcontracting worksheet preserves a manually entered cost + Initialize(); + + // [GIVEN] A backward-scheduled subcontracting worksheet line with a manually changed cost + CreateDateEffectiveSubcontractingScenario(Item, ProductionOrder, ProdOrderRoutingLine, EarlierPrice, LaterPrice); + SubcontractingMgmtLibrary.CreateReqWkshTemplateAndName(ReqWkshTemplate, RequisitionWkshName); + SubcontractingMgmtLibrary.CalculateSubcontractsAndFindReqLine(RequisitionWkshName, ProductionOrder."No.", RequisitionLine); + ManualCost := EarlierPrice + LibraryRandom.RandDecInRange(10, 100, 2); + RequisitionLine.Validate("Direct Unit Cost", ManualCost); + RequisitionLine.Modify(true); + + // [WHEN] The worksheet action is carried out + SubcontractingMgmtLibrary.CarryOutSubcontractingAction(RequisitionLine); + + // [THEN] The purchase line keeps the manually entered worksheet cost + SubcontractingMgmtLibrary.FindSubcPurchLineForProdOrder(PurchaseLine, Item."No.", ProductionOrder."No."); + Assert.AreEqual(ManualCost, PurchaseLine."Direct Unit Cost", 'The purchase line must preserve the manually entered worksheet cost.'); + end; + [Test] procedure ExpectedReceiptDateChangeRepricesBackwardScheduledSubcPurchLine() var @@ -223,6 +258,99 @@ codeunit 139982 "Subc. Pricing Test" Assert.AreEqual(LaterPrice, PurchaseLine."Direct Unit Cost", 'The purchase line must use the subcontractor price valid on its changed order date.'); end; + [Test] + procedure LeadTimeChangeRepricesSubcPurchLineWhenOrderDateChanges() + var + Item: Record Item; + ProductionOrder: Record "Production Order"; + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + PurchaseLine: Record "Purchase Line"; + NewLeadTimeCalculation: DateFormula; + EarlierPrice: Decimal; + LaterPrice: Decimal; + begin + // [SCENARIO 648535] Lead-time-only rescheduling reapplies the price when the resulting order date changes + Initialize(); + + // [GIVEN] A backward-scheduled subcontracting purchase line with a requested receipt date + CreateDateEffectiveSubcontractingScenario(Item, ProductionOrder, ProdOrderRoutingLine, EarlierPrice, LaterPrice); + CreateSubcontractingPurchaseLine(PurchaseLine, ProdOrderRoutingLine, Item."No.", ProductionOrder."No."); + PurchaseLine.TestField("Requested Receipt Date"); + Assert.AreEqual(EarlierPrice, PurchaseLine."Direct Unit Cost", 'The purchase line must initially use the earlier subcontractor price.'); + + // [WHEN] Lead Time Calculation is cleared without changing Planned Receipt Date + Clear(NewLeadTimeCalculation); + PurchaseLine.Validate("Lead Time Calculation", NewLeadTimeCalculation); + + // [THEN] The changed Order Date selects the later date-effective price + Assert.IsTrue(PurchaseLine."Order Date" >= WorkDate(), 'The recalculated purchase line order date must be on or after WorkDate.'); + Assert.AreEqual(LaterPrice, PurchaseLine."Direct Unit Cost", 'The purchase line must use the price valid on the recalculated order date.'); + end; + + [Test] + procedure DateChangeWithoutMatchingPricePreservesSubcPurchLineCost() + var + Item: Record Item; + ProductionOrder: Record "Production Order"; + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + PurchaseLine: Record "Purchase Line"; + SubcontractorPrice: Record "Subcontractor Price"; + EarlierPrice: Decimal; + LaterPrice: Decimal; + OriginalDirectUnitCost: Decimal; + begin + // [SCENARIO 648535] Rescheduling without a matching subcontractor price preserves the calculated purchase-line cost + Initialize(); + + // [GIVEN] A subcontracting purchase line whose subcontractor prices are no longer applicable + CreateDateEffectiveSubcontractingScenario(Item, ProductionOrder, ProdOrderRoutingLine, EarlierPrice, LaterPrice); + CreateSubcontractingPurchaseLine(PurchaseLine, ProdOrderRoutingLine, Item."No.", ProductionOrder."No."); + OriginalDirectUnitCost := PurchaseLine."Direct Unit Cost"; + SubcontractorPrice.SetRange("Item No.", Item."No."); + SubcontractorPrice.DeleteAll(true); + + // [WHEN] Planned Receipt Date is changed + PurchaseLine.Validate("Planned Receipt Date", CalcDate('<20D>', WorkDate())); + + // [THEN] The existing calculated cost is preserved + Assert.AreEqual(OriginalDirectUnitCost, PurchaseLine."Direct Unit Cost", 'Rescheduling without a matching price must preserve the existing cost.'); + end; + + [Test] + procedure ReleasedSubcPurchLineDateChangesDoNotReprice() + var + Item: Record Item; + ProductionOrder: Record "Production Order"; + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + NewOrderDate: Date; + NewPlannedReceiptDate: Date; + EarlierPrice: Decimal; + LaterPrice: Decimal; + ReleasedDirectUnitCost: Decimal; + begin + // [SCENARIO 648535] Scheduling dates remain editable on a released subcontracting order without changing financial terms + Initialize(); + + // [GIVEN] A released subcontracting purchase order using the earlier date-effective price + CreateDateEffectiveSubcontractingScenario(Item, ProductionOrder, ProdOrderRoutingLine, EarlierPrice, LaterPrice); + CreateSubcontractingPurchaseLine(PurchaseLine, ProdOrderRoutingLine, Item."No.", ProductionOrder."No."); + PurchaseHeader.Get(PurchaseLine."Document Type", PurchaseLine."Document No."); + LibraryPurchase.ReleasePurchaseDocument(PurchaseHeader); + ReleasedDirectUnitCost := PurchaseLine."Direct Unit Cost"; + NewPlannedReceiptDate := CalcDate('<20D>', WorkDate()); + NewOrderDate := CalcDate('<10D>', WorkDate()); + + // [WHEN] Planned Receipt Date and Order Date are changed + PurchaseLine.Validate("Planned Receipt Date", NewPlannedReceiptDate); + PurchaseLine.Validate("Order Date", NewOrderDate); + + // [THEN] The dates change without repricing the released line + Assert.AreEqual(NewOrderDate, PurchaseLine."Order Date", 'The released purchase line order date must remain editable.'); + Assert.AreEqual(ReleasedDirectUnitCost, PurchaseLine."Direct Unit Cost", 'Scheduling a released purchase line must not change its direct unit cost.'); + end; + [Test] [Scope('OnPrem')] procedure DeleteWorkCenterWithPricesDeletesRelatedPrices()