Fix three merge-critical items from Jesper's 2026-09-29 review

- Barcode: drop the false claim that '*value*' is mismatched with the
  IDAutomation Code 39 font; '*' is a documented start/stop form and
  '(' / ')' an accepted alternative. Cue and article now route only
  independently provable validation/checksum/font-binding defects.
- Dispatch good samples (and matching bad samples) now pass a
  Sales Invoice Header with the S.Invoice usage, matching the record
  the selected report (1306 "Standard Sales - Invoice") expects.
- custom-document-dispatch rule made disjunctive: a hardcoded report
  or a hand-built email is each a bypass on its own; scoped to
  customer/vendor-facing documents. Bad fixture shows the hardcoded
  report alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Michael Dieringer 2026-09-29 16:45:02 +02:00
parent 4cd41f08f7
commit aad3991d9f
7 changed files with 98 additions and 85 deletions

View file

@ -1,28 +1,20 @@
report 50102 "Sample Settlement Doc Bad"
codeunit 50102 "Sample Posted Invoice Send"
{
UsageCategory = ReportsAndAnalysis;
ApplicationArea = All;
dataset
{
dataitem(Customer; Customer)
{
column(No_Customer; "No.") { }
}
}
}
codeunit 50102 "Sample Settlement Document Send"
{
procedure SendSettlementDocument(var Customer: Record Customer)
procedure SendPostedInvoice(SalesInvoiceHeader: Record "Sales Invoice Header")
var
Customer: Record Customer;
begin
Customer.Get(SalesInvoiceHeader."Bill-to Customer No.");
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);
// WRONG: the report is hardcoded instead of resolved through the
// registered "S.Invoice" usage in Report Selections. This alone is
// the defect - no hand-built email is needed for it: a Report
// Selections row or a per-customer "Document Layouts" override
// that points this usage at a different report or layout is
// silently ignored, and the only way to change what this code
// prints is a code change and a new release.
SalesInvoiceHeader.SetRecFilter();
Report.RunModal(Report::"Standard Sales - Invoice", false, false, SalesInvoiceHeader);
end;
}

View file

@ -1,22 +1,31 @@
codeunit 50102 "Sample Settlement Document Send"
codeunit 50102 "Sample Posted Invoice Send"
{
procedure SendSettlementDocument(var Customer: Record Customer)
procedure SendPostedInvoice(SalesInvoiceHeader: Record "Sales Invoice Header")
var
ReportSelections: Record "Report Selections";
ReportDistributionMgt: Codeunit "Report Distribution Management";
begin
// Custom validation specific to this document stays here...
CheckReadyToSend(Customer);
// Custom validation specific to this dispatch stays here...
CheckReadyToSend(SalesInvoiceHeader);
// ...but dispatch goes through the registered usage, so per-account
// ...but dispatch goes through the registered usage. "S.Invoice"
// resolves to a report built on "Sales Invoice Header" (by default
// report 1306 "Standard Sales - Invoice"), so the record passed in
// matches what the selected report expects, and per-account
// report/layout overrides and email attachment/body configuration
// on Report Selections all apply automatically.
SalesInvoiceHeader.SetRecFilter();
ReportSelections.SendEmailToCust(
"Report Selection Usage"::"S.Invoice".AsInteger(), Customer, Customer."No.",
Customer.Name, true, Customer."No.");
"Report Selection Usage"::"S.Invoice".AsInteger(), SalesInvoiceHeader, SalesInvoiceHeader."No.",
ReportDistributionMgt.GetFullDocumentTypeText(SalesInvoiceHeader), true,
SalesInvoiceHeader."Bill-to Customer No.");
end;
local procedure CheckReadyToSend(var Customer: Record Customer)
local procedure CheckReadyToSend(SalesInvoiceHeader: Record "Sales Invoice Header")
var
Customer: Record Customer;
begin
Customer.Get(SalesInvoiceHeader."Bill-to Customer No.");
Customer.TestField("E-Mail");
end;
}

View file

