Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b204663
637301 slice 1: status override for extension layouts
V-CWurtzen Jul 10, 2026
5c3c879
637301 slice 2: description/obsolete override for extension layouts
V-CWurtzen Jul 13, 2026
53cd6cf
637301 slice 3 (Q1): scope handling for batch status overrides
V-CWurtzen Jul 13, 2026
e17c5ff
637301 slice 3: status-action wording + mixed-scope test coverage
V-CWurtzen Jul 13, 2026
1c0af99
Give MYLAYOUT2 its own layout file (fix AL0835)
V-CWurtzen Jul 21, 2026
3759a84
637301 slice 4: fix edit-info override scope/telemetry (PR #9428 review)
V-CWurtzen Jul 28, 2026
a7bf27c
CP0529-338 slice 4b: declining the global scope keeps the user's edits
V-CWurtzen Jul 30, 2026
131c3ec
Fix: don't evaluate the global-scope Confirm on company-scoped edits
V-CWurtzen Jul 30, 2026
ceb8ad9
CP0529-338 slice 4c: message on declined global scope + fix copy scope
V-CWurtzen Jul 30, 2026
7d21a82
Fix: unlock (not reveal) Available in All Companies for the copy path
V-CWurtzen Jul 30, 2026
1b0650f
Make the two scope toggles mutually exclusive in the edit dialog
V-CWurtzen Jul 30, 2026
262c33c
CP0529-338 slice 5: no scope choice in the everyday edit path
V-CWurtzen Jul 30, 2026
2641efd
CP0529-338: telemetry for the override path under its own event id 00…
V-CWurtzen Jul 30, 2026
b44b845
637301: resolve status override scope from the status field, not row …
V-CWurtzen Jul 31, 2026
172ea89
637301: tighten the override edit path and correct stale comments
V-CWurtzen Jul 31, 2026
0b48d1b
637301: default extension layout overrides to all companies
V-CWurtzen Aug 6, 2026
7124b41
637301: cover the all-companies default and fix the mixed-scope seed
V-CWurtzen Aug 7, 2026
3adc5c7
637301: resolve override company at point of use, not from call order
V-CWurtzen Aug 9, 2026
8270309
637301: address the app team review - comments, style, and test handlers
V-CWurtzen Aug 14, 2026
b7dd614
637301: fix the two dialog transition defects and finish the comment …
V-CWurtzen Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.';

Expand Down Expand Up @@ -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)
Expand All @@ -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;
}
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;

/// <summary>
/// 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.
/// </summary>
/// <param name="ReportLayoutList">The layout record from the virtual table</param>
/// <param name="NewStatus">The new status to set</param>
/// <returns>True if the status was updated, false if the layout is not user-defined or not found</returns>
/// <returns>True if the status was updated, false if the user-defined layout was not found</returns>
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;
Expand All @@ -72,20 +83,92 @@ codeunit 9660 "Report Layouts Impl."
exit(false);
end;

local procedure LayoutStatusIsGlobalScope(ReportLayoutList: Record "Report Layout List"): Boolean
Comment thread
V-CWurtzen marked this conversation as resolved.
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)
Comment thread
V-CWurtzen marked this conversation as resolved.
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;

/// <summary>
/// Sets the status for multiple selected layouts.
/// Use this from page actions when user selects multiple layouts.
/// </summary>
/// <param name="ReportLayoutList">Record set with selected layouts (filtered/marked)</param>
/// <param name="NewStatus">The new status to set</param>
/// <returns>Number of layouts updated (excludes extension-defined layouts)</returns>
/// <returns>Number of layouts updated — user-defined layouts in place, extension-installed layouts
/// through a "Tenant Report Layout Override" record</returns>
Comment thread
V-CWurtzen marked this conversation as resolved.
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
Comment thread
V-CWurtzen marked this conversation as resolved.
if not ReportLayoutList."User Defined" then
Comment thread
V-CWurtzen marked this conversation as resolved.
if LayoutStatusIsGlobalScope(ReportLayoutList) then
HasGlobalScope := true
else
HasCompanyScope := true;
until ReportLayoutList.Next() = 0;

if HasGlobalScope and HasCompanyScope then
Error(MixedScopeErr);
Comment thread
nhsejth marked this conversation as resolved.

ReportLayoutList.FindSet();
Comment thread
V-CWurtzen marked this conversation as resolved.
repeat
if SetLayoutStatus(ReportLayoutList, NewStatus) then
UpdateCount += 1;
Expand Down Expand Up @@ -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
Expand All @@ -607,14 +692,38 @@ codeunit 9660 "Report Layouts Impl."
CompanyName := SelectedCompany;

ReportLayoutEditDialog.SetupDialog(SelectedReportLayoutList, SelectedCompany);
if ReportLayoutEditDialog.RunModal() = Action::OK then begin
begin
Comment thread
V-CWurtzen marked this conversation as resolved.
if ReportLayoutEditDialog.RunModal() <> Action::OK then
exit;

NewDescription := ReportLayoutEditDialog.SelectedLayoutDescription();
NewLayoutName := ReportLayoutEditDialog.SelectedLayoutName();
CreateCopy := ReportLayoutEditDialog.CopyOperationEnabled();
AvailableInAllCompanies := ReportLayoutEditDialog.SelectedAvailableInAllCompanies();
NewIsObsolete := ReportLayoutEditDialog.SelectedIsObsolete();

if (not SelectedReportLayoutList."User Defined") and (not CreateCopy) then begin
Comment thread
V-CWurtzen marked this conversation as resolved.
// 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);
Comment thread
V-CWurtzen marked this conversation as resolved.
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
Expand Down
Loading
Loading