Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion evaluation/review-fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,19 @@
},
"data-modeling": {
"articles": [
"activate-new-price-calculation-handler-via-onfindsupportedsetup",
"check-blocked-in-referencing-code-not-in-master",
"code-must-not-change-workdate",
"custom-document-dispatch-must-not-bypass-report-selections",
"document-print-and-email-actions-call-report-selections-directly",
"extend-find-entries-navigate-for-new-document-types",
"extend-price-source-type-must-sync-document-subset-enum",
"extend-report-selection-usage-for-new-document-types",
"new-price-source-must-add-candidate-and-trigger-recalculation",
"pictures-must-use-media-not-blob",
"table-design-must-match-bc-table-type-conventions"
"report-barcodes-must-use-barcode-module-and-production-font-name",
"table-design-must-match-bc-table-type-conventions",
"transferfields-mirrored-fields-must-match-type-and-length"
]
},
"error-handling": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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";
}
}

// 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, 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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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";
}
}

// 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)]
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;
// 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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
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.

`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

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`. `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).

## 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`](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`
(`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).

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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
codeunit 50102 "Sample Posted Invoice Send"
{
procedure SendPostedInvoice(SalesInvoiceHeader: Record "Sales Invoice Header")
var
Customer: Record Customer;
begin
Customer.Get(SalesInvoiceHeader."Bill-to Customer No.");
Customer.TestField("E-Mail");

// WRONG: the report is hardcoded instead of resolved through the
// registered "S.Invoice" usage in Report Selections. This alone is
// the defect - no hand-built email is needed for it: a Report
// Selections row or a per-customer "Document Layouts" override
// that points this usage at a different report or layout is
// silently ignored, and the only way to change what this code
// prints is a code change and a new release.
SalesInvoiceHeader.SetRecFilter();
Report.RunModal(Report::"Standard Sales - Invoice", false, false, SalesInvoiceHeader);
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
codeunit 50102 "Sample Posted Invoice Send"
{
procedure SendPostedInvoice(SalesInvoiceHeader: Record "Sales Invoice Header")
var
ReportSelections: Record "Report Selections";
ReportDistributionMgt: Codeunit "Report Distribution Management";
begin
// Custom validation specific to this dispatch stays here...
CheckReadyToSend(SalesInvoiceHeader);

// ...but dispatch goes through the registered usage. "S.Invoice"
// resolves to a report built on "Sales Invoice Header" (by default
// report 1306 "Standard Sales - Invoice"), so the record passed in
// matches what the selected report expects, and per-account
// report/layout overrides and email attachment/body configuration
// on Report Selections all apply automatically.
SalesInvoiceHeader.SetRecFilter();
ReportSelections.SendEmailToCust(
"Report Selection Usage"::"S.Invoice".AsInteger(), SalesInvoiceHeader, SalesInvoiceHeader."No.",
ReportDistributionMgt.GetFullDocumentTypeText(SalesInvoiceHeader), true,
SalesInvoiceHeader."Bill-to Customer No.");
end;

local procedure CheckReadyToSend(SalesInvoiceHeader: Record "Sales Invoice Header")
var
Customer: Record Customer;
begin
Customer.Get(SalesInvoiceHeader."Bill-to Customer No.");
Customer.TestField("E-Mail");
end;
}
Loading
Loading