@ -11,11 +11,15 @@ application-area: [all]
## Description
A codeunit that hardcodes which report to run (`Report.RunModal(MyReportId, ...)`)
and builds its own email directly, instead of registering the document
A codeunit that hardcodes which report to run (`Report.RunModal(MyReportId, ...)`),
or 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
loses everything the platform's registry provides for free. Either
bypass is a defect on its own: a hardcoded report ignores the registered
report and any per-account layout override even when no email is
involved, and a hand-built email ignores the registry's attachment and
email-body configuration even when the report itself came from it. `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"`), plus a separate per-usage layout
@ -44,9 +48,10 @@ 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
A codeunit that runs a hardcoded report ID, or builds its own email
message directly, for a document that has (or should have) a
`Report Selections` usage — each is independently a bypass, and the
sample shows the first on its own. 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.

View file

@ -1,8 +1,9 @@
page 50101 "Sample Settlement Document Card"
page 50101 "Sample Posted Invoice Card"
{
PageType = Card;
SourceTable = Customer;
SourceTable = "Sales Invoice Header";
ApplicationArea = All;
Editable = false;
actions
{
@ -16,7 +17,9 @@ page 50101 "Sample Settlement Document Card"
trigger OnAction()
var
SalesInvoiceHeader: Record "Sales Invoice Header";
DocumentSendingProfile: Record "Document Sending Profile";
ReportDistributionMgt: Codeunit "Report Distribution Management";
begin
// WRONG: this is a plain, on-demand "Email" button, not
// part of a combined Post-and-Send action - but this
@ -28,10 +31,13 @@ page 50101 "Sample Settlement Document Card"
// Printer = Yes, "E-Mail" = No) turns this button into a
// silent no-op, with no indication an unrelated setup
// field is why.
DocumentSendingProfile.GetDefaultForCustomer(Rec."No.", DocumentSendingProfile);
SalesInvoiceHeader := Rec;
CurrPage.SetSelectionFilter(SalesInvoiceHeader);
DocumentSendingProfile.GetDefaultForCustomer(Rec."Bill-to Customer No.", DocumentSendingProfile);
DocumentSendingProfile.Send(
"Report Selection Usage"::"S.Invoice".AsInteger(), Rec, Rec."No.", Rec."No.",
Rec.Name, Rec.FieldNo("No."), Rec.FieldNo("No."));
"Report Selection Usage"::"S.Invoice".AsInteger(), SalesInvoiceHeader, Rec."No.",
Rec."Bill-to Customer No.", ReportDistributionMgt.GetFullDocumentTypeText(Rec),
SalesInvoiceHeader.FieldNo("Bill-to Customer No."), SalesInvoiceHeader.FieldNo("No."));
end;
}
}

View file

@ -1,8 +1,9 @@
page 50101 "Sample Settlement Document Card"
page 50101 "Sample Posted Invoice Card"
{
PageType = Card;
SourceTable = Customer;
SourceTable = "Sales Invoice Header";
ApplicationArea = All;
Editable = false;
actions
{
@ -16,7 +17,9 @@ page 50101 "Sample Settlement Document Card"
trigger OnAction()
var
SalesInvoiceHeader: Record "Sales Invoice Header";
ReportSelections: Record "Report Selections";
ReportDistributionMgt: Codeunit "Report Distribution Management";
begin
// Calls Report Selections directly - the button's outcome
// depends only on this customer's registered report/layout,
@ -24,9 +27,13 @@ page 50101 "Sample Settlement Document Card"
// DocumentSendingProfile.TrySendToEMail(...) instead would
// be equally correct: it never Get's the customer's
// actually assigned profile, only a local, hardcoded one.
// "S.Invoice" resolves to a report on "Sales Invoice
// Header", which is the record passed here.
SalesInvoiceHeader := Rec;
CurrPage.SetSelectionFilter(SalesInvoiceHeader);
ReportSelections.SendEmailToCust(
"Report Selection Usage"::"S.Invoice".AsInteger(), Rec, Rec."No.",
Rec.Name, true, Rec."No.");
"Report Selection Usage"::"S.Invoice".AsInteger(), SalesInvoiceHeader, Rec."No.",
ReportDistributionMgt.GetFullDocumentTypeText(Rec), true, Rec."Bill-to Customer No.");
end;
}
action(PrintDocument)
@ -37,10 +44,14 @@ page 50101 "Sample Settlement Document Card"
trigger OnAction()
var
SalesInvoiceHeader: Record "Sales Invoice Header";
ReportSelections: Record "Report Selections";
begin
SalesInvoiceHeader := Rec;
CurrPage.SetSelectionFilter(SalesInvoiceHeader);
ReportSelections.PrintWithDialogForCust(
"Report Selection Usage"::"S.Invoice", Rec, true, Rec.FieldNo("No."));
"Report Selection Usage"::"S.Invoice", SalesInvoiceHeader, true,
SalesInvoiceHeader.FieldNo("Bill-to Customer No."));
end;
}
}

