Skip to main content

Number Series Troubleshooting

Common errors, debugging tips, and performance considerations for number series implementation.

Common Errors

Error 1: "The field Statistical Account Nos. of table BCS Statistical Account Setup must have a value"

Cause: Setup record exists but "Statistical Account Nos." field is empty.

Trigger: BCSStatisticalAccountSetup.TestField("Statistical Account Nos."); in OnBeforeInsert.

Solution:

  1. Open setup page ("BCS Statistical Account Setup")
  2. Navigate to Number Series field
  3. Select appropriate number series from lookup
  4. Save setup

Prevention: Setup wizard or initialization routine can pre-populate with default series.


Error 2: Setup record not found

Symptoms:

  • Error: "The BCS Statistical Account Setup does not exist"
  • Occurs when trying to create new record

Cause: Setup table is empty (no initialization in setup page).

Solution: Add OnOpenPage trigger to setup page:

trigger OnOpenPage()
begin
Rec.Reset();
if not Rec.Get() then begin
Rec.Init();
Rec.Insert();
end;
end;

Alternative: Check existence before TestField:

trigger OnBeforeInsert()
begin
if "No." = '' then begin
if not BCSStatisticalAccountSetup.Get() then
Error('Setup record does not exist. Please configure Statistical Account Setup first.');
BCSStatisticalAccountSetup.TestField("Statistical Account Nos.");
// ... rest of logic
end;
end;

Error 3: Number series exhausted

Symptoms:

  • Error: "You cannot assign more numbers from the number series STAT"
  • Users cannot create new records

Cause: Number series has reached its ending number.

Solution:

  1. Navigate to "No. Series" page
  2. Find the exhausted series (e.g., STAT)
  3. Open "No. Series Lines"
  4. Extend ending number or add new line with higher range
  5. Save changes

Example fix:

  • Old line: Starting No. = STAT-0001, Ending No. = STAT-1000
  • New line: Starting No. = STAT-1001, Ending No. = STAT-9999

Prevention:

  • Use sufficiently large ranges (e.g., 0001-9999 instead of 0001-0100)
  • Monitor series usage with reports
  • Implement alerts when series approaches limit

Error 4: AssistEdit not working (no DrillDown button)

Symptoms:

  • "No." field has no DrillDown (F6) button
  • Clicking field does nothing

Cause: Page extension not wired up correctly.

Solution: Ensure page extension modifies "No." field with OnAssistEdit trigger:

pageextension 60700 "BCS Statistical Account Card" extends "Statistical Account Card"
{
layout
{
modify("No.")
{
trigger OnAssistEdit()
begin
if Rec.AssistEdit() then
CurrPage.Update();
end;
}
}
}

Verification:

  • Navigate to page in UI
  • Check if "No." field shows AssistEdit button (three dots or F6 indicator)
  • Press F6 to trigger lookup

Symptoms:

  • User selects related series (e.g., STAT-MANUAL) via AssistEdit
  • Next record reverts to default series (STAT) instead of continuing with STAT-MANUAL

Cause: Related series not configured in "No. Series" setup.

Solution:

  1. Navigate to "No. Series" page
  2. Open default series (e.g., STAT)
  3. Open "Relationships" FastTab
  4. Add related series (STAT-MANUAL, STAT-AUTO, etc.)
  5. Save

Code verification: Check AreRelated logic exists:

if NoSeries.AreRelated(BCSStatisticalAccountSetup."Statistical Account Nos.", xRec."BCS No. Series") then
"BCS No. Series" := xRec."BCS No. Series"
else
"BCS No. Series" := BCSStatisticalAccountSetup."Statistical Account Nos.";

Error 6: Manual numbering blocked

Symptoms:

  • User enters manual number (e.g., "CUSTOM-001")
  • Error: "You cannot manually enter a number for this series"

Cause: Number series configured with "Manual Nos." = false.

Solution:

  1. Navigate to "No. Series" page
  2. Find the series in question
  3. Enable "Manual Nos." field
  4. Save

Alternative: Create separate series for manual entry:

  • STAT: Default Manual Nos. = false
  • STAT-MANUAL: Manual Nos. = true
  • Configure as related series

Error 7: Number assigned twice

Symptoms:

  • Two records have same "No."
  • Primary key violation on insert

Cause: Race condition in concurrent insertions OR manual override bypassing series.

Solution:

For concurrent access: Number series handles locking automatically. Ensure:

  • Using NoSeries.GetNextNo() (modern codeunit)
  • Not implementing custom numbering logic
  • Database transactions committed properly

For manual bypass: Validate manual numbers don't conflict:

trigger OnBeforeInsert()
begin
if "No." <> '' then begin
// Validate manual number
StatisticalAccount.SetRange("No.", "No.");
if not StatisticalAccount.IsEmpty then
Error('The number %1 already exists. Please choose a different number.', "No.");
end else begin
// Automatic numbering logic
// ...
end;
end;

Debugging Tips

Tip 1: Trace number assignment

Add temporary message to see what's happening:

