bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
810 lines (675 loc) • 28.2 kB
Markdown
# No Series Implementation Patterns - AL Code Examples
This sample demonstrates implementing robust number generation and sequence management in Business Central.
## Basic No Series Implementation
```al
// Custom number generation codeunit
codeunit 50500 "Custom No. Series Management"
{
procedure GetNextNo(NoSeriesCode: Code[20]; SeriesDate: Date): Code[20]
var
NoSeries: Record "No. Series";
NoSeriesLine: Record "No. Series Line";
NextNo: Code[20];
begin
if not NoSeries.Get(NoSeriesCode) then
Error('No. Series %1 does not exist', NoSeriesCode);
NoSeriesLine := FindApplicableNoSeriesLine(NoSeriesCode, SeriesDate);
NextNo := GenerateNextNumber(NoSeriesLine);
UpdateNoSeriesLine(NoSeriesLine, NextNo);
LogNumberGeneration(NoSeriesCode, NextNo);
exit(NextNo);
end;
procedure GetNextNoWithGapManagement(NoSeriesCode: Code[20]; SeriesDate: Date; FillGaps: Boolean): Code[20]
var
NextNo: Code[20];
GapNo: Code[20];
begin
if FillGaps then begin
GapNo := FindFirstGap(NoSeriesCode);
if GapNo <> '' then begin
MarkGapAsFilled(NoSeriesCode, GapNo);
exit(GapNo);
end;
end;
exit(GetNextNo(NoSeriesCode, SeriesDate));
end;
procedure CreateCustomNoSeries(NoSeriesCode: Code[20]; Description: Text[100]; StartingNo: Code[20]; EndingNo: Code[20]): Boolean
var
NoSeries: Record "No. Series";
NoSeriesLine: Record "No. Series Line";
begin
// Create No. Series header
if NoSeries.Get(NoSeriesCode) then
exit(false); // Already exists
NoSeries.Init();
NoSeries.Code := NoSeriesCode;
NoSeries.Description := Description;
NoSeries."Default Nos." := true;
NoSeries."Manual Nos." := false;
NoSeries.Insert(true);
// Create No. Series line
NoSeriesLine.Init();
NoSeriesLine."Series Code" := NoSeriesCode;
NoSeriesLine."Line No." := 10000;
NoSeriesLine."Starting Date" := Today;
NoSeriesLine."Starting No." := StartingNo;
NoSeriesLine."Ending No." := EndingNo;
NoSeriesLine."Last No. Used" := '';
NoSeriesLine."Increment-by No." := 1;
NoSeriesLine.Insert(true);
exit(true);
end;
local procedure FindApplicableNoSeriesLine(NoSeriesCode: Code[20]; SeriesDate: Date): Record "No. Series Line"
var
NoSeriesLine: Record "No. Series Line";
begin
NoSeriesLine.SetRange("Series Code", NoSeriesCode);
NoSeriesLine.SetFilter("Starting Date", '<=%1', SeriesDate);
NoSeriesLine.SetFilter("Ending Date", '%1|>=%2', 0D, SeriesDate);
if not NoSeriesLine.FindLast() then
Error('No applicable No. Series Line found for %1 on %2', NoSeriesCode, SeriesDate);
exit(NoSeriesLine);
end;
local procedure GenerateNextNumber(var NoSeriesLine: Record "No. Series Line"): Code[20]
var
NextNo: Code[20];
NumberPart: BigInteger;
PrefixPart: Text;
SuffixPart: Text;
NumberLength: Integer;
begin
if NoSeriesLine."Last No. Used" = '' then
NextNo := NoSeriesLine."Starting No."
else begin
// Parse current number
ParseNumberComponents(NoSeriesLine."Last No. Used", PrefixPart, NumberPart, SuffixPart, NumberLength);
// Increment number
NumberPart += NoSeriesLine."Increment-by No.";
// Reconstruct number
NextNo := ReconstructNumber(PrefixPart, NumberPart, SuffixPart, NumberLength);
end;
// Validate against ending number
if (NoSeriesLine."Ending No." <> '') and (NextNo > NoSeriesLine."Ending No.") then
Error('No. Series %1 has reached its ending number %2', NoSeriesLine."Series Code", NoSeriesLine."Ending No.");
exit(NextNo);
end;
local procedure UpdateNoSeriesLine(var NoSeriesLine: Record "No. Series Line"; NextNo: Code[20])
begin
NoSeriesLine."Last No. Used" := NextNo;
NoSeriesLine."Last Date Used" := Today;
NoSeriesLine.Modify(true);
end;
local procedure ParseNumberComponents(NumberStr: Code[20]; var Prefix: Text; var Number: BigInteger; var Suffix: Text; var NumberLength: Integer)
var
i: Integer;
CurrentChar: Char;
NumberStartPos: Integer;
NumberEndPos: Integer;
NumberPart: Text;
begin
Clear(Prefix);
Clear(Suffix);
Number := 0;
NumberLength := 0;
NumberStartPos := 0;
NumberEndPos := 0;
// Find the start of the numeric part
for i := 1 to StrLen(NumberStr) do begin
CurrentChar := NumberStr[i];
if CurrentChar in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] then begin
if NumberStartPos = 0 then
NumberStartPos := i;
NumberEndPos := i;
end else begin
if NumberStartPos > 0 then
break;
end;
end;
if NumberStartPos > 0 then begin
// Extract components
if NumberStartPos > 1 then
Prefix := CopyStr(NumberStr, 1, NumberStartPos - 1);
NumberPart := CopyStr(NumberStr, NumberStartPos, NumberEndPos - NumberStartPos + 1);
NumberLength := StrLen(NumberPart);
Evaluate(Number, NumberPart);
if NumberEndPos < StrLen(NumberStr) then
Suffix := CopyStr(NumberStr, NumberEndPos + 1);
end;
end;
local procedure ReconstructNumber(Prefix: Text; Number: BigInteger; Suffix: Text; NumberLength: Integer): Code[20]
var
NumberPart: Text;
Result: Text;
begin
NumberPart := Format(Number);
// Pad with leading zeros if necessary
while StrLen(NumberPart) < NumberLength do
NumberPart := '0' + NumberPart;
Result := Prefix + NumberPart + Suffix;
exit(CopyStr(Result, 1, 20));
end;
local procedure FindFirstGap(NoSeriesCode: Code[20]): Code[20]
var
NoSeriesGap: Record "No. Series Gap";
begin
NoSeriesGap.SetRange("Series Code", NoSeriesCode);
NoSeriesGap.SetRange(Filled, false);
if NoSeriesGap.FindFirst() then
exit(NoSeriesGap."Gap No.");
exit('');
end;
local procedure MarkGapAsFilled(NoSeriesCode: Code[20]; GapNo: Code[20])
var
NoSeriesGap: Record "No. Series Gap";
begin
if NoSeriesGap.Get(NoSeriesCode, GapNo) then begin
NoSeriesGap.Filled := true;
NoSeriesGap."Fill Date" := Today;
NoSeriesGap."Fill Time" := Time;
NoSeriesGap.Modify(true);
end;
end;
local procedure LogNumberGeneration(NoSeriesCode: Code[20]; GeneratedNo: Code[20])
var
NoSeriesLog: Record "No. Series Log";
begin
NoSeriesLog.Init();
NoSeriesLog."Entry No." := GetNextLogEntryNo();
NoSeriesLog."Series Code" := NoSeriesCode;
NoSeriesLog."Generated No." := GeneratedNo;
NoSeriesLog."Generation Date" := Today;
NoSeriesLog."Generation Time" := Time;
NoSeriesLog."User ID" := UserId;
NoSeriesLog.Insert(true);
end;
local procedure GetNextLogEntryNo(): Integer
var
NoSeriesLog: Record "No. Series Log";
begin
if NoSeriesLog.FindLast() then
exit(NoSeriesLog."Entry No." + 1)
else
exit(1);
end;
}
```
## Concurrency-Safe Number Generation
```al
// High-performance number generation with concurrency control
codeunit 50501 "Concurrent No. Series Management"
{
procedure GetNextNoSafe(NoSeriesCode: Code[20]; SeriesDate: Date): Code[20]
var
NoSeriesLine: Record "No. Series Line";
NextNo: Code[20];
RetryCount: Integer;
MaxRetries: Integer;
begin
MaxRetries := 10;
RetryCount := 0;
repeat
RetryCount += 1;
if TryGetNextNoWithLock(NoSeriesCode, SeriesDate, NextNo) then
exit(NextNo);
if RetryCount >= MaxRetries then
Error('Unable to generate next number after %1 attempts. Series may be in use by another user.', MaxRetries);
// Wait before retry (exponential backoff)
Sleep(RetryCount * 100);
until false;
end;
[TryFunction]
local procedure TryGetNextNoWithLock(NoSeriesCode: Code[20]; SeriesDate: Date; var NextNo: Code[20])
var
NoSeriesLine: Record "No. Series Line";
NoSeriesLock: Record "No. Series Lock";
LockAcquired: Boolean;
begin
// Attempt to acquire lock
LockAcquired := TryAcquireSeriesLock(NoSeriesCode);
if not LockAcquired then
Error('Lock not acquired');
try
// Find applicable line
NoSeriesLine := FindApplicableNoSeriesLine(NoSeriesCode, SeriesDate);
// Generate next number
NextNo := GenerateNextNumberSafe(NoSeriesLine);
// Update line
UpdateNoSeriesLineSafe(NoSeriesLine, NextNo);
// Commit changes
Commit();
finally
// Always release lock
ReleaseSeriesLock(NoSeriesCode);
end;
end;
[TryFunction]
local procedure TryAcquireSeriesLock(NoSeriesCode: Code[20]): Boolean
var
NoSeriesLock: Record "No. Series Lock";
LockTimeout: DateTime;
begin
LockTimeout := CurrentDateTime + 5000; // 5 second timeout
NoSeriesLock.SetRange("Series Code", NoSeriesCode);
repeat
if not NoSeriesLock.FindFirst() then begin
// Create lock record
NoSeriesLock.Init();
NoSeriesLock."Series Code" := NoSeriesCode;
NoSeriesLock."Locked By" := UserId;
NoSeriesLock."Lock Time" := CurrentDateTime;
NoSeriesLock.Insert(true);
exit(true);
end;
// Check if lock is stale (older than 1 minute)
if NoSeriesLock."Lock Time" < (CurrentDateTime - 60000) then begin
NoSeriesLock.Delete(true);
continue;
end;
Sleep(50); // Wait 50ms before retry
until CurrentDateTime > LockTimeout;
exit(false);
end;
local procedure ReleaseSeriesLock(NoSeriesCode: Code[20])
var
NoSeriesLock: Record "No. Series Lock";
begin
NoSeriesLock.SetRange("Series Code", NoSeriesCode);
NoSeriesLock.SetRange("Locked By", UserId);
if NoSeriesLock.FindFirst() then
NoSeriesLock.Delete(true);
end;
local procedure FindApplicableNoSeriesLine(NoSeriesCode: Code[20]; SeriesDate: Date): Record "No. Series Line"
var
NoSeriesLine: Record "No. Series Line";
begin
NoSeriesLine.SetRange("Series Code", NoSeriesCode);
NoSeriesLine.SetFilter("Starting Date", '<=%1', SeriesDate);
NoSeriesLine.SetFilter("Ending Date", '%1|>=%2', 0D, SeriesDate);
if not NoSeriesLine.FindLast() then
Error('No applicable No. Series Line found for %1 on %2', NoSeriesCode, SeriesDate);
exit(NoSeriesLine);
end;
local procedure GenerateNextNumberSafe(var NoSeriesLine: Record "No. Series Line"): Code[20]
var
NextNo: Code[20];
LastUsedNo: Code[20];
begin
// Re-read to get latest values
NoSeriesLine.Find();
if NoSeriesLine."Last No. Used" = '' then
NextNo := NoSeriesLine."Starting No."
else begin
NextNo := IncrementNumber(NoSeriesLine."Last No. Used", NoSeriesLine."Increment-by No.");
end;
// Validate against ending number
if (NoSeriesLine."Ending No." <> '') and (NextNo > NoSeriesLine."Ending No.") then
Error('No. Series %1 has reached its ending number %2', NoSeriesLine."Series Code", NoSeriesLine."Ending No.");
exit(NextNo);
end;
local procedure UpdateNoSeriesLineSafe(var NoSeriesLine: Record "No. Series Line"; NextNo: Code[20])
begin
NoSeriesLine."Last No. Used" := NextNo;
NoSeriesLine."Last Date Used" := Today;
NoSeriesLine.Modify(true);
end;
local procedure IncrementNumber(CurrentNo: Code[20]; IncrementBy: Integer): Code[20]
var
NumberPart: BigInteger;
PrefixPart: Text;
SuffixPart: Text;
NumberLength: Integer;
begin
ParseNumberComponents(CurrentNo, PrefixPart, NumberPart, SuffixPart, NumberLength);
NumberPart += IncrementBy;
exit(ReconstructNumber(PrefixPart, NumberPart, SuffixPart, NumberLength));
end;
local procedure ParseNumberComponents(NumberStr: Code[20]; var Prefix: Text; var Number: BigInteger; var Suffix: Text; var NumberLength: Integer)
begin
// Implementation from previous example
end;
local procedure ReconstructNumber(Prefix: Text; Number: BigInteger; Suffix: Text; NumberLength: Integer): Code[20]
begin
// Implementation from previous example
end;
}
```
## Batch Number Allocation
```al
// Optimized batch allocation for high-volume scenarios
codeunit 50502 "Batch No. Series Allocation"
{
var
AllocatedNumbers: Dictionary of [Code[20], List of [Code[20]]];
procedure AllocateNumberBatch(NoSeriesCode: Code[20]; BatchSize: Integer): List of [Code[20]]
var
NoSeriesLine: Record "No. Series Line";
AllocatedBatch: List of [Code[20]];
NextNo: Code[20];
i: Integer;
begin
if BatchSize <= 0 then
Error('Batch size must be greater than zero');
if BatchSize > 1000 then
Error('Batch size cannot exceed 1000');
// Lock the series for batch allocation
LockNoSeriesForBatch(NoSeriesCode);
try
NoSeriesLine := FindApplicableNoSeriesLine(NoSeriesCode, Today);
// Allocate batch
for i := 1 to BatchSize do begin
NextNo := GenerateNextNumberInBatch(NoSeriesLine, i);
AllocatedBatch.Add(NextNo);
end;
// Update the line with the last allocated number
UpdateNoSeriesLineForBatch(NoSeriesLine, AllocatedBatch.Get(BatchSize));
// Cache allocated numbers
AllocatedNumbers.Set(NoSeriesCode, AllocatedBatch);
Commit();
finally
UnlockNoSeriesForBatch(NoSeriesCode);
end;
exit(AllocatedBatch);
end;
procedure GetNextFromBatch(NoSeriesCode: Code[20]): Code[20]
var
BatchList: List of [Code[20]];
NextNo: Code[20];
begin
if not AllocatedNumbers.Get(NoSeriesCode, BatchList) then
Error('No allocated batch found for series %1', NoSeriesCode);
if BatchList.Count = 0 then begin
// Batch exhausted, allocate new batch
BatchList := AllocateNumberBatch(NoSeriesCode, 100); // Default batch size
end;
NextNo := BatchList.Get(1);
BatchList.RemoveAt(1);
AllocatedNumbers.Set(NoSeriesCode, BatchList);
LogBatchUsage(NoSeriesCode, NextNo);
exit(NextNo);
end;
procedure GetBatchStatus(NoSeriesCode: Code[20]): Dictionary of [Text, Variant]
var
Status: Dictionary of [Text, Variant];
BatchList: List of [Code[20]];
begin
Status.Set('SeriesCode', NoSeriesCode);
if AllocatedNumbers.Get(NoSeriesCode, BatchList) then begin
Status.Set('HasBatch', true);
Status.Set('RemainingNumbers', BatchList.Count);
if BatchList.Count > 0 then begin
Status.Set('NextAvailable', BatchList.Get(1));
Status.Set('LastInBatch', BatchList.Get(BatchList.Count));
end;
end else begin
Status.Set('HasBatch', false);
Status.Set('RemainingNumbers', 0);
end;
exit(Status);
end;
local procedure FindApplicableNoSeriesLine(NoSeriesCode: Code[20]; SeriesDate: Date): Record "No. Series Line"
var
NoSeriesLine: Record "No. Series Line";
begin
NoSeriesLine.SetRange("Series Code", NoSeriesCode);
NoSeriesLine.SetFilter("Starting Date", '<=%1', SeriesDate);
NoSeriesLine.SetFilter("Ending Date", '%1|>=%2', 0D, SeriesDate);
if not NoSeriesLine.FindLast() then
Error('No applicable No. Series Line found for %1 on %2', NoSeriesCode, SeriesDate);
exit(NoSeriesLine);
end;
local procedure GenerateNextNumberInBatch(var NoSeriesLine: Record "No. Series Line"; BatchIndex: Integer): Code[20]
var
BaseNumber: Code[20];
NumberPart: BigInteger;
PrefixPart: Text;
SuffixPart: Text;
NumberLength: Integer;
begin
if BatchIndex = 1 then begin
if NoSeriesLine."Last No. Used" = '' then
BaseNumber := NoSeriesLine."Starting No."
else
BaseNumber := IncrementNumber(NoSeriesLine."Last No. Used", NoSeriesLine."Increment-by No.");
end else begin
BaseNumber := IncrementNumber(BaseNumber, NoSeriesLine."Increment-by No.");
end;
// Validate against ending number
if (NoSeriesLine."Ending No." <> '') and (BaseNumber > NoSeriesLine."Ending No.") then
Error('No. Series %1 would exceed its ending number %2 in batch allocation', NoSeriesLine."Series Code", NoSeriesLine."Ending No.");
exit(BaseNumber);
end;
local procedure UpdateNoSeriesLineForBatch(var NoSeriesLine: Record "No. Series Line"; LastAllocatedNo: Code[20])
begin
NoSeriesLine."Last No. Used" := LastAllocatedNo;
NoSeriesLine."Last Date Used" := Today;
NoSeriesLine.Modify(true);
end;
local procedure LockNoSeriesForBatch(NoSeriesCode: Code[20])
var
NoSeriesBatchLock: Record "No. Series Batch Lock";
begin
NoSeriesBatchLock.Init();
NoSeriesBatchLock."Series Code" := NoSeriesCode;
NoSeriesBatchLock."Locked By" := UserId;
NoSeriesBatchLock."Lock Time" := CurrentDateTime;
NoSeriesBatchLock.Insert(true);
end;
local procedure UnlockNoSeriesForBatch(NoSeriesCode: Code[20])
var
NoSeriesBatchLock: Record "No. Series Batch Lock";
begin
NoSeriesBatchLock.SetRange("Series Code", NoSeriesCode);
NoSeriesBatchLock.SetRange("Locked By", UserId);
if NoSeriesBatchLock.FindFirst() then
NoSeriesBatchLock.Delete(true);
end;
local procedure IncrementNumber(CurrentNo: Code[20]; IncrementBy: Integer): Code[20]
begin
// Implementation from previous examples
end;
local procedure LogBatchUsage(NoSeriesCode: Code[20]; UsedNo: Code[20])
var
NoSeriesBatchLog: Record "No. Series Batch Log";
begin
NoSeriesBatchLog.Init();
NoSeriesBatchLog."Entry No." := GetNextBatchLogEntryNo();
NoSeriesBatchLog."Series Code" := NoSeriesCode;
NoSeriesBatchLog."Used No." := UsedNo;
NoSeriesBatchLog."Usage Date" := Today;
NoSeriesBatchLog."Usage Time" := Time;
NoSeriesBatchLog."User ID" := UserId;
NoSeriesBatchLog.Insert(true);
end;
local procedure GetNextBatchLogEntryNo(): Integer
var
NoSeriesBatchLog: Record "No. Series Batch Log";
begin
if NoSeriesBatchLog.FindLast() then
exit(NoSeriesBatchLog."Entry No." + 1)
else
exit(1);
end;
}
```
## Multi-Dimensional Number Generation
```al
// Support for complex numbering schemes with multiple dimensions
codeunit 50503 "Multi-Dimensional No. Series"
{
procedure GetNextCompoundNo(BaseSeriesCode: Code[20]; Dimensions: Dictionary of [Text, Text]): Code[20]
var
CompoundCode: Code[20];
GeneratedNo: Code[20];
begin
CompoundCode := BuildCompoundSeriesCode(BaseSeriesCode, Dimensions);
GeneratedNo := GetOrCreateCompoundSeries(CompoundCode, Dimensions);
LogCompoundNumberGeneration(BaseSeriesCode, CompoundCode, GeneratedNo, Dimensions);
exit(GeneratedNo);
end;
procedure GetLocationBasedNo(BaseSeriesCode: Code[20]; LocationCode: Code[10]): Code[20]
var
Dimensions: Dictionary of [Text, Text];
begin
Dimensions.Set('LOCATION', LocationCode);
exit(GetNextCompoundNo(BaseSeriesCode, Dimensions));
end;
procedure GetYearBasedNo(BaseSeriesCode: Code[20]; FiscalYear: Integer): Code[20]
var
Dimensions: Dictionary of [Text, Text];
begin
Dimensions.Set('YEAR', Format(FiscalYear));
exit(GetNextCompoundNo(BaseSeriesCode, Dimensions));
end;
procedure GetDepartmentBasedNo(BaseSeriesCode: Code[20]; DepartmentCode: Code[10]; LocationCode: Code[10]): Code[20]
var
Dimensions: Dictionary of [Text, Text];
begin
Dimensions.Set('DEPARTMENT', DepartmentCode);
Dimensions.Set('LOCATION', LocationCode);
exit(GetNextCompoundNo(BaseSeriesCode, Dimensions));
end;
local procedure BuildCompoundSeriesCode(BaseSeriesCode: Code[20]; Dimensions: Dictionary of [Text, Text]): Code[20]
var
CompoundCode: Text;
DimensionKey: Text;
DimensionValue: Text;
SortedKeys: List of [Text];
begin
CompoundCode := BaseSeriesCode;
// Sort dimension keys for consistent ordering
foreach DimensionKey in Dimensions.Keys do
SortedKeys.Add(DimensionKey);
SortDimensionKeys(SortedKeys);
// Build compound code
foreach DimensionKey in SortedKeys do begin
Dimensions.Get(DimensionKey, DimensionValue);
CompoundCode += '-' + DimensionKey + '-' + DimensionValue;
end;
// Ensure code fits in 20 characters
if StrLen(CompoundCode) > 20 then
CompoundCode := CreateHashedCompoundCode(BaseSeriesCode, Dimensions);
exit(CopyStr(CompoundCode, 1, 20));
end;
local procedure GetOrCreateCompoundSeries(CompoundCode: Code[20]; Dimensions: Dictionary of [Text, Text]): Code[20]
var
NoSeries: Record "No. Series";
CustomNoSeriesManagement: Codeunit "Custom No. Series Management";
StartingNo: Code[20];
EndingNo: Code[20];
begin
if not NoSeries.Get(CompoundCode) then begin
// Create new compound series
StartingNo := BuildStartingNumber(Dimensions);
EndingNo := BuildEndingNumber(Dimensions);
if not CustomNoSeriesManagement.CreateCustomNoSeries(
CompoundCode,
'Auto-generated compound series',
StartingNo,
EndingNo) then
Error('Failed to create compound No. Series %1', CompoundCode);
end;
exit(CustomNoSeriesManagement.GetNextNo(CompoundCode, Today));
end;
local procedure BuildStartingNumber(Dimensions: Dictionary of [Text, Text]): Code[20]
var
StartingNo: Text;
DimensionKey: Text;
DimensionValue: Text;
begin
StartingNo := '';
// Build starting number based on dimensions
if Dimensions.Get('LOCATION', DimensionValue) then
StartingNo += DimensionValue;
if Dimensions.Get('DEPARTMENT', DimensionValue) then
StartingNo += DimensionValue;
if Dimensions.Get('YEAR', DimensionValue) then
StartingNo += CopyStr(DimensionValue, 3, 2); // Last 2 digits of year
StartingNo += '00001';
exit(CopyStr(StartingNo, 1, 20));
end;
local procedure BuildEndingNumber(Dimensions: Dictionary of [Text, Text]): Code[20]
var
EndingNo: Text;
DimensionKey: Text;
DimensionValue: Text;
begin
EndingNo := '';
// Build ending number based on dimensions
if Dimensions.Get('LOCATION', DimensionValue) then
EndingNo += DimensionValue;
if Dimensions.Get('DEPARTMENT', DimensionValue) then
EndingNo += DimensionValue;
if Dimensions.Get('YEAR', DimensionValue) then
EndingNo += CopyStr(DimensionValue, 3, 2); // Last 2 digits of year
EndingNo += '99999';
exit(CopyStr(EndingNo, 1, 20));
end;
local procedure CreateHashedCompoundCode(BaseSeriesCode: Code[20]; Dimensions: Dictionary of [Text, Text]): Code[20]
var
HashInput: Text;
DimensionKey: Text;
DimensionValue: Text;
HashValue: Text;
begin
HashInput := BaseSeriesCode;
foreach DimensionKey in Dimensions.Keys do begin
Dimensions.Get(DimensionKey, DimensionValue);
HashInput += DimensionKey + DimensionValue;
end;
// Create simple hash (in production, use proper hashing)
HashValue := Format(HashInput.GetHashCode(), 0, '<Integer,8><Filler Character,0>');
exit(CopyStr(BaseSeriesCode + '-' + HashValue, 1, 20));
end;
local procedure SortDimensionKeys(var Keys: List of [Text])
var
i, j: Integer;
temp: Text;
begin
// Simple bubble sort for demonstration
for i := 1 to Keys.Count - 1 do begin
for j := 1 to Keys.Count - i do begin
if Keys.Get(j) > Keys.Get(j + 1) then begin
temp := Keys.Get(j);
Keys.Set(j, Keys.Get(j + 1));
Keys.Set(j + 1, temp);
end;
end;
end;
end;
local procedure LogCompoundNumberGeneration(BaseSeriesCode: Code[20]; CompoundCode: Code[20]; GeneratedNo: Code[20]; Dimensions: Dictionary of [Text, Text])
var
CompoundNoLog: Record "Compound No. Log";
DimensionText: Text;
DimensionKey: Text;
DimensionValue: Text;
begin
// Build dimension text
foreach DimensionKey in Dimensions.Keys do begin
Dimensions.Get(DimensionKey, DimensionValue);
if DimensionText <> '' then
DimensionText += '; ';
DimensionText += DimensionKey + '=' + DimensionValue;
end;
CompoundNoLog.Init();
CompoundNoLog."Entry No." := GetNextCompoundLogEntryNo();
CompoundNoLog."Base Series Code" := BaseSeriesCode;
CompoundNoLog."Compound Series Code" := CompoundCode;
CompoundNoLog."Generated No." := GeneratedNo;
CompoundNoLog."Dimensions" := CopyStr(DimensionText, 1, 250);
CompoundNoLog."Generation Date" := Today;
CompoundNoLog."Generation Time" := Time;
CompoundNoLog."User ID" := UserId;
CompoundNoLog.Insert(true);
end;
local procedure GetNextCompoundLogEntryNo(): Integer
var
CompoundNoLog: Record "Compound No. Log";
begin
if CompoundNoLog.FindLast() then
exit(CompoundNoLog."Entry No." + 1)
else
exit(1);
end;
}
```
This comprehensive implementation provides robust number generation capabilities including basic number series management, concurrency control, batch allocation, and multi-dimensional numbering for complex Business Central scenarios.