Codeunit 3903 Retention Policy Setup Impl., source in 28

Source242526272829metadata in 28

src/System Application/App/Retention Policy/src/Retention Policy Setup/RetentionPolicySetupImpl.Codeunit.al484 lines, Copyright (c) Microsoft Corporation. MIT

// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------

namespace System.DataAdministration;

using System.Environment.Configuration;
using System.Reflection;

codeunit 3903 "Retention Policy Setup Impl."
{
    Access = Internal;
    InherentEntitlements = X;
    InherentPermissions = X;
    EventSubscriberInstance = Manual;
    Permissions = tabledata AllObj = r,
                  tabledata AllObjWithCaption = r,
                  tabledata Field = r,
                  tabledata "Retention Period" = ri,
                  tabledata "Retention Policy Setup Line" = ri;

    var
        ManualSetupTitleTxt: Label 'Retention policies';
        ManualSetupShortTitleTxt: Label 'Retention policies';
        ManualSetupDescriptionTxt: Label 'Set up retention policies for log tables to automatically delete expired records.';
        ManualSetupKeyWordsTxt: Label 'Retention, Delete, Cleanup, Log';
        RetentionPeriodUsedErr: Label 'You cannot delete the retention period %1 because one or more retention policies are using it.', Comment = '%1 = a retention period code';
        RetentionPeriodLockedErr: Label 'You cannot modify the retention period %1 because one or more mandatory retention policies are using it.', Comment = '%1 = a retention period code';
        TableNotAllowedErrorLbl: Label 'Table %1 %2 is not in the list of allowed tables.', Comment = '%1 = table number, %2 = table name';
        FilterPageBuilderCaptionLbl: Label '%1 Filters', Comment = '%1 = Table caption (i.e. Change Log Entry';
        MinExpirationDateErr: Label 'The mandatory minimum retention period for this retention policy is %1 days. The expiration date for this retention policy must be equal to or before %2.', Comment = '%1 integer, %2 = Date';
        RetentionPolicySetupLineLockedErr: Label 'The retention policy setup for table %1, %2 has mandatory filters that cannot be modified.', Comment = '%1 = table number, %2 = table caption';
        RetenPolAllowedTableFilterMismatchLbl: Label 'The retention policy allow list contains a mismatched filter for table ID %1. Filter table ID is %2', Comment = '%1 = table number, %2 = table number';
        DeleteAllowedInfoLbl: Label 'Allowed deletion of the locked retention policy setup line for table %1, %2.', Comment = '%1 = table number, %2 = table caption';
        DeleteAllowedList: List of [Integer];
        ReadPermissionNotificationLbl: Label 'The number of expired records cannot be calculated because you do not have read permission on table %1, %2.', Comment = '%1 = table number, %2 = table caption';
        TableDoesNotExistLbl: Label 'Table %1 does not exist.', Comment = '%1 = table number';

    procedure SetTableFilterView(var RetentionPolicySetupLine: Record "Retention Policy Setup Line"): Text
    var
        RetenPolFilterPageBuilder: FilterPageBuilder;
        OutStream: OutStream;
        FilterView: Text;
        FilterText: Text;
    begin
        FilterView := GetTableFilterView(RetentionPolicySetupLine);

        RetentionPolicySetupLine.CalcFields("Table Caption");
        RetenPolFilterPageBuilder.AddTable(RetentionPolicySetupLine."Table Caption", RetentionPolicySetupLine."Table ID");

        if FilterView <> '' then
            RetenPolFilterPageBuilder.SetView(RetentionPolicySetupLine."Table Caption", FilterView);

        RetenPolFilterPageBuilder.PageCaption(StrSubstNo(FilterPageBuilderCaptionLbl, RetentionPolicySetupLine."Table Caption"));
        if not RetenPolFilterPageBuilder.RunModal() then
            exit(RetentionPolicySetupLine."Table Filter Text"); // if cancelled, keep previous value

        FilterView := RetenPolFilterPageBuilder.GetView(RetentionPolicySetupLine."Table Caption", false);
        FilterText := ConvertFilterViewToFilterText(FilterView, RetentionPolicySetupLine."Table ID");

        if FilterText <> '' then begin
            RetentionPolicySetupLine."Table Filter".CreateOutStream(OutStream, TextEncoding::UTF8);
            OutStream.Write(FilterView);
        end else
            Clear(RetentionPolicySetupLine."Table Filter");

        exit(FilterText);
    end;

    procedure GetTableFilterView(RetentionPolicySetupLine: Record "Retention Policy Setup Line"): Text
    var
        TempRetentionPolicySetupLine: Record "Retention Policy Setup Line" temporary;
        InStream: InStream;
        FilterText: Text;
    begin
        if CalcTableFilterBlob(RetentionPolicySetupLine, TempRetentionPolicySetupLine) then begin
            TempRetentionPolicySetupLine."Table Filter".CreateInStream(InStream, TextEncoding::UTF8);
            InStream.Read(FilterText);
            exit(FilterText);
        end;
    end;

    procedure GetTableFilterText(RetentionPolicySetupLine: Record "Retention Policy Setup Line"): Text
    begin
        exit(ConvertFilterViewToFilterText(GetTableFilterView(RetentionPolicySetupLine), RetentionPolicySetupLine."Table ID"))
    end;

    local procedure ConvertFilterViewToFilterText(FilterView: Text; TableId: Integer): Text
    var
        RecordRef: RecordRef;
    begin
        RecordRef.Open(TableId);
        RecordRef.SetView(FilterView);
        exit(RecordRef.GetFilters());
    end;

    local procedure CalcTableFilterBlob(var RetentionPolicySetupLine: Record "Retention Policy Setup Line"; var TempRetentionPolicySetupLine: Record "Retention Policy Setup Line" temporary): Boolean
    begin
        TempRetentionPolicySetupLine."Table Filter" := RetentionPolicySetupLine."Table Filter";
        if not TempRetentionPolicySetupLine."Table Filter".HasValue then
            RetentionPolicySetupLine.CalcFields("Table Filter");
        TempRetentionPolicySetupLine."Table Filter" := RetentionPolicySetupLine."Table Filter";

        exit(TempRetentionPolicySetupLine."Table Filter".HasValue)
    end;

    procedure TableIdLookup(TableId: Integer): Integer
    var
        AllObjWithCaption: Record AllObjWithCaption;
        RetentionPolicySetup: Record "Retention Policy Setup";
        RetenPolAllowedTables: Codeunit "Reten. Pol. Allowed Tables";
        AllowedTablesFilter: Text;
    begin
        AllowedTablesFilter := RetenPolAllowedTables.GetAllowedTables();
        AllObjWithCaption.FilterGroup := 2;
        AllObjWithCaption.SetRange("Object Type", AllObjWithCaption."Object Type"::Table);
        if AllowedTablesFilter <> '' then
            AllObjWithCaption.SetFilter("Object ID", AllowedTablesFilter)
        else
            AllObjWithCaption.SetRange("Object ID", 0); // show empty list
        AllObjWithCaption.FilterGroup := 0;

        if RetentionPolicySetup.Get(TableId) then begin
            // lookup on an existing value, show all
            AllObjWithCaption."Object Type" := AllObjWithCaption."Object Type"::Table;
            AllObjWithCaption."Object ID" := TableId
        end else
            // filter out used values
            AllObjWithCaption.SetFilter("Object ID", GetUsedTablesFilter());

        if Page.RunModal(Page::Objects, AllObjWithCaption) = Action::LookupOK then
            TableId := AllObjWithCaption."Object ID";
        exit(TableId);
    end;

    local procedure GetUsedTablesFilter() FilterText: Text
    var
        RetentionPolicySetup: Record "Retention Policy Setup";
        Count: Integer;
    begin
        Count := RetentionPolicySetup.Count();
        if Count = 0 then
            exit('<> 0'); // -> don't set a filter

        RetentionPolicySetup.FindSet();
        FilterText := '<>' + Format(RetentionPolicySetup."Table Id");

        if Count >= 2 then
            repeat
                FilterText += '&<>' + Format(RetentionPolicySetup."Table Id");
            until RetentionPolicySetup.Next() = 0;
    end;

    procedure TableExists(TableId: Integer): Boolean
    var
        AllObj: Record allObj;
        RetentionPolicyLog: Codeunit "Retention Policy Log";
    begin
        if AllObj.Get(AllObj."Object Type"::Table, TableId) then
            exit(true);

        RetentionPolicyLog.LogWarning(LogCategory(), StrSubstNo(TableDoesNotExistLbl, TableId));
    end;

    procedure DateFieldNoLookup(TableId: Integer; FieldNo: Integer): Integer
    var
        SelectedField: Record Field;
        FieldSelection: Codeunit "Field Selection";
    begin
        SelectedField.FilterGroup := 2;
        SelectedField.SetRange(TableNo, TableId);
        SelectedField.SetFilter(Type, '%1|%2', SelectedField.Type::Date, SelectedField.Type::DateTime);
        SelectedField.FilterGroup := 0;
        SelectedField."No." := FieldNo;

        if FieldSelection.Open(SelectedField) then
            FieldNo := SelectedField."No.";

        exit(FieldNo);
    end;

    procedure IsRetentionPolicyEnabled(): Boolean
    var
        RetentionPolicySetup: Record "Retention Policy Setup";
    begin
        RetentionPolicySetup.SetRange(Enabled, true);
        exit(not RetentionPolicySetup.IsEmpty())
    end;

    procedure IsRetentionPolicyEnabled(TableId: Integer): Boolean
    var
        RetentionPolicySetup: Record "Retention Policy Setup";
    begin
        RetentionPolicySetup.Get(TableId);
        exit(RetentionPolicySetup.Enabled)
    end;

    procedure ValidateRetentionPeriod(RetentionPolicySetup: Record "Retention Policy Setup")
    var
        RetentionPeriod: Record "Retention Period";
    begin
        if RetentionPeriod.Get(RetentionPolicySetup."Retention Period") then
            ValidateRetentionPeriod(RetentionPeriod, RetentionPolicySetup."Table Id");
    end;

    procedure ValidateRetentionPeriod(RetentionPolicySetupLine: Record "Retention Policy Setup Line")
    var
        RetentionPeriod: Record "Retention Period";
    begin
        if RetentionPeriod.Get(RetentionPolicySetupLine."Retention Period") then
            ValidateRetentionPeriod(RetentionPeriod, RetentionPolicySetupLine."Table ID");
    end;

    local procedure ValidateRetentionPeriod(var RetentionPeriod: Record "Retention Period"; TableId: Integer)
    var
        RetenPolAllowedTables: Codeunit "Reten. Pol. Allowed Tables";
        RetentionPolicyLog: Codeunit "Retention Policy Log";
        RetentionPeriodInterface: Interface "Retention Period";
        ExpirationDate: Date;
        MinExpirationDate: Date;
    begin
        RetentionPeriodInterface := RetentionPeriod."Retention Period";
        ExpirationDate := RetentionPeriodInterface.CalculateExpirationDate(RetentionPeriod);
        if ExpirationDate = 99991231D then // "Never Delete"
            exit;
        MinExpirationDate := RetenPolAllowedTables.CalcMinimumExpirationDate(TableId);
        if ExpirationDate > MinExpirationDate then
            RetentionPolicyLog.LogError(LogCategory(), StrSubstNo(MinExpirationDateErr, RetenPolAllowedTables.GetMandatoryMinimumRetentionDays(TableId), MinExpirationDate));
    end;

    procedure LogCategory(): Enum "Retention Policy Log Category"
    var
        RetentionPolicyLogCategory: Enum "Retention Policy Log Category";
    begin
        exit(RetentionPolicyLogCategory::"Retention Policy - Setup")
    end;

    procedure AddRetentionPolicyOnRegisterManualSetup(GuidedExperience: Codeunit "Guided Experience")
    var
        ManualSetupCategory: Enum "Manual Setup Category";
        CurrModuleInfo: ModuleInfo;
    begin
        NavApp.GetCurrentModuleInfo(CurrModuleInfo);
        GuidedExperience.InsertManualSetup(ManualSetupTitleTxt, ManualSetupShortTitleTxt, ManualSetupDescriptionTxt, 5, ObjectType::Page,
            Page::"Retention Policy Setup List", ManualSetupCategory::Uncategorized, ManualSetupKeyWordsTxt);
    end;

    procedure VerifyRetentionPolicySetupOnbeforeDeleteRetentionPeriod(var RetentionPeriod: Record "Retention Period")
    var
        RetentionPolicySetup: Record "Retention Policy Setup";
        RetentionPolicySetupLine: Record "Retention Policy Setup Line";
        RetentionPolicyLog: Codeunit "Retention Policy Log";
    begin
        if RetentionPeriod.IsTemporary() then
            exit;

        if GetCurrentModuleExecutionContext() <> ExecutionContext::Normal then
            exit;

        RetentionPolicySetup.SetRange("Retention Period", RetentionPeriod.Code);
        if not RetentionPolicySetup.IsEmpty() then
            RetentionPolicyLog.LogError(LogCategory(), StrSubstNo(RetentionPeriodUsedErr, RetentionPeriod.Code));

        RetentionPolicySetupLine.SetRange("Retention Period", RetentionPeriod.Code);
        if not RetentionPolicySetupLine.IsEmpty() then
            RetentionPolicyLog.LogError(LogCategory(), StrSubstNo(RetentionPeriodUsedErr, RetentionPeriod.Code));
    end;

    procedure VerifyRetentionPolicySetupOnBeforeModifyRetentionPeriod(var RetentionPeriod: Record "Retention Period")
    var
        RetentionPolicySetupLine: Record "Retention Policy Setup Line";
        RetentionPolicyLog: Codeunit "Retention Policy Log";
    begin
        if RetentionPeriod.IsTemporary() then
            exit;

        if GetCurrentModuleExecutionContext() <> ExecutionContext::Normal then
            exit;

        RetentionPolicySetupLine.SetRange("Retention Period", RetentionPeriod.Code);
        RetentionPolicySetupLine.SetRange(Locked, true);
        if not RetentionPolicySetupLine.IsEmpty() then
            RetentionPolicyLog.LogError(LogCategory(), StrSubstNo(RetentionPeriodLockedErr, RetentionPeriod.Code));
    end;

    procedure VerifyRetentionPolicyAllowedTablesOnBeforeInsertRetenPolSetup(var RetentionPolicySetup: Record "Retention Policy Setup")
    var
        RetentionPolicyLog: Codeunit "Retention Policy Log";
        RetenPolAllowedTables: Codeunit "Reten. Pol. Allowed Tables";
        RetenPolAllowedTblImpl: Codeunit "Reten. Pol. Allowed Tbl. Impl.";
        TableFilters: JsonArray;
    begin
        if RetentionPolicySetup.IsTemporary() then
            exit;

        RetentionPolicySetup.CalcFields("Table Caption");
        if not RetenPolAllowedTables.IsAllowedTable(RetentionPolicySetup."Table Id") then
            RetentionPolicyLog.LogError(LogCategory(), StrSubstNo(TableNotAllowedErrorLbl, RetentionPolicySetup."Table Id", RetentionPolicySetup."Table Caption"));
        TableFilters := RetenPolAllowedTblImpl.GetTableFilters(RetentionPolicySetup."Table Id");
        if TableFilters.Count <> 0 then
            RetentionPolicySetup."Apply to all records" := false;
    end;

    procedure InsertDefaultTableFiltersOnAfterInsertRetenPolSetup(var RetentionPolicySetup: Record "Retention Policy Setup")
    var
        RetentionPolicySetupLine: Record "Retention Policy Setup Line";
        RetenPolAllowedTblImpl: Codeunit "Reten. Pol. Allowed Tbl. Impl.";
        RetentionPolicyLog: Codeunit "Retention Policy Log";
        RetPeriodCalc: DateFormula;
        TableFilters: JsonArray;
        JsonToken: JsonToken;
        JsonObject: JsonObject;
        OutStream: OutStream;
        i: Integer;
        TableId: Integer;
        RetentionPeriodEnum: Enum "Retention Period Enum";
        DateFieldNo: Integer;
        Enabled: Boolean;
        Locked: Boolean;
        TableFilter: Text;
    begin
        if RetentionPolicySetup.IsTemporary then
            exit;

        TableFilters := RetenPolAllowedTblImpl.GetTableFilters(RetentionPolicySetup."Table Id");
        if TableFilters.Count = 0 then
            exit;

        RetentionPolicySetupLine.SetRange("Table ID", RetentionPolicySetup."Table Id");
        RetentionPolicySetupLine.Validate("Table ID", RetentionPolicySetup."Table Id");
        if RetentionPolicySetupLine.FindLast() then;
        for i := 1 to TableFilters.Count() do begin
            RetentionPolicySetupLine.Init();
            RetentionPolicySetupLine."Line No." += 10000;
            TableFilters.Get(i - 1, JsonToken);
            JsonObject := JsonToken.AsObject();
            RetenPolAllowedTblImpl.ParseTableFilter(JsonObject, TableId, RetentionPeriodEnum, RetPeriodCalc, DateFieldNo, Enabled, Locked, TableFilter);
            if TableId <> RetentionPolicySetupLine."Table ID" then
                RetentionPolicyLog.LogWarning(LogCategory(), StrSubstNo(RetenPolAllowedTableFilterMismatchLbl, RetentionPolicySetupLine."Table ID", TableId))
            else begin
                RetentionPolicySetupLine.Validate("Table ID", TableId);
                RetentionPolicySetupLine.Validate("Retention Period", FindOrCreateRetentionPeriod(RetentionPeriodEnum, RetPeriodCalc));
                RetentionPolicySetupLine.Validate("Date Field No.", DateFieldNo);
                RetentionPolicySetupLine.Validate(Locked, Locked);
                if RetentionPolicySetupLine.IsLocked() then
                    RetentionPolicySetupLine.Validate(Enabled, true)
                else
                    RetentionPolicySetupLine.Validate(Enabled, Enabled);
                RetentionPolicySetupLine."Table Filter".CreateOutStream(OutStream, TextEncoding::UTF8);
                OutStream.Write(TableFilter);
                RetentionPolicySetupLine."Table Filter Text" := RetentionPolicySetupLine.GetTableFilterText();
                RetentionPolicySetupLine.Insert(true);
            end;
        end;
    end;

    procedure FindOrCreateRetentionPeriod(RetentionPeriodEnum: Enum "Retention Period Enum"): Code[20]
    var
        RetPeriodCalc: DateFormula;
    begin
        Evaluate(RetPeriodCalc, '');
        exit(FindOrCreateRetentionPeriod(RetentionPeriodEnum, RetPeriodCalc))
    end;

    procedure FindOrCreateRetentionPeriod(RetentionPeriodEnum: Enum "Retention Period Enum"; RetPeriodCalc: DateFormula): Code[20]
    begin
        exit(FindOrCreateRetentionPeriod('', RetentionPeriodEnum, RetPeriodCalc))
    end;

    procedure FindOrCreateRetentionPeriod(CodeName: Code[20]; RetentionPeriodEnum: Enum "Retention Period Enum"; RetPeriodCalc: DateFormula): Code[20]
    var
        RetentionPeriod: Record "Retention Period";
    begin
        // find
        RetentionPeriod.SetRange("Retention Period", RetentionPeriodEnum);
        if RetentionPeriodEnum = RetentionPeriodEnum::Custom then
            RetentionPeriod.SetRange("Ret. Period Calculation", RetPeriodCalc);
        if RetentionPeriod.FindFirst() then
            exit(RetentionPeriod.Code);

        // create
        if CodeName = '' then
            RetentionPeriod.Code := CreateUniqueRetentionPeriodCode(RetentionPeriodEnum)
        else
            RetentionPeriod.Code := CodeName;
        RetentionPeriod.Description := CopyStr(Format(RetentionPeriodEnum), 1, MaxStrLen(RetentionPeriod.Description));
        RetentionPeriod.Validate("Retention Period", RetentionPeriodEnum);
        if RetentionPeriod."Retention Period" = RetentionPeriod."Retention Period"::Custom then
            RetentionPeriod.Validate("Ret. Period Calculation", RetPeriodCalc);
        RetentionPeriod.Insert(true);
        exit(RetentionPeriod.Code);
    end;

    local procedure CreateUniqueRetentionPeriodCode(RetentionPeriodEnum: Enum "Retention Period Enum") RetentionPeriodCode: Code[20]
    var
        RetentionPeriod: Record "Retention Period";
    begin
        RetentionPeriodCode := CopyStr(Format(RetentionPeriodEnum), 1, MaxStrLen(RetentionPeriodCode));
        if RetentionPeriod.Get(RetentionPeriodCode) then begin
            // ensure a unique code
            RetentionPeriodCode := CopyStr(Format(RetentionPeriodEnum), 1, MaxStrLen(RetentionPeriodCode) - 2) + '01';
            while RetentionPeriod.Get(RetentionPeriodCode) do
                RetentionPeriodCode := IncStr(RetentionPeriodCode);
        end;
    end;

    procedure DeleteRetentionPolicySetup(var RetentionPolicySetup: Record "Retention Policy Setup")
    var
        RetentionPolicySetupLine: Record "Retention Policy Setup Line";
        RetentionPolicySetupImpl: Codeunit "Retention Policy Setup Impl.";
    begin
        if not RetentionPolicySetup.IsTemporary() then begin
            RetentionPolicySetupLine.SetRange("Table ID", RetentionPolicySetup."Table Id");
            if not RetentionPolicySetupLine.IsEmpty() then begin
                BindSubscription(RetentionPolicySetupImpl);
                RetentionPolicySetupImpl.AddTableIdToDeleteAllowedList(RetentionPolicySetup."Table Id");
                RetentionPolicySetupLine.DeleteAll(true);
                UnbindSubscription(RetentionPolicySetupImpl);
            end;
        end;
    end;

    procedure AddTableIdToDeleteAllowedList(TableId: Integer)
    begin
        DeleteAllowedList.Add(TableId);
    end;

    procedure NotifyOnMissingReadPermission(TableId: Integer): Guid
    var
        RetentionPolicySetup: Record "Retention Policy Setup";
        RetenPolAllowedTables: Codeunit "Reten. Pol. Allowed Tables";
        ReadPermissionNotification: Notification;
        RetenPolFiltering: Interface "Reten. Pol. Filtering";
    begin
        RetenPolFiltering := RetenPolAllowedTables.GetRetenPolFiltering(TableId);
        if RetenPolFiltering.HasReadPermission(TableId) then
            exit;

        if RetentionPolicySetup.Get(TableId) then
            RetentionPolicySetup.CalcFields("Table Caption");

        ReadPermissionNotification.Id := CreateGuid();
        ReadPermissionNotification.Message(StrSubstNo(ReadPermissionNotificationLbl, TableId, RetentionPolicySetup."Table Caption"));
        ReadPermissionNotification.Send();
        exit(ReadPermissionNotification.Id())
    end;

    /// <Summary>
    /// This is an internal event that only this module is allowed to subscribe to. It is raised by a subscriber to the OnBeforeDeleteEvent of the Retention Policy Setup Line table.
    /// The manual subscriber AllowRetentionPolicySetupLineDelete is bound just before the DeleteAll() in procedure DeleteRetentionPolicySetup.
    /// These elements combined are to ensure that only procedure DeleteRetentionPolicySetup can delete locked lines from the table.
    /// You should only be able to delete locked lines when deleting the 'header' Retention Policy Setup record.
    /// </Summary>
    [InternalEvent(false)]
    internal procedure OnVerifyDeleteAllowed(TableId: Integer; var DeleteAllowed: Boolean)
    begin
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Retention Policy Setup Impl.", OnVerifyDeleteAllowed, '', false, false)]
    local procedure AllowRetentionPolicySetupLineDelete(TableId: Integer; var DeleteAllowed: Boolean)
    begin
        DeleteAllowed := DeleteAllowedList.Contains(TableId);
    end;

    procedure CheckRecordLockedOnRetentionPolicySetupLine(RetentionPolicySetupLine: Record "Retention Policy Setup Line")
    var
        RetentionPolicyLog: Codeunit "Retention Policy Log";
        DeleteAllowed: Boolean;
    begin
        if RetentionPolicySetupLine.IsTemporary then
            exit;

        if RetentionPolicySetupLine.IsLocked() then begin
            RetentionPolicySetupLine.CalcFields("Table Caption");
            OnVerifyDeleteAllowed(RetentionPolicySetupLine."Table ID", DeleteAllowed);
            if DeleteAllowed then begin
                RetentionPolicyLog.LogInfo(LogCategory(), StrSubstNo(DeleteAllowedInfoLbl, RetentionPolicySetupLine."Table ID", RetentionPolicySetupLine."Table Caption"));
                exit; // allow delete
            end;
            RetentionPolicyLog.LogError(LogCategory(), StrSubstNo(RetentionPolicySetupLineLockedErr, RetentionPolicySetupLine."Table ID", RetentionPolicySetupLine."Table Caption"));
        end;
    end;
}