Codeunit 8800 Custom Layout Reporting

App
Base Application
Namespace
Microsoft.Foundation.Reporting
Versions
17-28

Procedures, 32Events, 22Obsolete, 2

Versions171819202122232425262728

Source29

Source in 29

src/Layers/W1/BaseApp/Foundation/Reporting/CustomLayoutReporting.Codeunit.al1813 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.Foundation.Reporting;

using Microsoft.EServices.EDocument;
using Microsoft.Finance.GeneralLedger.Journal;
using Microsoft.Purchases.Vendor;
using Microsoft.Sales.Customer;
using Microsoft.Utilities;
using System;
using System.Automation;
using System.Email;
using System.Environment;
using System.Environment.Configuration;
using System.IO;
using System.Reflection;
using System.Security.AccessControl;
using System.Text;
using System.Utilities;

codeunit 8800 "Custom Layout Reporting"
{
    // This codeunit implements batch printing and custom layout association on a per-object basis.
    // Reports may be based on one table, but we may want to specific layout per-customer, or per-vendor.
    // This codeunit provides functions to help manage the association between the table that backs the report and the object we want custom layouts on.
    // 
    // Example code below, for Customer Statements, backed by Customer and associating custom reports per-Customer:
    // 
    // // Get the RecordRef of the table that will source the data for our report - Statements should be backed by Customer
    // RecRef.OPEN(DATABASE::Customer);
    // // Set up the association between the Table that is the source of the report and the association to the table we want layouts for
    // // In this case, we are using Customer for the report and Customer for the layouts.
    // CustomerLayoutReporting.ProcessReport(ReportSelections.Usage::"C.Statement",RecRef,'No.',DATABASE::Customer,'No.',TRUE);

    EventSubscriberInstance = Manual;

    trigger OnRun()
    begin
    end;

    var
        ReportSelections: Record "Report Selections";
        CustomReportSelection: Record "Custom Report Selection";
        ReportLayoutSelection: Record "Report Layout Selection";
        TempNameValueBuffer: Record "Name/Value Buffer" temporary;
        TempEmailNameValueBuffer: Record "Name/Value Buffer" temporary;
        TempEraseFileNameValueBuffer: Record "Name/Value Buffer" temporary;
        TempBlobIndicesNameValueBuffer: Record "Name/Value Buffer" temporary;
        TempNameValueBufferUniqueFiles: Record "Name/Value Buffer" temporary;
        TempBlobList: Codeunit "Temp Blob List";
        RequestPageParametersHelper: Codeunit "Request Page Parameters Helper";
        FileManagement: Codeunit "File Management";
        DataCompression: Codeunit "Data Compression";
        ClientTypeManagement: Codeunit "Client Type Management";
        ErrorContextElement: Codeunit "Error Context Element";
        ErrorMessageManagement: Codeunit "Error Message Management";
        ErrorMessageHandler: Codeunit "Error Message Handler";
        ReportDataRecordRef: RecordRef;
        IteratorRecordRef: RecordRef;
        ReportDataIteratorFieldRef: FieldRef;
        IteratorJoinFieldRef: FieldRef;
        BalAcctTypeFieldRef: FieldRef;
        AcctTypeFieldRef: FieldRef;
        ZipFile: File;
        ZipFileOutStream: OutStream;
        OutputType: Option Print,Preview,PDF,Email,Excel,Word,XML;
        NotInitializedErr: Label 'Report data not initialized.';
        OutputNotSupportedErr: Label 'The chosen output method is not supported.';
        EmailAccountsNotSetupErr: Label 'To send as email, you must register an email account.';
        ReportingType: Option "Object","Layout";
        ZipFileName: Text;
        ZipDownloadTxt: Label 'AllReports.zip';
        Path: Text;
        OutputFileBaseName: Text;
        Initialized: Boolean;
        ReportDataAndIteratorDiffer: Boolean;
        SupressOutput: Boolean;
        PrintIfEmailIsMissing: Boolean;
        TestModeWebClient: Boolean;
        RunReportOncePerFilter: Boolean;
        PathLengthErr: Label 'The file name %1 is too long and cannot be used.', Comment = '%1: a file name, generated by the system';
        ReportFormatNotSupportedErr: Label 'Creation of report with format type %1 is not supported.', Comment = '%1 is the extension for the report.';
        NoOutputErr: Label 'No data exists for the specified report filters.';
        AnyOutputExists: Boolean;
        LastUsedTxt: Label 'Last used options and filters', Comment = 'Translation must match RequestPageLatestSavedSettingsName from Lang.resx';
        WordOutputXmlHasData: Boolean;
        WordOutputXmlHasDataVerified: Boolean;
        IgnoreRequestParameters: Boolean;
        PredefinedRequestParameters: Text;
        EscapeTok: Label '''%1''', Locked = true;
        ErrorForDataOccuredErr: Label 'The error, %1, occurred when running report %2 for %3.', Comment = '%1 - Error text, %2 - Report ID, %3 - Record ID.';
        TableFilterForReportID: Integer;
        TableFilterTok: Label '<?xml version="1.0" standalone="yes"?><ReportParameters id="@%1"><DataItems><DataItem name="Customer">VERSION(1) SORTING(Field1) where(Field1 =1(%2))</DataItem></DataItems></ReportParameters>', Locked = true;
        TableFilterTxt: Text;
        ForTok: Label ' for %1', Comment = '%1: customer name, Sample: Statement for Stan as of 21/02/2020';
        AsOfTok: Label ' as of %1', Comment = '%1: date, Sample: Statement for Stan as of 21/02/2020';
        TargetEmailAddressErr: Label 'The target email address has not been specified on the document layout for %1, %2. //Choose the Document Layouts action on the customer or vendor card to specify the email address.', Comment = '%1 - Source Data RecordID, %2 - Usage';
        TargetEmailAddressNotValidErr: Label 'The target email address "%1" is not valid on the document layout for %2, %3. //Choose the Document Layouts action on the customer or vendor card to adjust the email address.', Comment = '%1 - Target email address, %2 - Source Data RecordID, %3 - Usage';
        DifferentThanFilterTxt: Label '<>%1', Locked = true;

    procedure GetLayoutIteratorKeyFilter(var FilterRecordRef: RecordRef; var FilterRecordKeyFieldRef: FieldRef; CustomReportLayoutCode: Code[20])
    var
        CustomReportSelection2: Record "Custom Report Selection";
        "Filter": Text;
    begin
        // Further filters the items in FilterRecordRef by restricting them only to items who have entries in the "* Report Selection" tables
        // This prevents us from iterating over items that aren't related to the custom layouts
        CustomReportSelection2.SetView(CustomReportSelection.GetView());

        // Set the filters on the "* Report Selection" table and iterate through
        CustomReportSelection2.SetRange("Custom Report Layout Code", CustomReportLayoutCode);

        if CustomReportSelection2.FindSet() then
            case CustomReportSelection2."Source Type" of
                Database::Customer:
                    Filter := GetCustomerFilter(CustomReportSelection2);
                Database::Vendor:
                    Filter := GetVendorFilter(CustomReportSelection2);
                else
                repeat
                    if Filter <> '' then
                        Filter := Filter + '|';
                    Filter := StrSubstNo('%1%2', Filter, CustomReportSelection2."Source No.");
                until CustomReportSelection2.Next() = 0;
            end;

        // Set the more restrictive filter
        SetNextGroupFilter(FilterRecordRef, FilterRecordKeyFieldRef, Filter)
    end;

    local procedure GetCustomerFilter(var CustomReportSelection: Record "Custom Report Selection"): Text
    var
        Customer: Record Customer;
        SelectionFilterMgt: Codeunit SelectionFilterManagement;
        CustomerRecRef: RecordRef;
    begin
        repeat
            Customer.Get(CustomReportSelection."Source No.");
            Customer.Mark(true);
        until CustomReportSelection.Next() = 0;
        Customer.MarkedOnly(true);

        CustomerRecRef.GetTable(Customer);
        exit(SelectionFilterMgt.GetSelectionFilter(CustomerRecRef, Customer.FieldNo("No."), false));
    end;

    local procedure GetVendorFilter(var CustomReportSelection: Record "Custom Report Selection"): Text
    var
        Vendor: Record Vendor;
        SelectionFilterMgt: Codeunit SelectionFilterManagement;
        VendorRecRef: RecordRef;
    begin
        repeat
            Vendor.Get(CustomReportSelection."Source No.");
            Vendor.Mark(true);
        until CustomReportSelection.Next() = 0;
        Vendor.MarkedOnly(true);

        VendorRecRef.GetTable(Vendor);
        exit(SelectionFilterMgt.GetSelectionFilter(VendorRecRef, Vendor.FieldNo("No."), false));
    end;

    [Scope('OnPrem')]
    procedure ProcessReport()
    var
        EntryTempBlob: Codeunit "Temp Blob";
        EntryInStream: InStream;
        RequestPageParamsView: Text;
        FilterGroup: Integer;
    begin
        // If we're not yet initialized, exit - data needs to be set up before reports can be run
        if not Initialized then
            exit;

        ClearLastError();
        AnyOutputExists := true;

        ErrorMessageManagement.Activate(ErrorMessageHandler);
        ErrorMessageManagement.PushContext(ErrorContextElement, ReportSelections, 0, '');

        // Iterate through the selections and run the reports.
        if ReportSelections.FindSet() then begin
            AnyOutputExists := false;
            repeat
                SetOutputType(ReportSelections."Report ID");

                PrintIfEmailIsMissing := false;

                // If our report's data item and the 'join table' are the same, then set its filter from the request page.
                if not ReportDataAndIteratorDiffer then begin
                    RequestPageParamsView := GetViewFromParameters(ReportSelections."Report ID", IteratorRecordRef.Number);
                    IteratorRecordRef.SetView(RequestPageParamsView);
                end;

                FilterGroup := ReportDataRecordRef.FilterGroup();
                ReportDataRecordRef.FilterGroup(FindNextEmptyFilterGroup(ReportDataRecordRef)); // Set the request page filters separately to preserve the existing filters
                RequestPageParamsView := GetViewFromParameters(ReportSelections."Report ID", ReportDataRecordRef.Number);
                ReportDataRecordRef.SetView(RequestPageParamsView);

                CustomReportSelection.SetFilter("Report ID", StrSubstNo('0|%1', ReportSelections."Report ID"));
                CustomReportSelection.SetRange(Usage, ReportSelections.Usage);

                SetCustomReportSelectionTableFilter(ReportDataRecordRef.Number, RequestPageParamsView);

                case OutputType of
                    OutputType::Email:
                        begin
                            if Evaluate(
                                    PrintIfEmailIsMissing, GetOptionValueFromRequestPage(
                                        GetRequestParametersText(ReportSelections."Report ID"), 'PrintIfEmailIsMissing'))
                            then
                                ;
                            ProcessReportPerObject();
                        end;
                    OutputType::PDF,
                    OutputType::Word,
                    OutputType::Excel,
                    OutputType::XML:
                        ProcessReportPerObject();
                    OutputType::Preview,
                    OutputType::Print:
                        ProcessReportPerLayout();
                end;
                LogAndClearLastError(ReportSelections."Report Caption", ReportDataRecordRef.RecordId);
            until ReportSelections.Next() = 0;
        end;

        // Download the .zip file containing the reports if one was generated (usually from being on the web client)
        TempNameValueBuffer.Reset();  // Filters need to be cleared in order to get an accurate count.
        if (ZipFileName <> '') and not SupressOutput and TempNameValueBuffer.FindSet() then
            // If there's a single file, download it directly instead of the zip file
            if TempNameValueBuffer.Count = 1 then
                if IsTestMode() then
                    FileManagement.CopyServerFile(TempNameValueBuffer.Value, FileManagement.CombinePath(Path, TempNameValueBuffer.Name), true)
                else
                    if IsBackground() and FileManagement.ServerFileExists(TempNameValueBuffer.Value) then
                        SendToReportInbox(false, ReportSelections."Report ID")
                    else
                        FileManagement.DownloadHandler(TempNameValueBuffer.Value, '', '', '', TempNameValueBuffer.Name)
            else begin
                repeat
                    FileManagement.BLOBImportFromServerFile(EntryTempBlob, TempNameValueBuffer.Value);
                    EntryTempBlob.CreateInStream(EntryInStream);
                    DataCompression.AddEntry(EntryInStream, TempNameValueBuffer.Name);
                    TempEraseFileNameValueBuffer.AddNewEntry(TempNameValueBuffer.Value, '');
                until TempNameValueBuffer.Next() = 0;
                DataCompression.SaveZipArchive(ZipFileOutStream);
                DataCompression.CloseZipArchive();
                ZipFile.Close();
                // If we're in test mode, save the zip to the save path. Otherwise send to the client.
                if IsTestMode() then
                    FileManagement.CopyServerFile(ZipFileName, FileManagement.CombinePath(Path, ZipDownloadTxt), true)
                else
                    if IsBackground() and FileManagement.ServerFileExists(ZipFileName) then
                        SendToReportInbox(true, ReportSelections."Report ID")
                    else
                        FileManagement.DownloadHandler(ZipFileName, '', '', '', ZipDownloadTxt)
            end;

        CleanupTempFiles();

        OnAfterProcessReport();

        if not (SupressOutput or AnyOutputExists) then
            LogSimpleError(NoOutputErr);

        if ErrorMessageHandler.HasErrors() then begin
            OnBeforeThrowProcessReportError(ErrorMessageHandler, ReportDataRecordRef, OutputType);
            if not IsBackground() then
                if ErrorMessageHandler.ShowErrors() then
                    Error('');
        end;
    end;

    procedure ProcessReportData(ReportUsage: Enum "Report Selection Usage"; var DataRecordRef: RecordRef; SourceJoinFieldName: Text; DataRecordJoinTable: Integer; IteratorTableFieldName: Text; DataItemTableSameAsIterator: Boolean)
    begin
        // Provides a single function to run initialization code, check for issues, and start report processing
        if not Initialized then
            InitializeReportData(
                ReportUsage, DataRecordRef, SourceJoinFieldName, DataRecordJoinTable, IteratorTableFieldName,
                DataItemTableSameAsIterator);
        // If there was an error during initalization, exit
        if not Initialized then
            exit;

        ProcessReport();
    end;

    local procedure SetCustomReportSelectionTableFilter(TableNo: Integer; RequestPageParamsView: Text)
    var
        Customer: Record Customer;
        Vendor: Record Vendor;
    begin
        case TableNo of
            Database::Customer:
                begin
                    Customer.SetView(RequestPageParamsView);
                    CustomReportSelection.SetFilter("Source No.", Customer.GetFilter(Customer."No."));
                end;
            Database::Vendor:
                begin
                    Vendor.SetView(RequestPageParamsView);
                    CustomReportSelection.SetFilter("Source No.", Vendor.GetFilter(Vendor."No."));
                end;
        end;
    end;

    local procedure ProcessReportPerLayout()
    var
        TempRecordRef: RecordRef;
        TempRecordKeyFieldRef: FieldRef;
        ReportedLayouts: DotNet ArrayList;
        ReportedObjects: DotNet ArrayList;
        CustomReportLayoutCode: Code[20];
        ReportedRecordKeyVal: Text;
    begin
        ReportingType := ReportingType::Layout;
        // Prevent early calls to the function
        if not Initialized then
            Error(NotInitializedErr);

        // Temporary lists to keep track of what we've reported over, used to determine what to report on using the default layout
        ReportedLayouts := ReportedLayouts.ArrayList();
        ReportedObjects := ReportedObjects.ArrayList();

        // Iterate through the layouts in the Custom Report Selection table
        if CustomReportSelection.FindSet() then
            repeat
                CustomReportLayoutCode := CustomReportSelection."Custom Report Layout Code";

                // Reset the TempRecordRef object
                TempRecordRef := ReportDataRecordRef.Duplicate();
                TempRecordKeyFieldRef := TempRecordRef.Field(ReportDataIteratorFieldRef.Number);

                // If we've not already reported on this layout and it truly is a custom layout, then report on it
                if not ReportedLayouts.Contains(CustomReportLayoutCode) and
                   (ResolveCustomReportLayoutCode(CustomReportSelection) <> '')
                then begin
                    ReportedLayouts.Add(CustomReportLayoutCode);

                    // Run the report only for those objects that have this layout
                    GetLayoutIteratorKeyFilter(TempRecordRef, TempRecordKeyFieldRef, CustomReportLayoutCode); // set view based on intersection of lists

                    if TempRecordRef.FindFirst() then begin
                        // Run the report on the data record, using the above filter
                        RunReportWithCustomReportSelection(TempRecordRef, ReportSelections."Report ID", CustomReportSelection, PrintIfEmailIsMissing);

                        // Save this list of objects reported on, using the 'iterator field', they will not get the default report layout later
                        repeat
                            ReportedRecordKeyVal := Format(TempRecordKeyFieldRef.Value);
                            if not ReportedObjects.Contains(ReportedRecordKeyVal) then
                                ReportedObjects.Add(ReportedRecordKeyVal);
                        until TempRecordRef.Next() = 0;
                    end;
                end;
            until CustomReportSelection.Next() = 0;

        // Construct the filter for the remaining objects - iterate through all items in the data record and build a filter that contains the
        // items that are not in the list of already reported items.
        TempRecordRef := ReportDataRecordRef.Duplicate();
        TempRecordKeyFieldRef := TempRecordRef.Field(ReportDataIteratorFieldRef.Number);

        // Set the exclusion filter based on the items we've already reported on and set that filter on top of the filter already applied
        ExcludeReportedObjects(TempRecordRef, TempRecordKeyFieldRef, ReportedObjects);

        // Only run the report if we still have something to report on
        if TempRecordRef.FindFirst() then
            RunReport(TempRecordRef, ReportSelections, PrintIfEmailIsMissing);
    end;

    local procedure ExcludeReportedObjects(var TempRecordRef: RecordRef; var TempRecordKeyFieldRef: FieldRef; var ReportedObjects: DotNet ArrayList)
    var
        SelectionFilterManagement: Codeunit SelectionFilterManagement;
        FilterStatementsCount: Integer;
        SafeNumberOfFilterStatements: Integer;
        ExclusionFilter: Text;
        AndFilterCharacter: Char;
    begin
        if not TempRecordRef.FindFirst() then
            exit;

        AndFilterCharacter := '&';

        // Previous implementation was using filter groups to exclude reported objects. 
        // Platform ignores more than 255 filter groups silently, so we would run the report on reported objects twice if there were more than 255 reported objects (maximum number of filter groups).
        // The current implementation supports up to 2000 reported objects (Limit to SQL statements). If there are more objects than that we will run report twice. 
        SafeNumberOfFilterStatements := SelectionFilterManagement.GetMaximumNumberOfParametersInSQLQuery();

        repeat
            if ReportedObjects.Contains(Format(TempRecordKeyFieldRef.Value)) and (FilterStatementsCount < SafeNumberOfFilterStatements) then begin
#pragma warning disable AA0005
                ExclusionFilter += StrSubstNo(DifferentThanFilterTxt, SelectionFilterManagement.AddQuotes((TempRecordKeyFieldRef.Value))) + AndFilterCharacter;
                FilterStatementsCount += 1;
            end;
#pragma warning restore AA0005            
        until (TempRecordRef.Next() = 0) or (FilterStatementsCount = SafeNumberOfFilterStatements);

        if ExclusionFilter <> '' then begin
            ExclusionFilter := ExclusionFilter.TrimEnd(AndFilterCharacter);
            SetNextGroupFilter(TempRecordRef, TempRecordKeyFieldRef, ExclusionFilter);
            Clear(ExclusionFilter);
        end;
    end;

    local procedure ProcessReportPerObject()
    var
        PrevRecordID: RecordID;
        IteratorJoinFieldValue: Code[20];
        ReportID: Integer;
        IteratorFilterGroup: Integer;
        JoinValue: Code[20];
        PrevFilterNextGroup: Text;
        PrevFilterCurrentGroup: Text;
        FiltersChanged: Boolean;
        IsHandled: Boolean;
    begin
        ReportingType := ReportingType::Object;
        if not Initialized then
            Error(NotInitializedErr);

        ReportID := ReportSelections."Report ID";

        Evaluate(IteratorJoinFieldValue, Format(IteratorJoinFieldRef.Value));

        OnProcessReportPerObjectOnBeforeReportDataRecordRefLoop(ReportDataAndIteratorDiffer, IteratorRecordRef);

        // Set the data filter to be the item we're iterating over:

        if ReportDataRecordRef.FindSet() then
            repeat
                // Get and set the report selection for this particular object/report combination
                JoinValue := Format(ReportDataIteratorFieldRef.Value);
                CustomReportSelection.SetRange("Source No.", JoinValue);
                IteratorFilterGroup := SetNextGroupFilter(ReportDataRecordRef, ReportDataIteratorFieldRef, StrSubstNo(EscapeTok, JoinValue));
                PrevRecordID := ReportDataRecordRef.RecordId;

                SetIteratorJoinFieldRef();
                // Find the 'join' value in the associated table, this helps us generate the name
                IteratorJoinFieldRef.SetRange(JoinValue);
                IteratorRecordRef.FindFirst();

                IsHandled := false;
                OnProcessReportPerObjectOnBeforeRunReport(ReportDataAndIteratorDiffer, IteratorRecordRef, IsHandled);
                if not IsHandled then begin
                    FiltersChanged :=
                        (ReportDataRecordRef.GetFilters() <> PrevFilterCurrentGroup) or
                        (GetNextGroupFilters(ReportDataRecordRef, IteratorFilterGroup) <> PrevFilterNextGroup);
                    if (not IsRunReportOncePerFilter()) or FiltersChanged then
                        // If the object has custom layouts defined - process each one based on the selected output type, otherwise use the default layout
                        if CustomReportSelection.FindSet() then
                            repeat
                                RunReportWithCustomReportSelection(ReportDataRecordRef, ReportID, CustomReportSelection, PrintIfEmailIsMissing);
                            until CustomReportSelection.Next() = 0
                        else
                            RunReport(ReportDataRecordRef, ReportSelections, PrintIfEmailIsMissing);

                    PrevFilterCurrentGroup := ReportDataRecordRef.GetFilters();
                    PrevFilterNextGroup := GetNextGroupFilters(ReportDataRecordRef, IteratorFilterGroup);

                    OnProcessReportPerObjectOnAfterRunReport(ReportDataAndIteratorDiffer, IteratorRecordRef);
                end;

                // Clear out the filter and reset:
                SetGroupFilter(ReportDataRecordRef, ReportDataIteratorFieldRef, '', IteratorFilterGroup);
                ReportDataRecordRef.Get(PrevRecordID);

            until ReportDataRecordRef.Next() = 0;

        OnAfterProcessReportPerObject(ReportDataAndIteratorDiffer);
    end;

    local procedure RunReportWithCustomReportSelection(var DataRecRef: RecordRef; ReportID: Integer; var CustomReportSelection: Record "Custom Report Selection"; EmailPrintIfEmailIsMissing: Boolean)
    var
        CustomReportLayoutCode: Code[20];
        SendToEmailID: Text[250];
        EmailBlankOrNotValid: Boolean;
        IsHandled: Boolean;
    begin
        OnBeforeRunReportWithCustomReportSelection(
            DataRecRef, ReportID, CustomReportSelection, EmailPrintIfEmailIsMissing, TempBlobIndicesNameValueBuffer, TempBlobList,
            OutputType, AnyOutputExists, IsHandled);
        if IsHandled then begin
            CustomReportSelection.Validate("Report ID", ReportID);
            LogAndClearLastError(CustomReportSelection."Report Caption", DataRecRef.RecordId);
            exit;
        end;

        // Set the custom report layout
        if CustomReportSelection."Email Attachment Layout Name" <> '' then
            ReportLayoutSelection.SetTempLayoutSelectedName(CustomReportSelection."Email Attachment Layout Name")
        else begin
            CustomReportLayoutCode := ResolveCustomReportLayoutCode(CustomReportSelection);
            ReportLayoutSelection.SetTempLayoutSelected(CustomReportLayoutCode);
        end;

        case OutputType of
            OutputType::Email:
                begin
                    GetSendToEmailID(CustomReportSelection, SendToEmailID);
                    EmailBlankOrNotValid := CheckEmailSendTo(SendToEmailID);
                    if EmailBlankOrNotValid then begin
                        if EmailPrintIfEmailIsMissing then
                            if IsWordLayout(ReportID, CustomReportLayoutCode) then
                                SaveAsReport(DataRecRef, ReportID, REPORTFORMAT::Word)
                            else
                                SaveAsReport(DataRecRef, ReportID, REPORTFORMAT::PDF)
                        else
                            LogErrorEmailSendTo(DataRecRef, CustomReportSelection, EmailBlankOrNotValid, SendToEmailID);
                    end else
                        EmailReport(DataRecRef, ReportID, CustomReportSelection);
                end;
            OutputType::PDF:
                SaveAsReport(DataRecRef, ReportID, REPORTFORMAT::Pdf);
            OutputType::Excel:
                SaveAsReport(DataRecRef, ReportID, REPORTFORMAT::Excel);
            OutputType::Word:
                SaveAsReport(DataRecRef, ReportID, REPORTFORMAT::Word);
            OutputType::Print:
                PrintReport(DataRecRef, ReportID);
            OutputType::Preview:
                PreviewReport(DataRecRef, ReportID, CustomReportLayoutCode);
            OutputType::XML:
                SaveAsReport(DataRecRef, ReportID, REPORTFORMAT::Xml);
        end;

        ReportLayoutSelection.ClearTempLayoutSelected();

        CustomReportSelection.Validate("Report ID", ReportID);
        LogAndClearLastError(CustomReportSelection."Report Caption", DataRecRef.RecordId);
    end;

    local procedure RunReport(var DataRecRef: RecordRef; ReportSelections: Record "Report Selections"; EmailPrintRemaining: Boolean)
    var
        NullCustomReportSelection: Record "Custom Report Selection";
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeRunReport(DataRecRef, ReportSelections, EmailPrintRemaining, TempBlobIndicesNameValueBuffer, TempBlobList, OutputType, AnyOutputExists, IsHandled);
        if IsHandled then begin
            LogAndClearLastError(ReportSelections."Report Caption", DataRecRef.RecordId);
            exit;
        end;

        // If we know we don't need a custom report selection, e.g. we don't need layouts or won't be sending email
        NullCustomReportSelection.Init();
        NullCustomReportSelection.Usage := ReportSelections.Usage;
        RunReportWithCustomReportSelection(DataRecRef, ReportSelections."Report ID", NullCustomReportSelection, EmailPrintRemaining);
    end;

    procedure SetOutputOption(OutputOption: Integer)
    begin
        OutputType := OutputOption;
    end;

    [Scope('OnPrem')]
    procedure GetOutputOption(ReportID: Integer): Integer
    var
        OptionText: Text;
        OptionInt: Integer;
    begin
        // Given a report ID, get the currently selected output option
        OptionText := GetOptionValueFromRequestPageForReport(ReportID, 'ChosenOutputMethod');
        if Evaluate(OptionInt, OptionText) then
            exit(OptionInt);

        exit(-1); // Invalid output option, still a valid return code - signals no output
    end;

    procedure GetPrintOption(): Integer
    begin
        exit(OutputType::Print);
    end;

    procedure GetEmailOption(): Integer
    begin
        exit(OutputType::Email);
    end;

    procedure GetPreviewOption(): Integer
    begin
        exit(OutputType::Preview);
    end;

    procedure GetExcelOption(): Integer
    begin
        exit(OutputType::Excel);
    end;

    procedure GetPDFOption(): Integer
    begin
        exit(OutputType::PDF);
    end;

    procedure GetWordOption(): Integer
    begin
        exit(OutputType::Word);
    end;

    procedure GetXMLOption(): Integer
    begin
        exit(OutputType::XML);
    end;

    local procedure PrintReport(var DataRecRef: RecordRef; ReportID: Integer)
    begin
        OnBeforePrintReport(DataRecRef, ReportID);

        if SupressOutput then
            exit;

        REPORT.Print(ReportID, GetRequestParametersText(ReportID), '', DataRecRef);
        AnyOutputExists := true;
    end;

    local procedure EmailReport(var DataRecRef: RecordRef; ReportID: Integer; CustomReportSelection: Record "Custom Report Selection")
    var
        MailManagement: Codeunit "Mail Management";
        ReceiverRecord: RecordRef;
        FieldRef1: FieldRef;
        FieldRef2: FieldRef;
        ReportRecordVariant: Variant;
        TempPdfFilePath: Text[250];
        TempEmailBodyFilePath: Text[250];
        FileName: Text[250];
        PdfFileName: Text[250];
        TempReportLayoutCode: Code[20];
        EmailBodyLayoutCode: Code[20];
        TempReportLayoutName: Text[250];
    begin
        TempPdfFilePath := CreateReportWithExtension(DataRecRef, ReportID, REPORTFORMAT::Pdf, FileName);
        if TempPdfFilePath = '' then
            exit;

        // Set the pdf file name to be used later when sending an email.
        PdfFileName := FileName;

        // Use the iterator values if the data item and iterator differ
        if ReportDataAndIteratorDiffer then begin
            GetKeyFieldRef(IteratorRecordRef, FieldRef1);
            GetNameFieldRef(IteratorRecordRef, FieldRef2);
        end else begin
            GetKeyFieldRef(DataRecRef, FieldRef1);
            GetNameFieldRef(DataRecRef, FieldRef2);
        end;

        EmailBodyLayoutCode := ResolveEmailBodyLayoutCode(CustomReportSelection, ReportSelections);
        if (EmailBodyLayoutCode <> '') or (CustomReportSelection."Email Body Layout Name" <> '') then begin
            TempReportLayoutCode := ReportLayoutSelection.GetTempLayoutSelected();
            TempReportLayoutName := ReportLayoutSelection.GetTempSelectedLayoutName();
            if CustomReportSelection."Email Body Layout Name" <> '' then
                ReportLayoutSelection.SetTempLayoutSelectedName(CustomReportSelection."Email Body Layout Name")
            else
                ReportLayoutSelection.SetTempLayoutSelected(EmailBodyLayoutCode);
            ReportRecordVariant := DataRecRef;
            BindSubscription(MailManagement);
            TempEmailBodyFilePath := CreateReportWithExtension(ReportRecordVariant, ReportID, REPORTFORMAT::Html, FileName);
            UnbindSubscription(MailManagement);
            if TempEmailBodyFilePath = '' then
                exit;
            if TempReportLayoutName <> '' then
                ReportLayoutSelection.SetTempLayoutSelectedName(TempReportLayoutName)
            else
                ReportLayoutSelection.SetTempLayoutSelected(TempReportLayoutCode);
        end;

        if SupressOutput or RemoveEmptyFile(TempPdfFilePath) then
            exit;

        AnyOutputExists := true;
        ReceiverRecord := FieldRef1.Record();
        TryEmailReport(TempPdfFilePath, PdfFileName, TempEmailBodyFilePath, CustomReportSelection, ReceiverRecord, FieldRef2);
    end;

    local procedure PreviewReport(var DataRecRef: RecordRef; ReportID: Integer; CustomReportLayoutCode: Code[20])
    begin
        if IsWebClient() then begin
            if IsWordLayout(ReportID, CustomReportLayoutCode) then
                SaveAsReport(DataRecRef, ReportID, REPORTFORMAT::Word)
            else
                SaveAsReport(DataRecRef, ReportID, REPORTFORMAT::PDF)
        end else
            if not SupressOutput then begin
                REPORT.Execute(ReportID, GetRequestParametersText(ReportID), DataRecRef);
                AnyOutputExists := true;
            end;
    end;

    local procedure SaveAsReport(var DataRecRef: RecordRef; ReportID: Integer; RepFormat: ReportFormat)
    var
        File: File;
        FileStream: OutStream;
        FileName: Text;
        Extension: Text;
        TempFilePath: Text;
        ReportSaved: Boolean;
        BasePath: Text;
    begin
        // Handle both 'save file' types of reports together - PDF and Excel
        case RepFormat of
            REPORTFORMAT::Excel:
                Extension := '.xlsx';
            REPORTFORMAT::Pdf:
                Extension := '.pdf';
            REPORTFORMAT::Word:
                Extension := '.docx';
            REPORTFORMAT::Xml:
                Extension := '.xml';
            else
                Error(OutputNotSupportedErr);
        end;

        // If we're not on the web client, use the path that was selected for saving
        // In the web client, the path isn't used since we zip up the files and send them to the client
        if not IsWebClient() then
            BasePath := Path;
        FileName := GenerateFileNameForReport(ReportID, Extension, BasePath, DataRecRef);

        TempFilePath := FileManagement.ServerTempFileName(Extension);
        File.Create(TempFilePath);
        File.CreateOutStream(FileStream);
        ReportSaved := BoundCallReportSaveAs(ReportID, GetRequestParametersText(ReportID), RepFormat, FileStream, DataRecRef);
        OnSaveAsReportOnBeforeFileClose(ReportSaved, DataRecRef, ReportID, RepFormat, FileStream);
        File.Close();

        if ReportSaved and not RemoveEmptyFile(TempFilePath) and FileManagement.ServerFileExists(TempFilePath) and not SupressOutput then
            if IsWebClient() or IsBackground() then
                AddFileToClientZip(TempFilePath, FileName)
            else begin
                FileManagement.DownloadHandler(TempFilePath, '', '', '', FileName);
                TempEraseFileNameValueBuffer.AddNewEntry(Format(TempFilePath, 250), '');
                AnyOutputExists := true;
            end;
    end;

    local procedure GenerateFileNameForReport(ReportID: Integer; Extension: Text; FilePath: Text; DataRecRef: RecordRef): Text[250]
    var
        NameFieldRef: FieldRef;
        ObjectName: Text;
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeGenerateFileNameForReport(IteratorRecordRef, NameFieldRef, ReportDataRecordRef, ObjectName, IsHandled);
        if IsHandled then
            exit(GenerateFileName(ObjectName, ReportID, Extension, FilePath, DataRecRef));

        // If we're iterating through Customer or Vendor, get the appropriate name
        if not ReportDataAndIteratorDiffer then begin
            if GetNameFieldRef(DataRecRef, NameFieldRef) then
                ObjectName := Format(NameFieldRef.Value);
        end else
            if GetNameFieldRef(IteratorRecordRef, NameFieldRef) then
                ObjectName := Format(NameFieldRef.Value)
            else
                ObjectName := Format(IteratorJoinFieldRef.Value);

        exit(GenerateFileName(ObjectName, ReportID, Extension, FilePath, DataRecRef));
    end;

    local procedure GenerateFileName(ObjectName: Text; ReportID: Integer; Extension: Text; FilePath: Text; DataRecRef: RecordRef): Text
    var
        FileName: Text;
        EndDate: Text;
        ReportParameters: Text;
        FileBaseName: Text;
        AppendIndex: Integer;
    begin
        ReportParameters := GetRequestParametersText(ReportID);

        if StrPos(Extension, '.') <> 1 then
            Extension := '.' + Extension;

        // We need to limit the file name - limit of other functions
        if (StrLen(ObjectName) + StrLen(FilePath)) >= 250 then
            ObjectName := Format(ObjectName, 250 - StrLen(FilePath));

        // Fetch request page parameters
        EndDate := GetOptionValueFromRequestPage(ReportParameters, 'EndDate');

        // Construct with the end date, if it exists. Format the object name to adhere to filename size limits
        if OutputFileBaseName = '' then
            FileBaseName := GetTempLayoutReportCaption(ReportID)
        else
            FileBaseName := OutputFileBaseName;
        if EndDate <> '' then
            FileName := FileBaseName + GetFileNameForPart(ObjectName) + GetFileNameAsOfPart(EndDate) + Extension
        else
            FileName := FileBaseName + GetFileNameForPart(ObjectName) + Extension;

        OnGenerateFileNameOnAfterAssignFileName(FileName, ReportID, Extension, DataRecRef);

        FileName := FileManagement.StripNotsupportChrInFileName(FileName);

        FileBaseName := FileName;
        TempNameValueBufferUniqueFiles.SetRange(Name, FileName);
        while not TempNameValueBufferUniqueFiles.IsEmpty() and (StrLen(FileName) <= 250) do begin
            AppendIndex += 1;
            FileName := FileManagement.AppendFileNameWithIndex(FileBaseName, AppendIndex);
            TempNameValueBufferUniqueFiles.SetRange(Name, FileName)
        end;
        TempNameValueBufferUniqueFiles.AddNewEntry(CopyStr(FileName, 1, 250), '');

        if FilePath <> '' then
            FileName := FileManagement.CombinePath(FilePath, FileName);

        exit(FileName);
    end;

    local procedure GetFileNameForPart(ObjectName: Text): Text
    begin
        if ObjectName <> '' then
            exit(StrSubstNo(ForTok, ObjectName));
        exit('');
    end;

    local procedure GetFileNameAsOfPart(EndDate: Text): Text
    begin
        exit(StrSubstNo(AsOfTok, EndDate));
    end;

    local procedure GetOptionValueFromRequestPage(ReportParameters: Text; OptionName: Text): Text
    begin
        exit(RequestPageParametersHelper.GetRequestPageOptionValue(OptionName, ReportParameters));
    end;

    [Scope('OnPrem')]
    procedure GetOptionValueFromRequestPageForReport(ReportID: Integer; OptionName: Text): Text
    begin
        // Given a report ID - get the option from the request parameters page
        if not TempBlobIndicesNameValueBuffer.Get(ReportID) then
            exit('');
        exit(GetOptionValueFromRequestPage(GetRequestParametersText(ReportID), OptionName));
    end;

    local procedure GetTempLayoutReportCaption(ReportID: Integer): Text
    var
        AllObjWithCaption: Record AllObjWithCaption;
        ReportCaption: Text;
    begin
        AllObjWithCaption.Get(AllObjWithCaption."Object Type"::Report, ReportID);
        ReportCaption := AllObjWithCaption."Object Caption";
        exit(ReportCaption);
    end;

    procedure SetSavePath(SavePath: Text)
    begin
        // This allows us to set the path ahead of setting request parameters if we know it or need to set it ahead of time
        // e.g. for unit tests
        Path := SavePath;
    end;

    local procedure GetViewFromParameters(ReportID: Integer; TableNumber: Integer): Text
    var
        TempBlob: Codeunit "Temp Blob";
        RecordRef: RecordRef;
        Index: Integer;
    begin
        TempBlobIndicesNameValueBuffer.Get(ReportID);
        Evaluate(Index, TempBlobIndicesNameValueBuffer.Value);
        TempBlobList.Get(Index, TempBlob);
        // Use the request page helper to parse the parameters and set the view to the RecordRef and the Record
        RecordRef.Open(TableNumber);
        RequestPageParametersHelper.ConvertParametersToFilters(RecordRef, TempBlob);

        exit(RecordRef.GetView());
    end;

    local procedure GetRequestParameters()
    var
        EmailAccount: Codeunit "Email Account";
        DataVariant: Variant;
        RequestPageParameters: Text;
        FilterGroup: Integer;
        LocalRepId: Integer;
    begin
        ReportSelections.SetFilter("Report ID", '<>0');
        ReportSelections.FindFirst();

        if ReportSelections.FindSet() then
            repeat
                LocalRepId := ReportSelections."Report ID";
                // If we're at this point, and we're set in test mode with XML output - run the report directly from here
                // This is done to retain compatibility with report testing (which uses RequestPage.SaveAsXML and needs the traditional output options buttons)
                // XML output can't use the layouts anyways.
                if IsTestMode() and (OutputType = OutputType::XML) then begin
                    DataVariant := ReportDataRecordRef;
                    REPORT.RunModal(LocalRepId, true, false, DataVariant);
                    Error(''); // Exit early in an uninitialized state, prevents the full initialization flag from being set
                end;
                Commit();
                RequestPageParameters := RunRequestPage(LocalRepId);
                // If the user cancelled out of the request page - exclude this report ID from processing:
                if RequestPageParameters = '' then begin
                    // Advance to the next open filtergroup outside of the system range
                    FilterGroup := ReportSelections.FilterGroup;
                    if ReportSelections.HasFilter or (ReportSelections.FilterGroup < 10) then
                        repeat
                            ReportSelections.FilterGroup(ReportSelections.FilterGroup + 1)
                        until not ReportSelections.HasFilter and (ReportSelections.FilterGroup >= 10);
                    ReportSelections.SetFilter("Report ID", StrSubstNo('<>%1', LocalRepId));
                    ReportSelections.FilterGroup(FilterGroup);
                    break;
                end;

                StoreRequestParameters(LocalRepId, RequestPageParameters);
                SaveReportRequestPageParameters(LocalRepId, RequestPageParameters);
                // Validate output type and get a file save path, if necessary, only prompt for windows clients that are not in test mode
                SetOutputType(LocalRepId);

                // Use the temp path if we're set in test mode and the path wasn't already set
                if (Path = '') and IsTestMode() then
                    Path := TemporaryPath;

                // If email is chosen, ensure that email account is set up
                if (OutputType = OutputType::Email) and not EmailAccount.IsAnyAccountRegistered() and not SupressOutput then
                    Error(EmailAccountsNotSetupErr);
            until ReportSelections.Next() = 0;
        Initialized := true;
    end;

    local procedure SetOutputType(ReportID: Integer)
    var
        OutputMethod: Text;
        OptionInt: Integer;
    begin
        // The request page should have the appropriate parameters set for the chosen output method
        OutputMethod := GetOptionValueFromRequestPage(GetRequestParametersText(ReportID), 'ChosenOutputMethod');
        OnSetOutputTypeOnAfterSetOutputMethod(OutputMethod);
        if OutputMethod <> '' then begin
            if not Evaluate(OptionInt, OutputMethod) then
                Error(OutputNotSupportedErr);
        end else
            Error(OutputNotSupportedErr);

        SetOutputOption(OptionInt);
    end;

    local procedure SetReportUsage(ReportSelectionUsage: Enum "Report Selection Usage")
    begin
        ReportSelections.SetRange(Usage, ReportSelectionUsage);
        CustomReportSelection.SetRange(Usage, ReportSelectionUsage);
    end;

    procedure SetOutputSupression(SupressOutputFlag: Boolean)
    begin
        SupressOutput := SupressOutputFlag;
    end;

    local procedure StoreRequestParameters(ReportID: Integer; Parameters: Text)
    var
        TempBlob: Codeunit "Temp Blob";
        OutStr: OutStream;
        Index: Integer;
    begin
        // Insert or Modify - based on if it exists already or not
        TempBlob.CreateOutStream(OutStr, TextEncoding::UTF8);
        OutStr.WriteText(Parameters);
        if TempBlobIndicesNameValueBuffer.Get(ReportID) then begin
            Evaluate(Index, TempBlobIndicesNameValueBuffer.Value);
            TempBlobList.Set(Index, TempBlob);
        end else begin
            TempBlobList.Add(TempBlob);
            TempBlobIndicesNameValueBuffer.ID := ReportID;
            TempBlobIndicesNameValueBuffer.Value := Format(TempBlobList.Count());
            TempBlobIndicesNameValueBuffer.Insert();
        end;
    end;

    local procedure GetRequestParametersText(ReportID: Integer): Text
    var
        TempBlob: Codeunit "Temp Blob";
        InStr: InStream;
        ReqPageXML: Text;
        Index: Integer;
    begin
        TempBlobIndicesNameValueBuffer.Get(ReportID);
        Evaluate(Index, TempBlobIndicesNameValueBuffer.Value);
        TempBlobList.Get(Index, TempBlob);
        TempBlob.CreateInStream(InStr, TextEncoding::UTF8);
        InStr.ReadText(ReqPageXML);
        exit(ReqPageXML);
    end;

    local procedure AddFileToClientZip(TempFileName: Text; ClientFileName: Text)
    begin
        if StrLen(TempFileName) > 250 then
            Error(PathLengthErr, TempFileName);

        if StrLen(ClientFileName) > 250 then
            Error(PathLengthErr, ClientFileName);

        // Ensure we have a zip file object
        if ZipFileName = '' then begin
            ZipFileName := FileManagement.ServerTempFileName('zip');
            ZipFile.Create(ZipFileName);
            ZipFile.CreateOutStream(ZipFileOutStream);
            DataCompression.CreateZipArchive();
        end;

        TempNameValueBuffer.SetRange(Name, CopyStr(ClientFileName, 1, 250));
        if not TempNameValueBuffer.FindFirst() then
            TempNameValueBuffer.AddNewEntry(CopyStr(ClientFileName, 1, 250), CopyStr(TempFileName, 1, 250));

        AnyOutputExists := true;
    end;

    procedure CallReportSaveAs(ReportID: Integer; RequestParameterText: Text; ReportFormatValue: ReportFormat; var FileStream: OutStream; var RecRef: RecordRef): Boolean
    var
        ReportSaved: Boolean;
    begin
        OnBeforeCallReportSaveAs(ReportID, ReportFormatValue);
        ReportSaved := REPORT.SaveAs(ReportID, RequestParameterText, ReportFormatValue, FileStream, RecRef);

        if not ReportSaved then
            exit(false);

        if not WordOutputXmlHasDataVerified then
            exit(true);

        exit(WordOutputXmlHasData);
    end;

    local procedure BoundCallReportSaveAs(ReportID: Integer; RequestParameterText: Text; ReportFormatValue: ReportFormat; var FileStream: OutStream; var RecRef: RecordRef): Boolean
    var
        CustomLayoutReporting: Codeunit "Custom Layout Reporting";
    begin
        BindSubscription(CustomLayoutReporting);
        exit(CustomLayoutReporting.CallReportSaveAs(ReportID, RequestParameterText, ReportFormatValue, FileStream, RecRef))
    end;

    local procedure IsWordLayout(ReportID: Integer; CustomReportLayoutCode: Code[20]): Boolean
    var
        CustomReportLayout: Record "Custom Report Layout";
        ReportManagementHelper: Codeunit "Report Management Helper";
    begin
        if CustomReportLayoutCode <> '' then begin
            CustomReportLayout.Code := CustomReportLayoutCode;
            if CustomReportLayout.Find('=') then
                exit(CustomReportLayout.Type = CustomReportLayout.Type::Word);

            exit(ReportManagementHelper.SelectedLayoutType(ReportID) = ReportLayoutType::Word);
        end;
        exit(ReportManagementHelper.SelectedLayoutType(ReportID) = ReportLayoutType::Word);
    end;

    local procedure SetReportDataItem(var DataRecordRef: RecordRef; SourceJoinFieldName: Text; DataRecordJoinTable: Integer; IteratorTableFieldName: Text; DataItemTableSameAsIterator: Boolean)
    var
        "Field": Record "Field";
        ConfigValidateManagement: Codeunit "Config. Validate Management";
        RelationTable: Integer;
        RelationField: Integer;
    begin
        // Copy the RecordRef so as to not disturb the original
        ReportDataRecordRef := DataRecordRef.Duplicate();

        // Find the fields that relate the iterator to the data record - based on caption:
        Field.SetRange(FieldName, SourceJoinFieldName);
        Field.SetRange(TableNo, ReportDataRecordRef.Number);
        Field.SetFilter(ObsoleteState, '<>%1', Field.ObsoleteState::Removed);
        if Field.FindFirst() then
            ReportDataIteratorFieldRef := ReportDataRecordRef.Field(Field."No.");
        // If the tables are different, 'join' using the filter of the data item passed in.
        if DataItemTableSameAsIterator then begin
            IteratorRecordRef.Open(ReportDataRecordRef.Number);
            IteratorJoinFieldRef := IteratorRecordRef.Field(ReportDataIteratorFieldRef.Number);
            SetNextGroupFilter(IteratorRecordRef, IteratorJoinFieldRef, ReportDataIteratorFieldRef.GetFilter);
            ReportDataAndIteratorDiffer := false;
        end else begin
            ConfigValidateManagement.GetRelationInfoByIDs(
              ReportDataRecordRef.Number, ReportDataIteratorFieldRef.Number, RelationTable, RelationField);
            IteratorRecordRef.Open(DataRecordJoinTable);
            Field.Reset();
            Field.SetRange(FieldName, IteratorTableFieldName);
            Field.SetRange(TableNo, IteratorRecordRef.Number);
            Field.SetFilter(ObsoleteState, '<>%1', Field.ObsoleteState::Removed);
            if Field.FindFirst() then
                IteratorJoinFieldRef := IteratorRecordRef.Field(Field."No.");
            ReportDataAndIteratorDiffer := true;
        end;

        CustomReportSelection.SetRange("Source Type", IteratorRecordRef.Number);
    end;

    local procedure RemoveEmptyFile(FileName: Text): Boolean
    var
        File: File;
    begin
        // This function cleans up empty files, allowing us to remove reports that do not save correctly or error out/have no output.
        File.Open(FileName);
        if File.Len = 0 then begin
            File.Close();
            Erase(FileName);
            exit(true);
        end;
        File.Close();
        exit(false);
    end;

    procedure IsWebClient(): Boolean
    begin
        if TestModeWebClient then
            exit(true);
        exit(ClientTypeManagement.GetCurrentClientType() in [CLIENTTYPE::Web, CLIENTTYPE::Phone, CLIENTTYPE::Tablet, CLIENTTYPE::Desktop]);
    end;

    procedure SetOutputFileBaseName(FileBaseName: Text)
    begin
        // Sets a text base name for the output files:
        // e.g. code calling reports with usage of 'Statement' would set 'Statement' here
        OutputFileBaseName := FileBaseName;
    end;

    local procedure IsTestMode() TestMode: Boolean
    begin
        // Check to see if the test mode flag is set (usually via test codeunits by subscribing to OnIsTestMode event)
        OnIsTestMode(TestMode);
    end;

    procedure SetTestModeWebClient(TestModeSpoofWebClient: Boolean)
    begin
        TestModeWebClient := TestModeSpoofWebClient;
    end;

    local procedure ResolveCustomReportLayoutCode(var CustomReportSelection: Record "Custom Report Selection"): Code[20]
    begin
        // Given a custom report selection, return the custom layout, unless report ID  = 0, then resolve to the appropriate company-wide layout for the report ID
        if CustomReportSelection."Custom Report Layout Code" <> '' then
            exit(CustomReportSelection."Custom Report Layout Code");

        // If we don't have a custom layout defined in this selection record, get the default for the report number
        if ReportLayoutSelection.Get(CustomReportSelection."Report ID", CompanyName) then
            exit(ReportLayoutSelection."Custom Report Layout Code");

        exit(''); // We haven't found any custom report layouts
    end;

    local procedure ResolveEmailBodyLayoutCode(var CustomReportSelection: Record "Custom Report Selection"; var ReportSelections: Record "Report Selections"): Code[20]
    begin
        if CustomReportSelection."Use for Email Body" then
            exit(CustomReportSelection."Email Body Layout Code");

        if ReportSelections."Use for Email Body" then
            exit(ReportSelections."Email Body Layout Code");

        exit(''); // We haven't found any custom report layouts
    end;

    [Scope('OnPrem')]
    procedure InitializeReportData(ReportUsage: Enum "Report Selection Usage"; var DataRecordRef: RecordRef; SourceJoinFieldName: Text; DataRecordJoinTable: Integer; IteratorTableFieldName: Text; DataItemTableSameAsIterator: Boolean)
    begin
        // Initialize parameters and request pages, but do not run the reports yet
        SetReportUsage(ReportUsage);
        SetReportDataItem(DataRecordRef, SourceJoinFieldName, DataRecordJoinTable, IteratorTableFieldName, DataItemTableSameAsIterator);
        GetRequestParameters();
    end;

    [Scope('OnPrem')]
    procedure InitializeData(ReportSelectionUsage: Integer; var DataRecordRef: RecordRef; SourceJoinFieldName: Text; DataRecordJoinTable: Integer; IteratorTableFieldName: Text; DataItemTableSameAsIterator: Boolean)
    begin
        InitializeReportData(
            "Report Selection Usage".FromInteger(ReportSelectionUsage),
            DataRecordRef, SourceJoinFieldName, DataRecordJoinTable, IteratorTableFieldName, DataItemTableSameAsIterator);
    end;

    procedure HasRequestParameterData(ReportID: Integer): Boolean
    begin
        // Allows the caller to determine if valid request parameters XML is stored for the given report ID.
        // This is mostly useful to determine if a given report's request page has been canceled or not.
        exit(TempBlobIndicesNameValueBuffer.Get(ReportID));
    end;

    local procedure GetKeyFieldRef(var TableRecordRef: RecordRef; var KeyFieldRef: FieldRef): Boolean
    var
        DataTypeManagement: Codeunit "Data Type Management";
    begin
        case TableRecordRef.Number of
            DATABASE::Customer,
            DATABASE::Vendor:
                begin
                    DataTypeManagement.FindFieldByName(TableRecordRef, KeyFieldRef, 'No.');
                    exit(true);
                end;
            else
                exit(false);
        end;
    end;

    local procedure GetNameFieldRef(var TableRecordRef: RecordRef; var NameFieldRef: FieldRef): Boolean
    var
        DataTypeManagement: Codeunit "Data Type Management";
    begin
        case TableRecordRef.Number of
            DATABASE::Customer,
            DATABASE::Vendor:
                begin
                    DataTypeManagement.FindFieldByName(TableRecordRef, NameFieldRef, 'Name');
                    exit(true);
                end;
            else
                exit(false);
        end;
    end;

    local procedure FindNextEmptyFilterGroup(var RecordRef: RecordRef): Integer
    var
        FilterGroup: Integer;
        StartingGroup: Integer;
    begin
        // Finds the next empty filter group, with a minimum of group 10 to ensure we're in a non-system group.
        StartingGroup := RecordRef.FilterGroup;
        FilterGroup := StartingGroup;

        if FilterGroup < 10 then
            FilterGroup := 10;

        // Find the next empty group
        RecordRef.FilterGroup(FilterGroup);
        if RecordRef.HasFilter() then
            repeat
                FilterGroup += 1;
                RecordRef.FilterGroup(FilterGroup);
            until not RecordRef.HasFilter();

        // Reset the group back to the original value
        RecordRef.FilterGroup(StartingGroup);

        exit(FilterGroup);
    end;

    local procedure SetNextGroupFilter(var RecordRef: RecordRef; var FieldRef: FieldRef; "Filter": Text): Integer
    var
        NextGroup: Integer;
    begin
        NextGroup := FindNextEmptyFilterGroup(RecordRef);
        SetGroupFilter(RecordRef, FieldRef, Filter, NextGroup);
        exit(NextGroup);
    end;

    local procedure SetGroupFilter(var RecordRef: RecordRef; var FieldRef: FieldRef; "Filter": Text; GroupNumber: Integer)
    var
        FilterGroup: Integer;
    begin
        FilterGroup := RecordRef.FilterGroup;
        RecordRef.FilterGroup(GroupNumber);
        FieldRef.SetFilter(Filter);
        RecordRef.FilterGroup(FilterGroup);
    end;

    local procedure GetNextGroupFilters(var RecordRef: RecordRef; GroupNumber: Integer) Filters: Text
    var
        SavedFilterGroup: Integer;
    begin
        SavedFilterGroup := RecordRef.FilterGroup;
        RecordRef.FilterGroup(GroupNumber);
        Filters := RecordRef.GetFilters();
        RecordRef.FilterGroup(SavedFilterGroup);
    end;

    local procedure CleanupTempFiles()
    begin
        // Sometimes file handles are kept by .NET - we try to delete what we can.
        if TempEraseFileNameValueBuffer.FindSet() then
            repeat
                if TryDeleteFile(TempEraseFileNameValueBuffer.Name) then; // ignore errors
            until TempEraseFileNameValueBuffer.Next() = 0;
    end;

    [TryFunction]
    local procedure TryDeleteFile(FileName: Text)
    begin
        FileManagement.DeleteServerFile(FileName);
    end;

    [TryFunction]
    local procedure TryCreateFileStream(var File: File; ReportID: Integer; var TempFilePath: Text[250]; var FileName: Text[250]; var FileStream: OutStream; Extension: Text; DataRecRef: RecordRef)
    begin
        TempFilePath := CopyStr(FileManagement.ServerTempFileName(Extension), 1, 250);

        FileName := GenerateFileNameForReport(ReportID, Extension, '', DataRecRef);

        File.Create(TempFilePath);
        File.CreateOutStream(FileStream);
    end;

    [TryFunction]
    local procedure TryEmailReport(TempFilePath: Text[250]; FileName: Text[250]; TempEmailBodyFilePath: Text[250]; var CustomReportSelection: Record "Custom Report Selection"; var ReceiverRecord: RecordRef; var FieldRef2: FieldRef)
    var
        DocumentMailing: Codeunit "Document-Mailing";
        EmailBodyTempBlob: Codeunit "Temp Blob";
        TempBlob: Codeunit "Temp Blob";
        SourceReference: RecordRef;
        AttachmentStream: Instream;
        MailSent: Boolean;
        SourceTableIDs, SourceRelationTypes : List of [Integer];
        SourceIDs: List of [Guid];
        SendToEmailID: Text[250];
    begin
        FileManagement.BLOBImportFromServerFile(TempBlob, TempFilePath);
        TempBlob.CreateInStream(AttachmentStream);
        if TempEmailBodyFilePath <> '' then
            FileManagement.BLOBImportFromServerFile(EmailBodyTempBlob, TempEmailBodyFilePath);

        SourceReference.GetTable(CustomReportSelection);
        SourceReference.GetBySystemId(CustomReportSelection.SystemId);

        SourceTableIDs.Add(SourceReference.Number());
        SourceIDs.Add(SourceReference.Field(SourceReference.SystemIdNo()).Value());
        SourceRelationTypes.Add(Enum::"Email Relation Type"::"Primary Source".AsInteger());

        SourceTableIDs.Add(ReceiverRecord.Number());
        SourceIDs.Add(ReceiverRecord.Field(ReceiverRecord.SystemIdNo()).Value());
        SourceRelationTypes.Add(Enum::"Email Relation Type"::"Related Entity".AsInteger());
        GetSendToEmailID(CustomReportSelection, SendToEmailID);

        MailSent :=
            DocumentMailing.EmailFile(
                AttachmentStream, FileName, EmailBodyTempBlob, '', SendToEmailID,
                StrSubstNo('%1', FieldRef2.Value), true, CustomReportSelection.Usage.AsInteger(), SourceTableIDs, SourceIDs, SourceRelationTypes);
        if Exists(TempFilePath) then begin
            TempEraseFileNameValueBuffer.AddNewEntry(TempFilePath, Format(FieldRef2.Value));
            if not MailSent then
                AddFileToClientZip(TempFilePath, FileName);
        end;
        if Exists(TempEmailBodyFilePath) then
            TempEraseFileNameValueBuffer.AddNewEntry(TempEmailBodyFilePath, '');
        if not MailSent then
            ClearLastError();
    end;

    local procedure CreateReportWithExtension(var DataRecRef: RecordRef; ReportID: Integer; ReportFormatType: ReportFormat; var FileName: Text[250]): Text[250]
    var
        File: File;
        FileStream: OutStream;
        TempFilePath: Text[250];
        ErasePath: Text;
    begin
        case ReportFormatType of
            REPORTFORMAT::Pdf:
                begin
                    TryCreateFileStream(File, ReportID, TempFilePath, FileName, FileStream, 'pdf', DataRecRef);
                    TempEmailNameValueBuffer.SetRange(Name, FileName);
                    if not TempEmailNameValueBuffer.FindFirst() then
                        if BoundCallReportSaveAs(ReportID, GetRequestParametersText(ReportID), REPORTFORMAT::Pdf, FileStream, DataRecRef) then begin
                            TempEmailNameValueBuffer.AddNewEntry(CopyStr(FileName, 1, 250), CopyStr(TempFilePath, 1, 250));
                            OnCreateReportWithExtensionOnBeforePdfFileClose(ReportID, ReportFormatType, DataRecRef, FileStream);
                            File.Close();
                            exit(TempFilePath);
                        end;
                    ErasePath := File.Name;
                    File.Close();
                    Erase(ErasePath);
                    exit('');
                end;
            REPORTFORMAT::Html:
                begin
                    TryCreateFileStream(File, ReportID, TempFilePath, FileName, FileStream, 'html', DataRecRef);
                    TempEmailNameValueBuffer.SetRange(Name, FileName);
                    if not TempEmailNameValueBuffer.FindFirst() then
                        if BoundCallReportSaveAs(ReportID, GetRequestParametersText(ReportID), REPORTFORMAT::Html, FileStream, DataRecRef) then begin
                            TempEmailNameValueBuffer.AddNewEntry(CopyStr(FileName, 1, 250), CopyStr(TempFilePath, 1, 250));
                            OnCreateReportWithExtensionOnBeforeHtmlFileClose(ReportID, ReportFormatType, DataRecRef, FileStream);
                            File.Close();
                            exit(TempFilePath);
                        end;
                    ErasePath := File.Name;
                    File.Close();
                    Erase(ErasePath);
                    exit('');
                end;
            else
                Error(ReportFormatNotSupportedErr, ReportFormatType);
        end;
    end;

    local procedure SetIteratorJoinFieldRef()
    var
        Vendor: Record Vendor;
        Customer: Record Customer;
        GenJournalLine: Record "Gen. Journal Line";
        IteratorTableFieldName: Text;
        i: Integer;
        DataRecordJoinTable: Integer;
        IsHandled: Boolean;
    begin
        IsHandled := false;
        OnBeforeSetIteratorJoinFieldRef(ReportDataRecordRef, BalAcctTypeFieldRef, AcctTypeFieldRef, IsHandled);
        if IsHandled then
            exit;

        BalAcctTypeFieldRef := ReportDataRecordRef.Field(63);
        AcctTypeFieldRef := ReportDataRecordRef.Field(3);

        case Format(BalAcctTypeFieldRef.Value) of
            Format(GenJournalLine."Bal. Account Type"::Vendor):
                begin
                    DataRecordJoinTable := DATABASE::Vendor;
                    IteratorTableFieldName := Vendor.FieldName("No.");
                end;
            Format(GenJournalLine."Bal. Account Type"::Customer):
                begin
                    DataRecordJoinTable := DATABASE::Customer;
                    IteratorTableFieldName := Customer.FieldName("No.");
                end;
            Format(GenJournalLine."Bal. Account Type"::"Bank Account"):
                case Format(AcctTypeFieldRef.Value) of
                    Format(GenJournalLine."Account Type"::Customer):
                        begin
                            DataRecordJoinTable := DATABASE::Customer;
                            IteratorTableFieldName := Customer.FieldName("No.");
                        end;
                    Format(GenJournalLine."Account Type"::Vendor):
                        begin
                            DataRecordJoinTable := DATABASE::Vendor;
                            IteratorTableFieldName := Vendor.FieldName("No.");
                        end;
                end;
        end;
        if DataRecordJoinTable <> 0 then begin
            IteratorRecordRef.Close();
            IteratorRecordRef.Open(DataRecordJoinTable);
            for i := 1 to IteratorRecordRef.FieldCount() do
                if IteratorRecordRef.Field(i).Name = IteratorTableFieldName then begin
                    IteratorJoinFieldRef := IteratorRecordRef.Field(i);
                    break;
                end;
        end;
    end;

    [Scope('OnPrem')]
    procedure RunRequestPage(ReportId: Integer): Text
    var
        SavedParameters: Text;
    begin
        if PredefinedRequestParameters <> '' then
            exit(PredefinedRequestParameters);

        SavedParameters := GetReportRequestPageParameters(ReportId);
        OnRunRequestPageOnBeforeReportRunRequestPage(ReportId, SavedParameters);
        exit(REPORT.RunRequestPage(ReportId, SavedParameters));
    end;

    local procedure LogSimpleError(ErrorText: Text)
    begin
        ErrorMessageManagement.LogSimpleErrorMessage(ErrorText);
    end;

    local procedure LogAndClearLastError(ReportCaption: Text; RecID: RecordID)
    begin
        if GetLastErrorText <> '' then begin
            LogSimpleError(StrSubstNo(ErrorForDataOccuredErr, GetLastErrorText, ReportCaption, RecID));
            ClearLastError();
        end;
    end;

    local procedure IsBackground(): Boolean
    begin
        exit(ClientTypeManagement.GetCurrentClientType() in [CLIENTTYPE::Background]);
    end;

    local procedure IsCustomReportSelectionNull(CustomReportSelection: Record "Custom Report Selection"): Boolean
    begin
        exit((CustomReportSelection."Source Type" = 0) and (CustomReportSelection."Source No." = '') and (CustomReportSelection.Sequence = 0));
    end;

    procedure SetTableFilterForReportID(ReportID: Integer; CustomerNo: Text);
    begin
        TableFilterForReportID := ReportID;
        TableFilterTxt := StrSubstNo(TableFilterTok, Format(ReportID), STRSUBSTNO(EscapeTok, ParseCustomerNo(CustomerNo)));
    end;

    local procedure ParseCustomerNo(CustomerNo: Text): Text;
    var
        I: Integer;
        OutputTxt: text;
    begin
        for i := 1 to StrLen(CustomerNo) do
            case CustomerNo[I] of
                '&':
                    OutputTxt := OutputTxt + '&amp;';
                '>':
                    OutputTxt := OutputTxt + '&gt;';
                '<':
                    OutputTxt := OutputTxt + '&lt;';
                '''':
                    OutputTxt := OutputTxt + '''''';
                else
                    OutputTxt := OutputTxt + CustomerNo[I];
            end;
        exit(OutputTxt);
    end;

    procedure GetReportRequestPageParameters(ReportID: Integer) XMLTxt: Text
    var
        ObjectOptions: Record "Object Options";
        InStr: InStream;
    begin
        if IgnoreRequestParameters then
            exit('');
        if ReportId = TableFilterForReportID then
            exit(TableFilterTxt);
        if not ObjectOptions.Get(LastUsedTxt, ReportID, ObjectOptions."Object Type"::Report, UserId, CompanyName) then
            exit('');
        ObjectOptions.CalcFields("Option Data");
        ObjectOptions."Option Data".CreateInStream(InStr);
        InStr.ReadText(XMLTxt);
        exit(XMLTxt);
    end;

    procedure SaveReportRequestPageParameters(ReportID: Integer; XMLText: Text; OptionDataEncoding: TextEncoding)
    var
        ObjectOptions: Record "Object Options";
        OutStr: OutStream;
    begin
        if not IsObjectOptionsInsertDeleteAllowed() then
            exit;

        if IgnoreRequestParameters then
            exit;

        if XMLText = '' then
            exit;

        if ObjectOptions.Get(LastUsedTxt, ReportID, ObjectOptions."Object Type"::Report, UserId, CompanyName) then
            ObjectOptions.Delete();
        ObjectOptions.Init();
        ObjectOptions."Parameter Name" := LastUsedTxt;
        ObjectOptions."Object Type" := ObjectOptions."Object Type"::Report;
        ObjectOptions."Object ID" := ReportID;
        ObjectOptions."User Name" := UserId();
        ObjectOptions."Company Name" := CompanyName;
        ObjectOptions."Created By" := UserId();
        ObjectOptions."Option Data".CreateOutStream(OutStr, OptionDataEncoding);
        OutStr.WriteText(XMLText);
        ObjectOptions.Insert();
    end;

    procedure SaveReportRequestPageParameters(ReportID: Integer; XMLText: Text)
    begin
        SaveReportRequestPageParameters(ReportID, XMLText, TextEncoding::MSDos);
    end;

    procedure CheckForCustomLayoutReportingJob()
    var
        CustomerLayoutStatement: Codeunit "Customer Layout - Statement";
    begin
        CustomerLayoutStatement.CheckReportRunningInBackground();
    end;

    local procedure IsObjectOptionsInsertDeleteAllowed(): Boolean
    var
        [SecurityFiltering(SecurityFilter::Ignored)]
        ObjectOptions: Record "Object Options";
        User: Record User;
    begin
        if not ObjectOptions.WritePermission then
            exit(false);

        if not User.Get(UserSecurityId()) then
            exit(true);

        exit(User."License Type" <> User."License Type"::"Limited User");
    end;

    [Scope('OnPrem')]
    procedure SetIgnoreRequestParameters(IgnoreParams: Boolean)
    begin
        IgnoreRequestParameters := IgnoreParams;
    end;

    [IntegrationEvent(false, false)]
    local procedure OnIsTestMode(var TestMode: Boolean)
    begin
    end;

    [Scope('OnPrem')]
    procedure SetPredefinedRequestParameters(NewPredefinedRequestParameters: Text)
    begin
        PredefinedRequestParameters := NewPredefinedRequestParameters;
    end;

    local procedure ConvertToRepInboxOutputType(var ReportInbox: Record "Report Inbox"; ReportID: Integer)
    begin
        case OutputType of
            OutputType::Print:
                SetReportInboxOutputTypeForPrint(ReportInbox, ReportID);
            OutputType::Email:
                if PrintIfEmailIsMissing then
                    SetReportInboxOutputTypeForPrint(ReportInbox, ReportID);
            OutputType::PDF:
                ReportInbox."Output Type" := ReportInbox."Output Type"::PDF;
            OutputType::Word:
                ReportInbox."Output Type" := ReportInbox."Output Type"::Word;
            OutputType::Excel:
                ReportInbox."Output Type" := ReportInbox."Output Type"::Excel;
            OutputType::XML,
            OutputType::Preview:
                Error(OutputNotSupportedErr);
        end;
    end;

    local procedure SetReportInboxOutputTypeForPrint(var ReportInbox: Record "Report Inbox"; ReportID: Integer)
    var
        LocalCusRepLayoutCode: Code[20];
    begin
        LocalCusRepLayoutCode := ResolveCustomReportLayoutCode(CustomReportSelection);
        if IsWordLayout(ReportID, LocalCusRepLayoutCode) then
            ReportInbox."Output Type" := ReportInbox."Output Type"::Word
        else
            ReportInbox."Output Type" := ReportInbox."Output Type"::PDF;
    end;

    local procedure SendToReportInbox(IsZipFile: Boolean; ReportID: Integer)
    var
        ReportInbox: Record "Report Inbox";
        InputFile: File;
        InStr: InStream;
        OutStr: OutStream;
        ReportCaption: Text;
    begin
        ReportInbox."User ID" := CopyStr(UserId(), 1, MaxStrLen(ReportInbox."User ID"));
        if IsZipFile then begin
            ReportInbox.Validate("Output Type", ReportInbox."Output Type"::Zip);
            InputFile.Open(ZipFileName);
        end else begin
            ConvertToRepInboxOutputType(ReportInbox, ReportID);
            InputFile.Open(TempNameValueBuffer.Value);
        end;

        InputFile.CreateInStream(InStr);
        ReportInbox."Report Output".CreateOutStream(OutStr);
        CopyStream(OutStr, InStr);
        ReportCaption := GetTempLayoutReportCaption(ReportID);
        ReportInbox.Description := ReportCaption;
        ReportInbox."Report ID" := ReportID;
        ReportInbox."Report Name" := ReportCaption;
        ReportInbox."Created Date-Time" := RoundDateTime(CurrentDateTime, 60000);
        if not ReportInbox.Insert(true) then
            ReportInbox.Modify(true);
    end;

    local procedure CheckEmailSendTo(SendToEmailID: Text[250]): Boolean
    var
        EmailAccount: Codeunit "Email Account";
    begin
        if SendToEmailID = '' then
            exit(true);

        if not EmailAccount.ValidateEmailAddresses(SendToEmailID, false) then begin
            ClearLastError();
            exit(true);
        end;
    end;

    local procedure LogErrorEmailSendTo(DataRecordRef: RecordRef; UsedCustomReportSelection: Record "Custom Report Selection"; EmailBlankOrNotValid: Boolean; SendToEmailID: Text)
    var
        ReportSelectionUsage: Text;
        ErrorMessage: Text;
    begin
        if not EmailBlankOrNotValid then
            exit;

        if IsCustomReportSelectionNull(UsedCustomReportSelection) then
            ReportSelectionUsage := Format(ReportSelections.Usage)
        else
            ReportSelectionUsage := Format(UsedCustomReportSelection.Usage);

        if UsedCustomReportSelection."Send To Email" = '' then
            ErrorMessage := StrSubstNo(TargetEmailAddressErr, DataRecordRef.RecordId(), ReportSelectionUsage)
        else
            ErrorMessage := StrSubstNo(TargetEmailAddressNotValidErr, SendToEmailID, DataRecordRef.RecordId(), ReportSelectionUsage);
        ErrorMessageManagement.LogError(UsedCustomReportSelection, ErrorMessage, '');
    end;

    procedure SetRunReportOncePerFilter(NewRunReportOncePerFilter: Boolean)
    begin
        RunReportOncePerFilter := NewRunReportOncePerFilter;
    end;

    local procedure IsRunReportOncePerFilter(): Boolean
    begin
        exit(RunReportOncePerFilter);
    end;

    local procedure GetSendToEmailID(var CustomReportSelection: Record "Custom Report Selection"; var EmailID: Text[250])
    begin
        if CustomReportSelection.GetSendToEmail(true) = '' then
            GetSendToEmailIDFromSource(CustomReportSelection, EmailID)
        else
            EmailID := CustomReportSelection.GetSendToEmail(true);

        OnAfterGetSendToEmailID(CustomReportSelection, EmailID);
    end;

    local procedure GetSendToEmailIDFromSource(var CustomReportSelection: Record "Custom Report Selection"; var EmailID: Text[250])
    var
        Customer: Record Customer;
        Vendor: Record Vendor;
    begin
        case CustomReportSelection."Source Type" of
            Database::Customer:
                if (Customer.Get(CustomReportSelection."Source No.")) then
                    EmailID := Customer."E-Mail";
            Database::Vendor:
                if (Vendor.Get(CustomReportSelection."Source No.")) then
                    EmailID := Vendor."E-Mail";
            else
                OnAfterGetSendToEmailIDFromSource(CustomReportSelection, EmailID);
        end;
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterProcessReport()
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeCallReportSaveAs(ReportID: Integer; ReportFormatValue: ReportFormat)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforePrintReport(var DataRecRef: RecordRef; ReportID: Integer)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeRunReport(var DataRecRef: RecordRef; var ReportSelections: Record "Report Selections"; var EmailPrintIfEmailIsMissing: Boolean; var TempBlobIndicesNameValueBuffer: Record "Name/Value Buffer" temporary; var TempBlobList: Codeunit "Temp Blob List"; var OutputType: Option; var AnyOutputExists: Boolean; var InHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeRunReportWithCustomReportSelection(var DataRecRef: RecordRef; var ReportID: Integer; var CustomReportSelection: Record "Custom Report Selection"; var EmailPrintIfEmailIsMissing: Boolean; var TempBlobIndicesNameValueBuffer: Record "Name/Value Buffer" temporary; var TempBlobList: Codeunit "Temp Blob List"; var OutputType: Option; var AnyOutputExists: Boolean; var InHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeSetIteratorJoinFieldRef(var ReportDataRecordRef: RecordRef; var BalAcctTypeFieldRef: FieldRef; var AcctTypeFieldRef: FieldRef; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnCreateReportWithExtensionOnBeforeHtmlFileClose(ReportID: Integer; RepFormat: ReportFormat; var DataRecRef: RecordRef; var FileStream: OutStream)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnCreateReportWithExtensionOnBeforePdfFileClose(ReportID: Integer; RepFormat: ReportFormat; var DataRecRef: RecordRef; var FileStream: OutStream)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnGenerateFileNameOnAfterAssignFileName(var FileName: Text; ReportID: Integer; Extension: Text; DataRecRef: RecordRef)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnSaveAsReportOnBeforeFileClose(ReportSaved: Boolean; var DataRecRef: RecordRef; ReportID: Integer; RepFormat: ReportFormat; var FileStream: OutStream)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnSetOutputTypeOnAfterSetOutputMethod(var OutputMethod: Text)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterGetSendToEmailID(var CustomReportSelection: Record "Custom Report Selection"; var EmailID: Text[250])
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterGetSendToEmailIDFromSource(var CustomReportSelection: Record "Custom Report Selection"; var EmailID: Text[250])
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnRunRequestPageOnBeforeReportRunRequestPage(ReportId: Integer; var SavedParameters: Text)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnProcessReportPerObjectOnBeforeReportDataRecordRefLoop(ReportDataAndIteratorDiffer: Boolean; IteratorRecordRef: RecordRef)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnProcessReportPerObjectOnBeforeRunReport(ReportDataAndIteratorDiffer: Boolean; IteratorRecordRef: RecordRef; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnProcessReportPerObjectOnAfterRunReport(ReportDataAndIteratorDiffer: Boolean; IteratorRecordRef: RecordRef)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterProcessReportPerObject(ReportDataAndIteratorDiffer: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeGenerateFileNameForReport(IteratorRecordRef: RecordRef; var NameFieldRef: FieldRef; ReportDataRecordRef: RecordRef; var ObjectName: Text; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeThrowProcessReportError(var ErrorMessageHandler: Codeunit "Error Message Handler"; var ReportDataRecordRef: RecordRef; var OutputType: Option)
    begin
    end;
}