Codeunit 9200 Matrix Management, source in 29

Source29

src/Layers/W1/BaseApp/Finance/Analysis/MatrixManagement.Codeunit.al869 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 Microsoft.Finance.Analysis;

using Microsoft.Finance.Currency;
using Microsoft.Finance.Dimension;
using Microsoft.Finance.GeneralLedger.Setup;
using Microsoft.Foundation.Enums;
using Microsoft.Foundation.Period;
using System.Utilities;

/// <summary>
/// Provides matrix data generation and formatting functionality for analysis and reporting features.
/// Handles period-based and dimension-based matrix column creation with various formatting options.
/// </summary>
/// <remarks>
/// Core utility codeunit for matrix reports and analysis pages. Supports period matrix generation,
/// dimension matrix creation, and amount formatting with rounding factors. Used extensively by analysis views,
/// account schedules, and financial reporting features.
/// </remarks>
codeunit 9200 "Matrix Management"
{

    trigger OnRun()
    begin
    end;

    var
        GeneralLedgerSetup: Record "General Ledger Setup";
        GLSetupRead: Boolean;

#pragma warning disable AA0074
        Text001: Label 'The previous column set could not be found.';
        Text002: Label 'The period could not be found.';
        Text003: Label 'There are no Calendar entries within the filter.';
#pragma warning restore AA0074
        RoundingFormatTxt: Label '<Precision,%1><Standard Format,0>%2', Locked = true;
        NegativeInParenthesesFormatTxt: Label ';(#,##0.00)', Locked = true;

    /// <summary>
    /// Sets up period-based column set for matrix reports with date filters and navigation.
    /// </summary>
    /// <param name="DateFilter">Date filter expression for period selection</param>
    /// <param name="PeriodType">Type of period (day, week, month, quarter, year)</param>
    /// <param name="Direction">Direction for period navigation (backward or forward)</param>
    /// <param name="FirstColumn">Returns the first date in the column set</param>
    /// <param name="LastColumn">Returns the last date in the column set</param>
    /// <param name="NoOfColumns">Number of columns to generate in the matrix</param>
    procedure SetPeriodColumnSet(DateFilter: Text; PeriodType: Enum "Analysis Period Type"; Direction: Option Backward,Forward; var FirstColumn: Date; var LastColumn: Date; NoOfColumns: Integer)
    var
        Period: Record Date;
        PeriodPageMgt: Codeunit PeriodPageManagement;
        Steps: Integer;
        TmpFirstColumn: Date;
        TmpLastColumn: Date;
    begin
        Period.SetRange("Period Type", PeriodType);
        if DateFilter = '' then begin
            Period."Period Start" := WorkDate();
            if PeriodPageMgt.FindDate('<=', Period, PeriodType) then
                Steps := 1;
            PeriodPageMgt.NextDate(Steps, Period, PeriodType);
            DateFilter := '>=' + Format(Period."Period Start");
        end else begin
            Period.SetFilter("Period Start", DateFilter);
            Period.Find('-');
        end;

        if (Format(FirstColumn) = '') and (Format(LastColumn) = '') then begin
            FirstColumn := Period."Period Start";
            Period.Next(NoOfColumns - 1);
            LastColumn := Period."Period Start";
            exit;
        end;

        if Direction = Direction::Forward then begin
            Period.SetFilter("Period Start", DateFilter);
            if Period.Get(PeriodType, LastColumn) then
                Period.Next();
            TmpFirstColumn := Period."Period Start";
            Period.Next(NoOfColumns - 1);
            TmpLastColumn := Period."Period Start";
            if TmpFirstColumn <> LastColumn then begin
                FirstColumn := TmpFirstColumn;
                LastColumn := TmpLastColumn;
            end;
            exit;
        end;

        if Direction = Direction::Backward then begin
            if Period.Get(PeriodType, FirstColumn) then
                Period.Next(-1);
            TmpLastColumn := Period."Period Start";
            Period.Next(-NoOfColumns + 1);
            TmpFirstColumn := Period."Period Start";
            if TmpLastColumn <> FirstColumn then begin
                FirstColumn := TmpFirstColumn;
                LastColumn := TmpLastColumn;
            end;
        end;
    end;

    /// <summary>
    /// Sets up dimension-based column set for matrix reports with dimension value navigation.
    /// </summary>
    /// <param name="DimensionCode">Dimension code to create columns for</param>
    /// <param name="DimFilter">Filter expression for dimension values</param>
    /// <param name="SetWanted">Navigation direction (Initial, Same, Next, Previous, NextColumn, PreviousColumn)</param>
    /// <param name="RecordPosition">Current record position for navigation</param>
    /// <param name="FirstColumn">Returns the first dimension value code in the column set</param>
    /// <param name="LastColumn">Returns the last dimension value code in the column set</param>
    /// <param name="NoOfColumns">Number of columns to generate in the matrix</param>
    procedure SetDimColumnSet(DimensionCode: Code[20]; DimFilter: Text; SetWanted: Option; var RecordPosition: Text; var FirstColumn: Text; var LastColumn: Text; NoOfColumns: Integer)
    var
        DimVal: Record "Dimension Value";
        TmpFirstColumn: Text[1024];
        TmpLastColumn: Text[1024];
        TmpSteps: Integer;
    begin
        DimVal.SetRange("Dimension Code", DimensionCode);
        if DimFilter <> '' then
            DimVal.SetFilter(Code, DimFilter);
        OnSetDimColumnSetOnAfterDimValSetFilters(DimensionCode, DimFilter, DimVal);

        case "Matrix Page Step Type".FromInteger(SetWanted) of
            "Matrix Page Step Type"::Initial:
                if DimVal.Find('-') then begin
                    RecordPosition := DimVal.GetPosition();
                    FirstColumn := DimVal.Code;
                    TmpSteps := DimVal.Next(NoOfColumns - 1);
                    LastColumn := DimVal.Code;
                end;
            "Matrix Page Step Type"::Same:
                if RecordPosition <> '' then begin
                    DimVal.SetPosition(RecordPosition);
                    DimVal.Find('=');
                    FirstColumn := DimVal.Code;
                    TmpSteps := DimVal.Next(NoOfColumns - 1);
                    LastColumn := DimVal.Code;
                end;
            "Matrix Page Step Type"::Next:
                if RecordPosition <> '' then begin
                    DimVal.SetPosition(RecordPosition);
                    DimVal.Find('=');
                    if DimVal.Next(NoOfColumns) <> 0 then begin
                        RecordPosition := DimVal.GetPosition();
                        TmpFirstColumn := DimVal.Code;
                        TmpSteps := DimVal.Next(NoOfColumns - 1);
                        TmpLastColumn := DimVal.Code;
                        if TmpFirstColumn <> LastColumn then begin
                            FirstColumn := TmpFirstColumn;
                            LastColumn := TmpLastColumn;
                        end;
                    end else
                        SetDimColumnSet(
                            DimensionCode, DimFilter, "Matrix Page Step Type"::Same.AsInteger(), RecordPosition, FirstColumn, LastColumn, NoOfColumns);
                end;
            "Matrix Page Step Type"::Previous:
                if RecordPosition <> '' then begin
                    DimVal.SetPosition(RecordPosition);
                    DimVal.Find('=');
                    if DimVal.Next(-1) <> 0 then begin
                        TmpLastColumn := DimVal.Code;
                        TmpSteps := DimVal.Next(-NoOfColumns + 1);
                        RecordPosition := DimVal.GetPosition();
                        TmpFirstColumn := DimVal.Code;
                        if TmpLastColumn <> FirstColumn then begin
                            FirstColumn := TmpFirstColumn;
                            LastColumn := TmpLastColumn;
                        end;
                    end else
                        SetDimColumnSet(
                            DimensionCode, DimFilter, "Matrix Page Step Type"::Same.AsInteger(), RecordPosition, FirstColumn, LastColumn, NoOfColumns);
                end;
            "Matrix Page Step Type"::NextColumn:
                if RecordPosition <> '' then begin
                    DimVal.SetPosition(RecordPosition);
                    DimVal.Find('=');
                    if DimVal.Next() <> 0 then begin
                        RecordPosition := DimVal.GetPosition();
                        TmpFirstColumn := DimVal.Code;
                        TmpSteps := DimVal.Next(NoOfColumns - 1);
                        TmpLastColumn := DimVal.Code;
                        if TmpFirstColumn <> LastColumn then begin
                            FirstColumn := TmpFirstColumn;
                            LastColumn := TmpLastColumn;
                        end;
                    end else
                        SetDimColumnSet(
                            DimensionCode, DimFilter, "Matrix Page Step Type"::Same.AsInteger(), RecordPosition, FirstColumn, LastColumn, NoOfColumns);
                end;
            "Matrix Page Step Type"::PreviousColumn:
                if RecordPosition <> '' then begin
                    DimVal.SetPosition(RecordPosition);
#pragma warning disable AA0181, AA0233 // Positional Find() paired with Next(); suppression tracked for follow-up
                    DimVal.Find('=');
                    if DimVal.Next(-1) <> 0 then begin
#pragma warning restore AA0181, AA0233
                        RecordPosition := DimVal.GetPosition();
                        TmpFirstColumn := DimVal.Code;
                        TmpSteps := DimVal.Next(NoOfColumns - 1);
                        TmpLastColumn := DimVal.Code;
                        if TmpLastColumn <> FirstColumn then begin
                            FirstColumn := TmpFirstColumn;
                            LastColumn := TmpLastColumn;
                        end;
                    end else
                        SetDimColumnSet(
                            DimensionCode, DimFilter, "Matrix Page Step Type"::Same.AsInteger(), RecordPosition, FirstColumn, LastColumn, NoOfColumns);
                end;
        end;

        if Abs(TmpSteps) <> NoOfColumns then
            NoOfColumns := Abs(TmpSteps);
    end;

    /// <summary>
    /// Generates matrix data for dimension captions and records within a specified range.
    /// </summary>
    /// <param name="CaptionSet">Array to store column captions</param>
    /// <param name="MatrixRecords">Array to store dimension code buffer records</param>
    /// <param name="DimensionCode">Dimension code to generate data for</param>
    /// <param name="FirstColumn">First dimension value in the range</param>
    /// <param name="LastColumn">Last dimension value in the range</param>
    /// <param name="NumberOfColumns">Returns the number of columns generated</param>
    /// <param name="ShowColumnName">Whether to show dimension value names instead of codes</param>
    /// <param name="CaptionRange">Returns the caption range text</param>
    /// <param name="DimensionValueFilter">Filter for dimension values</param>
    procedure DimToCaptions(var CaptionSet: array[32] of Text[80]; var MatrixRecords: array[32] of Record "Dimension Code Buffer"; DimensionCode: Code[20]; FirstColumn: Text; LastColumn: Text; var NumberOfColumns: Integer; ShowColumnName: Boolean; var CaptionRange: Text; DimensionValueFilter: Text)
    var
        DimensionValue: Record "Dimension Value";
        i: Integer;
    begin
        DimensionValue.SetRange("Dimension Code", DimensionCode);
        DimensionValue.SetRange(Code, FirstColumn, LastColumn);
        DimensionValue.FilterGroup(7);
        if DimensionValueFilter <> '' then
            DimensionValue.SetFilter(Code, DimensionValueFilter);
        DimensionValue.FilterGroup(0);

        OnDimToCaptionsOnAfterDimensionValueSetFiltersOnBeforeFindSet(DimensionValue);

        i := 0;
        if DimensionValue.FindSet() then
            repeat
                i := i + 1;
                MatrixRecords[i].Code := DimensionValue.Code;
                MatrixRecords[i].Name := DimensionValue.Name;
                MatrixRecords[i].Totaling := DimensionValue.Totaling;
                if ShowColumnName then
                    CaptionSet[i] := DimensionValue.Name
                else
                    CaptionSet[i] := DimensionValue.Code
            until (i = ArrayLen(CaptionSet)) or (DimensionValue.Next() = 0);

        NumberOfColumns := i;

        if NumberOfColumns > 1 then
            CaptionRange := CopyStr(CaptionSet[1] + '..' + CaptionSet[NumberOfColumns], 1, MaxStrLen(CaptionRange))
        else
            CaptionRange := CaptionSet[1];
    end;

    /// <summary>
    /// Generates matrix data with standard caption length limit for record navigation.
    /// </summary>
    /// <param name="RecRef">Record reference to generate matrix data from</param>
    /// <param name="SetWanted">Navigation direction for matrix data</param>
    /// <param name="MaximumSetLength">Maximum number of records to include</param>
    /// <param name="CaptionFieldNo">Field number to use for captions</param>
    /// <param name="RecordPosition">Current record position for navigation</param>
    /// <param name="CaptionSet">Array to store column captions</param>
    /// <param name="CaptionRange">Returns the caption range text</param>
    /// <param name="CurrSetLength">Returns the current set length</param>
    procedure GenerateMatrixData(var RecRef: RecordRef; SetWanted: Option; MaximumSetLength: Integer; CaptionFieldNo: Integer; var RecordPosition: Text; var CaptionSet: array[32] of Text[80]; var CaptionRange: Text; var CurrSetLength: Integer)
    begin
        GenerateMatrixDataExtended(
          RecRef, SetWanted, MaximumSetLength, CaptionFieldNo, RecordPosition, CaptionSet, CaptionRange, CurrSetLength, 80);
    end;

    /// <summary>
    /// Generates matrix data with extended caption length support for record navigation.
    /// </summary>
    /// <param name="RecRef">Record reference to generate matrix data from</param>
    /// <param name="SetWanted">Navigation direction for matrix data</param>
    /// <param name="MaximumSetLength">Maximum number of records to include</param>
    /// <param name="CaptionFieldNo">Field number to use for captions</param>
    /// <param name="RecordPosition">Current record position for navigation</param>
    /// <param name="CaptionSet">Array to store column captions</param>
    /// <param name="CaptionRange">Returns the caption range text</param>
    /// <param name="CurrSetLength">Returns the current set length</param>
    /// <param name="MaxCaptionLength">Maximum length for caption text</param>
    procedure GenerateMatrixDataExtended(var RecRef: RecordRef; SetWanted: Option; MaximumSetLength: Integer; CaptionFieldNo: Integer; var RecordPosition: Text; var CaptionSet: array[32] of Text; var CaptionRange: Text; var CurrSetLength: Integer; MaxCaptionLength: Integer)
    var
        Steps: Integer;
        Caption: Text;
        IsHandled: Boolean;
    begin
        Clear(CaptionSet);
        CaptionRange := '';
        CurrSetLength := 0;

        if RecRef.IsEmpty() then begin
            RecordPosition := '';
            exit;
        end;

        case "Matrix Page Step Type".FromInteger(SetWanted) of
            "Matrix Page Step Type"::Initial:
                RecRef.FindFirst();
            "Matrix Page Step Type"::Previous:
                begin
                    RecRef.SetPosition(RecordPosition);
                    RecRef.Get(RecRef.RecordId);
                    Steps := RecRef.Next(-MaximumSetLength);
                    IsHandled := false;
                    OnGenerateMatrixDataExtendedOnBeforeValidatePreviousStep(Steps, MaximumSetLength, IsHandled);
                    if not IsHandled then
                        if not (Steps in [-MaximumSetLength .. 0]) then
                            Error(Text001);
                end;
            "Matrix Page Step Type"::Same:
                begin
                    RecRef.SetPosition(RecordPosition);
                    RecRef.Get(RecRef.RecordId);
                end;
            "Matrix Page Step Type"::Next:
                begin
                    RecRef.SetPosition(RecordPosition);
                    RecRef.Get(RecRef.RecordId);
                    if not (RecRef.Next(MaximumSetLength) = MaximumSetLength) then begin
                        RecRef.SetPosition(RecordPosition);
                        RecRef.Get(RecRef.RecordId);
                    end;
                end;
            "Matrix Page Step Type"::PreviousColumn:
                begin
                    RecRef.SetPosition(RecordPosition);
                    RecRef.Get(RecRef.RecordId);
                    Steps := RecRef.Next(-1);
                    if not (Steps in [-1, 0]) then
                        Error(Text001);
                end;
            "Matrix Page Step Type"::NextColumn:
                begin
                    RecRef.SetPosition(RecordPosition);
                    RecRef.Get(RecRef.RecordId);
                    if not (RecRef.Next(1) = 1) then begin
                        RecRef.SetPosition(RecordPosition);
                        RecRef.Get(RecRef.RecordId);
                    end;
                end;
            else
                OnMatrixPageStepTypeInGenerateMatrixDataExtended(SetWanted, MaximumSetLength, RecRef);
        end;

        RecordPosition := RecRef.GetPosition();

        repeat
            CurrSetLength := CurrSetLength + 1;
            Caption := GetCaption(RecRef, CaptionFieldNo);
            if StrLen(Caption) <= MaxCaptionLength then
                CaptionSet[CurrSetLength] := CopyStr(Caption, 1, MaxCaptionLength)
            else
                CaptionSet[CurrSetLength] := CopyStr(Caption, 1, MaxCaptionLength - 3) + '...';
        until (CurrSetLength = MaximumSetLength) or (RecRef.Next() <> 1);

        if CurrSetLength = 1 then
            CaptionRange := CaptionSet[1]
        else
            CaptionRange := CaptionSet[1] + '..' + CaptionSet[CurrSetLength];
    end;

    local procedure GetCaption(var RecRef: RecordRef; CaptionFieldNo: Integer) Caption: Text;
    begin
        Caption := Format(RecRef.Field(CaptionFieldNo).Value);
        OnAfterGetCaption(RecRef, CaptionFieldNo, Caption);
    end;

    /// <summary>
    /// Generates period-based matrix data for analysis views and reports with time-based columns.
    /// Creates column captions, date ranges, and period records for matrix displays.
    /// </summary>
    /// <param name="SetWanted">Navigation option (Previous, Same, Next) for matrix column set</param>
    /// <param name="MaximumSetLength">Maximum number of columns to generate for matrix display</param>
    /// <param name="UseNameForCaption">Whether to use period name or period start for column captions</param>
    /// <param name="PeriodType">Analysis period type (Day, Week, Month, Quarter, Year)</param>
    /// <param name="DateFilter">Date filter expression for period selection and boundaries</param>
    /// <param name="RecordPosition">Current position in record set for navigation tracking</param>
    /// <param name="CaptionSet">Array of column captions generated for matrix headers</param>
    /// <param name="CaptionRange">Text range description for matrix column span</param>
    /// <param name="CurrSetLength">Current number of columns generated in the matrix set</param>
    /// <param name="PeriodRecords">Array of period records corresponding to matrix columns</param>
    procedure GeneratePeriodMatrixData(SetWanted: Option; MaximumSetLength: Integer; UseNameForCaption: Boolean; PeriodType: Enum "Analysis Period Type"; DateFilter: Text; var RecordPosition: Text; var CaptionSet: array[32] of Text[80]; var CaptionRange: Text; var CurrSetLength: Integer; var PeriodRecords: array[32] of Record Date temporary)
    var
        Calendar: Record Date;
        PeriodPageMgt: Codeunit PeriodPageManagement;
        Steps: Integer;
        IsHandled: Boolean;
    begin
        Clear(CaptionSet);
        CaptionRange := '';
        CurrSetLength := 0;
        Clear(PeriodRecords);
        Clear(Calendar);
        Clear(PeriodPageMgt);

        Calendar.SetFilter("Period Start", PeriodPageMgt.GetFullPeriodDateFilter(PeriodType, DateFilter));

        if not FindDate('-', Calendar, PeriodType, false) then begin
            RecordPosition := '';
            Error(Text003);
        end;

        OnGeneratePeriodMatrixDataOnBeforeFindDateBasedOnStepType(SetWanted, Calendar, PeriodType);

        case "Matrix Page Step Type".FromInteger(SetWanted) of
            "Matrix Page Step Type"::Initial:
                begin
                    if (PeriodType = PeriodType::"Accounting Period") or (DateFilter <> '') then
                        FindDate('-', Calendar, PeriodType, true)
                    else
                        Calendar."Period Start" := 0D;
                    FindDate('=><', Calendar, PeriodType, true);
                end;
            "Matrix Page Step Type"::Previous:
                begin
                    Calendar.SetPosition(RecordPosition);
                    FindDate('=', Calendar, PeriodType, true);
                    Steps := PeriodPageMgt.NextDate(-MaximumSetLength, Calendar, PeriodType);
                    if not (Steps in [-MaximumSetLength .. 0]) then
                        Error(Text001);
                end;
            "Matrix Page Step Type"::PreviousColumn:
                begin
                    Calendar.SetPosition(RecordPosition);
                    FindDate('=', Calendar, PeriodType, true);
                    Steps := PeriodPageMgt.NextDate(-1, Calendar, PeriodType);
                    if not (Steps in [-1, 0]) then
                        Error(Text001);
                end;
            "Matrix Page Step Type"::NextColumn:
                begin
                    Calendar.SetPosition(RecordPosition);
                    FindDate('=', Calendar, PeriodType, true);
                    if not (PeriodPageMgt.NextDate(1, Calendar, PeriodType) = 1) then begin
                        Calendar.SetPosition(RecordPosition);
                        FindDate('=', Calendar, PeriodType, true);
                    end;
                end;
            "Matrix Page Step Type"::Same:
                begin
                    Calendar.SetPosition(RecordPosition);
                    FindDate('=', Calendar, PeriodType, true)
                end;
            "Matrix Page Step Type"::Next:
                begin
                    Calendar.SetPosition(RecordPosition);
                    FindDate('=', Calendar, PeriodType, true);
                    if not (PeriodPageMgt.NextDate(MaximumSetLength, Calendar, PeriodType) = MaximumSetLength) then begin
                        Calendar.SetPosition(RecordPosition);
                        FindDate('=', Calendar, PeriodType, true);
                    end;
                end;
        end;

        RecordPosition := Calendar.GetPosition();

        repeat
            GeneratePeriodAndCaption(CaptionSet, PeriodRecords, CurrSetLength, Calendar, UseNameForCaption, PeriodType);
        until (CurrSetLength = MaximumSetLength) or (PeriodPageMgt.NextDate(1, Calendar, PeriodType) <> 1);

        if CurrSetLength = 1 then
            CaptionRange := CaptionSet[1]
        else
            CaptionRange := CaptionSet[1] + '..' + CaptionSet[CurrSetLength];

        IsHandled := false;
        OnGeneratePeriodMatrixDataOnBeforeAdjustPeriodWithDateFilter(DateFilter, PeriodRecords, CurrSetLength, IsHandled);
        if not IsHandled then
            AdjustPeriodWithDateFilter(DateFilter, PeriodRecords[1]."Period Start",
              PeriodRecords[CurrSetLength]."Period End");
    end;

    local procedure GeneratePeriodAndCaption(var CaptionSet: array[32] of Text[80]; var PeriodRecords: array[32] of Record Date temporary; var CurrSetLength: Integer; var Calendar: Record Date; UseNameForCaption: Boolean; PeriodType: Enum "Analysis Period Type")
    var
        PeriodPageMgt: Codeunit PeriodPageManagement;
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeGeneratePeriodAndCaption(PeriodType, Calendar, IsHandled, UseNameForCaption, CurrSetLength, CaptionSet, PeriodRecords);
        if IsHandled then
            exit;

        CurrSetLength := CurrSetLength + 1;
        if UseNameForCaption then
            CaptionSet[CurrSetLength] := Format(Calendar."Period Name")
        else
            CaptionSet[CurrSetLength] := PeriodPageMgt.CreatePeriodFormat(PeriodType, Calendar."Period Start");
        PeriodRecords[CurrSetLength].Copy(Calendar);
    end;

    local procedure FindDate(SearchString: Text[3]; var Calendar: Record Date; PeriodType: Enum "Analysis Period Type"; ErrorWhenNotFound: Boolean): Boolean
    var
        PeriodPageMgt: Codeunit PeriodPageManagement;
        Found: Boolean;
    begin
        Clear(PeriodPageMgt);
        Found := PeriodPageMgt.FindDate(SearchString, Calendar, PeriodType);
        if ErrorWhenNotFound and not Found then
            Error(Text002);
        exit(Found);
    end;

    /// <summary>
    /// Adds indentation spaces to a text string for hierarchical display formatting.
    /// Used to create visual hierarchy in matrix displays and analysis reports.
    /// </summary>
    /// <param name="TextString">Text string to indent</param>
    /// <param name="Indentation">Indentation level (multiplied by 2 for spacing)</param>
    procedure SetIndentation(var TextString: Text[1024]; Indentation: Integer)
    var
        Substr: Text[1024];
    begin
        Substr := PadStr(Substr, Indentation * 2, ' ');
        TextString := Substr + TextString;
    end;

    /// <summary>
    /// Gets the primary key range for matrix column generation based on record position and set length.
    /// Calculates the key range for matrix navigation and column boundary determination.
    /// </summary>
    /// <param name="RecRef">Record reference for matrix data source table</param>
    /// <param name="KeyFieldNo">Field number of the primary key field for range calculation</param>
    /// <param name="RecordPosition">Current record position in the matrix navigation</param>
    /// <param name="CurrSetLength">Current matrix column set length for range span</param>
    /// <returns>Primary key range string for matrix column boundaries</returns>
    procedure GetPKRange(var RecRef: RecordRef; KeyFieldNo: Integer; RecordPosition: Text; CurrSetLength: Integer) PKRange: Text[100]
    var
        FieldRef: FieldRef;
        CurFilter: Text;
        RecCount: Integer;
    begin
        RecRef.SetPosition(RecordPosition);
        RecRef.Get(RecRef.RecordId);
        PKRange := Format(RecRef.Field(KeyFieldNo).Value);
        if CurrSetLength = 1 then
            exit(PKRange);
        RecRef.Next(CurrSetLength);
        PKRange := PKRange + '..' + Format(RecRef.Field(KeyFieldNo).Value);
        FieldRef := RecRef.Field(KeyFieldNo);
        CurFilter := FieldRef.GetFilter;
        if CurFilter = '' then
            exit(PKRange);
        FieldRef.SetFilter(PKRange);
        RecCount := RecRef.Count();
        FieldRef.SetFilter(CurFilter);
        if CurrSetLength = RecCount then
            exit(PKRange);
        exit('');
    end;

    /// <summary>
    /// Generates dimension column captions for matrix display based on dimension values and filters.
    /// Creates header text and populates dimension code buffers for matrix column generation.
    /// </summary>
    /// <param name="DimensionCode">Dimension code to generate columns for</param>
    /// <param name="DimFilter">Dimension value filter to restrict column generation</param>
    /// <param name="SetWanted">Navigation direction (First, Previous, Next, Last)</param>
    /// <param name="RecordPosition">Current record position for navigation</param>
    /// <param name="FirstColumn">First column identifier in the set</param>
    /// <param name="LastColumn">Last column identifier in the set</param>
    /// <param name="CaptionSet">Array to store generated column captions</param>
    /// <param name="DimensionCodeBuffer">Array to store dimension code buffer records</param>
    /// <param name="NumberOfColumns">Number of columns generated</param>
    /// <param name="ShowColumnName">Whether to show column names in captions</param>
    /// <param name="CaptionRange">Range text for caption display</param>
    procedure GenerateDimColumnCaption(DimensionCode: Code[20]; DimFilter: Text; SetWanted: Option; var RecordPosition: Text; FirstColumn: Text; LastColumn: Text; var CaptionSet: array[32] of Text[80]; var DimensionCodeBuffer: array[32] of Record "Dimension Code Buffer"; var NumberOfColumns: Integer; ShowColumnName: Boolean; var CaptionRange: Text)
    begin
        SetDimColumnSet(
          DimensionCode, DimFilter, SetWanted, RecordPosition, FirstColumn, LastColumn, NumberOfColumns);
        DimToCaptions(
          CaptionSet, DimensionCodeBuffer, DimensionCode,
          FirstColumn, LastColumn, NumberOfColumns, ShowColumnName, CaptionRange, DimFilter);
    end;

    local procedure AdjustPeriodWithDateFilter(DateFilter: Text; var PeriodStartDate: Date; var PeriodEndDate: Date)
    var
        Period: Record Date;
    begin
        if DateFilter <> '' then begin
            Period.SetFilter("Period End", DateFilter);
            if Period.GetRangeMax("Period End") < PeriodEndDate then
                PeriodEndDate := Period.GetRangeMax("Period End");
            Period.Reset();
            Period.SetFilter("Period Start", DateFilter);
            if Period.GetRangeMin("Period Start") > PeriodStartDate then
                PeriodStartDate := Period.GetRangeMin("Period Start");
        end;
    end;

    /// <summary>
    /// Rounds amounts according to the specified analysis rounding factor for display purposes.
    /// Provides consistent amount rounding for analysis reports and matrix displays.
    /// </summary>
    /// <param name="Amount">Decimal amount to be rounded for analysis display</param>
    /// <param name="RoundingFactor">Analysis rounding factor enum (1, 1000, 1000000, etc.)</param>
    /// <returns>Rounded amount according to the specified rounding factor</returns>
    procedure RoundAmount(Amount: Decimal; RoundingFactor: Enum "Analysis Rounding Factor"): Decimal
    begin
        if Amount = 0 then
            exit(0);

        case RoundingFactor of
            RoundingFactor::"1":
                Amount := Round(Amount, 1);
            RoundingFactor::"1000":
                Amount := Round(Amount / 1000, 0.1);
            RoundingFactor::"1000000":
                Amount := Round(Amount / 1000000, 0.1);
            else
                OnRoundAmountOnElse(Amount, RoundingFactor);
        end;

        OnRoundAmountOnAfterRound(Amount, RoundingFactor);
        exit(Amount);
    end;

    /// <summary>
    /// Formats decimal amount values with specified rounding and currency formatting.
    /// Applies rounding factor and formats the result for display in matrix cells.
    /// </summary>
    /// <param name="Value">Decimal value to format</param>
    /// <param name="RoundingFactor">Rounding factor to apply (1, 1000, 1000000)</param>
    /// <param name="AddCurrency">Whether to include currency formatting</param>
    /// <returns>Formatted amount as text string with appropriate rounding and currency format</returns>
    procedure FormatAmount(Value: Decimal; RoundingFactor: Enum "Analysis Rounding Factor"; AddCurrency: Boolean): Text[30]
    begin
        Value := RoundAmount(Value, RoundingFactor);

        if Value <> 0 then
            exit(Format(Value, 0, FormatRoundingFactor(RoundingFactor, AddCurrency)));
    end;

    local procedure ReadNormalDecimalFormat(AddCurrency: Boolean): Text
    var
        Currency: Record Currency;
    begin
        if not GLSetupRead then begin
            GeneralLedgerSetup.Get();
            GLSetupRead := true;
            if AddCurrency then
                GeneralLedgerSetup.TestField("Additional Reporting Currency");
        end;

        if AddCurrency and
           Currency.Get(GeneralLedgerSetup."Additional Reporting Currency")
        then
            exit(Currency."Amount Decimal Places");

        exit(GeneralLedgerSetup."Amount Decimal Places");
    end;

    /// <summary>
    /// Generates formatting string for rounding factor display with currency support.
    /// Returns format pattern for decimal formatting with appropriate currency symbols.
    /// </summary>
    /// <param name="RoundingFactor">Rounding factor to format for</param>
    /// <param name="AddCurrency">Whether to include currency formatting</param>
    /// <returns>Format string for decimal formatting with rounding factor and currency</returns>
    procedure FormatRoundingFactor(RoundingFactor: Enum "Analysis Rounding Factor"; AddCurrency: Boolean): Text
    begin
        exit(FormatRoundingFactor(RoundingFactor, AddCurrency, Enum::"Analysis Negative Format"::"Minus Sign"));
    end;

    /// <summary>
    /// Generates formatting string for rounding factor display with currency and negative format support.
    /// Returns format pattern for decimal formatting with specified rounding, currency, and negative formatting.
    /// </summary>
    /// <param name="RoundingFactor">Rounding factor to format for</param>
    /// <param name="AddCurrency">Whether to include currency formatting</param>
    /// <param name="NegativeAmountFormat">Format for negative amounts (minus sign, parentheses, etc.)</param>
    /// <returns>Complete format string for decimal formatting with all specified options</returns>
    procedure FormatRoundingFactor(RoundingFactor: Enum "Analysis Rounding Factor"; AddCurrency: Boolean; NegativeAmountFormat: Enum "Analysis Negative Format") Result: Text
    var
        AmountDecimal: Text;
    begin
        case RoundingFactor of
            RoundingFactor::None:
                AmountDecimal := ReadNormalDecimalFormat(AddCurrency);
            RoundingFactor::"1":
                AmountDecimal := Format(0);
            RoundingFactor::"1000", RoundingFactor::"1000000":
                AmountDecimal := Format(1);
            else
                OnFormatRoundingFactorOnElse(AmountDecimal, RoundingFactor);
        end;

        OnFormatRoundingFactorOnAfterSetAmountDecimal(AmountDecimal, RoundingFactor);

        case NegativeAmountFormat of
            NegativeAmountFormat::"Minus Sign":
                Result := StrSubstNo(RoundingFormatTxt, AmountDecimal, '');
            NegativeAmountFormat::"Parentheses":
                Result := StrSubstNo(RoundingFormatTxt, AmountDecimal, NegativeInParenthesesFormatTxt);
            else
                OnFormatRoundingFactorNegativeFormatOnElse(AmountDecimal, NegativeAmountFormat, Result);
        end;
    end;

    /// <summary>
    /// Integration event raised after retrieving caption text for record display.
    /// Enables customization of caption formatting for matrix columns and rows.
    /// </summary>
    /// <param name="RecRef">Record reference for caption generation</param>
    /// <param name="CaptionFieldNo">Field number used for caption generation</param>
    /// <param name="Caption">Caption text that can be modified by subscribers</param>
    [IntegrationEvent(false, false)]
    local procedure OnAfterGetCaption(var RecRef: RecordRef; CaptionFieldNo: Integer; var Caption: Text)
    begin
    end;

    /// <summary>
    /// Integration event raised before generating period-based matrix columns and captions.
    /// Enables custom period generation logic and caption formatting for time-based analysis.
    /// </summary>
    /// <param name="PeriodType">Type of periods to generate (Day, Week, Month, Quarter, Year)</param>
    /// <param name="Calendar">Date record for period calculations</param>
    /// <param name="IsHandled">Set to true to skip standard period generation</param>
    /// <param name="UseNameForCaption">Whether to use period names instead of dates in captions</param>
    /// <param name="CurrSetLength">Current set length for period arrays</param>
    /// <param name="CaptionSet">Array of caption texts for period columns</param>
    /// <param name="PeriodRecords">Array of period date records</param>
    [IntegrationEvent(false, false)]
    local procedure OnBeforeGeneratePeriodAndCaption(PeriodType: Enum "Analysis Period Type"; Calendar: Record Date; var IsHandled: Boolean; UseNameForCaption: Boolean; var CurrSetLength: Integer; var CaptionSet: array[32] of Text[80]; var PeriodRecords: array[32] of Record Date temporary)
    begin
    end;

    /// <summary>
    /// Integration event raised after applying dimension value filters in column set generation.
    /// Enables additional filtering logic for dimension-based matrix columns.
    /// </summary>
    /// <param name="DimensionCode">Dimension code being processed</param>
    /// <param name="DimFilter">Applied dimension filter text</param>
    /// <param name="DimensionValue">Dimension value record with applied filters</param>
    [IntegrationEvent(false, false)]
    local procedure OnSetDimColumnSetOnAfterDimValSetFilters(DimensionCode: Code[20]; DimFilter: Text; var DimensionValue: Record "Dimension Value")
    begin
    end;

    /// <summary>
    /// Integration event for custom rounding factor formatting when standard cases don't apply.
    /// Enables extending rounding factor formatting with custom logic.
    /// </summary>
    /// <param name="AmountDecimal">Amount decimal format text to customize</param>
    /// <param name="RoundingFactor">Rounding factor requiring custom formatting</param>
    [IntegrationEvent(false, false)]
    local procedure OnFormatRoundingFactorOnElse(var AmountDecimal: Text; RoundingFactor: Enum "Analysis Rounding Factor")
    begin
    end;

    /// <summary>
    /// Integration event raised when handling negative format options for rounding factor formatting.
    /// Allows custom formatting logic for negative amounts that don't match standard formats.
    /// </summary>
    /// <param name="AmountDecimal">Decimal amount converted to text for formatting</param>
    /// <param name="NegativeAmountFormat">Negative format option being processed</param>
    /// <param name="Result">Formatted result text that can be modified by subscribers</param>
    [IntegrationEvent(false, false)]
    local procedure OnFormatRoundingFactorNegativeFormatOnElse(AmountDecimal: Text; NegativeAmountFormat: Enum "Analysis Negative Format"; var Result: Text)
    begin
    end;

    /// <summary>
    /// Integration event raised when rounding amounts with custom rounding factors.
    /// Allows custom rounding logic for rounding factors not handled by standard processing.
    /// </summary>
    /// <param name="Amount">Amount to be rounded</param>
    /// <param name="RoundingFactor">Rounding factor option being applied</param>
    [IntegrationEvent(false, false)]
    local procedure OnRoundAmountOnElse(var Amount: Decimal; RoundingFactor: Enum "Analysis Rounding Factor")
    begin
    end;

    /// <summary>
    /// Integration event raised after an amount has been rounded according to the rounding factor.
    /// Allows subscribers to further adjust the rounded amount, for example to round to whole numbers.
    /// </summary>
    /// <param name="Amount">Rounded amount, passed by reference so subscribers can change it.</param>
    /// <param name="RoundingFactor">Rounding factor that was applied.</param>
    [IntegrationEvent(false, false)]
    local procedure OnRoundAmountOnAfterRound(var Amount: Decimal; RoundingFactor: Enum "Analysis Rounding Factor")
    begin
    end;

    /// <summary>
    /// Integration event raised after the decimal format text has been set for the rounding factor and before the negative amount format is applied.
    /// Allows subscribers to change the decimal formatting based on the rounding factor.
    /// </summary>
    /// <param name="AmountDecimal">Decimal format text, passed by reference so subscribers can change it.</param>
    /// <param name="RoundingFactor">Rounding factor being formatted.</param>
    [IntegrationEvent(false, false)]
    local procedure OnFormatRoundingFactorOnAfterSetAmountDecimal(var AmountDecimal: Text; RoundingFactor: Enum "Analysis Rounding Factor")
    begin
    end;

    /// <summary>
    /// Integration event raised before validating previous step in matrix data generation.
    /// Allows custom validation logic for matrix navigation and step processing.
    /// </summary>
    /// <param name="Steps">Number of steps being validated</param>
    /// <param name="MaximumSetLength">Maximum allowed set length for matrix</param>
    /// <param name="IsHandled">Set to true to skip standard validation processing</param>
    [IntegrationEvent(false, false)]
    local procedure OnGenerateMatrixDataExtendedOnBeforeValidatePreviousStep(Steps: Integer; MaximumSetLength: Integer; var IsHandled: Boolean)
    begin
    end;

    /// <summary>
    /// Integration event raised before adjusting period records with date filter in matrix data generation.
    /// Allows custom period adjustment logic for date-filtered matrix generation.
    /// </summary>
    /// <param name="DateFilter">Date filter being applied to period records</param>
    /// <param name="TempPeriodRecords">Array of temporary period records being adjusted</param>
    /// <param name="CurrSetLength">Current set length of period records</param>
    /// <param name="IsHandled">Set to true to skip standard adjustment processing</param>
    [IntegrationEvent(false, false)]
    local procedure OnGeneratePeriodMatrixDataOnBeforeAdjustPeriodWithDateFilter(DateFilter: Text; var TempPeriodRecords: array[32] of Record Date temporary; var CurrSetLength: Integer; var IsHandled: Boolean)
    begin
    end;

    /// <summary>
    /// Integration event raised before finding date based on step type in period matrix data generation.
    /// Allows custom date finding logic for different step types in period-based matrices.
    /// </summary>
    /// <param name="SetWanted">Set option indicating which set is being processed</param>
    /// <param name="CalendarDate">Calendar date record used for date calculations</param>
    /// <param name="PeriodType">Period type controlling date calculations</param>
    [IntegrationEvent(false, false)]
    local procedure OnGeneratePeriodMatrixDataOnBeforeFindDateBasedOnStepType(SetWanted: Option; var CalendarDate: Record Date; PeriodType: Enum "Analysis Period Type")
    begin
    end;

    /// <summary>
    /// Integration event raised when handling matrix page step type in extended matrix data generation.
    /// Allows custom step processing logic for matrix navigation in extended scenarios.
    /// </summary>
    /// <param name="SetWanted">Set option indicating which set is being processed</param>
    /// <param name="MaximumSetLength">Maximum allowed set length for matrix</param>
    /// <param name="RecRef">Record reference for matrix data source</param>
    [IntegrationEvent(false, false)]
    local procedure OnMatrixPageStepTypeInGenerateMatrixDataExtended(SetWanted: Option; MaximumSetLength: Integer; var RecRef: RecordRef)
    begin
    end;

    /// <summary>
    /// Integration event raised after setting filters on dimension value record in dimension to captions procedure.
    /// Allows additional filtering or processing after standard filter application.
    /// </summary>
    /// <param name="DimensionValue">Dimension value record being filtered</param>
    [IntegrationEvent(false, false)]
    local procedure OnDimToCaptionsOnAfterDimensionValueSetFiltersOnBeforeFindSet(var DimensionValue: Record "Dimension Value")
    begin
    end;    
}