mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
Fix 5 merge-critical issues from Jesper's 2026-09-24 review round
- activate-new-price-calculation-handler-via-onfindsupportedsetup: Default := true is required only for the fallback branch of PriceCalculationMgt's two-stage FindSetup - a handler reachable via a specific Dtld. Price Calculation Setup row needs no Default. Softened the article and its worklist cue accordingly. Also fixed an undefined "Sample Price Calc - Special" codeunit referenced but never declared in the eval fixtures - added a real implementation of interface "Price Calculation" with stub methods. - new-price-source-must-add-candidate-and-trigger-recalculation: the good fixture called UpdateUnitPriceByField directly, which is a silent no-op without a prior PlanPriceCalcByField call (FieldCausedPriceCalculation gating, verified against SalesLine.Table.al). Switched to the public UpdateUnitPrice wrapper, matching real BCApps usage in ItemReferenceManagement.Codeunit.al. - report-barcodes-must-use-barcode-module-and-production-font-name: split the 1D (ValidateInput + EncodeFont) and 2D (EncodeFont only) Barcode Font Provider interfaces, which the article previously conflated. Reframed the Code 39 anti-pattern around demonstrable encoding/checksum mismatch (verified against IDA1DCode39Encoder.Codeunit.al's real '(value)' output) rather than rejecting all manual delimiter use, since '*' is a legitimate Code 39 start/stop character. Also fixed extend-find-entries-navigate- for-new-document-types' eval fixtures, which referenced an undefined "Sample Posted Document Header" table/page - declared both. All claims re-verified against live microsoft/BCApps source. Validators: frontmatter 0/0, review-fixtures 126/20 domains PASSED, knowledge-index 342/575 PASSED, skill-index 19 leaves PASSED. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
e22352b248
commit
eb9cae0af4
12 changed files with 371 additions and 120 deletions
|
|
@ -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.
|
// WRONG: no subscriber to Price Calculation Mgt.'s OnFindSupportedSetup.
|
||||||
// "Sample Special Price" is a real, working implementation of the Price
|
// "Sample Special Price" is a real, working implementation of the Price
|
||||||
// Calculation interface - it simply has no Price Calculation Setup row
|
// Calculation interface - it simply has no Price Calculation Setup row
|
||||||
// naming it, so Price Calculation Mgt. never selects it for any sale,
|
// naming it, so Price Calculation Mgt. never selects it for any sale,
|
||||||
// purchase, or job line. It ships invisible until someone notices and
|
// purchase, or job line, whether through the Default fallback or through
|
||||||
// configures a setup row for it by hand.
|
// a "Dtld. Price Calculation Setup" row. It ships invisible until someone
|
||||||
|
// notices and configures a setup row for it by hand.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
codeunit 50104 "Sample Price Calc Setup Install"
|
||||||
{
|
{
|
||||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Price Calculation Mgt.", 'OnFindSupportedSetup', '', false, false)]
|
[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."Asset Type" := TempPriceCalculationSetup."Asset Type"::" ";
|
||||||
TempPriceCalculationSetup.Implementation := TempPriceCalculationSetup.Implementation::"Sample Special Price";
|
TempPriceCalculationSetup.Implementation := TempPriceCalculationSetup.Implementation::"Sample Special Price";
|
||||||
TempPriceCalculationSetup.Enabled := true;
|
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.Default := true;
|
||||||
TempPriceCalculationSetup.Insert();
|
TempPriceCalculationSetup.Insert();
|
||||||
end;
|
end;
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
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,
|
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
|
because `Price Calculation Mgt.` has no setup row that names it.
|
||||||
row that exists but doesn't match is just as invisible: `FindSetup`
|
|
||||||
filters candidates with `SetRange(Default, true)` and `SetRange(Method,
|
`FindSetup` resolves a handler in two stages, and only the second one
|
||||||
DtldPriceCalcSetup.Method)` (a document's blank Method is normalized to
|
looks at `Default`. It first asks `codeunit 7004 "Price Calculation Dtld.
|
||||||
`"Lowest Price"` before that filter runs), so a row inserted without
|
Setup"` to match the line against `table 7008 "Dtld. Price Calculation
|
||||||
`Default := true`, or with a `Method` that doesn't match, is never
|
Setup"` ("Detailed Price Calculation Setup", keyed to an exact
|
||||||
selected either — same symptom, different cause.
|
`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
|
## Best Practice
|
||||||
|
|
||||||
|
|
@ -37,8 +45,13 @@ Ship a new `Price Calculation Handler` value together with an
|
||||||
`OnFindSupportedSetup` subscriber that inserts at least one `Price
|
`OnFindSupportedSetup` subscriber that inserts at least one `Price
|
||||||
Calculation Setup` record naming it as the `Implementation`, for the
|
Calculation Setup` record naming it as the `Implementation`, for the
|
||||||
relevant `Method` (e.g. `"Lowest Price"`), `Type` (`Sale`/`Purchase`), and
|
relevant `Method` (e.g. `"Lowest Price"`), `Type` (`Sale`/`Purchase`), and
|
||||||
`Asset Type` — with `Default := true`, since `FindSetup` only considers
|
`Asset Type`. `Default := true` is required only when this row is the
|
||||||
rows where `Default` is set when resolving a handler for a line.
|
*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).
|
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/`):
|
BCApps (`src/Layers/W1/BaseApp/Pricing/Calculation/`):
|
||||||
`PriceCalculationHandler.Enum.al` (`enum 7011 "Price Calculation Handler"
|
`PriceCalculationHandler.Enum.al` (`enum 7011 "Price Calculation Handler"
|
||||||
implements "Price Calculation"`); `PriceCalculationMgt.Codeunit.al`
|
implements "Price Calculation"`); `PriceCalculationMgt.Codeunit.al`
|
||||||
(`local procedure OnFindSupportedSetup(var TempPriceCalculationSetup:
|
(`OnFindSupportedSetup(var TempPriceCalculationSetup: Record "Price
|
||||||
Record "Price Calculation Setup" temporary)`, called during setup
|
Calculation Setup" temporary)`, and `FindSetup(...): Boolean`, which
|
||||||
resolution, and `procedure FindSetup(...)`, which requires
|
first calls `PriceCalculationDtldSetup.FindSetup(DtldPriceCalcSetup)` and
|
||||||
`SetRange(Default, true)` and a matching `SetRange(Method,
|
on a match does `PriceCalculationSetup.Get(... "Setup Code")` with no
|
||||||
DtldPriceCalcSetup.Method)` before a row can be selected);
|
`Default` filter — only on failure does it fall back to
|
||||||
`PriceCalculationSetup.Table.al` (`table 7006 "Price Calculation Setup"`:
|
`SetRange(Enabled, true)`, `SetRange(Default, true)`, `SetRange(Method,
|
||||||
`Code` (Code[100]), `Method` (Enum "Price Calculation Method"), `Type`
|
...)`); `PriceCalculationSetup.Table.al` (`table 7006 "Price Calculation
|
||||||
(Enum "Price Type"), `"Asset Type"` (Enum "Price Asset Type"),
|
Setup"`: `Code`, `Method`, `Type`, `"Asset Type"`, `Implementation`,
|
||||||
`Implementation` (Enum "Price Calculation Handler"), `Enabled` (Boolean),
|
`Enabled`, `Default`); `PriceCalculationDtldSetup.Codeunit.al` (`codeunit
|
||||||
`Default` (Boolean)).
|
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`
|
Microsoft Learn, "Extending Price Calculations": "Each codeunit that
|
||||||
(`enum 7009 "Price Type"`: `Any`(0)/`Sale`(1)/`Purchase`(2)).
|
implements the Price Calculation interface must subscribe to the
|
||||||
|
OnFindSupportedSetup() event... to fill the price calculation setup
|
||||||
Microsoft Learn, "Extending Price Calculations": "For the new codeunit,
|
table." Same article: "You can enter detailed setup records for
|
||||||
you must extend the Price Calculation Handler enum that implements Price
|
non-default setup lines... If a matching setup is found its
|
||||||
Calculation interface... Afterwards you can insert a record in the Price
|
implementation is used... If there is no matching setup exception, we
|
||||||
Calculation Setup table... Each codeunit that implements the Price
|
use the default implementation."
|
||||||
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)
|
(https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-extending-best-price-calculations)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
codeunit 50103 "Sample Navigate Subscribers"
|
||||||
{
|
{
|
||||||
// WRONG: registers the row, so it appears in the Find Entries result
|
// WRONG: registers the row, so it appears in the Find Entries result
|
||||||
|
|
|
||||||
|
|
@ -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"
|
codeunit 50103 "Sample Navigate Subscribers"
|
||||||
{
|
{
|
||||||
[EventSubscriber(ObjectType::Page, Page::Navigate, 'OnAfterFindRecords', '', false, false)]
|
[EventSubscriber(ObjectType::Page, Page::Navigate, 'OnAfterFindRecords', '', false, false)]
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ tableextension 50105 "Sample Sales Line Ext" extends "Sales Line"
|
||||||
// WRONG: no OnValidate trigger. The field is registered as a
|
// WRONG: no OnValidate trigger. The field is registered as a
|
||||||
// price source below via OnAfterAddSources, so new lines price
|
// price source below via OnAfterAddSources, so new lines price
|
||||||
// correctly - but changing this field on an existing line never
|
// correctly - but changing this field on an existing line never
|
||||||
// calls UpdateUnitPriceByField, so the unit price silently keeps
|
// triggers a recalculation (e.g. via UpdateUnitPrice), so the
|
||||||
// its old value.
|
// unit price silently keeps its old value.
|
||||||
field(50100; "Sample Loyalty Customer No."; Code[20])
|
field(50100; "Sample Loyalty Customer No."; Code[20])
|
||||||
{
|
{
|
||||||
Caption = 'Sample Loyalty Customer No.';
|
Caption = 'Sample Loyalty Customer No.';
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,16 @@ tableextension 50105 "Sample Sales Line Ext" extends "Sales Line"
|
||||||
// the field on an existing line never re-runs price
|
// the field on an existing line never re-runs price
|
||||||
// calculation, even though the source is already a known
|
// calculation, even though the source is already a known
|
||||||
// candidate via OnAfterAddSources below.
|
// 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;
|
end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,23 @@ being priced by it. `codeunit "Sales Line - Price"` publishes
|
||||||
`PriceSourceList.Add(SourceType, SourceNo)` makes the source a candidate
|
`PriceSourceList.Add(SourceType, SourceNo)` makes the source a candidate
|
||||||
the calculation considers. But nothing about that subscription causes
|
the calculation considers. But nothing about that subscription causes
|
||||||
the price to be *recalculated* when the source field's value changes on
|
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
|
an existing line. That's the second, separate piece, and it needs to be
|
||||||
`procedure UpdateUnitPriceByField(CalledByFieldNo: Integer)` must be
|
wired correctly: `Sales Line`'s `procedure
|
||||||
called from the source field's own trigger — the same way Microsoft's
|
UpdateUnitPriceByField(CalledByFieldNo: Integer)` only recalculates if
|
||||||
own Location example is wired from a `Sales Line` validation event, not
|
the field was already *planned* — internally it exits immediately unless
|
||||||
from the price source registration itself.
|
`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
|
Add the source without wiring recalculation, and the failure hides
|
||||||
easily: a *new* line still prices correctly, because the field already
|
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
|
`OnAfterAddSources` subscriber that adds it via `PriceSourceList.Add`, and
|
||||||
a trigger on the field itself (its own `OnValidate`, or a matching
|
a trigger on the field itself (its own `OnValidate`, or a matching
|
||||||
`OnAfterValidate` integration event) that calls
|
`OnAfterValidate` integration event) that calls
|
||||||
`SalesLine.UpdateUnitPriceByField(SalesLine.FieldNo(<TheField>))`.
|
`SalesLine.UpdateUnitPrice(SalesLine.FieldNo(<TheField>))`. 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).
|
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
|
## Anti Pattern
|
||||||
|
|
||||||
Subscribing to `OnAfterAddSources` to register a custom field as a price
|
Subscribing to `OnAfterAddSources` to register a custom field as a price
|
||||||
source, without also triggering recalculation from that field's own
|
source, without also triggering recalculation (via `UpdateUnitPrice`, or
|
||||||
validation. The field is a genuine, working calculation candidate — new
|
the `PlanPriceCalcByField` + `UpdateUnitPriceByField` pair) from that
|
||||||
lines price correctly — but editing the field on an existing line leaves
|
field's own validation. The field is a genuine, working calculation
|
||||||
the unit price stale, with nothing to indicate why.
|
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).
|
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`
|
PriceSourceList: Codeunit "Price Source List")`); `Pricing/Source/PriceSourceList.Codeunit.al`
|
||||||
(`procedure Add(SourceType: Enum "Price Source Type"; SourceNo: Code[20])`);
|
(`procedure Add(SourceType: Enum "Price Source Type"; SourceNo: Code[20])`);
|
||||||
`Sales/Document/SalesLine.Table.al` (`procedure
|
`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
|
Microsoft Learn, "Extending Price Calculations" (Location example): "To
|
||||||
recalculate the price, we can subscribe to events that pass the sales
|
recalculate the price, we can subscribe to events that pass the sales
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,23 @@ report 50110 "Sample Item Barcode Label"
|
||||||
trigger OnAfterGetRecord()
|
trigger OnAfterGetRecord()
|
||||||
begin
|
begin
|
||||||
// WRONG: hand-rolled "encoding" instead of the Barcode
|
// WRONG: hand-rolled "encoding" instead of the Barcode
|
||||||
// module's provider/encoder API. This produces a string
|
// module's provider/encoder API. This is not wrong merely
|
||||||
// that looks like a Code 39 barcode (asterisk delimiters)
|
// because the delimiter was added by hand - Code 39's own
|
||||||
// but carries none of the platform's actual character-set
|
// symbology does use "*" as its start/stop character
|
||||||
// or checksum handling - wrong regardless of which font
|
// (Microsoft Learn, "Barcode Fonts with Business Central
|
||||||
// is applied to it in the layout.
|
// 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." + '*';
|
BarcodeText := '*' + "No." + '*';
|
||||||
end;
|
end;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,32 +9,46 @@ report 50110 "Sample Item Barcode Label"
|
||||||
dataitem(Item; Item)
|
dataitem(Item; Item)
|
||||||
{
|
{
|
||||||
column(No_; "No.") { }
|
column(No_; "No.") { }
|
||||||
column(Barcode; BarcodeText) { }
|
column(Barcode1D; BarcodeText) { }
|
||||||
|
column(Barcode2D; QRCodeText) { }
|
||||||
|
|
||||||
trigger OnAfterGetRecord()
|
trigger OnAfterGetRecord()
|
||||||
var
|
var
|
||||||
BarcodeFontProvider: Interface "Barcode Font Provider";
|
BarcodeFontProvider: Interface "Barcode Font Provider";
|
||||||
|
BarcodeFontProvider2D: Interface "Barcode Font Provider 2D";
|
||||||
begin
|
begin
|
||||||
|
// One-dimensional: "Barcode Font Provider" declares both
|
||||||
|
// ValidateInput and EncodeFont - call both.
|
||||||
BarcodeFontProvider := Enum::"Barcode Font Provider"::IDAutomation1D;
|
BarcodeFontProvider := Enum::"Barcode Font Provider"::IDAutomation1D;
|
||||||
BarcodeFontProvider.ValidateInput("No.", BarcodeSymbology);
|
BarcodeFontProvider.ValidateInput("No.", BarcodeSymbology);
|
||||||
BarcodeText := BarcodeFontProvider.EncodeFont("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;
|
end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var
|
var
|
||||||
BarcodeSymbology: Enum "Barcode Symbology";
|
BarcodeSymbology: Enum "Barcode Symbology";
|
||||||
|
BarcodeSymbology2D: Enum "Barcode Symbology 2D";
|
||||||
BarcodeText: Text;
|
BarcodeText: Text;
|
||||||
|
QRCodeText: Text;
|
||||||
|
|
||||||
trigger OnInitReport()
|
trigger OnInitReport()
|
||||||
begin
|
begin
|
||||||
BarcodeSymbology := Enum::"Barcode Symbology"::Code39;
|
BarcodeSymbology := Enum::"Barcode Symbology"::Code39;
|
||||||
|
BarcodeSymbology2D := Enum::"Barcode Symbology 2D"::"QR-Code";
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Layout requirement (can't be enforced in AL, so it's stated here):
|
// 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
|
// name - IDAutomationHC39M for Code 39 - never an evaluation name
|
||||||
// like "IDAutomationSHC39M Demo". Per Microsoft Learn, using the
|
// like "IDAutomationSHC39M Demo". Per Microsoft Learn, using the
|
||||||
// evaluation name in a Business Central online production
|
// 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).
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
bc-version: [all]
|
bc-version: [all]
|
||||||
domain: data-modeling
|
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]
|
technologies: [al]
|
||||||
countries: [w1]
|
countries: [w1]
|
||||||
application-area: [all]
|
application-area: [all]
|
||||||
|
|
@ -12,85 +12,89 @@ application-area: [all]
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Business Central's barcode support lives in the System Application's
|
Business Central's barcode support lives in the System Application's
|
||||||
`Barcode` module (`src/System Application/App/Barcode`), not in a
|
`Barcode` module (`src/System Application/App/Barcode`): `interface
|
||||||
project's own code: `interface "Barcode Font Provider"` /
|
"Barcode Font Provider"` / `"Barcode Font Provider 2D"`, `enum "Barcode
|
||||||
`"Barcode Font Provider 2D"`, `enum "Barcode Symbology"` /
|
Symbology"` / `"Barcode Symbology 2D"`, and built-in implementations
|
||||||
`"Barcode Symbology 2D"` (Code39, Code128, EAN-13, QR-Code, Data Matrix,
|
(`codeunit 9215`/`9221`). A report encodes a data string via this API;
|
||||||
and more), and built-in implementations
|
the layout then displays it using a barcode *font*.
|
||||||
(`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:
|
The two interfaces are not symmetric: `"Barcode Font Provider"` (1D)
|
||||||
"With Business Central online, the IDAutomation fonts are automatically
|
declares both `ValidateInput` and `EncodeFont`; `"Barcode Font Provider
|
||||||
available as part of the service. So you can start adding barcodes to
|
2D"` declares only `EncodeFont` (see Source). BCApps' `Item GTIN Label`
|
||||||
reports right away." (Microsoft Learn, "Adding Barcodes to Reports") —
|
report reflects that split exactly — it validates then encodes through
|
||||||
unlike on-premises, where the fonts must be purchased and installed on
|
the 1D provider, but only encodes through the 2D provider, for the same
|
||||||
the server.
|
"No." value.
|
||||||
|
|
||||||
That ease hides a SaaS-specific trap in the one manual step the API
|
On Business Central online this needs no setup ("the IDAutomation fonts
|
||||||
doesn't cover: naming the actual font in the report layout. IDAutomation
|
are automatically available as part of the service" — Microsoft Learn),
|
||||||
ships both a purchased font and a same-looking evaluation font per
|
unlike on-premises, where fonts must be purchased and installed. That
|
||||||
version (Code 39: `IDAutomationHC39M` purchased vs.
|
ease hides a SaaS-specific trap the API doesn't cover: naming the actual
|
||||||
`IDAutomationSHC39M Demo` evaluation). Per Microsoft Learn ("Barcode
|
font. IDAutomation ships both a purchased font and a same-looking
|
||||||
Fonts with Business Central Online"): "When you're applying barcode font
|
evaluation font per version (Code 39: `IDAutomationHC39M` purchased vs.
|
||||||
in the report layout for a Business Central online production
|
`IDAutomationSHC39M Demo`) — per Microsoft Learn, "be sure to use the
|
||||||
environment, be sure to use the purchased font name; not the evaluation
|
purchased font name... If you use the evaluation font name, the barcode
|
||||||
font name. If you use the evaluation font name, the barcode won't
|
won't render." The wrong name produces nothing, in the layout not AL, so
|
||||||
render." Getting the font name wrong doesn't distort the barcode — it
|
no reviewer catches it reading the object.
|
||||||
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
|
## Best Practice
|
||||||
|
|
||||||
Encode through the real API — declare the provider via its interface and
|
Encode through the real API, matching the calls to what the chosen
|
||||||
enum, then call `ValidateInput`/`EncodeFont` — and treat naming the
|
interface actually declares. One-dimensional: declare `Interface
|
||||||
production font in the layout as an equally required part of the same
|
"Barcode Font Provider"` and call both `ValidateInput` and `EncodeFont`
|
||||||
task, not an afterthought left to whoever happens to touch the `.docx`/
|
— skipping validation lets a value outside the character set, or one
|
||||||
`.rdl` file. For a two-dimensional symbology other than Maxicode, the
|
needing a checksum setting never applied, reach the font unchecked.
|
||||||
font name to specify is literally `IDAutomation2D` (Maxicode itself uses
|
Two-dimensional: declare `Interface "Barcode Font Provider 2D"` and call
|
||||||
`IDAutomation2D MaxiCode`); for a one-dimensional symbology, use the
|
`EncodeFont` alone — there is no `ValidateInput` on this interface.
|
||||||
purchased version name for that specific font (e.g. `IDAutomationHC39M`
|
|
||||||
for Code 39), never a name containing `Demo`.
|
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).
|
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
|
## Anti Pattern
|
||||||
|
|
||||||
Constructing a barcode string by hand — string concatenation, manual
|
Constructing a barcode string by hand instead of using the module's
|
||||||
delimiters — instead of going through the Barcode module's provider
|
provider/encoder API — not because a manual delimiter is inherently
|
||||||
interface. It can look right (asterisks around a value, resembling
|
wrong (Code 39's own symbology does use `*` as start/stop; Microsoft
|
||||||
Code 39) while carrying none of the platform's actual character-set
|
Learn's font table says so), but because hand-rolled construction is
|
||||||
handling or checksum logic, so it's wrong regardless of which font is
|
demonstrably mismatched with what the real encoder produces: it skips
|
||||||
later applied to it.
|
`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
|
Flag demonstrably invalid or mismatched hand construction, not manual
|
||||||
through the real API, but naming the evaluation font instead of the
|
delimiter use as a category — a custom provider paired with a font that
|
||||||
purchased one in the layout. Both produce a report that looks complete
|
genuinely expects literal `*` delimiters is a different, legitimate case.
|
||||||
in review and testing and fails silently — the first because the encoded
|
|
||||||
data was never a real barcode, the second because Business Central
|
A second version of the same mistake: encoding correctly, but naming the
|
||||||
online refuses to render it at all.
|
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).
|
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
|
## Source
|
||||||
|
|
||||||
BCApps System Application (`src/System Application/App/Barcode/src/`):
|
BCApps (`src/System Application/App/Barcode/src/`):
|
||||||
`Barcode Provider/Font/BarcodeFontProvider.Interface.al`
|
`Barcode Provider/Font/BarcodeFontProvider.Interface.al` (1D:
|
||||||
(`ValidateInput(InputText: Text; BarcodeSymbology: Enum "Barcode Symbology")`,
|
`ValidateInput` + `EncodeFont`); `Barcode Provider 2D/Font/
|
||||||
`EncodeFont(InputText: Text; BarcodeSymbology: Enum "Barcode Symbology"): Text`),
|
BarcodeFontProvider2D.Interface.al` (2D: only `EncodeFont`); both read
|
||||||
`Barcode Provider/Font/BarcodeFontProvider.Enum.al`
|
fresh from source. `IDAutomation 1D Provider/Encoders/
|
||||||
(`value(0; IDAutomation1D)`), `Barcode Provider/BarcodeSymbology.Enum.al`
|
IDA1DCode39Encoder.Codeunit.al` (`codeunit 9204`, regex accepts literal
|
||||||
(`value(100; Code39)`). Real BaseApp usage:
|
`*` as plain input; `EncodeFont` → `DotNet FontEncoder.Code39`). Split
|
||||||
`src/Layers/W1/BaseApp/Inventory/Item/ItemGTINLabel.Report.al`
|
and delimiter mismatch both confirmed live: `.../Inventory/Item/
|
||||||
(`report 6625 "Item GTIN Label"`, lines 44-64).
|
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"
|
Microsoft Learn "Adding Barcodes to Reports" and "Barcode Fonts with
|
||||||
(https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-report-add-barcodes)
|
Business Central Online" — quoted above, incl. the Code39 row ("`*` is
|
||||||
and "Barcode Fonts with Business Central Online"
|
used for both start and stop delimiters").
|
||||||
(https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-report-barcode-fonts)
|
|
||||||
— both quoted verbatim above.
|
|
||||||
|
|
|
||||||
|
|
@ -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 `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.
|
- An `enumextension` extends `"Report Selection Usage"` and registers a report via `ReportSelections.InsertRecord`, without subscribing to the matching *single* counterparty's full triad — the filter event (`OnAfterFilterCustomerUsageReportSelections` on `page 9657` for a sales usage, `OnAfterFilterVendorUsageReportSelections` on `page 9658` for a purchase usage) AND the page-facing usage-enum map/validate events (`enumextension` on `"Custom Report Selection Sales"`/`"Report Selection Usage Vendor"` plus the matching map/validate subscribers) — `extend-report-selection-usage-for-new-document-types`. Requiring or wiring *both* counterparties by default for a one-sided document is also the anti-pattern (`ReportSelectionHandlerCZZ` partitions strictly by counterparty); only a genuinely two-sided usage (as `ReportSelectionHandlerCZC` demonstrates for Compensation) needs both.
|
||||||
- The same field number is added as a new field on two or more tables connected by a `TransferFields` call in a posting cascade (e.g. a header table and the posted-document table `SalesPost.Codeunit.al`/`PurchPost.Codeunit.al` transfer into), with a different data type or length on one side — `transferfields-mirrored-fields-must-match-type-and-length`. A field defined on only one side of the cascade is out of scope; this cues only on a field deliberately mirrored across the cascade with a type or length mismatch.
|
- The same field number is added as a new field on two or more tables connected by a `TransferFields` call in a posting cascade (e.g. a header table and the posted-document table `SalesPost.Codeunit.al`/`PurchPost.Codeunit.al` transfer into), with a different data type or length on one side — `transferfields-mirrored-fields-must-match-type-and-length`. A field defined on only one side of the cascade is out of scope; this cues only on a field deliberately mirrored across the cascade with a type or length mismatch.
|
||||||
- An `enumextension` extends `"Price Calculation Handler"` and implements the `Price Calculation` interface, without a matching `OnFindSupportedSetup` subscriber inserting a `Price Calculation Setup` record naming that implementation as the `Implementation` for a `Type`/`Asset Type`, with `Method` and `Default := true` also set — `activate-new-price-calculation-handler-via-onfindsupportedsetup`.
|
- An `enumextension` extends `"Price 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`.
|
- 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 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`.
|
- 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`.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue