From c6acb74ab120e9415ce860a5b15fba9f2abeb06b Mon Sep 17 00:00:00 2001 From: chethant Date: Wed, 9 Sep 2026 22:04:03 +0200 Subject: [PATCH 01/10] Fix subcontracting repricing regressions Preserve manual and calculated worksheet costs, retain released-order scheduling, and reprice lead-time-only date changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 255f96a1-3a67-418c-9b69-ca50ae31e5b0 --- .../SubcReqWkshMakeOrd.Codeunit.al | 8 + .../Purchase/SubcPriceManagement.Codeunit.al | 101 +++++++-- .../Purchase/SubcPurchaseLineExt.Codeunit.al | 34 ++- .../Test/Tests/SubcPricingTest.Codeunit.al | 205 ++++++++++++++++++ 4 files changed, 323 insertions(+), 25 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..2de76fcf90b 100644 --- a/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al @@ -33,6 +33,7 @@ codeunit 20516 "Subc. Req. Wksh. Make Ord." local procedure OnInsertPurchOrderLineOnAfterTransferFromReqLineToPurchLine(var PurchOrderLine: Record "Purchase Line"; RequisitionLine: Record "Requisition Line") var SubcPriceManagement: Codeunit "Subc. Price Management"; + ReqLineDatePriceListCost: Decimal; begin #if not CLEAN29 #pragma warning disable AL0432 @@ -43,6 +44,13 @@ codeunit 20516 "Subc. Req. Wksh. Make Ord." if (RequisitionLine."Prod. Order No." = '') or (RequisitionLine."Operation No." = '') then exit; + if not SubcPriceManagement.TryGetSubcPriceListCostForPurchLine( + PurchOrderLine, RequisitionLine."Order Date", ReqLineDatePriceListCost) + then + exit; + if PurchOrderLine."Direct Unit Cost" <> ReqLineDatePriceListCost then + exit; + SubcPriceManagement.GetSubcPriceForPurchLine(PurchOrderLine); end; 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..27b2aa7fb24 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al @@ -506,10 +506,8 @@ codeunit 20508 "Subc. Price Management" procedure GetSubcPriceForPurchLine(var PurchaseLine: Record "Purchase Line") var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; - SubcontractorPrice: Record "Subcontractor Price"; - PriceListUOM: Code[10]; OrderDate: Date; - DirectCost, PriceListCost, PriceListQty, PriceListQtyPerUOM : Decimal; + DirectCost: Decimal; begin #if not CLEAN29 #pragma warning disable AL0432 @@ -521,36 +519,85 @@ codeunit 20508 "Subc. Price Management" if OrderDate = 0D then OrderDate := WorkDate(); + if not TryGetSubcPriceListCostForPurchLine(PurchaseLine, OrderDate, DirectCost) then begin + GetProdOrderRtngLine( + PurchaseLine."Prod. Order No.", PurchaseLine."Routing Reference No.", + PurchaseLine."Routing No.", PurchaseLine."Operation No.", ProdOrderRoutingLine); + ProdOrderRoutingLine.TestField(Type, "Capacity Type"::"Work Center"); + DirectCost := GetNonPriceListDirectCost(ProdOrderRoutingLine); + end; + + PurchaseLine."Direct Unit Cost" := DirectCost; + PurchaseLine.Validate("Line Discount %"); + end; + + internal procedure TryGetSubcPriceListCostForPurchLine(PurchaseLine: Record "Purchase Line"; OrderDate: Date; var DirectCost: Decimal): Boolean + var + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + SubcontractorPrice: Record "Subcontractor Price"; + PriceListUOM: Code[10]; + PriceListCost, PriceListQty, PriceListQtyPerUOM : Decimal; + begin + DirectCost := 0; +#if not CLEAN29 +#pragma warning disable AL0432 + if not SubcFeatureFlagHandler.IsSubcontractingEnabled() then +#pragma warning restore AL0432 + exit(false); +#endif + if OrderDate = 0D then + OrderDate := WorkDate(); + + if not TryFindProdOrderRtngLine( + PurchaseLine."Prod. Order No.", PurchaseLine."Routing Reference No.", + PurchaseLine."Routing No.", PurchaseLine."Operation No.", ProdOrderRoutingLine) + then + exit(false); + SubcontractorPrice.SetRange("Vendor No.", PurchaseLine."Buy-from Vendor No."); SubcontractorPrice.SetRange("Work Center No.", PurchaseLine."Work Center No."); SubcontractorPrice.SetRange("Item No.", PurchaseLine."No."); SubcontractorPrice.SetFilter("Variant Code", '%1|%2', PurchaseLine."Variant Code", ''); SubcontractorPrice.SetFilter("Unit of Measure Code", '%1|%2', PurchaseLine."Unit of Measure Code", ''); - - GetProdOrderRtngLine(PurchaseLine."Prod. Order No.", PurchaseLine."Routing Reference No.", PurchaseLine."Routing No.", PurchaseLine."Operation No.", ProdOrderRoutingLine); - SubcontractorPrice.SetFilter("Standard Task Code", '%1|%2', ProdOrderRoutingLine."Standard Task Code", ''); SubcontractorPrice.SetFilter("Currency Code", '%1|%2', PurchaseLine."Currency Code", ''); SubcontractorPrice.SetRange("Starting Date", 0D, OrderDate); SubcontractorPrice.SetFilter("Ending Date", '>=%1|%2', OrderDate, 0D); - if SubcontractorPrice.FindLast() then begin - if SubcontractorPrice."Unit of Measure Code" = PurchaseLine."Unit of Measure Code" then - PriceListUOM := SubcontractorPrice."Unit of Measure Code"; - GetUOMPrice(PurchaseLine."No.", GetQuantityBase(PurchaseLine), SubcontractorPrice, PriceListUOM, PriceListQtyPerUOM, PriceListQty); - GetPriceByUOM(SubcontractorPrice, PriceListQty, PriceListCost); - if PriceListCost <> 0 then begin - ConvertPriceToUOM(PurchaseLine."Unit of Measure Code", PurchaseLine.GetQuantityPerUOM(), PriceListUOM, PriceListQtyPerUOM, PriceListCost, DirectCost); - ConvertPriceToCurrency(PurchaseLine."Currency Code", SubcontractorPrice."Currency Code", PriceListCost, DirectCost) - end; - end else begin - GetUOMPrice(PurchaseLine."No.", PurchaseLine.GetQuantityBase(), SubcontractorPrice, PriceListUOM, PriceListQtyPerUOM, PriceListQty); - ProdOrderRoutingLine.TestField(Type, "Capacity Type"::"Work Center"); - DirectCost := ProdOrderRoutingLine."Direct Unit Cost"; - end; + if not SubcontractorPrice.FindLast() then + exit(false); - PurchaseLine."Direct Unit Cost" := DirectCost; - PurchaseLine.Validate("Line Discount %"); + if SubcontractorPrice."Unit of Measure Code" = PurchaseLine."Unit of Measure Code" then + PriceListUOM := SubcontractorPrice."Unit of Measure Code"; + GetUOMPrice(PurchaseLine."No.", GetQuantityBase(PurchaseLine), SubcontractorPrice, PriceListUOM, PriceListQtyPerUOM, PriceListQty); + GetPriceByUOM(SubcontractorPrice, PriceListQty, PriceListCost); + if PriceListCost = 0 then + exit(true); + + ConvertPriceToUOM(PurchaseLine."Unit of Measure Code", PurchaseLine.GetQuantityPerUOM(), PriceListUOM, PriceListQtyPerUOM, PriceListCost, DirectCost); + ConvertPriceToCurrency(PurchaseLine."Currency Code", SubcontractorPrice."Currency Code", PriceListCost, DirectCost); + exit(true); + end; + + local procedure GetNonPriceListDirectCost(ProdOrderRoutingLine: Record "Prod. Order Routing Line"): Decimal + var + GeneralLedgerSetup: Record "General Ledger Setup"; + ProdOrderLine: Record "Prod. Order Line"; + begin + GetLine(ProdOrderLine, ProdOrderRoutingLine); + GeneralLedgerSetup.Get(); + if ProdOrderRoutingLine."Unit Cost Calculation" = ProdOrderRoutingLine."Unit Cost Calculation"::Units then + exit( + Round( + ProdOrderRoutingLine."Direct Unit Cost" * ProdOrderLine."Qty. per Unit of Measure", + GeneralLedgerSetup."Unit-Amount Rounding Precision")); + + ProdOrderLine.CalcFields("Total Exp. Oper. Output (Qty.)"); + exit( + Round( + (ProdOrderRoutingLine."Expected Operation Cost Amt." - ProdOrderRoutingLine."Expected Capacity Ovhd. Cost") / + ProdOrderLine."Total Exp. Oper. Output (Qty.)", + GeneralLedgerSetup."Unit-Amount Rounding Precision")); end; local procedure GetProdOrderRtngLine(ProdOrderNo: Code[20]; RtngRefNo: Integer; RoutingNo: Code[20]; OperationNo: Code[10]; var ProdOrderRoutingLine: Record "Prod. Order Routing Line") @@ -564,6 +611,16 @@ codeunit 20508 "Subc. Price Management" ProdOrderRoutingLine.FindFirst(); end; + local procedure TryFindProdOrderRtngLine(ProdOrderNo: Code[20]; RtngRefNo: Integer; RoutingNo: Code[20]; OperationNo: Code[10]; var ProdOrderRoutingLine: Record "Prod. Order Routing Line"): Boolean + begin + ProdOrderRoutingLine.SetFilter(Status, '%1|%2', ProdOrderRoutingLine.Status::Released, ProdOrderRoutingLine.Status::Finished); + ProdOrderRoutingLine.SetRange("Prod. Order No.", ProdOrderNo); + ProdOrderRoutingLine.SetRange("Routing Reference No.", RtngRefNo); + ProdOrderRoutingLine.SetRange("Routing No.", RoutingNo); + ProdOrderRoutingLine.SetRange("Operation No.", OperationNo); + exit(ProdOrderRoutingLine.FindFirst()); + end; + local procedure SetSubcontractorPriceForPriceCalculation(var SubcontractorPrice: Record "Subcontractor Price"; VendorNo: Code[20]; ItemNo: Code[20]; VariantCode: Code[10]; StandardTaskCode: Code[10]; WorkCenterNo: Code[20]; UoM: Code[10]; StartingDate: Date) var Vendor: Record Vendor; 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..34bd30ee862 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,14 @@ codeunit 20534 "Subc. Purchase Line Ext" if GetExecutionContext() = ExecutionContext::Upgrade then exit; - if Rec."Planned Receipt Date" = xRec."Planned Receipt Date" then + // Gate on the resulting Order Date rather than Planned Receipt Date so lead-time-only + // reschedules (for example changing Lead Time Calculation on an open line with nonblank + // Requested and Planned Receipt Dates) still trigger date-effective repricing when the + // planned-date validation reassigns Order Date without changing Planned Receipt Date. + if Rec."Order Date" = xRec."Order Date" then exit; - GetSubcontractingPrice(Rec); + RepriceSubcPurchLineOnScheduleChange(Rec); end; [EventSubscriber(ObjectType::Table, Database::"Purchase Line", OnAfterValidateEvent, "Order Date", false, false)] @@ -124,7 +128,7 @@ codeunit 20534 "Subc. Purchase Line Ext" if Rec."Order Date" = xRec."Order Date" then exit; - GetSubcontractingPrice(Rec); + RepriceSubcPurchLineOnScheduleChange(Rec); end; [EventSubscriber(ObjectType::Table, Database::"Purchase Line", OnAfterValidateEvent, Quantity, false, false)] @@ -354,6 +358,30 @@ codeunit 20534 "Subc. Purchase Line Ext" SubcPriceManagement.GetSubcPriceForPurchLine(PurchaseLine); end; + local procedure RepriceSubcPurchLineOnScheduleChange(var PurchaseLine: Record "Purchase Line") + var + PurchaseHeader: Record "Purchase Header"; + begin + if PurchaseLine."Prod. Order No." = '' then + exit; + + // Preserve released-order scheduling: repricing a subcontracting line after release + // would call Validate("Line Discount %") through GetSubcPriceForPurchLine, which in + // turn calls TestStatusOpen on the released header and fails the date edit. The base + // test suite explicitly permits Planned Receipt Date and Order Date edits on released + // purchase order lines (see ERMSalesPurchStatusError CanChangeOrderDateOnReleasedPurchOrderLine + // and CanChangePlannedReceiptDateOnReleasedPurchOrderLine). Bypass repricing entirely + // once the header is no longer Open so scheduling still works without silently + // changing released financial terms. + PurchaseHeader.SetLoadFields(Status); + if not PurchaseHeader.Get(PurchaseLine."Document Type", PurchaseLine."Document No.") then + exit; + if PurchaseHeader.Status <> PurchaseHeader.Status::Open then + exit; + + GetSubcontractingPrice(PurchaseLine); + 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/Test/Tests/SubcPricingTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al index 5d3037e20c9..fc19a90bef9 100644 --- a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al @@ -223,6 +223,211 @@ 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 NoMatchPriceListPreservesCalculatedWorksheetCostForTimeBasedRouting() + 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"; + RoutingHeader: Record "Routing Header"; + RoutingLine: Record "Routing Line"; + SubcontractorPrice: Record "Subcontractor Price"; + Vendor: Record Vendor; + WorkCenter: Record "Work Center"; + CalculatedWorksheetCost: Decimal; + RoutingRate: Decimal; + begin + // [SCENARIO 648535] Carry-out preserves the calculated worksheet Direct Unit Cost when + // no subcontractor price covers the purchase-line date and the routing has a nontrivial + // per-output-unit multiplier, instead of overwriting with the raw routing rate. + Initialize(); + + // [GIVEN] A subcontracting item whose routing operation has a multi-minute Run Time + CreateSubcontractingItemWithSingleOperationRouting(Item, Vendor, WorkCenter, ''); + RoutingHeader.Get(Item."Routing No."); + RoutingHeader.Validate(Status, RoutingHeader.Status::New); + RoutingHeader.Modify(true); + RoutingLine.SetRange("Routing No.", Item."Routing No."); + RoutingLine.FindFirst(); + RoutingLine.Validate("Run Time", 5); + RoutingLine.Modify(true); + RoutingHeader.Validate(Status, RoutingHeader.Status::Certified); + RoutingHeader.Modify(true); + + // [GIVEN] No subcontractor price entries exist for the operation + SubcontractorPrice.SetRange("Vendor No.", Vendor."No."); + SubcontractorPrice.SetRange("Work Center No.", WorkCenter."No."); + SubcontractorPrice.SetRange("Item No.", Item."No."); + Assert.IsTrue(SubcontractorPrice.IsEmpty(), 'Test setup expects no subcontractor prices for the operation.'); + + // [GIVEN] A released production order whose worksheet line reflects the time-based cost + LibraryManufacturing.CreateProductionOrder( + ProductionOrder, "Production Order Status"::Released, Item, '', '', 1, WorkDate()); + ProdOrderRoutingLine.SetRange(Status, ProductionOrder.Status); + ProdOrderRoutingLine.SetRange("Prod. Order No.", ProductionOrder."No."); + ProdOrderRoutingLine.SetRange("Work Center No.", WorkCenter."No."); + ProdOrderRoutingLine.FindFirst(); + RoutingRate := ProdOrderRoutingLine."Direct Unit Cost"; + + SubcontractingMgmtLibrary.CreateReqWkshTemplateAndName(ReqWkshTemplate, RequisitionWkshName); + SubcontractingMgmtLibrary.CalculateSubcontractsAndFindReqLine(RequisitionWkshName, ProductionOrder."No.", RequisitionLine); + CalculatedWorksheetCost := RequisitionLine."Direct Unit Cost"; + Assert.AreNotEqual(RoutingRate, CalculatedWorksheetCost, 'Test setup expects the calculated worksheet cost to include the routing multiplier.'); + + // [WHEN] The worksheet action is carried out + SubcontractingMgmtLibrary.CarryOutSubcontractingAction(RequisitionLine); + + // [THEN] The purchase line preserves the calculated worksheet cost instead of falling + // back to the raw routing rate + SubcontractingMgmtLibrary.FindSubcPurchLineForProdOrder(PurchaseLine, Item."No.", ProductionOrder."No."); + Assert.AreEqual(CalculatedWorksheetCost, PurchaseLine."Direct Unit Cost", + 'Carry-out must preserve the worksheet Direct Unit Cost when no subcontractor price matches the purchase-line date.'); + Assert.AreNotEqual(RoutingRate, PurchaseLine."Direct Unit Cost", + 'The purchase line must not fall back to the raw routing Direct Unit Cost when a proper worksheet cost was calculated.'); + end; + + [Test] + procedure ManualWorksheetDirectUnitCostOverridePreservedOnCarryOut() + 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] Carry-out preserves a manually overridden worksheet Direct Unit + // Cost even when a subcontractor price valid on the purchase-line date exists, so + // deliberate worksheet values are not silently replaced by the automatic lookup. + Initialize(); + + // [GIVEN] A subcontracting operation with adjacent price-list entries and a worksheet + // line whose Direct Unit Cost has been manually overridden to a distinct value + CreateDateEffectiveSubcontractingScenario(Item, ProductionOrder, ProdOrderRoutingLine, EarlierPrice, LaterPrice); + SubcontractingMgmtLibrary.CreateReqWkshTemplateAndName(ReqWkshTemplate, RequisitionWkshName); + SubcontractingMgmtLibrary.CalculateSubcontractsAndFindReqLine(RequisitionWkshName, ProductionOrder."No.", RequisitionLine); + ManualCost := EarlierPrice + LaterPrice; + Assert.AreNotEqual(RequisitionLine."Direct Unit Cost", ManualCost, 'Test setup expects the manual override to differ from the automatic price.'); + RequisitionLine.Validate("Direct Unit Cost", ManualCost); + RequisitionLine.Modify(true); + + // [WHEN] The worksheet action is carried out + SubcontractingMgmtLibrary.CarryOutSubcontractingAction(RequisitionLine); + + // [THEN] The purchase line preserves the manually overridden cost + SubcontractingMgmtLibrary.FindSubcPurchLineForProdOrder(PurchaseLine, Item."No.", ProductionOrder."No."); + Assert.AreEqual(ManualCost, PurchaseLine."Direct Unit Cost", + 'Carry-out must preserve a manually overridden worksheet Direct Unit Cost.'); + end; + + [Test] + procedure DateEditsOnReleasedSubcPurchOrderLinePreserveFinancialTerms() + var + Item: Record Item; + ProductionOrder: Record "Production Order"; + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + EarlierPrice: Decimal; + LaterPrice: Decimal; + NewPlannedReceiptDate: Date; + OriginalDirectUnitCost: Decimal; + OriginalLineDiscount: Decimal; + begin + // [SCENARIO 648535] Editing Planned Receipt Date and Order Date on a released + // subcontracting purchase order line must succeed without a status-open error and must + // not silently change financial terms, matching base test coverage in + // ERMSalesPurchStatusError CanChangePlannedReceiptDateOnReleasedPurchOrderLine and + // CanChangeOrderDateOnReleasedPurchOrderLine. + Initialize(); + + // [GIVEN] A backward-scheduled subcontracting purchase line then released to the vendor + CreateDateEffectiveSubcontractingScenario(Item, ProductionOrder, ProdOrderRoutingLine, EarlierPrice, LaterPrice); + CreateSubcontractingPurchaseLine(PurchaseLine, ProdOrderRoutingLine, Item."No.", ProductionOrder."No."); + OriginalDirectUnitCost := PurchaseLine."Direct Unit Cost"; + OriginalLineDiscount := PurchaseLine."Line Discount %"; + PurchaseHeader.Get(PurchaseLine."Document Type", PurchaseLine."Document No."); + LibraryPurchase.ReleasePurchaseDocument(PurchaseHeader); + PurchaseLine.Find(); + + // [WHEN] Planned Receipt Date is edited on the released line to move Order Date into + // the later price period + NewPlannedReceiptDate := CalcDate('<20D>', WorkDate()); + PurchaseLine.Validate("Planned Receipt Date", NewPlannedReceiptDate); + + // [THEN] The edit is accepted and the released financial terms are preserved + PurchaseLine.TestField("Planned Receipt Date", NewPlannedReceiptDate); + Assert.AreEqual(OriginalDirectUnitCost, PurchaseLine."Direct Unit Cost", + 'Direct Unit Cost must not change when Planned Receipt Date is edited on a released subcontracting line.'); + Assert.AreEqual(OriginalLineDiscount, PurchaseLine."Line Discount %", + 'Line Discount % must not change when Planned Receipt Date is edited on a released subcontracting line.'); + + // [WHEN] Order Date is edited directly on the released line + PurchaseLine.Validate("Order Date", WorkDate()); + + // [THEN] The edit is accepted and financial terms remain preserved + PurchaseLine.TestField("Order Date", WorkDate()); + Assert.AreEqual(OriginalDirectUnitCost, PurchaseLine."Direct Unit Cost", + 'Direct Unit Cost must not change when Order Date is edited on a released subcontracting line.'); + Assert.AreEqual(OriginalLineDiscount, PurchaseLine."Line Discount %", + 'Line Discount % must not change when Order Date is edited on a released subcontracting line.'); + end; + + [Test] + procedure LeadTimeCalculationOnlyOrderDateShiftRepricesSubcPurchLine() + var + Item: Record Item; + ProductionOrder: Record "Production Order"; + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + PurchaseLine: Record "Purchase Line"; + EarlierPrice: Decimal; + LaterPrice: Decimal; + AlignedDate: Date; + NewLeadTime: DateFormula; + OriginalPlannedReceiptDate: Date; + begin + // [SCENARIO 648535] A rescheduling that keeps Planned Receipt Date unchanged but + // reassigns Order Date across a price boundary — for example changing Lead Time + // Calculation on an open line with nonblank Requested and Planned Receipt Dates and + // blank Promised Receipt Date — must reprice the subcontracting purchase line. + Initialize(); + + // [GIVEN] A backward-scheduled subcontracting purchase line initially using the + // earlier price + CreateDateEffectiveSubcontractingScenario(Item, ProductionOrder, ProdOrderRoutingLine, EarlierPrice, LaterPrice); + CreateSubcontractingPurchaseLine(PurchaseLine, ProdOrderRoutingLine, Item."No.", ProductionOrder."No."); + Assert.AreEqual(EarlierPrice, PurchaseLine."Direct Unit Cost", 'Initial purchase-line Direct Unit Cost must match the earlier subcontractor price.'); + + // [GIVEN] Requested Receipt Date is populated and Promised Receipt Date blank so that + // the Lead Time Calculation change validates Planned Receipt Date without changing it + // and reassigns Order Date directly, and Planned Receipt Date sits in the later price + // period so a zero-day lead time places Order Date there. + AlignedDate := CalcDate('<20D>', WorkDate()); + PurchaseLine.Validate("Requested Receipt Date", AlignedDate); + PurchaseLine.Validate("Promised Receipt Date", 0D); + OriginalPlannedReceiptDate := PurchaseLine."Planned Receipt Date"; + Assert.IsTrue(OriginalPlannedReceiptDate >= WorkDate(), 'Planned Receipt Date must reach the later price period for this test scenario.'); + + // [WHEN] Lead Time Calculation is set to zero so Order Date shifts to Planned Receipt + // Date without changing that field + Evaluate(NewLeadTime, '<0D>'); + PurchaseLine.Validate("Lead Time Calculation", NewLeadTime); + + // [THEN] Planned Receipt Date is unchanged, Order Date crossed the boundary and the + // line reprices from the resulting Order Date + Assert.AreEqual(OriginalPlannedReceiptDate, PurchaseLine."Planned Receipt Date", 'Planned Receipt Date must remain unchanged for a lead-time-only reschedule.'); + Assert.IsTrue(PurchaseLine."Order Date" >= WorkDate(), 'Order Date must reach the later price period after the lead-time-only reschedule.'); + Assert.AreEqual(LaterPrice, PurchaseLine."Direct Unit Cost", 'The purchase line must reprice to the subcontractor price valid on the resulting Order Date after a lead-time-only reschedule.'); + end; + [Test] [Scope('OnPrem')] procedure DeleteWorkCenterWithPricesDeletesRelatedPrices() From 1da8f0fbaafaab660be68fecfd280f2e87dc4d4c Mon Sep 17 00:00:00 2001 From: chethant Date: Thu, 10 Sep 2026 12:05:36 +0200 Subject: [PATCH 02/10] Fix subcontracting test variable order Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 255f96a1-3a67-418c-9b69-ca50ae31e5b0 --- .../W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al index fc19a90bef9..b77882e48c7 100644 --- a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al @@ -388,10 +388,10 @@ codeunit 139982 "Subc. Pricing Test" ProductionOrder: Record "Production Order"; ProdOrderRoutingLine: Record "Prod. Order Routing Line"; PurchaseLine: Record "Purchase Line"; + NewLeadTime: DateFormula; EarlierPrice: Decimal; LaterPrice: Decimal; AlignedDate: Date; - NewLeadTime: DateFormula; OriginalPlannedReceiptDate: Date; begin // [SCENARIO 648535] A rescheduling that keeps Planned Receipt Date unchanged but From 288ca7d660832c774e109fd5601c89f285517b06 Mon Sep 17 00:00:00 2001 From: chethant Date: Thu, 10 Sep 2026 12:27:36 +0200 Subject: [PATCH 03/10] Address subcontracting repricing review feedback Guard zero expected output, avoid unnecessary record loads, and cover direct no-price fallback calculations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 255f96a1-3a67-418c-9b69-ca50ae31e5b0 --- .../Purchase/SubcPriceManagement.Codeunit.al | 4 + .../Purchase/SubcPurchaseLineExt.Codeunit.al | 4 +- .../Test/Tests/SubcPricingTest.Codeunit.al | 109 ++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) 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 27b2aa7fb24..882e3e1a521 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al @@ -548,6 +548,7 @@ codeunit 20508 "Subc. Price Management" if OrderDate = 0D then OrderDate := WorkDate(); + ProdOrderRoutingLine.SetLoadFields("Standard Task Code"); if not TryFindProdOrderRtngLine( PurchaseLine."Prod. Order No.", PurchaseLine."Routing Reference No.", PurchaseLine."Routing No.", PurchaseLine."Operation No.", ProdOrderRoutingLine) @@ -593,6 +594,9 @@ codeunit 20508 "Subc. Price Management" GeneralLedgerSetup."Unit-Amount Rounding Precision")); ProdOrderLine.CalcFields("Total Exp. Oper. Output (Qty.)"); + if ProdOrderLine."Total Exp. Oper. Output (Qty.)" = 0 then + exit(0); + exit( Round( (ProdOrderRoutingLine."Expected Operation Cost Amt." - ProdOrderRoutingLine."Expected Capacity Ovhd. 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 34bd30ee862..976b55f9a52 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPurchaseLineExt.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPurchaseLineExt.Codeunit.al @@ -362,7 +362,9 @@ codeunit 20534 "Subc. Purchase Line Ext" var PurchaseHeader: Record "Purchase Header"; begin - if PurchaseLine."Prod. Order No." = '' then + if (PurchaseLine.Type <> PurchaseLine.Type::Item) or (PurchaseLine."No." = '') or + (PurchaseLine."Prod. Order No." = '') or (PurchaseLine."Operation No." = '') + then exit; // Preserve released-order scheduling: repricing a subcontracting line after release diff --git a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al index b77882e48c7..d5e9272f34b 100644 --- a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al @@ -290,6 +290,68 @@ codeunit 139982 "Subc. Pricing Test" 'The purchase line must not fall back to the raw routing Direct Unit Cost when a proper worksheet cost was calculated.'); end; + [Test] + procedure NoMatchPriceListFallbackUsesCalculatedCostForUnitsAndTime() + var + ProdOrderLine: Record "Prod. Order Line"; + PurchaseLine: Record "Purchase Line"; + SubcPriceManagement: Codeunit "Subc. Price Management"; + TimeDirectUnitCost: Decimal; + UnitsDirectUnitCost: Decimal; + begin + // [SCENARIO 648535] Direct purchase-line repricing without a matching price uses the + // standard subcontracting calculation for both unit- and time-based operations. + Initialize(); + + CreateNoPriceSubcontractingPurchaseLine( + PurchaseLine, ProdOrderLine, Enum::"Unit Cost Calculation Type"::Units); + UnitsDirectUnitCost := PurchaseLine."Direct Unit Cost"; + Assert.AreNotEqual(0, UnitsDirectUnitCost, 'Test setup expects a nonzero Units Direct Unit Cost.'); + PurchaseLine."Direct Unit Cost" := 0; + + SubcPriceManagement.GetSubcPriceForPurchLine(PurchaseLine); + + Assert.AreEqual( + UnitsDirectUnitCost, PurchaseLine."Direct Unit Cost", + 'The Units fallback must match the Direct Unit Cost calculated when the purchase line was created.'); + + CreateNoPriceSubcontractingPurchaseLine( + PurchaseLine, ProdOrderLine, Enum::"Unit Cost Calculation Type"::Time); + TimeDirectUnitCost := PurchaseLine."Direct Unit Cost"; + Assert.AreNotEqual(0, TimeDirectUnitCost, 'Test setup expects a nonzero Time Direct Unit Cost.'); + Assert.AreNotEqual(UnitsDirectUnitCost, TimeDirectUnitCost, 'Test setup expects Units and Time calculations to produce different costs.'); + PurchaseLine."Direct Unit Cost" := 0; + + SubcPriceManagement.GetSubcPriceForPurchLine(PurchaseLine); + + Assert.AreEqual( + TimeDirectUnitCost, PurchaseLine."Direct Unit Cost", + 'The Time fallback must match the Direct Unit Cost calculated when the purchase line was created.'); + end; + + [Test] + procedure NoMatchPriceListFallbackHandlesZeroExpectedOutputQuantity() + var + ProdOrderLine: Record "Prod. Order Line"; + PurchaseLine: Record "Purchase Line"; + SubcPriceManagement: Codeunit "Subc. Price Management"; + begin + // [SCENARIO 648535] Time-based fallback pricing returns zero instead of dividing by + // zero when the production order has no expected operation output quantity. + Initialize(); + + CreateNoPriceSubcontractingPurchaseLine( + PurchaseLine, ProdOrderLine, Enum::"Unit Cost Calculation Type"::Time); + ProdOrderLine.Quantity := 0; + ProdOrderLine.Modify(); + + SubcPriceManagement.GetSubcPriceForPurchLine(PurchaseLine); + + Assert.AreEqual( + 0, PurchaseLine."Direct Unit Cost", + 'The no-price fallback must be zero when expected operation output quantity is zero.'); + end; + [Test] procedure ManualWorksheetDirectUnitCostOverridePreservedOnCarryOut() var @@ -1178,6 +1240,53 @@ codeunit 139982 "Subc. Pricing Test" SubcontractingMgmtLibrary.FindSubcPurchLineForProdOrder(PurchaseLine, ItemNo, ProdOrderNo); end; + local procedure CreateNoPriceSubcontractingPurchaseLine(var PurchaseLine: Record "Purchase Line"; var ProdOrderLine: Record "Prod. Order Line"; UnitCostCalculation: Enum "Unit Cost Calculation Type") + var + Item: Record Item; + ProductionOrder: Record "Production Order"; + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + RoutingHeader: Record "Routing Header"; + RoutingLine: Record "Routing Line"; + SubcontractorPrice: Record "Subcontractor Price"; + Vendor: Record Vendor; + WorkCenter: Record "Work Center"; + begin + CreateSubcontractingItemWithSingleOperationRouting(Item, Vendor, WorkCenter, ''); + WorkCenter.Validate("Direct Unit Cost", 10); + WorkCenter.Validate("Unit Cost Calculation", UnitCostCalculation); + WorkCenter.Modify(true); + + RoutingHeader.Get(Item."Routing No."); + RoutingHeader.Validate(Status, RoutingHeader.Status::New); + RoutingHeader.Modify(true); + RoutingLine.SetRange("Routing No.", Item."Routing No."); + RoutingLine.FindFirst(); + RoutingLine.Validate("Run Time", 5); + RoutingLine.Modify(true); + RoutingHeader.Validate(Status, RoutingHeader.Status::Certified); + RoutingHeader.Modify(true); + + SubcontractorPrice.SetRange("Vendor No.", Vendor."No."); + SubcontractorPrice.SetRange("Work Center No.", WorkCenter."No."); + SubcontractorPrice.SetRange("Item No.", Item."No."); + Assert.IsTrue(SubcontractorPrice.IsEmpty(), 'Test setup expects no subcontractor prices for the operation.'); + + LibraryManufacturing.CreateProductionOrder( + ProductionOrder, "Production Order Status"::Released, Item, '', '', 1, WorkDate()); + ProdOrderRoutingLine.SetRange(Status, ProductionOrder.Status); + ProdOrderRoutingLine.SetRange("Prod. Order No.", ProductionOrder."No."); + ProdOrderRoutingLine.SetRange("Work Center No.", WorkCenter."No."); + ProdOrderRoutingLine.FindFirst(); + + CreateSubcontractingPurchaseLine(PurchaseLine, ProdOrderRoutingLine, Item."No.", ProductionOrder."No."); + + ProdOrderLine.SetRange(Status, ProductionOrder.Status); + ProdOrderLine.SetRange("Prod. Order No.", ProductionOrder."No."); + ProdOrderLine.SetRange("Routing No.", ProdOrderRoutingLine."Routing No."); + ProdOrderLine.SetRange("Routing Reference No.", ProdOrderRoutingLine."Routing Reference No."); + ProdOrderLine.FindFirst(); + end; + local procedure CreateItemVendorAndSubcontractingWorkCenter(var Item: Record Item; var Vendor: Record Vendor; var WorkCenter: Record "Work Center") begin LibraryInventory.CreateItem(Item); From a8c0c6ea3b3bc54f3e3ee8959a098a145672c5d9 Mon Sep 17 00:00:00 2001 From: chethant Date: Thu, 10 Sep 2026 13:47:46 +0200 Subject: [PATCH 04/10] Fix subcontracting minimum quantity price lookup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 255f96a1-3a67-418c-9b69-ca50ae31e5b0 --- .../Purchase/SubcPriceManagement.Codeunit.al | 19 +++--- .../Test/Tests/SubcPricingTest.Codeunit.al | 58 +++++++++++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) 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 882e3e1a521..4e7536b5d74 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al @@ -365,16 +365,18 @@ codeunit 20508 "Subc. Price Management" PriceListQty := QtyBase / PriceListQtyPerUOM; end; - local procedure GetPriceByUOM(var SubcontractorPrice: Record "Subcontractor Price"; PriceListQty: Decimal; var PriceListCost: Decimal) + local procedure GetPriceByUOM(var SubcontractorPrice: Record "Subcontractor Price"; PriceListQty: Decimal; var PriceListCost: Decimal): Boolean begin SubcontractorPrice.SetRange("Minimum Quantity", 0, PriceListQty); SubcontractorPrice.SetRange("Unit of Measure Code", SubcontractorPrice."Unit of Measure Code"); - if SubcontractorPrice.FindLast() then begin - PriceListCost := SubcontractorPrice."Direct Unit Cost"; - if PriceListCost <> 0 then - if (PriceListCost * PriceListQty) < SubcontractorPrice."Minimum Amount" then - PriceListCost := SubcontractorPrice."Minimum Amount" / PriceListQty; - end; + if not SubcontractorPrice.FindLast() then + exit(false); + + PriceListCost := SubcontractorPrice."Direct Unit Cost"; + if PriceListCost <> 0 then + if (PriceListCost * PriceListQty) < SubcontractorPrice."Minimum Amount" then + PriceListCost := SubcontractorPrice."Minimum Amount" / PriceListQty; + exit(true); end; procedure ConvertPriceToUOM(ProdUOM: Code[10]; ProdQtyPerUoM: Decimal; PriceListUOM: Code[10]; PriceListQtyPerUOM: Decimal; PriceListCost: Decimal; var DirectCost: Decimal) @@ -571,7 +573,8 @@ codeunit 20508 "Subc. Price Management" if SubcontractorPrice."Unit of Measure Code" = PurchaseLine."Unit of Measure Code" then PriceListUOM := SubcontractorPrice."Unit of Measure Code"; GetUOMPrice(PurchaseLine."No.", GetQuantityBase(PurchaseLine), SubcontractorPrice, PriceListUOM, PriceListQtyPerUOM, PriceListQty); - GetPriceByUOM(SubcontractorPrice, PriceListQty, PriceListCost); + if not GetPriceByUOM(SubcontractorPrice, PriceListQty, PriceListCost) then + exit(false); if PriceListCost = 0 then exit(true); diff --git a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al index d5e9272f34b..cc66527d5ce 100644 --- a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al @@ -329,6 +329,64 @@ codeunit 139982 "Subc. Pricing Test" 'The Time fallback must match the Direct Unit Cost calculated when the purchase line was created.'); end; + [Test] + procedure NoApplicableMinimumQuantityPriceUsesCalculatedFallback() + var + ProdOrderLine: Record "Prod. Order Line"; + PurchaseLine: Record "Purchase Line"; + SubcontractorPrice: Record "Subcontractor Price"; + SubcPriceManagement: Codeunit "Subc. Price Management"; + CalculatedFallbackCost: Decimal; + begin + // [SCENARIO 648535] A date-compatible price with a minimum quantity above the purchase + // quantity is not an applicable price and must not replace the calculated fallback with zero. + Initialize(); + + CreateNoPriceSubcontractingPurchaseLine( + PurchaseLine, ProdOrderLine, Enum::"Unit Cost Calculation Type"::Time); + CalculatedFallbackCost := PurchaseLine."Direct Unit Cost"; + Assert.AreNotEqual(0, CalculatedFallbackCost, 'Test setup expects a nonzero calculated fallback cost.'); + SubcontractingMgmtLibrary.CreateSubContractingPrice( + SubcontractorPrice, PurchaseLine."Work Center No.", PurchaseLine."Buy-from Vendor No.", + PurchaseLine."No.", '', PurchaseLine."Variant Code", PurchaseLine."Order Date", + PurchaseLine."Unit of Measure Code", PurchaseLine.Quantity + 1, PurchaseLine."Currency Code"); + SubcontractorPrice.Validate("Direct Unit Cost", CalculatedFallbackCost * 2); + SubcontractorPrice.Modify(true); + PurchaseLine."Direct Unit Cost" := 0; + + SubcPriceManagement.GetSubcPriceForPurchLine(PurchaseLine); + + Assert.AreEqual( + CalculatedFallbackCost, PurchaseLine."Direct Unit Cost", + 'A price above the purchase quantity threshold must not suppress the calculated fallback.'); + end; + + [Test] + procedure ApplicableZeroPriceIsNotReplacedByCalculatedFallback() + var + ProdOrderLine: Record "Prod. Order Line"; + PurchaseLine: Record "Purchase Line"; + SubcontractorPrice: Record "Subcontractor Price"; + SubcPriceManagement: Codeunit "Subc. Price Management"; + begin + // [SCENARIO 648535] A matched price tier can intentionally have a zero direct unit cost. + Initialize(); + + CreateNoPriceSubcontractingPurchaseLine( + PurchaseLine, ProdOrderLine, Enum::"Unit Cost Calculation Type"::Time); + Assert.AreNotEqual(0, PurchaseLine."Direct Unit Cost", 'Test setup expects a nonzero calculated fallback cost.'); + SubcontractingMgmtLibrary.CreateSubContractingPrice( + SubcontractorPrice, PurchaseLine."Work Center No.", PurchaseLine."Buy-from Vendor No.", + PurchaseLine."No.", '', PurchaseLine."Variant Code", PurchaseLine."Order Date", + PurchaseLine."Unit of Measure Code", 0, PurchaseLine."Currency Code"); + + SubcPriceManagement.GetSubcPriceForPurchLine(PurchaseLine); + + Assert.AreEqual( + 0, PurchaseLine."Direct Unit Cost", + 'An applicable zero price must not be replaced by the calculated fallback.'); + end; + [Test] procedure NoMatchPriceListFallbackHandlesZeroExpectedOutputQuantity() var From 57e15f71cfceb2c78ee329d04e4557386b3de5bc Mon Sep 17 00:00:00 2001 From: chethant Date: Thu, 10 Sep 2026 16:53:49 +0200 Subject: [PATCH 05/10] Address subcontracting carry-out review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 255f96a1-3a67-418c-9b69-ca50ae31e5b0 --- .../SubcReqWkshMakeOrd.Codeunit.al | 9 +- .../Purchase/SubcPriceManagement.Codeunit.al | 42 ++++---- .../Test/Tests/SubcPricingTest.Codeunit.al | 100 +++++++++++++++++- 3 files changed, 120 insertions(+), 31 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 2de76fcf90b..fd61afe1170 100644 --- a/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al @@ -33,7 +33,7 @@ codeunit 20516 "Subc. Req. Wksh. Make Ord." local procedure OnInsertPurchOrderLineOnAfterTransferFromReqLineToPurchLine(var PurchOrderLine: Record "Purchase Line"; RequisitionLine: Record "Requisition Line") var SubcPriceManagement: Codeunit "Subc. Price Management"; - ReqLineDatePriceListCost: Decimal; + AutomaticReqLineCost: Decimal; begin #if not CLEAN29 #pragma warning disable AL0432 @@ -44,11 +44,8 @@ codeunit 20516 "Subc. Req. Wksh. Make Ord." if (RequisitionLine."Prod. Order No." = '') or (RequisitionLine."Operation No." = '') then exit; - if not SubcPriceManagement.TryGetSubcPriceListCostForPurchLine( - PurchOrderLine, RequisitionLine."Order Date", ReqLineDatePriceListCost) - then - exit; - if PurchOrderLine."Direct Unit Cost" <> ReqLineDatePriceListCost then + AutomaticReqLineCost := SubcPriceManagement.GetAutomaticSubcCostForReqLine(RequisitionLine); + if RequisitionLine."Direct Unit Cost" <> AutomaticReqLineCost then exit; SubcPriceManagement.GetSubcPriceForPurchLine(PurchOrderLine); 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 4e7536b5d74..3849ddde1e4 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al @@ -485,7 +485,8 @@ codeunit 20508 "Subc. Price Management" end else GetUOMPrice(RequisitionLine."No.", RequisitionLine.GetQuantityBase(), SubcontractorPrice, PriceListUOM, PriceListQtyPerUOM, PriceListQty); - GetPriceByUOM(SubcontractorPrice, PriceListQty, PriceListCost); + if not GetPriceByUOM(SubcontractorPrice, PriceListQty, PriceListCost) then + exit; if PriceListCost <> 0 then begin ConvertPriceToUOM(RequisitionLine."Unit of Measure Code", RequisitionLine.GetQuantityForUOM(), PriceListUOM, PriceListQtyPerUOM, PriceListCost, DirectCost); ConvertPriceToCurrency(RequisitionLine."Currency Code", SubcontractorPrice."Currency Code", PriceListCost, DirectCost); @@ -505,6 +506,19 @@ codeunit 20508 "Subc. Price Management" end; end; + internal procedure GetAutomaticSubcCostForReqLine(RequisitionLine: Record "Requisition Line"): Decimal + var + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + begin + GetProdOrderRtngLine( + RequisitionLine."Prod. Order No.", RequisitionLine."Routing Reference No.", + RequisitionLine."Routing No.", RequisitionLine."Operation No.", ProdOrderRoutingLine); + ProdOrderRoutingLine.TestField(Type, "Capacity Type"::"Work Center"); + RequisitionLine."Direct Unit Cost" := GetNonPriceListDirectCost(ProdOrderRoutingLine); + GetSubcPriceForReqLine(RequisitionLine, ''); + exit(RequisitionLine."Direct Unit Cost"); + end; + procedure GetSubcPriceForPurchLine(var PurchaseLine: Record "Purchase Line") var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; @@ -521,10 +535,7 @@ codeunit 20508 "Subc. Price Management" if OrderDate = 0D then OrderDate := WorkDate(); - if not TryGetSubcPriceListCostForPurchLine(PurchaseLine, OrderDate, DirectCost) then begin - GetProdOrderRtngLine( - PurchaseLine."Prod. Order No.", PurchaseLine."Routing Reference No.", - PurchaseLine."Routing No.", PurchaseLine."Operation No.", ProdOrderRoutingLine); + if not TryGetSubcPriceListCostForPurchLine(PurchaseLine, OrderDate, DirectCost, ProdOrderRoutingLine) then begin ProdOrderRoutingLine.TestField(Type, "Capacity Type"::"Work Center"); DirectCost := GetNonPriceListDirectCost(ProdOrderRoutingLine); end; @@ -533,9 +544,8 @@ codeunit 20508 "Subc. Price Management" PurchaseLine.Validate("Line Discount %"); end; - internal procedure TryGetSubcPriceListCostForPurchLine(PurchaseLine: Record "Purchase Line"; OrderDate: Date; var DirectCost: Decimal): Boolean + local procedure TryGetSubcPriceListCostForPurchLine(PurchaseLine: Record "Purchase Line"; OrderDate: Date; var DirectCost: Decimal; var ProdOrderRoutingLine: Record "Prod. Order Routing Line"): Boolean var - ProdOrderRoutingLine: Record "Prod. Order Routing Line"; SubcontractorPrice: Record "Subcontractor Price"; PriceListUOM: Code[10]; PriceListCost, PriceListQty, PriceListQtyPerUOM : Decimal; @@ -551,11 +561,9 @@ codeunit 20508 "Subc. Price Management" OrderDate := WorkDate(); ProdOrderRoutingLine.SetLoadFields("Standard Task Code"); - if not TryFindProdOrderRtngLine( - PurchaseLine."Prod. Order No.", PurchaseLine."Routing Reference No.", - PurchaseLine."Routing No.", PurchaseLine."Operation No.", ProdOrderRoutingLine) - then - exit(false); + GetProdOrderRtngLine( + PurchaseLine."Prod. Order No.", PurchaseLine."Routing Reference No.", + PurchaseLine."Routing No.", PurchaseLine."Operation No.", ProdOrderRoutingLine); SubcontractorPrice.SetRange("Vendor No.", PurchaseLine."Buy-from Vendor No."); SubcontractorPrice.SetRange("Work Center No.", PurchaseLine."Work Center No."); @@ -618,16 +626,6 @@ codeunit 20508 "Subc. Price Management" ProdOrderRoutingLine.FindFirst(); end; - local procedure TryFindProdOrderRtngLine(ProdOrderNo: Code[20]; RtngRefNo: Integer; RoutingNo: Code[20]; OperationNo: Code[10]; var ProdOrderRoutingLine: Record "Prod. Order Routing Line"): Boolean - begin - ProdOrderRoutingLine.SetFilter(Status, '%1|%2', ProdOrderRoutingLine.Status::Released, ProdOrderRoutingLine.Status::Finished); - ProdOrderRoutingLine.SetRange("Prod. Order No.", ProdOrderNo); - ProdOrderRoutingLine.SetRange("Routing Reference No.", RtngRefNo); - ProdOrderRoutingLine.SetRange("Routing No.", RoutingNo); - ProdOrderRoutingLine.SetRange("Operation No.", OperationNo); - exit(ProdOrderRoutingLine.FindFirst()); - end; - local procedure SetSubcontractorPriceForPriceCalculation(var SubcontractorPrice: Record "Subcontractor Price"; VendorNo: Code[20]; ItemNo: Code[20]; VariantCode: Code[10]; StandardTaskCode: Code[10]; WorkCenterNo: Code[20]; UoM: Code[10]; StartingDate: Date) var Vendor: Record Vendor; diff --git a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al index cc66527d5ce..64e2b522533 100644 --- a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al @@ -248,6 +248,8 @@ codeunit 139982 "Subc. Pricing Test" // [GIVEN] A subcontracting item whose routing operation has a multi-minute Run Time CreateSubcontractingItemWithSingleOperationRouting(Item, Vendor, WorkCenter, ''); + WorkCenter.Validate("Direct Unit Cost", LibraryRandom.RandDecInRange(10, 100, 2)); + WorkCenter.Modify(true); RoutingHeader.Get(Item."Routing No."); RoutingHeader.Validate(Status, RoutingHeader.Status::New); RoutingHeader.Modify(true); @@ -329,6 +331,98 @@ codeunit 139982 "Subc. Pricing Test" 'The Time fallback must match the Direct Unit Cost calculated when the purchase line was created.'); end; + [Test] + procedure FinalPurchaseLineDateAppliesPriceWhenReqLineDateHasNoPrice() + var + Item: Record Item; + ProductionOrder: Record "Production Order"; + PurchaseLine: Record "Purchase Line"; + ReqWkshTemplate: Record "Req. Wksh. Template"; + RequisitionLine: Record "Requisition Line"; + RequisitionWkshName: Record "Requisition Wksh. Name"; + SubcontractorPrice: Record "Subcontractor Price"; + Vendor: Record Vendor; + WorkCenter: Record "Work Center"; + FinalDatePrice: Decimal; + begin + // [SCENARIO 648535] An automatically calculated worksheet fallback is repriced when + // the final purchase-line date has an applicable subcontractor price. + Initialize(); + + CreateSubcontractingItemWithSingleOperationRouting(Item, Vendor, WorkCenter, ''); + WorkCenter.Validate("Direct Unit Cost", LibraryRandom.RandDecInRange(10, 100, 2)); + WorkCenter.Modify(true); + Evaluate(Item."Lead Time Calculation", '<5D>'); + Item.Modify(true); + LibraryManufacturing.CreateProductionOrder( + ProductionOrder, "Production Order Status"::Released, Item, '', '', 1, WorkDate()); + SubcontractingMgmtLibrary.CreateReqWkshTemplateAndName(ReqWkshTemplate, RequisitionWkshName); + SubcontractingMgmtLibrary.CalculateSubcontractsAndFindReqLine( + RequisitionWkshName, ProductionOrder."No.", RequisitionLine); + RequisitionLine.Validate("Order Date", WorkDate()); + RequisitionLine.Modify(true); + FinalDatePrice := LibraryRandom.RandDecInRange(100, 200, 2); + SubcontractingMgmtLibrary.CreateSubContractingPrice( + SubcontractorPrice, WorkCenter."No.", Vendor."No.", Item."No.", '', '', + CalcDate('<-1M>', WorkDate()), Item."Base Unit of Measure", 0, ''); + SubcontractorPrice.Validate("Ending Date", CalcDate('<-1D>', WorkDate())); + SubcontractorPrice.Validate("Direct Unit Cost", FinalDatePrice); + SubcontractorPrice.Modify(true); + + SubcontractingMgmtLibrary.CarryOutSubcontractingAction(RequisitionLine); + + SubcontractingMgmtLibrary.FindSubcPurchLineForProdOrder( + PurchaseLine, Item."No.", ProductionOrder."No."); + Assert.IsTrue( + PurchaseLine."Order Date" < WorkDate(), + 'The final purchase-line Order Date must be in the price validity period.'); + Assert.AreEqual( + FinalDatePrice, PurchaseLine."Direct Unit Cost", + 'The price valid on the final purchase-line date must replace the automatic worksheet fallback.'); + end; + + [Test] + procedure ReqLinePriceAboveMinimumQuantityPreservesCalculatedFallback() + var + Item: Record Item; + ProductionOrder: Record "Production Order"; + ReqWkshTemplate: Record "Req. Wksh. Template"; + RequisitionLine: Record "Requisition Line"; + RequisitionWkshName: Record "Requisition Wksh. Name"; + SubcontractorPrice: Record "Subcontractor Price"; + Vendor: Record Vendor; + WorkCenter: Record "Work Center"; + SubcPriceManagement: Codeunit "Subc. Price Management"; + CalculatedFallbackCost: Decimal; + begin + // [SCENARIO 648535] A requisition-line price whose minimum quantity is too high does + // not replace the standard calculated subcontracting cost with zero. + Initialize(); + + CreateSubcontractingItemWithSingleOperationRouting(Item, Vendor, WorkCenter, ''); + WorkCenter.Validate("Direct Unit Cost", LibraryRandom.RandDecInRange(10, 100, 2)); + WorkCenter.Modify(true); + LibraryManufacturing.CreateProductionOrder( + ProductionOrder, "Production Order Status"::Released, Item, '', '', 1, WorkDate()); + SubcontractingMgmtLibrary.CreateReqWkshTemplateAndName(ReqWkshTemplate, RequisitionWkshName); + SubcontractingMgmtLibrary.CalculateSubcontractsAndFindReqLine( + RequisitionWkshName, ProductionOrder."No.", RequisitionLine); + CalculatedFallbackCost := RequisitionLine."Direct Unit Cost"; + Assert.AreNotEqual(0, CalculatedFallbackCost, 'Test setup expects a nonzero calculated fallback cost.'); + SubcontractingMgmtLibrary.CreateSubContractingPrice( + SubcontractorPrice, WorkCenter."No.", Vendor."No.", Item."No.", '', '', + RequisitionLine."Order Date", RequisitionLine."Unit of Measure Code", + RequisitionLine.Quantity + 1, RequisitionLine."Currency Code"); + SubcontractorPrice.Validate("Direct Unit Cost", CalculatedFallbackCost * 2); + SubcontractorPrice.Modify(true); + + SubcPriceManagement.GetSubcPriceForReqLine(RequisitionLine, ''); + + Assert.AreEqual( + CalculatedFallbackCost, RequisitionLine."Direct Unit Cost", + 'A requisition-line price above the quantity threshold must preserve the calculated fallback.'); + end; + [Test] procedure NoApplicableMinimumQuantityPriceUsesCalculatedFallback() var @@ -400,8 +494,8 @@ codeunit 139982 "Subc. Pricing Test" CreateNoPriceSubcontractingPurchaseLine( PurchaseLine, ProdOrderLine, Enum::"Unit Cost Calculation Type"::Time); - ProdOrderLine.Quantity := 0; - ProdOrderLine.Modify(); + ProdOrderLine.Validate(Quantity, 0); + ProdOrderLine.Modify(true); SubcPriceManagement.GetSubcPriceForPurchLine(PurchaseLine); @@ -476,7 +570,7 @@ codeunit 139982 "Subc. Pricing Test" OriginalLineDiscount := PurchaseLine."Line Discount %"; PurchaseHeader.Get(PurchaseLine."Document Type", PurchaseLine."Document No."); LibraryPurchase.ReleasePurchaseDocument(PurchaseHeader); - PurchaseLine.Find(); + PurchaseLine.Get(PurchaseLine."Document Type", PurchaseLine."Document No.", PurchaseLine."Line No."); // [WHEN] Planned Receipt Date is edited on the released line to move Order Date into // the later price period From d538bdcd94cf47a11631cad7721765c0322938f9 Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah Date: Thu, 17 Sep 2026 11:50:08 +0200 Subject: [PATCH 06/10] Address PR review feedback on subcontracting repricing - Guard GetPriceByUOM against divide-by-zero when PriceListQty is 0 while a Minimum Amount is configured. - Extend SetLoadFields on the fallback Prod. Order Routing Line so Type, Unit Cost Calculation, Direct Unit Cost, Expected Operation Cost Amt. and Expected Capacity Ovhd. Cost are available when the no-price-list fallback path runs (previously only Standard Task Code was loaded, which could error or silently zero out on fallback). - Add SetLoadFields to the Prod. Order Line lookup in GetNonPriceListDirectCost to avoid materializing unused columns. - Avoid computing the subcontracting price twice per requisition line during Carry Out / Make Order: GetAutomaticSubcCostForReqLine now has an overload that returns the Prod. Order Routing Line it looked up, and GetSubcPriceForPurchLine accepts that record to skip a redundant Prod. Order Routing Line lookup for the same operation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SubcReqWkshMakeOrd.Codeunit.al | 5 ++- .../Purchase/SubcPriceManagement.Codeunit.al | 38 ++++++++++++++++--- 2 files changed, 36 insertions(+), 7 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 fd61afe1170..c5ff993c479 100644 --- a/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al @@ -33,6 +33,7 @@ codeunit 20516 "Subc. Req. Wksh. Make Ord." local procedure OnInsertPurchOrderLineOnAfterTransferFromReqLineToPurchLine(var PurchOrderLine: Record "Purchase Line"; RequisitionLine: Record "Requisition Line") var SubcPriceManagement: Codeunit "Subc. Price Management"; + ProdOrderRoutingLine: Record "Prod. Order Routing Line"; AutomaticReqLineCost: Decimal; begin #if not CLEAN29 @@ -44,11 +45,11 @@ codeunit 20516 "Subc. Req. Wksh. Make Ord." if (RequisitionLine."Prod. Order No." = '') or (RequisitionLine."Operation No." = '') then exit; - AutomaticReqLineCost := SubcPriceManagement.GetAutomaticSubcCostForReqLine(RequisitionLine); + AutomaticReqLineCost := SubcPriceManagement.GetAutomaticSubcCostForReqLine(RequisitionLine, ProdOrderRoutingLine); if RequisitionLine."Direct Unit Cost" <> AutomaticReqLineCost then exit; - SubcPriceManagement.GetSubcPriceForPurchLine(PurchOrderLine); + SubcPriceManagement.GetSubcPriceForPurchLine(PurchOrderLine, ProdOrderRoutingLine); end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Req. Wksh.-Make Order", OnInsertPurchOrderLineOnAfterCheckInsertFinalizePurchaseOrderHeader, '', false, false)] 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 3849ddde1e4..b11f72a007f 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al @@ -373,7 +373,7 @@ codeunit 20508 "Subc. Price Management" exit(false); PriceListCost := SubcontractorPrice."Direct Unit Cost"; - if PriceListCost <> 0 then + if (PriceListCost <> 0) and (PriceListQty <> 0) then if (PriceListCost * PriceListQty) < SubcontractorPrice."Minimum Amount" then PriceListCost := SubcontractorPrice."Minimum Amount" / PriceListQty; exit(true); @@ -509,6 +509,11 @@ codeunit 20508 "Subc. Price Management" internal procedure GetAutomaticSubcCostForReqLine(RequisitionLine: Record "Requisition Line"): Decimal var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + begin + exit(GetAutomaticSubcCostForReqLine(RequisitionLine, ProdOrderRoutingLine)); + end; + + internal procedure GetAutomaticSubcCostForReqLine(RequisitionLine: Record "Requisition Line"; var ProdOrderRoutingLine: Record "Prod. Order Routing Line"): Decimal begin GetProdOrderRtngLine( RequisitionLine."Prod. Order No.", RequisitionLine."Routing Reference No.", @@ -522,6 +527,14 @@ codeunit 20508 "Subc. Price Management" procedure GetSubcPriceForPurchLine(var PurchaseLine: Record "Purchase Line") var ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + begin + GetSubcPriceForPurchLine(PurchaseLine, ProdOrderRoutingLine); + end; + + // ProdOrderRoutingLine can be pre-populated by the caller (e.g. from a prior GetAutomaticSubcCostForReqLine call + // for the same operation) to avoid re-reading the same Prod. Order Routing Line record. + procedure GetSubcPriceForPurchLine(var PurchaseLine: Record "Purchase Line"; var ProdOrderRoutingLine: Record "Prod. Order Routing Line") + var OrderDate: Date; DirectCost: Decimal; begin @@ -560,10 +573,14 @@ codeunit 20508 "Subc. Price Management" if OrderDate = 0D then OrderDate := WorkDate(); - ProdOrderRoutingLine.SetLoadFields("Standard Task Code"); - GetProdOrderRtngLine( - PurchaseLine."Prod. Order No.", PurchaseLine."Routing Reference No.", - PurchaseLine."Routing No.", PurchaseLine."Operation No.", ProdOrderRoutingLine); + if not IsProdOrderRoutingLineForPurchLine(ProdOrderRoutingLine, PurchaseLine) then begin + ProdOrderRoutingLine.SetLoadFields( + "Standard Task Code", Type, "Unit Cost Calculation", "Direct Unit Cost", + "Expected Operation Cost Amt.", "Expected Capacity Ovhd. Cost"); + GetProdOrderRtngLine( + PurchaseLine."Prod. Order No.", PurchaseLine."Routing Reference No.", + PurchaseLine."Routing No.", PurchaseLine."Operation No.", ProdOrderRoutingLine); + end; SubcontractorPrice.SetRange("Vendor No.", PurchaseLine."Buy-from Vendor No."); SubcontractorPrice.SetRange("Work Center No.", PurchaseLine."Work Center No."); @@ -596,6 +613,7 @@ codeunit 20508 "Subc. Price Management" GeneralLedgerSetup: Record "General Ledger Setup"; ProdOrderLine: Record "Prod. Order Line"; begin + ProdOrderLine.SetLoadFields("Qty. per Unit of Measure"); GetLine(ProdOrderLine, ProdOrderRoutingLine); GeneralLedgerSetup.Get(); if ProdOrderRoutingLine."Unit Cost Calculation" = ProdOrderRoutingLine."Unit Cost Calculation"::Units then @@ -626,6 +644,16 @@ codeunit 20508 "Subc. Price Management" ProdOrderRoutingLine.FindFirst(); end; + local procedure IsProdOrderRoutingLineForPurchLine(ProdOrderRoutingLine: Record "Prod. Order Routing Line"; PurchaseLine: Record "Purchase Line"): Boolean + begin + exit( + (ProdOrderRoutingLine."Prod. Order No." <> '') and + (ProdOrderRoutingLine."Prod. Order No." = PurchaseLine."Prod. Order No.") and + (ProdOrderRoutingLine."Routing Reference No." = PurchaseLine."Routing Reference No.") and + (ProdOrderRoutingLine."Routing No." = PurchaseLine."Routing No.") and + (ProdOrderRoutingLine."Operation No." = PurchaseLine."Operation No.")); + end; + local procedure SetSubcontractorPriceForPriceCalculation(var SubcontractorPrice: Record "Subcontractor Price"; VendorNo: Code[20]; ItemNo: Code[20]; VariantCode: Code[10]; StandardTaskCode: Code[10]; WorkCenterNo: Code[20]; UoM: Code[10]; StartingDate: Date) var Vendor: Record Vendor; From 5358ef1271e8ab65b2f8ed87094b9e52f66cd5bb Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah Date: Thu, 17 Sep 2026 15:59:09 +0200 Subject: [PATCH 07/10] Fix AA0244/AA0021 CodeCop warnings blocking CI - Rename UnitCostCalculation parameter to UnitCostCalculationType in CreateNoPriceSubcontractingPurchaseLine to avoid shadowing the codeunit's global variable of the same name (AA0244). - Reorder var declarations in OnInsertPurchOrderLineOnAfterTransferFromReqLineToPurchLine so the Record var precedes the Codeunit var, per AA0021 ordering rules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al | 2 +- .../W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al | 4 ++-- 2 files changed, 3 insertions(+), 3 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 c5ff993c479..371ed2c791b 100644 --- a/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Manufacturing/SubcReqWkshMakeOrd.Codeunit.al @@ -32,8 +32,8 @@ codeunit 20516 "Subc. Req. Wksh. Make Ord." [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"; ProdOrderRoutingLine: Record "Prod. Order Routing Line"; + SubcPriceManagement: Codeunit "Subc. Price Management"; AutomaticReqLineCost: Decimal; begin #if not CLEAN29 diff --git a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al index 64e2b522533..8cb673059b6 100644 --- a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al @@ -1392,7 +1392,7 @@ codeunit 139982 "Subc. Pricing Test" SubcontractingMgmtLibrary.FindSubcPurchLineForProdOrder(PurchaseLine, ItemNo, ProdOrderNo); end; - local procedure CreateNoPriceSubcontractingPurchaseLine(var PurchaseLine: Record "Purchase Line"; var ProdOrderLine: Record "Prod. Order Line"; UnitCostCalculation: Enum "Unit Cost Calculation Type") + local procedure CreateNoPriceSubcontractingPurchaseLine(var PurchaseLine: Record "Purchase Line"; var ProdOrderLine: Record "Prod. Order Line"; UnitCostCalculationType: Enum "Unit Cost Calculation Type") var Item: Record Item; ProductionOrder: Record "Production Order"; @@ -1405,7 +1405,7 @@ codeunit 139982 "Subc. Pricing Test" begin CreateSubcontractingItemWithSingleOperationRouting(Item, Vendor, WorkCenter, ''); WorkCenter.Validate("Direct Unit Cost", 10); - WorkCenter.Validate("Unit Cost Calculation", UnitCostCalculation); + WorkCenter.Validate("Unit Cost Calculation", UnitCostCalculationType); WorkCenter.Modify(true); RoutingHeader.Get(Item."Routing No."); From e23cbd2f1d04ce204f15e1f46dc06bdc8eb1193a Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah Date: Fri, 18 Sep 2026 12:18:46 +0200 Subject: [PATCH 08/10] Address review feedback: reset stale price-list state on GetSubcPriceForReqLine early exit When GetPriceByUOM finds no applicable price tier, GetSubcPriceForReqLine now clears Subc. Pricelist Cost, Subc. UoM for Pricelist, and both conversion ratios (restoring them to 1) instead of leaving stale values from a prior calculation on the requisition line. Adds regression coverage that starts with populated price-list metadata, recalculates with no applicable tier, and verifies all four fields are reset. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Purchase/SubcPriceManagement.Codeunit.al | 7 ++- .../Test/Tests/SubcPricingTest.Codeunit.al | 59 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) 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 b11f72a007f..a106f302efc 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al @@ -485,8 +485,13 @@ codeunit 20508 "Subc. Price Management" end else GetUOMPrice(RequisitionLine."No.", RequisitionLine.GetQuantityBase(), SubcontractorPrice, PriceListUOM, PriceListQtyPerUOM, PriceListQty); - if not GetPriceByUOM(SubcontractorPrice, PriceListQty, PriceListCost) then + if not GetPriceByUOM(SubcontractorPrice, PriceListQty, PriceListCost) then begin + RequisitionLine."Subc. Pricelist Cost" := 0; + RequisitionLine."Subc. UoM for Pricelist" := ''; + RequisitionLine."Base UM Qty/PL UM Qty" := 1; + RequisitionLine."PL UM Qty/Base UM Qty" := 1; exit; + end; if PriceListCost <> 0 then begin ConvertPriceToUOM(RequisitionLine."Unit of Measure Code", RequisitionLine.GetQuantityForUOM(), PriceListUOM, PriceListQtyPerUOM, PriceListCost, DirectCost); ConvertPriceToCurrency(RequisitionLine."Currency Code", SubcontractorPrice."Currency Code", PriceListCost, DirectCost); diff --git a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al index 8cb673059b6..90584b5615c 100644 --- a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al @@ -1695,6 +1695,65 @@ codeunit 139982 "Subc. Pricing Test" 'GetSubcPriceForReqLine must pick the price row matching the line''s Unit of Measure when FixedUOM is empty.'); end; + [Test] + procedure NoApplicablePriceTierClearsStalePricelistStateOnReqLine() + var + Item: Record Item; + Vendor: Record Vendor; + WorkCenter: Record "Work Center"; + SubcontractorPrice: Record "Subcontractor Price"; + RequisitionLine: Record "Requisition Line"; + SubcPriceManagement: Codeunit "Subc. Price Management"; + PriceListUnitCost: Decimal; + begin + // [SCENARIO 648535] When recalculating a requisition line and no subcontractor price + // tier applies (every tier's Minimum Quantity exceeds the line quantity), GetSubcPriceForReqLine + // must clear any previously populated Subc. Pricelist Cost, Subc. UoM for Pricelist, and + // conversion ratios instead of leaving stale price-list state paired with the fallback cost. + Initialize(); + + // [GIVEN] Item, vendor, and work center linked as a subcontractor. + LibraryInventory.CreateItem(Item); + LibraryPurchase.CreateVendor(Vendor); + LibraryManufacturing.CreateWorkCenter(WorkCenter); + WorkCenter.Validate("Subcontractor No.", Vendor."No."); + WorkCenter.Modify(true); + + // [GIVEN] A subcontractor price with Minimum Quantity 1, applicable to a line with Quantity 5. + PriceListUnitCost := 1000; + SubcontractingMgmtLibrary.CreateSubContractingPrice( + SubcontractorPrice, WorkCenter."No.", Vendor."No.", Item."No.", '', '', WorkDate(), '', 1, ''); + SubcontractorPrice.Validate("Direct Unit Cost", PriceListUnitCost); + SubcontractorPrice.Modify(true); + + RequisitionLine.Init(); + RequisitionLine."No." := Item."No."; + RequisitionLine."Unit of Measure Code" := Item."Base Unit of Measure"; + RequisitionLine."Vendor No." := Vendor."No."; + RequisitionLine."Work Center No." := WorkCenter."No."; + RequisitionLine."Order Date" := WorkDate(); + RequisitionLine.Quantity := 5; + + // [GIVEN] The line is priced once, populating the price-list cost/UoM and conversion ratios. + SubcPriceManagement.GetSubcPriceForReqLine(RequisitionLine, ''); + Assert.AreNotEqual(0, RequisitionLine."Subc. Pricelist Cost", 'Test setup expects a nonzero Subc. Pricelist Cost.'); + Assert.AreNotEqual('', RequisitionLine."Subc. UoM for Pricelist", 'Test setup expects a populated Subc. UoM for Pricelist.'); + + // [GIVEN] The line's quantity drops below every price tier's Minimum Quantity. + SubcontractorPrice.Validate("Minimum Quantity", 100); + SubcontractorPrice.Modify(true); + RequisitionLine.Quantity := 1; + + // [WHEN] The requisition line is recalculated and no price tier applies. + SubcPriceManagement.GetSubcPriceForReqLine(RequisitionLine, ''); + + // [THEN] The stale price-list cost, UoM, and conversion ratios are cleared. + Assert.AreEqual(0, RequisitionLine."Subc. Pricelist Cost", 'Subc. Pricelist Cost must be reset when no price tier applies.'); + Assert.AreEqual('', RequisitionLine."Subc. UoM for Pricelist", 'Subc. UoM for Pricelist must be reset when no price tier applies.'); + Assert.AreEqual(1, RequisitionLine."Base UM Qty/PL UM Qty", 'Base UM Qty/PL UM Qty must be reset to 1 when no price tier applies.'); + Assert.AreEqual(1, RequisitionLine."PL UM Qty/Base UM Qty", 'PL UM Qty/Base UM Qty must be reset to 1 when no price tier applies.'); + end; + [Test] procedure FactboxCountsBlankUoMPriceWhenPurchLineHasUoM() var From 5fcf2f85a0bb70cecf9712c987ca4f0d992dee65 Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah Date: Fri, 18 Sep 2026 14:04:41 +0200 Subject: [PATCH 09/10] Restrict Prod. Order Routing Line load to used fields in GetAutomaticSubcCostForReqLine Adds SetLoadFields (Standard Task Code, Type, Unit Cost Calculation, Direct Unit Cost, Expected Operation Cost Amt., Expected Capacity Ovhd. Cost) before fetching the routing line, matching the equivalent restriction already applied in the sibling TryGetSubcPriceListCostForPurchLine, avoiding an inconsistent full-row load on this per-line hot path. Found by local AL review (al-performance-review). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../App/src/Purchase/SubcPriceManagement.Codeunit.al | 3 +++ 1 file changed, 3 insertions(+) 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 a106f302efc..1f3a38fa60f 100644 --- a/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al +++ b/src/Apps/W1/Subcontracting/App/src/Purchase/SubcPriceManagement.Codeunit.al @@ -520,6 +520,9 @@ codeunit 20508 "Subc. Price Management" internal procedure GetAutomaticSubcCostForReqLine(RequisitionLine: Record "Requisition Line"; var ProdOrderRoutingLine: Record "Prod. Order Routing Line"): Decimal begin + ProdOrderRoutingLine.SetLoadFields( + "Standard Task Code", Type, "Unit Cost Calculation", "Direct Unit Cost", + "Expected Operation Cost Amt.", "Expected Capacity Ovhd. Cost"); GetProdOrderRtngLine( RequisitionLine."Prod. Order No.", RequisitionLine."Routing Reference No.", RequisitionLine."Routing No.", RequisitionLine."Operation No.", ProdOrderRoutingLine); From 9e5ba15e7173b615c98ce773981b8d42b271c276 Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah Date: Mon, 21 Sep 2026 09:49:08 +0200 Subject: [PATCH 10/10] Fix subcontractor price test record rename Use Record.Rename when changing Minimum Quantity because it is part of the Subcontractor Price primary key. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al index 90584b5615c..a4081f35810 100644 --- a/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/Tests/SubcPricingTest.Codeunit.al @@ -1740,8 +1740,10 @@ codeunit 139982 "Subc. Pricing Test" Assert.AreNotEqual('', RequisitionLine."Subc. UoM for Pricelist", 'Test setup expects a populated Subc. UoM for Pricelist.'); // [GIVEN] The line's quantity drops below every price tier's Minimum Quantity. - SubcontractorPrice.Validate("Minimum Quantity", 100); - SubcontractorPrice.Modify(true); + SubcontractorPrice.Rename( + SubcontractorPrice."Vendor No.", SubcontractorPrice."Item No.", SubcontractorPrice."Work Center No.", + SubcontractorPrice."Variant Code", SubcontractorPrice."Standard Task Code", SubcontractorPrice."Starting Date", + SubcontractorPrice."Unit of Measure Code", 100, SubcontractorPrice."Currency Code"); RequisitionLine.Quantity := 1; // [WHEN] The requisition line is recalculated and no price tier applies.