trigger OnBeforeInsert()
begin
if "No." = '' then begin
BCSStatisticalAccountSetup.Get();
BCSStatisticalAccountSetup.TestField("Statistical Account Nos.");

Message('Setup Series: %1, Previous Series: %2',
BCSStatisticalAccountSetup."Statistical Account Nos.",
xRec."BCS No. Series");

if NoSeries.AreRelated(BCSStatisticalAccountSetup."Statistical Account Nos.", xRec."BCS No. Series") then
"BCS No. Series" := xRec."BCS No. Series"
else
"BCS No. Series" := BCSStatisticalAccountSetup."Statistical Account Nos.";

"No." := NoSeries.GetNextNo("BCS No. Series");

Message('Assigned No.: %1, Series Used: %2', "No.", "BCS No. Series");
end;
end;

Remove messages after debugging.


Tip 2: Check series configuration

Create diagnostic procedure:

procedure DiagnoseNumberSeries()
var
NoSeriesRec: Record "No. Series";
NoSeriesLine: Record "No. Series Line";
begin
if not BCSStatisticalAccountSetup.Get() then begin
Message('ERROR: Setup record does not exist');
exit;
end;

if BCSStatisticalAccountSetup."Statistical Account Nos." = '' then begin
Message('ERROR: Statistical Account Nos. field is empty');
exit;
end;

if not NoSeriesRec.Get(BCSStatisticalAccountSetup."Statistical Account Nos.") then begin
Message('ERROR: Number series %1 does not exist', BCSStatisticalAccountSetup."Statistical Account Nos.");
exit;
end;

NoSeriesLine.SetRange("Series Code", BCSStatisticalAccountSetup."Statistical Account Nos.");
if NoSeriesLine.IsEmpty then begin
Message('ERROR: No lines defined for series %1', BCSStatisticalAccountSetup."Statistical Account Nos.");
exit;
end;

Message('SUCCESS: Number series is configured correctly');
end;

Call from setup page action:

actions
{
area(processing)
{
action(DiagnoseSeries)
{
Caption = 'Diagnose Number Series';
Image = TestReport;

trigger OnAction()
var
StatAccMgmt: Codeunit "BCS Stat Acc Management";
begin
StatAccMgmt.DiagnoseNumberSeries();
end;
}
}
}

Tip 3: Snapshot debugging for triggers

Use Snapshot Debugger to capture OnBeforeInsert execution:

  1. Enable snapshot debugging in VS Code
  2. Set breakpoint in OnBeforeInsert trigger
  3. Reproduce issue (create new record)
  4. Analyze snapshot variables:
    • xRec."BCS No. Series"
    • BCSStatisticalAccountSetup."Statistical Account Nos."
    • NoSeries.GetNextNo() result

Tip 4: Check event subscribers

If implementing via event subscribers instead of table extension triggers:

[EventSubscriber(ObjectType::Table, Database::"Statistical Account", 'OnBeforeInsertEvent', '', false, false)]
local procedure OnBeforeInsertStatisticalAccount(var Rec: Record "Statistical Account"; RunTrigger: Boolean)
begin
if not RunTrigger then
exit;

if Rec."No." = '' then begin
// Numbering logic
end;
end;

Common mistake: Forgetting if not RunTrigger then exit; causes logic to execute even when triggers are disabled.


Performance Considerations

Consideration 1: GetNextNo() performance

Issue: NoSeries.GetNextNo() acquires database locks to ensure unique numbers.

Impact:

  • Slight delay on insert (typically < 100ms)
  • Concurrent inserts wait in queue

Mitigation:

  • Not an issue for typical user-driven inserts (creating records via UI)
  • Matters for bulk import scenarios (importing 1000+ records)

Bulk import pattern:

procedure ImportRecords(var TempImportBuffer: Record "Import Buffer" temporary)
var
StatisticalAccount: Record "Statistical Account";
NoSeriesBatch: Codeunit "No. Series - Batch";
begin
BCSStatisticalAccountSetup.Get();
BCSStatisticalAccountSetup.TestField("Statistical Account Nos.");

if TempImportBuffer.FindSet() then
repeat
StatisticalAccount.Init();
StatisticalAccount."No." := NoSeriesBatch.GetNextNo(BCSStatisticalAccountSetup."Statistical Account Nos.", WorkDate());
StatisticalAccount.Name := TempImportBuffer.Name;
// ... other fields
StatisticalAccount.Insert(true);
until TempImportBuffer.Next() = 0;
end;

Note: NoSeriesBatch pre-allocates range for better performance in loops.


Consideration 2: Setup record retrieval

Issue: Every insert calls BCSStatisticalAccountSetup.Get().

Impact: Database read on each insert.

Mitigation: Cache setup record in codeunit:

codeunit 60700 "BCS Stat Acc Management"
{
var
BCSStatisticalAccountSetup: Record "BCS Statistical Account Setup";
SetupRead: Boolean;

procedure GetSetup(): Record "BCS Statistical Account Setup"
begin
if not SetupRead then begin
BCSStatisticalAccountSetup.Get();
SetupRead := true;
end;
exit(BCSStatisticalAccountSetup);
end;
}

Use cached version:

