mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 06:36:55 +01:00
Add reporting review guidance and evaluation fixtures (#183)
* knowledge(performance): add job queue reliability guidance * Address Job Queue review feedback * Address Job Queue routing review feedback * Encode job queue conflict in fixtures * Add reporting review guidance and fixtures * Fix reporting article reference
This commit is contained in:
parent
b74967bc5b
commit
b545b22fb9
31 changed files with 827 additions and 1 deletions
|
|
@ -0,0 +1,16 @@
|
|||
codeunit 50102 "Run Customer Reports"
|
||||
{
|
||||
procedure RunBlockedAndUnblockedCustomers()
|
||||
var
|
||||
Customer: Record Customer;
|
||||
CustomerList: Report "Customer - List";
|
||||
begin
|
||||
Customer.SetRange(Blocked, Customer.Blocked::All);
|
||||
CustomerList.SetTableView(Customer);
|
||||
CustomerList.RunModal();
|
||||
|
||||
Customer.SetRange(Blocked, Customer.Blocked::" ");
|
||||
CustomerList.SetTableView(Customer);
|
||||
CustomerList.RunModal();
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
codeunit 50102 "Run Customer Reports"
|
||||
{
|
||||
procedure RunBlockedAndUnblockedCustomers()
|
||||
var
|
||||
Customer: Record Customer;
|
||||
CustomerList: Report "Customer - List";
|
||||
begin
|
||||
Customer.SetRange(Blocked, Customer.Blocked::All);
|
||||
CustomerList.SetTableView(Customer);
|
||||
CustomerList.RunModal();
|
||||
|
||||
Clear(CustomerList);
|
||||
Customer.SetRange(Blocked, Customer.Blocked::" ");
|
||||
CustomerList.SetTableView(Customer);
|
||||
CustomerList.RunModal();
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: reporting
|
||||
keywords: [report, runmodal, clear, settableview, instance, state, filters]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Clear a Report variable before an independent RunModal execution
|
||||
|
||||
## Description
|
||||
|
||||
`Report.Run()` automatically clears the report variable after execution, but `Report.RunModal()` does not. Reconfiguring and running the same variable for an independent operation can therefore retain filters and other instance state from the previous run.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Call `Clear(ReportVariable)` before configuring a new, logically independent `RunModal()` execution on a reused report variable. No clear is required after a single execution, and retaining state is valid when the subsequent run intentionally continues with the same configuration.
|
||||
|
||||
See sample: [`clear-report-variable-before-independent-runmodal.good.al`](clear-report-variable-before-independent-runmodal.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Run the same report variable modally for two independent views without clearing it between runs. The second `SetTableView` can only narrow the existing report view, so filters retained by the instance can make the second result incomplete or empty.
|
||||
|
||||
See sample: [`clear-report-variable-before-independent-runmodal.bad.al`](clear-report-variable-before-independent-runmodal.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
`Report.RunModal()` method — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/report/reportinstance-runmodal-method
|
||||
|
||||
`Report.Run()` method — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/report/reportinstance-run-method
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
report 50105 "Customer Entry Review"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(Customer; Customer)
|
||||
{
|
||||
trigger OnAfterGetRecord()
|
||||
var
|
||||
EntryNo: Integer;
|
||||
begin
|
||||
repeat
|
||||
EntryNo += 1;
|
||||
if EntryNo = 5 then
|
||||
CurrReport.Break();
|
||||
until EntryNo = 10;
|
||||
|
||||
MarkCustomerReviewed();
|
||||
end;
|
||||
}
|
||||
}
|
||||
|
||||
local procedure MarkCustomerReviewed()
|
||||
begin
|
||||
ReviewedCustomerCount += 1;
|
||||
end;
|
||||
|
||||
var
|
||||
ReviewedCustomerCount: Integer;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
report 50105 "Customer Entry Review"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(Customer; Customer)
|
||||
{
|
||||
trigger OnAfterGetRecord()
|
||||
var
|
||||
EntryNo: Integer;
|
||||
StopReview: Boolean;
|
||||
begin
|
||||
repeat
|
||||
EntryNo += 1;
|
||||
StopReview := EntryNo = 5;
|
||||
until StopReview or (EntryNo = 10);
|
||||
|
||||
MarkCustomerReviewed();
|
||||
end;
|
||||
}
|
||||
}
|
||||
|
||||
local procedure MarkCustomerReviewed()
|
||||
begin
|
||||
ReviewedCustomerCount += 1;
|
||||
end;
|
||||
|
||||
var
|
||||
ReviewedCustomerCount: Integer;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: reporting
|
||||
keywords: [report, currreport, break, loop, trigger, control-flow]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CurrReport.Break ends the current trigger
|
||||
|
||||
## Description
|
||||
|
||||
`CurrReport.Break()` inside a report dataitem trigger does more than leave an AL loop. It terminates the current trigger and omits the current record from the dataset. The report runtime still invokes the remaining triggers for that record. Consequently, statements after the loop in the current trigger do not run, while later report triggers can still produce side effects.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Use an explicit loop condition or the AL `break` statement when only the loop must end and the current trigger must continue. Use `CurrReport.Break()` only when ending the trigger and omitting the current record are both intended, and keep subsequent report triggers safe for that omitted record.
|
||||
|
||||
See sample: [`currreport-break-ends-the-current-trigger.good.al`](currreport-break-ends-the-current-trigger.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Call `CurrReport.Break()` inside a loop and rely on statements after the loop to finish processing the current record. Those statements are unreachable when the call executes, the record is omitted, and remaining report triggers still run.
|
||||
|
||||
See sample: [`currreport-break-ends-the-current-trigger.bad.al`](currreport-break-ends-the-current-trigger.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
`Report.Break()` method — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/report/reportinstance-break-method
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
report 50101 "Update Customer Review"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(Customer; Customer)
|
||||
{
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
"Last Date Modified" := Today();
|
||||
Modify();
|
||||
|
||||
if Blocked <> Blocked::" " then
|
||||
CurrReport.Quit();
|
||||
end;
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnPostReport()
|
||||
begin
|
||||
Message(CompletedMsg);
|
||||
end;
|
||||
|
||||
var
|
||||
CompletedMsg: Label 'Customer review completed.';
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
report 50101 "Update Customer Review"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(Customer; Customer)
|
||||
{
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
if Blocked <> Blocked::" " then
|
||||
Error(BlockedCustomerErr, "No.");
|
||||
|
||||
"Last Date Modified" := Today();
|
||||
Modify();
|
||||
end;
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnPostReport()
|
||||
begin
|
||||
Message(CompletedMsg);
|
||||
end;
|
||||
|
||||
var
|
||||
BlockedCustomerErr: Label 'Customer %1 is blocked.', Comment = '%1 = customer number';
|
||||
CompletedMsg: Label 'Customer review completed.';
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: reporting
|
||||
keywords: [report, currreport, quit, rollback, onpostreport, transaction, control-flow]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CurrReport.Quit rolls back report changes and skips OnPostReport
|
||||
|
||||
## Description
|
||||
|
||||
`CurrReport.Quit()` aborts the report without committing database changes made during its execution. It also prevents `OnPostReport` from running. It is therefore not a normal early-return mechanism for a processing report that expects earlier writes or finalization in `OnPostReport` to survive.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Use `CurrReport.Quit()` only when silently aborting the report, rolling back its database changes, and skipping `OnPostReport` are all intentional. When processing must stop with a failure, raise an error. When completed work and `OnPostReport` must be preserved, structure the dataitem control flow without `Quit()`.
|
||||
|
||||
See sample: [`currreport-quit-rolls-back-and-skips-onpostreport.good.al`](currreport-quit-rolls-back-and-skips-onpostreport.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Modify data and then call `CurrReport.Quit()` while relying on those writes or on `OnPostReport` finalization. The report exits without committing its changes and never invokes `OnPostReport`.
|
||||
|
||||
See sample: [`currreport-quit-rolls-back-and-skips-onpostreport.bad.al`](currreport-quit-rolls-back-and-skips-onpostreport.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
`Report.Quit()` method — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/report/reportinstance-quit-method
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
report 50100 "Released Customer List"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(Customer; Customer)
|
||||
{
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
if Blocked <> Blocked::" " then
|
||||
CurrReport.Skip();
|
||||
|
||||
CountIncludedCustomer();
|
||||
end;
|
||||
}
|
||||
}
|
||||
|
||||
local procedure CountIncludedCustomer()
|
||||
begin
|
||||
IncludedCustomerCount += 1;
|
||||
end;
|
||||
|
||||
var
|
||||
IncludedCustomerCount: Integer;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
report 50100 "Released Customer List"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(Customer; Customer)
|
||||
{
|
||||
trigger OnAfterGetRecord()
|
||||
begin
|
||||
if Blocked <> Blocked::" " then begin
|
||||
CurrReport.Skip();
|
||||
exit;
|
||||
end;
|
||||
|
||||
CountIncludedCustomer();
|
||||
end;
|
||||
}
|
||||
}
|
||||
|
||||
local procedure CountIncludedCustomer()
|
||||
begin
|
||||
IncludedCustomerCount += 1;
|
||||
end;
|
||||
|
||||
var
|
||||
IncludedCustomerCount: Integer;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: reporting
|
||||
keywords: [report, currreport, skip, trigger, onaftergetrecord, control-flow]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CurrReport.Skip omits the record but does not stop trigger code
|
||||
|
||||
## Description
|
||||
|
||||
`CurrReport.Skip()` omits the current record from the report dataset and continues processing with the next record. It does not terminate the current trigger, and the remaining triggers for the current record still run. Code placed after `Skip()` can therefore produce side effects for a record that never appears in the output.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When no further code in the current trigger should run for a skipped record, call `CurrReport.Skip()` and then exit the trigger explicitly. Keep later record triggers safe for skipped records because the report runtime still invokes them.
|
||||
|
||||
See sample: [`currreport-skip-does-not-stop-trigger-code.good.al`](currreport-skip-does-not-stop-trigger-code.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Call `CurrReport.Skip()` and rely on it to bypass subsequent statements or later record triggers. The record is removed from the dataset, but those statements and triggers can still update state, write data, or perform expensive work.
|
||||
|
||||
See sample: [`currreport-skip-does-not-stop-trigger-code.bad.al`](currreport-skip-does-not-stop-trigger-code.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
`Report.Skip()` method — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/report/reportinstance-skip-method
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50106 "Download Customer Reports"
|
||||
{
|
||||
procedure DownloadReports(var Customer: Record Customer)
|
||||
var
|
||||
CustomerView: Record Customer;
|
||||
begin
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
CustomerView := Customer;
|
||||
CustomerView.SetRecFilter();
|
||||
Report.Run(Report::"Customer - List", false, false, CustomerView);
|
||||
until Customer.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
codeunit 50106 "Download Customer Reports"
|
||||
{
|
||||
procedure DownloadReports(var Customer: Record Customer)
|
||||
var
|
||||
CustomerView: Record Customer;
|
||||
CustomerList: Report "Customer - List";
|
||||
DataCompression: Codeunit "Data Compression";
|
||||
ReportTempBlob: Codeunit "Temp Blob";
|
||||
ZipTempBlob: Codeunit "Temp Blob";
|
||||
ReportInStream: InStream;
|
||||
ZipInStream: InStream;
|
||||
ReportOutStream: OutStream;
|
||||
ZipOutStream: OutStream;
|
||||
ZipFileName: Text;
|
||||
begin
|
||||
DataCompression.CreateZipArchive();
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
Clear(CustomerList);
|
||||
Clear(ReportTempBlob);
|
||||
CustomerView := Customer;
|
||||
CustomerView.SetRecFilter();
|
||||
CustomerList.SetTableView(CustomerView);
|
||||
ReportTempBlob.CreateOutStream(ReportOutStream);
|
||||
CustomerList.SaveAs('', ReportFormat::Pdf, ReportOutStream);
|
||||
ReportTempBlob.CreateInStream(ReportInStream);
|
||||
DataCompression.AddEntry(ReportInStream, Customer."No." + '.pdf');
|
||||
until Customer.Next() = 0;
|
||||
|
||||
ZipTempBlob.CreateOutStream(ZipOutStream);
|
||||
DataCompression.SaveZipArchive(ZipOutStream);
|
||||
DataCompression.CloseZipArchive();
|
||||
ZipTempBlob.CreateInStream(ZipInStream);
|
||||
ZipFileName := 'CustomerReports.zip';
|
||||
DownloadFromStream(ZipInStream, '', '', '*.zip', ZipFileName);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: reporting
|
||||
keywords: [report, run, saveas, downloadfromstream, web-client, loop, zip, data-compression]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Report output in a loop needs one client download
|
||||
|
||||
## Description
|
||||
|
||||
The Business Central Web client can deliver only one file per request. When AL generates or downloads a report file repeatedly in the same request, only the last file is delivered to the browser. Earlier report output is silently unavailable to the user even though every iteration ran.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Generate each report into a stream, add the streams to one archive, and call `DownloadFromStream` once after the loop. A direct report run or download inside a loop is valid only when the execution context does not use the Web client or the loop is guaranteed to execute at most once.
|
||||
|
||||
See sample: [`report-output-in-a-loop-needs-one-client-download.good.al`](report-output-in-a-loop-needs-one-client-download.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Call `Report.Run`, `Report.RunModal`, or `DownloadFromStream` repeatedly in a loop initiated by one Web client action and expect every generated file to reach the browser. The client receives only the last download.
|
||||
|
||||
See sample: [`report-output-in-a-loop-needs-one-client-download.bad.al`](report-output-in-a-loop-needs-one-client-download.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
`File.DownloadFromStream` method — https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/file/file-downloadfromstream-method
|
||||
|
||||
`Data Compression` codeunit — https://learn.microsoft.com/dynamics365/business-central/application/system-application/codeunit/system.io.data-compression
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
report 50103 "Base Customer Export"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(Customer; Customer)
|
||||
{
|
||||
trigger OnPreDataItem()
|
||||
begin
|
||||
SetRange(Blocked, Blocked::" ");
|
||||
SetRange("Country/Region Code");
|
||||
end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reportextension 50104 "Local Customer Export" extends "Base Customer Export"
|
||||
{
|
||||
dataset
|
||||
{
|
||||
modify(Customer)
|
||||
{
|
||||
trigger OnBeforePreDataItem()
|
||||
begin
|
||||
SetFilter("Country/Region Code", '<>%1', '');
|
||||
end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
report 50103 "Base Customer Export"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(Customer; Customer)
|
||||
{
|
||||
trigger OnPreDataItem()
|
||||
begin
|
||||
SetRange(Blocked, Blocked::" ");
|
||||
SetRange("Country/Region Code");
|
||||
end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reportextension 50104 "Local Customer Export" extends "Base Customer Export"
|
||||
{
|
||||
dataset
|
||||
{
|
||||
modify(Customer)
|
||||
{
|
||||
trigger OnAfterPreDataItem()
|
||||
begin
|
||||
SetFilter("Country/Region Code", '<>%1', '');
|
||||
end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
bc-version: [19..]
|
||||
domain: reporting
|
||||
keywords: [reportextension, report, dataitem, trigger-order, onbeforepredataitem, onafterpredataitem, onbeforeaftergetrecord, onafteraftergetrecord]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Choose ReportExtension dataitem triggers by their order around the base trigger
|
||||
|
||||
## Description
|
||||
|
||||
ReportExtension dataitem triggers run at defined points around the corresponding base-report trigger. `OnBeforePreDataItem` and `OnBeforeAfterGetRecord` run before the base trigger; `OnAfterPreDataItem` and `OnAfterAfterGetRecord` run after it. A filter or calculated value can be overwritten when an extension uses a before-trigger even though its result must be final after base processing.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Choose the before or after trigger from the required ordering relative to base behavior. Use an after-trigger when the extension must observe or refine the final view or value produced by the base trigger. A before-trigger is valid when the base report must consume the extension's state.
|
||||
|
||||
See sample: [`reportextension-dataitem-trigger-order-is-explicit.good.al`](reportextension-dataitem-trigger-order-is-explicit.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Place extension logic in a before-trigger while relying on its filter or value to survive a base trigger that can replace it. Do not report a before-trigger merely because an after-trigger exists; the defect requires visible base behavior or another reliable source showing that ordering changes the result.
|
||||
|
||||
See sample: [`reportextension-dataitem-trigger-order-is-explicit.bad.al`](reportextension-dataitem-trigger-order-is-explicit.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
`OnBeforePreDataItem` report-extension trigger — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/triggers-auto/reportextensiondatasetmodify/devenv-onbeforepredataitem-reportextensiondatasetmodify-trigger
|
||||
|
||||
`OnAfterPreDataItem` report-extension trigger — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/triggers-auto/reportextensiondatasetmodify/devenv-onafterpredataitem-reportextensiondatasetmodify-trigger
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
report 50110 "Customer Export"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(Customer; Customer)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnPreReport()
|
||||
var
|
||||
ExportSetup: Record "Customer Export Setup";
|
||||
begin
|
||||
ExportSetup.Get();
|
||||
ExportSetup.TestField("Export Date");
|
||||
end;
|
||||
}
|
||||
|
||||
reportextension 50111 "Customer Export Extension" extends "Customer Export"
|
||||
{
|
||||
trigger OnPreReport()
|
||||
var
|
||||
ExportSetup: Record "Customer Export Setup";
|
||||
begin
|
||||
ExportSetup.Get();
|
||||
ExportSetup.Validate("Export Date", Today());
|
||||
ExportSetup.Modify(true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
report 50110 "Customer Export"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(Customer; Customer)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnPreReport()
|
||||
var
|
||||
ExportDate: Date;
|
||||
begin
|
||||
OnBeforeResolveExportDate(ExportDate);
|
||||
if ExportDate = 0D then
|
||||
Error(ExportDateRequiredErr);
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnBeforeResolveExportDate(var ExportDate: Date)
|
||||
begin
|
||||
end;
|
||||
|
||||
var
|
||||
ExportDateRequiredErr: Label 'An export date is required.';
|
||||
}
|
||||
|
||||
codeunit 50111 "Customer Export Extension"
|
||||
{
|
||||
[EventSubscriber(ObjectType::Report, Report::"Customer Export", 'OnBeforeResolveExportDate', '', false, false)]
|
||||
local procedure SetExportDate(var ExportDate: Date)
|
||||
begin
|
||||
ExportDate := Today();
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [18..]
|
||||
domain: reporting
|
||||
keywords: [reportextension, report, trigger-order, onprereport, onpostreport, base-report, integration-event]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# ReportExtension report triggers run after base report triggers
|
||||
|
||||
## Description
|
||||
|
||||
`OnPreReport` and `OnPostReport` on a ReportExtension run after the corresponding triggers on the base report. An extension `OnPreReport` cannot prepare state that the base `OnPreReport` must consume, and an extension `OnPostReport` cannot affect finalization that the base `OnPostReport` has already completed.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Use a base-report event at the required execution point when extension logic must run before or within a base trigger. Use ReportExtension `OnPreReport` and `OnPostReport` only for work that is correct after the corresponding base trigger. Report a violation only when the base trigger and extension dependency are both visible or otherwise established.
|
||||
|
||||
See sample: [`reportextension-report-triggers-run-after-base-triggers.good.al`](reportextension-report-triggers-run-after-base-triggers.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Initialize data in a ReportExtension `OnPreReport` and rely on the base report's `OnPreReport` to consume it, or perform extension `OnPostReport` work that the base `OnPostReport` needed beforehand. The base trigger has already run.
|
||||
|
||||
See sample: [`reportextension-report-triggers-run-after-base-triggers.bad.al`](reportextension-report-triggers-run-after-base-triggers.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
Report extension object — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-report-ext-object
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
report 50107 "Selected Sales Orders"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(SalesHeader; "Sales Header")
|
||||
{
|
||||
DataItemTableView = where("Document Type" = const(Order), Status = const(Open));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
codeunit 50108 "Run Selected Sales Orders"
|
||||
{
|
||||
procedure RunReleasedOrders()
|
||||
var
|
||||
SalesHeader: Record "Sales Header";
|
||||
SelectedSalesOrders: Report "Selected Sales Orders";
|
||||
begin
|
||||
SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order);
|
||||
SalesHeader.SetRange(Status, SalesHeader.Status::Released);
|
||||
SelectedSalesOrders.SetTableView(SalesHeader);
|
||||
SelectedSalesOrders.RunModal();
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
report 50107 "Selected Sales Orders"
|
||||
{
|
||||
ProcessingOnly = true;
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(SalesHeader; "Sales Header")
|
||||
{
|
||||
DataItemTableView = where("Document Type" = const(Order));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
codeunit 50108 "Run Selected Sales Orders"
|
||||
{
|
||||
procedure RunReleasedOrders()
|
||||
var
|
||||
SalesHeader: Record "Sales Header";
|
||||
SelectedSalesOrders: Report "Selected Sales Orders";
|
||||
begin
|
||||
SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order);
|
||||
SalesHeader.SetRange(Status, SalesHeader.Status::Released);
|
||||
SelectedSalesOrders.SetTableView(SalesHeader);
|
||||
SelectedSalesOrders.RunModal();
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: reporting
|
||||
keywords: [report, settableview, dataitemtableview, filter, view, narrowing]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# SetTableView cannot broaden DataItemTableView
|
||||
|
||||
## Description
|
||||
|
||||
`Report.SetTableView()` applies the supplied record view by narrowing the view already defined by the report dataitem's `DataItemTableView`. It cannot remove or broaden a static dataitem filter. A caller that requests records excluded by `DataItemTableView` therefore produces an empty dataset rather than overriding the report filter.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Keep only invariant restrictions in `DataItemTableView`. When callers must select among values, leave that dimension open in the static view and pass the required filter through `SetTableView`. Review this as a defect only when the report definition and caller together show a contradictory filter.
|
||||
|
||||
See sample: [`settableview-cannot-broaden-dataitemtableview.good.al`](settableview-cannot-broaden-dataitemtableview.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Define a static filter in `DataItemTableView` and call `SetTableView` with a mutually exclusive filter while expecting the runtime view to replace the static one. The filters are intersected and no records are selected.
|
||||
|
||||
See sample: [`settableview-cannot-broaden-dataitemtableview.bad.al`](settableview-cannot-broaden-dataitemtableview.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
`Report.SetTableView()` method — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/report/reportinstance-settableview-method
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
codeunit 50109 "Export Customer Report"
|
||||
{
|
||||
procedure ExportReport()
|
||||
var
|
||||
TempBlob: Codeunit "Temp Blob";
|
||||
ReportOutStream: OutStream;
|
||||
RequestPageParameters: Text;
|
||||
begin
|
||||
RequestPageParameters := Report.RunRequestPage(Report::"Customer - List");
|
||||
TempBlob.CreateOutStream(ReportOutStream);
|
||||
Report.SaveAs(
|
||||
Report::"Customer - List",
|
||||
RequestPageParameters,
|
||||
ReportFormat::Pdf,
|
||||
ReportOutStream);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
codeunit 50109 "Export Customer Report"
|
||||
{
|
||||
procedure ExportReport()
|
||||
var
|
||||
TempBlob: Codeunit "Temp Blob";
|
||||
ReportOutStream: OutStream;
|
||||
RequestPageParameters: Text;
|
||||
begin
|
||||
RequestPageParameters := Report.RunRequestPage(Report::"Customer - List");
|
||||
if RequestPageParameters = '' then
|
||||
exit;
|
||||
|
||||
TempBlob.CreateOutStream(ReportOutStream);
|
||||
Report.SaveAs(
|
||||
Report::"Customer - List",
|
||||
RequestPageParameters,
|
||||
ReportFormat::Pdf,
|
||||
ReportOutStream);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: reporting
|
||||
keywords: [report, runrequestpage, cancel, parameters, saveas, execute, print]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Stop when RunRequestPage returns empty parameters
|
||||
|
||||
## Description
|
||||
|
||||
`Report.RunRequestPage()` returns an empty string when the user chooses **Cancel**. Passing that value to `Report.Execute`, `Report.Print`, or `Report.SaveAs` ignores the cancellation and can run the report with default parameters instead.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Test the returned parameter string immediately after `RunRequestPage()` and exit when it is empty. Pass the value to `Execute`, `Print`, or `SaveAs` only after the user has confirmed the request page.
|
||||
|
||||
See sample: [`stop-when-runrequestpage-returns-empty-parameters.good.al`](stop-when-runrequestpage-returns-empty-parameters.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Call `RunRequestPage()` and unconditionally pass its return value to a report execution method. Choosing **Cancel** can still execute, print, or save the report.
|
||||
|
||||
See sample: [`stop-when-runrequestpage-returns-empty-parameters.bad.al`](stop-when-runrequestpage-returns-empty-parameters.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
`Report.RunRequestPage()` method — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/report/report-runrequestpage-method
|
||||
Loading…
Add table
Add a link
Reference in a new issue