6.3 Document Reports & Request Page Programming

Key Takeaways

  • Professional Business Central document reports utilize the Integer dataitem loop pattern (CopyLoop) to render the original document and multiple numbered copies in a single print job.
  • PageLoop integer dataitems isolate line item pagination, running balances, and transport headers/footers per document copy.
  • The requestpage block defines user options, date ranges, and output filters, allowing users to customize execution parameters prior to dataset generation.
  • Setting SaveValues = true on the request page persists user selections across sessions on a per-user, per-report basis in the tenant database.
  • The OnQueryClosePage trigger on request pages validates user parameters before processing begins, returning false or throwing an error to prevent report execution if inputs are invalid.
Last updated: August 2026

6.3 Document Reports & Request Page Programming

Commercial ERP document reports—such as Sales Invoices, Purchase Orders, and Order Confirmations—require complex operational capabilities that extend far beyond simple tabular data dumps. In enterprise environments, document reports must reliably handle multi-copy generation with dynamic watermarks ("ORIGINAL", "COPY 1", "COPY 2"), per-copy page numbering resets, transport running balances across multi-page tables, and interactive request page parameters. For developers preparing for the MB-820 certification exam, mastering the structural patterns and lifecycle triggers of document reporting is a crucial requirement.


1. The Professional Document Report Pattern (CopyLoop & PageLoop)

In standard Business Central document reporting, invoices, shipments, and orders must frequently be printed in duplicate or triplicate for logistics, accounting, and customer archiving. Rather than requiring users to manually run the report multiple times, the report architecture utilizes nested Integer DataItem loops (conventionally named CopyLoop and PageLoop).

report 50120 "Standard Sales Order Conf."
{
    UsageCategory = Documents;
    ApplicationArea = Basic, Suite;
    DefaultRenderingLayout = RDLC_Layout;

    dataset
    {
        dataitem(Header; "Sales Header")
        {
            DataItemTableView = sorting("Document Type", "No.") where("Document Type" = const(Order));
            RequestFilterFields = "No.", "Sell-to Customer No.", "Posting Date";

            dataitem(CopyLoop; Integer)
            {
                DataItemTableView = sorting(Number);
                
                column(CopyNo; Number) { }
                column(CopyText; CopyText) { }
                column(OutputNo; OutputNo) { }

                dataitem(PageLoop; Integer)
                {
                    DataItemTableView = sorting(Number);
                    
                    column(CompanyInfoName; CompanyInfo.Name) { }
                    column(CompanyInfoAddress; CompanyInfo.Address) { }
                    column(CompanyInfoVATRegNo; CompanyInfo."VAT Registration No.") { }
                    column(DocumentTitle; DocumentTitle) { }
                    column(BillToCustomerNo; Header."Bill-to Customer No.") { IncludeCaption = true; }
                    column(BillToCustName; Header."Bill-to Name") { }

                    dataitem(Line; "Sales Line")
                    {
                        DataItemLink = "Document Type" = field("Document Type"),
                                       "Document No." = field("No.");
                        DataItemLinkReference = Header;
                        DataItemTableView = sorting("Document Type", "Document No.", "Line No.");

                        column(LineNo; "Line No.") { }
                        column(ItemNo; "No.") { IncludeCaption = true; }
                        column(Description; Description) { IncludeCaption = true; }
                        column(Quantity; Quantity) { IncludeCaption = true; }
                        column(UnitPrice; "Unit Price") { IncludeCaption = true; }
                        column(LineAmount; "Line Amount") { IncludeCaption = true; }
                    }

                    dataitem(VATCounter; Integer)
                    {
                        DataItemTableView = sorting(Number);

                        column(VATBase; TempVATAmountLine."VAT Base") { AutoFormatType = 1; }
                        column(VATAmount; TempVATAmountLine."VAT Amount") { AutoFormatType = 1; }
                        column(VATIdentifier; TempVATAmountLine."VAT Identifier") { }

                        trigger OnPreDataItem()
                        begin
                            VATCounter.SetRange(Number, 1, TempVATAmountLine.Count());
                        end;

                        trigger OnAfterGetRecord()
                        begin
                            TempVATAmountLine.GetLine(Number);
                        end;
                    }

                    trigger OnPreDataItem()
                    begin
                        PageLoop.SetRange(Number, 1, 1);
                    end;
                }

                trigger OnPreDataItem()
                begin
                    NoOfLoops := 1 + Abs(NoOfCopies);
                    CopyLoop.SetRange(Number, 1, NoOfLoops);
                    OutputNo := 1;
                end;

                trigger OnAfterGetRecord()
                begin
                    if Number > 1 then begin
                        CopyText := FormatDocument.GetCopyText();
                        OutputNo += 1;
                    end else
                        CopyText := '';
                end;
            }
        }
    }

    requestpage
    {
        // Request page implementation
    }

    var
        CompanyInfo: Record "Company Information";
        TempVATAmountLine: Record "VAT Amount Line" temporary;
        FormatDocument: Codeunit "Format Document";
        NoOfCopies: Integer;
        NoOfLoops: Integer;
        OutputNo: Integer;
        CopyText: Text[30];
        DocumentTitle: Text[50];
}

