mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 06:36:55 +01:00
Add foundational AL developer knowledge (#195)
Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
38ad6b8810
commit
dd833133e0
23 changed files with 483 additions and 4 deletions
|
|
@ -0,0 +1,28 @@
|
|||
table 50603 "Sample Order Header Bad"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "No."; Code[20])
|
||||
{
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
field(2; "Document Date"; Date)
|
||||
{
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnInsert()
|
||||
var
|
||||
SalesSetup: Record "Sales & Receivables Setup";
|
||||
NoSeries: Codeunit "No. Series";
|
||||
begin
|
||||
"Document Date" := WorkDate();
|
||||
|
||||
if "No." = '' then begin
|
||||
SalesSetup.Get();
|
||||
SalesSetup.TestField("Order Nos.");
|
||||
"No." := NoSeries.GetNextNo(SalesSetup."Order Nos.");
|
||||
end;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
table 50602 "Sample Order Header Good"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "No."; Code[20])
|
||||
{
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
field(2; "Document Date"; Date)
|
||||
{
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnInsert()
|
||||
var
|
||||
SalesSetup: Record "Sales & Receivables Setup";
|
||||
NoSeries: Codeunit "No. Series";
|
||||
begin
|
||||
if "No." = '' then begin
|
||||
SalesSetup.Get();
|
||||
SalesSetup.TestField("Order Nos.");
|
||||
"No." := NoSeries.GetNextNo(SalesSetup."Order Nos.");
|
||||
end;
|
||||
|
||||
InitRecord();
|
||||
end;
|
||||
|
||||
procedure InitRecord()
|
||||
begin
|
||||
OnBeforeInitRecord(Rec);
|
||||
"Document Date" := WorkDate();
|
||||
OnAfterInitRecord(Rec);
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnBeforeInitRecord(var SampleOrderHeader: Record "Sample Order Header Good")
|
||||
begin
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnAfterInitRecord(var SampleOrderHeader: Record "Sample Order Header Good")
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: data-modeling
|
||||
keywords: [document-header, initrecord, number-series, default-values, oninsert, initialization]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Initialize document defaults in `InitRecord` after assigning the number
|
||||
|
||||
## Description
|
||||
|
||||
Business Central document headers assign their number series first and then call an `InitRecord` procedure that owns the remaining business defaults, such as posting and document dates. Keeping that sequence and extensibility point makes initialization consistent for every creation path and lets extensions subscribe around one documented operation. Defaults scattered across page triggers or unrelated helpers can differ between UI, API, test, and background creation.
|
||||
|
||||
## Best Practice
|
||||
|
||||
In the document table's insert path, assign the document number and then call `InitRecord`. Keep the default assignments in that procedure and expose narrow before/after events when other extensions must participate.
|
||||
|
||||
See sample: [`initialize-document-defaults-in-initrecord.good.al`](initialize-document-defaults-in-initrecord.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Assigning document defaults in a page trigger, or scattering them directly through `OnInsert` with no `InitRecord` boundary. Non-page creation paths can then miss the defaults, and extensions have no stable initialization hook.
|
||||
|
||||
See sample: [`initialize-document-defaults-in-initrecord.bad.al`](initialize-document-defaults-in-initrecord.bad.al).
|
||||
|
||||
## Reference
|
||||
|
||||
[Use the InitRecord function](https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/3-use-initrecord-function)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
codeunit 50601 "Directed Rounding Bad"
|
||||
{
|
||||
procedure FloorAmount(Value: Decimal; Precision: Decimal): Decimal
|
||||
begin
|
||||
// For negative values, '<' rounds toward zero rather than toward negative infinity.
|
||||
exit(Round(Value, Precision, '<'));
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
codeunit 50600 "Directed Rounding Good"
|
||||
{
|
||||
procedure RoundAmount(Value: Decimal; Precision: Decimal; IncreaseMagnitude: Boolean): Decimal
|
||||
begin
|
||||
if IncreaseMagnitude then
|
||||
exit(Round(Value, Precision, '>'));
|
||||
|
||||
exit(Round(Value, Precision, '<'));
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: data-modeling
|
||||
keywords: [round, rounding, direction, precision, negative-decimal, amount]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# `Round` direction symbols follow magnitude, not mathematical ordering
|
||||
|
||||
## Description
|
||||
|
||||
AL's `Round(Number, Precision, Direction)` uses `'>'` to round away from zero and `'<'` to round toward zero. For a negative value this reverses mathematical ordering: `Round(-1234.56789, 0.001, '<')` returns `-1234.567`, while direction `'>'` returns `-1234.568`. Code that treats the symbols as mathematical ceiling and floor produces sign-dependent amount errors, commonly on credit documents and negative adjustments.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Choose the direction from the business meaning: `'>'` increases absolute magnitude and `'<'` decreases absolute magnitude for both positive and negative values. Include positive and negative cases whenever a directed rounding rule is tested.
|
||||
|
||||
See sample: [`round-direction-symbols-use-magnitude.good.al`](round-direction-symbols-use-magnitude.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Using `'<'` as a mathematical floor or `'>'` as a mathematical ceiling. The result looks correct for positive amounts but moves in the opposite mathematical direction for negative amounts.
|
||||
|
||||
See sample: [`round-direction-symbols-use-magnitude.bad.al`](round-direction-symbols-use-magnitude.bad.al).
|
||||
|
||||
## Reference
|
||||
|
||||
[Use the Round function](https://learn.microsoft.com/en-us/training/modules/use-document-standards-business-central/4a-use-round-function)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
interface "I Quote Amount Bad"
|
||||
{
|
||||
procedure GetAmount(): Decimal;
|
||||
}
|
||||
|
||||
interface "I Quote Date Bad"
|
||||
{
|
||||
procedure GetDate(): Date;
|
||||
}
|
||||
|
||||
codeunit 50611 "Quote Reader Bad"
|
||||
{
|
||||
procedure GetDate(Quote: Interface "I Quote Amount Bad"): Date
|
||||
var
|
||||
DatedQuote: Interface "I Quote Date Bad";
|
||||
begin
|
||||
DatedQuote := Quote as "I Quote Date Bad";
|
||||
exit(DatedQuote.GetDate());
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
interface "I Quote Amount Good"
|
||||
{
|
||||
procedure GetAmount(): Decimal;
|
||||
}
|
||||
|
||||
interface "I Quote Date Good"
|
||||
{
|
||||
procedure GetDate(): Date;
|
||||
}
|
||||
|
||||
codeunit 50610 "Quote Reader Good"
|
||||
{
|
||||
procedure TryGetDate(Quote: Interface "I Quote Amount Good"; var QuoteDate: Date): Boolean
|
||||
var
|
||||
DatedQuote: Interface "I Quote Date Good";
|
||||
begin
|
||||
if not (Quote is "I Quote Date Good") then
|
||||
exit(false);
|
||||
|
||||
DatedQuote := Quote as "I Quote Date Good";
|
||||
QuoteDate := DatedQuote.GetDate();
|
||||
exit(true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [25..]
|
||||
domain: interfaces
|
||||
keywords: [interface, is-operator, as-operator, type-test, cast, variant, runtime-error]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Guard optional interface casts with `is`
|
||||
|
||||
## Description
|
||||
|
||||
From runtime 15.0, AL can type-test an interface or `Variant` with `is` and cast it to another interface with `as`. The test is non-throwing, but `as` raises a runtime error when the underlying codeunit does not implement the target interface. This matters when an extended capability is optional or implementations can come from other extensions.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Use `is` to establish that the value supports the target interface before using `as`. Cast directly only where the target implementation is an invariant guaranteed by the surrounding contract.
|
||||
|
||||
See sample: [`guard-interface-casts-with-is.good.al`](guard-interface-casts-with-is.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Using `as` unconditionally for an optional extended interface. An otherwise valid implementation of the base interface then fails at runtime merely because it does not implement the additional contract.
|
||||
|
||||
See sample: [`guard-interface-casts-with-is.bad.al`](guard-interface-casts-with-is.bad.al).
|
||||
|
||||
## Reference
|
||||
|
||||
[Understand type testing and casting operators for interfaces](https://learn.microsoft.com/en-us/training/modules/business-central-interfaces/type-testing)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
tableextension 50622 "Ship-to Dropdown Bad" extends "Ship-to Address"
|
||||
{
|
||||
fieldgroups
|
||||
{
|
||||
addlast(DropDown; "Address 2")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
tableextension 50620 "Ship-to Dropdown Good" extends "Ship-to Address"
|
||||
{
|
||||
fieldgroups
|
||||
{
|
||||
addlast(DropDown; "Address 2")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageextension 50621 "Ship-to Lookup Good" extends "Ship-to Address List"
|
||||
{
|
||||
layout
|
||||
{
|
||||
modify("Address 2")
|
||||
{
|
||||
Visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: ui
|
||||
keywords: [fieldgroup, dropdown, addlast, lookup-page, visible, tableextension, pageextension]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# A `DropDown` field remains hidden when its lookup-page control is hidden
|
||||
|
||||
## Description
|
||||
|
||||
A tableextension can append a field to the `DropDown` field group with `addlast`, but the client still omits that field when its control on the underlying lookup page has `Visible = false`. Changing only the table field group therefore compiles while producing no visible UI change. The field-group name is case-sensitive and must be written as `DropDown`.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When adding a hidden field to a `DropDown` field group, also extend the page used for the lookup and make that field control visible. Verify the actual lookup page rather than assuming the table definition alone controls the drop-down.
|
||||
|
||||
See sample: [`dropdown-fieldgroup-respects-lookup-page-visibility.good.al`](dropdown-fieldgroup-respects-lookup-page-visibility.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Adding the field with `addlast(DropDown; ...)` while leaving its lookup-page control hidden, then expecting the field to appear in the drop-down.
|
||||
|
||||
See sample: [`dropdown-fieldgroup-respects-lookup-page-visibility.bad.al`](dropdown-fieldgroup-respects-lookup-page-visibility.bad.al).
|
||||
|
||||
## Reference
|
||||
|
||||
[Add a new FieldGroup to an existing table](https://learn.microsoft.com/en-us/training/modules/extend-modify-existing-table/add-field-group)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
page 50631 "Sample Order Bad"
|
||||
{
|
||||
PageType = Document;
|
||||
SourceTable = "Sales Header";
|
||||
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
group(General)
|
||||
{
|
||||
field(Amount; Rec.Amount)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the total amount of the order.';
|
||||
}
|
||||
}
|
||||
part(Lines; "Sales Order Subform")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
SubPageLink = "Document Type" = field("Document Type"),
|
||||
"Document No." = field("No.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
page 50630 "Sample Order Good"
|
||||
{
|
||||
PageType = Document;
|
||||
SourceTable = "Sales Header";
|
||||
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
group(General)
|
||||
{
|
||||
field(Amount; Rec.Amount)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the total amount of the order.';
|
||||
}
|
||||
}
|
||||
part(Lines; "Sales Order Subform")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
SubPageLink = "Document Type" = field("Document Type"),
|
||||
"Document No." = field("No.");
|
||||
UpdatePropagation = Both;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: ui
|
||||
keywords: [updatepropagation, page-part, subpage, main-page, refresh, flowfield, document-lines]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use `UpdatePropagation = Both` when line edits must refresh the main page
|
||||
|
||||
## Description
|
||||
|
||||
A page part does not automatically refresh its parent page when the subpage changes. `UpdatePropagation = Subpage` updates only the part; `Both` also refreshes the main page. Without `Both`, header totals, FlowFields, and FactBoxes that depend on edited lines can remain stale until another user action refreshes the page.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Set `UpdatePropagation = Both` on a part when edits in that subpage must immediately update values rendered by the main page. Leave propagation at `Subpage` when the parent has no dependent presentation to avoid unnecessary refreshes.
|
||||
|
||||
See sample: [`updatepropagation-both-refreshes-main-page.good.al`](updatepropagation-both-refreshes-main-page.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Displaying a line-dependent total on the main page while the editable lines part updates only itself. The persisted values can be correct while the parent page continues to show an old total.
|
||||
|
||||
See sample: [`updatepropagation-both-refreshes-main-page.bad.al`](updatepropagation-both-refreshes-main-page.bad.al).
|
||||
|
||||
## Reference
|
||||
|
||||
[Set different control properties](https://learn.microsoft.com/en-us/training/modules/work-with-pages/8-controls)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: upgrade
|
||||
keywords: [appversion, dataversion, moduleinfo, install-codeunit, upgrade-codeunit, version-context]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# `ModuleInfo.AppVersion` changes meaning with execution context
|
||||
|
||||
## Description
|
||||
|
||||
`ModuleInfo.AppVersion()` is the installed version during normal operation, the version being installed inside install code, and the target version inside upgrade code. It is therefore not the source data version during an upgrade. In upgrade code, `DataVersion()` describes the version of the existing data, whether from the currently installed app or the version most recently uninstalled.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Interpret `AppVersion()` as the code package entering the context and `DataVersion()` as the existing data state. Prefer upgrade tags for controlling individual migration steps; when version information is needed for diagnostics or preconditions, name variables so target app version and source data version cannot be confused.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Reading `AppVersion()` from an upgrade codeunit and treating it as the version being upgraded from. The comparison actually observes the target package and can skip or misroute migration logic.
|
||||
|
||||
## Reference
|
||||
|
||||
[Create proper installation and upgrade codeunits](https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/3-installation-upgrade-codeunits)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
codeunit 50641 "Sample Upgrade Part One"
|
||||
{
|
||||
Subtype = Upgrade;
|
||||
|
||||
trigger OnUpgradePerCompany()
|
||||
begin
|
||||
CreateUpgradeState();
|
||||
end;
|
||||
|
||||
local procedure CreateUpgradeState()
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
||||
codeunit 50642 "Sample Upgrade Part Two"
|
||||
{
|
||||
Subtype = Upgrade;
|
||||
|
||||
trigger OnUpgradePerCompany()
|
||||
begin
|
||||
// This can run before Part One; object IDs do not sequence upgrade codeunits.
|
||||
MigrateDataThatRequiresUpgradeState();
|
||||
end;
|
||||
|
||||
local procedure MigrateDataThatRequiresUpgradeState()
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
codeunit 50640 "Sample Upgrade Good"
|
||||
{
|
||||
Subtype = Upgrade;
|
||||
|
||||
trigger OnUpgradePerCompany()
|
||||
begin
|
||||
CreateUpgradeState();
|
||||
MigrateDependentData();
|
||||
end;
|
||||
|
||||
local procedure CreateUpgradeState()
|
||||
begin
|
||||
end;
|
||||
|
||||
local procedure MigrateDependentData()
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: upgrade
|
||||
keywords: [install-codeunit, upgrade-codeunit, execution-order, subtype-install, subtype-upgrade, sequencing]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Separate install or upgrade codeunits have no execution order
|
||||
|
||||
## Description
|
||||
|
||||
An extension can contain multiple `Install` or `Upgrade` codeunits, but Business Central does not guarantee the order in which codeunits of the same subtype execute. Upgrade trigger phases are ordered globally, yet one codeunit's `OnUpgradePerCompany` must not assume another codeunit's same-phase trigger already ran. Object ID and source-file order do not provide sequencing.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Keep separate install or upgrade codeunits independent. When two steps have a real dependency, coordinate them from one owning trigger in the required order; use upgrade tags to make each completed step idempotent.
|
||||
|
||||
See sample: [`install-and-upgrade-codeunits-have-no-order.good.al`](install-and-upgrade-codeunits-have-no-order.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Splitting dependent steps into separate codeunits and relying on names, object IDs, or declaration order. The dependent codeunit can run first and fail or observe partially migrated data.
|
||||
|
||||
See sample: [`install-and-upgrade-codeunits-have-no-order.bad.al`](install-and-upgrade-codeunits-have-no-order.bad.al).
|
||||
|
||||
## Reference
|
||||
|
||||
[Create proper installation and upgrade codeunits](https://learn.microsoft.com/en-us/training/modules/easy-application-upgrade/3-installation-upgrade-codeunits)
|
||||
Loading…
Add table
Add a link
Reference in a new issue