mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
Add 5 AL/BC patterns: document distribution (Report Selections, Document Sending Profile, Find Entries, TransferFields)
Five rules about Business Central's document distribution architecture, verified against BCApps source and Microsoft Learn. - custom-document-dispatch-must-not-bypass-report-selections - document-print-and-email-actions-call-report-selections-directly - extend-find-entries-navigate-for-new-document-types - extend-report-selection-usage-for-new-document-types - transferfields-mirrored-fields-must-match-type-and-length Wired into al-data-modeling-review.md's worklist cues. Added a disambiguation note on the TransferFields article distinguishing it from the existing transferfields-skip-type-mismatch-can-drop-data.md (type-mismatch skipping vs. length mismatch, which SkipFieldsNotMatchingType does not affect). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
8584217c75
commit
83f041b662
16 changed files with 761 additions and 0 deletions
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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/`.
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: data-modeling
|
||||||
|
keywords: [navigate, find-entries, document-entry, integration-event, drill-down]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Extend Find Entries (Navigate) for new document or transaction tables
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
`page 344 Navigate` (caption "Find entries") lets a user enter a document
|
||||||
|
number and posting date and see, across every document and ledger entry
|
||||||
|
table BC knows about, how many matching records exist — then drill into
|
||||||
|
any of those rows. It works over a temporary `table "Document Entry"`
|
||||||
|
that gets populated, one row per source table, by dozens of separate
|
||||||
|
lookups hardcoded into the page (`Rec.InsertIntoDocEntry(Database::"Sales
|
||||||
|
Invoice Header", ...)` and similar, one per table). A new custom document
|
||||||
|
or transaction table is invisible to Find Entries by default — nobody
|
||||||
|
searching by document number will ever see it in the result list — until
|
||||||
|
it registers itself.
|
||||||
|
|
||||||
|
Registration is a two-sided integration event, and only implementing one
|
||||||
|
side produces a page that is worse than not participating at all. The
|
||||||
|
`OnAfterFindRecords` event lets a subscriber add a row to the result list
|
||||||
|
for a custom table. But the subsequent "show records" action, `procedure
|
||||||
|
ShowRecords`, resolves which page to open through its own hardcoded `case
|
||||||
|
Rec."Table ID" of` — the same shape as the row-population code, and just
|
||||||
|
as unaware of any table added by an extension. That `case` statement has
|
||||||
|
no `else` branch. A custom table's row can appear in the result list,
|
||||||
|
with a correct count, and be entirely un-clickable: the user selects it,
|
||||||
|
chooses "Show records", and nothing happens, silently.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
|
||||||
|
Subscribe to both `Navigate::OnAfterFindRecords` and
|
||||||
|
`Navigate::OnBeforeShowRecords` together, as one unit of work, for any
|
||||||
|
custom table that should be searchable by document number:
|
||||||
|
|
||||||
|
- In `OnAfterFindRecords`, filter the custom table by the given
|
||||||
|
`DocNoFilter`/`PostingDateFilter` and call
|
||||||
|
`DocumentEntry.InsertIntoDocEntry(Database::"My Table", TableCaption,
|
||||||
|
Count)` to add it to the result list.
|
||||||
|
- In `OnBeforeShowRecords`, check whether
|
||||||
|
`TempDocumentEntry."Table ID" = Database::"My Table"`; if so, re-apply
|
||||||
|
the same filters, open the appropriate card or list page, and set
|
||||||
|
`IsHandled := true` so the page's own unrelated `case` statement is
|
||||||
|
never reached for this table.
|
||||||
|
- If `OnAfterFindRecords` filters the custom table by a field that is not
|
||||||
|
already that table's own unique key — for example an external
|
||||||
|
reference number received from a counterparty, rather than the
|
||||||
|
table's own `No.` — add a key combining that field with `Posting Date`,
|
||||||
|
the same way BCApps does for `Purch. Inv. Header`'s `"Vendor Invoice
|
||||||
|
No."` (see Source). This does not apply when filtering the table's own
|
||||||
|
primary key, which is already unique on its own: `Sales Invoice
|
||||||
|
Header` filters `"No."` and `"Posting Date"` through two separate,
|
||||||
|
uncombined keys, with no compound key between them, because `"No."`
|
||||||
|
alone is already sufficient.
|
||||||
|
|
||||||
|
See sample: `extend-find-entries-navigate-for-new-document-types.good.al`.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
|
||||||
|
Subscribing only to `OnAfterFindRecords` (or only to
|
||||||
|
`OnBeforeShowRecords`). Registering the row without handling its
|
||||||
|
drill-down produces a search result that looks complete — the table name
|
||||||
|
and a correct record count both show up — but leads nowhere when
|
||||||
|
selected, with no error and no indication to the user that anything is
|
||||||
|
wrong.
|
||||||
|
|
||||||
|
See sample: `extend-find-entries-navigate-for-new-document-types.bad.al`.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
BCApps `Navigate.Page.al` (page 344, `src/Layers/W1/BaseApp/Foundation/Navigate/`):
|
||||||
|
- `[IntegrationEvent(true, false)] local procedure OnAfterFindRecords(var DocumentEntry: Record "Document Entry"; DocNoFilter: Text; PostingDateFilter: Text)`
|
||||||
|
- `[IntegrationEvent(true, 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)`
|
||||||
|
- `procedure ShowRecords()`'s `case Rec."Table ID" of ... end;` has no `else` branch — confirmed by reading the full case block, which ends directly with `end;` followed by `OnAfterShowRecords(...)`.
|
||||||
|
|
||||||
|
BCApps `DocumentEntry.Table.al` (table backing page 344):
|
||||||
|
`procedure InsertIntoDocEntry(DocTableID: Integer; DocTableName: Text; DocNoOfRecords: Integer)` — the registration entry point called from `OnAfterFindRecords` subscribers.
|
||||||
|
|
||||||
|
BCApps `SalesInvoiceHeader.Table.al` (`src/Layers/W1/BaseApp/Sales/History/`):
|
||||||
|
`key(Key1; "No.")` (`Clustered = true`) and `key(Key9; "Posting Date")` are
|
||||||
|
two separate, uncombined keys — no compound key exists between them.
|
||||||
|
|
||||||
|
BCApps `PurchInvHeader.Table.al` (`src/Layers/W1/BaseApp/Purchases/History/`):
|
||||||
|
`key(Key4; "Vendor Invoice No.", "Posting Date")` — a compound key
|
||||||
|
combining a non-unique, externally-supplied reference number with
|
||||||
|
`Posting Date`, distinct from `key(Key1; "No.")`, its own unique primary
|
||||||
|
key.
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
enumextension 50100 "Sample Report Selection Usage Ext" extends "Report Selection Usage"
|
||||||
|
{
|
||||||
|
value(50100; "Sample.SettlementDoc")
|
||||||
|
{
|
||||||
|
Caption = 'Sample Settlement Document';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report 50100 "Sample Settlement Document"
|
||||||
|
{
|
||||||
|
UsageCategory = ReportsAndAnalysis;
|
||||||
|
ApplicationArea = All;
|
||||||
|
|
||||||
|
dataset
|
||||||
|
{
|
||||||
|
dataitem(Customer; Customer)
|
||||||
|
{
|
||||||
|
column(No_Customer; "No.") { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeunit 50100 "Sample Report Selection Install"
|
||||||
|
{
|
||||||
|
procedure InstallDefaultReportSelection()
|
||||||
|
var
|
||||||
|
ReportSelections: Record "Report Selections";
|
||||||
|
begin
|
||||||
|
ReportSelections.InsertRecord(
|
||||||
|
"Report Selection Usage"::"Sample.SettlementDoc", '1', Report::"Sample Settlement Document");
|
||||||
|
// Registration ends here. No subscriber added to
|
||||||
|
// OnAfterFilterCustomerUsageReportSelections / OnAfterFilterVendorUsageReportSelections
|
||||||
|
// - the tenant-wide default works, but "Copy from Report Selection"
|
||||||
|
// on the Document Layouts page never lists this usage value, so a
|
||||||
|
// per-account override can only be entered by hand, if a user even
|
||||||
|
// knows to look for it.
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
enumextension 50100 "Sample Report Selection Usage Ext" extends "Report Selection Usage"
|
||||||
|
{
|
||||||
|
value(50100; "Sample.SettlementDoc")
|
||||||
|
{
|
||||||
|
Caption = 'Sample Settlement Document';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report 50100 "Sample Settlement Document"
|
||||||
|
{
|
||||||
|
UsageCategory = ReportsAndAnalysis;
|
||||||
|
ApplicationArea = All;
|
||||||
|
|
||||||
|
dataset
|
||||||
|
{
|
||||||
|
dataitem(Customer; Customer)
|
||||||
|
{
|
||||||
|
column(No_Customer; "No.") { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeunit 50100 "Sample Report Selection Install"
|
||||||
|
{
|
||||||
|
procedure InstallDefaultReportSelection()
|
||||||
|
var
|
||||||
|
ReportSelections: Record "Report Selections";
|
||||||
|
begin
|
||||||
|
ReportSelections.InsertRecord(
|
||||||
|
"Report Selection Usage"::"Sample.SettlementDoc", '1', Report::"Sample Settlement Document");
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
||||||
|
codeunit 50101 "Sample Report Selection Subscribers"
|
||||||
|
{
|
||||||
|
// Appends to whatever the standard filter already contains, following
|
||||||
|
// the real BCApps pattern in ReportSelectionHandlerCZC.Codeunit.al.
|
||||||
|
[EventSubscriber(ObjectType::Page, Page::"Customer Report Selections", 'OnAfterFilterCustomerUsageReportSelections', '', false, false)]
|
||||||
|
local procedure AddSampleUsageOnAfterFilterCustomerUsageReportSelections(var ReportSelections: Record "Report Selections")
|
||||||
|
begin
|
||||||
|
ReportSelections.SetFilter(Usage, GetUsageFilter(ReportSelections));
|
||||||
|
end;
|
||||||
|
|
||||||
|
[EventSubscriber(ObjectType::Page, Page::"Vendor Report Selections", 'OnAfterFilterVendorUsageReportSelections', '', false, false)]
|
||||||
|
local procedure AddSampleUsageOnAfterFilterVendorUsageReportSelections(var ReportSelections: Record "Report Selections")
|
||||||
|
begin
|
||||||
|
ReportSelections.SetFilter(Usage, GetUsageFilter(ReportSelections));
|
||||||
|
end;
|
||||||
|
|
||||||
|
local procedure GetUsageFilter(var ReportSelections: Record "Report Selections") UsageFilter: Text
|
||||||
|
begin
|
||||||
|
UsageFilter := Format("Report Selection Usage"::"Sample.SettlementDoc");
|
||||||
|
if ReportSelections.GetFilter(Usage) <> '' then
|
||||||
|
UsageFilter := StrSubstNo('%1|%2', ReportSelections.GetFilter(Usage), UsageFilter);
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: data-modeling
|
||||||
|
keywords: [report-selections, report-selection-usage, enumextension, document-layouts, custom-report-selection]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Register a new document type through Report Selections, and extend the Document Layouts filter
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
A custom document that needs to be printed or emailed should be registered
|
||||||
|
through `table 77 "Report Selections"`, not given its own bespoke
|
||||||
|
report/layout lookup. `enum 77 "Report Selection Usage"` is
|
||||||
|
`Extensible = true` specifically so a new document type can add its own
|
||||||
|
usage value via an `enumextension`, then register a default report for it
|
||||||
|
with `ReportSelections.InsertRecord(Usage, Sequence, ReportID)` — the same
|
||||||
|
mechanism every standard Sales/Purchase/Service document uses.
|
||||||
|
|
||||||
|
Registering through table 77 also brings per-account customization for
|
||||||
|
free: `table 9657 "Custom Report Selection"` (surfaced as the "Document
|
||||||
|
Layouts" action on the Customer and Vendor cards) lets one specific
|
||||||
|
account override both the report and the layout, and the platform's
|
||||||
|
lookup checks that table first before falling back to the tenant-wide
|
||||||
|
default. But the "Copy from Report Selection" action on the Document
|
||||||
|
Layouts pages — the convenience button a user actually uses to seed a
|
||||||
|
per-account override — filters to a **hardcoded** list of usage values
|
||||||
|
(`FilterCustomerUsageReportSelections`/`FilterVendorUsageReportSelections`
|
||||||
|
on `page 9657 "Customer Report Selections"`/`page 9658 "Vendor Report
|
||||||
|
Selections"`). A new custom usage value is not included automatically. Both
|
||||||
|
pages publish `OnAfterFilterCustomerUsageReportSelections(var
|
||||||
|
ReportSelections: Record "Report Selections")` /
|
||||||
|
`OnAfterFilterVendorUsageReportSelections(...)` for exactly this reason —
|
||||||
|
real BCApps localization apps (e.g. the Czech Compensation localization,
|
||||||
|
`ReportSelectionHandlerCZC.Codeunit.al`) subscribe to both events and
|
||||||
|
extend the filter with `StrSubstNo('%1|%2', ReportSelections.GetFilter(Usage),
|
||||||
|
UsageFilter)`, appending to whatever filter already exists rather than
|
||||||
|
replacing it.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
|
||||||
|
Add the new usage value via `enumextension ... extends "Report Selection
|
||||||
|
Usage"`, register a tenant-wide default row with
|
||||||
|
`ReportSelections.InsertRecord(...)`, and subscribe to both
|
||||||
|
`OnAfterFilterCustomerUsageReportSelections` and
|
||||||
|
`OnAfterFilterVendorUsageReportSelections` — even if the document only
|
||||||
|
ever applies to one counterparty side — appending to the existing filter
|
||||||
|
rather than overwriting it. Treat the registration and the filter
|
||||||
|
subscription as one inseparable step: shipping one without the other
|
||||||
|
leaves per-account layout customization silently unreachable through the
|
||||||
|
standard UI.
|
||||||
|
|
||||||
|
See sample: `extend-report-selection-usage-for-new-document-types.good.al`.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
|
||||||
|
Adding a new `Report Selection Usage` value and registering a default
|
||||||
|
report, but never subscribing to the filter events. The tenant-wide
|
||||||
|
default works, so the gap isn't visible in testing — but a user who opens
|
||||||
|
"Document Layouts" on a specific customer or vendor and clicks "Copy from
|
||||||
|
Report Selection" to start a per-account override will never see the new
|
||||||
|
document type in the list, with no error and no visible sign that
|
||||||
|
anything is missing.
|
||||||
|
|
||||||
|
See sample: `extend-report-selection-usage-for-new-document-types.bad.al`.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
BCApps `ReportSelections.Table.al` (table 77, `InsertRecord` at line 344),
|
||||||
|
`ReportSelectionUsage.Enum.al` (enum 77, `Extensible = true`),
|
||||||
|
`CustomReportSelection.Table.al` (table 9657), `CustomerReportSelections.Page.al`
|
||||||
|
(page 9657, `FilterCustomerUsageReportSelections` and
|
||||||
|
`OnAfterFilterCustomerUsageReportSelections` at line 335),
|
||||||
|
`VendorReportSelections.Page.al` (page 9658, `OnAfterFilterVendorUsageReportSelections`
|
||||||
|
at line 296) — all under `src/Layers/W1/BaseApp/`. Real subscriber
|
||||||
|
precedent: `src/Apps/CZ/CompensationLocalization/app/Src/Codeunits/ReportSelectionHandlerCZC.Codeunit.al`,
|
||||||
|
`GetUsageFilter` (line 104) and both event subscribers (lines 38, 66).
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
tableextension 50100 "Sample Sales Header Ext" extends "Sales Header"
|
||||||
|
{
|
||||||
|
fields
|
||||||
|
{
|
||||||
|
field(50000; "Reference No."; Code[20])
|
||||||
|
{
|
||||||
|
Caption = 'Reference No.';
|
||||||
|
DataClassification = CustomerContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tableextension 50101 "Sample Sales Invoice Header Ext" extends "Sales Invoice Header"
|
||||||
|
{
|
||||||
|
fields
|
||||||
|
{
|
||||||
|
// WRONG: same field number 50000, but a shorter length than the
|
||||||
|
// Sales Header extension above. This compiles fine and posts
|
||||||
|
// fine for every "Reference No." of 10 characters or less -
|
||||||
|
// SalesInvHeader.TransferFields(SalesHeader) in
|
||||||
|
// SalesPost.Codeunit.al only throws once an actual value longer
|
||||||
|
// than 10 characters reaches posting, which typical test data
|
||||||
|
// never triggers.
|
||||||
|
field(50000; "Reference No."; Code[10])
|
||||||
|
{
|
||||||
|
Caption = 'Reference No.';
|
||||||
|
DataClassification = CustomerContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
tableextension 50100 "Sample Sales Header Ext" extends "Sales Header"
|
||||||
|
{
|
||||||
|
fields
|
||||||
|
{
|
||||||
|
field(50000; "Reference No."; Code[20])
|
||||||
|
{
|
||||||
|
Caption = 'Reference No.';
|
||||||
|
DataClassification = CustomerContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tableextension 50101 "Sample Sales Invoice Header Ext" extends "Sales Invoice Header"
|
||||||
|
{
|
||||||
|
fields
|
||||||
|
{
|
||||||
|
// Same field number, same type, same length as the Sales Header
|
||||||
|
// extension above. SalesInvHeader.TransferFields(SalesHeader) in
|
||||||
|
// SalesPost.Codeunit.al only bridges two fields that agree on all
|
||||||
|
// three - matching all three here is what makes this value
|
||||||
|
// survive posting for every possible "Reference No." value.
|
||||||
|
field(50000; "Reference No."; Code[20])
|
||||||
|
{
|
||||||
|
Caption = 'Reference No.';
|
||||||
|
DataClassification = CustomerContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: data-modeling
|
||||||
|
keywords: [transferfields, field-number, posting-cascade, schema-design, custom-field]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mirrored TransferFields cascade fields must match type and length exactly
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Most custom fields genuinely belong to only one table — a status used
|
||||||
|
only before posting, a note relevant only afterwards, whatever the case
|
||||||
|
may be. That is the ordinary, unremarkable default, and it needs no
|
||||||
|
justification: `TransferFields` never touches a field that doesn't exist
|
||||||
|
on the destination. Per Microsoft's own documentation, a source field's
|
||||||
|
contents are copied "if such a field exists" on the destination with a
|
||||||
|
matching field number — a field defined on only one side of a posting
|
||||||
|
cascade is simply outside `TransferFields`' reach, not a gap to fix.
|
||||||
|
|
||||||
|
The narrower case this rule addresses is when a field **is** deliberately
|
||||||
|
mirrored across a known cascade — the same field number reused on
|
||||||
|
another table specifically so the value survives posting, for example a
|
||||||
|
field added to both `Sales Header` (36) and `Sales Invoice Header` (112),
|
||||||
|
which `SalesPost.Codeunit.al` connects via
|
||||||
|
`SalesInvHeader.TransferFields(SalesHeader)`. The two definitions have to
|
||||||
|
agree on type and, less obviously, on length. A field defined `Text[100]`
|
||||||
|
on `Sales Header` and `Text[50]` on `Sales Invoice Header` compiles
|
||||||
|
cleanly on both sides, and the `TransferFields` call runs without error
|
||||||
|
for every value up to 50 characters. Per Microsoft's documentation, a
|
||||||
|
runtime error only occurs when there isn't "room for the actual length
|
||||||
|
of the contents of the field to be copied" — so nothing fails while test
|
||||||
|
data, or early production data, stays short. The error surfaces only the
|
||||||
|
day an actual value finally exceeds the shorter definition, on a document
|
||||||
|
type that may have been posting cleanly for months.
|
||||||
|
|
||||||
|
See also `transferfields-skip-type-mismatch-can-drop-data.md`, which
|
||||||
|
covers `SkipFieldsNotMatchingType = true` silently skipping a *type*
|
||||||
|
mismatch between same-extension fields. That parameter has no effect on
|
||||||
|
length: two fields of the same type but different length still raise the
|
||||||
|
runtime error described above regardless of how `SkipFieldsNotMatchingType`
|
||||||
|
is set, which is the distinct failure mode this article addresses.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
|
||||||
|
When mirroring a field across a `TransferFields` cascade, define it with
|
||||||
|
the exact same field number, data type, and length on every table in
|
||||||
|
that cascade, at creation time. A field intentionally left local to one
|
||||||
|
table is unaffected by this and needs no mirroring at all — this is a
|
||||||
|
consistency requirement between definitions that are already meant to be
|
||||||
|
linked, not a mandate to check every field against every table on the
|
||||||
|
cascade.
|
||||||
|
|
||||||
|
See sample: `transferfields-mirrored-fields-must-match-type-and-length.good.al`.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
|
||||||
|
The same field number added to two tables that `TransferFields` connects
|
||||||
|
in a posting cascade (e.g. `Sales Header` (36) and `Sales Invoice Header`
|
||||||
|
(112), linked by `SalesPost.Codeunit.al`), with a shorter length — or an
|
||||||
|
incompatible data type — on one side. Both definitions compile without
|
||||||
|
error; nothing fails until an actual value exceeds the shorter one, which
|
||||||
|
typical test data never does.
|
||||||
|
|
||||||
|
See sample: `transferfields-mirrored-fields-must-match-type-and-length.bad.al`.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
Microsoft Learn, `Record.TransferFields(var Record [, Boolean])`:
|
||||||
|
"The `TransferFields` method copies fields based on the field number on
|
||||||
|
the fields. For each field in `Record` (the destination), the contents
|
||||||
|
of the field that has the same field number in `FromRecord` (the source)
|
||||||
|
will be copied, **if such a field exists**." And: "The fields must have
|
||||||
|
the *same data type* for the copying to succeed... There must be room
|
||||||
|
for the actual length of the contents of the field to be copied in the
|
||||||
|
field to which it is to be copied. If any one of these conditions aren't
|
||||||
|
fulfilled, a runtime error will occur."
|
||||||
|
(https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/record/record-transferfields-table-boolean-method)
|
||||||
|
|
||||||
|
BCApps `SalesPost.Codeunit.al` (`src/Layers/W1/BaseApp/Sales/Posting/`):
|
||||||
|
`SalesShptHeader.TransferFields(SalesHeader);` (line 7104),
|
||||||
|
`ReturnRcptHeader.TransferFields(SalesHeader);` (line 7166),
|
||||||
|
`SalesInvHeader.TransferFields(SalesHeader);` (line 7220),
|
||||||
|
`SalesCrMemoHeader.TransferFields(SalesHeader);` (line 7275) — the real
|
||||||
|
cascade a mirrored field on `Sales Header` (36) is checked against.
|
||||||
|
|
@ -52,6 +52,11 @@ The following targeted checks cover every current `data-modeling` article. Treat
|
||||||
- A master table adds or changes `Last Date Modified`, `OnModify`, or `OnRename`, but the non-editable field is not assigned `Today()` in both triggers — `set-last-date-modified-in-onmodify-and-onrename`.
|
- A master table adds or changes `Last Date Modified`, `OnModify`, or `OnRename`, but the non-editable field is not assigned `Today()` in both triggers — `set-last-date-modified-in-onmodify-and-onrename`.
|
||||||
- A `tableextension` appends a conditional `TableRelation` as if it overrides an earlier unconditional relation, or relation branches are otherwise designed without accounting for additive top-down evaluation — `table-relation-extensions-are-additive-and-top-down`.
|
- A `tableextension` appends a conditional `TableRelation` as if it overrides an earlier unconditional relation, or relation branches are otherwise designed without accounting for additive top-down evaluation — `table-relation-extensions-are-additive-and-top-down`.
|
||||||
- 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 `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 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 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`, but no subscriber is added for `OnAfterFilterCustomerUsageReportSelections`/`OnAfterFilterVendorUsageReportSelections` on `page 9657`/`page 9658` — `extend-report-selection-usage-for-new-document-types`. Registration and the filter-event subscription are one inseparable unit of work.
|
||||||
|
- The same field number is added as a new field on two or more tables connected by a `TransferFields` call in a posting cascade (e.g. a header table and the posted-document table `SalesPost.Codeunit.al`/`PurchPost.Codeunit.al` transfer into), with a different data type or length on one side — `transferfields-mirrored-fields-must-match-type-and-length`. A field defined on only one side of the cascade is out of scope; this cues only on a field deliberately mirrored across the cascade with a type or length mismatch.
|
||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue