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:
Stefano Demiliani 2026-09-15 10:38:32 +02:00 • committed by GitHub
parent b74967bc5b
commit b545b22fb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 827 additions and 1 deletions

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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.';
}

View file

@ -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.';
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -25,6 +25,7 @@ sub-skills:
- microsoft/skills/review/al-testing-review.md
- microsoft/skills/review/al-data-modeling-review.md
- microsoft/skills/review/al-query-review.md
- microsoft/skills/review/al-reporting-review.md
- microsoft/skills/review/al-appsource-review.md
- microsoft/skills/review/al-telemetry-review.md
---

View file

@ -0,0 +1,63 @@
---
kind: action-skill
id: al-reporting-review
version: 1
title: AL reporting review
description: Reviews AL Report and ReportExtension code against BCQuality reporting guidance.
inputs: [pr-diff, file-path, folder-path]
outputs: [findings-report]
bc-version: [all]
technologies: [al]
countries: [w1]
application-area: [all]
---
# AL reporting review
Reviews AL source changes against the `reporting` knowledge domain in BCQuality. This is a leaf action skill composed by `al-code-review`.
## Source
Use READ's **Bounded retrieval for review skills** workflow with `-Domain reporting`. Consume every catalog page across enabled layers before applying this leaf's Relevance and Worklist; preserve each exact catalog path and open complete bodies only for exact paths selected by the Worklist. If the helper or prepared index is unavailable or invalid, use READ's explicit path-discovery and bounded native-read fallback.
## Relevance
Apply READ's frontmatter matching rules against the task context. Use the target version from `app.json` when available and `[al]` for technologies. Retain conditionally applicable files only when configured; cap resulting confidence at `medium` and name every unknown dimension in the finding message.
Return `not-applicable` when the input contains no Report or ReportExtension declaration and no Report variable or method call.
## Worklist
Match relevant entries against changed `report` and `reportextension` objects, variables typed as `Report`, and the tokens `CurrReport`, `Skip`, `Break`, `Quit`, `Run`, `RunModal`, `RunRequestPage`, `Execute`, `Print`, `SaveAs`, `DownloadFromStream`, `Data Compression`, `SetTableView`, `DataItemTableView`, `OnPreReport`, `OnPostReport`, `OnPreDataItem`, `OnAfterGetRecord`, and report-extension dataset triggers.
Apply this targeted check even when token overlap would rank the article below the worklist cutoff:
- The same Report variable has two logically independent `RunModal()` executions without `Clear` before the second configuration — `clear-report-variable-before-independent-runmodal`.
- `CurrReport.Break()` is used inside an explicit loop while reachable statements after the loop are expected to finish the current trigger — `currreport-break-ends-the-current-trigger`.
- `CurrReport.Quit()` follows database writes or the report relies on `OnPostReport` finalization — `currreport-quit-rolls-back-and-skips-onpostreport`.
- `CurrReport.Skip()` is followed by reachable code in the same trigger, or later record triggers contain work that is unsafe for skipped records — `currreport-skip-does-not-stop-trigger-code`.
- A loop reachable from one Web client action calls `Report.Run`, `Report.RunModal`, or `DownloadFromStream` more than once instead of producing one archive download — `report-output-in-a-loop-needs-one-client-download`. Do not select this article when the context is non-Web or the loop is provably single-iteration.
- A ReportExtension before-trigger establishes a filter or value that visible base-trigger code subsequently replaces — `reportextension-dataitem-trigger-order-is-explicit`. Do not select this article from a before-trigger alone.
- A ReportExtension `OnPreReport` prepares state consumed by the base `OnPreReport`, or its `OnPostReport` prepares state already consumed by the base `OnPostReport` — `reportextension-report-triggers-run-after-base-triggers`. Require visible base behavior or equivalent established evidence.
- A report's `DataItemTableView` and a caller's `SetTableView` apply mutually exclusive filters to the same field — `settableview-cannot-broaden-dataitemtableview`. Require both views or equivalent direct evidence; `SetTableView` alone is not a finding.
- The value returned by `Report.RunRequestPage()` reaches `Report.Execute`, `Report.Print`, or `Report.SaveAs` without an empty-string cancellation check — `stop-when-runrequestpage-returns-empty-parameters`.
Resolve layer conflicts per READ. When no reporting knowledge exists, emit `no-knowledge`; when knowledge exists but no article matches the changed report code, emit `completed` with no findings.
## Action
Evaluate every worklist article against the diff's report control flow and surrounding triggers.
- Emit `major` for an unambiguous Anti Pattern that causes incorrect output, persisted side effects, or lost work.
- Emit `minor` when code contradicts a Best Practice but the effect depends on unseen report or caller context.
- Do not emit applicability-only information. A reporting article produces a finding only when changed code violates its normative guidance.
Set confidence to `high` for locally visible control flow and `medium` when base-report behavior, callers, or missing context affect the conclusion. Domain-scoped agent findings follow DO's precision bar and remain capped at `minor`/`medium`.
Provide `suggested-code` only when the replacement is complete, local, and unambiguous. Otherwise set `suggested-code-omission-reason`.
Outcome selection follows DO: `completed`, `no-knowledge`, `not-applicable`, `partial`, or `failed`.
## Output
Output conforms to the DO findings-report contract. Every finding this skill emits MUST set `findings[].domain` to `"Reporting"`.