diff --git a/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.bad.al b/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.bad.al index ff560e9..fbff442 100644 --- a/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.bad.al +++ b/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.bad.al @@ -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; } diff --git a/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.good.al b/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.good.al index bb210f2..f19b00a 100644 --- a/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.good.al +++ b/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.good.al @@ -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; } diff --git a/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.md b/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.md index 96a8208..f6b15f9 100644 --- a/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.md +++ b/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.md @@ -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. diff --git a/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.bad.al b/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.bad.al index 49c5d45..a8799c0 100644 --- a/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.bad.al +++ b/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.bad.al @@ -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; } } diff --git a/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.good.al b/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.good.al index 533057c..737e09b 100644 --- a/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.good.al +++ b/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.good.al @@ -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; } } diff --git a/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md b/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md index 1d21211..e85d7e1 100644 --- a/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md +++ b/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md @@ -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. diff --git a/microsoft/skills/review/al-data-modeling-review.md b/microsoft/skills/review/al-data-modeling-review.md index e263500..3ecbd74 100644 --- a/microsoft/skills/review/al-data-modeling-review.md +++ b/microsoft/skills/review/al-data-modeling-review.md @@ -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()`, or the explicit `SalesLine.PlanPriceCalcByField()` followed by `SalesLine.UpdateUnitPriceByField()`. 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`.