Codeunit 8800 Custom Layout Reporting, source in 29
Source29
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 + '&';
'>':
OutputTxt := OutputTxt + '>';
'<':
OutputTxt := OutputTxt + '<';
'''':
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;
}