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:
- Open setup page ("BCS Statistical Account Setup")
- Navigate to Number Series field
- Select appropriate number series from lookup
- 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:
- Navigate to "No. Series" page
- Find the exhausted series (e.g., STAT)
- Open "No. Series Lines"
- Extend ending number or add new line with higher range
- 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
Error 5: Related series not recognized
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:
- Navigate to "No. Series" page
- Open default series (e.g., STAT)
- Open "Relationships" FastTab
- Add related series (STAT-MANUAL, STAT-AUTO, etc.)
- 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:
- Navigate to "No. Series" page
- Find the series in question
- Enable "Manual Nos." field
- 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:
- Enable snapshot debugging in VS Code
- Set breakpoint in OnBeforeInsert trigger
- Reproduce issue (create new record)
- 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
- Always validate setup: Use
TestField()to ensure series configured before use - Auto-initialize setup: Use OnOpenPage trigger to create setup record if missing
- Support manual override: Allow users to enter numbers manually when needed
- Implement AssistEdit: Provide DrillDown for easy series selection
- Handle related series: Use
AreRelated()to respect user's series choice - Use modern NoSeries codeunit: Leverage
Codeunit "No. Series"for automatic locking and management - Test concurrent access: Verify multi-user scenarios work correctly
- Monitor series limits: Alert users before series exhausts
- Document series purpose: Use clear captions and tooltips explaining each series
- 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.