Breakdown of the Multi-Loop Hierarchy

DataItem LevelDataItem Name & TypeArchitectural Purpose & Execution Mechanics
Level 1 (Root)Header (Sales Header)Queries and iterates through the selected sales orders based on table filters and request page criteria.
Level 2 (Copy)CopyLoop (Integer)Evaluates NoOfCopies in OnPreDataItem and sets its range from 1 to (1 + NoOfCopies). Iteration 1 represents the "ORIGINAL" document; iterations 2+ represent duplicate copies with CopyText watermarks.
Level 3 (Page)PageLoop (Integer)Filtered strictly to SetRange(Number, 1, 1) to execute once per copy. Acts as the primary grouping anchor for page headers, customer billing information, and subtotal resets in the layout.
Level 4 (Lines)Line (Sales Line)Linked directly to Header via DataItemLinkReference = Header and DataItemLink. Emits line item details for every copy loop iteration.
Level 4 (Summary)VATCounter (Integer)Iterates through the in-memory TempVATAmountLine temporary table buffer to render tax summary grids after all document lines have processed.
Loading diagram...
Document Report DataItem Hierarchy & Iteration Execution Tree

2. Pagination, Page Breaks & Transport Balances

In commercial document printing, long sales orders and invoices regularly span multiple physical pages. Adhering to international accounting and tax standards requires precise control over page breaks and carried-forward totals.

Transport Headers & Transport Footers (Carried-Forward Balances)

When line items overflow onto a second or third page:

  1. Transport Footer (Page Bottom): Displays a running subtotal of all lines printed up to the bottom of the current page, labeled as "Carried Forward: $X,XXX.XX".
  2. Transport Header (Page Top): Displays the identical carried-forward balance at the top of the next page, labeled as "Brought Forward: $X,XXX.XX".
  3. Implementation: In AL and RDLC, developers track cumulative line amounts in AL variables or RDLC running total expressions (=RunningValue(Fields!LineAmount.Value, Sum, "PageLoopGroup")), toggling visibility based on page position.

RDLC Grouping and Page Breaks

To guarantee that multiple documents (or copies of the same document) print cleanly without page number bleeding:

  • Group Break Placement: On the Tablix group bound to Header and CopyLoop, developers configure PageBreak.BreakLocation = Between (or After).
  • Page Number Reset: Setting PageBreak.ResetPageNumber = True ensures each invoice and each copy begins on Page 1, resetting the built-in Globals!PageNumber counter.
  • KeepTogether Property: Applying KeepTogether = True to individual Tablix rows or entire summary sections (e.g., VAT Breakdown or Total Summary grids) prevents the rendering engine from splitting a single summary block across physical page breaks.

3. Report Request Page Architecture & UI Design

The requestpage block allows developers to capture user options, specify runtime filters, and validate parameters prior to dataset generation.

requestpage
{
    SaveValues = true;
    SaveLayout = false;

    layout
    {
        area(Content)
        {
            group(Options)
            {
                Caption = 'Options';
                
                field(NoOfCopiesField; NoOfCopies)
                {
                    ApplicationArea = All;
                    Caption = 'No. of Copies';
                    ToolTip = 'Specifies the number of copies to print in addition to the original document.';
                    MinValue = 0;
                    MaxValue = 10;
                }
                field(ShowInternalInfoField; ShowInternalInfo)
                {
                    ApplicationArea = All;
                    Caption = 'Show Internal Information';
                    ToolTip = 'Specifies if internal notes and hidden work lines should be included.';
                }
                field(LogInteractionField; LogInteraction)
                {
                    ApplicationArea = All;
                    Caption = 'Log Interaction';
                    ToolTip = 'Specifies if an interaction entry should be created for this customer.';
                }
                field(DisplayInLCYField; DisplayInLCY)
                {
                    ApplicationArea = All;
                    Caption = 'Show Amounts in LCY';
                    ToolTip = 'Specifies if foreign currency amounts should be converted to local currency.';
                }
            }
        }
    }

    actions
    {
        area(Processing)
        {
            action(ResetDefaults)
            {
                ApplicationArea = All;
                Caption = 'Reset to Defaults';
                Image = ResetStatus;
                
                trigger OnAction()
                begin
                    NoOfCopies := 0;
                    ShowInternalInfo := false;
                    LogInteraction := true;
                    DisplayInLCY := false;
                end;
            }
        }
    }

    trigger OnOpenPage()
    begin
        if not SaveValuesLoaded then
            InitDefaults();
    end;

    trigger OnQueryClosePage(CloseAction: Action): Boolean
    begin
        if CloseAction in [Action::OK, Action::LookupOK, Action::Preview] then begin
            if (StartDate <> 0D) and (EndDate <> 0D) and (EndDate < StartDate) then
                Error('End Date (%1) cannot be earlier than Start Date (%2).', EndDate, StartDate);
        end;
        exit(true);
    end;
}

Request Page Lifecycle and Property Mechanics

  • SaveValues = true: When enabled, Business Central stores the user-entered values of request page controls and dataitem filters in the tenant database per user ID and object ID. When that same user runs the report again, the previous settings are preloaded automatically.
  • OnOpenPage: Trigger executed when the request page is instantiated in the client. Used to initialize default filter dates, lookup values, and dynamic control visibility.
  • OnQueryClosePage(CloseAction: Action): Boolean: Trigger executed when the user clicks Print, Preview, Schedule, or OK. It receives the CloseAction parameter. If validation logic finds invalid inputs (e.g., an illegal date range or missing required parameter), raising an Error() or returning false halts closure and prevents report dataset processing.

Programmatic Invocation and Parameter Passing

Reports can also be executed programmatically in AL without displaying the request page UI:

// Execute report directly using stored filters or default parameters
Report.Run(Report::"Standard Sales - Invoice", false, false, SalesHeaderRec);

// Run request page programmatically to capture parameter XML
var
    ParametersXml: Text;
begin
    ParametersXml := Report.RunRequestPage(Report::"Sales Order Summary");
    Report.Execute(Report::"Sales Order Summary", ParametersXml);
end;
Test Your Knowledge

Why do standard Business Central document reports (such as the Sales Invoice or Purchase Order) nest an Integer DataItem named 'CopyLoop' directly beneath the primary document header DataItem?

A
B
C
D
Test Your Knowledge

A developer creates an AL report with multiple user options on the request page (such as 'Include Details' and 'Posting Period'). The client requests that whenever a user opens the report, the request page must remember and preload the exact options that specific user selected during their previous run. What property should the developer configure?

A
B
C
D
Test Your Knowledge

A developer needs to validate user input on a report request page to ensure that a mandatory 'End Date' option is greater than or equal to 'Start Date'. If the user enters an invalid date range and clicks 'Preview', the report execution must be stopped immediately and the request page must remain open with an error message. Which request page trigger should contain this validation logic?

A
B
C
D
Test Your Knowledge

In standard Business Central document reports, what is the primary architectural purpose of the 'PageLoop' Integer DataItem nested directly beneath 'CopyLoop'?

A
B
C
D