From 18cb503540c82c806b2de9b7bac374828aeabca8 Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Wed, 1 Jul 2026 11:00:05 +0200 Subject: [PATCH] Add data-modeling and appsource knowledge articles (MICROSOFT layer) Author 7 remedial BCQuality knowledge articles plus good/bad AL samples (21 files) covering AL master-table and data-model design: - data-modeling: master No. from number series in OnInsert; use codeunit "No. Series" not obsolete NoSeriesManagement; setup table is a singleton; set Last Date Modified in OnModify and OnRename; enforce Blocked in referencing code not in the master. - style: ApplicationArea required on page controls (AS0062). - appsource: object affixes prevent collisions (AS0011). Clean-room authored from own BC knowledge; specifics verified against public sources only (learn.microsoft.com, microsoft/BCApps). Introduces two new domains (data-modeling, appsource). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../object-affixes-prevent-collisions.bad.al | 43 ++++++++++ .../object-affixes-prevent-collisions.good.al | 40 +++++++++ .../object-affixes-prevent-collisions.md | 28 +++++++ ...d-in-referencing-code-not-in-master.bad.al | 71 ++++++++++++++++ ...-in-referencing-code-not-in-master.good.al | 84 +++++++++++++++++++ ...ocked-in-referencing-code-not-in-master.md | 28 +++++++ ...e-no-from-number-series-in-oninsert.bad.al | 31 +++++++ ...-no-from-number-series-in-oninsert.good.al | 45 ++++++++++ ...table-no-from-number-series-in-oninsert.md | 28 +++++++ ...e-modified-in-onmodify-and-onrename.bad.al | 39 +++++++++ ...-modified-in-onmodify-and-onrename.good.al | 40 +++++++++ ...-date-modified-in-onmodify-and-onrename.md | 28 +++++++ .../setup-table-is-a-singleton.bad.al | 55 ++++++++++++ .../setup-table-is-a-singleton.good.al | 70 ++++++++++++++++ .../setup-table-is-a-singleton.md | 28 +++++++ ...ies-codeunit-not-noseriesmanagement.bad.al | 51 +++++++++++ ...es-codeunit-not-noseriesmanagement.good.al | 55 ++++++++++++ ...-series-codeunit-not-noseriesmanagement.md | 28 +++++++ ...ationarea-required-on-page-controls.bad.al | 25 ++++++ ...tionarea-required-on-page-controls.good.al | 40 +++++++++ ...plicationarea-required-on-page-controls.md | 28 +++++++ 21 files changed, 885 insertions(+) create mode 100644 microsoft/knowledge/appsource/object-affixes-prevent-collisions.bad.al create mode 100644 microsoft/knowledge/appsource/object-affixes-prevent-collisions.good.al create mode 100644 microsoft/knowledge/appsource/object-affixes-prevent-collisions.md create mode 100644 microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.bad.al create mode 100644 microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.good.al create mode 100644 microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.md create mode 100644 microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.bad.al create mode 100644 microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.good.al create mode 100644 microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.md create mode 100644 microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.bad.al create mode 100644 microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.good.al create mode 100644 microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.md create mode 100644 microsoft/knowledge/data-modeling/setup-table-is-a-singleton.bad.al create mode 100644 microsoft/knowledge/data-modeling/setup-table-is-a-singleton.good.al create mode 100644 microsoft/knowledge/data-modeling/setup-table-is-a-singleton.md create mode 100644 microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.bad.al create mode 100644 microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.good.al create mode 100644 microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.md create mode 100644 microsoft/knowledge/style/applicationarea-required-on-page-controls.bad.al create mode 100644 microsoft/knowledge/style/applicationarea-required-on-page-controls.good.al create mode 100644 microsoft/knowledge/style/applicationarea-required-on-page-controls.md diff --git a/microsoft/knowledge/appsource/object-affixes-prevent-collisions.bad.al b/microsoft/knowledge/appsource/object-affixes-prevent-collisions.bad.al new file mode 100644 index 0000000..dc1c6f3 --- /dev/null +++ b/microsoft/knowledge/appsource/object-affixes-prevent-collisions.bad.al @@ -0,0 +1,43 @@ +// Anti-pattern: an own object with no affix. Another app that also defines a +// "Loyalty Tier" table cannot be installed alongside this one. +table 50379 "Loyalty Tier" +{ + Caption = 'Loyalty Tier'; + DataClassification = CustomerContent; + + fields + { + field(1; "Code"; Code[20]) + { + Caption = 'Code'; + } + field(10; Description; Text[100]) + { + Caption = 'Description'; + } + } + + keys + { + key(PK; "Code") + { + Clustered = true; + } + } +} + +// Anti-pattern (the common half-measure): the extension object carries the +// affix, but the field it adds to the standard Customer table does not. That +// unaffixed field still collides with any other app that adds "Loyalty Points" +// to Customer, and AS0011 flags it. +tableextension 50378 "ABC Customer Ext" extends Customer +{ + fields + { + field(50378; "Loyalty Points"; Integer) + { + Caption = 'Loyalty Points'; + DataClassification = CustomerContent; + } + } +} diff --git a/microsoft/knowledge/appsource/object-affixes-prevent-collisions.good.al b/microsoft/knowledge/appsource/object-affixes-prevent-collisions.good.al new file mode 100644 index 0000000..28bfa4d --- /dev/null +++ b/microsoft/knowledge/appsource/object-affixes-prevent-collisions.good.al @@ -0,0 +1,40 @@ +// Own object: the affix "ABC" is carried at object-name level. +table 50377 "ABC Loyalty Tier" +{ + Caption = 'Loyalty Tier'; + DataClassification = CustomerContent; + + fields + { + field(1; "Code"; Code[20]) + { + Caption = 'Code'; + } + field(10; Description; Text[100]) + { + Caption = 'Description'; + } + } + + keys + { + key(PK; "Code") + { + Clustered = true; + } + } +} + +// Extension of a standard object: the added field is individually affixed, +// because the object name (Customer) belongs to the base application. +tableextension 50376 "ABC Customer Ext" extends Customer +{ + fields + { + field(50376; "Loyalty Points ABC"; Integer) + { + Caption = 'Loyalty Points'; + DataClassification = CustomerContent; + } + } +} diff --git a/microsoft/knowledge/appsource/object-affixes-prevent-collisions.md b/microsoft/knowledge/appsource/object-affixes-prevent-collisions.md new file mode 100644 index 0000000..49ef80c --- /dev/null +++ b/microsoft/knowledge/appsource/object-affixes-prevent-collisions.md @@ -0,0 +1,28 @@ +--- +bc-version: [all] +domain: appsource +keywords: [object-affix, prefix, suffix, as0011, appsourcecop, collision, tableextension] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Apply a reserved affix to objects and to members added to base objects + +## Description + +An AppSource extension must carry a reserved affix — a prefix or a suffix of at least three characters — on the names of the objects it owns **and** on any field, key, control, or action it adds to a base-application object. The affix is registered with Microsoft; when two coexisting extensions would otherwise collide, the registrant of the affix wins. Without it, two apps that both add a `Loyalty Points` field to `Customer`, or both define a `Loyalty Tier` table, cannot be installed side by side. + +AppSourceCop enforces this. The primary rule is AS0011 ("An affix is required"); the affixes are configured through `mandatoryAffixes` (and `mandatoryPrefix`) in `AppSourceCop.json`. Two placements matter and are easy to get half-right: an object you define carries the affix at **object-name** level, while a member you add to a **standard** object carries the affix on that **member's** name. Adding an affixed object is not enough — an unaffixed field bolted onto `Customer` still collides and still fails validation. + +## Best Practice + +Own objects are named with the affix (e.g. a table `ABC Loyalty Tier`), and every field or action added to a standard object is individually affixed (e.g. `Loyalty Points ABC` on a `Customer` tableextension). + +See sample: `object-affixes-prevent-collisions.good.al`. + +## Anti Pattern + +Unaffixed object or member names, or the common half-measure: the extension object carries the affix but a field it adds to a standard table does not. AS0011 flags the missing affix and the field can still collide with another app. + +See sample: `object-affixes-prevent-collisions.bad.al`. diff --git a/microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.bad.al b/microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.bad.al new file mode 100644 index 0000000..7cf9d52 --- /dev/null +++ b/microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.bad.al @@ -0,0 +1,71 @@ +table 50372 "Loyalty Member" +{ + Caption = 'Loyalty Member'; + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) + { + Caption = 'No.'; + } + field(10; Name; Text[100]) + { + Caption = 'Name'; + } + field(20; Blocked; Boolean) + { + Caption = 'Blocked'; + } + } + + keys + { + key(PK; "No.") + { + Clustered = true; + } + } + + // Anti-pattern: the block check sits in the master's own trigger. Editing a + // blocked member is rare; referencing it is constant, and references never + // fire OnModify. So this stops nothing that matters. + trigger OnModify() + begin + TestField(Blocked, false); + end; +} + +table 50373 "Loyalty Point Entry" +{ + Caption = 'Loyalty Point Entry'; + DataClassification = CustomerContent; + + fields + { + field(1; "Entry No."; Integer) + { + Caption = 'Entry No.'; + AutoIncrement = true; + } + field(10; "Member No."; Code[20]) + { + Caption = 'Member No.'; + TableRelation = "Loyalty Member"."No."; + // No block check on the referencing side: a line can freely + // reference a blocked member, and posting proceeds unchecked. + } + field(20; Points; Integer) + { + Caption = 'Points'; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + } +} diff --git a/microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.good.al b/microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.good.al new file mode 100644 index 0000000..f76a46a --- /dev/null +++ b/microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.good.al @@ -0,0 +1,84 @@ +table 50370 "Loyalty Member" +{ + Caption = 'Loyalty Member'; + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) + { + Caption = 'No.'; + } + field(10; Name; Text[100]) + { + Caption = 'Name'; + } + // Blocked is inert data here: the master carries the flag but no logic. + field(20; Blocked; Boolean) + { + Caption = 'Blocked'; + } + } + + keys + { + key(PK; "No.") + { + Clustered = true; + } + } +} + +table 50371 "Loyalty Point Entry" +{ + Caption = 'Loyalty Point Entry'; + DataClassification = CustomerContent; + + fields + { + field(1; "Entry No."; Integer) + { + Caption = 'Entry No.'; + AutoIncrement = true; + } + field(10; "Member No."; Code[20]) + { + Caption = 'Member No.'; + TableRelation = "Loyalty Member"."No."; + + trigger OnValidate() + var + LoyaltyMember: Record "Loyalty Member"; + begin + if "Member No." = '' then + exit; + // Enforcement lives at the point of use: reject a blocked master + // as soon as a line references it. + LoyaltyMember.Get("Member No."); + LoyaltyMember.TestField(Blocked, false); + end; + } + field(20; Points; Integer) + { + Caption = 'Points'; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + } + + procedure Post() + var + LoyaltyMember: Record "Loyalty Member"; + begin + // Re-check before committing the transaction, in case the member was + // blocked after the line was created. + LoyaltyMember.Get("Member No."); + LoyaltyMember.TestField(Blocked, false); + end; +} diff --git a/microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.md b/microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.md new file mode 100644 index 0000000..e324d45 --- /dev/null +++ b/microsoft/knowledge/data-modeling/check-blocked-in-referencing-code-not-in-master.md @@ -0,0 +1,28 @@ +--- +bc-version: [all] +domain: data-modeling +keywords: [blocked-field, testfield, referencing-code, point-of-use, enforcement, journal-line] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Enforce `Blocked` where the master is used, not in the master itself + +## Description + +The `Blocked` field on a master record (`Item`, `Customer`, `Resource`, or a custom master) is inert data. The master table holds **no** logic that acts on it. Enforcement belongs in the **consuming** code: when a journal line, document line, or posting routine references the master by its `No.`, that referencing object tests the flag at the point of use, e.g. `LoyaltyMember.Get("Member No."); LoyaltyMember.TestField(Blocked, false);` in the line's `OnValidate` and again before posting. + +Putting the block check inside the master's own `OnInsert`/`OnModify` does nothing to stop transactional use: a blocked master is edited rarely, but it is *referenced* constantly, and those references never touch the master's own triggers. Base BC follows this split — `Item.Blocked` is checked by sales/purchase/journal code, not by the `Item` table. A boolean `Blocked` uses `TestField(Blocked, false)`; an option-style block (e.g. `Sales`/`All`) needs the specific option compared at each relevant path. + +## Best Practice + +The referencing line validates `Master.TestField(Blocked, false)` in `OnValidate` of the reference field and re-checks before posting. The master table stays logic-free on `Blocked`. + +See sample: `check-blocked-in-referencing-code-not-in-master.good.al`. + +## Anti Pattern + +The block check sits in the master's own `OnModify`/`OnInsert` (so referencing and posting proceed unchecked), or there is no check at all on the referencing side. + +See sample: `check-blocked-in-referencing-code-not-in-master.bad.al`. diff --git a/microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.bad.al b/microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.bad.al new file mode 100644 index 0000000..307b925 --- /dev/null +++ b/microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.bad.al @@ -0,0 +1,31 @@ +table 50361 "Loyalty Member" +{ + Caption = 'Loyalty Member'; + DataClassification = CustomerContent; + + fields + { + // Anti-pattern: an autoincrement Integer surrogate used as the business key. + field(1; "Entry No."; Integer) + { + Caption = 'Entry No.'; + AutoIncrement = true; + } + field(10; Name; Text[100]) + { + Caption = 'Name'; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + } + + // No OnInsert, no number series, no "No." code, and no "No. Series" field. + // Records get an opaque integer users never see and cannot quote on the phone, + // and the master is cut off from BC's standard numbering and manual-entry flow. +} diff --git a/microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.good.al b/microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.good.al new file mode 100644 index 0000000..5bc49d3 --- /dev/null +++ b/microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.good.al @@ -0,0 +1,45 @@ +table 50360 "Loyalty Member" +{ + Caption = 'Loyalty Member'; + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) + { + Caption = 'No.'; + NotBlank = true; + } + field(2; "No. Series"; Code[20]) + { + Caption = 'No. Series'; + Editable = false; + TableRelation = "No. Series"; + } + field(10; Name; Text[100]) + { + Caption = 'Name'; + } + } + + keys + { + key(PK; "No.") + { + Clustered = true; + } + } + + trigger OnInsert() + var + LoyaltySetup: Record "Loyalty Setup"; + NoSeries: Codeunit "No. Series"; + begin + if "No." = '' then begin + LoyaltySetup.Get(); + LoyaltySetup.TestField("Member Nos."); + "No. Series" := LoyaltySetup."Member Nos."; + "No." := NoSeries.GetNextNo("No. Series"); + end; + end; +} diff --git a/microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.md b/microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.md new file mode 100644 index 0000000..f4c6a15 --- /dev/null +++ b/microsoft/knowledge/data-modeling/master-table-no-from-number-series-in-oninsert.md @@ -0,0 +1,28 @@ +--- +bc-version: [all] +domain: data-modeling +keywords: [no-series, primary-key, code20, oninsert, autoincrement, number-assignment] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# A master table's `No.` primary key comes from a number series in `OnInsert` + +## Description + +In Business Central, a master table (Customer, Vendor, Item, and any custom equivalent) uses a single primary-key field named `No.` of type `Code[20]`. It is populated from a number series — configured on the feature's application-area setup table — inside the table's `OnInsert` trigger, but only when `No.` is still blank (so a user may still type a manual number when the series allows it). The record also keeps a non-editable `No. Series` `Code[20]` field recording which series produced the number. + +This is not an `Integer` `AutoIncrement` key, a GUID, or the `SystemId`. Those are surrogate/system identifiers that users never see and cannot quote; BC's whole document flow — lookups, filtering, printed references, telephone support — depends on a short, human-readable, business-controlled `No.`. Use the modern assignment API described in `use-no-series-codeunit-not-noseriesmanagement.md`. + +## Best Practice + +`No.` `Code[20]` is the sole primary key; a non-editable `No. Series` `Code[20]` field records the source series. `OnInsert` checks `if "No." = ''`, reads the setup table, `TestField`s the configured series, stores it in `No. Series`, and assigns `No.` from the series. + +See sample: `master-table-no-from-number-series-in-oninsert.good.al`. + +## Anti Pattern + +An `Integer` `AutoIncrement` (or GUID / `SystemId`) primary key used as the business key, with no `OnInsert` number assignment. Records get an opaque identifier no user can reference, and the master no longer participates in the standard numbering and manual-entry behavior every other BC master follows. + +See sample: `master-table-no-from-number-series-in-oninsert.bad.al`. diff --git a/microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.bad.al b/microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.bad.al new file mode 100644 index 0000000..c2031a0 --- /dev/null +++ b/microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.bad.al @@ -0,0 +1,39 @@ +table 50369 "Loyalty Member" +{ + Caption = 'Loyalty Member'; + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) + { + Caption = 'No.'; + } + field(10; Name; Text[100]) + { + Caption = 'Name'; + } + field(20; "Last Date Modified"; Date) + { + Caption = 'Last Date Modified'; + Editable = false; + } + } + + keys + { + key(PK; "No.") + { + Clustered = true; + } + } + + trigger OnModify() + begin + "Last Date Modified" := Today(); + end; + + // Missing OnRename: renaming the member changes the primary key without + // firing OnModify, so "Last Date Modified" keeps its old, stale value and + // change-detection logic downstream skips the renamed record. +} diff --git a/microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.good.al b/microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.good.al new file mode 100644 index 0000000..18e1434 --- /dev/null +++ b/microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.good.al @@ -0,0 +1,40 @@ +table 50368 "Loyalty Member" +{ + Caption = 'Loyalty Member'; + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) + { + Caption = 'No.'; + } + field(10; Name; Text[100]) + { + Caption = 'Name'; + } + field(20; "Last Date Modified"; Date) + { + Caption = 'Last Date Modified'; + Editable = false; + } + } + + keys + { + key(PK; "No.") + { + Clustered = true; + } + } + + trigger OnModify() + begin + "Last Date Modified" := Today(); + end; + + trigger OnRename() + begin + "Last Date Modified" := Today(); + end; +} diff --git a/microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.md b/microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.md new file mode 100644 index 0000000..dbc0a64 --- /dev/null +++ b/microsoft/knowledge/data-modeling/set-last-date-modified-in-onmodify-and-onrename.md @@ -0,0 +1,28 @@ +--- +bc-version: [all] +domain: data-modeling +keywords: [last-date-modified, onmodify, onrename, audit-field, non-editable, stale-value] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Refresh `Last Date Modified` in both `OnModify` and `OnRename` + +## Description + +Master tables carry a non-editable `Last Date Modified` field of type `Date`. It records when the record last changed and is refreshed by table triggers, not by the user. The refresh must happen in **both** `OnModify` and `OnRename`. + +The reason is a BC-specific trap: renaming a record changes its primary key and fires `OnRename` — it does **not** fire `OnModify`. A table that updates `Last Date Modified` only in `OnModify` therefore leaves a stale date behind every rename. Downstream logic that keys on this field (incremental sync, integration deltas, "changed since" reports) then silently skips the renamed record. Assign `Today` (the system date), not `WorkDate`, because the field reflects the real modification moment. + +## Best Practice + +Both `OnModify` and `OnRename` set `"Last Date Modified" := Today();`, and the field is declared `Editable = false` so only the triggers maintain it. + +See sample: `set-last-date-modified-in-onmodify-and-onrename.good.al`. + +## Anti Pattern + +Only `OnModify` assigns `Last Date Modified`. After a rename the value is stale, and any process that trusts it to detect changes misses the record. + +See sample: `set-last-date-modified-in-onmodify-and-onrename.bad.al`. diff --git a/microsoft/knowledge/data-modeling/setup-table-is-a-singleton.bad.al b/microsoft/knowledge/data-modeling/setup-table-is-a-singleton.bad.al new file mode 100644 index 0000000..c6585c8 --- /dev/null +++ b/microsoft/knowledge/data-modeling/setup-table-is-a-singleton.bad.al @@ -0,0 +1,55 @@ +table 50366 "Loyalty Setup" +{ + Caption = 'Loyalty Setup'; + DataClassification = CustomerContent; + + fields + { + // Anti-pattern: an autoincrement key lets the table hold many rows, + // so "the setup" is no longer a single, well-known record. + field(1; "Entry No."; Integer) + { + Caption = 'Entry No.'; + AutoIncrement = true; + } + field(10; "Member Nos."; Code[20]) + { + Caption = 'Member Nos.'; + TableRelation = "No. Series"; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + } +} + +page 50367 "Loyalty Setup List" +{ + // Anti-pattern: a List page over a setup table invites multiple rows and + // never guarantees that a row exists to read. + Caption = 'Loyalty Setup List'; + PageType = List; + SourceTable = "Loyalty Setup"; + UsageCategory = Administration; + ApplicationArea = All; + + layout + { + area(Content) + { + repeater(Group) + { + field("Member Nos."; Rec."Member Nos.") + { + ApplicationArea = All; + ToolTip = 'Specifies the number series used to assign member numbers.'; + } + } + } + } +} diff --git a/microsoft/knowledge/data-modeling/setup-table-is-a-singleton.good.al b/microsoft/knowledge/data-modeling/setup-table-is-a-singleton.good.al new file mode 100644 index 0000000..d299810 --- /dev/null +++ b/microsoft/knowledge/data-modeling/setup-table-is-a-singleton.good.al @@ -0,0 +1,70 @@ +table 50364 "Loyalty Setup" +{ + Caption = 'Loyalty Setup'; + DataClassification = CustomerContent; + + fields + { + field(1; "Primary Key"; Code[10]) + { + Caption = 'Primary Key'; + } + field(10; "Member Nos."; Code[20]) + { + Caption = 'Member Nos.'; + TableRelation = "No. Series"; + } + } + + keys + { + key(PK; "Primary Key") + { + Clustered = true; + } + } + + procedure GetRecordOnce() + begin + if Rec.Get() then + exit; + Rec.Init(); + Rec.Insert(); + end; +} + +page 50365 "Loyalty Setup" +{ + Caption = 'Loyalty Setup'; + PageType = Card; + SourceTable = "Loyalty Setup"; + UsageCategory = Administration; + ApplicationArea = All; + InsertAllowed = false; + DeleteAllowed = false; + + layout + { + area(Content) + { + group(Numbering) + { + Caption = 'Numbering'; + field("Member Nos."; Rec."Member Nos.") + { + ApplicationArea = All; + ToolTip = 'Specifies the number series used to assign member numbers.'; + } + } + } + } + + trigger OnOpenPage() + begin + Rec.Reset(); + if not Rec.Get() then begin + Rec.Init(); + Rec.Insert(); + end; + end; +} diff --git a/microsoft/knowledge/data-modeling/setup-table-is-a-singleton.md b/microsoft/knowledge/data-modeling/setup-table-is-a-singleton.md new file mode 100644 index 0000000..774963f --- /dev/null +++ b/microsoft/knowledge/data-modeling/setup-table-is-a-singleton.md @@ -0,0 +1,28 @@ +--- +bc-version: [all] +domain: data-modeling +keywords: [setup-table, insertallowed, deleteallowed, getrecordonce, primary-key, card-page] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# A setup table is a singleton: one blank-keyed row, no insert or delete + +## Description + +An application-area setup table (`Sales & Receivables Setup`, `Inventory Setup`, and any custom `* Setup`) holds exactly one record per company. Its primary key is a single `Code[10]` field named `Primary Key`, and the row's value is left blank. Nothing else identifies the row — there is only ever one. + +The setup **card** page enforces the singleton: `InsertAllowed = false` and `DeleteAllowed = false` stop a second row or an empty table, and the page guarantees the row exists on first open — typically `OnOpenPage` with `if not Rec.Get() then begin Rec.Init(); Rec.Insert(); end;`, or a `GetRecordOnce` helper on the table. Consuming code then reads it with a plain `Get()`. The read side needs no access optimization — see `singleton-setup-tables-need-no-access-optimization.md`. + +## Best Practice + +`Primary Key` `Code[10]` is the sole key; the setup is surfaced through a Card page with `InsertAllowed = false`, `DeleteAllowed = false`, and an open-time guard that inserts the blank row if it is missing. + +See sample: `setup-table-is-a-singleton.good.al`. + +## Anti Pattern + +An `Integer` / `AutoIncrement` key, a page that allows insert or delete, or a List page over the setup table. Any of these lets the table hold zero or many rows, so "the setup" becomes ambiguous and `Get()` may fail or read the wrong record. + +See sample: `setup-table-is-a-singleton.bad.al`. diff --git a/microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.bad.al b/microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.bad.al new file mode 100644 index 0000000..cfac379 --- /dev/null +++ b/microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.bad.al @@ -0,0 +1,51 @@ +table 50363 "Loyalty Member" +{ + Caption = 'Loyalty Member'; + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) + { + Caption = 'No.'; + + trigger OnValidate() + begin + if "No." = xRec."No." then + exit; + LoyaltySetup.Get(); + // Obsolete-pending: NoSeriesManagement.TestManual raises a + // deprecation warning and is scheduled for removal. + NoSeriesMgt.TestManual(LoyaltySetup."Member Nos."); + "No. Series" := ''; + end; + } + field(2; "No. Series"; Code[20]) + { + Caption = 'No. Series'; + Editable = false; + } + } + + keys + { + key(PK; "No.") + { + Clustered = true; + } + } + + var + LoyaltySetup: Record "Loyalty Setup"; + NoSeriesMgt: Codeunit NoSeriesManagement; + + trigger OnInsert() + begin + if "No." = '' then begin + LoyaltySetup.Get(); + LoyaltySetup.TestField("Member Nos."); + // Obsolete-pending legacy assignment call; use codeunit "No. Series". + NoSeriesMgt.InitSeries(LoyaltySetup."Member Nos.", xRec."No. Series", 0D, "No.", "No. Series"); + end; + end; +} diff --git a/microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.good.al b/microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.good.al new file mode 100644 index 0000000..debe070 --- /dev/null +++ b/microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.good.al @@ -0,0 +1,55 @@ +table 50362 "Loyalty Member" +{ + Caption = 'Loyalty Member'; + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) + { + Caption = 'No.'; + + trigger OnValidate() + var + NoSeries: Codeunit "No. Series"; + begin + if "No." = xRec."No." then + exit; + LoyaltySetup.Get(); + if not NoSeries.IsManual(LoyaltySetup."Member Nos.") then + Error(ManualNosNotAllowedErr); + "No. Series" := ''; + end; + } + field(2; "No. Series"; Code[20]) + { + Caption = 'No. Series'; + Editable = false; + TableRelation = "No. Series"; + } + } + + keys + { + key(PK; "No.") + { + Clustered = true; + } + } + + var + LoyaltySetup: Record "Loyalty Setup"; + ManualNosNotAllowedErr: Label 'Numbers are assigned automatically. Allow manual numbers on the No. Series to enter one by hand.'; + + trigger OnInsert() + var + NoSeries: Codeunit "No. Series"; + begin + if "No." = '' then begin + LoyaltySetup.Get(); + LoyaltySetup.TestField("Member Nos."); + "No. Series" := LoyaltySetup."Member Nos."; + "No." := NoSeries.GetNextNo("No. Series"); + end; + end; +} diff --git a/microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.md b/microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.md new file mode 100644 index 0000000..b4d19b9 --- /dev/null +++ b/microsoft/knowledge/data-modeling/use-no-series-codeunit-not-noseriesmanagement.md @@ -0,0 +1,28 @@ +--- +bc-version: [22..] +domain: data-modeling +keywords: [no-series, getnextno, ismanual, noseriesmanagement, obsolete-pending, testmanual] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Assign numbers with codeunit `"No. Series"`, not the obsolete `NoSeriesManagement` + +## Description + +Since 2023 release wave 1 (v22) the number-series API is codeunit **310** `"No. Series"`, called by name in AL. Its methods include `GetNextNo`, `PeekNextNo`, `IsManual`, `TestManual`, and `LookupRelatedNoSeries`. The older codeunit **396** `NoSeriesManagement` and its `InitSeries` / `SelectSeries` / `SetSeries` / `TestManual` methods are marked obsolete-pending: they still compile but raise a deprecation warning and are scheduled for removal, so they must not appear in new code. + +LLMs reproduce the legacy `NoSeriesManagement` pattern because it dominates pre-2023 training data. Prefer the new codeunit: it has a cleaner surface and is the only version that survives the deprecation. (The numbers matter — `310` is the current codeunit; `396` is the legacy one being retired.) Verify signatures on learn.microsoft.com or in the `microsoft/BCApps` source before use. + +## Best Practice + +`OnInsert` assigns the number with `NoSeries.GetNextNo("No. Series")` where `NoSeries` is `Codeunit "No. Series"`. The `No.` field's `OnValidate` guards manual entry by calling `NoSeries.IsManual(...)` (or `TestManual`) before clearing `No. Series`. + +See sample: `use-no-series-codeunit-not-noseriesmanagement.good.al`. + +## Anti Pattern + +`NoSeriesMgt.InitSeries(...)` for assignment and `NoSeriesMgt.TestManual(...)` for the manual check, where `NoSeriesMgt` is `Codeunit NoSeriesManagement`. Both are obsolete-pending and emit compiler warnings. + +See sample: `use-no-series-codeunit-not-noseriesmanagement.bad.al`. diff --git a/microsoft/knowledge/style/applicationarea-required-on-page-controls.bad.al b/microsoft/knowledge/style/applicationarea-required-on-page-controls.bad.al new file mode 100644 index 0000000..8da7777 --- /dev/null +++ b/microsoft/knowledge/style/applicationarea-required-on-page-controls.bad.al @@ -0,0 +1,25 @@ +page 50375 "Sample App Area Bad" +{ + PageType = Card; + SourceTable = Customer; + layout + { + area(Content) + { + group(General) + { + // Anti-pattern: no ApplicationArea. AS0062 flags this control, + // and it is silently hidden in the Web client for profiles whose + // enabled areas do not already cover it. + field("No."; Rec."No.") + { + ToolTip = 'Specifies the number that identifies the customer.'; + } + field(Name; Rec.Name) + { + ToolTip = 'Specifies the customer''s name.'; + } + } + } + } +} diff --git a/microsoft/knowledge/style/applicationarea-required-on-page-controls.good.al b/microsoft/knowledge/style/applicationarea-required-on-page-controls.good.al new file mode 100644 index 0000000..d344337 --- /dev/null +++ b/microsoft/knowledge/style/applicationarea-required-on-page-controls.good.al @@ -0,0 +1,40 @@ +page 50374 "Sample App Area Good" +{ + PageType = Card; + SourceTable = Customer; + layout + { + area(Content) + { + group(General) + { + field("No."; Rec."No.") + { + ApplicationArea = All; + ToolTip = 'Specifies the number that identifies the customer.'; + } + field(Name; Rec.Name) + { + ApplicationArea = All; + ToolTip = 'Specifies the customer''s name.'; + } + } + } + } + actions + { + area(Processing) + { + action(Refresh) + { + ApplicationArea = All; + ToolTip = 'Reloads the current record.'; + + trigger OnAction() + begin + CurrPage.Update(false); + end; + } + } + } +} diff --git a/microsoft/knowledge/style/applicationarea-required-on-page-controls.md b/microsoft/knowledge/style/applicationarea-required-on-page-controls.md new file mode 100644 index 0000000..606e08a --- /dev/null +++ b/microsoft/knowledge/style/applicationarea-required-on-page-controls.md @@ -0,0 +1,28 @@ +--- +bc-version: [all] +domain: style +keywords: [application-area, page-control, as0062, appsourcecop, hidden-control, web-client] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Every page control needs an `ApplicationArea` (AppSourceCop AS0062) + +## Description + +A field control on a page or pageextension that has no `ApplicationArea` property is silently hidden in the Web client for every profile whose enabled application areas do not cover it. There is no error and no warning at runtime — the field simply does not appear, which reads as data loss to the user. AppSourceCop AS0062 flags any page control or action that is missing the `ApplicationArea` property, and AppSource technical validation rejects the app until it is set. + +Set the property to an area the app actually enables. `All` makes the control visible under every profile and is the common default; if the app declares narrower areas in `app.json`, use one of those. The property applies to field controls and to actions. This is a sibling concern to `caption-required-on-page-fields.md` and `tooltip-required-on-page-fields.md`; note that the ToolTip requirement is the separate CodeCop rule AA0218, not AS0062. + +## Best Practice + +Every field control and action carries `ApplicationArea = All;` (or a declared area of the app). The value is set once per control and keeps the control visible in the Web client. + +See sample: `applicationarea-required-on-page-controls.good.al`. + +## Anti Pattern + +A field control with no `ApplicationArea`. AS0062 flags it, and the control is invisible in the Web client for any profile that does not already enable a matching area. + +See sample: `applicationarea-required-on-page-controls.bad.al`.