mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
Fix four merge-critical blockers from Jesper's review; add 4 more patterns
Addresses microsoft/BCQuality#175 review feedback: - Extend al-data-modeling-review's entry gate/relevance scope and token list to recognize document actions, Navigate subscribers, Report Selection registration, price-calculation/price-source extensibility, TransferFields posting-cascade mirroring, and barcode font-provider usage - previously excluded before any worklist cue could run. - Fix document-print-and-email-actions-call-report-selections-directly: permit the legitimate stateless DocumentSendingProfile.TrySendToPrinter/ TrySendToEMail path; rework the bad fixture to load a configured profile instead of demonstrating a trivial blank-record no-op. - Fix extend-report-selection-usage-for-new-document-types: scope to the applicable single counterparty (ReportSelectionHandlerCZZ partitions strictly; only genuinely two-sided usages like Compensation need both), and add the page-facing usage-enum map/validate events alongside the filter-event subscription for full Document Layouts support. - Fix a stale field-citation in custom-document-dispatch-must-not-bypass- report-selections (Custom Report Layout Code is field 7, not part of the 19-26 email-configuration range). - Add deterministic positive/clean evaluation coverage (review-fixtures.json additionalArticles + Test-ReviewFixtures.ps1 support) so all 9 new good/bad pairs are actually exercised, not just present. - Add 4 new patterns: activate-new-price-calculation-handler-via- onfindsupportedsetup, extend-price-source-type-must-sync-document- subset-enum, new-price-source-must-add-candidate-and-trigger- recalculation, report-barcodes-must-use-barcode-module-and-production- font-name. All claims verified against live microsoft/BCApps source and Microsoft Learn. Validators: frontmatter 0/0, review-fixtures 52 cases/17 domains PASSED, knowledge-index 309 articles PASSED. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
83f041b662
commit
31206f6616
23 changed files with 801 additions and 156 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
The evaluation is convention-driven. The harness discovers every `<layer>/skills/review/al-<domain>-review.md` leaf across the enabled `microsoft`, `community`, and `custom` layers. Duplicate domains resolve with `custom > community > microsoft` precedence. For each selected leaf, the harness finds paired knowledge across the same layers, applies the same precedence to duplicate article slugs, selects the first article (by filename) with both `.bad.al` and `.good.al` companions, and derives the expected positive and clean control automatically. Adding a conforming leaf requires no scoring-contract edit.
|
The evaluation is convention-driven. The harness discovers every `<layer>/skills/review/al-<domain>-review.md` leaf across the enabled `microsoft`, `community`, and `custom` layers. Duplicate domains resolve with `custom > community > microsoft` precedence. For each selected leaf, the harness finds paired knowledge across the same layers, applies the same precedence to duplicate article slugs, selects the first article (by filename) with both `.bad.al` and `.good.al` companions, and derives the expected positive and clean control automatically. Adding a conforming leaf requires no scoring-contract edit.
|
||||||
|
|
||||||
`review-fixtures.json` contains only global thresholds and optional exceptional overrides. An override may select a different article or add context when the generic convention cannot express a scenario. It should remain empty in the normal case.
|
`review-fixtures.json` contains only global thresholds and optional exceptional overrides. An override may select a different article or add context when the generic convention cannot express a scenario. It should remain empty in the normal case. An override may also list `additionalArticles` — other same-domain slugs (each with a `.good.al`/`.bad.al` pair) that get their own deterministic positive/clean case pair alongside the convention-selected one. Use this when a single leaf's worklist covers several distinct, newly-added rules and each one needs its own proof of reachability rather than riding on whichever article the generic convention happens to select.
|
||||||
|
|
||||||
Model-facing preparation hashes case IDs, neutralizes `Good`/`Bad` object-name tokens, and removes full-line sample comments so neither the article slug, domain, nor expected outcome reveals the answer.
|
Model-facing preparation hashes case IDs, neutralizes `Good`/`Bad` object-name tokens, and removes full-line sample comments so neither the article slug, domain, nor expected outcome reveals the answer.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,20 @@
|
||||||
"breaking-changes": {
|
"breaking-changes": {
|
||||||
"article": "do-not-expose-sensitive-data-through-public-api"
|
"article": "do-not-expose-sensitive-data-through-public-api"
|
||||||
},
|
},
|
||||||
|
"data-modeling": {
|
||||||
|
"article": "check-blocked-in-referencing-code-not-in-master",
|
||||||
|
"additionalArticles": [
|
||||||
|
"extend-report-selection-usage-for-new-document-types",
|
||||||
|
"document-print-and-email-actions-call-report-selections-directly",
|
||||||
|
"custom-document-dispatch-must-not-bypass-report-selections",
|
||||||
|
"transferfields-mirrored-fields-must-match-type-and-length",
|
||||||
|
"extend-find-entries-navigate-for-new-document-types",
|
||||||
|
"activate-new-price-calculation-handler-via-onfindsupportedsetup",
|
||||||
|
"extend-price-source-type-must-sync-document-subset-enum",
|
||||||
|
"new-price-source-must-add-candidate-and-trigger-recalculation",
|
||||||
|
"report-barcodes-must-use-barcode-module-and-production-font-name"
|
||||||
|
]
|
||||||
|
},
|
||||||
"events": {
|
"events": {
|
||||||
"article": "reset-ishandled-only-when-the-value-can-carry-over"
|
"article": "reset-ishandled-only-when-the-value-can-carry-over"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
enumextension 50102 "Sample Price Calc Handler Ext" extends "Price Calculation Handler"
|
||||||
|
{
|
||||||
|
value(50102; "Sample Special Price")
|
||||||
|
{
|
||||||
|
Caption = 'Sample Special Price';
|
||||||
|
Implementation = "Price Calculation" = "Sample Price Calc - Special";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WRONG: no subscriber to Price Calculation Mgt.'s OnFindSupportedSetup.
|
||||||
|
// "Sample Special Price" is a real, working implementation of the Price
|
||||||
|
// Calculation interface - it simply has no Price Calculation Setup row
|
||||||
|
// naming it, so Price Calculation Mgt. never selects it for any sale,
|
||||||
|
// purchase, or job line. It ships invisible until someone notices and
|
||||||
|
// configures a setup row for it by hand.
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
enumextension 50102 "Sample Price Calc Handler Ext" extends "Price Calculation Handler"
|
||||||
|
{
|
||||||
|
value(50102; "Sample Special Price")
|
||||||
|
{
|
||||||
|
Caption = 'Sample Special Price';
|
||||||
|
Implementation = "Price Calculation" = "Sample Price Calc - Special";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeunit 50104 "Sample Price Calc Setup Install"
|
||||||
|
{
|
||||||
|
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Price Calculation Mgt.", 'OnFindSupportedSetup', '', false, false)]
|
||||||
|
local procedure AddSampleSpecialPriceSetup(var TempPriceCalculationSetup: Record "Price Calculation Setup" temporary)
|
||||||
|
begin
|
||||||
|
TempPriceCalculationSetup.Init();
|
||||||
|
TempPriceCalculationSetup.Code := 'SAMPLE-SPECIAL';
|
||||||
|
TempPriceCalculationSetup.Method := TempPriceCalculationSetup.Method::"Lowest Price";
|
||||||
|
TempPriceCalculationSetup.Type := TempPriceCalculationSetup.Type::Sale;
|
||||||
|
TempPriceCalculationSetup."Asset Type" := TempPriceCalculationSetup."Asset Type"::" ";
|
||||||
|
TempPriceCalculationSetup.Implementation := TempPriceCalculationSetup.Implementation::"Sample Special Price";
|
||||||
|
TempPriceCalculationSetup.Enabled := true;
|
||||||
|
TempPriceCalculationSetup.Default := true;
|
||||||
|
TempPriceCalculationSetup.Insert();
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: data-modeling
|
||||||
|
keywords: [price-calculation, price-calculation-handler, price-calculation-setup, integration-event, pricing]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Activate a new Price Calculation Handler through OnFindSupportedSetup, not just by implementing it
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
`enum 7011 "Price Calculation Handler"` (`implements "Price
|
||||||
|
Calculation"`) is how a new pricing engine plugs into Business Central —
|
||||||
|
extend the enum with a value pointing at a codeunit that implements the
|
||||||
|
`Price Calculation` interface. That alone does not make the new handler
|
||||||
|
usable on any document. `codeunit 7001 "Price Calculation Mgt."` decides
|
||||||
|
which handler applies to a given line by looking up `table 7006 "Price
|
||||||
|
Calculation Setup"`, a table of `(Code, Method, Type, Asset Type,
|
||||||
|
Implementation, Enabled, Default)` rows populated at startup by its own
|
||||||
|
`OnFindSupportedSetup` event — every implementation codeunit is expected
|
||||||
|
to subscribe to that event and insert its own setup row(s). A handler
|
||||||
|
enum value with no matching setup row is real and selectable in the enum
|
||||||
|
itself, but never chosen for any actual sale, purchase, or job line,
|
||||||
|
because `Price Calculation Mgt.` has no setup row that names it. A setup
|
||||||
|
row that exists but doesn't match is just as invisible: `FindSetup`
|
||||||
|
filters candidates with `SetRange(Default, true)` and `SetRange(Method,
|
||||||
|
DtldPriceCalcSetup.Method)` (a document's blank Method is normalized to
|
||||||
|
`"Lowest Price"` before that filter runs), so a row inserted without
|
||||||
|
`Default := true`, or with a `Method` that doesn't match, is never
|
||||||
|
selected either — same symptom, different cause.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
|
||||||
|
Ship a new `Price Calculation Handler` value together with an
|
||||||
|
`OnFindSupportedSetup` subscriber that inserts at least one `Price
|
||||||
|
Calculation Setup` record naming it as the `Implementation`, for the
|
||||||
|
relevant `Method` (e.g. `"Lowest Price"`), `Type` (`Sale`/`Purchase`), and
|
||||||
|
`Asset Type` — with `Default := true`, since `FindSetup` only considers
|
||||||
|
rows where `Default` is set when resolving a handler for a line.
|
||||||
|
|
||||||
|
See sample: `activate-new-price-calculation-handler-via-onfindsupportedsetup.good.al`.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
|
||||||
|
Extending `Price Calculation Handler` and implementing the `Price
|
||||||
|
Calculation` interface, without subscribing to `OnFindSupportedSetup` to
|
||||||
|
insert a setup record. The new handler exists, compiles, and can even be
|
||||||
|
selected manually if a user creates their own `Price Calculation Setup`
|
||||||
|
row through the UI — but ships with no default row, so it's never active
|
||||||
|
for anyone until someone notices it's missing and configures it by hand.
|
||||||
|
|
||||||
|
See sample: `activate-new-price-calculation-handler-via-onfindsupportedsetup.bad.al`.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
BCApps (`src/Layers/W1/BaseApp/Pricing/Calculation/`):
|
||||||
|
`PriceCalculationHandler.Enum.al` (`enum 7011 "Price Calculation Handler"
|
||||||
|
implements "Price Calculation"`); `PriceCalculationMgt.Codeunit.al`
|
||||||
|
(`local procedure OnFindSupportedSetup(var TempPriceCalculationSetup:
|
||||||
|
Record "Price Calculation Setup" temporary)`, called during setup
|
||||||
|
resolution, and `procedure FindSetup(...)`, which requires
|
||||||
|
`SetRange(Default, true)` and a matching `SetRange(Method,
|
||||||
|
DtldPriceCalcSetup.Method)` before a row can be selected);
|
||||||
|
`PriceCalculationSetup.Table.al` (`table 7006 "Price Calculation Setup"`:
|
||||||
|
`Code` (Code[100]), `Method` (Enum "Price Calculation Method"), `Type`
|
||||||
|
(Enum "Price Type"), `"Asset Type"` (Enum "Price Asset Type"),
|
||||||
|
`Implementation` (Enum "Price Calculation Handler"), `Enabled` (Boolean),
|
||||||
|
`Default` (Boolean)).
|
||||||
|
|
||||||
|
BCApps (`src/Layers/W1/BaseApp/Pricing/PriceList/`): `PriceType.Enum.al`
|
||||||
|
(`enum 7009 "Price Type"`: `Any`(0)/`Sale`(1)/`Purchase`(2)).
|
||||||
|
|
||||||
|
Microsoft Learn, "Extending Price Calculations": "For the new codeunit,
|
||||||
|
you must extend the Price Calculation Handler enum that implements Price
|
||||||
|
Calculation interface... Afterwards you can insert a record in the Price
|
||||||
|
Calculation Setup table... Each codeunit that implements the Price
|
||||||
|
Calculation interface must subscribe to the OnFindSupportedSetup() event
|
||||||
|
of the Price Calculation Mgt codeunit to fill the price calculation setup
|
||||||
|
table with new options."
|
||||||
|
(https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-extending-best-price-calculations)
|
||||||
|
|
@ -18,7 +18,8 @@ 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. `Report
|
||||||
Selections` carries its own attachment/email-body configuration per usage
|
Selections` carries its own attachment/email-body configuration per usage
|
||||||
(`"Use for Email Attachment"`, `"Use for Email Body"`, `"Email Body Layout
|
(`"Use for Email Attachment"`, `"Use for Email Body"`, `"Email Body Layout
|
||||||
Code"`, `"Email Body Layout Type"`, `"Custom Report Layout Code"`), and
|
Code"`, `"Email Body Layout Type"`), plus a separate per-usage layout
|
||||||
|
override, `"Custom Report Layout Code"`, and
|
||||||
`table 9657 "Custom Report Selection"` (the "Document Layouts" page on the
|
`table 9657 "Custom Report Selection"` (the "Document Layouts" page on the
|
||||||
Customer/Vendor card) lets one specific account override the report or
|
Customer/Vendor card) lets one specific account override the report or
|
||||||
layout without touching code at all. None of that exists for a document
|
layout without touching code at all. None of that exists for a document
|
||||||
|
|
@ -54,9 +55,10 @@ See sample: `custom-document-dispatch-must-not-bypass-report-selections.bad.al`.
|
||||||
|
|
||||||
## Source
|
## Source
|
||||||
|
|
||||||
BCApps `ReportSelections.Table.al` (table 77 — fields 19–26 for email
|
BCApps `ReportSelections.Table.al` (table 77 — field 7,
|
||||||
attachment/body configuration; `SendEmailToCust`/`PrintWithDialogForCust`
|
`"Custom Report Layout Code"`; fields 19–26 for email attachment/body
|
||||||
as the registry-backed dispatch entry points) and
|
configuration; `SendEmailToCust`/`PrintWithDialogForCust` as the
|
||||||
|
registry-backed dispatch entry points) and
|
||||||
`CustomReportSelection.Table.al` (table 9657, the per-account override
|
`CustomReportSelection.Table.al` (table 9657, the per-account override
|
||||||
backing the "Document Layouts" page) — both under
|
backing the "Document Layouts" page) — both under
|
||||||
`src/Layers/W1/BaseApp/Foundation/Reporting/`.
|
`src/Layers/W1/BaseApp/Foundation/Reporting/`.
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,16 @@ page 50101 "Sample Settlement Document Card"
|
||||||
DocumentSendingProfile: Record "Document Sending Profile";
|
DocumentSendingProfile: Record "Document Sending Profile";
|
||||||
begin
|
begin
|
||||||
// WRONG: this is a plain, on-demand "Email" button, not
|
// WRONG: this is a plain, on-demand "Email" button, not
|
||||||
// part of a combined Post-and-Send action - but routing
|
// part of a combined Post-and-Send action - but this
|
||||||
// it through Document Sending Profile means the outcome
|
// loads the customer's ACTUAL assigned profile (or the
|
||||||
// now silently depends on this customer's assigned
|
// tenant default, if none is assigned - the same lookup
|
||||||
// profile. If that profile's "E-Mail" option is No, the
|
// Sales-Post and Send performs) and calls Send on it, so
|
||||||
// user sees nothing happen after clicking Email, with no
|
// the outcome now silently depends on that profile. A
|
||||||
// indication that an unrelated setup field is why.
|
// profile set up for Post-and-Send printing only (say,
|
||||||
|
// 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);
|
||||||
DocumentSendingProfile.Send(
|
DocumentSendingProfile.Send(
|
||||||
"Report Selection Usage"::"S.Invoice".AsInteger(), Rec, Rec."No.", Rec."No.",
|
"Report Selection Usage"::"S.Invoice".AsInteger(), Rec, Rec."No.", Rec."No.",
|
||||||
Rec.Name, Rec.FieldNo("No."), Rec.FieldNo("No."));
|
Rec.Name, Rec.FieldNo("No."), Rec.FieldNo("No."));
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@ page 50101 "Sample Settlement Document Card"
|
||||||
begin
|
begin
|
||||||
// Calls Report Selections directly - the button's outcome
|
// Calls Report Selections directly - the button's outcome
|
||||||
// depends only on this customer's registered report/layout,
|
// depends only on this customer's registered report/layout,
|
||||||
// not on any Document Sending Profile setting.
|
// not on any Document Sending Profile setting. Calling
|
||||||
|
// DocumentSendingProfile.TrySendToEMail(...) instead would
|
||||||
|
// be equally correct: it never Get's the customer's
|
||||||
|
// actually assigned profile, only a local, hardcoded one.
|
||||||
ReportSelections.SendEmailToCust(
|
ReportSelections.SendEmailToCust(
|
||||||
"Report Selection Usage"::"S.Invoice".AsInteger(), Rec, Rec."No.",
|
"Report Selection Usage"::"S.Invoice".AsInteger(), Rec, Rec."No.",
|
||||||
Rec.Name, true, Rec."No.");
|
Rec.Name, true, Rec."No.");
|
||||||
|
|
|
||||||
|
|
@ -11,89 +11,90 @@ application-area: [all]
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
`table 60 "Document Sending Profile"` is not a general gateway that every
|
`table 60 "Document Sending Profile"` is not a general gateway for every
|
||||||
print/email path should route through — it exists specifically for the
|
print/email path — it exists specifically for the combined **Post and
|
||||||
combined **Post and Send** action: "You can set each customer up with a
|
Send** action: "You can set each customer up with a preferred method of
|
||||||
preferred method of sending sales documents, so that you do not have to
|
sending sales documents, so that you do not have to select a sending
|
||||||
select a sending option every time you choose the Post and Send action"
|
option every time you choose the Post and Send action" (Microsoft Learn,
|
||||||
(Microsoft Learn, "Set Up Document Sending Profiles"). A document's own, ordinary
|
"Set Up Document Sending Profiles"). A document's own, ordinary
|
||||||
Print/Email ribbon actions call `table 77 "Report Selections"` directly
|
Print/Email actions are unaffected by any *configured* profile either
|
||||||
and are not affected by any Document Sending Profile at all. This is the
|
way: the unposted Sales Order's "Print Confirmation"/"Email
|
||||||
pattern BC's own base application uses for a document's plain print/email
|
Confirmation" (`codeunit "Document-Print"`,
|
||||||
actions: the Sales Order's "Print Confirmation"/"Email Confirmation"
|
`PrintSalesOrder`/`EmailSalesHeader`) and the posted `Purch. Inv.
|
||||||
actions (`codeunit "Document-Print"`, `PrintSalesOrder`/`EmailSalesHeader`)
|
Header`'s `PrintRecords` call `Report Selections` literally directly
|
||||||
call `ReportSelections.PrintWithDialogForCust`/`SendEmailToCust` directly,
|
(`PrintWithDialogForCust`/`SendEmailToCust`/`PrintWithDialogForVend`),
|
||||||
and the posted `Purch. Inv. Header`'s own `PrintRecords` does the same
|
while the posted `Sales Invoice Header`'s `PrintRecords`/`EmailRecords`
|
||||||
through `ReportSelection.PrintWithDialogForVend` — no customer's or
|
and the unposted `Purchase Header`'s `PrintRecords` go through
|
||||||
vendor's actually assigned Document Sending Profile is consulted by
|
`DocumentSendingProfile.TrySendToPrinter`/`TrySendToEMail`/
|
||||||
either.
|
`TrySendToPrinterVendor` instead. Those three helpers each declare a
|
||||||
|
fresh, local, never-`Get`'d profile record, hardcode its
|
||||||
|
`Printer`/`"E-Mail"` field to a "Yes" option themselves, and feed it into
|
||||||
|
`SendToPrinter`/`SendToEMailGroupedMultipleSelection` — which resolve
|
||||||
|
into Report Selections just like the direct route. The table is a
|
||||||
|
throwaway options carrier here, not the counterparty's configuration.
|
||||||
|
|
||||||
The unposted `Purchase Header`'s own `PrintRecords` is a partial exception
|
Only a genuinely configured profile changes the outcome, and that only
|
||||||
worth naming precisely: it calls `DocumentSendingProfile.TrySendToPrinterVendor(...)`,
|
happens for the combined Post-and-Send flow: `Sales-Post and Send` loads
|
||||||
but only as a stateless, never-`Get`'d local record carrying print-dialog
|
the customer's assigned profile (`Get(Customer."Document Sending
|
||||||
options, never a vendor's actually configured profile — that helper still
|
Profile")`, or the tenant default) before `Sales Invoice
|
||||||
resolves the report through `ReportSelections.PrintWithDialogForVend(...)`,
|
Header.SendProfile` → `DocumentSendingProfile.Send`, which gates
|
||||||
the same as everywhere else.
|
`SendToPrinter`/`SendToEMail`/`SendToDisk` on whatever that record holds.
|
||||||
|
|
||||||
Only the combined Post-and-Send flow resolves through Document Sending
|
Whether a document needs outbound distribution isn't determined by
|
||||||
Profile: `Sales-Post and Send` calls `Sales Invoice Header.SendProfile`,
|
Customer vs. Vendor, but by whether it's genuinely *outbound* to that
|
||||||
which calls `DocumentSendingProfile.Send(...)`, which then decides
|
party: a posted Purchase Invoice records what a vendor already billed,
|
||||||
Print/Email/Disk/Electronic based on the customer's assigned profile and
|
so the posted `Purch. Inv. Header` has only a bare `PrintRecords`; a
|
||||||
only *then* calls back into `Report Selections` (for the PDF cases) or
|
Purchase *Order* is still outbound before posting, so the rich
|
||||||
`Electronic Document Format` (for machine-readable cases).
|
`SendProfile`/`SendRecords`/`PrintRecords` triplet lives there instead.
|
||||||
|
|
||||||
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
|
## Best Practice
|
||||||
|
|
||||||
For a document's own interactive Print/Email actions, call the relevant
|
For a document's own interactive Print/Email actions, either call the
|
||||||
`Report Selections` procedure directly —
|
relevant `Report Selections` procedure directly —
|
||||||
`PrintForCust`/`PrintWithDialogForCust`/`SendEmailToCust` for a
|
`PrintForCust`/`PrintWithDialogForCust`/`SendEmailToCust` for a
|
||||||
customer-facing document, `PrintWithDialogForVend`/`SendEmailToVendor` for
|
customer-facing document, `PrintWithDialogForVend`/`SendEmailToVendor`
|
||||||
a vendor-facing one — using the usage value registered per
|
for a vendor-facing one — or call one of `Document Sending Profile`'s
|
||||||
`extend-report-selection-usage-for-new-document-types.md`. Wire into
|
stateless `TrySendToPrinter`/`TrySendToEMail`/`TrySendToPrinterVendor`
|
||||||
`Document Sending Profile` only when specifically building a combined
|
helpers, using the usage value registered per
|
||||||
Post-and-Send action for that document. Before adding any send capability
|
`extend-report-selection-usage-for-new-document-types.md`. Both are
|
||||||
at all, confirm the document is genuinely outbound to the counterparty
|
equally correct; neither reads the counterparty's assigned profile.
|
||||||
it's attached to; a document that only records something already received
|
Reserve a genuine `Get`/`GetDefaultForCustomer`/`GetDefaultForVendor`
|
||||||
needs print-for-reference at most, not a send path.
|
lookup and `Send`/`SendVendor` for Post-and-Send.
|
||||||
|
|
||||||
See sample: `document-print-and-email-actions-call-report-selections-directly.good.al`.
|
See sample: `document-print-and-email-actions-call-report-selections-directly.good.al`.
|
||||||
|
|
||||||
## Anti Pattern
|
## Anti Pattern
|
||||||
|
|
||||||
Routing a document's plain, on-demand "Email" button through
|
Loading the counterparty's *actually assigned* `Document Sending
|
||||||
`DocumentSendingProfile.Send`/`SendVendor` instead of calling
|
Profile` (or the tenant default, via `Get`/`GetDefaultForCustomer`/
|
||||||
`ReportSelections.SendEmailToCust`/`SendEmailToVendor` directly. The
|
`GetDefaultForVendor` — the same lookup `Sales-Post and Send` performs)
|
||||||
button's outcome now silently depends on that customer's or vendor's
|
and calling `Send`/`SendVendor` on it from a plain, on-demand "Email"
|
||||||
assigned Document Sending Profile — if its `"E-Mail"` option happens to be
|
button, instead of `ReportSelections.SendEmailToCust`/`SendEmailToVendor`
|
||||||
`No`, clicking "Email" does nothing observable, with no indication to the
|
directly. The button's outcome now silently depends on a profile
|
||||||
user that a profile setting (meant for the Post-and-Send flow) is the
|
configured for Post-and-Send — if its `"E-Mail"` option is `No`,
|
||||||
reason. A second version of the same mistake: adding an email action to a
|
clicking "Email" does nothing observable. A second version of the same
|
||||||
document that only receives from its counterparty and was never meant to
|
mistake: an email action on a document that only receives from its
|
||||||
send anything back.
|
counterparty and was never meant to send anything back.
|
||||||
|
|
||||||
See sample: `document-print-and-email-actions-call-report-selections-directly.bad.al`.
|
See sample: `document-print-and-email-actions-call-report-selections-directly.bad.al`.
|
||||||
|
|
||||||
## Source
|
## Source
|
||||||
|
|
||||||
BCApps `DocumentPrint.Codeunit.al` (`EmailSalesHeader`/`DoPrintSalesHeader`/`PrintSalesOrder`,
|
BCApps `DocumentPrint.Codeunit.al` (`EmailSalesHeader`/`DoPrintSalesHeader`/
|
||||||
calling `ReportSelections.SendEmailToCust`/`PrintForCust`/`PrintWithDialogForCust`
|
`PrintSalesOrder` → `ReportSelections.SendEmailToCust`/`PrintForCust`/
|
||||||
directly), `PurchaseHeader.Table.al` (`SendProfile` at line ~6387, calling
|
`PrintWithDialogForCust` directly), `SalesInvoiceHeader.Table.al`
|
||||||
`DocumentSendingProfile.SendVendor`), `PurchInvHeader.Table.al` (`PrintRecords`
|
(`PrintRecords`/`EmailRecords`, lines 1453/1528 → `TrySendToPrinter`/
|
||||||
calling `ReportSelection.PrintWithDialogForVend` directly, no send capability),
|
`TrySendToEMail`, lines 1462/1541, on a local never-`Get`'d record),
|
||||||
`SalesPost.Codeunit.al`
|
`PurchaseHeader.Table.al` (`PrintRecords` line 6357 →
|
||||||
(`SendPostedDocumentRecord` at line 7660 → `SalesInvHeader.SendProfile` at
|
`TrySendToPrinterVendor` line 6374; `SendProfile` line 6387 →
|
||||||
lines 7680/7699 → `DocumentSendingProfile.Send`),
|
`SendVendor` line 6403), `PurchInvHeader.Table.al` (`PrintRecords` →
|
||||||
`DocumentSendingProfile.Table.al` (table 60; `TrySendToPrinterVendor` at
|
`ReportSelection.PrintWithDialogForVend` directly, no send capability),
|
||||||
line 552 and `SendToPrinterVendor` at line 716, called from
|
`SalesPostandSend.Codeunit.al`/`SalesPost.Codeunit.al`
|
||||||
`PurchaseHeader.PrintRecords` at line 6357) — all under
|
(`ConfirmPostAndSend` loads `Get(Customer."Document Sending
|
||||||
`src/Layers/W1/BaseApp/`. Microsoft Learn, "Set Up Document Sending Profiles":
|
Profile")`/`GetDefault`; `SendPostedDocumentRecord` line 7660 →
|
||||||
https://learn.microsoft.com/dynamics365/business-central/sales-how-setup-document-send-profiles
|
`SalesInvHeader.SendProfile` lines 7680/7699 →
|
||||||
|
`DocumentSendingProfile.Send`), `DocumentSendingProfile.Table.al` (table
|
||||||
|
60; `TrySendToPrinter`/`TrySendToEMail` lines 536/562,
|
||||||
|
`TrySendToPrinterVendor` line 552, `GetDefaultForCustomer` line 195,
|
||||||
|
`Send`/`SendVendor` lines 482/506) — 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,14 @@
|
||||||
|
enumextension 50100 "Sample Price Source Ext" extends "Price Source Type"
|
||||||
|
{
|
||||||
|
value(50100; "Sample.LoyaltyTier")
|
||||||
|
{
|
||||||
|
Caption = 'Loyalty Tier';
|
||||||
|
Implementation = "Price Source" = "Price Source - Customer", "Price Source Group" = "Price Source Group - Customer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WRONG: no matching value was added to "Sales Price Source Type" (or the
|
||||||
|
// purchase/job equivalents). "Sample.LoyaltyTier" compiles, installs, and
|
||||||
|
// is a real value on "Price Source Type" - it just never appears as an
|
||||||
|
// Applies-to Type option on the Sales Price List page, because that page
|
||||||
|
// is driven by the separate subset enum, not the base one.
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
enumextension 50100 "Sample Price Source Ext" extends "Price Source Type"
|
||||||
|
{
|
||||||
|
value(50100; "Sample.LoyaltyTier")
|
||||||
|
{
|
||||||
|
Caption = 'Loyalty Tier';
|
||||||
|
Implementation = "Price Source" = "Price Source - Customer", "Price Source Group" = "Price Source Group - Customer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enumextension 50101 "Sample Sales Price Source Ext" extends "Sales Price Source Type"
|
||||||
|
{
|
||||||
|
// Same numeric ID (50100) as the Price Source Type value above. That
|
||||||
|
// match is what makes "Sample.LoyaltyTier" show up as a selectable
|
||||||
|
// Applies-to Type on an actual sales price list.
|
||||||
|
value(50100; "Sample.LoyaltyTier")
|
||||||
|
{
|
||||||
|
Caption = 'Loyalty Tier';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: data-modeling
|
||||||
|
keywords: [price-calculation, price-source, price-source-type, enumextension, pricing]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Extend Price Source Type and its matching document subset enum together, with the same ID
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
`enum 7003 "Price Source Type"` (`implements "Price Source", "Price Source
|
||||||
|
Group"`) is the base list of who a price can apply to — Customer, Vendor,
|
||||||
|
Customer Price Group, Campaign, and so on. It is not, by itself, what
|
||||||
|
drives the "Applies-to Type" field on an actual sales, purchase, or job
|
||||||
|
price list. Each document area has its own subset enum —
|
||||||
|
`enum 7006 "Sales Price Source Type"`, the equivalent purchase and job
|
||||||
|
enums — and these are what the price list pages actually expose. Every
|
||||||
|
value the two enums share today uses the identical numeric ID: `All
|
||||||
|
Customers`/`Customer`/`Customer Price Group`/`Customer Disc.
|
||||||
|
Group`/`Campaign`/`Contact` are 10/11/12/13/50/51 in both `Price Source
|
||||||
|
Type` and `Sales Price Source Type`.
|
||||||
|
|
||||||
|
Adding a new value to `Price Source Type` alone does nothing for a sales
|
||||||
|
price list: the base enum and the document subset enum are two separate
|
||||||
|
extensible enums, linked only by convention, not by any platform
|
||||||
|
mechanism that keeps their IDs in sync. Give the new value a different ID
|
||||||
|
in each enum, or extend only the base enum, and the source is real and
|
||||||
|
selectable in some contexts (the base enum is used elsewhere, such as
|
||||||
|
the generic `Price Source` table) but absent from the specific document
|
||||||
|
price list a developer actually tested against.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
|
||||||
|
When a new price source should be usable in a sales, purchase, or job
|
||||||
|
price list, extend `Price Source Type` and the matching document subset
|
||||||
|
enum (`Sales Price Source Type`, `Purchase Price Source Type`, `Job Price
|
||||||
|
Source Type`) together, using the identical numeric ID in both.
|
||||||
|
|
||||||
|
See sample: `extend-price-source-type-must-sync-document-subset-enum.good.al`.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
|
||||||
|
Extending `Price Source Type` with a new value intended for sales price
|
||||||
|
lists, without extending `Sales Price Source Type` with a value of the
|
||||||
|
same ID — or giving it a different ID. Either way, the new source is
|
||||||
|
absent from the "Applies-to Type" options on an actual sales price list,
|
||||||
|
with no error anywhere: the base enum extension compiles and installs
|
||||||
|
cleanly on its own.
|
||||||
|
|
||||||
|
See sample: `extend-price-source-type-must-sync-document-subset-enum.bad.al`.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
BCApps (`src/Layers/W1/BaseApp/`): `Pricing/Source/PriceSourceType.Enum.al`
|
||||||
|
(`enum 7003 "Price Source Type"`, values `10/11/12/13/50/51` for `All
|
||||||
|
Customers`/`Customer`/`Customer Price Group`/`Customer Disc.
|
||||||
|
Group`/`Campaign`/`Contact`) and `Sales/Pricing/SalesPriceSourceType.Enum.al`
|
||||||
|
(`enum 7006 "Sales Price Source Type"`, the same six values at the same
|
||||||
|
six IDs). Microsoft Learn, "Extending Price Calculations": "The Price
|
||||||
|
Source Type enum implements the Applies-to Type field in the header of
|
||||||
|
the price list. Additionally, the Sales Price Source Type, Purchase Price
|
||||||
|
Source Type, and Job Price Source Type are subsets of the Price Source
|
||||||
|
Type enum... For compatibility, the new value must have the same ID in
|
||||||
|
both enums."
|
||||||
|
(https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-extending-best-price-calculations)
|
||||||
|
|
@ -28,11 +28,20 @@ codeunit 50100 "Sample Report Selection Install"
|
||||||
begin
|
begin
|
||||||
ReportSelections.InsertRecord(
|
ReportSelections.InsertRecord(
|
||||||
"Report Selection Usage"::"Sample.SettlementDoc", '1', Report::"Sample Settlement Document");
|
"Report Selection Usage"::"Sample.SettlementDoc", '1', Report::"Sample Settlement Document");
|
||||||
// Registration ends here. No subscriber added to
|
// Registration ends here. No enumextension was added to
|
||||||
// OnAfterFilterCustomerUsageReportSelections / OnAfterFilterVendorUsageReportSelections
|
// "Custom Report Selection Sales" (or "Report Selection Usage
|
||||||
// - the tenant-wide default works, but "Copy from Report Selection"
|
// Vendor"), and no subscriber was added to
|
||||||
// on the Document Layouts page never lists this usage value, so a
|
// OnAfterOnMapTableUsageValueToPageValue, OnValidateUsage2OnCaseElse,
|
||||||
// per-account override can only be entered by hand, if a user even
|
// or OnAfterFilterCustomerUsageReportSelections /
|
||||||
// knows to look for it.
|
// OnAfterFilterVendorUsageReportSelections.
|
||||||
|
//
|
||||||
|
// The tenant-wide default works, so the gap isn't visible in
|
||||||
|
// testing - but on the Document Layouts page for a specific
|
||||||
|
// customer or vendor: an existing row for this usage shows blank in
|
||||||
|
// the Usage column (no map event), a user cannot pick this usage
|
||||||
|
// from the Usage dropdown at all (no validate event and no
|
||||||
|
// page-facing enum value to pick), and "Copy from Report Selection"
|
||||||
|
// never lists it either (no filter event). No error, no visible
|
||||||
|
// sign that anything is missing.
|
||||||
end;
|
end;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,19 @@ enumextension 50100 "Sample Report Selection Usage Ext" extends "Report Selectio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This document is only ever issued to a customer, so only the customer-side
|
||||||
|
// page-facing enum is extended - not the vendor-side one too. This mirrors
|
||||||
|
// BCApps' ReportSelectionHandlerCZZ, which extends "Custom Report Selection
|
||||||
|
// Sales" for its customer-only usages and "Report Selection Usage Vendor"
|
||||||
|
// for its vendor-only usages, never both for the same one-sided value.
|
||||||
|
enumextension 50101 "Sample Cust. Rep. Sel. Sales Ext" extends "Custom Report Selection Sales"
|
||||||
|
{
|
||||||
|
value(50100; "Sample.SettlementDoc")
|
||||||
|
{
|
||||||
|
Caption = 'Sample Settlement Document';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
report 50100 "Sample Settlement Document"
|
report 50100 "Sample Settlement Document"
|
||||||
{
|
{
|
||||||
UsageCategory = ReportsAndAnalysis;
|
UsageCategory = ReportsAndAnalysis;
|
||||||
|
|
@ -33,16 +46,34 @@ codeunit 50100 "Sample Report Selection Install"
|
||||||
|
|
||||||
codeunit 50101 "Sample Report Selection Subscribers"
|
codeunit 50101 "Sample Report Selection Subscribers"
|
||||||
{
|
{
|
||||||
// Appends to whatever the standard filter already contains, following
|
// Customer-only document: all three subscribers below are on
|
||||||
// the real BCApps pattern in ReportSelectionHandlerCZC.Codeunit.al.
|
// "Customer Report Selections" only. There are no matching subscribers
|
||||||
[EventSubscriber(ObjectType::Page, Page::"Customer Report Selections", 'OnAfterFilterCustomerUsageReportSelections', '', false, false)]
|
// on "Vendor Report Selections" - subscribing there too would be the
|
||||||
local procedure AddSampleUsageOnAfterFilterCustomerUsageReportSelections(var ReportSelections: Record "Report Selections")
|
// overbroad mistake this sample avoids (see the .bad.al companion and
|
||||||
|
// the article's Anti Pattern #2).
|
||||||
|
|
||||||
|
// 1) Map: lets an existing row display in the Usage column instead of
|
||||||
|
// showing blank.
|
||||||
|
[EventSubscriber(ObjectType::Page, Page::"Customer Report Selections", 'OnAfterOnMapTableUsageValueToPageValue', '', false, false)]
|
||||||
|
local procedure AddSampleUsageOnAfterOnMapTableUsageValueToPageValue(var Usage2: Enum "Custom Report Selection Sales"; CustomReportSelection: Record "Custom Report Selection")
|
||||||
begin
|
begin
|
||||||
ReportSelections.SetFilter(Usage, GetUsageFilter(ReportSelections));
|
if CustomReportSelection.Usage = "Report Selection Usage"::"Sample.SettlementDoc" then
|
||||||
|
Usage2 := "Custom Report Selection Sales"::"Sample.SettlementDoc";
|
||||||
end;
|
end;
|
||||||
|
|
||||||
[EventSubscriber(ObjectType::Page, Page::"Vendor Report Selections", 'OnAfterFilterVendorUsageReportSelections', '', false, false)]
|
// 2) Validate: lets a user pick the new value from the Usage dropdown.
|
||||||
local procedure AddSampleUsageOnAfterFilterVendorUsageReportSelections(var ReportSelections: Record "Report Selections")
|
[EventSubscriber(ObjectType::Page, Page::"Customer Report Selections", 'OnValidateUsage2OnCaseElse', '', false, false)]
|
||||||
|
local procedure AddSampleUsageOnValidateUsage2OnCaseElse(var CustomReportSelection: Record "Custom Report Selection"; ReportUsage: Option)
|
||||||
|
begin
|
||||||
|
if ReportUsage = "Custom Report Selection Sales"::"Sample.SettlementDoc".AsInteger() then
|
||||||
|
CustomReportSelection.Usage := "Report Selection Usage"::"Sample.SettlementDoc";
|
||||||
|
end;
|
||||||
|
|
||||||
|
// 3) Filter: wires "Copy from Report Selection" - the piece most
|
||||||
|
// guidance stops at, appending to whatever filter already exists rather
|
||||||
|
// than replacing it.
|
||||||
|
[EventSubscriber(ObjectType::Page, Page::"Customer Report Selections", 'OnAfterFilterCustomerUsageReportSelections', '', false, false)]
|
||||||
|
local procedure AddSampleUsageOnAfterFilterCustomerUsageReportSelections(var ReportSelections: Record "Report Selections")
|
||||||
begin
|
begin
|
||||||
ReportSelections.SetFilter(Usage, GetUsageFilter(ReportSelections));
|
ReportSelections.SetFilter(Usage, GetUsageFilter(ReportSelections));
|
||||||
end;
|
end;
|
||||||
|
|
|
||||||
|
|
@ -7,73 +7,93 @@ countries: [w1]
|
||||||
application-area: [all]
|
application-area: [all]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Register a new document type through Report Selections, and extend the Document Layouts filter
|
# Register a new document type through Report Selections, and wire it into Document Layouts correctly
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
A custom document that needs to be printed or emailed should be registered
|
A custom document that needs printing/emailing should be registered
|
||||||
through `table 77 "Report Selections"`, not given its own bespoke
|
through `table 77 "Report Selections"`. `enum 77 "Report Selection Usage"`
|
||||||
report/layout lookup. `enum 77 "Report Selection Usage"` is
|
is `Extensible = true` for exactly this: add a value via `enumextension`,
|
||||||
`Extensible = true` specifically so a new document type can add its own
|
then `ReportSelections.InsertRecord(Usage, Sequence, ReportID)` for a
|
||||||
usage value via an `enumextension`, then register a default report for it
|
tenant-wide default — the mechanism every standard document uses.
|
||||||
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
|
That alone does not make the value usable in "Document Layouts"
|
||||||
free: `table 9657 "Custom Report Selection"` (surfaced as the "Document
|
(`page 9657 "Customer Report Selections"` / `page 9658 "Vendor Report
|
||||||
Layouts" action on the Customer and Vendor cards) lets one specific
|
Selections"`, table 9657 "Custom Report Selection"). Both pages hide
|
||||||
account override both the report and the layout, and the platform's
|
`enum 77` behind their own page-facing enum — `enum 9657 "Custom Report
|
||||||
lookup checks that table first before falling back to the tenant-wide
|
Selection Sales"` (customer) / `enum 9658 "Report Selection Usage Vendor"`
|
||||||
default. But the "Copy from Report Selection" action on the Document
|
(vendor) — in a field named `Usage2`. A new value stays invisible there
|
||||||
Layouts pages — the convenience button a user actually uses to seed a
|
until that page enum is extended too and three events are handled:
|
||||||
per-account override — filters to a **hardcoded** list of usage values
|
`OnAfterOnMapTableUsageValueToPageValue` / `OnMapTableUsageValueToPage
|
||||||
(`FilterCustomerUsageReportSelections`/`FilterVendorUsageReportSelections`
|
ValueOnCaseElse` (Usage column display), `OnValidateUsage2OnCaseElse`
|
||||||
on `page 9657 "Customer Report Selections"`/`page 9658 "Vendor Report
|
(picking it from the dropdown), and `OnAfterFilterCustomerUsageReport
|
||||||
Selections"`). A new custom usage value is not included automatically. Both
|
Selections` / `OnAfterFilterVendorUsageReportSelections` (the **"Copy from
|
||||||
pages publish `OnAfterFilterCustomerUsageReportSelections(var
|
Report Selection"** action only — a hardcoded-list filter, nothing more).
|
||||||
ReportSelections: Record "Report Selections")` /
|
|
||||||
`OnAfterFilterVendorUsageReportSelections(...)` for exactly this reason —
|
Which side(s) need this depends on the counterparty the document actually
|
||||||
real BCApps localization apps (e.g. the Czech Compensation localization,
|
applies to — not "always both." BCApps' `ReportSelectionHandlerCZZ`
|
||||||
`ReportSelectionHandlerCZC.Codeunit.al`) subscribe to both events and
|
(Advance Payments) partitions strictly: `"Sales Advance..."` usages get
|
||||||
extend the filter with `StrSubstNo('%1|%2', ReportSelections.GetFilter(Usage),
|
only the customer-side triad, `"Purchase Advance..."` only the vendor-side
|
||||||
UsageFilter)`, appending to whatever filter already exists rather than
|
triad. `ReportSelectionHandlerCZC` (Compensation) subscribes both sides —
|
||||||
replacing it.
|
legitimately, since that document posts to both ledgers, not by default.
|
||||||
|
|
||||||
## Best Practice
|
## Best Practice
|
||||||
|
|
||||||
Add the new usage value via `enumextension ... extends "Report Selection
|
1. Add the usage value (`enumextension ... extends "Report Selection
|
||||||
Usage"`, register a tenant-wide default row with
|
Usage"`) and register the tenant-wide default.
|
||||||
`ReportSelections.InsertRecord(...)`, and subscribe to both
|
2. Decide which counterparty(ies) apply — customer, vendor, or both.
|
||||||
`OnAfterFilterCustomerUsageReportSelections` and
|
3. For each applicable side, extend the matching page enum
|
||||||
`OnAfterFilterVendorUsageReportSelections` — even if the document only
|
(`"Custom Report Selection Sales"` / `"Report Selection Usage Vendor"`)
|
||||||
ever applies to one counterparty side — appending to the existing filter
|
and subscribe to that page's map, validate, and filter events —
|
||||||
rather than overwriting it. Treat the registration and the filter
|
appending with `StrSubstNo('%1|%2', ReportSelections.GetFilter(Usage),
|
||||||
subscription as one inseparable step: shipping one without the other
|
UsageFilter)`, never overwriting.
|
||||||
leaves per-account layout customization silently unreachable through the
|
4. Do not subscribe the other side for a one-sided document: skip the
|
||||||
standard UI.
|
triad and the value is unreachable in Document Layouts; wire both sides
|
||||||
|
needlessly and the picker is cluttered with a value that never applies.
|
||||||
|
|
||||||
See sample: `extend-report-selection-usage-for-new-document-types.good.al`.
|
See sample: `extend-report-selection-usage-for-new-document-types.good.al`
|
||||||
|
(customer-only document — only the customer-side enum and triad added).
|
||||||
|
|
||||||
## Anti Pattern
|
## Anti Pattern
|
||||||
|
|
||||||
Adding a new `Report Selection Usage` value and registering a default
|
1. Register the usage value but add no page-enum extension and no
|
||||||
report, but never subscribing to the filter events. The tenant-wide
|
subscribers. Works via the tenant-wide default, so it's invisible in
|
||||||
default works, so the gap isn't visible in testing — but a user who opens
|
testing — but Document Layouts shows the value's rows blank, can't offer
|
||||||
"Document Layouts" on a specific customer or vendor and clicks "Copy from
|
it in the Usage dropdown, and "Copy from Report Selection" never lists
|
||||||
Report Selection" to start a per-account override will never see the new
|
it. See sample: `extend-report-selection-usage-for-new-document-types.bad.al`.
|
||||||
document type in the list, with no error and no visible sign that
|
2. Subscribe both counterparties' triads for a one-sided document. This is
|
||||||
anything is missing.
|
the overbroad default Jesper Schulz-Wedde's review caught: it
|
||||||
|
contradicts how `ReportSelectionHandlerCZZ` actually partitions its
|
||||||
See sample: `extend-report-selection-usage-for-new-document-types.bad.al`.
|
usages, and clutters the other counterparty's picker with a value that
|
||||||
|
will never resolve a report there.
|
||||||
|
|
||||||
## Source
|
## Source
|
||||||
|
|
||||||
BCApps `ReportSelections.Table.al` (table 77, `InsertRecord` at line 344),
|
`ReportSelections.Table.al` (table 77, `InsertRecord` line 344),
|
||||||
`ReportSelectionUsage.Enum.al` (enum 77, `Extensible = true`),
|
`ReportSelectionUsage.Enum.al` (enum 77, `Extensible = true`),
|
||||||
`CustomReportSelection.Table.al` (table 9657), `CustomerReportSelections.Page.al`
|
`CustomReportSelection.Table.al` (table 9657) — all under
|
||||||
(page 9657, `FilterCustomerUsageReportSelections` and
|
`src/Layers/W1/BaseApp/Foundation/Reporting/`.
|
||||||
`OnAfterFilterCustomerUsageReportSelections` at line 335),
|
|
||||||
`VendorReportSelections.Page.al` (page 9658, `OnAfterFilterVendorUsageReportSelections`
|
`CustomerReportSelections.Page.al` (page 9657, `.../Sales/Setup/`):
|
||||||
at line 296) — all under `src/Layers/W1/BaseApp/`. Real subscriber
|
`FilterCustomerUsageReportSelections` (307),
|
||||||
precedent: `src/Apps/CZ/CompensationLocalization/app/Src/Codeunits/ReportSelectionHandlerCZC.Codeunit.al`,
|
`OnAfterFilterCustomerUsageReportSelections` (335),
|
||||||
`GetUsageFilter` (line 104) and both event subscribers (lines 38, 66).
|
`OnAfterOnMapTableUsageValueToPageValue` (325),
|
||||||
|
`OnValidateUsage2OnCaseElse` (330); enum `CustomReportSelectionSales.Enum.al`
|
||||||
|
(9657, same folder). `VendorReportSelections.Page.al` (page 9658,
|
||||||
|
`.../Purchases/Setup/`): `FilterVendorUsageReportSelections` (281),
|
||||||
|
`OnAfterFilterVendorUsageReportSelections` (296),
|
||||||
|
`OnMapTableUsageValueToPageValueOnCaseElse` (301),
|
||||||
|
`OnValidateUsage2OnCaseElse` (306); enum `ReportSelectionUsageVendor.Enum.al`
|
||||||
|
(9658, same folder).
|
||||||
|
|
||||||
|
Partitioning precedent: `.../AdvancePaymentsLocalization/app/Src/Codeunits/
|
||||||
|
ReportSelectionHandlerCZZ.Codeunit.al` (codeunit 31420) — customer-only
|
||||||
|
triad (47, 58, 69) for `"Sales Advance..."`, vendor-only triad (80, 91,
|
||||||
|
102) for `"Purchase Advance..."`, never both for one usage. Enum
|
||||||
|
extensions: `CustomReportSelSalesCZZ.EnumExt.al` (31008), `ReportSelUsage
|
||||||
|
VendorCZZ.EnumExt.al` (11708).
|
||||||
|
|
||||||
|
Contrast (two-sided): `.../CompensationLocalization/app/Src/Codeunits/
|
||||||
|
ReportSelectionHandlerCZC.Codeunit.al` (codeunit 11765) subscribes both
|
||||||
|
triads (16/27/38, 44/55/66) for `"Compensation CZC"`, which posts to both
|
||||||
|
a customer and a vendor ledger. (Lines as of `main`; may shift by version.)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
tableextension 50105 "Sample Sales Line Ext" extends "Sales Line"
|
||||||
|
{
|
||||||
|
fields
|
||||||
|
{
|
||||||
|
// WRONG: no OnValidate trigger. The field is registered as a
|
||||||
|
// price source below via OnAfterAddSources, so new lines price
|
||||||
|
// correctly - but changing this field on an existing line never
|
||||||
|
// calls UpdateUnitPriceByField, so the unit price silently keeps
|
||||||
|
// its old value.
|
||||||
|
field(50100; "Sample Loyalty Customer No."; Code[20])
|
||||||
|
{
|
||||||
|
Caption = 'Sample Loyalty Customer No.';
|
||||||
|
TableRelation = Customer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeunit 50106 "Sample Sales Line Price Sources"
|
||||||
|
{
|
||||||
|
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales Line - Price", 'OnAfterAddSources', '', false, false)]
|
||||||
|
local procedure AddLoyaltyCustomerSource(SalesHeader: Record "Sales Header"; SalesLine: Record "Sales Line"; PriceType: Enum "Price Type"; var PriceSourceList: Codeunit "Price Source List")
|
||||||
|
begin
|
||||||
|
PriceSourceList.Add(Enum::"Price Source Type"::Customer, SalesLine."Sample Loyalty Customer No.");
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
tableextension 50105 "Sample Sales Line Ext" extends "Sales Line"
|
||||||
|
{
|
||||||
|
fields
|
||||||
|
{
|
||||||
|
field(50100; "Sample Loyalty Customer No."; Code[20])
|
||||||
|
{
|
||||||
|
Caption = 'Sample Loyalty Customer No.';
|
||||||
|
TableRelation = Customer;
|
||||||
|
|
||||||
|
trigger OnValidate()
|
||||||
|
begin
|
||||||
|
// Second half of the wiring: without this call, changing
|
||||||
|
// the field on an existing line never re-runs price
|
||||||
|
// calculation, even though the source is already a known
|
||||||
|
// candidate via OnAfterAddSources below.
|
||||||
|
UpdateUnitPriceByField(FieldNo("Sample Loyalty Customer No."));
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeunit 50106 "Sample Sales Line Price Sources"
|
||||||
|
{
|
||||||
|
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales Line - Price", 'OnAfterAddSources', '', false, false)]
|
||||||
|
local procedure AddLoyaltyCustomerSource(SalesHeader: Record "Sales Header"; SalesLine: Record "Sales Line"; PriceType: Enum "Price Type"; var PriceSourceList: Codeunit "Price Source List")
|
||||||
|
begin
|
||||||
|
PriceSourceList.Add(Enum::"Price Source Type"::Customer, SalesLine."Sample Loyalty Customer No.");
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: data-modeling
|
||||||
|
keywords: [price-calculation, price-source, onafteraddsources, recalculation, pricing]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
---
|
||||||
|
|
||||||
|
# A new price source needs both a calculation candidate and a recalculation trigger
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Making a custom field usable as a price source on a sales line is two
|
||||||
|
separate, independent pieces of wiring, and doing only one produces a
|
||||||
|
line that looks like it's using the new source without ever actually
|
||||||
|
being priced by it. `codeunit "Sales Line - Price"` publishes
|
||||||
|
`OnAfterAddSources(SalesHeader: Record "Sales Header"; SalesLine: Record
|
||||||
|
"Sales Line"; PriceType: Enum "Price Type"; var PriceSourceList: Codeunit
|
||||||
|
"Price Source List")` — subscribing here and calling
|
||||||
|
`PriceSourceList.Add(SourceType, SourceNo)` makes the source a candidate
|
||||||
|
the calculation considers. But nothing about that subscription causes
|
||||||
|
the price to be *recalculated* when the source field's value changes on
|
||||||
|
an existing line. That's the second, separate piece: `Sales Line`'s own
|
||||||
|
`procedure UpdateUnitPriceByField(CalledByFieldNo: Integer)` must be
|
||||||
|
called from the source field's own trigger — the same way Microsoft's
|
||||||
|
own Location example is wired from a `Sales Line` validation event, not
|
||||||
|
from the price source registration itself.
|
||||||
|
|
||||||
|
Add the source without wiring recalculation, and the failure hides
|
||||||
|
easily: a *new* line still prices correctly, because the field already
|
||||||
|
holds its value when calculation first runs on insert. The gap only
|
||||||
|
shows up when someone *changes* the source field's value on an existing
|
||||||
|
line — the price silently keeps its old value until something unrelated
|
||||||
|
happens to trigger recalculation.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
|
||||||
|
Wire both halves together whenever a field becomes a price source: an
|
||||||
|
`OnAfterAddSources` subscriber that adds it via `PriceSourceList.Add`, and
|
||||||
|
a trigger on the field itself (its own `OnValidate`, or a matching
|
||||||
|
`OnAfterValidate` integration event) that calls
|
||||||
|
`SalesLine.UpdateUnitPriceByField(SalesLine.FieldNo(<TheField>))`.
|
||||||
|
|
||||||
|
See sample: `new-price-source-must-add-candidate-and-trigger-recalculation.good.al`.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
|
||||||
|
Subscribing to `OnAfterAddSources` to register a custom field as a price
|
||||||
|
source, without also triggering recalculation from that field's own
|
||||||
|
validation. The field is a genuine, working calculation candidate — new
|
||||||
|
lines price correctly — but editing the field on an existing line leaves
|
||||||
|
the unit price stale, with nothing to indicate why.
|
||||||
|
|
||||||
|
See sample: `new-price-source-must-add-candidate-and-trigger-recalculation.bad.al`.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
BCApps (`src/Layers/W1/BaseApp/`): `Sales/Pricing/SalesLinePrice.Codeunit.al`
|
||||||
|
(`local procedure OnAfterAddSources(SalesHeader: Record "Sales Header";
|
||||||
|
SalesLine: Record "Sales Line"; PriceType: Enum "Price Type"; var
|
||||||
|
PriceSourceList: Codeunit "Price Source List")`); `Pricing/Source/PriceSourceList.Codeunit.al`
|
||||||
|
(`procedure Add(SourceType: Enum "Price Source Type"; SourceNo: Code[20])`);
|
||||||
|
`Sales/Document/SalesLine.Table.al` (`procedure
|
||||||
|
UpdateUnitPriceByField(CalledByFieldNo: Integer)`).
|
||||||
|
|
||||||
|
Microsoft Learn, "Extending Price Calculations" (Location example): "To
|
||||||
|
recalculate the price, we can subscribe to events that pass the sales
|
||||||
|
line by reference... We'll call the UpdateUnitPriceByLocationCode()
|
||||||
|
method, which is a simplified version of the UpdateUnitPriceByField()
|
||||||
|
method... To add the location in the source list for price calculations,
|
||||||
|
we'll subscribe to the OnAfterAddSources event of Codeunit 'Sales Line -
|
||||||
|
Price,' and add the Location Code as a source."
|
||||||
|
(https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-extending-best-price-calculations)
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
report 50110 "Sample Item Barcode Label"
|
||||||
|
{
|
||||||
|
UsageCategory = Tasks;
|
||||||
|
ApplicationArea = All;
|
||||||
|
Caption = 'Sample Item Barcode Label';
|
||||||
|
|
||||||
|
dataset
|
||||||
|
{
|
||||||
|
dataitem(Item; Item)
|
||||||
|
{
|
||||||
|
column(No_; "No.") { }
|
||||||
|
column(Barcode; BarcodeText) { }
|
||||||
|
|
||||||
|
trigger OnAfterGetRecord()
|
||||||
|
begin
|
||||||
|
// WRONG: hand-rolled "encoding" instead of the Barcode
|
||||||
|
// module's provider/encoder API. This produces a string
|
||||||
|
// that looks like a Code 39 barcode (asterisk delimiters)
|
||||||
|
// but carries none of the platform's actual character-set
|
||||||
|
// or checksum handling - wrong regardless of which font
|
||||||
|
// is applied to it in the layout.
|
||||||
|
BarcodeText := '*' + "No." + '*';
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var
|
||||||
|
BarcodeText: Text;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
report 50110 "Sample Item Barcode Label"
|
||||||
|
{
|
||||||
|
UsageCategory = Tasks;
|
||||||
|
ApplicationArea = All;
|
||||||
|
Caption = 'Sample Item Barcode Label';
|
||||||
|
|
||||||
|
dataset
|
||||||
|
{
|
||||||
|
dataitem(Item; Item)
|
||||||
|
{
|
||||||
|
column(No_; "No.") { }
|
||||||
|
column(Barcode; BarcodeText) { }
|
||||||
|
|
||||||
|
trigger OnAfterGetRecord()
|
||||||
|
var
|
||||||
|
BarcodeFontProvider: Interface "Barcode Font Provider";
|
||||||
|
begin
|
||||||
|
BarcodeFontProvider := Enum::"Barcode Font Provider"::IDAutomation1D;
|
||||||
|
BarcodeFontProvider.ValidateInput("No.", BarcodeSymbology);
|
||||||
|
BarcodeText := BarcodeFontProvider.EncodeFont("No.", BarcodeSymbology);
|
||||||
|
end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var
|
||||||
|
BarcodeSymbology: Enum "Barcode Symbology";
|
||||||
|
BarcodeText: Text;
|
||||||
|
|
||||||
|
trigger OnInitReport()
|
||||||
|
begin
|
||||||
|
BarcodeSymbology := Enum::"Barcode Symbology"::Code39;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Layout requirement (can't be enforced in AL, so it's stated here):
|
||||||
|
// the Barcode column's text box must use the real, purchased font
|
||||||
|
// name - IDAutomationHC39M for Code 39 - never an evaluation name
|
||||||
|
// like "IDAutomationSHC39M Demo". Per Microsoft Learn, using the
|
||||||
|
// evaluation name in a Business Central online production
|
||||||
|
// environment means "the barcode won't render" at all.
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
---
|
||||||
|
bc-version: [all]
|
||||||
|
domain: data-modeling
|
||||||
|
keywords: [barcode, qr-code, barcode-font-provider, report-layout, saas, idautomation]
|
||||||
|
technologies: [al]
|
||||||
|
countries: [w1]
|
||||||
|
application-area: [all]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Generate report barcodes through the Barcode module, with the production font name
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Business Central's barcode support lives in the System Application's
|
||||||
|
`Barcode` module (`src/System Application/App/Barcode`), not in a
|
||||||
|
project's own code: `interface "Barcode Font Provider"` /
|
||||||
|
`"Barcode Font Provider 2D"`, `enum "Barcode Symbology"` /
|
||||||
|
`"Barcode Symbology 2D"` (Code39, Code128, EAN-13, QR-Code, Data Matrix,
|
||||||
|
and more), and built-in implementations
|
||||||
|
(`codeunit 9215 "IDAutomation 1D Provider"`,
|
||||||
|
`codeunit 9221 "IDAutomation 2D Provider"`). A report encodes a data
|
||||||
|
string into a barcode string via this API; the layout then displays that
|
||||||
|
string using a barcode *font*.
|
||||||
|
|
||||||
|
On Business Central online, this is available with no setup at all:
|
||||||
|
"With Business Central online, the IDAutomation fonts are automatically
|
||||||
|
available as part of the service. So you can start adding barcodes to
|
||||||
|
reports right away." (Microsoft Learn, "Adding Barcodes to Reports") —
|
||||||
|
unlike on-premises, where the fonts must be purchased and installed on
|
||||||
|
the server.
|
||||||
|
|
||||||
|
That ease hides a SaaS-specific trap in the one manual step the API
|
||||||
|
doesn't cover: naming the actual font in the report layout. IDAutomation
|
||||||
|
ships both a purchased font and a same-looking evaluation font per
|
||||||
|
version (Code 39: `IDAutomationHC39M` purchased vs.
|
||||||
|
`IDAutomationSHC39M Demo` evaluation). Per Microsoft Learn ("Barcode
|
||||||
|
Fonts with Business Central Online"): "When you're applying barcode font
|
||||||
|
in the report layout for a Business Central online production
|
||||||
|
environment, be sure to use the purchased font name; not the evaluation
|
||||||
|
font name. If you use the evaluation font name, the barcode won't
|
||||||
|
render." Getting the font name wrong doesn't distort the barcode — it
|
||||||
|
produces nothing, in a step that lives in the layout file, not in AL, so
|
||||||
|
no compiler or reviewer catches it by reading the report object. Nothing
|
||||||
|
in the cited documentation says what an evaluation font name does outside
|
||||||
|
a production environment — the claim here is scoped exactly as
|
||||||
|
Microsoft states it: wrong in production, full stop.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
|
||||||
|
Encode through the real API — declare the provider via its interface and
|
||||||
|
enum, then call `ValidateInput`/`EncodeFont` — and treat naming the
|
||||||
|
production font in the layout as an equally required part of the same
|
||||||
|
task, not an afterthought left to whoever happens to touch the `.docx`/
|
||||||
|
`.rdl` file. For a two-dimensional symbology other than Maxicode, the
|
||||||
|
font name to specify is literally `IDAutomation2D` (Maxicode itself uses
|
||||||
|
`IDAutomation2D MaxiCode`); for a one-dimensional symbology, use the
|
||||||
|
purchased version name for that specific font (e.g. `IDAutomationHC39M`
|
||||||
|
for Code 39), never a name containing `Demo`.
|
||||||
|
|
||||||
|
See sample: `report-barcodes-must-use-barcode-module-and-production-font-name.good.al`.
|
||||||
|
|
||||||
|
## Anti Pattern
|
||||||
|
|
||||||
|
Constructing a barcode string by hand — string concatenation, manual
|
||||||
|
delimiters — instead of going through the Barcode module's provider
|
||||||
|
interface. It can look right (asterisks around a value, resembling
|
||||||
|
Code 39) while carrying none of the platform's actual character-set
|
||||||
|
handling or checksum logic, so it's wrong regardless of which font is
|
||||||
|
later applied to it.
|
||||||
|
|
||||||
|
A second version of the same underlying mistake: encoding correctly
|
||||||
|
through the real API, but naming the evaluation font instead of the
|
||||||
|
purchased one in the layout. Both produce a report that looks complete
|
||||||
|
in review and testing and fails silently — the first because the encoded
|
||||||
|
data was never a real barcode, the second because Business Central
|
||||||
|
online refuses to render it at all.
|
||||||
|
|
||||||
|
See sample: `report-barcodes-must-use-barcode-module-and-production-font-name.bad.al`.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
BCApps System Application (`src/System Application/App/Barcode/src/`):
|
||||||
|
`Barcode Provider/Font/BarcodeFontProvider.Interface.al`
|
||||||
|
(`ValidateInput(InputText: Text; BarcodeSymbology: Enum "Barcode Symbology")`,
|
||||||
|
`EncodeFont(InputText: Text; BarcodeSymbology: Enum "Barcode Symbology"): Text`),
|
||||||
|
`Barcode Provider/Font/BarcodeFontProvider.Enum.al`
|
||||||
|
(`value(0; IDAutomation1D)`), `Barcode Provider/BarcodeSymbology.Enum.al`
|
||||||
|
(`value(100; Code39)`). Real BaseApp usage:
|
||||||
|
`src/Layers/W1/BaseApp/Inventory/Item/ItemGTINLabel.Report.al`
|
||||||
|
(`report 6625 "Item GTIN Label"`, lines 44-64).
|
||||||
|
|
||||||
|
Microsoft Learn: "Adding Barcodes to Reports"
|
||||||
|
(https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-report-add-barcodes)
|
||||||
|
and "Barcode Fonts with Business Central Online"
|
||||||
|
(https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-report-barcode-fonts)
|
||||||
|
— both quoted verbatim above.
|
||||||
|
|
@ -16,7 +16,7 @@ application-area: [all]
|
||||||
|
|
||||||
Reviews AL source changes against the `data-modeling` knowledge domain in BCQuality and emits a findings report. This is a leaf action skill: it invokes no sub-skills. It is one of the skills composed by `al-code-review`.
|
Reviews AL source changes against the `data-modeling` knowledge domain in BCQuality and emits a findings report. This is a leaf action skill: it invokes no sub-skills. It is one of the skills composed by `al-code-review`.
|
||||||
|
|
||||||
An orchestrator invokes this skill with either a `pr-diff` (the standard PR-review entry point) or a `file-path` (single-file review). Data-modeling findings are narrow by design — they apply when the diff touches setup or master tables, their card pages, primary keys, number-series assignment, block enforcement, or audit fields. The skill returns `not-applicable` when none of those apply.
|
An orchestrator invokes this skill with either a `pr-diff` (the standard PR-review entry point) or a `file-path` (single-file review). Data-modeling findings are narrow by design — they apply when the diff touches setup or master tables, their card pages, primary keys, number-series assignment, block enforcement, audit fields, document print/email/Post-and-Send actions, `Navigate` page subscribers, Report Selection registration or dispatch, price-calculation/price-source extensibility, `TransferFields`-based posting-cascade field mirroring, or barcode/report-layout font-provider usage. The skill returns `not-applicable` when none of those apply.
|
||||||
|
|
||||||
## Source
|
## Source
|
||||||
|
|
||||||
|
|
@ -37,9 +37,9 @@ Discard files that are not applicable. Retain conditionally applicable files (an
|
||||||
|
|
||||||
Narrow the relevant files to the subset that applies to the changes under review. For each relevant file, compute overlap against:
|
Narrow the relevant files to the subset that applies to the changes under review. For each relevant file, compute overlap against:
|
||||||
|
|
||||||
- The changed AL object names and types — especially `* Setup` singleton tables and Card pages, custom master tables, tableextensions that add master-data fields, and document or journal lines that reference a master.
|
- The changed AL object names and types — especially `* Setup` singleton tables and Card pages, custom master tables, tableextensions that add master-data fields, document or journal lines that reference a master, document pages/codeunits exposing print/email/Post-and-Send actions, codeunits subscribing to `Navigate`, enumextensions to `"Report Selection Usage"`/`"Price Calculation Handler"`/`"Price Source Type"`, and report objects that render barcodes.
|
||||||
- The changed fields, keys, triggers, and procedures, weighted toward `Primary Key`, `No.`, `No. Series`, `Blocked`, `Last Date Modified`, `OnInsert`, `OnModify`, `OnRename`, reference-field `OnValidate`, and posting validation.
|
- The changed fields, keys, triggers, and procedures, weighted toward `Primary Key`, `No.`, `No. Series`, `Blocked`, `Last Date Modified`, `OnInsert`, `OnModify`, `OnRename`, reference-field `OnValidate`, posting validation, and posting-cascade `TransferFields` calls.
|
||||||
- Tokens extracted from the diff that relate to data modeling (`setup`, `master`, `Primary Key`, `Code[10]`, `Code[20]`, `AutoIncrement`, `SystemId`, `No.`, `No. Series`, `NoSeriesManagement`, `Codeunit "No. Series"`, `GetNextNo`, `IsManual`, `TestManual`, `Blocked`, `TestField`, `Last Date Modified`, `Today`, `WorkDate`, `InsertAllowed`, `DeleteAllowed`, `PageType = Card`, `OnOpenPage`, `GetRecordOnce`, `OnInsert`, `OnModify`, `OnRename`, `TableRelation`, `tableextension`, `enumextension`, `Media`, `MediaSet`, `Item`, `Count`).
|
- Tokens extracted from the diff that relate to data modeling (`setup`, `master`, `Primary Key`, `Code[10]`, `Code[20]`, `AutoIncrement`, `SystemId`, `No.`, `No. Series`, `NoSeriesManagement`, `Codeunit "No. Series"`, `GetNextNo`, `IsManual`, `TestManual`, `Blocked`, `TestField`, `Last Date Modified`, `Today`, `WorkDate`, `InsertAllowed`, `DeleteAllowed`, `PageType = Card`, `OnOpenPage`, `GetRecordOnce`, `OnInsert`, `OnModify`, `OnRename`, `TableRelation`, `tableextension`, `enumextension`, `Media`, `MediaSet`, `Item`, `Count`, `TransferFields`, `Navigate`, `OnAfterFindRecords`, `OnBeforeShowRecords`, `Report Selections`, `Report Selection Usage`, `InsertRecord`, `Document Sending Profile`, `PrintForCust`, `PrintWithDialogForCust`, `PrintWithDialogForVend`, `SendEmailToCust`, `SendEmailToVendor`, `Report.RunModal`, `Report.Run`, `Price Calculation Handler`, `Price Calculation`, `OnFindSupportedSetup`, `Price Calculation Setup`, `Price Source Type`, `PriceSourceList`, `OnAfterAddSources`, `UpdateUnitPriceByField`, `Barcode Font Provider`, `Barcode Font Provider 2D`, `EncodeFont`, `ValidateInput`).
|
||||||
|
|
||||||
A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. When the diff contains no data-modeling changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files.
|
A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. When the diff contains no data-modeling changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files.
|
||||||
|
|
||||||
|
|
@ -55,8 +55,12 @@ The following targeted checks cover every current `data-modeling` article. Treat
|
||||||
- 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 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.
|
- 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 `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.
|
- 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.
|
||||||
- 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.
|
- 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.
|
||||||
|
- 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 `Type`/`Asset Type`, with `Method` and `Default := true` also set — `activate-new-price-calculation-handler-via-onfindsupportedsetup`.
|
||||||
|
- 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 calls `SalesLine.UpdateUnitPriceByField` to recalculate — `new-price-source-must-add-candidate-and-trigger-recalculation`.
|
||||||
|
- A report builds a barcode string by manual concatenation/delimiters instead of the Barcode module's `"Barcode Font Provider"`/`"Barcode Font Provider 2D"` interface (`ValidateInput`/`EncodeFont`), or an otherwise correctly encoded barcode's 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`.
|
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`.
|
||||||
|
|
||||||
|
|
@ -86,7 +90,7 @@ Outcome selection:
|
||||||
|
|
||||||
- `completed` — the skill evaluated every worklist item.
|
- `completed` — the skill evaluated every worklist item.
|
||||||
- `no-knowledge` — no applicable data-modeling knowledge survived filtering.
|
- `no-knowledge` — no applicable data-modeling knowledge survived filtering.
|
||||||
- `not-applicable` — the diff touches no setup/master table, page, key, numbering, block-check, or audit-field surface.
|
- `not-applicable` — the diff touches no setup/master table, page, key, numbering, block-check, or audit-field surface, and no document print/email/Post-and-Send action, `Navigate` subscriber, Report Selection registration/dispatch, price-calculation/price-source extensibility point, posting-cascade `TransferFields` mirroring, or barcode/report-font-provider usage.
|
||||||
- `partial` — a budget was hit before the worklist was exhausted.
|
- `partial` — a budget was hit before the worklist was exhausted.
|
||||||
- `failed` — an unrecoverable error occurred.
|
- `failed` — an unrecoverable error occurred.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,47 @@ foreach ($domain in $leafDomains) {
|
||||||
}
|
}
|
||||||
$caseList.Add($case) | Out-Null
|
$caseList.Add($case) | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($override -and ($override.PSObject.Properties.Name -contains 'additionalArticles')) {
|
||||||
|
foreach ($additionalArticleName in @($override.additionalArticles)) {
|
||||||
|
$additionalName = [string]$additionalArticleName
|
||||||
|
if ($additionalName.EndsWith('.md')) {
|
||||||
|
$additionalName = [System.IO.Path]::GetFileNameWithoutExtension($additionalName)
|
||||||
|
}
|
||||||
|
$additionalArticle = $articles | Where-Object BaseName -eq $additionalName | Select-Object -First 1
|
||||||
|
if (-not $additionalArticle) {
|
||||||
|
$articleExists = @(
|
||||||
|
foreach ($layer in $layers) {
|
||||||
|
$articleFile = Join-Path $Root "$($layer.Name)/knowledge/$domain/$additionalName.md"
|
||||||
|
if (Test-Path -LiteralPath $articleFile -PathType Leaf) {
|
||||||
|
$articleFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).Count -gt 0
|
||||||
|
if ($articleExists) {
|
||||||
|
$problems.Add("${domain}: additionalArticles entry does not have both .good.al and .bad.al companion samples: $additionalName.md") | Out-Null
|
||||||
|
} else {
|
||||||
|
$problems.Add("${domain}: additionalArticles entry does not exist: $additionalName.md") | Out-Null
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ($additionalArticle.BaseName -eq $selectedArticle.BaseName) {
|
||||||
|
$problems.Add("${domain}: additionalArticles entry duplicates the selected article: $additionalName") | Out-Null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$additionalArticlePath = [string]$additionalArticle.ArticlePath
|
||||||
|
$additionalSampleDirectory = (Split-Path -Parent $additionalArticlePath).Replace('\', '/')
|
||||||
|
foreach ($kind in 'bad', 'good') {
|
||||||
|
$additionalCase = [pscustomobject]@{
|
||||||
|
id = "$domain-$kind-$($additionalArticle.BaseName)"
|
||||||
|
domain = $domain
|
||||||
|
input = "$additionalSampleDirectory/$($additionalArticle.BaseName).$kind.al"
|
||||||
|
expected = if ($kind -eq 'bad') { @($additionalArticlePath) } else { @() }
|
||||||
|
}
|
||||||
|
$caseList.Add($additionalCase) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$cases = @($caseList)
|
$cases = @($caseList)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue