From 25f58e212569974682d48807e903a6618259b447 Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah <41570277+ChethanT@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:45:24 +0200 Subject: [PATCH 1/6] [IT] Precheck subcontracting migration locations Block migration when legacy subcontracting locations use unsupported warehouse settings and report every affected location. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../App/src/ITSubcMigration.Codeunit.al | 63 ++++++++++ .../Test/src/ITSubcMigrationTests.Codeunit.al | 114 ++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al b/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al index 4f695c8ecfd..86a3fa07a67 100644 --- a/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al +++ b/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al @@ -1,6 +1,7 @@ #if not CLEAN28 namespace Microsoft.Manufacturing.Subcontracting.Migration; +using Microsoft.Inventory.Location; using Microsoft.Inventory.Transfer; using Microsoft.Manufacturing.Document; using Microsoft.Manufacturing.Routing; @@ -41,6 +42,7 @@ codeunit 149951 "IT Subc. Migration" #if not CLEAN28 LegacySubcFeatureHandler.CheckCanDisableLegacySubcontracting(); #endif + CheckSubcontractingLocations(); UIAllowed := ShowDialog and GuiAllowed(); if UIAllowed then begin ConfirmDisableLegacySubcontracting(); @@ -573,6 +575,66 @@ codeunit 149951 "IT Subc. Migration" PurchaseHeader.SetFilter("Subcontracting Location Code", '<>%1', ''); end; + [ErrorBehavior(ErrorBehavior::Collect)] + internal procedure CheckSubcontractingLocations() + var + Vendor: Record Vendor; + PurchaseHeader: Record "Purchase Header"; + Location: Record Location; + LegacySubcontractingLocations: Dictionary of [Code[10], Boolean]; + LocationCode: Code[10]; + UnsupportedWarehouseSettings: Text; + begin + SetVendorMigrationFilters(Vendor); + Vendor.SetLoadFields("Subcontracting Location Code"); + if Vendor.FindSet() then + repeat + AddLegacySubcontractingLocation(LegacySubcontractingLocations, Vendor."Subcontracting Location Code"); + until Vendor.Next() = 0; + + SetPurchaseHeaderMigrationFilters(PurchaseHeader); + PurchaseHeader.SetLoadFields("Subcontracting Location Code"); + if PurchaseHeader.FindSet() then + repeat + AddLegacySubcontractingLocation(LegacySubcontractingLocations, PurchaseHeader."Subcontracting Location Code"); + until PurchaseHeader.Next() = 0; + + foreach LocationCode in LegacySubcontractingLocations.Keys() do begin + Location.Get(LocationCode); + UnsupportedWarehouseSettings := GetUnsupportedWarehouseSettings(Location); + if UnsupportedWarehouseSettings <> '' then + Error(UnsupportedSubcontractingLocationErr, Location.Code, UnsupportedWarehouseSettings); + end; + end; + + local procedure AddLegacySubcontractingLocation(var LegacySubcontractingLocations: Dictionary of [Code[10], Boolean]; LocationCode: Code[10]) + begin + if not LegacySubcontractingLocations.ContainsKey(LocationCode) then + LegacySubcontractingLocations.Add(LocationCode, true); + end; + + local procedure GetUnsupportedWarehouseSettings(Location: Record Location): Text + var + UnsupportedWarehouseSettings: Text; + begin + AddUnsupportedWarehouseSetting(UnsupportedWarehouseSettings, Location."Bin Mandatory", Location.FieldCaption("Bin Mandatory")); + AddUnsupportedWarehouseSetting(UnsupportedWarehouseSettings, Location."Require Pick", Location.FieldCaption("Require Pick")); + AddUnsupportedWarehouseSetting(UnsupportedWarehouseSettings, Location."Require Put-away", Location.FieldCaption("Require Put-away")); + AddUnsupportedWarehouseSetting(UnsupportedWarehouseSettings, Location."Require Receive", Location.FieldCaption("Require Receive")); + AddUnsupportedWarehouseSetting(UnsupportedWarehouseSettings, Location."Require Shipment", Location.FieldCaption("Require Shipment")); + exit(UnsupportedWarehouseSettings); + end; + + local procedure AddUnsupportedWarehouseSetting(var UnsupportedWarehouseSettings: Text; IsEnabled: Boolean; WarehouseSettingCaption: Text) + begin + if not IsEnabled then + exit; + + if UnsupportedWarehouseSettings <> '' then + UnsupportedWarehouseSettings += ', '; + UnsupportedWarehouseSettings += WarehouseSettingCaption; + end; + local procedure VerifyMigration() var TransferLine: Record "Transfer Line"; @@ -658,6 +720,7 @@ codeunit 149951 "IT Subc. Migration" VerifyingPhaseLbl: Label 'Verifying migration...'; VerifyingProgressEntityLbl: Label 'Verification step'; MigrationVerificationFailedErr: Label 'Migration verification failed for %1: expected %2 record(s) but found %3 after migration.', Comment = '%1 = entity name, %2 = pre-migration count, %3 = post-migration count'; + UnsupportedSubcontractingLocationErr: Label 'Migration can''t start because subcontracting location %1 uses unsupported warehouse settings: %2. Update the location or subcontracting setup, and then run the precheck again.', Comment = '%1 = location code, %2 = unsupported warehouse settings'; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Legacy Subc. Feature Handler", 'OnMigrationSubcontractingData', '', false, false)] local procedure MigrateSubconOnMigrationSubcontractingData() diff --git a/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al b/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al index 0eb2157044b..7a659b40847 100644 --- a/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al +++ b/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al @@ -33,6 +33,7 @@ codeunit 149956 "IT Subc. Migration Tests" LibraryWarehouse: Codeunit "Library - Warehouse"; LibraryUtility: Codeunit "Library - Utility"; Initialized: Boolean; + UnsupportedSubcontractingLocationErr: Label 'Migration can''t start because subcontracting location %1 uses unsupported warehouse settings: %2. Update the location or subcontracting setup, and then run the precheck again.', Comment = '%1 = location code, %2 = unsupported warehouse settings'; [Test] [Scope('OnPrem')] @@ -828,6 +829,108 @@ codeunit 149956 "IT Subc. Migration Tests" 'Routing line "Transfer WIP Item" should be migrated.'); end; + [Test] + [ErrorBehavior(ErrorBehavior::Collect)] + [Scope('OnPrem')] + procedure CheckSubcontractingLocations_ReportsEveryLocationAndSetting() + var + Vendor: Record Vendor; + PurchaseHeader: Record "Purchase Header"; + VendorLocation: Record Location; + PurchaseLocation: Record Location; + ITSubcMigration: Codeunit "IT Subc. Migration"; + CollectedErrors: List of [ErrorInfo]; + begin + // [SCENARIO] The migration precheck reports every incompatible location and its unsupported warehouse settings + Initialize(); + + // [GIVEN] A vendor whose legacy subcontracting location requires bins and picks + LibraryWarehouse.CreateLocation(VendorLocation); + VendorLocation."Bin Mandatory" := true; + VendorLocation."Require Pick" := true; + VendorLocation.Modify(false); + LibraryPurchase.CreateVendor(Vendor); + Vendor."Subcontracting Location Code" := VendorLocation.Code; + Vendor.Modify(false); + + // [GIVEN] A purchase header with a different legacy location that requires warehouse handling + LibraryWarehouse.CreateLocation(PurchaseLocation); + PurchaseLocation."Require Put-away" := true; + PurchaseLocation."Require Receive" := true; + PurchaseLocation."Require Shipment" := true; + PurchaseLocation.Modify(false); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); + PurchaseHeader."Subcontracting Location Code" := PurchaseLocation.Code; + PurchaseHeader.Modify(false); + + // [WHEN] The subcontracting location precheck runs + ITSubcMigration.CheckSubcontractingLocations(); + + // [THEN] One error per incompatible location identifies every unsupported setting + CollectedErrors := GetCollectedErrors(true); + Assert.AreEqual(2, CollectedErrors.Count, 'The precheck should report every incompatible subcontracting location.'); + AssertCollectedError( + CollectedErrors, + StrSubstNo( + UnsupportedSubcontractingLocationErr, + VendorLocation.Code, + 'Bin Mandatory, Require Pick')); + AssertCollectedError( + CollectedErrors, + StrSubstNo( + UnsupportedSubcontractingLocationErr, + PurchaseLocation.Code, + 'Require Put-away, Require Receive, Require Shipment')); + end; + + [Test] + [Scope('OnPrem')] + procedure StartDisableLegacySubcontracting_BlocksUnsupportedLocationBeforeMigration() + var + Vendor: Record Vendor; + Location: Record Location; + TransferLine: Record "Transfer Line"; + PurchaseLine: Record "Purchase Line"; + ITSubcMigration: Codeunit "IT Subc. Migration"; + begin + // [SCENARIO] Disabling legacy subcontracting stops before migration when a location has unsupported warehouse settings + Initialize(); + + // [GIVEN] No open WIP transfers or purchase orders + TransferLine.SetRange("WIP Item", true); + if not TransferLine.IsEmpty() then + TransferLine.DeleteAll(); + PurchaseLine.SetRange("Document Type", PurchaseLine."Document Type"::Order); +#pragma warning disable AL0432 + PurchaseLine.SetRange("WIP Item", true); +#pragma warning restore AL0432 + if not PurchaseLine.IsEmpty() then + PurchaseLine.DeleteAll(); + + // [GIVEN] A vendor with an unmigrated legacy subcontracting location that requires bins + LibraryWarehouse.CreateLocation(Location); + Location."Bin Mandatory" := true; + Location.Modify(false); + LibraryPurchase.CreateVendor(Vendor); + Vendor."Subcontracting Location Code" := Location.Code; + Vendor."Subc. Location Code" := ''; + Vendor.Modify(false); + + // [WHEN] Legacy subcontracting is disabled + asserterror ITSubcMigration.StartDisableLegacySubcontracting(false); + + // [THEN] The precheck reports the incompatible location + Assert.ExpectedError( + StrSubstNo( + UnsupportedSubcontractingLocationErr, + Location.Code, + 'Bin Mandatory')); + + // [THEN] Migration has not changed the vendor + Vendor.Get(Vendor."No."); + Assert.AreEqual('', Vendor."Subc. Location Code", 'The vendor must not be migrated when the precheck fails.'); + end; + [Test] [HandlerFunctions('ConfirmHandlerReturnFalse')] [Scope('OnPrem')] @@ -995,6 +1098,17 @@ codeunit 149956 "IT Subc. Migration Tests" #pragma warning restore AA0233 end; + local procedure AssertCollectedError(CollectedErrors: List of [ErrorInfo]; ExpectedMessage: Text) + var + CollectedError: ErrorInfo; + begin + foreach CollectedError in CollectedErrors do + if CollectedError.Message = ExpectedMessage then + exit; + + Error('Expected collected error was not found: %1', ExpectedMessage); + end; + local procedure ActivateLegacySubcontracting() var ManufacturingSetup: Record "Manufacturing Setup"; From a1f4be2f23f927401e51c14821dbbba8bc200969 Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah <41570277+ChethanT@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:09:17 +0200 Subject: [PATCH 2/6] [IT] Handle collected subcontracting precheck errors explicitly The migration precheck marked CheckSubcontractingLocations with ErrorBehavior::Collect but never handled the collection, so it fell back to the platform's concatenated error dialog (a BCQuality error-handling anti-pattern). After collecting every unsupported subcontracting location, inspect HasCollectedErrors, retrieve and clear the list with GetCollectedErrors(true), and raise a single deliberate blocking error. Update the precheck test to assert the aggregated blocking error via asserterror/GetLastErrorText. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../App/src/ITSubcMigration.Codeunit.al | 10 ++++ .../Test/src/ITSubcMigrationTests.Codeunit.al | 47 +++++++------------ 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al b/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al index 86a3fa07a67..4bdbbc4e10c 100644 --- a/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al +++ b/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al @@ -584,6 +584,9 @@ codeunit 149951 "IT Subc. Migration" LegacySubcontractingLocations: Dictionary of [Code[10], Boolean]; LocationCode: Code[10]; UnsupportedWarehouseSettings: Text; + CollectedErrors: List of [ErrorInfo]; + CollectedError: ErrorInfo; + BlockingErrorText: Text; begin SetVendorMigrationFilters(Vendor); Vendor.SetLoadFields("Subcontracting Location Code"); @@ -605,6 +608,13 @@ codeunit 149951 "IT Subc. Migration" if UnsupportedWarehouseSettings <> '' then Error(UnsupportedSubcontractingLocationErr, Location.Code, UnsupportedWarehouseSettings); end; + + if HasCollectedErrors() then begin + CollectedErrors := GetCollectedErrors(true); + foreach CollectedError in CollectedErrors do + BlockingErrorText += CollectedError.Message() + '\'; + Error(ErrorInfo.Create(BlockingErrorText, false)); + end; end; local procedure AddLegacySubcontractingLocation(var LegacySubcontractingLocations: Dictionary of [Code[10], Boolean]; LocationCode: Code[10]) diff --git a/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al b/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al index 7a659b40847..7771a9db3f6 100644 --- a/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al +++ b/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al @@ -830,7 +830,6 @@ codeunit 149956 "IT Subc. Migration Tests" end; [Test] - [ErrorBehavior(ErrorBehavior::Collect)] [Scope('OnPrem')] procedure CheckSubcontractingLocations_ReportsEveryLocationAndSetting() var @@ -839,7 +838,7 @@ codeunit 149956 "IT Subc. Migration Tests" VendorLocation: Record Location; PurchaseLocation: Record Location; ITSubcMigration: Codeunit "IT Subc. Migration"; - CollectedErrors: List of [ErrorInfo]; + BlockingError: Text; begin // [SCENARIO] The migration precheck reports every incompatible location and its unsupported warehouse settings Initialize(); @@ -864,23 +863,24 @@ codeunit 149956 "IT Subc. Migration Tests" PurchaseHeader.Modify(false); // [WHEN] The subcontracting location precheck runs - ITSubcMigration.CheckSubcontractingLocations(); + asserterror ITSubcMigration.CheckSubcontractingLocations(); - // [THEN] One error per incompatible location identifies every unsupported setting - CollectedErrors := GetCollectedErrors(true); - Assert.AreEqual(2, CollectedErrors.Count, 'The precheck should report every incompatible subcontracting location.'); - AssertCollectedError( - CollectedErrors, - StrSubstNo( - UnsupportedSubcontractingLocationErr, - VendorLocation.Code, - 'Bin Mandatory, Require Pick')); - AssertCollectedError( - CollectedErrors, - StrSubstNo( - UnsupportedSubcontractingLocationErr, - PurchaseLocation.Code, - 'Require Put-away, Require Receive, Require Shipment')); + // [THEN] The blocking error reports every incompatible location and its unsupported settings + BlockingError := GetLastErrorText(); + Assert.IsTrue( + BlockingError.Contains( + StrSubstNo( + UnsupportedSubcontractingLocationErr, + VendorLocation.Code, + 'Bin Mandatory, Require Pick')), + 'The precheck should report the vendor location and its unsupported settings.'); + Assert.IsTrue( + BlockingError.Contains( + StrSubstNo( + UnsupportedSubcontractingLocationErr, + PurchaseLocation.Code, + 'Require Put-away, Require Receive, Require Shipment')), + 'The precheck should report the purchase location and its unsupported settings.'); end; [Test] @@ -1098,17 +1098,6 @@ codeunit 149956 "IT Subc. Migration Tests" #pragma warning restore AA0233 end; - local procedure AssertCollectedError(CollectedErrors: List of [ErrorInfo]; ExpectedMessage: Text) - var - CollectedError: ErrorInfo; - begin - foreach CollectedError in CollectedErrors do - if CollectedError.Message = ExpectedMessage then - exit; - - Error('Expected collected error was not found: %1', ExpectedMessage); - end; - local procedure ActivateLegacySubcontracting() var ManufacturingSetup: Record "Manufacturing Setup"; From 4d949e9ad79a751fde5bb9d5289d8b032614f4a7 Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah Date: Sun, 6 Sep 2026 10:06:59 +0200 Subject: [PATCH 3/6] Fix subcontracting migration precheck race Revalidate legacy locations under table locks, report orphaned references safely, and add focused regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../App/src/ITSubcMigration.Codeunit.al | 31 ++++++++++--- .../Test/src/ITSubcMigrationTests.Codeunit.al | 46 +++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al b/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al index 4bdbbc4e10c..a827fd68f24 100644 --- a/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al +++ b/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al @@ -51,6 +51,7 @@ codeunit 149951 "IT Subc. Migration" LockTables(); Clear(PreMigrationCounts); + CheckSubcontractingLocations(); RunMigration(); if UIAllowed then @@ -443,6 +444,7 @@ codeunit 149951 "IT Subc. Migration" RoutingLine: Record "Routing Line"; Vendor: Record Vendor; PurchaseHeader: Record "Purchase Header"; + Location: Record Location; #pragma warning disable AL0432 LegacySubcontractorPrice: Record "Subcontractor Prices"; #pragma warning restore AL0432 @@ -457,6 +459,7 @@ codeunit 149951 "IT Subc. Migration" RoutingLine.LockTable(); Vendor.LockTable(); PurchaseHeader.LockTable(); + Location.LockTable(); LegacySubcontractorPrice.LockTable(); SubcontractorPrice.LockTable(); ManufacturingSetup.LockTable(); @@ -586,6 +589,7 @@ codeunit 149951 "IT Subc. Migration" UnsupportedWarehouseSettings: Text; CollectedErrors: List of [ErrorInfo]; CollectedError: ErrorInfo; + BlockingError: ErrorInfo; BlockingErrorText: Text; begin SetVendorMigrationFilters(Vendor); @@ -602,18 +606,30 @@ codeunit 149951 "IT Subc. Migration" AddLegacySubcontractingLocation(LegacySubcontractingLocations, PurchaseHeader."Subcontracting Location Code"); until PurchaseHeader.Next() = 0; - foreach LocationCode in LegacySubcontractingLocations.Keys() do begin - Location.Get(LocationCode); - UnsupportedWarehouseSettings := GetUnsupportedWarehouseSettings(Location); - if UnsupportedWarehouseSettings <> '' then - Error(UnsupportedSubcontractingLocationErr, Location.Code, UnsupportedWarehouseSettings); - end; + Location.SetLoadFields( + "Bin Mandatory", + "Require Pick", + "Require Put-away", + "Require Receive", + "Require Shipment"); + foreach LocationCode in LegacySubcontractingLocations.Keys() do + if not Location.Get(LocationCode) then + Error(MissingSubcontractingLocationErr, LocationCode) + else begin + UnsupportedWarehouseSettings := GetUnsupportedWarehouseSettings(Location); + if UnsupportedWarehouseSettings <> '' then + Error(UnsupportedSubcontractingLocationErr, Location.Code, UnsupportedWarehouseSettings); + end; if HasCollectedErrors() then begin CollectedErrors := GetCollectedErrors(true); foreach CollectedError in CollectedErrors do BlockingErrorText += CollectedError.Message() + '\'; - Error(ErrorInfo.Create(BlockingErrorText, false)); + BlockingError.Message := BlockingErrorText; + BlockingError.DataClassification := DataClassification::CustomerContent; + BlockingError.ErrorType := ErrorType::Client; + BlockingError.Collectible := false; + Error(BlockingError); end; end; @@ -731,6 +747,7 @@ codeunit 149951 "IT Subc. Migration" VerifyingProgressEntityLbl: Label 'Verification step'; MigrationVerificationFailedErr: Label 'Migration verification failed for %1: expected %2 record(s) but found %3 after migration.', Comment = '%1 = entity name, %2 = pre-migration count, %3 = post-migration count'; UnsupportedSubcontractingLocationErr: Label 'Migration can''t start because subcontracting location %1 uses unsupported warehouse settings: %2. Update the location or subcontracting setup, and then run the precheck again.', Comment = '%1 = location code, %2 = unsupported warehouse settings'; + MissingSubcontractingLocationErr: Label 'Migration can''t start because legacy subcontracting data references location %1, but that location doesn''t exist. Update the legacy vendor or purchase document, and then run the precheck again.', Comment = '%1 = location code'; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Legacy Subc. Feature Handler", 'OnMigrationSubcontractingData', '', false, false)] local procedure MigrateSubconOnMigrationSubcontractingData() diff --git a/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al b/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al index 7771a9db3f6..3e312cf8b7e 100644 --- a/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al +++ b/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al @@ -34,6 +34,7 @@ codeunit 149956 "IT Subc. Migration Tests" LibraryUtility: Codeunit "Library - Utility"; Initialized: Boolean; UnsupportedSubcontractingLocationErr: Label 'Migration can''t start because subcontracting location %1 uses unsupported warehouse settings: %2. Update the location or subcontracting setup, and then run the precheck again.', Comment = '%1 = location code, %2 = unsupported warehouse settings'; + MissingSubcontractingLocationErr: Label 'Migration can''t start because legacy subcontracting data references location %1, but that location doesn''t exist. Update the legacy vendor or purchase document, and then run the precheck again.', Comment = '%1 = location code'; [Test] [Scope('OnPrem')] @@ -883,6 +884,50 @@ codeunit 149956 "IT Subc. Migration Tests" 'The precheck should report the purchase location and its unsupported settings.'); end; + [Test] + [Scope('OnPrem')] + procedure CheckSubcontractingLocations_ReportsMissingAndUnsupportedLegacyLocations() + var + Vendor: Record Vendor; + PurchaseHeader: Record "Purchase Header"; + MissingLocation: Record Location; + UnsupportedLocation: Record Location; + ITSubcMigration: Codeunit "IT Subc. Migration"; + BlockingError: Text; + begin + // [SCENARIO] The migration precheck aggregates missing and unsupported legacy subcontracting locations + Initialize(); + + // [GIVEN] A vendor whose legacy subcontracting location references a deleted location + LibraryWarehouse.CreateLocation(MissingLocation); + LibraryPurchase.CreateVendor(Vendor); + Vendor."Subcontracting Location Code" := MissingLocation.Code; + Vendor.Modify(false); + MissingLocation.Delete(false); + + // [GIVEN] A purchase header whose legacy subcontracting location requires bins + LibraryWarehouse.CreateLocation(UnsupportedLocation); + UnsupportedLocation."Bin Mandatory" := true; + UnsupportedLocation.Modify(false); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Invoice, Vendor."No."); + PurchaseHeader."Subcontracting Location Code" := UnsupportedLocation.Code; + PurchaseHeader.Modify(false); + Commit(); + + // [WHEN] The subcontracting location precheck runs + asserterror ITSubcMigration.CheckSubcontractingLocations(); + + // [THEN] The blocking error reports both legacy location problems + BlockingError := GetLastErrorText(); + Assert.IsTrue( + BlockingError.Contains(StrSubstNo(MissingSubcontractingLocationErr, MissingLocation.Code)), + 'The precheck should report the missing legacy location.'); + Assert.IsTrue( + BlockingError.Contains( + StrSubstNo(UnsupportedSubcontractingLocationErr, UnsupportedLocation.Code, 'Bin Mandatory')), + 'The precheck should continue and report the unsupported legacy location.'); + end; + [Test] [Scope('OnPrem')] procedure StartDisableLegacySubcontracting_BlocksUnsupportedLocationBeforeMigration() @@ -915,6 +960,7 @@ codeunit 149956 "IT Subc. Migration Tests" Vendor."Subcontracting Location Code" := Location.Code; Vendor."Subc. Location Code" := ''; Vendor.Modify(false); + Commit(); // [WHEN] Legacy subcontracting is disabled asserterror ITSubcMigration.StartDisableLegacySubcontracting(false); From 81139a798a89af8d56b224d08a2dd709a7611813 Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah Date: Sun, 6 Sep 2026 13:16:08 +0200 Subject: [PATCH 4/6] Fix subcontracting precheck error aggregation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../App/src/ITSubcMigration.Codeunit.al | 11 ++++++----- .../Test/src/ITSubcMigrationTests.Codeunit.al | 3 +++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al b/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al index a827fd68f24..ec44fd161e9 100644 --- a/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al +++ b/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al @@ -590,7 +590,7 @@ codeunit 149951 "IT Subc. Migration" CollectedErrors: List of [ErrorInfo]; CollectedError: ErrorInfo; BlockingError: ErrorInfo; - BlockingErrorText: Text; + BlockingErrorTextBuilder: TextBuilder; begin SetVendorMigrationFilters(Vendor); Vendor.SetLoadFields("Subcontracting Location Code"); @@ -614,18 +614,18 @@ codeunit 149951 "IT Subc. Migration" "Require Shipment"); foreach LocationCode in LegacySubcontractingLocations.Keys() do if not Location.Get(LocationCode) then - Error(MissingSubcontractingLocationErr, LocationCode) + Error(ErrorInfo.Create(StrSubstNo(MissingSubcontractingLocationErr, LocationCode), true)) else begin UnsupportedWarehouseSettings := GetUnsupportedWarehouseSettings(Location); if UnsupportedWarehouseSettings <> '' then - Error(UnsupportedSubcontractingLocationErr, Location.Code, UnsupportedWarehouseSettings); + Error(ErrorInfo.Create(StrSubstNo(UnsupportedSubcontractingLocationErr, Location.Code, UnsupportedWarehouseSettings), true)); end; if HasCollectedErrors() then begin CollectedErrors := GetCollectedErrors(true); foreach CollectedError in CollectedErrors do - BlockingErrorText += CollectedError.Message() + '\'; - BlockingError.Message := BlockingErrorText; + BlockingErrorTextBuilder.AppendLine(CollectedError.Message()); + BlockingError.Message := StrSubstNo(SubcontractingLocationsBlockedErr, BlockingErrorTextBuilder.ToText()); BlockingError.DataClassification := DataClassification::CustomerContent; BlockingError.ErrorType := ErrorType::Client; BlockingError.Collectible := false; @@ -746,6 +746,7 @@ codeunit 149951 "IT Subc. Migration" VerifyingPhaseLbl: Label 'Verifying migration...'; VerifyingProgressEntityLbl: Label 'Verification step'; MigrationVerificationFailedErr: Label 'Migration verification failed for %1: expected %2 record(s) but found %3 after migration.', Comment = '%1 = entity name, %2 = pre-migration count, %3 = post-migration count'; + SubcontractingLocationsBlockedErr: Label 'Migration can''t start because one or more subcontracting locations are invalid. Resolve the following issues and run the precheck again:\%1', Comment = '%1 = detailed location validation errors'; UnsupportedSubcontractingLocationErr: Label 'Migration can''t start because subcontracting location %1 uses unsupported warehouse settings: %2. Update the location or subcontracting setup, and then run the precheck again.', Comment = '%1 = location code, %2 = unsupported warehouse settings'; MissingSubcontractingLocationErr: Label 'Migration can''t start because legacy subcontracting data references location %1, but that location doesn''t exist. Update the legacy vendor or purchase document, and then run the precheck again.', Comment = '%1 = location code'; diff --git a/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al b/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al index 3e312cf8b7e..d120bd5bbd5 100644 --- a/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al +++ b/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al @@ -33,6 +33,7 @@ codeunit 149956 "IT Subc. Migration Tests" LibraryWarehouse: Codeunit "Library - Warehouse"; LibraryUtility: Codeunit "Library - Utility"; Initialized: Boolean; + SubcontractingLocationsBlockedErr: Label 'Migration can''t start because one or more subcontracting locations are invalid.'; UnsupportedSubcontractingLocationErr: Label 'Migration can''t start because subcontracting location %1 uses unsupported warehouse settings: %2. Update the location or subcontracting setup, and then run the precheck again.', Comment = '%1 = location code, %2 = unsupported warehouse settings'; MissingSubcontractingLocationErr: Label 'Migration can''t start because legacy subcontracting data references location %1, but that location doesn''t exist. Update the legacy vendor or purchase document, and then run the precheck again.', Comment = '%1 = location code'; @@ -865,6 +866,7 @@ codeunit 149956 "IT Subc. Migration Tests" // [WHEN] The subcontracting location precheck runs asserterror ITSubcMigration.CheckSubcontractingLocations(); + Assert.ExpectedError(SubcontractingLocationsBlockedErr); // [THEN] The blocking error reports every incompatible location and its unsupported settings BlockingError := GetLastErrorText(); @@ -916,6 +918,7 @@ codeunit 149956 "IT Subc. Migration Tests" // [WHEN] The subcontracting location precheck runs asserterror ITSubcMigration.CheckSubcontractingLocations(); + Assert.ExpectedError(SubcontractingLocationsBlockedErr); // [THEN] The blocking error reports both legacy location problems BlockingError := GetLastErrorText(); From 7153bde1a291ab209ef463af3e03edf9110fc400 Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah Date: Sun, 6 Sep 2026 13:33:17 +0200 Subject: [PATCH 5/6] Use localized captions in migration tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Test/src/ITSubcMigrationTests.Codeunit.al | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al b/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al index d120bd5bbd5..e7c1a868eda 100644 --- a/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al +++ b/src/Apps/IT/SubcontractingMigrationIT/Test/src/ITSubcMigrationTests.Codeunit.al @@ -875,14 +875,16 @@ codeunit 149956 "IT Subc. Migration Tests" StrSubstNo( UnsupportedSubcontractingLocationErr, VendorLocation.Code, - 'Bin Mandatory, Require Pick')), + VendorLocation.FieldCaption("Bin Mandatory") + ', ' + VendorLocation.FieldCaption("Require Pick"))), 'The precheck should report the vendor location and its unsupported settings.'); Assert.IsTrue( BlockingError.Contains( StrSubstNo( UnsupportedSubcontractingLocationErr, PurchaseLocation.Code, - 'Require Put-away, Require Receive, Require Shipment')), + PurchaseLocation.FieldCaption("Require Put-away") + ', ' + + PurchaseLocation.FieldCaption("Require Receive") + ', ' + + PurchaseLocation.FieldCaption("Require Shipment"))), 'The precheck should report the purchase location and its unsupported settings.'); end; @@ -927,7 +929,10 @@ codeunit 149956 "IT Subc. Migration Tests" 'The precheck should report the missing legacy location.'); Assert.IsTrue( BlockingError.Contains( - StrSubstNo(UnsupportedSubcontractingLocationErr, UnsupportedLocation.Code, 'Bin Mandatory')), + StrSubstNo( + UnsupportedSubcontractingLocationErr, + UnsupportedLocation.Code, + UnsupportedLocation.FieldCaption("Bin Mandatory"))), 'The precheck should continue and report the unsupported legacy location.'); end; @@ -973,7 +978,7 @@ codeunit 149956 "IT Subc. Migration Tests" StrSubstNo( UnsupportedSubcontractingLocationErr, Location.Code, - 'Bin Mandatory')); + Location.FieldCaption("Bin Mandatory"))); // [THEN] Migration has not changed the vendor Vendor.Get(Vendor."No."); From aa3ab81b72989610717867534ecb5fb04c52f47b Mon Sep 17 00:00:00 2001 From: Chethan Thopaiah Date: Sun, 6 Sep 2026 22:02:27 +0200 Subject: [PATCH 6/6] Document authoritative migration precheck Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../App/src/ITSubcMigration.Codeunit.al | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al b/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al index ec44fd161e9..572446f70fe 100644 --- a/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al +++ b/src/Apps/IT/SubcontractingMigrationIT/App/src/ITSubcMigration.Codeunit.al @@ -42,6 +42,7 @@ codeunit 149951 "IT Subc. Migration" #if not CLEAN28 LegacySubcFeatureHandler.CheckCanDisableLegacySubcontracting(); #endif + // Validate before confirmation for prompt feedback, then repeat under locks to prevent concurrent changes. CheckSubcontractingLocations(); UIAllowed := ShowDialog and GuiAllowed(); if UIAllowed then begin @@ -51,6 +52,7 @@ codeunit 149951 "IT Subc. Migration" LockTables(); Clear(PreMigrationCounts); + // This authoritative validation covers changes made by other sessions while confirmation was pending. CheckSubcontractingLocations(); RunMigration();