trigger OnBeforeInsert()
var
StatAccMgmt: Codeunit "BCS Stat Acc Management";
Setup: Record "BCS Statistical Account Setup";
begin
if "No." = '' then begin
Setup := StatAccMgmt.GetSetup();
Setup.TestField("Statistical Account Nos.");
// ... rest of logic
end;
end;

Trade-off:

  • Pros: Faster performance (avoids repeated DB reads)
  • Cons: Changes to setup require restart or cache invalidation

Verdict: Usually NOT worth complexity. Get() is fast, and setup changes are rare.


Consideration 3: AreRelated() lookup

Issue: NoSeries.AreRelated() queries "No. Series Relationship" table.

Impact: Additional database read on each insert.

Mitigation: Only call if user has changed series:

trigger OnBeforeInsert()
begin
if "No." = '' then begin
BCSStatisticalAccountSetup.Get();
BCSStatisticalAccountSetup.TestField("Statistical Account Nos.");

// Only check if user previously selected a series
if xRec."BCS No. Series" <> '' then begin
if NoSeries.AreRelated(BCSStatisticalAccountSetup."Statistical Account Nos.", xRec."BCS No. Series") then
"BCS No. Series" := xRec."BCS No. Series"
else
"BCS No. Series" := BCSStatisticalAccountSetup."Statistical Account Nos.";
end else
"BCS No. Series" := BCSStatisticalAccountSetup."Statistical Account Nos.";

"No." := NoSeries.GetNextNo("BCS No. Series");
end;
end;

Verdict: Micro-optimization. Original pattern is clear and performant enough.


Best Practices Summary

  1. Always validate setup: Use TestField() to ensure series configured before use
  2. Auto-initialize setup: Use OnOpenPage trigger to create setup record if missing
  3. Support manual override: Allow users to enter numbers manually when needed
  4. Implement AssistEdit: Provide DrillDown for easy series selection
  5. Handle related series: Use AreRelated() to respect user's series choice
  6. Use modern NoSeries codeunit: Leverage Codeunit "No. Series" for automatic locking and management
  7. Test concurrent access: Verify multi-user scenarios work correctly
  8. Monitor series limits: Alert users before series exhausts
  9. Document series purpose: Use clear captions and tooltips explaining each series
  10. Consistent naming: Use "[Prefix] No. Series" pattern for custom fields

Advanced Scenarios

Scenario 1: Conditional AutoIncrement

Requirement: Use number series for some records, AutoIncrement for others.

Pattern:

table 60740 "BCS Mixed Numbering Table"
{
fields
{
field(1; "No."; Integer) // AutoIncrement for internal records
{
AutoIncrement = true;
}
field(2; "External No."; Code[20]) // Number series for external records
{
Caption = 'External No.';
}
field(10; "Entry Type"; Option)
{
OptionMembers = Internal,External;
}
}

trigger OnBeforeInsert()
begin
case "Entry Type" of
"Entry Type"::Internal:
; // AutoIncrement handles it
"Entry Type"::External:
if "External No." = '' then begin
Setup.Get();
Setup.TestField("External Entry Nos.");
"External No." := NoSeries.GetNextNo(Setup."External Entry Nos.");
end;
end;
end;
}

Scenario 2: Composite Keys with Partial Numbering

Requirement: Table has composite key (Type, No.), and number series only applies to specific Type values.

Pattern:

table 60750 "BCS Composite Key Table"
{
fields
{
field(1; "Type"; Option)
{
OptionMembers = Manual,Automatic,External;
}
field(2; "No."; Code[20])
{
Caption = 'No.';
}
}

keys
{
key(PK; "Type", "No.")
{
Clustered = true;
}
}

trigger OnBeforeInsert()
begin
if "No." = '' then begin
case Type of
Type::Automatic:
begin
Setup.Get();
Setup.TestField("Auto Entry Nos.");
"No." := NoSeries.GetNextNo(Setup."Auto Entry Nos.");
end;
Type::External:
begin
Setup.Get();
Setup.TestField("External Entry Nos.");
"No." := NoSeries.GetNextNo(Setup."External Entry Nos.");
end;
// Type::Manual: User must provide "No."
end;
end;
end;
}

Scenario 3: Rollback and Error Handling

Requirement: If subsequent validation fails after number assignment, ensure number isn't consumed.

Pattern:

trigger OnBeforeInsert()
begin
if "No." = '' then begin
Setup.Get();
Setup.TestField("Entry Nos.");
"No." := NoSeries.GetNextNo(Setup."Entry Nos.");
end;

// Validation that might fail
ValidateMandatoryFields(); // If this errors, transaction rolls back
end;

local procedure ValidateMandatoryFields()
begin
TestField(Name);
TestField("Posting Date");
if "Posting Date" > Today then
Error('Posting date cannot be in the future');
end;

Behavior:

  • Number assigned: "ENTRY-0100"
  • Validation fails (e.g., Name is blank)
  • Transaction rolls back
  • Number "ENTRY-0100" is NOT consumed (available for next insert)

Why: Database transactions ensure atomicity. NoSeries codeunit handles rollback internally.


Resources