View file

@ -56,34 +56,26 @@ See sample: [`report-barcodes-must-use-barcode-module-and-production-font-name.g
## Anti Pattern
Constructing a barcode string by hand instead of using the module's
provider/encoder API — not because a manual delimiter is inherently
wrong (Code 39's own symbology does use `*` as start/stop; Microsoft
Learn's font table says so), but because hand-rolled construction is
demonstrably mismatched with what the real encoder produces: it skips
`ValidateInput` (so a value outside the character set, or needing a
checksum/extended-charset setting never applied, reaches the font
unvalidated), and IDAutomation 1D Provider's own Code 39 output is
wrapped in `(`/`)`, not literal `*` (BCApps test:
`EncodeFont('1234', Code39) = '(1234)'`) — the paired font maps those
parentheses to the real start/stop glyph, so `'*' + value + '*'` is
simply the wrong characters, plus no checksum.
Constructing a barcode string by hand where that construction has a
concrete, independently provable defect: a source value that can contain
characters outside the symbology's character set is never validated, a
checksum the symbology or setup requires is never applied, or there is
concrete evidence of an incompatible font binding.
Flag demonstrably invalid or mismatched hand construction, not manual
delimiter use as a category — a custom provider paired with a font that
genuinely expects literal `*` delimiters is a different, legitimate case.
The delimiter itself is not the defect. `*value*` is a documented, valid
Code 39 form for IDAutomation fonts (Microsoft Learn's font table and
IDAutomation's own manual both give `*` as start/stop); the `(`/`)` that
IDAutomation 1D Provider's encoder emits (BCApps test:
`EncodeFont('1234', Code39) = '(1234)'`) is an alternative start/stop
form the same fonts accept, used to keep `*` out of the human-readable
text. Never flag delimiter choice alone.
A second version of the same mistake: encoding correctly, but naming the
evaluation font instead of the purchased one. Both look complete in
review and fail silently — the first because the data was never a real
barcode, the second because BC online refuses to render it.
The same gap exists even when the module *is* used: a 1D path that calls
`EncodeFont` on `"Barcode Font Provider"` without `ValidateInput`.
IDAutomation 1D Provider's `EncodeFont` does not validate on its own, so
a value outside the symbology's character set is never rejected — it
reaches the font as an unscannable barcode. The sample shows this
variant, because it is visible in AL alone without layout evidence.
The same validation gap exists when the module *is* used: a 1D path that
calls `EncodeFont` on `"Barcode Font Provider"` without `ValidateInput`
(IDAutomation 1D Provider's `EncodeFont` does not validate on its own).
The sample shows this variant, visible in AL alone. A last version:
encoding correctly but naming the evaluation font, which BC online
refuses to render.
See sample: [`report-barcodes-must-use-barcode-module-and-production-font-name.bad.al`](report-barcodes-must-use-barcode-module-and-production-font-name.bad.al).
@ -93,17 +85,15 @@ BCApps (`src/System Application/App/Barcode/src/`):
`Barcode Provider/Font/BarcodeFontProvider.Interface.al` (1D:
`ValidateInput` + `EncodeFont`); `IDAutomation 1D Provider/
IDAutomation1DProvider.Codeunit.al` (`EncodeFont` goes straight to the
symbology encoder; only `ValidateInput` calls `IsValidInput`); `Barcode Provider 2D/Font/
BarcodeFontProvider2D.Interface.al` (2D: only `EncodeFont`); both read
fresh from source. `IDAutomation 1D Provider/Encoders/
IDA1DCode39Encoder.Codeunit.al` (`codeunit 9204`, regex accepts literal
`*` as plain input; `EncodeFont` → `DotNet FontEncoder.Code39`). Split
and delimiter mismatch both confirmed live: `.../Inventory/Item/
ItemGTINLabel.Report.al` (`report 6625`, validates+encodes 1D, only
encodes 2D, same value) and `.../Test/Barcode/.../IDA1DCode39Test.
Codeunit.al` (`codeunit 135044`): `EncodeFontSuccessTest('1234', Code39,
'(1234)')` — wrapped in `(`/`)`, never literal `*`.
symbology encoder; only `ValidateInput` calls `IsValidInput`); `Barcode Provider 2D/Font/BarcodeFontProvider2D.Interface.al`
(2D: only `EncodeFont`). `IDAutomation 1D Provider/Encoders/IDA1DCode39Encoder.Codeunit.al`
(`codeunit 9204`, regex accepts literal `*`; `EncodeFont` → `DotNet FontEncoder.Code39`).
1D/2D split: `.../Inventory/Item/ItemGTINLabel.Report.al` (`report 6625`,
validates+encodes 1D, only encodes 2D). Encoder output form: `IDA1DCode39Test.Codeunit.al`
(`codeunit 135044`): `EncodeFontSuccessTest('1234', Code39, '(1234)')`.
Microsoft Learn "Adding Barcodes to Reports" and "Barcode Fonts with
Business Central Online" — quoted above, incl. the Code39 row ("`*` is
used for both start and stop delimiters").
used for both start and stop delimiters"). IDAutomation, "Code 39 Font
User Manual" (https://idautomation.com/barcode-fonts/code-39/fontnames/):
`*` start/stop, or parentheses to keep `*` out of the human-readable text.

View file

@ -54,7 +54,7 @@ The following targeted checks cover every current `data-modeling` article. Treat
- A `Media` or `MediaSet` field is assigned directly between different table types or different field IDs instead of registering each shared item with `MediaSet.Insert` — `share-mediaset-items-with-insert-not-field-assignment`.
- A custom document header assigns defaults outside an `InitRecord` boundary, calls `InitRecord` before assigning its number, or places UI-independent defaults only in a page trigger — `initialize-document-defaults-in-initrecord`.
- Directed `Round` calls use `'<'` as mathematical floor or `'>'` as mathematical ceiling, especially where negative amounts are possible — `round-direction-symbols-use-magnitude`.
- A codeunit dispatches a document by calling `Report.Run`/`Report.RunModal` with a hardcoded report ID and building its own email directly, with no accompanying `Report Selections` registration for that document — `custom-document-dispatch-must-not-bypass-report-selections`. A call that already goes through `Report Selections`' own Print/Email procedures is not this anti-pattern.
- A codeunit dispatches a document by calling `Report.Run`/`Report.RunModal` with a hardcoded report ID, or by building its own email directly, instead of going through the `Report Selections` usage for that document — `custom-document-dispatch-must-not-bypass-report-selections`. Either bypass is a finding on its own; both need not be present. Scope this to customer/vendor-facing documents that have (or should have) a `Report Selection Usage` — a hardcoded `Report.Run` of an ordinary list/analysis report is not this anti-pattern. A call that already goes through `Report Selections`' own Print/Email procedures is not this anti-pattern.
- A document's own interactive Print/Email action routes through `Document Sending Profile` (`DocumentSendingProfile.Send`/`SendVendor`) instead of calling `Report Selections` (`PrintForCust`/`PrintWithDialogForCust`/`SendEmailToCust`/`PrintWithDialogForVend`/`SendEmailToVendor`) directly — `document-print-and-email-actions-call-report-selections-directly`. Do not flag `Document Sending Profile` usage that is genuinely part of a combined Post-and-Send action.
- An `EventSubscriber` is added for `Navigate::OnAfterFindRecords` (registering a custom table in Find Entries) without a matching `Navigate::OnBeforeShowRecords` subscriber for the same table, or vice versa — `extend-find-entries-navigate-for-new-document-types`. Both subscribers must be added together for the same table.
- An `enumextension` extends `"Report Selection Usage"` and registers a report via `ReportSelections.InsertRecord`, without subscribing to the matching *single* counterparty's full triad — the filter event (`OnAfterFilterCustomerUsageReportSelections` on `page 9657` for a sales usage, `OnAfterFilterVendorUsageReportSelections` on `page 9658` for a purchase usage) AND the page-facing usage-enum map/validate events (`enumextension` on `"Custom Report Selection Sales"`/`"Report Selection Usage Vendor"` plus the matching map/validate subscribers) — `extend-report-selection-usage-for-new-document-types`. Requiring or wiring *both* counterparties by default for a one-sided document is also the anti-pattern (`ReportSelectionHandlerCZZ` partitions strictly by counterparty); only a genuinely two-sided usage (as `ReportSelectionHandlerCZC` demonstrates for Compensation) needs both.
@ -62,7 +62,7 @@ The following targeted checks cover every current `data-modeling` article. Treat
- An `enumextension` extends `"Price Calculation Handler"` and implements the `Price Calculation` interface, without a matching `OnFindSupportedSetup` subscriber inserting a `Price Calculation Setup` record naming that implementation as the `Implementation` for a `Method`/`Type`/`Asset Type` — `activate-new-price-calculation-handler-via-onfindsupportedsetup`. `Default := true` is only required on that row when it is meant as the fallback for its `Method`/`Type`/`Asset Type` combination; a row meant to be selected only through an explicit, specific `"Dtld. Price Calculation Setup"` row does not need it, so do not flag a missing `Default := true` by itself — flag the missing setup row/subscriber entirely.
- An `enumextension` extends `"Price Source Type"` with a new value intended for a sales, purchase, or job price list, without extending the matching document subset enum (`"Sales Price Source Type"`, `"Purchase Price Source Type"`, `"Job Price Source Type"`) with a value at the same numeric ID — `extend-price-source-type-must-sync-document-subset-enum`.
- A codeunit subscribes to `"Sales Line - Price"`'s `OnAfterAddSources` to register a custom field as a price source via `PriceSourceList.Add`, but that field has no `OnValidate` (or matching `OnAfterValidate`) that triggers recalculation — either `SalesLine.UpdateUnitPrice(<field no.>)`, or the explicit `SalesLine.PlanPriceCalcByField(<field no.>)` followed by `SalesLine.UpdateUnitPriceByField(<same field no.>)`. A bare `UpdateUnitPriceByField` without a preceding `PlanPriceCalcByField` for the same field number does not count as recalculation (it exits without recalculating) — `new-price-source-must-add-candidate-and-trigger-recalculation`.
- A report hand-constructs a barcode string that is demonstrably invalid or mismatched with the font provider it is rendered with (e.g. `'*' + Value + '*'` rendered with the IDAutomation Code 39 font, whose paired IDAutomation 1D provider encoder emits `(`/`)` wrapping, not literal `*` — with no validation or checksum applied) instead of encoding through the Barcode module. Do not flag manual delimiter use as a category — a custom provider paired with a font that genuinely expects those literal delimiters is not a finding. Also flag module use that does not match the interface: a 1D `"Barcode Font Provider"` path must call both `ValidateInput` and `EncodeFont`; a 2D `"Barcode Font Provider 2D"` path calls `EncodeFont` only (the 2D interface has no `ValidateInput`, so its absence there is not a finding). Separately, flag an otherwise correctly encoded barcode whose report layout names an evaluation/demo font instead of the purchased production font name — `report-barcodes-must-use-barcode-module-and-production-font-name`.
- A report hand-constructs a barcode string only where a concrete, independently provable defect is visible: the source value can contain characters outside the symbology's character set and is never validated, a checksum the symbology/setup requires is never applied, or there is concrete evidence of an incompatible font binding. Do not flag manual start/stop delimiters by themselves — `*value*` is a documented, valid Code 39 form for IDAutomation fonts (IDAutomation also accepts parentheses), so delimiter choice alone is never a finding. Also flag module use that does not match the interface: a 1D `"Barcode Font Provider"` path must call both `ValidateInput` and `EncodeFont`; a 2D `"Barcode Font Provider 2D"` path calls `EncodeFont` only (the 2D interface has no `ValidateInput`, so its absence there is not a finding). Separately, flag an otherwise correctly encoded barcode whose report layout names an evaluation/demo font instead of the purchased production font name — `report-barcodes-must-use-barcode-module-and-production-font-name`.
Once the candidate worklist is known, resolve layer-precedence conflicts per READ. Drop lower-precedence files whose normative guidance (`## Best Practice` or `## Anti Pattern`) directly contradicts a higher-precedence candidate, and record each dropped file in `suppressed` with `reason: "layer-precedence"`. Files that would have been candidates but are hidden because their layer is disabled in consumer configuration are recorded with `reason: "configuration"`. Files that never became candidates are NOT recorded in `suppressed`.