diff --git a/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutEditDialog.page.al b/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutEditDialog.page.al
index e306714a899..15423b2c6a6 100644
--- a/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutEditDialog.page.al
+++ b/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutEditDialog.page.al
@@ -40,6 +40,7 @@ page 9661 "Report Layout Edit Dialog"
ApplicationArea = Basic, Suite;
NotBlank = true;
ShowMandatory = true;
+ Editable = LayoutNameEditable;
Caption = 'Layout Name';
ToolTip = 'Specifies the name of the layout.';
@@ -74,16 +75,27 @@ page 9661 "Report Layout Edit Dialog"
trigger OnValidate()
begin
- if (CreateCopy) then
- AvailableInAllCompaniesEditable := true
- else
- if (IsLayoutOwnedByCurrentCompany) then begin
- AvailableInAllCompaniesEditable := true;
- AvailableInAllCompanies := false;
- end else begin
- AvailableInAllCompaniesEditable := false;
+ if OverrideMode then begin
+ LayoutNameEditable := CreateCopy;
+ AvailableInAllCompaniesEditable := CreateCopy;
+ IsObsoleteEditable := CreateCopy or (not ObsoleteInMetadata);
+ if not CreateCopy then begin
+ // Back to an in-place override: restore the values it will actually write,
+ // so a locked field never displays something the write contradicts.
AvailableInAllCompanies := true;
+ IsObsolete := ObsoleteInMetadata;
end;
+ end else
+ if CreateCopy then
+ AvailableInAllCompaniesEditable := true
+ else
+ if IsLayoutOwnedByCurrentCompany then begin
+ AvailableInAllCompaniesEditable := true;
+ AvailableInAllCompanies := false;
+ end else begin
+ AvailableInAllCompaniesEditable := false;
+ AvailableInAllCompanies := true;
+ end;
end;
}
field(AvailableInAllCompanies; AvailableInAllCompanies)
@@ -98,7 +110,7 @@ page 9661 "Report Layout Edit Dialog"
ApplicationArea = Basic, Suite;
Caption = 'Mark layout as obsolete';
ToolTip = 'Specifies whether the layout is obsolete.';
- Editable = true;
+ Editable = IsObsoleteEditable;
}
}
}
@@ -119,6 +131,10 @@ page 9661 "Report Layout Edit Dialog"
AvailableInAllCompaniesEditable: Boolean;
IsLayoutOwnedByCurrentCompany: Boolean;
IsObsolete: Boolean;
+ LayoutNameEditable: Boolean;
+ IsObsoleteEditable: Boolean;
+ OverrideMode: Boolean;
+ ObsoleteInMetadata: Boolean;
internal procedure SelectedLayoutDescription(): Text[250]
begin
@@ -153,13 +169,20 @@ page 9661 "Report Layout Edit Dialog"
OldLayoutName := ReportLayoutList."Caption";
NewLayoutName := OldLayoutName;
IsObsolete := ReportLayoutList.IsObsolete;
+ LayoutNameEditable := true;
+ IsObsoleteEditable := true;
+ OverrideMode := false;
if not ReportLayoutList."User Defined" then begin
- CreateCopy := true;
- CreateCopyEditable := false;
- AvailableInAllCompaniesEditable := true;
- AvailableInAllCompanies := true;
+ OverrideMode := true;
+ ObsoleteInMetadata := ReportLayoutList.IsObsolete;
+ CreateCopy := false;
+ CreateCopyEditable := true;
+ LayoutNameEditable := false;
+ IsObsoleteEditable := not ObsoleteInMetadata;
+ AvailableInAllCompanies := true;
+ AvailableInAllCompaniesEditable := false;
end else begin
CreateCopy := false;
CreateCopyEditable := true;
diff --git a/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayouts.page.al b/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayouts.page.al
index 45a019caaaa..81fcbbed7dc 100644
--- a/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayouts.page.al
+++ b/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayouts.page.al
@@ -236,11 +236,7 @@ page 9660 "Report Layouts"
var
NewEditedLayoutName: Text;
begin
- if not Rec."User Defined" then begin
- if Dialog.Confirm(EditInfoExtensionLayoutTxt, false) then
- ReportLayoutsImpl.EditReportLayout(Rec, NewEditedLayoutName);
- end else
- ReportLayoutsImpl.EditReportLayout(Rec, NewEditedLayoutName);
+ ReportLayoutsImpl.EditReportLayout(Rec, NewEditedLayoutName);
SetFocusedRecord(Rec."Report ID", NewEditedLayoutName);
end;
}
@@ -477,11 +473,13 @@ page 9660 "Report Layouts"
}
}
+ // BaseSystemPermissionSet grants Tenant Report Layout and its override table together, so
+ // gating on Tenant Report Layout = M covers both.
action(SetApproved)
{
ApplicationArea = Basic, Suite;
Caption = 'Set Approved';
- ToolTip = 'Mark the selected user-defined layouts as approved. Only approved layouts are available for selection on report request pages.';
+ ToolTip = 'Mark the selected layouts as approved. Only approved layouts are available for selection on report request pages.';
Image = Approve;
Enabled = CanModifyStatus;
AccessByPermission = tabledata "Tenant Report Layout" = M;
@@ -495,7 +493,7 @@ page 9660 "Report Layouts"
{
ApplicationArea = Basic, Suite;
Caption = 'Set Draft';
- ToolTip = 'Mark the selected user-defined layouts as draft. Draft layouts are not available for selection on report request pages.';
+ ToolTip = 'Mark the selected layouts as draft. Draft layouts are not available for selection on report request pages.';
Image = OpenWorksheet;
Enabled = CanModifyStatus;
AccessByPermission = tabledata "Tenant Report Layout" = M;
@@ -509,7 +507,7 @@ page 9660 "Report Layouts"
{
ApplicationArea = Basic, Suite;
Caption = 'Set Pending Approval';
- ToolTip = 'Mark the selected user-defined layouts as pending approval. Pending layouts are not available for selection on report request pages.';
+ ToolTip = 'Mark the selected layouts as pending approval. Pending layouts are not available for selection on report request pages.';
Image = AddWatch;
Enabled = CanModifyStatus;
AccessByPermission = tabledata "Tenant Report Layout" = M;
@@ -523,7 +521,7 @@ page 9660 "Report Layouts"
{
ApplicationArea = Basic, Suite;
Caption = 'Set Retired';
- ToolTip = 'Mark the selected user-defined layouts as retired. Retired layouts are not available for selection on report request pages.';
+ ToolTip = 'Mark the selected layouts as retired. Retired layouts are not available for selection on report request pages.';
Image = Archive;
Enabled = CanModifyStatus;
AccessByPermission = tabledata "Tenant Report Layout" = M;
@@ -728,9 +726,7 @@ page 9660 "Report Layouts"
IsMultiSelect := SelectedReportLayoutList.Count() > 1;
ShareOptionsVisible := DocumentSharing.ShareEnabled(Enum::"Document Sharing Source"::System);
ShareOptionsEnabled := LayoutIsSelected and (not IsMultiSelect) and Rec."User Defined" and (Rec."Layout Format" <> Rec."Layout Format"::RDLC);
- SelectedReportLayoutList.SetRange("User Defined", true);
- CanModifyStatus := LayoutIsSelected and not SelectedReportLayoutList.IsEmpty();
- SelectedReportLayoutList.SetRange("User Defined");
+ CanModifyStatus := LayoutIsSelected;
UpdateUserDisplayName();
end;
@@ -776,7 +772,6 @@ page 9660 "Report Layouts"
DocumentReportExperienceEnabled: Boolean;
WordLayoutSelected: Boolean;
ModifyNonUserLayoutErr: Label 'Only user-defined layouts can be modified or removed.';
- EditInfoExtensionLayoutTxt: Label 'It is not possible to modify the layout info for this layout because it is provided by an extension. Do you want to edit a copy of the layout instead ?';
ReplaceConfirmationTxt: Label 'This action will replace the layout file of the currently selected layout "%1". Do you want to continue ?', Comment = '%1 = LayoutName';
LayoutStatusChangedMsg: Label '%1 layout(s) set to %2.', Comment = '%1 = Number of layouts updated, %2 = Status name';
DeletePartWithReferencesQst: Label 'Layout part "%1" is referenced in the Tenant Report Layout Configuration. Deleting it will clear those references and may result in reports rendering without the expected header/footer or theme. Do you want to continue?', Comment = '%1 = Layout Name';
diff --git a/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al b/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al
index 56abe24440c..af2a35a84a5 100644
--- a/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al
+++ b/src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al
@@ -21,7 +21,8 @@ codeunit 9660 "Report Layouts Impl."
{
Access = Internal;
Permissions = tabledata "Tenant Report Layout" = rimd,
- tabledata "Tenant Report Layout Selection" = rimd;
+ tabledata "Tenant Report Layout Selection" = rimd,
+ tabledata "Tenant Report Layout Override" = rimd;
var
TenantReportLayoutSelection: Record "Tenant Report Layout Selection";
@@ -43,26 +44,36 @@ codeunit 9660 "Report Layouts Impl."
EmptyLayoutNameTxt: Label 'A layout name must be specified.';
CannotUpdateLayoutTxt: Label 'The Layout could not be updated for export. The exported file will contain the original layout.';
LayoutAlreadyExistsErr: Label 'A layout named "%1" already exists.', Comment = '%1 = Layout Name';
+ MixedScopeErr: Label 'The selected layouts have different scopes. Some apply to all companies and some only to the current company. Select layouts of a single scope and try again.';
internal procedure SetSelectedCompany(NewCompanyName: Text)
begin
SelectedCompany := CopyStr(NewCompanyName, 1, MaxStrLen(SelectedCompany));
end;
+ local procedure OverrideCompany(): Text[30]
+ begin
+ // Falls back: "Report Theme and Header/Footer" calls SetLayoutStatusBatch without SetSelectedCompany.
+ if SelectedCompany <> '' then
+ exit(SelectedCompany);
+ exit(CopyStr(CompanyName(), 1, MaxStrLen(SelectedCompany)));
+ end;
+
///
- /// Sets the status for a user-defined layout.
- /// Only user-defined layouts have entries in "Tenant Report Layout" (table 2000000232).
- /// Extension-defined layouts reside in the read-only App database and cannot be modified.
+ /// Sets the status for a layout. Extension-installed layouts live in the read-only App database,
+ /// so their status is written as an override record rather than by copying the layout.
///
/// The layout record from the virtual table
/// The new status to set
- /// True if the status was updated, false if the layout is not user-defined or not found
+ /// True if the status was updated, false if the user-defined layout was not found
internal procedure SetLayoutStatus(ReportLayoutList: Record "Report Layout List"; NewStatus: Enum "Report Layout Status"): Boolean
var
TenantReportLayout: Record "Tenant Report Layout";
begin
- if not ReportLayoutList."User Defined" then
- exit(false);
+ if not ReportLayoutList."User Defined" then begin
+ UpsertLayoutOverride(ReportLayoutList, LayoutStatusIsGlobalScope(ReportLayoutList), false, '', true, NewStatus, false, false);
+ exit(true);
+ end;
if TenantReportLayout.Get(ReportLayoutList."Report ID", ReportLayoutList."Name", EmptyGuid) then begin
TenantReportLayout."Layout Status" := NewStatus;
@@ -72,20 +83,92 @@ codeunit 9660 "Report Layouts Impl."
exit(false);
end;
+ local procedure LayoutStatusIsGlobalScope(ReportLayoutList: Record "Report Layout List"): Boolean
+ var
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ begin
+ // Field-granular: only an "Override Layout Status" row makes the change company-scoped, and the
+ // UI no longer writes one - these come from an earlier version or a vendor install codeunit.
+ if TenantReportLayoutOverride.Get(ReportLayoutList."Report ID", ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", OverrideCompany()) then
+ if TenantReportLayoutOverride."Override Layout Status" then
+ exit(false);
+
+ exit(true);
+ end;
+
+ local procedure UpsertLayoutOverride(ReportLayoutList: Record "Report Layout List"; MakeGlobal: Boolean; ApplyDescription: Boolean; NewDescription: Text[250]; ApplyStatus: Boolean; NewStatus: Enum "Report Layout Status"; ApplyObsolete: Boolean; NewIsObsolete: Boolean)
+ var
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ OverrideCompanyName: Text[30];
+ OverrideExists: Boolean;
+ begin
+ if MakeGlobal then
+ OverrideCompanyName := ''
+ else
+ OverrideCompanyName := OverrideCompany();
+
+ OverrideExists := TenantReportLayoutOverride.Get(ReportLayoutList."Report ID", ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", OverrideCompanyName);
+ if not OverrideExists then begin
+ TenantReportLayoutOverride.Init();
+ TenantReportLayoutOverride."Report ID" := ReportLayoutList."Report ID";
+ TenantReportLayoutOverride."Name" := ReportLayoutList."Name";
+ TenantReportLayoutOverride."Runtime Package ID" := ReportLayoutList."Runtime Package ID";
+ TenantReportLayoutOverride."Company Name" := OverrideCompanyName;
+ end;
+
+ if ApplyDescription then begin
+ TenantReportLayoutOverride.Description := NewDescription;
+ TenantReportLayoutOverride."Override Description" := true;
+ end;
+
+ if ApplyStatus then begin
+ TenantReportLayoutOverride."Layout Status" := NewStatus;
+ TenantReportLayoutOverride."Override Layout Status" := true;
+ end;
+
+ // One-way: only ever mark obsolete; never write false over a metadata-obsolete layout.
+ if ApplyObsolete and NewIsObsolete then begin
+ TenantReportLayoutOverride.IsObsolete := true;
+ TenantReportLayoutOverride."Override IsObsolete" := true;
+ end;
+
+ if OverrideExists then
+ TenantReportLayoutOverride.Modify(true)
+ else
+ TenantReportLayoutOverride.Insert(true);
+ end;
+
///
/// Sets the status for multiple selected layouts.
/// Use this from page actions when user selects multiple layouts.
///
/// Record set with selected layouts (filtered/marked)
/// The new status to set
- /// Number of layouts updated (excludes extension-defined layouts)
+ /// Number of layouts updated — user-defined layouts in place, extension-installed layouts
+ /// through a "Tenant Report Layout Override" record
internal procedure SetLayoutStatusBatch(var ReportLayoutList: Record "Report Layout List"; NewStatus: Enum "Report Layout Status"): Integer
var
CustomDimensions: Dictionary of [Text, Text];
UpdateCount: Integer;
+ HasGlobalScope: Boolean;
+ HasCompanyScope: Boolean;
begin
if not ReportLayoutList.FindSet() then
exit(0);
+
+ // First pass: classify scope. User-defined layouts update in place and do not affect it.
+ repeat
+ if not ReportLayoutList."User Defined" then
+ if LayoutStatusIsGlobalScope(ReportLayoutList) then
+ HasGlobalScope := true
+ else
+ HasCompanyScope := true;
+ until ReportLayoutList.Next() = 0;
+
+ if HasGlobalScope and HasCompanyScope then
+ Error(MixedScopeErr);
+
+ ReportLayoutList.FindSet();
repeat
if SetLayoutStatus(ReportLayoutList, NewStatus) then
UpdateCount += 1;
@@ -598,6 +681,8 @@ codeunit 9660 "Report Layouts Impl."
AllCompaniesTxt: Label '';
AvailableInAllCompanies: Boolean;
NewIsObsolete: Boolean;
+ ApplyDescription: Boolean;
+ ApplyObsolete: Boolean;
CustomDimensions: Dictionary of [Text, Text];
begin
if SelectedReportLayoutList."User Defined" then begin
@@ -607,7 +692,9 @@ codeunit 9660 "Report Layouts Impl."
CompanyName := SelectedCompany;
ReportLayoutEditDialog.SetupDialog(SelectedReportLayoutList, SelectedCompany);
- if ReportLayoutEditDialog.RunModal() = Action::OK then begin
+ begin
+ if ReportLayoutEditDialog.RunModal() <> Action::OK then
+ exit;
NewDescription := ReportLayoutEditDialog.SelectedLayoutDescription();
NewLayoutName := ReportLayoutEditDialog.SelectedLayoutName();
@@ -615,6 +702,28 @@ codeunit 9660 "Report Layouts Impl."
AvailableInAllCompanies := ReportLayoutEditDialog.SelectedAvailableInAllCompanies();
NewIsObsolete := ReportLayoutEditDialog.SelectedIsObsolete();
+ if (not SelectedReportLayoutList."User Defined") and (not CreateCopy) then begin
+ // All companies, which is what the read-only Yes in the dialog states.
+ AvailableInAllCompanies := true;
+
+ NewEditedLayoutName := SelectedReportLayoutList.Name;
+ ApplyDescription := NewDescription <> SelectedReportLayoutList."Description";
+ ApplyObsolete := NewIsObsolete and (not SelectedReportLayoutList.IsObsolete);
+ if not (ApplyDescription or ApplyObsolete) then
+ exit;
+
+ UpsertLayoutOverride(SelectedReportLayoutList, AvailableInAllCompanies, ApplyDescription, NewDescription, false, Enum::"Report Layout Status"::Draft, ApplyObsolete, NewIsObsolete);
+
+ CustomDimensions.Add('ReportId', Format(SelectedReportLayoutList."Report ID"));
+ CustomDimensions.Add('LayoutName', SelectedReportLayoutList.Name);
+ CustomDimensions.Add('DescriptionChanged', Format(ApplyDescription));
+ CustomDimensions.Add('ObsoleteSet', Format(ApplyObsolete));
+ CustomDimensions.Add('OverrideScope', 'AllCompanies');
+ AddReportLayoutDimensionsAction('EditOverride', CustomDimensions);
+ Log('0000RTQ', 'Report layout properties overridden by user', CustomDimensions);
+ exit;
+ end;
+
// Check if a layout having NewLayoutName already exists
if TenantReportLayout.Get(SelectedReportLayoutList."Report ID", NewLayoutName, EmptyGuid) then
if CreateCopy or (SelectedReportLayoutList.Name <> NewLayoutName) then
diff --git a/src/Layers/W1/Tests/Report/Layouts/TestReportLayoutsReport2.rdl b/src/Layers/W1/Tests/Report/Layouts/TestReportLayoutsReport2.rdl
new file mode 100644
index 00000000000..73b7fbaa064
--- /dev/null
+++ b/src/Layers/W1/Tests/Report/Layouts/TestReportLayoutsReport2.rdl
@@ -0,0 +1,77 @@
+
+
+ 0
+
+
+
+ SQL
+
+
+ None
+
+
+
+
+
+ 2in
+
+
+ 6.5in
+
+
+
+
+
+ Public Function BlankZero(ByVal Value As Decimal)
+ if Value = 0 then
+ Return ""
+ end if
+ Return Value
+End Function
+
+Public Function BlankPos(ByVal Value As Decimal)
+ if Value > 0 then
+ Return ""
+ end if
+ Return Value
+End Function
+
+Public Function BlankZeroAndPos(ByVal Value As Decimal)
+ if Value >= 0 then
+ Return ""
+ end if
+ Return Value
+End Function
+
+Public Function BlankNeg(ByVal Value As Decimal)
+ if Value < 0 then
+ Return ""
+ end if
+ Return Value
+End Function
+
+Public Function BlankNegAndZero(ByVal Value As Decimal)
+ if Value <= 0 then
+ Return ""
+ end if
+ Return Value
+End Function
+
+ =User!Language
+ true
+ Inch
+ 0eeb6585-38ae-40f1-885b-8d50088d51b4
+
+
+
+
+ ColumnName
+
+
+
+ DataSource
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Layers/W1/Tests/Report/ReportLayoutsTest.Codeunit.al b/src/Layers/W1/Tests/Report/ReportLayoutsTest.Codeunit.al
index b0761c39d64..192dc5348fc 100644
--- a/src/Layers/W1/Tests/Report/ReportLayoutsTest.Codeunit.al
+++ b/src/Layers/W1/Tests/Report/ReportLayoutsTest.Codeunit.al
@@ -492,9 +492,16 @@ codeunit 139595 "Report Layouts Test"
local procedure EnsureNewLayoutsAreCleaned()
var
TenantReportLayout: Record "Tenant Report Layout";
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
begin
TenantReportLayout.SetRange("Report ID", 139595);
TenantReportLayout.DeleteAll();
+
+ TenantReportLayoutOverride.SetRange("Report ID", 139595);
+ TenantReportLayoutOverride.DeleteAll();
+
+ // A test failing between Enqueue and Dequeue would otherwise leak into the next one.
+ LibraryVariableStorage.Clear();
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Report Layouts Impl.", 'OnBeforeUpload', '', false, false)]
@@ -659,17 +666,745 @@ codeunit 139595 "Report Layouts Test"
'Layout status should be Draft after cycling back.');
end;
+ [Test]
+ [HandlerFunctions('StatusChangedMessageHandler')]
+ procedure TestSetExtensionLayoutStatusWritesOverride()
+ var
+ TenantReportLayout: Record "Tenant Report Layout";
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] Setting the status of an extension-installed layout writes an ALL-COMPANIES
+ // Tenant Report Layout Override record instead of copying the layout into the tenant table.
+
+ // Init - remove any tenant layouts/overrides for the test report
+ EnsureNewLayoutsAreCleaned();
+
+ // The test report (139595) ships an RDLC layout via its rendering section, so it surfaces in
+ // Report Layout List as an extension-installed layout (User Defined = false).
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ // Act - Set status to Approved via the page action
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ Assert.IsTrue(ReportLayoutsPage.SetApproved.Enabled(), 'Set Approved should be enabled for extension layouts.');
+ ReportLayoutsPage.SetApproved.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - a global override carries the Approved status...
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ 'A global override record should have been created for the extension layout.');
+ Assert.IsTrue(TenantReportLayoutOverride."Override Layout Status", 'The Override Layout Status flag should be set.');
+ Assert.AreEqual(
+ Enum::"Report Layout Status"::Approved,
+ TenantReportLayoutOverride."Layout Status",
+ 'The override should carry the Approved status.');
+ Assert.IsFalse(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", CompanyName()),
+ 'No company-specific override should have been created.');
+
+ // ...and no copy was made into the tenant table.
+ TenantReportLayout.SetRange("Report ID", 139595);
+ Assert.IsTrue(TenantReportLayout.IsEmpty(), 'No copy should have been created in Tenant Report Layout.');
+ end;
+
[MessageHandler]
procedure StatusChangedMessageHandler(Message: Text[1024])
begin
end;
+ [Test]
+ [HandlerFunctions('StatusChangedMessageHandler')]
+ procedure TestSetGlobalScopeExtensionLayoutStatusUpdatesGlobal()
+ var
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] Changing the status of an extension layout whose STATUS is already overridden
+ // globally updates that global override rather than creating a company-specific one. No
+ // confirmation is raised — all-companies is the normal scope, so no ConfirmHandler is
+ // registered and an unexpected prompt would fail this test.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ // Seed the STATUS field specifically: the table is field-granular, so scope is resolved from
+ // "Override Layout Status" and a description-only row would not establish it.
+ TenantReportLayoutOverride.Init();
+ TenantReportLayoutOverride."Report ID" := 139595;
+ TenantReportLayoutOverride."Name" := ReportLayoutList."Name";
+ TenantReportLayoutOverride."Runtime Package ID" := ReportLayoutList."Runtime Package ID";
+ TenantReportLayoutOverride."Company Name" := '';
+ TenantReportLayoutOverride."Layout Status" := Enum::"Report Layout Status"::Draft;
+ TenantReportLayoutOverride."Override Layout Status" := true;
+ TenantReportLayoutOverride.Insert(true);
+
+ // Act - Set status to Approved; scope is global and no confirmation should be raised
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.SetApproved.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - the GLOBAL override carries the Approved status; no company-specific override was created
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ 'The global override should still exist.');
+ Assert.IsTrue(TenantReportLayoutOverride."Override Layout Status", 'Override Layout Status should be set on the global override.');
+ Assert.AreEqual(
+ Enum::"Report Layout Status"::Approved,
+ TenantReportLayoutOverride."Layout Status",
+ 'The global override should carry the Approved status.');
+ Assert.IsFalse(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", CompanyName()),
+ 'No company-specific override should have been created for a global-scope layout.');
+ end;
+
+ [Test]
+ [HandlerFunctions('StatusChangedMessageHandler')]
+ procedure TestCompanyDescriptionOnlyOverrideDoesNotForkGlobalStatus()
+ var
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] The override table is field-granular. A company-specific row that overrides only the
+ // DESCRIPTION must not make a status change company-scoped - that would silently fork layout
+ // status per company. Scope is resolved from "Override Layout Status", so the change stays
+ // all-companies and updates the global row.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ // Global row owns the STATUS...
+ TenantReportLayoutOverride.Init();
+ TenantReportLayoutOverride."Report ID" := 139595;
+ TenantReportLayoutOverride."Name" := ReportLayoutList."Name";
+ TenantReportLayoutOverride."Runtime Package ID" := ReportLayoutList."Runtime Package ID";
+ TenantReportLayoutOverride."Company Name" := '';
+ TenantReportLayoutOverride."Layout Status" := Enum::"Report Layout Status"::Draft;
+ TenantReportLayoutOverride."Override Layout Status" := true;
+ TenantReportLayoutOverride.Insert(true);
+
+ // ...while a company row owns only the DESCRIPTION.
+ TenantReportLayoutOverride.Init();
+ TenantReportLayoutOverride."Report ID" := 139595;
+ TenantReportLayoutOverride."Name" := ReportLayoutList."Name";
+ TenantReportLayoutOverride."Runtime Package ID" := ReportLayoutList."Runtime Package ID";
+ TenantReportLayoutOverride."Company Name" := CopyStr(CompanyName(), 1, MaxStrLen(TenantReportLayoutOverride."Company Name"));
+ TenantReportLayoutOverride.Description := EditedLayoutNameTxt;
+ TenantReportLayoutOverride."Override Description" := true;
+ TenantReportLayoutOverride.Insert(true);
+
+ // Act - Set status to Approved; scope stays all-companies and no confirmation should be raised
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.SetApproved.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - the GLOBAL row took the new status...
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ 'The global override should still exist.');
+ Assert.AreEqual(
+ Enum::"Report Layout Status"::Approved,
+ TenantReportLayoutOverride."Layout Status",
+ 'The global override should have taken the Approved status.');
+
+ // ...and the company row still overrides only the description, with no forked status.
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", CompanyName()),
+ 'The company-specific description override should still exist.');
+ Assert.IsFalse(
+ TenantReportLayoutOverride."Override Layout Status",
+ 'The status must not be forked into the company-specific override.');
+ end;
+
+ [Test]
+ [HandlerFunctions('StatusChangedMessageHandler')]
+ procedure TestGlobalDescriptionOnlyOverrideTakesStatusGlobally()
+ var
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] A GLOBAL row that overrides only the description gains the status override on the
+ // same row rather than causing a second row to be created. No confirmation is raised
+ // (no ConfirmHandler is registered, so an unexpected Confirm would fail this test).
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ TenantReportLayoutOverride.Init();
+ TenantReportLayoutOverride."Report ID" := 139595;
+ TenantReportLayoutOverride."Name" := ReportLayoutList."Name";
+ TenantReportLayoutOverride."Runtime Package ID" := ReportLayoutList."Runtime Package ID";
+ TenantReportLayoutOverride."Company Name" := '';
+ TenantReportLayoutOverride.Description := EditedLayoutNameTxt;
+ TenantReportLayoutOverride."Override Description" := true;
+ TenantReportLayoutOverride.Insert(true);
+
+ // Act - Set status to Approved; all-companies is the default scope, no confirmation expected
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.SetApproved.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - the existing GLOBAL row now carries both overrides...
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ 'The global override should still exist.');
+ Assert.IsTrue(TenantReportLayoutOverride."Override Layout Status", 'The Override Layout Status flag should be set.');
+ Assert.AreEqual(
+ Enum::"Report Layout Status"::Approved,
+ TenantReportLayoutOverride."Layout Status",
+ 'The global override should carry the Approved status.');
+ Assert.IsTrue(TenantReportLayoutOverride."Override Description", 'The existing description override must be preserved.');
+
+ // ...and no company-specific row was created.
+ Assert.IsFalse(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", CompanyName()),
+ 'No company-specific override should have been created.');
+ end;
+
+ [Test]
+ [HandlerFunctions('StatusChangedMessageHandler')]
+ procedure TestCompanyStatusOverrideKeepsStatusCompanyScoped()
+ var
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] Company precedence survives the move to all-companies-by-default: where this company
+ // ALREADY has a status override, a further status change stays in that company and does not leak
+ // into a global row. Such rows can no longer be created from the UI, but they may come from an
+ // earlier version or a vendor's install codeunit — and this is the branch a future
+ // company-scoped option would build on.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ TenantReportLayoutOverride.Init();
+ TenantReportLayoutOverride."Report ID" := 139595;
+ TenantReportLayoutOverride."Name" := ReportLayoutList."Name";
+ TenantReportLayoutOverride."Runtime Package ID" := ReportLayoutList."Runtime Package ID";
+ TenantReportLayoutOverride."Company Name" := CopyStr(CompanyName(), 1, MaxStrLen(TenantReportLayoutOverride."Company Name"));
+ TenantReportLayoutOverride."Layout Status" := Enum::"Report Layout Status"::Draft;
+ TenantReportLayoutOverride."Override Layout Status" := true;
+ TenantReportLayoutOverride.Insert(true);
+
+ // Act
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.SetApproved.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - the COMPANY row took the new status, and nothing went global
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", CompanyName()),
+ 'The company-specific override should still exist.');
+ Assert.AreEqual(
+ Enum::"Report Layout Status"::Approved,
+ TenantReportLayoutOverride."Layout Status",
+ 'The company override should have taken the Approved status.');
+ Assert.IsFalse(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ 'No global override should have been created when this company already owns the status.');
+ end;
+
+ [Test]
+ procedure TestMixedScopeBatchStatusIsRejected()
+ var
+ ReportLayoutList: Record "Report Layout List";
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutsImpl: Codeunit "Report Layouts Impl.";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] A batch status change spanning mixed scopes is rejected, keeping each run to a
+ // single unambiguous scope.
+ // Driven through the internal impl codeunit (Tests-Report is in BaseApp internalsVisibleTo)
+ // because a TestPage cannot multi-select records for CurrPage.SetSelectionFilter.
+ EnsureNewLayoutsAreCleaned();
+
+ // Report 139595 ships two extension layouts. All-companies is now the DEFAULT scope, so mixing
+ // requires giving one layout a COMPANY-SPECIFIC status override; the untouched second layout
+ // resolves to global. (Seeding a global override on one would no longer create a mix — both
+ // sides would be global.)
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.AreEqual(2, ReportLayoutList.Count(), 'The test report should ship two extension layouts.');
+ ReportLayoutList.FindFirst();
+
+ TenantReportLayoutOverride.Init();
+ TenantReportLayoutOverride."Report ID" := 139595;
+ TenantReportLayoutOverride."Name" := ReportLayoutList."Name";
+ TenantReportLayoutOverride."Runtime Package ID" := ReportLayoutList."Runtime Package ID";
+ TenantReportLayoutOverride."Company Name" := CopyStr(CompanyName(), 1, MaxStrLen(TenantReportLayoutOverride."Company Name"));
+ TenantReportLayoutOverride."Layout Status" := Enum::"Report Layout Status"::Draft;
+ TenantReportLayoutOverride."Override Layout Status" := true;
+ TenantReportLayoutOverride.Insert(true);
+
+ // Act - batch over BOTH extension layouts (mixed scope)
+ ReportLayoutList.Reset();
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ ReportLayoutsImpl.SetSelectedCompany(CompanyName());
+ asserterror ReportLayoutsImpl.SetLayoutStatusBatch(ReportLayoutList, Enum::"Report Layout Status"::Approved);
+
+ // Assert - rejected with the mixed-scope error
+ Assert.ExpectedError('different scopes');
+ end;
+
+ [Test]
+ procedure TestBatchStatusResolvesScopeWithoutSetSelectedCompany()
+ var
+ ReportLayoutList: Record "Report Layout List";
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutsImpl: Codeunit "Report Layouts Impl.";
+ UpdateCount: Integer;
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] Scope resolution must not depend on a caller having called SetSelectedCompany first.
+ // "Report Theme and Header/Footer" reaches SetLayoutStatusBatch without calling it, and a blank
+ // company would make the company-row lookup probe the GLOBAL row instead: an already-overridden
+ // layout would then classify as company scope while a fresh one classified as global, and this
+ // batch would fail with the mixed-scope error even though both are global.
+ // Note this test deliberately does NOT call SetSelectedCompany.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.AreEqual(2, ReportLayoutList.Count(), 'The test report should ship two extension layouts.');
+ ReportLayoutList.FindFirst();
+
+ // One layout already carries a GLOBAL status override; the other has none. Both are global scope.
+ TenantReportLayoutOverride.Init();
+ TenantReportLayoutOverride."Report ID" := 139595;
+ TenantReportLayoutOverride."Name" := ReportLayoutList."Name";
+ TenantReportLayoutOverride."Runtime Package ID" := ReportLayoutList."Runtime Package ID";
+ TenantReportLayoutOverride."Company Name" := '';
+ TenantReportLayoutOverride."Layout Status" := Enum::"Report Layout Status"::Draft;
+ TenantReportLayoutOverride."Override Layout Status" := true;
+ TenantReportLayoutOverride.Insert(true);
+
+ // Act - batch over BOTH, with SelectedCompany never set
+ ReportLayoutList.Reset();
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ UpdateCount := ReportLayoutsImpl.SetLayoutStatusBatch(ReportLayoutList, Enum::"Report Layout Status"::Approved);
+
+ // Assert - no spurious mixed-scope error, and both layouts updated globally
+ Assert.AreEqual(2, UpdateCount, 'Both layouts should have been updated; a blank company must not split the scope.');
+ ReportLayoutList.FindSet();
+ repeat
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ StrSubstNo(GlobalOverrideMissingErr, ReportLayoutList."Name"));
+ Assert.AreEqual(
+ Enum::"Report Layout Status"::Approved,
+ TenantReportLayoutOverride."Layout Status",
+ StrSubstNo(GlobalOverrideApprovedErr, ReportLayoutList."Name"));
+ until ReportLayoutList.Next() = 0;
+ end;
+
+ [Test]
+ procedure TestBatchStatusOverAllGlobalLayoutsUpdatesEveryOne()
+ var
+ ReportLayoutList: Record "Report Layout List";
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutsImpl: Codeunit "Report Layouts Impl.";
+ UpdateCount: Integer;
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] The success path of a multi-layout batch run: two fresh extension layouts both
+ // resolve to all-companies scope, so the run is single-scope and applies to both. No
+ // confirmation is raised — no ConfirmHandler is registered, so a prompt would fail this test,
+ // which is what pins the removal of the former all-companies confirmation.
+ // Driven through the internal impl codeunit because a TestPage cannot multi-select.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.AreEqual(2, ReportLayoutList.Count(), 'The test report should ship two extension layouts.');
+
+ // Act - batch over BOTH layouts, neither of which has any override yet
+ ReportLayoutsImpl.SetSelectedCompany(CompanyName());
+ UpdateCount := ReportLayoutsImpl.SetLayoutStatusBatch(ReportLayoutList, Enum::"Report Layout Status"::Retired);
+
+ // Assert - both were updated...
+ Assert.AreEqual(2, UpdateCount, 'Both extension layouts should have been updated.');
+
+ // ...each through a GLOBAL override, with no company-specific rows anywhere.
+ ReportLayoutList.FindSet();
+ repeat
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ StrSubstNo(GlobalOverrideMissingErr, ReportLayoutList."Name"));
+ Assert.AreEqual(
+ Enum::"Report Layout Status"::Retired,
+ TenantReportLayoutOverride."Layout Status",
+ StrSubstNo(GlobalOverrideRetiredErr, ReportLayoutList."Name"));
+ Assert.IsFalse(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", CompanyName()),
+ StrSubstNo(CompanyOverrideUnexpectedErr, ReportLayoutList."Name"));
+ until ReportLayoutList.Next() = 0;
+ end;
+
+ [Test]
+ [HandlerFunctions('EditExtensionAssertObsoleteLockedHandler')]
+ procedure TestObsoleteExtensionLayoutCannotBeUnObsoleted()
+ var
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] IsObsolete is ONE-WAY. Once a layout resolves to obsolete, the edit dialog must not
+ // offer a way back: the field is locked, so retiring cannot be undone through the UI. This is
+ // the guarantee the design leans on when treating obsoleting as a grave act.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ // Make the layout obsolete via a global override, the way the everyday edit path would.
+ TenantReportLayoutOverride.Init();
+ TenantReportLayoutOverride."Report ID" := 139595;
+ TenantReportLayoutOverride."Name" := ReportLayoutList."Name";
+ TenantReportLayoutOverride."Runtime Package ID" := ReportLayoutList."Runtime Package ID";
+ TenantReportLayoutOverride."Company Name" := '';
+ TenantReportLayoutOverride.IsObsolete := true;
+ TenantReportLayoutOverride."Override IsObsolete" := true;
+ TenantReportLayoutOverride.Insert(true);
+
+ // Act - reopen Edit info; the handler records whether the obsolete field was editable
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.EditLayout.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - the field was locked, and the layout is still obsolete
+ Assert.IsFalse(
+ LibraryVariableStorage.DequeueBoolean(),
+ 'Mark layout as obsolete must be locked once the layout is already obsolete.');
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ 'The global obsolete override should still exist.');
+ Assert.IsTrue(TenantReportLayoutOverride.IsObsolete, 'The layout must still be obsolete.');
+ LibraryVariableStorage.AssertEmpty();
+ end;
+
+ [Test]
+ [HandlerFunctions('EditExtensionOverrideDescHandler')]
+ procedure TestEditExtensionLayoutWritesGlobalDescriptionOverride()
+ var
+ TenantReportLayout: Record "Tenant Report Layout";
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] Editing an extension-installed layout's description writes an ALL-COMPANIES
+ // Tenant Report Layout Override record instead of copying the layout. An extension layout is the
+ // same layout in every company, so its description is overridden tenant-wide by default.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ // Act - Edit info (override mode)
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.EditLayout.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - a GLOBAL description override exists (and no company-specific one), no tenant copy
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ 'A global override record should have been created.');
+ Assert.IsTrue(TenantReportLayoutOverride."Override Description", 'The Override Description flag should be set.');
+ Assert.AreEqual(EditedLayoutNameTxt, TenantReportLayoutOverride.Description, 'The override should carry the edited description.');
+ Assert.IsFalse(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", CompanyName()),
+ 'An everyday edit must not create a company-specific override.');
+
+ TenantReportLayout.SetRange("Report ID", 139595);
+ Assert.IsTrue(TenantReportLayout.IsEmpty(), 'No copy should have been created in Tenant Report Layout.');
+ end;
+
+ [Test]
+ [HandlerFunctions('EditExtensionOverrideObsoleteHandler')]
+ procedure TestEditExtensionLayoutWritesGlobalObsoleteOverride()
+ var
+ TenantReportLayout: Record "Tenant Report Layout";
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] Marking an extension-installed layout obsolete writes an ALL-COMPANIES override
+ // (one-way IsObsolete) instead of copying the layout.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ // Act - Edit info (override mode), mark obsolete
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.EditLayout.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - a global obsolete override exists, no tenant copy
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ 'A global override record should have been created.');
+ Assert.IsTrue(TenantReportLayoutOverride."Override IsObsolete", 'The Override IsObsolete flag should be set.');
+ Assert.IsTrue(TenantReportLayoutOverride.IsObsolete, 'The override should mark the layout obsolete.');
+
+ TenantReportLayout.SetRange("Report ID", 139595);
+ Assert.IsTrue(TenantReportLayout.IsEmpty(), 'No copy should have been created in Tenant Report Layout.');
+ end;
+
+ [Test]
+ [HandlerFunctions('EditExtensionOverrideNoOpHandler')]
+ procedure TestEditExtensionLayoutNoOpWritesNoOverride()
+ var
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] Edit info on an extension layout + OK with no changes writes NO override
+ // (no silent global override for a no-op edit).
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.EditLayout.Invoke();
+ ReportLayoutsPage.Close();
+
+ TenantReportLayoutOverride.SetRange("Report ID", 139595);
+ Assert.IsTrue(TenantReportLayoutOverride.IsEmpty(), 'A no-op edit must not create any override.');
+ end;
+
+ [ModalPageHandler]
+ procedure EditExtensionOverrideDescHandler(var ReportLayoutEditDialog: TestPage "Report Layout Edit Dialog")
+ begin
+ ReportLayoutEditDialog.Description.SetValue(EditedLayoutNameTxt);
+ ReportLayoutEditDialog.OK().Invoke();
+ end;
+
+ [ModalPageHandler]
+ procedure EditExtensionOverrideObsoleteHandler(var ReportLayoutEditDialog: TestPage "Report Layout Edit Dialog")
+ begin
+ ReportLayoutEditDialog.IsObsolete.SetValue(true);
+ ReportLayoutEditDialog.OK().Invoke();
+ end;
+
+ [ModalPageHandler]
+ procedure EditExtensionAssertObsoleteLockedHandler(var ReportLayoutEditDialog: TestPage "Report Layout Edit Dialog")
+ begin
+ // Record only. The test asserts, so a mismatch cannot be swallowed by the calling UI operation.
+ LibraryVariableStorage.Enqueue(ReportLayoutEditDialog.IsObsolete.Editable());
+ ReportLayoutEditDialog.OK().Invoke();
+ end;
+
+ [ModalPageHandler]
+ procedure EditExtensionOverrideNoOpHandler(var ReportLayoutEditDialog: TestPage "Report Layout Edit Dialog")
+ begin
+ // Change nothing, just confirm — should write no override.
+ ReportLayoutEditDialog.OK().Invoke();
+ end;
+
+
+ [Test]
+ [HandlerFunctions('EditExtensionCopyAllCompaniesHandler')]
+ procedure TestCopyOfExtensionLayoutKeepsAllCompaniesScope()
+ var
+ TenantReportLayout: Record "Tenant Report Layout";
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ EmptyGuid: Guid;
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] Copying an extension layout ("Save Changes to a Copy") creates an ordinary tenant
+ // layout whose company scope comes from "Available in All Companies" (default: all companies),
+ // NOT from the override-scope control — and writes no override record.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ // Act - Edit info -> tick "Save Changes to a Copy", rename, leave availability at its default
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.EditLayout.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - the dialog defaulted to all companies, the copy is a GLOBAL tenant layout, no override
+ Assert.AreEqual('Yes', LibraryVariableStorage.DequeueText(), 'A copy should default to all companies.');
+ Assert.IsTrue(
+ TenantReportLayout.Get(139595, EditedLayoutNameTxt, EmptyGuid),
+ 'The copy should exist in Tenant Report Layout under its new name.');
+ Assert.AreEqual('', TenantReportLayout."Company Name", 'The copy should be available in all companies by default.');
+
+ TenantReportLayoutOverride.SetRange("Report ID", 139595);
+ Assert.IsTrue(TenantReportLayoutOverride.IsEmpty(), 'Copying must not write an override record.');
+ LibraryVariableStorage.AssertEmpty();
+ end;
+
+ [Test]
+ [HandlerFunctions('EditExtensionToggleCopyOffHandler')]
+ procedure TestUntickingCopyRestoresAllCompaniesScope()
+ var
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] Ticking "Save Changes to a Copy", choosing company-only, then unticking it must put
+ // the scope back to all companies. Otherwise the field shows a read-only No while the in-place
+ // edit writes an all-companies override - the dialog would contradict what is written.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ // Act - tick Copy, select company-only, untick Copy, then edit the description and save
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.EditLayout.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - the dialog showed Yes again, and the override it wrote is global
+ Assert.AreEqual(
+ 'Yes', LibraryVariableStorage.DequeueText(),
+ 'Unticking Save Changes to a Copy must restore the all-companies scope shown in the dialog.');
+ Assert.IsTrue(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", ''),
+ 'A global override should have been written.');
+ Assert.IsFalse(
+ TenantReportLayoutOverride.Get(139595, ReportLayoutList."Name", ReportLayoutList."Runtime Package ID", CompanyName()),
+ 'No company-specific override should exist.');
+ LibraryVariableStorage.AssertEmpty();
+ end;
+
+ [ModalPageHandler]
+ procedure EditExtensionToggleCopyOffHandler(var ReportLayoutEditDialog: TestPage "Report Layout Edit Dialog")
+ begin
+ ReportLayoutEditDialog.CreateCopy.SetValue(true);
+ ReportLayoutEditDialog.AvailableInAllCompanies.SetValue(false);
+ ReportLayoutEditDialog.CreateCopy.SetValue(false);
+ LibraryVariableStorage.Enqueue(ReportLayoutEditDialog.AvailableInAllCompanies.Value);
+ ReportLayoutEditDialog.Description.SetValue(EditedLayoutNameTxt);
+ ReportLayoutEditDialog.OK().Invoke();
+ end;
+
+ [Test]
+ [HandlerFunctions('CopyObsoleteExtensionLayoutHandler')]
+ procedure TestCopyOfObsoleteExtensionLayoutCanClearObsolete()
+ var
+ TenantReportLayout: Record "Tenant Report Layout";
+ TenantReportLayoutOverride: Record "Tenant Report Layout Override";
+ ReportLayoutList: Record "Report Layout List";
+ ReportLayoutsPage: TestPage "Report Layouts";
+ EmptyGuid: Guid;
+ begin
+ // [FEATURE] [AI TEST]
+ // [SCENARIO] The one-way obsolete lock belongs to the in-place override path only. Taking a copy
+ // of an obsolete extension layout must re-enable the field, because the copy is an ordinary
+ // tenant layout - otherwise the copy is stuck obsolete and the old copy flow regresses.
+ EnsureNewLayoutsAreCleaned();
+
+ ReportLayoutList.SetRange("Report ID", 139595);
+ ReportLayoutList.SetRange("User Defined", false);
+ Assert.IsTrue(ReportLayoutList.FindFirst(), 'The extension-installed test layout should be present.');
+
+ // Make the layout resolve to obsolete, the way the everyday edit path would
+ TenantReportLayoutOverride.Init();
+ TenantReportLayoutOverride."Report ID" := 139595;
+ TenantReportLayoutOverride."Name" := ReportLayoutList."Name";
+ TenantReportLayoutOverride."Runtime Package ID" := ReportLayoutList."Runtime Package ID";
+ TenantReportLayoutOverride."Company Name" := '';
+ TenantReportLayoutOverride.IsObsolete := true;
+ TenantReportLayoutOverride."Override IsObsolete" := true;
+ TenantReportLayoutOverride.Insert(true);
+
+ // Act - Edit info -> tick Copy; the handler records whether the obsolete field became editable
+ ReportLayoutsPage.OpenView();
+ ReportLayoutsPage.GoToRecord(ReportLayoutList);
+ ReportLayoutsPage.EditLayout.Invoke();
+ ReportLayoutsPage.Close();
+
+ // Assert - copy mode unlocked the field, and the copy exists
+ Assert.IsTrue(
+ LibraryVariableStorage.DequeueBoolean(),
+ 'Mark layout as obsolete must be editable again once Save Changes to a Copy is selected.');
+ Assert.IsTrue(
+ TenantReportLayout.Get(139595, EditedLayoutNameTxt, EmptyGuid),
+ 'The copy should exist in Tenant Report Layout under its new name.');
+ LibraryVariableStorage.AssertEmpty();
+ end;
+
+ [ModalPageHandler]
+ procedure CopyObsoleteExtensionLayoutHandler(var ReportLayoutEditDialog: TestPage "Report Layout Edit Dialog")
+ begin
+ ReportLayoutEditDialog.CreateCopy.SetValue(true);
+ LibraryVariableStorage.Enqueue(ReportLayoutEditDialog.IsObsolete.Editable());
+ ReportLayoutEditDialog.LayoutName.SetValue(EditedLayoutNameTxt);
+ ReportLayoutEditDialog.OK().Invoke();
+ end;
+
+ [ModalPageHandler]
+ procedure EditExtensionCopyAllCompaniesHandler(var ReportLayoutEditDialog: TestPage "Report Layout Edit Dialog")
+ begin
+ ReportLayoutEditDialog.CreateCopy.SetValue(true);
+ ReportLayoutEditDialog.LayoutName.SetValue(EditedLayoutNameTxt);
+ // Record only; the test asserts it.
+ LibraryVariableStorage.Enqueue(ReportLayoutEditDialog.AvailableInAllCompanies.Value);
+ ReportLayoutEditDialog.OK().Invoke();
+ end;
+
+
var
Assert: Codeunit Assert;
TempBlob: Codeunit "Temp Blob";
+ LibraryVariableStorage: Codeunit "Library - Variable Storage";
NewLayoutNameTxt: Label 'NewLayout';
EditedLayoutNameTxt: Label 'EditedLayout';
SampleTextTxt: Label 'ATAKLOA, TINWTABSBATF.';
AlternateLayoutTextTxt: Label 'IWATSTGIFLBOTG.';
+ GlobalOverrideMissingErr: Label 'A global override should exist for layout %1.', Comment = '%1 = layout name';
+ GlobalOverrideApprovedErr: Label 'The global override for %1 should carry the Approved status.', Comment = '%1 = layout name';
+ GlobalOverrideRetiredErr: Label 'The global override for %1 should carry the Retired status.', Comment = '%1 = layout name';
+ CompanyOverrideUnexpectedErr: Label 'No company-specific override should exist for layout %1.', Comment = '%1 = layout name';
InsertedLayoutContextTxt: Text;
}
diff --git a/src/Layers/W1/Tests/Report/Reports/TestReportLayoutsReport.Report.al b/src/Layers/W1/Tests/Report/Reports/TestReportLayoutsReport.Report.al
index d73e244a299..403b28d6cfe 100644
--- a/src/Layers/W1/Tests/Report/Reports/TestReportLayoutsReport.Report.al
+++ b/src/Layers/W1/Tests/Report/Reports/TestReportLayoutsReport.Report.al
@@ -27,5 +27,13 @@ report 139595 TestReportLayoutsReport
Type = RDLC;
LayoutFile = 'Layouts/TestReportLayoutsReport.rdl';
}
+ // Second extension-installed layout so tests can exercise multi-layout scenarios
+ // (e.g. mixed global/company scope in a batch status change - CP0529-338).
+ // Uses its own layout file: a layout file must not be shared between layouts (AL0835).
+ layout(MYLAYOUT2)
+ {
+ Type = RDLC;
+ LayoutFile = 'Layouts/TestReportLayoutsReport2.rdl';
+ }
}
}
\ No newline at end of file