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 index a3f7c0f..6a02378 100644 --- 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 @@ -7,9 +7,68 @@ enumextension 50102 "Sample Price Calc Handler Ext" extends "Price Calculation H } } +// Demonstration-only AL: every method below is stubbed. This article is +// about activating a handler through OnFindSupportedSetup, not about the +// "Price Calculation" interface's own pricing logic. +codeunit 50103 "Sample Price Calc - Special" implements "Price Calculation" +{ + procedure Init(LineWithPrice: Interface "Line With Price"; PriceCalculationSetup: Record "Price Calculation Setup") + begin + end; + + procedure GetLine(var Line: Variant) + begin + end; + + procedure ApplyDiscount() + begin + end; + + procedure ApplyPrice(CalledByFieldNo: Integer) + begin + end; + + procedure CountDiscount(ShowAll: Boolean) Result: Integer + begin + end; + + procedure CountPrice(ShowAll: Boolean) Result: Integer + begin + end; + + procedure FindDiscount(var TempPriceListLine: Record "Price List Line"; ShowAll: Boolean) Found: Boolean + begin + end; + + procedure FindPrice(var TempPriceListLine: Record "Price List Line"; ShowAll: Boolean) Found: Boolean + begin + end; + + procedure IsDiscountExists(ShowAll: Boolean) Result: Boolean + begin + end; + + procedure IsPriceExists(ShowAll: Boolean) Result: Boolean + begin + end; + + procedure PickDiscount() + begin + end; + + procedure PickPrice() + begin + end; + + procedure ShowPrices(var TempPriceListLine: Record "Price List Line") + begin + end; +} + // 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. +// purchase, or job line, whether through the Default fallback or through +// a "Dtld. Price Calculation Setup" row. 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 index c36928b..8f27beb 100644 --- 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 @@ -7,6 +7,64 @@ enumextension 50102 "Sample Price Calc Handler Ext" extends "Price Calculation H } } +// Demonstration-only AL: every method below is stubbed. This article is +// about activating a handler through OnFindSupportedSetup, not about the +// "Price Calculation" interface's own pricing logic. +codeunit 50103 "Sample Price Calc - Special" implements "Price Calculation" +{ + procedure Init(LineWithPrice: Interface "Line With Price"; PriceCalculationSetup: Record "Price Calculation Setup") + begin + end; + + procedure GetLine(var Line: Variant) + begin + end; + + procedure ApplyDiscount() + begin + end; + + procedure ApplyPrice(CalledByFieldNo: Integer) + begin + end; + + procedure CountDiscount(ShowAll: Boolean) Result: Integer + begin + end; + + procedure CountPrice(ShowAll: Boolean) Result: Integer + begin + end; + + procedure FindDiscount(var TempPriceListLine: Record "Price List Line"; ShowAll: Boolean) Found: Boolean + begin + end; + + procedure FindPrice(var TempPriceListLine: Record "Price List Line"; ShowAll: Boolean) Found: Boolean + begin + end; + + procedure IsDiscountExists(ShowAll: Boolean) Result: Boolean + begin + end; + + procedure IsPriceExists(ShowAll: Boolean) Result: Boolean + begin + end; + + procedure PickDiscount() + begin + end; + + procedure PickPrice() + begin + end; + + procedure ShowPrices(var TempPriceListLine: Record "Price List Line") + begin + end; +} + codeunit 50104 "Sample Price Calc Setup Install" { [EventSubscriber(ObjectType::Codeunit, Codeunit::"Price Calculation Mgt.", 'OnFindSupportedSetup', '', false, false)] @@ -19,6 +77,13 @@ codeunit 50104 "Sample Price Calc Setup Install" TempPriceCalculationSetup."Asset Type" := TempPriceCalculationSetup."Asset Type"::" "; TempPriceCalculationSetup.Implementation := TempPriceCalculationSetup.Implementation::"Sample Special Price"; TempPriceCalculationSetup.Enabled := true; + // Default := true here because this row is meant as the fallback + // for Method = Lowest Price / Type = Sale / Asset Type = " " (all) + // - the combination Price Calculation Mgt.'s FindSetup selects via + // its own SetRange(Default, true) branch when no "Dtld. Price + // Calculation Setup" row names a more specific match. A handler + // meant to be picked only through such a specific, explicit + // detailed-setup row would not need Default := true at all. 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 index a76b644..3654468 100644 --- 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 @@ -23,13 +23,21 @@ Implementation, Enabled, Default)` rows populated at startup by its own 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. +because `Price Calculation Mgt.` has no setup row that names it. + +`FindSetup` resolves a handler in two stages, and only the second one +looks at `Default`. It first asks `codeunit 7004 "Price Calculation Dtld. +Setup"` to match the line against `table 7008 "Dtld. Price Calculation +Setup"` ("Detailed Price Calculation Setup", keyed to an exact +`Method`/`Type`/`Asset Type`/`Source`/`Asset No.` combination via its own +`"Setup Code"`); on a match it does `PriceCalculationSetup.Get(... +"Setup Code")` directly, with no `Default` filter. Only when no detailed +row matches does it fall back to `SetRange(Default, true)` plus +`SetRange(Method, ...)` to pick the one catch-all row for that +combination. A row without `Default := true` is invisible to *that* +fallback, but not invisible outright — a detailed-setup row can still +select it by naming its `Code`. A row whose `Method` matches neither path +is invisible either way — same symptom, different cause. ## Best Practice @@ -37,8 +45,13 @@ 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. +`Asset Type`. `Default := true` is required only when this row is the +*fallback* for that combination — the row `FindSetup`'s own +`SetRange(Default, true)` branch selects when no more specific setup +applies. A handler meant to be selected only for specific customers or +items should instead be reachable through a matching `"Dtld. Price +Calculation Setup"` row; `FindSetup` resolves that before it ever checks +`Default`, so it needs no `Default := true`. See sample: [`activate-new-price-calculation-handler-via-onfindsupportedsetup.good.al`](activate-new-price-calculation-handler-via-onfindsupportedsetup.good.al). @@ -58,25 +71,28 @@ See sample: [`activate-new-price-calculation-handler-via-onfindsupportedsetup.ba 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)). +(`OnFindSupportedSetup(var TempPriceCalculationSetup: Record "Price +Calculation Setup" temporary)`, and `FindSetup(...): Boolean`, which +first calls `PriceCalculationDtldSetup.FindSetup(DtldPriceCalcSetup)` and +on a match does `PriceCalculationSetup.Get(... "Setup Code")` with no +`Default` filter — only on failure does it fall back to +`SetRange(Enabled, true)`, `SetRange(Default, true)`, `SetRange(Method, +...)`); `PriceCalculationSetup.Table.al` (`table 7006 "Price Calculation +Setup"`: `Code`, `Method`, `Type`, `"Asset Type"`, `Implementation`, +`Enabled`, `Default`); `PriceCalculationDtldSetup.Codeunit.al` (`codeunit +7004 "Price Calculation Dtld. Setup"`, `FindSetup(var DtldPriceCalcSetup: +Record "Dtld. Price Calculation Setup"): Boolean`, matching progressively +looser `Source Group`/`Source No.`/`Asset Type`/`Asset No.` combinations — +never `Default`); `DtldPriceCalculationSetup.Table.al` (`table 7008 "Dtld. +Price Calculation Setup"`, Caption "Detailed Price Calculation Setup", +`"Setup Code"` relates to `"Price Calculation Setup".Code where(Enabled = +const(true))` — no `Default` condition). -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." +Microsoft Learn, "Extending Price Calculations": "Each codeunit that +implements the Price Calculation interface must subscribe to the +OnFindSupportedSetup() event... to fill the price calculation setup +table." Same article: "You can enter detailed setup records for +non-default setup lines... If a matching setup is found its +implementation is used... If there is no matching setup exception, we +use the default implementation." (https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-extending-best-price-calculations) diff --git a/microsoft/knowledge/data-modeling/extend-find-entries-navigate-for-new-document-types.bad.al b/microsoft/knowledge/data-modeling/extend-find-entries-navigate-for-new-document-types.bad.al index 9283439..486eee8 100644 --- a/microsoft/knowledge/data-modeling/extend-find-entries-navigate-for-new-document-types.bad.al +++ b/microsoft/knowledge/data-modeling/extend-find-entries-navigate-for-new-document-types.bad.al @@ -1,3 +1,19 @@ +table 50104 "Sample Posted Document Header" +{ + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) { Caption = 'No.'; } + field(2; "Posting Date"; Date) { Caption = 'Posting Date'; } + } + + keys + { + key(PK; "No.") { Clustered = true; } + } +} + codeunit 50103 "Sample Navigate Subscribers" { // WRONG: registers the row, so it appears in the Find Entries result diff --git a/microsoft/knowledge/data-modeling/extend-find-entries-navigate-for-new-document-types.good.al b/microsoft/knowledge/data-modeling/extend-find-entries-navigate-for-new-document-types.good.al index 843935c..c0d658b 100644 --- a/microsoft/knowledge/data-modeling/extend-find-entries-navigate-for-new-document-types.good.al +++ b/microsoft/knowledge/data-modeling/extend-find-entries-navigate-for-new-document-types.good.al @@ -1,3 +1,36 @@ +table 50104 "Sample Posted Document Header" +{ + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) { Caption = 'No.'; } + field(2; "Posting Date"; Date) { Caption = 'Posting Date'; } + } + + keys + { + key(PK; "No.") { Clustered = true; } + } +} + +page 50104 "Sample Posted Document" +{ + PageType = Card; + SourceTable = "Sample Posted Document Header"; + UsageCategory = None; + ApplicationArea = All; + + layout + { + area(Content) + { + field("No."; Rec."No.") { ApplicationArea = All; } + field("Posting Date"; Rec."Posting Date") { ApplicationArea = All; } + } + } +} + codeunit 50103 "Sample Navigate Subscribers" { [EventSubscriber(ObjectType::Page, Page::Navigate, 'OnAfterFindRecords', '', false, false)] 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 index 56659fe..6132c60 100644 --- 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 @@ -5,8 +5,8 @@ tableextension 50105 "Sample Sales Line Ext" extends "Sales Line" // 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. + // triggers a recalculation (e.g. via UpdateUnitPrice), so the + // unit price silently keeps its old value. field(50100; "Sample Loyalty Customer No."; Code[20]) { Caption = 'Sample Loyalty Customer No.'; 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 index 27f5d1c..6f8b157 100644 --- 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 @@ -13,7 +13,16 @@ tableextension 50105 "Sample Sales Line Ext" extends "Sales Line" // 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.")); + // + // UpdateUnitPriceByField(CalledByFieldNo) only recalculates + // if PlanPriceCalcByField(CalledByFieldNo) was already + // called for that same field - calling it alone is a + // silent no-op. UpdateUnitPrice(CalledByFieldNo) does both + // steps in the right order (plan, then update) in one + // call; it's the same method the base app itself calls + // from outside Sales Line to trigger recalculation for a + // field it just changed. + UpdateUnitPrice(FieldNo("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 index 819e16a..828fc8c 100644 --- 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 @@ -21,11 +21,23 @@ being priced by it. `codeunit "Sales Line - Price"` publishes `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. +an existing line. That's the second, separate piece, and it needs to be +wired correctly: `Sales Line`'s `procedure +UpdateUnitPriceByField(CalledByFieldNo: Integer)` only recalculates if +the field was already *planned* — internally it exits immediately unless +`procedure PlanPriceCalcByField(CurrPriceFieldNo: Integer)` was already +called for that same field number. Calling `UpdateUnitPriceByField` on +its own, without a matching `PlanPriceCalcByField` call first, compiles +fine and looks correct, but silently recalculates nothing. `Sales Line` +also exposes `procedure UpdateUnitPrice(CalledByFieldNo: Integer)`, a +convenience wrapper that does both steps in the right order (plan, then +update) in one call — this is the method the base app itself calls from +*outside* `Sales Line` to trigger recalculation for a field it just +changed (see `Inventory/Item/Catalog/ItemReferenceManagement.Codeunit.al`: +`SalesLine.UpdateUnitPrice(SalesLine.FieldNo("Item Reference No."))`), and +it's what a custom price source field's own trigger should call too — 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 @@ -40,17 +52,22 @@ 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())`. +`SalesLine.UpdateUnitPrice(SalesLine.FieldNo())`. Calling +`UpdateUnitPriceByField` directly, without first calling +`PlanPriceCalcByField` for that same field number, is *not* equivalent — +it exits immediately and recalculates nothing. `UpdateUnitPrice` does +both calls, in the correct order, in one step. See sample: [`new-price-source-must-add-candidate-and-trigger-recalculation.good.al`](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. +source, without also triggering recalculation (via `UpdateUnitPrice`, or +the `PlanPriceCalcByField` + `UpdateUnitPriceByField` pair) 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`](new-price-source-must-add-candidate-and-trigger-recalculation.bad.al). @@ -62,7 +79,13 @@ 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)`). +PlanPriceCalcByField(CurrPriceFieldNo: Integer)`; `procedure +UpdateUnitPrice(CalledByFieldNo: Integer)`; `procedure +UpdateUnitPriceByField(CalledByFieldNo: Integer)`, which exits immediately +unless `FieldCausedPriceCalculation` already equals `CalledByFieldNo` — +the state `PlanPriceCalcByField` sets). External, idiomatic use of the +one-call form: `Inventory/Item/Catalog/ItemReferenceManagement.Codeunit.al` +(`SalesLine.UpdateUnitPrice(SalesLine.FieldNo("Item Reference No."))`). Microsoft Learn, "Extending Price Calculations" (Location example): "To recalculate the price, we can subscribe to events that pass the sales 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 index 1aae8d4..0de8b58 100644 --- 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 @@ -14,11 +14,23 @@ report 50110 "Sample Item Barcode Label" 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. + // module's provider/encoder API. This is not wrong merely + // because the delimiter was added by hand - Code 39's own + // symbology does use "*" as its start/stop character + // (Microsoft Learn, "Barcode Fonts with Business Central + // Online"). It's wrong because it's demonstrably mismatched + // with what encoding "No." through the real API would + // produce: + // - it skips ValidateInput, so a "No." value outside Code + // 39's character set, or one that needs a checksum this + // code never applies, reaches the font unvalidated; + // - IDAutomation 1D Provider's own EncodeFont output for + // Code 39 wraps the value in "(" / ")", not literal "*" + // (BCApps' own encoder test: EncodeFont('1234', Code39) + // = '(1234)') - the paired font maps those parentheses to + // the real start/stop glyph, so a string built with + // literal asterisks is simply the wrong characters for + // that font, on top of carrying no real checksum. BarcodeText := '*' + "No." + '*'; end; } 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 index cc3dcc6..8364ad8 100644 --- 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 @@ -9,32 +9,46 @@ report 50110 "Sample Item Barcode Label" dataitem(Item; Item) { column(No_; "No.") { } - column(Barcode; BarcodeText) { } + column(Barcode1D; BarcodeText) { } + column(Barcode2D; QRCodeText) { } trigger OnAfterGetRecord() var BarcodeFontProvider: Interface "Barcode Font Provider"; + BarcodeFontProvider2D: Interface "Barcode Font Provider 2D"; begin + // One-dimensional: "Barcode Font Provider" declares both + // ValidateInput and EncodeFont - call both. BarcodeFontProvider := Enum::"Barcode Font Provider"::IDAutomation1D; BarcodeFontProvider.ValidateInput("No.", BarcodeSymbology); BarcodeText := BarcodeFontProvider.EncodeFont("No.", BarcodeSymbology); + + // Two-dimensional: "Barcode Font Provider 2D" declares only + // EncodeFont - there is no ValidateInput to call here. + BarcodeFontProvider2D := Enum::"Barcode Font Provider 2D"::IDAutomation2D; + QRCodeText := BarcodeFontProvider2D.EncodeFont("No.", BarcodeSymbology2D); end; } } var BarcodeSymbology: Enum "Barcode Symbology"; + BarcodeSymbology2D: Enum "Barcode Symbology 2D"; BarcodeText: Text; + QRCodeText: Text; trigger OnInitReport() begin BarcodeSymbology := Enum::"Barcode Symbology"::Code39; + BarcodeSymbology2D := Enum::"Barcode Symbology 2D"::"QR-Code"; 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 + // the Barcode1D 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. + // environment means "the barcode won't render" at all. The + // Barcode2D column's font name is IDAutomation2D (IDAutomation2D + // MaxiCode for Maxicode specifically). } diff --git a/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md b/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md index 1a7ecf5..892c2f1 100644 --- a/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md +++ b/microsoft/knowledge/data-modeling/report-barcodes-must-use-barcode-module-and-production-font-name.md @@ -1,7 +1,7 @@ --- bc-version: [all] domain: data-modeling -keywords: [barcode, qr-code, barcode-font-provider, report-layout, saas, idautomation] +keywords: [barcode, qr-code, barcode-font-provider, barcode-font-provider-2d, report-layout, saas, idautomation, code-39, checksum] technologies: [al] countries: [w1] application-area: [all] @@ -12,85 +12,89 @@ application-area: [all] ## 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*. +`Barcode` module (`src/System Application/App/Barcode`): `interface +"Barcode Font Provider"` / `"Barcode Font Provider 2D"`, `enum "Barcode +Symbology"` / `"Barcode Symbology 2D"`, and built-in implementations +(`codeunit 9215`/`9221`). A report encodes a data string via this API; +the layout then displays it 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. +The two interfaces are not symmetric: `"Barcode Font Provider"` (1D) +declares both `ValidateInput` and `EncodeFont`; `"Barcode Font Provider +2D"` declares only `EncodeFont` (see Source). BCApps' `Item GTIN Label` +report reflects that split exactly — it validates then encodes through +the 1D provider, but only encodes through the 2D provider, for the same +"No." value. -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. +On Business Central online this needs no setup ("the IDAutomation fonts +are automatically available as part of the service" — Microsoft Learn), +unlike on-premises, where fonts must be purchased and installed. That +ease hides a SaaS-specific trap the API doesn't cover: naming the actual +font. IDAutomation ships both a purchased font and a same-looking +evaluation font per version (Code 39: `IDAutomationHC39M` purchased vs. +`IDAutomationSHC39M Demo`) — per Microsoft Learn, "be sure to use the +purchased font name... If you use the evaluation font name, the barcode +won't render." The wrong name produces nothing, in the layout not AL, so +no reviewer catches it reading the object. ## 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`. +Encode through the real API, matching the calls to what the chosen +interface actually declares. One-dimensional: declare `Interface +"Barcode Font Provider"` and call both `ValidateInput` and `EncodeFont` +— skipping validation lets a value outside the character set, or one +needing a checksum setting never applied, reach the font unchecked. +Two-dimensional: declare `Interface "Barcode Font Provider 2D"` and call +`EncodeFont` alone — there is no `ValidateInput` on this interface. + +Treat naming the production font in the layout as equally required, not +an afterthought. Two-dimensional symbologies other than Maxicode use +`IDAutomation2D` (Maxicode: `IDAutomation2D MaxiCode`); one-dimensional +symbologies use the purchased version name (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`](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. +Constructing a barcode string by hand instead of using the module's +provider/encoder API — not because a manual delimiter is inherently +wrong (Code 39's own symbology does use `*` as start/stop; Microsoft +Learn's font table says so), but because hand-rolled construction is +demonstrably mismatched with what the real encoder produces: it skips +`ValidateInput` (so a value outside the character set, or needing a +checksum/extended-charset setting never applied, reaches the font +unvalidated), and IDAutomation 1D Provider's own Code 39 output is +wrapped in `(`/`)`, not literal `*` (BCApps test: +`EncodeFont('1234', Code39) = '(1234)'`) — the paired font maps those +parentheses to the real start/stop glyph, so `'*' + value + '*'` is +simply the wrong characters, plus no checksum. -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. +Flag demonstrably invalid or mismatched hand construction, not manual +delimiter use as a category — a custom provider paired with a font that +genuinely expects literal `*` delimiters is a different, legitimate case. + +A second version of the same mistake: encoding correctly, but naming the +evaluation font instead of the purchased one. Both look complete in +review and fail silently — the first because the data was never a real +barcode, the second because BC online refuses to render it. See sample: [`report-barcodes-must-use-barcode-module-and-production-font-name.bad.al`](report-barcodes-must-use-barcode-module-and-production-font-name.bad.al). ## 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). +BCApps (`src/System Application/App/Barcode/src/`): +`Barcode Provider/Font/BarcodeFontProvider.Interface.al` (1D: +`ValidateInput` + `EncodeFont`); `Barcode Provider 2D/Font/ +BarcodeFontProvider2D.Interface.al` (2D: only `EncodeFont`); both read +fresh from source. `IDAutomation 1D Provider/Encoders/ +IDA1DCode39Encoder.Codeunit.al` (`codeunit 9204`, regex accepts literal +`*` as plain input; `EncodeFont` → `DotNet FontEncoder.Code39`). Split +and delimiter mismatch both confirmed live: `.../Inventory/Item/ +ItemGTINLabel.Report.al` (`report 6625`, validates+encodes 1D, only +encodes 2D, same value) and `.../Test/Barcode/.../IDA1DCode39Test. +Codeunit.al` (`codeunit 135044`): `EncodeFontSuccessTest('1234', Code39, +'(1234)')` — wrapped in `(`/`)`, never literal `*`. -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. +Microsoft Learn "Adding Barcodes to Reports" and "Barcode Fonts with +Business Central Online" — quoted above, incl. the Code39 row ("`*` is +used for both start and stop delimiters"). diff --git a/microsoft/skills/review/al-data-modeling-review.md b/microsoft/skills/review/al-data-modeling-review.md index 325a706..20dfaa1 100644 --- a/microsoft/skills/review/al-data-modeling-review.md +++ b/microsoft/skills/review/al-data-modeling-review.md @@ -59,7 +59,7 @@ The following targeted checks cover every current `data-modeling` article. Treat - An `EventSubscriber` is added for `Navigate::OnAfterFindRecords` (registering a custom table in Find Entries) without a matching `Navigate::OnBeforeShowRecords` subscriber for the same table, or vice versa — `extend-find-entries-navigate-for-new-document-types`. Both subscribers must be added together for the same table. - An `enumextension` extends `"Report Selection Usage"` and registers a report via `ReportSelections.InsertRecord`, without subscribing to the matching *single* counterparty's full triad — the filter event (`OnAfterFilterCustomerUsageReportSelections` on `page 9657` for a sales usage, `OnAfterFilterVendorUsageReportSelections` on `page 9658` for a purchase usage) AND the page-facing usage-enum map/validate events (`enumextension` on `"Custom Report Selection Sales"`/`"Report Selection Usage Vendor"` plus the matching map/validate subscribers) — `extend-report-selection-usage-for-new-document-types`. Requiring or wiring *both* counterparties by default for a one-sided document is also the anti-pattern (`ReportSelectionHandlerCZZ` partitions strictly by counterparty); only a genuinely two-sided usage (as `ReportSelectionHandlerCZC` demonstrates for Compensation) needs both. - 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 Calculation Handler"` and implements the `Price Calculation` interface, without a matching `OnFindSupportedSetup` subscriber inserting a `Price Calculation Setup` record naming that implementation as the `Implementation` for a `Method`/`Type`/`Asset Type` — `activate-new-price-calculation-handler-via-onfindsupportedsetup`. `Default := true` is only required on that row when it is meant as the fallback for its `Method`/`Type`/`Asset Type` combination; a row meant to be selected only through an explicit, specific `"Dtld. Price Calculation Setup"` row does not need it, so do not flag a missing `Default := true` by itself — flag the missing setup row/subscriber entirely. - An `enumextension` extends `"Price Source Type"` with a new value intended for a sales, purchase, or job price list, without extending the matching document subset enum (`"Sales Price Source Type"`, `"Purchase Price Source Type"`, `"Job Price Source Type"`) with a value at the same numeric ID — `extend-price-source-type-must-sync-document-subset-enum`. - A codeunit subscribes to `"Sales Line - Price"`'s `OnAfterAddSources` to register a custom field as a price source via `PriceSourceList.Add`, but that field has no `OnValidate` (or matching `OnAfterValidate`) that 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`.