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 +