Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
report 50102 "Sample Settlement Doc Bad"
{
UsageCategory = ReportsAndAnalysis;
ApplicationArea = All;

dataset
{
dataitem(Customer; Customer)
{
column(No_Customer; "No.") { }
}
}
}

codeunit 50102 "Sample Settlement Document Send"
{
procedure SendSettlementDocument(var Customer: Record Customer)
begin
Customer.TestField("E-Mail");

// WRONG: hardcoded report, no Report Selections row backing it.
// Works for the default case, but there is nowhere for an admin to
// change the report or layout for one specific customer - this
// document never shows up on "Document Layouts" at all, and the
// only way to change it is a code change and a new release.
Report.RunModal(Report::"Sample Settlement Doc Bad", false, false, Customer);
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
codeunit 50102 "Sample Settlement Document Send"
{
procedure SendSettlementDocument(var Customer: Record Customer)
var
ReportSelections: Record "Report Selections";
begin
// Custom validation specific to this document stays here...
CheckReadyToSend(Customer);

// ...but dispatch goes through the registered usage, so per-account
// report/layout overrides and email attachment/body configuration
// on Report Selections all apply automatically.
ReportSelections.SendEmailToCust(
"Report Selection Usage"::"S.Invoice".AsInteger(), Customer, Customer."No.",
Customer.Name, true, Customer."No.");
end;

local procedure CheckReadyToSend(var Customer: Record Customer)
begin
Customer.TestField("E-Mail");
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
bc-version: [all]
domain: data-modeling
keywords: [report-selections, document-layouts, custom-report-layout, email-attachment, bespoke-dispatch]
technologies: [al]
countries: [w1]
application-area: [all]
---

# Custom document dispatch must not bypass Report Selections

## Description

A codeunit that hardcodes which report to run (`Report.RunModal(MyReportId, ...)`)
and builds its own email directly, instead of registering the document
through `table 77 "Report Selections"` and calling its own
Print/Email procedures, works for the one case it was written for — and
loses everything the platform's registry provides for free. `Report
Selections` carries its own attachment/email-body configuration per usage
(`"Use for Email Attachment"`, `"Use for Email Body"`, `"Email Body Layout
Code"`, `"Email Body Layout Type"`, `"Custom Report Layout Code"`), and
`table 9657 "Custom Report Selection"` (the "Document Layouts" page on the
Customer/Vendor card) lets one specific account override the report or
layout without touching code at all. None of that exists for a document
whose dispatch was hand-rolled: there is no registry row to point
"Document Layouts" at, so an admin who goes looking for where to change
this document's layout — the same place they'd look for every other
document in the system — finds nothing, because the document was never
registered there.

## Best Practice

Register the document under a `Report Selection Usage` value (see
`extend-report-selection-usage-for-new-document-types.md`) and dispatch
through `Report Selections`' own Print/Email procedures (see
`document-print-and-email-actions-call-report-selections-directly.md`),
even when the surrounding business logic — which counterparty to use,
what validation must pass before sending — is genuinely specific to the
document. Custom logic belongs around the call to `Report Selections`,
not instead of it.

See sample: `custom-document-dispatch-must-not-bypass-report-selections.good.al`.

## Anti Pattern

A codeunit that runs a hardcoded report ID and builds its own email
message directly, with no `Report Selections` row backing it. It works for
the default case, but the report/layout cannot be changed per account
without a code change and a new release, and the document is invisible to
"Document Layouts" — the standard place every other document's
distribution is configured.

See sample: `custom-document-dispatch-must-not-bypass-report-selections.bad.al`.

## Source

BCApps `ReportSelections.Table.al` (table 77 — fields 19–26 for email
attachment/body configuration; `SendEmailToCust`/`PrintWithDialogForCust`
as the registry-backed dispatch entry points) and
`CustomReportSelection.Table.al` (table 9657, the per-account override
backing the "Document Layouts" page) — both under
`src/Layers/W1/BaseApp/Foundation/Reporting/`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
page 50101 "Sample Settlement Document Card"
{
PageType = Card;
SourceTable = Customer;
ApplicationArea = All;

actions
{
area(Processing)
{
action(EmailDocument)
{
ApplicationArea = All;
Caption = 'Email';
Image = Email;

trigger OnAction()
var
DocumentSendingProfile: Record "Document Sending Profile";
begin
// WRONG: this is a plain, on-demand "Email" button, not
// part of a combined Post-and-Send action - but routing
// it through Document Sending Profile means the outcome
// now silently depends on this customer's assigned
// profile. If that profile's "E-Mail" option is No, the
// user sees nothing happen after clicking Email, with no
// indication that an unrelated setup field is why.
DocumentSendingProfile.Send(
"Report Selection Usage"::"S.Invoice".AsInteger(), Rec, Rec."No.", Rec."No.",
Rec.Name, Rec.FieldNo("No."), Rec.FieldNo("No."));
end;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
page 50101 "Sample Settlement Document Card"
{
PageType = Card;
SourceTable = Customer;
ApplicationArea = All;

actions
{
area(Processing)
{
action(EmailDocument)
{
ApplicationArea = All;
Caption = 'Email';
Image = Email;

trigger OnAction()
var
ReportSelections: Record "Report Selections";
begin
// Calls Report Selections directly - the button's outcome
// depends only on this customer's registered report/layout,
// not on any Document Sending Profile setting.
ReportSelections.SendEmailToCust(
"Report Selection Usage"::"S.Invoice".AsInteger(), Rec, Rec."No.",
Rec.Name, true, Rec."No.");
end;
}
action(PrintDocument)
{
ApplicationArea = All;
Caption = 'Print';
Image = Print;

trigger OnAction()
var
ReportSelections: Record "Report Selections";
begin
ReportSelections.PrintWithDialogForCust(
"Report Selection Usage"::"S.Invoice", Rec, true, Rec.FieldNo("No."));
end;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
bc-version: [all]
domain: data-modeling
keywords: [report-selections, document-sending-profile, print, email, post-and-send]
technologies: [al]
countries: [w1]
application-area: [all]
---

# A document's own Print/Email actions call Report Selections directly; Document Sending Profile is scoped to Post-and-Send

## Description

`table 60 "Document Sending Profile"` is not a general gateway that every
print/email path should route through — it exists specifically for the
combined **Post and Send** action: "You can set each customer up with a
preferred method of sending sales documents, so that you do not have to
select a sending option every time you choose the Post and Send action"
(Microsoft Learn, "Set Up Document Sending Profiles"). A document's own, ordinary
Print/Email ribbon actions call `table 77 "Report Selections"` directly
and are not affected by any Document Sending Profile at all. This is the
pattern BC's own base application uses for a document's plain print/email
actions: the Sales Order's "Print Confirmation"/"Email Confirmation"
actions (`codeunit "Document-Print"`, `PrintSalesOrder`/`EmailSalesHeader`)
call `ReportSelections.PrintWithDialogForCust`/`SendEmailToCust` directly,
and the posted `Purch. Inv. Header`'s own `PrintRecords` does the same
through `ReportSelection.PrintWithDialogForVend` — no customer's or
vendor's actually assigned Document Sending Profile is consulted by
either.

The unposted `Purchase Header`'s own `PrintRecords` is a partial exception
worth naming precisely: it calls `DocumentSendingProfile.TrySendToPrinterVendor(...)`,
but only as a stateless, never-`Get`'d local record carrying print-dialog
options, never a vendor's actually configured profile — that helper still
resolves the report through `ReportSelections.PrintWithDialogForVend(...)`,
the same as everywhere else.

Only the combined Post-and-Send flow resolves through Document Sending
Profile: `Sales-Post and Send` calls `Sales Invoice Header.SendProfile`,
which calls `DocumentSendingProfile.Send(...)`, which then decides
Print/Email/Disk/Electronic based on the customer's assigned profile and
only *then* calls back into `Report Selections` (for the PDF cases) or
`Electronic Document Format` (for machine-readable cases).

Whether a document needs outbound distribution at all isn't determined by
Customer-vs-Vendor, but by whether the document is genuinely *outbound* to
its counterparty. A posted Purchase Invoice records what a vendor already
billed you — nothing to send back — and its posted `Purch. Inv. Header`
exposes only a bare `PrintRecords`, no `SendProfile`/`SendRecords`/email at
all. A Purchase *Order* is genuinely outbound before posting, which is why
the full `SendProfile`/`SendRecords`/`PrintRecords` triplet lives on the
unposted `Purchase Header` instead.

## Best Practice

For a document's own interactive Print/Email actions, call the relevant
`Report Selections` procedure directly —
`PrintForCust`/`PrintWithDialogForCust`/`SendEmailToCust` for a
customer-facing document, `PrintWithDialogForVend`/`SendEmailToVendor` for
a vendor-facing one — using the usage value registered per
`extend-report-selection-usage-for-new-document-types.md`. Wire into
`Document Sending Profile` only when specifically building a combined
Post-and-Send action for that document. Before adding any send capability
at all, confirm the document is genuinely outbound to the counterparty
it's attached to; a document that only records something already received
needs print-for-reference at most, not a send path.

See sample: `document-print-and-email-actions-call-report-selections-directly.good.al`.

## Anti Pattern

Routing a document's plain, on-demand "Email" button through
`DocumentSendingProfile.Send`/`SendVendor` instead of calling
`ReportSelections.SendEmailToCust`/`SendEmailToVendor` directly. The
button's outcome now silently depends on that customer's or vendor's
assigned Document Sending Profile — if its `"E-Mail"` option happens to be
`No`, clicking "Email" does nothing observable, with no indication to the
user that a profile setting (meant for the Post-and-Send flow) is the
reason. A second version of the same mistake: adding an email action to a
document that only receives from its counterparty and was never meant to
send anything back.

See sample: `document-print-and-email-actions-call-report-selections-directly.bad.al`.

## Source

BCApps `DocumentPrint.Codeunit.al` (`EmailSalesHeader`/`DoPrintSalesHeader`/`PrintSalesOrder`,
calling `ReportSelections.SendEmailToCust`/`PrintForCust`/`PrintWithDialogForCust`
directly), `PurchaseHeader.Table.al` (`SendProfile` at line ~6387, calling
`DocumentSendingProfile.SendVendor`), `PurchInvHeader.Table.al` (`PrintRecords`
calling `ReportSelection.PrintWithDialogForVend` directly, no send capability),
`SalesPost.Codeunit.al`
(`SendPostedDocumentRecord` at line 7660 → `SalesInvHeader.SendProfile` at
lines 7680/7699 → `DocumentSendingProfile.Send`),
`DocumentSendingProfile.Table.al` (table 60; `TrySendToPrinterVendor` at
line 552 and `SendToPrinterVendor` at line 716, called from
`PurchaseHeader.PrintRecords` at line 6357) — all under
`src/Layers/W1/BaseApp/`. Microsoft Learn, "Set Up Document Sending Profiles":
https://learn.microsoft.com/dynamics365/business-central/sales-how-setup-document-send-profiles
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
codeunit 50103 "Sample Navigate Subscribers"
{
// WRONG: registers the row, so it appears in the Find Entries result
// list with a correct table name and record count - but there is no
// OnBeforeShowRecords subscriber for this table. ShowRecords()'s own
// case statement has no branch and no else for it either, so
// selecting this row and choosing "Show records" does nothing,
// silently, with no error.
[EventSubscriber(ObjectType::Page, Page::Navigate, 'OnAfterFindRecords', '', false, false)]
local procedure OnAfterFindRecords(var DocumentEntry: Record "Document Entry"; DocNoFilter: Text; PostingDateFilter: Text)
var
SampleDocHeader: Record "Sample Posted Document Header";
begin
SampleDocHeader.SetFilter("No.", DocNoFilter);
SampleDocHeader.SetFilter("Posting Date", PostingDateFilter);
DocumentEntry.InsertIntoDocEntry(
Database::"Sample Posted Document Header", SampleDocHeader.TableCaption(), SampleDocHeader.Count());
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
codeunit 50103 "Sample Navigate Subscribers"
{
[EventSubscriber(ObjectType::Page, Page::Navigate, 'OnAfterFindRecords', '', false, false)]
local procedure OnAfterFindRecords(var DocumentEntry: Record "Document Entry"; DocNoFilter: Text; PostingDateFilter: Text)
var
SampleDocHeader: Record "Sample Posted Document Header";
begin
SampleDocHeader.SetFilter("No.", DocNoFilter);
SampleDocHeader.SetFilter("Posting Date", PostingDateFilter);
DocumentEntry.InsertIntoDocEntry(
Database::"Sample Posted Document Header", SampleDocHeader.TableCaption(), SampleDocHeader.Count());
end;

// Without this second subscriber, the row added above shows up in the
// Find Entries result list with a correct count, but "Show records"
// has nothing to open it with - see the .bad.al sample.
[EventSubscriber(ObjectType::Page, Page::Navigate, 'OnBeforeShowRecords', '', false, false)]
local procedure OnBeforeShowRecords(var TempDocumentEntry: Record "Document Entry" temporary; DocNoFilter: Text; PostingDateFilter: Text; ItemTrackingSearch: Boolean; ContactNo: Code[250]; ExtDocNo: Code[250]; var IsHandled: Boolean)
var
SampleDocHeader: Record "Sample Posted Document Header";
begin
if TempDocumentEntry."Table ID" <> Database::"Sample Posted Document Header" then
exit;

SampleDocHeader.SetFilter("No.", DocNoFilter);
SampleDocHeader.SetFilter("Posting Date", PostingDateFilter);
if TempDocumentEntry."No. of Records" = 1 then begin
SampleDocHeader.FindFirst();
Page.Run(Page::"Sample Posted Document", SampleDocHeader);
end else
Page.Run(0, SampleDocHeader);

IsHandled := true;
end;
}
Loading