From 2ba8365e377dced444b36bcd1ccadc948f658264 Mon Sep 17 00:00:00 2001 From: Jeremy Vyska <35526546+JeremyVyska@users.noreply.github.com> Date: Sun, 27 Feb 2022 17:41:39 +0100 Subject: [PATCH 1/7] Revived Pattern: Number Series --- content/docs/patterns/no-series/index.md | 186 +++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 content/docs/patterns/no-series/index.md diff --git a/content/docs/patterns/no-series/index.md b/content/docs/patterns/no-series/index.md new file mode 100644 index 00000000..36bc2558 --- /dev/null +++ b/content/docs/patterns/no-series/index.md @@ -0,0 +1,186 @@ ++++ +title = "No. Series" +tags = ["AL"] +categories = ["Pattern"] ++++ + +_Created by Microsoft, Described by Jeremy Vyska (Spare Brained Ideas)_ + +## Abstract + +The "Number Series" system is used extensively to provide numbers to master records, documents, and other transactions through Microsoft Dynamics 365 Business Central. + +## Description + +At the heart of things, the Number Series engine allows users to define structure for a sequential numeric or alphanumeric string (collectively referred to as a 'number series'), then assign that structure to different parts of the system. + +Typically, one creates a single number series for each _type_ of data entity. For example, Customers or Sales Orders each could have a series defined so that all new Customers or Sales Orders get a new number automatically. + +The Number Series system serves a few ancillary roles: + +- maintains the usage information to know when the last number was generated and on which date +- allows for date driven structures, so that different periods may have different structures +- allows control of if manual entries are or are not permitted +- allow for incrementing in different steps (+1 each time or +1000 each time) +- warn users as a series is running out of numbers +- control if any gaps in a series are permitted (as some regional laws do not allow skipping) + +This is many roles, features, and controls for generation of a single field so the implementation of this can seem difficult at first. + +{{% alert title="Note" color="info" %}} +One additional (and somewhat optional) feature in the Number Series engine allows multiple sequences per type, called **Relationships**. For example, different numbers for Items that are finished goods versus raw materials. This requires additional hooks on the Page. +{{% /alert %}} + +## Usage in Business Central + +To understand an example use in the Base App, the Customer data entity is a good choice. + +Implementation to connect the Customer "No." field to the Number Series engine is done at the table level. The Customer table contains: + +A field to contain the number (typically the primary key), which will be Code type, length of 20: + +```AL +field(1; "No."; Code[20]) +{ + Caption = 'No.'; + + trigger OnValidate() + begin + [...] + end; +} +``` + +A field to contain the unique ID of the Number Series, typically called "No. Series" + +```AL +field(107; "No. Series"; Code[20]) +{ + Caption = 'No. Series'; + Editable = false; + TableRelation = "No. Series"; +} +``` +{{% alert title="Note" color="warning" %}} +The **TableRelation** is important, and the **Editable** being false is advised. +{{% /alert %}} + +And on the **OnInsert** trigger, code populates the **No. Series** and **No.** field. + +```AL +trigger OnInsert() +var + IsHandled: Boolean; +begin + IsHandled := false; + OnBeforeInsert(Rec, IsHandled); + if IsHandled then + exit; + + if "No." = '' then begin + SalesSetup.Get(); + SalesSetup.TestField("Customer Nos."); + NoSeriesMgt.InitSeries(SalesSetup."Customer Nos.", xRec."No. Series", 0D, "No.", "No. Series"); + end; + [...] + + OnAfterOnInsert(Rec, xRec); + end; +``` + +In the case of Customer, this is a Data Entity within the Sales module of the system. The Sales module has a **Sales Setup** table where the user can specify a **No. Series** to use for Customers by default. + +`SalesSetup.Get();` fetches the setup table. +`SalesSetup.TestField("Customer Nos.");` is the basic validation that the **Sales Setup** table has a non-empty **Customer Nos.** field. If the setup field isn't populated, when the user attempts to create a new Customer, they will receive an error message. + +`NoSeriesMgt.InitSeries(SalesSetup."Customer Nos.", xRec."No. Series", 0D, "No.", "No. Series");` is more parameters to a function than most expect. + +The function call takes the following parameters: + +```AL +procedure InitSeries( + DefaultNoSeriesCode: Code[20]; + OldNoSeriesCode: Code[20]; + NewDate: Date; + var NewNo: Code[20]; + var NewNoSeriesCode: Code[20]) +``` + +The **DefaultNoSeriesCode** parameter is typically from a setup table. In the Customer example, this comes from the **Sales Setup** **Customer Nos.** setting. + +The **OldNoSeriesCode** is used to verify when changing from one No Series to another that they are related. + +The **NewDate** parameter is used to drive numbering based on Dates. This is typically used on Documents. For master entities, like Customer, an empty date `0D` can be passed in. + +{{% alert title="Note" color="info" %}} +Many parts of the NoSeriesManagement codeunit predate method overloading, so if the system was created today, some parameters like NewDate would likely be optional. +{{% /alert %}} + +The **NewNo** is a `var` parameter, and is how the new value comes back from the engine. This also serves two other purposes: + - if passed in blank, the Number Series used must be configured to have **Default Nos.** enabled + - if passed in with a value, the Number Series used must be configured to have **Manual Nos** enabled. + +The **NewNoSeriesCode** is more often used to switch between related number series, but is a required parameter, and is also passed back from the engine, so it is also a `var`. + + +Since the Customer data entity supports the **No. Series Relationship** functionality, there are additional components. On the table, there is a function called `AssistEdit`: + +```AL +procedure AssistEdit(OldCust: Record Customer): Boolean +var + Cust: Record Customer; +begin + with Cust do begin + Cust := Rec; + SalesSetup.Get(); + SalesSetup.TestField("Customer Nos."); + if NoSeriesMgt.SelectSeries(SalesSetup."Customer Nos.", OldCust."No. Series", "No. Series") then begin + NoSeriesMgt.SetSeries("No."); + Rec := Cust; + OnAssistEditOnBeforeExit(Cust); + exit(true); + end; + end; +end; +``` + +Similar to the OnInsert, some setup fields are checked. + +Then, the `SelectSeries` function is called. This will present a List to the user of available and relevant **No. Series** that are connected to the `SalesSetup."Customer Nos."` by a Number Series Relationship. + +From the Customer Page (a Card type page), the **No.** field has an AssistEdit trigger: + +```AL +trigger OnAssistEdit() +begin + if AssistEdit(xRec) then + CurrPage.Update(); +end; +``` + +## Objects to Inspect + +Business Central objects in the Base App to review to find out more: + +| Object Type | Object ID | Object Name | +|-------------|-----------|--------------------------| +| Table | 308 | No. Series | +| Table | 309 | No. Series Line | +| Table | 310 | No. Series Relationship | +| Page | 456 | No. Series | +| Page | 457 | No. Series Lines | +| Page | 458 | No. Series Relationships | +| Page | 571 | No. Series List | +| Codeunit | 396 | NoSeriesManagement | + +## When not to use + +Typically, this pattern is used for unique Data Entities. It is not recommended for use in parts of the system where entries are created permanently (such as an Entry No. for ledgers) or highly mutable / working line data (such as Line No. for journals or document lines). + +## List of references + +For usage of number series, there is more information available on: +- [Microsoft Docs: Create Number Series](https://docs.microsoft.com/en-us/dynamics365/business-central/ui-create-number-series) +- [Microsoft Learn: Set up number series and trail codes](https://docs.microsoft.com/en-us/learn/modules/number-series-trail-codes-dynamics-365-business-central/) + +For more programming details, there is more information on [Microsoft Docs: Number Sequences in Business Central](https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-number-sequences). \ No newline at end of file From 8543650944cbaf95e5de5c987d8e3b9e3532742d Mon Sep 17 00:00:00 2001 From: Jeremy Vyska <35526546+JeremyVyska@users.noreply.github.com> Date: Sun, 27 Feb 2022 21:27:58 +0100 Subject: [PATCH 2/7] Update content/docs/patterns/no-series/index.md Co-authored-by: Henrik Helgesen --- content/docs/patterns/no-series/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/patterns/no-series/index.md b/content/docs/patterns/no-series/index.md index 36bc2558..245413df 100644 --- a/content/docs/patterns/no-series/index.md +++ b/content/docs/patterns/no-series/index.md @@ -62,7 +62,7 @@ field(107; "No. Series"; Code[20]) } ``` {{% alert title="Note" color="warning" %}} -The **TableRelation** is important, and the **Editable** being false is advised. +The **`TableRelation`** is important, and the **`Editable`** being false is advised. {{% /alert %}} And on the **OnInsert** trigger, code populates the **No. Series** and **No.** field. From ccbf573ec8f327dd68148e5f5da364db87146a1b Mon Sep 17 00:00:00 2001 From: Jeremy Vyska <35526546+JeremyVyska@users.noreply.github.com> Date: Sun, 27 Feb 2022 21:28:15 +0100 Subject: [PATCH 3/7] Update content/docs/patterns/no-series/index.md Co-authored-by: Henrik Helgesen --- content/docs/patterns/no-series/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/patterns/no-series/index.md b/content/docs/patterns/no-series/index.md index 245413df..af44bba1 100644 --- a/content/docs/patterns/no-series/index.md +++ b/content/docs/patterns/no-series/index.md @@ -37,7 +37,7 @@ To understand an example use in the Base App, the Customer data entity is a good Implementation to connect the Customer "No." field to the Number Series engine is done at the table level. The Customer table contains: -A field to contain the number (typically the primary key), which will be Code type, length of 20: +A field to contain the number (typically the primary key), which will be of type **`Code`**, length of **20**: ```AL field(1; "No."; Code[20]) From d7792f54899a4bcdeb552dc78cc7a9332c21c9d0 Mon Sep 17 00:00:00 2001 From: Jeremy Vyska <35526546+JeremyVyska@users.noreply.github.com> Date: Sun, 27 Feb 2022 21:28:21 +0100 Subject: [PATCH 4/7] Update content/docs/patterns/no-series/index.md Co-authored-by: Henrik Helgesen --- content/docs/patterns/no-series/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/patterns/no-series/index.md b/content/docs/patterns/no-series/index.md index af44bba1..6a3a2193 100644 --- a/content/docs/patterns/no-series/index.md +++ b/content/docs/patterns/no-series/index.md @@ -65,7 +65,7 @@ field(107; "No. Series"; Code[20]) The **`TableRelation`** is important, and the **`Editable`** being false is advised. {{% /alert %}} -And on the **OnInsert** trigger, code populates the **No. Series** and **No.** field. +And on the **`OnInsert`** trigger, code populates the **`No. Series`** and **`No.`** field. ```AL trigger OnInsert() From 9b97d1151e444319ca9cca582aefe66c0df998a0 Mon Sep 17 00:00:00 2001 From: Jeremy Vyska <35526546+JeremyVyska@users.noreply.github.com> Date: Sun, 27 Feb 2022 21:28:29 +0100 Subject: [PATCH 5/7] Update content/docs/patterns/no-series/index.md Co-authored-by: Henrik Helgesen --- content/docs/patterns/no-series/index.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/content/docs/patterns/no-series/index.md b/content/docs/patterns/no-series/index.md index 6a3a2193..fd733671 100644 --- a/content/docs/patterns/no-series/index.md +++ b/content/docs/patterns/no-series/index.md @@ -99,12 +99,11 @@ The function call takes the following parameters: ```AL procedure InitSeries( - DefaultNoSeriesCode: Code[20]; - OldNoSeriesCode: Code[20]; - NewDate: Date; - var NewNo: Code[20]; - var NewNoSeriesCode: Code[20]) -``` + DefaultNoSeriesCode: Code[20]; + OldNoSeriesCode: Code[20]; + NewDate: Date; + var NewNo: Code[20]; + var NewNoSeriesCode: Code[20]) The **DefaultNoSeriesCode** parameter is typically from a setup table. In the Customer example, this comes from the **Sales Setup** **Customer Nos.** setting. From 24f04c7fda28d553c249d5fa4a112aad2e28f857 Mon Sep 17 00:00:00 2001 From: Jeremy Vyska <35526546+JeremyVyska@users.noreply.github.com> Date: Sun, 27 Feb 2022 21:48:06 +0100 Subject: [PATCH 6/7] Fixing formatting, adding clarity, WITH warn --- content/docs/patterns/no-series/index.md | 42 ++++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/content/docs/patterns/no-series/index.md b/content/docs/patterns/no-series/index.md index fd733671..23d55724 100644 --- a/content/docs/patterns/no-series/index.md +++ b/content/docs/patterns/no-series/index.md @@ -31,11 +31,11 @@ This is many roles, features, and controls for generation of a single field so t One additional (and somewhat optional) feature in the Number Series engine allows multiple sequences per type, called **Relationships**. For example, different numbers for Items that are finished goods versus raw materials. This requires additional hooks on the Page. {{% /alert %}} -## Usage in Business Central +## Usage in Data Entities To understand an example use in the Base App, the Customer data entity is a good choice. -Implementation to connect the Customer "No." field to the Number Series engine is done at the table level. The Customer table contains: +Implementation to connect the Customer **`No.`** field to the Number Series engine is done at the table level. The Customer table contains: A field to contain the number (typically the primary key), which will be of type **`Code`**, length of **20**: @@ -90,7 +90,8 @@ begin In the case of Customer, this is a Data Entity within the Sales module of the system. The Sales module has a **Sales Setup** table where the user can specify a **No. Series** to use for Customers by default. -`SalesSetup.Get();` fetches the setup table. +`SalesSetup.Get();` fetches the sole setup table record. + `SalesSetup.TestField("Customer Nos.");` is the basic validation that the **Sales Setup** table has a non-empty **Customer Nos.** field. If the setup field isn't populated, when the user attempts to create a new Customer, they will receive an error message. `NoSeriesMgt.InitSeries(SalesSetup."Customer Nos.", xRec."No. Series", 0D, "No.", "No. Series");` is more parameters to a function than most expect. @@ -104,6 +105,7 @@ procedure InitSeries( NewDate: Date; var NewNo: Code[20]; var NewNoSeriesCode: Code[20]) +``` The **DefaultNoSeriesCode** parameter is typically from a setup table. In the Customer example, this comes from the **Sales Setup** **Customer Nos.** setting. @@ -121,6 +123,30 @@ The **NewNo** is a `var` parameter, and is how the new value comes back from the The **NewNoSeriesCode** is more often used to switch between related number series, but is a required parameter, and is also passed back from the engine, so it is also a `var`. +Additionally, it is a good idea to have `OnValidate` functionality on the **`No.`** field. The complete code for the Customer **`No.`** field: + +```AL +field(1; "No."; Code[20]) +{ + Caption = 'No.'; + + trigger OnValidate() + begin + if "No." <> xRec."No." then begin + SalesSetup.Get(); + NoSeriesMgt.TestManual(SalesSetup."Customer Nos."); + "No. Series" := ''; + end; + if "Invoice Disc. Code" = '' then + "Invoice Disc. Code" := "No."; + end; +} +``` + +If the user has changed the **`No.`** field (`"No." <> xRec."No."`), then: +- the Number Series is checked if manually setting a new value is allowed via the `TestManual` function +- The `No. Series` is cleared on the record, as it has no longer been given a value from that Series. + Since the Customer data entity supports the **No. Series Relationship** functionality, there are additional components. On the table, there is a function called `AssistEdit`: @@ -143,11 +169,15 @@ begin end; ``` -Similar to the OnInsert, some setup fields are checked. +{{% alert title="Note" color="warning" %}} +The use of **`WITH`** is deprecated. While this code block represents the current state of the Base App, the use of **`WTIH`** should not be copied. +{{% /alert %}} + +Similar to the **`OnInsert`** trigger, some setup fields are checked. Then, the `SelectSeries` function is called. This will present a List to the user of available and relevant **No. Series** that are connected to the `SalesSetup."Customer Nos."` by a Number Series Relationship. -From the Customer Page (a Card type page), the **No.** field has an AssistEdit trigger: +From the **`Customer Page`** (a Card type page), the **No.** field has an **`AssistEdit`** trigger: ```AL trigger OnAssistEdit() @@ -174,7 +204,7 @@ Business Central objects in the Base App to review to find out more: ## When not to use -Typically, this pattern is used for unique Data Entities. It is not recommended for use in parts of the system where entries are created permanently (such as an Entry No. for ledgers) or highly mutable / working line data (such as Line No. for journals or document lines). +Typically, this pattern is used for unique Data Entities. It is not recommended for use in parts of the system where entries are created permanently (such as an **`Entry No.`** for ledgers) or highly mutable / working line data (such as **`Line No.`** for journals or document lines). ## List of references From a4a46ab85a97924d3a5ab745b161b9ecae7c4123 Mon Sep 17 00:00:00 2001 From: Jeremy Vyska <35526546+JeremyVyska@users.noreply.github.com> Date: Sun, 27 Feb 2022 22:09:51 +0100 Subject: [PATCH 7/7] Added Journal NoSeries info --- content/docs/patterns/no-series/index.md | 71 ++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/content/docs/patterns/no-series/index.md b/content/docs/patterns/no-series/index.md index 23d55724..a28c48ec 100644 --- a/content/docs/patterns/no-series/index.md +++ b/content/docs/patterns/no-series/index.md @@ -187,6 +187,77 @@ begin end; ``` +## Usage in Journals + +Journals utilize a **`Document No.`** as a non-primary key field and use a different strategy for use of the Number Series engine. For each Journal Batch, a different **`No. Series`** can be set. + +For example, on the **`General Journal`** Page, in the **`OnNewRecord`**, the **`SetUpNewLine`** function on the **`Gen. Journal Line`** Table is called: + +```AL +procedure SetUpNewLine(LastGenJnlLine: Record "Gen. Journal Line"; Balance: Decimal; BottomLine: Boolean) +var + IsHandled: Boolean; +begin + IsHandled := false; + OnBeforeSetUpNewLine(GenJnlTemplate, GenJnlBatch, GenJnlLine, LastGenJnlLine, GLSetupRead, Balance, BottomLine, IsHandled); + if IsHandled then + exit; + + GenJnlTemplate.Get("Journal Template Name"); + GenJnlBatch.Get("Journal Template Name", "Journal Batch Name"); + GenJnlLine.SetRange("Journal Template Name", "Journal Template Name"); + GenJnlLine.SetRange("Journal Batch Name", "Journal Batch Name"); + if GenJnlLine.FindFirst then begin + "Posting Date" := LastGenJnlLine."Posting Date"; + "Document Date" := LastGenJnlLine."Posting Date"; + "Document No." := LastGenJnlLine."Document No."; + OnSetUpNewLineOnBeforeIncrDocNo(GenJnlLine, LastGenJnlLine, Balance, BottomLine); + if BottomLine and + (Balance - LastGenJnlLine."Balance (LCY)" = 0) and + not LastGenJnlLine.EmptyLine + then + IncrementDocumentNo(GenJnlBatch, "Document No."); + end else begin + "Posting Date" := WorkDate; + "Document Date" := WorkDate; + if GenJnlBatch."No. Series" <> '' then begin + Clear(NoSeriesMgt); + "Document No." := NoSeriesMgt.TryGetNextNo(GenJnlBatch."No. Series", "Posting Date"); + end; + end; + [...] +``` + +If the Batch is empty, and if the **`Gen. Journal Batch`** has a **`No. Series`** set, the **`Document No.`** is set from the number series via the **`NoSeriesManagement`** codeunit's **`TryGetNextNo`** function. This takes two parameters: +- Which **`No. Series`** to get the next number from +- Which date to fetch for + +This function does *not* update the **`Last No. Used`** and **`Last Date Used`** fields on the number series. Those will be updated during the Posting process. + + +If the Batch is not empty *and* the sum of the existing lines totals to zero (in balance), the General Journal assumes the user wants to start a new set of lines under a new **`Document No.`**. The table level procedure **`IncrementDocumentNo`** function is called: + +```AL +procedure IncrementDocumentNo(GenJnlBatch: Record "Gen. Journal Batch"; var LastDocNumber: Code[20]) +var + NoSeriesLine: Record "No. Series Line"; +begin + if GenJnlBatch."No. Series" <> '' then begin + NoSeriesMgt.SetNoSeriesLineFilter(NoSeriesLine, GenJnlBatch."No. Series", "Posting Date"); + if NoSeriesLine."Increment-by No." > 1 then + NoSeriesMgt.IncrementNoText(LastDocNumber, NoSeriesLine."Increment-by No.") + else + LastDocNumber := IncStr(LastDocNumber); + end else + LastDocNumber := IncStr(LastDocNumber); +end; +``` + +If the batch's **`No. Series`** is set, it is checked if the **`Increment-By No.`** setting is anything besides `1`. If so, use the special **`IncrementNoText`** function. + +If neither of those cases is true, then the line's **`Document No.`** is updated with the language function **`IncStr`**. + + ## Objects to Inspect Business Central objects in the Base App to review to find out more: