mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 17:36:53 +01:00
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>
This commit is contained in:
parent
24e62f5ec8
commit
18cb503540
21 changed files with 885 additions and 0 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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.
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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.
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue