From 31206f6616ae9b7183da6af077a51c82bdbc21e4 Mon Sep 17 00:00:00 2001 From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com> Date: Thu, 24 Sep 2026 06:33:04 +0200 Subject: [PATCH] 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 --- evaluation/README.md | 2 +- evaluation/review-fixtures.json | 14 ++ ...on-handler-via-onfindsupportedsetup.bad.al | 15 ++ ...n-handler-via-onfindsupportedsetup.good.al | 25 ++++ ...lation-handler-via-onfindsupportedsetup.md | 82 ++++++++++ ...patch-must-not-bypass-report-selections.md | 10 +- ...ons-call-report-selections-directly.bad.al | 16 +- ...ns-call-report-selections-directly.good.al | 5 +- ...actions-call-report-selections-directly.md | 141 +++++++++--------- ...type-must-sync-document-subset-enum.bad.al | 14 ++ ...ype-must-sync-document-subset-enum.good.al | 19 +++ ...rce-type-must-sync-document-subset-enum.md | 68 +++++++++ ...ection-usage-for-new-document-types.bad.al | 21 ++- ...ction-usage-for-new-document-types.good.al | 45 +++++- ...-selection-usage-for-new-document-types.md | 130 +++++++++------- ...candidate-and-trigger-recalculation.bad.al | 25 ++++ ...andidate-and-trigger-recalculation.good.al | 29 ++++ ...add-candidate-and-trigger-recalculation.md | 74 +++++++++ ...ode-module-and-production-font-name.bad.al | 29 ++++ ...de-module-and-production-font-name.good.al | 40 +++++ ...barcode-module-and-production-font-name.md | 96 ++++++++++++ .../skills/review/al-data-modeling-review.md | 16 +- tools/Test-ReviewFixtures.ps1 | 41 +++++ 23 files changed, 801 insertions(+), 156 deletions(-) create mode 100644 microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.bad.al create mode 100644 microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.good.al create mode 100644 microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.md create mode 100644 microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.bad.al create mode 100644 microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.good.al create mode 100644 microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.md create mode 100644 microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.bad.al create mode 100644 microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.good.al create mode 100644 microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.md create mode 100644 microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.bad.al create mode 100644 microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.good.al create mode 100644 microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md diff --git a/evaluation/README.md b/evaluation/README.md index 7063baf..e29b428 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -2,7 +2,7 @@ The evaluation is convention-driven. The harness discovers every `/skills/review/al--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. diff --git a/evaluation/review-fixtures.json b/evaluation/review-fixtures.json index f5c7046..88b2414 100644 --- a/evaluation/review-fixtures.json +++ b/evaluation/review-fixtures.json @@ -13,6 +13,20 @@ "breaking-changes": { "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": { "article": "reset-ishandled-only-when-the-value-can-carry-over" }, diff --git a/microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.bad.al b/microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.bad.al new file mode 100644 index 0000000..a3f7c0f --- /dev/null +++ b/microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.bad.al @@ -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. diff --git a/microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.good.al b/microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.good.al new file mode 100644 index 0000000..c36928b --- /dev/null +++ b/microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.good.al @@ -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; +} diff --git a/microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.md b/microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.md new file mode 100644 index 0000000..f95ad4d --- /dev/null +++ b/microsoft/knowledge/data-modeling/activate-new-price-calculation-handler-via-onfindsupportedsetup.md @@ -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) diff --git a/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.md b/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.md index 00fafd7..6a96a3a 100644 --- a/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.md +++ b/microsoft/knowledge/data-modeling/custom-document-dispatch-must-not-bypass-report-selections.md @@ -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 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 +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 Customer/Vendor card) lets one specific account override the report or 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 -BCApps `ReportSelections.Table.al` (table 77 — fields 19–26 for email -attachment/body configuration; `SendEmailToCust`/`PrintWithDialogForCust` -as the registry-backed dispatch entry points) and +BCApps `ReportSelections.Table.al` (table 77 — field 7, +`"Custom Report Layout Code"`; 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/`. diff --git a/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.bad.al b/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.bad.al index b37ee9d..49c5d45 100644 --- a/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.bad.al +++ b/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.bad.al @@ -19,12 +19,16 @@ page 50101 "Sample Settlement Document Card" 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. + // part of a combined Post-and-Send action - but this + // loads the customer's ACTUAL assigned profile (or the + // tenant default, if none is assigned - the same lookup + // Sales-Post and Send performs) and calls Send on it, so + // the outcome now silently depends on that profile. A + // 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( "Report Selection Usage"::"S.Invoice".AsInteger(), Rec, Rec."No.", Rec."No.", Rec.Name, Rec.FieldNo("No."), Rec.FieldNo("No.")); diff --git a/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.good.al b/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.good.al index a681dac..533057c 100644 --- a/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.good.al +++ b/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.good.al @@ -20,7 +20,10 @@ page 50101 "Sample Settlement Document Card" 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. + // 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( "Report Selection Usage"::"S.Invoice".AsInteger(), Rec, Rec."No.", Rec.Name, true, Rec."No."); diff --git a/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.md b/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.md index 9999095..337c096 100644 --- a/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.md +++ b/microsoft/knowledge/data-modeling/document-print-and-email-actions-call-report-selections-directly.md @@ -11,89 +11,90 @@ application-area: [all] ## 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. +`table 60 "Document Sending Profile"` is not a general gateway for every +print/email path — 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 actions are unaffected by any *configured* profile either +way: the unposted Sales Order's "Print Confirmation"/"Email +Confirmation" (`codeunit "Document-Print"`, +`PrintSalesOrder`/`EmailSalesHeader`) and the posted `Purch. Inv. +Header`'s `PrintRecords` call `Report Selections` literally directly +(`PrintWithDialogForCust`/`SendEmailToCust`/`PrintWithDialogForVend`), +while the posted `Sales Invoice Header`'s `PrintRecords`/`EmailRecords` +and the unposted `Purchase Header`'s `PrintRecords` go through +`DocumentSendingProfile.TrySendToPrinter`/`TrySendToEMail`/ +`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 -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 a genuinely configured profile changes the outcome, and that only +happens for the combined Post-and-Send flow: `Sales-Post and Send` loads +the customer's assigned profile (`Get(Customer."Document Sending +Profile")`, or the tenant default) before `Sales Invoice +Header.SendProfile` → `DocumentSendingProfile.Send`, which gates +`SendToPrinter`/`SendToEMail`/`SendToDisk` on whatever that record holds. -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. +Whether a document needs outbound distribution isn't determined by +Customer vs. Vendor, but by whether it's genuinely *outbound* to that +party: a posted Purchase Invoice records what a vendor already billed, +so the posted `Purch. Inv. Header` has only a bare `PrintRecords`; a +Purchase *Order* is still outbound before posting, so the rich +`SendProfile`/`SendRecords`/`PrintRecords` triplet lives there instead. ## Best Practice -For a document's own interactive Print/Email actions, call the relevant -`Report Selections` procedure directly — +For a document's own interactive Print/Email actions, either 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. +customer-facing document, `PrintWithDialogForVend`/`SendEmailToVendor` +for a vendor-facing one — or call one of `Document Sending Profile`'s +stateless `TrySendToPrinter`/`TrySendToEMail`/`TrySendToPrinterVendor` +helpers, using the usage value registered per +`extend-report-selection-usage-for-new-document-types.md`. Both are +equally correct; neither reads the counterparty's assigned profile. +Reserve a genuine `Get`/`GetDefaultForCustomer`/`GetDefaultForVendor` +lookup and `Send`/`SendVendor` for Post-and-Send. 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. +Loading the counterparty's *actually assigned* `Document Sending +Profile` (or the tenant default, via `Get`/`GetDefaultForCustomer`/ +`GetDefaultForVendor` — the same lookup `Sales-Post and Send` performs) +and calling `Send`/`SendVendor` on it from a plain, on-demand "Email" +button, instead of `ReportSelections.SendEmailToCust`/`SendEmailToVendor` +directly. The button's outcome now silently depends on a profile +configured for Post-and-Send — if its `"E-Mail"` option is `No`, +clicking "Email" does nothing observable. A second version of the same +mistake: an email action on 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 +BCApps `DocumentPrint.Codeunit.al` (`EmailSalesHeader`/`DoPrintSalesHeader`/ +`PrintSalesOrder` → `ReportSelections.SendEmailToCust`/`PrintForCust`/ +`PrintWithDialogForCust` directly), `SalesInvoiceHeader.Table.al` +(`PrintRecords`/`EmailRecords`, lines 1453/1528 → `TrySendToPrinter`/ +`TrySendToEMail`, lines 1462/1541, on a local never-`Get`'d record), +`PurchaseHeader.Table.al` (`PrintRecords` line 6357 → +`TrySendToPrinterVendor` line 6374; `SendProfile` line 6387 → +`SendVendor` line 6403), `PurchInvHeader.Table.al` (`PrintRecords` → +`ReportSelection.PrintWithDialogForVend` directly, no send capability), +`SalesPostandSend.Codeunit.al`/`SalesPost.Codeunit.al` +(`ConfirmPostAndSend` loads `Get(Customer."Document Sending +Profile")`/`GetDefault`; `SendPostedDocumentRecord` line 7660 → +`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 diff --git a/microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.bad.al b/microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.bad.al new file mode 100644 index 0000000..0435992 --- /dev/null +++ b/microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.bad.al @@ -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. diff --git a/microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.good.al b/microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.good.al new file mode 100644 index 0000000..8acd6c6 --- /dev/null +++ b/microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.good.al @@ -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'; + } +} diff --git a/microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.md b/microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.md new file mode 100644 index 0000000..68f2c47 --- /dev/null +++ b/microsoft/knowledge/data-modeling/extend-price-source-type-must-sync-document-subset-enum.md @@ -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) diff --git a/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.bad.al b/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.bad.al index 046777e..d09d34a 100644 --- a/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.bad.al +++ b/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.bad.al @@ -28,11 +28,20 @@ codeunit 50100 "Sample Report Selection Install" 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. + // Registration ends here. No enumextension was added to + // "Custom Report Selection Sales" (or "Report Selection Usage + // Vendor"), and no subscriber was added to + // OnAfterOnMapTableUsageValueToPageValue, OnValidateUsage2OnCaseElse, + // or OnAfterFilterCustomerUsageReportSelections / + // 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; } diff --git a/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.good.al b/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.good.al index f2f3244..065e802 100644 --- a/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.good.al +++ b/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.good.al @@ -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" { UsageCategory = ReportsAndAnalysis; @@ -33,16 +46,34 @@ codeunit 50100 "Sample Report Selection Install" 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") + // Customer-only document: all three subscribers below are on + // "Customer Report Selections" only. There are no matching subscribers + // on "Vendor Report Selections" - subscribing there too would be the + // 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 - ReportSelections.SetFilter(Usage, GetUsageFilter(ReportSelections)); + if CustomReportSelection.Usage = "Report Selection Usage"::"Sample.SettlementDoc" then + Usage2 := "Custom Report Selection Sales"::"Sample.SettlementDoc"; end; - [EventSubscriber(ObjectType::Page, Page::"Vendor Report Selections", 'OnAfterFilterVendorUsageReportSelections', '', false, false)] - local procedure AddSampleUsageOnAfterFilterVendorUsageReportSelections(var ReportSelections: Record "Report Selections") + // 2) Validate: lets a user pick the new value from the Usage dropdown. + [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 ReportSelections.SetFilter(Usage, GetUsageFilter(ReportSelections)); end; diff --git a/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.md b/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.md index 1f46b9f..62c7839 100644 --- a/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.md +++ b/microsoft/knowledge/data-modeling/extend-report-selection-usage-for-new-document-types.md @@ -7,73 +7,93 @@ countries: [w1] 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 -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. +A custom document that needs printing/emailing should be registered +through `table 77 "Report Selections"`. `enum 77 "Report Selection Usage"` +is `Extensible = true` for exactly this: add a value via `enumextension`, +then `ReportSelections.InsertRecord(Usage, Sequence, ReportID)` for a +tenant-wide default — the mechanism every standard 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. +That alone does not make the value usable in "Document Layouts" +(`page 9657 "Customer Report Selections"` / `page 9658 "Vendor Report +Selections"`, table 9657 "Custom Report Selection"). Both pages hide +`enum 77` behind their own page-facing enum — `enum 9657 "Custom Report +Selection Sales"` (customer) / `enum 9658 "Report Selection Usage Vendor"` +(vendor) — in a field named `Usage2`. A new value stays invisible there +until that page enum is extended too and three events are handled: +`OnAfterOnMapTableUsageValueToPageValue` / `OnMapTableUsageValueToPage +ValueOnCaseElse` (Usage column display), `OnValidateUsage2OnCaseElse` +(picking it from the dropdown), and `OnAfterFilterCustomerUsageReport +Selections` / `OnAfterFilterVendorUsageReportSelections` (the **"Copy from +Report Selection"** action only — a hardcoded-list filter, nothing more). + +Which side(s) need this depends on the counterparty the document actually +applies to — not "always both." BCApps' `ReportSelectionHandlerCZZ` +(Advance Payments) partitions strictly: `"Sales Advance..."` usages get +only the customer-side triad, `"Purchase Advance..."` only the vendor-side +triad. `ReportSelectionHandlerCZC` (Compensation) subscribes both sides — +legitimately, since that document posts to both ledgers, not by default. ## 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. +1. Add the usage value (`enumextension ... extends "Report Selection + Usage"`) and register the tenant-wide default. +2. Decide which counterparty(ies) apply — customer, vendor, or both. +3. For each applicable side, extend the matching page enum + (`"Custom Report Selection Sales"` / `"Report Selection Usage Vendor"`) + and subscribe to that page's map, validate, and filter events — + appending with `StrSubstNo('%1|%2', ReportSelections.GetFilter(Usage), + UsageFilter)`, never overwriting. +4. Do not subscribe the other side for a one-sided document: skip the + 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 -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`. +1. Register the usage value but add no page-enum extension and no + subscribers. Works via the tenant-wide default, so it's invisible in + testing — but Document Layouts shows the value's rows blank, can't offer + it in the Usage dropdown, and "Copy from Report Selection" never lists + it. See sample: `extend-report-selection-usage-for-new-document-types.bad.al`. +2. Subscribe both counterparties' triads for a one-sided document. This is + the overbroad default Jesper Schulz-Wedde's review caught: it + contradicts how `ReportSelectionHandlerCZZ` actually partitions its + usages, and clutters the other counterparty's picker with a value that + will never resolve a report there. ## 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`), -`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). +`CustomReportSelection.Table.al` (table 9657) — all under +`src/Layers/W1/BaseApp/Foundation/Reporting/`. + +`CustomerReportSelections.Page.al` (page 9657, `.../Sales/Setup/`): +`FilterCustomerUsageReportSelections` (307), +`OnAfterFilterCustomerUsageReportSelections` (335), +`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.) diff --git a/microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.bad.al b/microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.bad.al new file mode 100644 index 0000000..56659fe --- /dev/null +++ b/microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.bad.al @@ -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; +} diff --git a/microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.good.al b/microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.good.al new file mode 100644 index 0000000..27f5d1c --- /dev/null +++ b/microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.good.al @@ -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; +} diff --git a/microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.md b/microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.md new file mode 100644 index 0000000..52bf9e2 --- /dev/null +++ b/microsoft/knowledge/data-modeling/new-price-source-must-add-candidate-and-trigger-recalculation.md @@ -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())`. + +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) diff --git a/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.bad.al b/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.bad.al new file mode 100644 index 0000000..1aae8d4 --- /dev/null +++ b/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.bad.al @@ -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; +} diff --git a/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.good.al b/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.good.al new file mode 100644 index 0000000..cc3dcc6 --- /dev/null +++ b/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.good.al @@ -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. +} diff --git a/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md b/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md new file mode 100644 index 0000000..0150947 --- /dev/null +++ b/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md @@ -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. diff --git a/microsoft/skills/review/al-data-modeling-review.md b/microsoft/skills/review/al-data-modeling-review.md index 9e11323..fff276d 100644 --- a/microsoft/skills/review/al-data-modeling-review.md +++ b/microsoft/skills/review/al-data-modeling-review.md @@ -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`. -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 @@ -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: -- 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 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. -- 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`). +- 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`, 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`, `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. @@ -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 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. +- 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. +- 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`. @@ -86,7 +90,7 @@ Outcome selection: - `completed` — the skill evaluated every worklist item. - `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. - `failed` — an unrecoverable error occurred. diff --git a/tools/Test-ReviewFixtures.ps1 b/tools/Test-ReviewFixtures.ps1 index a9912b7..e0fc8cf 100644 --- a/tools/Test-ReviewFixtures.ps1 +++ b/tools/Test-ReviewFixtures.ps1 @@ -245,6 +245,47 @@ foreach ($domain in $leafDomains) { } $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)