UNPKG

bc-code-intelligence-mcp

Version:

BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows

758 lines (624 loc) 26.8 kB
# No Series Validation Patterns - AL Code Examples This sample demonstrates implementing comprehensive validation patterns for number sequence integrity in Business Central. ## Basic Validation Framework ```al // Core validation interface for no series interface INoSeriesValidator { procedure ValidateFormat(SeriesCode: Code[20]; NumberValue: Code[20]): Boolean; procedure ValidateSequence(SeriesCode: Code[20]): Boolean; procedure ValidateIntegrity(SeriesCode: Code[20]): Boolean; procedure GetValidationErrors(): List of [Text]; } ``` ## Comprehensive No Series Validator ```al // Main validation implementation codeunit 50900 "No Series Validator" implements INoSeriesValidator { var ValidationErrors: List of [Text]; ValidationWarnings: List of [Text]; procedure ValidateFormat(SeriesCode: Code[20]; NumberValue: Code[20]): Boolean var NoSeriesLine: Record "No. Series Line"; FormatValid: Boolean; begin Clear(ValidationErrors); FormatValid := true; if not FindNoSeriesLine(SeriesCode, NoSeriesLine) then begin AddValidationError(StrSubstNo('No. Series line not found for series %1', SeriesCode)); exit(false); end; // Validate number format if not ValidateNumberFormat(NumberValue, NoSeriesLine) then FormatValid := false; // Validate number range if not ValidateNumberRange(NumberValue, NoSeriesLine) then FormatValid := false; // Validate increment pattern if not ValidateIncrementPattern(NumberValue, NoSeriesLine) then FormatValid := false; exit(FormatValid); end; procedure ValidateSequence(SeriesCode: Code[20]): Boolean var NoSeriesLine: Record "No. Series Line"; NoSeriesLog: Record "No. Series Log"; SequenceValid: Boolean; GapCount: Integer; DuplicateCount: Integer; begin Clear(ValidationErrors); Clear(ValidationWarnings); SequenceValid := true; if not FindNoSeriesLine(SeriesCode, NoSeriesLine) then begin AddValidationError(StrSubstNo('No. Series line not found for series %1', SeriesCode)); exit(false); end; // Check for gaps in sequence GapCount := CheckForGaps(SeriesCode, NoSeriesLine); if GapCount > 0 then begin AddValidationWarning(StrSubstNo('%1 gaps found in sequence %2', GapCount, SeriesCode)); end; // Check for duplicates DuplicateCount := CheckForDuplicates(SeriesCode); if DuplicateCount > 0 then begin AddValidationError(StrSubstNo('%1 duplicate numbers found in sequence %2', DuplicateCount, SeriesCode)); SequenceValid := false; end; // Validate last used number if not ValidateLastUsedNumber(NoSeriesLine) then SequenceValid := false; // Check sequence boundaries if not ValidateSequenceBoundaries(NoSeriesLine) then SequenceValid := false; exit(SequenceValid); end; procedure ValidateIntegrity(SeriesCode: Code[20]): Boolean var IntegrityValid: Boolean; begin Clear(ValidationErrors); IntegrityValid := true; // Validate cross-table references if not ValidateCrossTableReferences(SeriesCode) then IntegrityValid := false; // Validate concurrency conflicts if not ValidateConcurrencyIntegrity(SeriesCode) then IntegrityValid := false; // Validate business rule compliance if not ValidateBusinessRules(SeriesCode) then IntegrityValid := false; // Validate system consistency if not ValidateSystemConsistency(SeriesCode) then IntegrityValid := false; exit(IntegrityValid); end; procedure GetValidationErrors(): List of [Text] begin exit(ValidationErrors); end; procedure GetValidationWarnings(): List of [Text] begin exit(ValidationWarnings); end; local procedure FindNoSeriesLine(SeriesCode: Code[20]; var NoSeriesLine: Record "No. Series Line"): Boolean begin NoSeriesLine.SetRange("Series Code", SeriesCode); NoSeriesLine.SetFilter("Starting Date", '<=%1', Today); NoSeriesLine.SetFilter("Ending Date", '%1|>=%2', 0D, Today); exit(NoSeriesLine.FindLast()); end; local procedure ValidateNumberFormat(NumberValue: Code[20]; NoSeriesLine: Record "No. Series Line"): Boolean var ExpectedFormat: Text; ActualFormat: Text; FormatValid: Boolean; begin FormatValid := true; // Validate length if StrLen(NumberValue) > 20 then begin AddValidationError(StrSubstNo('Number %1 exceeds maximum length of 20 characters', NumberValue)); FormatValid := false; end; // Validate prefix consistency if not ValidatePrefix(NumberValue, NoSeriesLine) then FormatValid := false; // Validate suffix consistency if not ValidateSuffix(NumberValue, NoSeriesLine) then FormatValid := false; // Validate numeric portion if not ValidateNumericPortion(NumberValue, NoSeriesLine) then FormatValid := false; exit(FormatValid); end; local procedure ValidateNumberRange(NumberValue: Code[20]; NoSeriesLine: Record "No. Series Line"): Boolean var RangeValid: Boolean; begin RangeValid := true; // Check if number is within starting range if (NoSeriesLine."Starting No." <> '') and (NumberValue < NoSeriesLine."Starting No.") then begin AddValidationError(StrSubstNo('Number %1 is below starting number %2', NumberValue, NoSeriesLine."Starting No.")); RangeValid := false; end; // Check if number is within ending range if (NoSeriesLine."Ending No." <> '') and (NumberValue > NoSeriesLine."Ending No.") then begin AddValidationError(StrSubstNo('Number %1 exceeds ending number %2', NumberValue, NoSeriesLine."Ending No.")); RangeValid := false; end; exit(RangeValid); end; local procedure ValidateIncrementPattern(NumberValue: Code[20]; NoSeriesLine: Record "No. Series Line"): Boolean var ExpectedIncrement: Integer; ActualIncrement: Integer; LastUsedNumber: Code[20]; begin if NoSeriesLine."Last No. Used" = '' then exit(true); // First number, no increment to validate LastUsedNumber := NoSeriesLine."Last No. Used"; ExpectedIncrement := NoSeriesLine."Increment-by No."; ActualIncrement := CalculateIncrement(LastUsedNumber, NumberValue); if ActualIncrement <> ExpectedIncrement then begin AddValidationError(StrSubstNo('Number %1 does not follow expected increment pattern. Expected: %2, Actual: %3', NumberValue, ExpectedIncrement, ActualIncrement)); exit(false); end; exit(true); end; local procedure CheckForGaps(SeriesCode: Code[20]; NoSeriesLine: Record "No. Series Line"): Integer var NoSeriesLog: Record "No. Series Log"; ExpectedNumber: Code[20]; CurrentNumber: Code[20]; GapCount: Integer; PreviousNumber: Code[20]; begin GapCount := 0; PreviousNumber := NoSeriesLine."Starting No."; NoSeriesLog.SetRange("Series Code", SeriesCode); NoSeriesLog.SetCurrentKey("Generation Date", "Generation Time"); if NoSeriesLog.FindSet() then repeat CurrentNumber := NoSeriesLog."Generated No."; ExpectedNumber := IncrementNumber(PreviousNumber, NoSeriesLine."Increment-by No."); if CurrentNumber <> ExpectedNumber then begin // Gap detected GapCount += 1; LogGap(SeriesCode, ExpectedNumber, CurrentNumber); end; PreviousNumber := CurrentNumber; until NoSeriesLog.Next() = 0; exit(GapCount); end; local procedure CheckForDuplicates(SeriesCode: Code[20]): Integer var NoSeriesLog: Record "No. Series Log"; TempNoSeriesLog: Record "No. Series Log" temporary; DuplicateCount: Integer; begin DuplicateCount := 0; NoSeriesLog.SetRange("Series Code", SeriesCode); if NoSeriesLog.FindSet() then repeat TempNoSeriesLog.SetRange("Generated No.", NoSeriesLog."Generated No."); if TempNoSeriesLog.FindFirst() then begin // Duplicate found DuplicateCount += 1; LogDuplicate(SeriesCode, NoSeriesLog."Generated No.", NoSeriesLog."Generation Date", TempNoSeriesLog."Generation Date"); end else begin TempNoSeriesLog.TransferFields(NoSeriesLog); TempNoSeriesLog.Insert(); end; until NoSeriesLog.Next() = 0; exit(DuplicateCount); end; local procedure ValidateLastUsedNumber(NoSeriesLine: Record "No. Series Line"): Boolean var NoSeriesLog: Record "No. Series Log"; LastLoggedNumber: Code[20]; begin if NoSeriesLine."Last No. Used" = '' then exit(true); // Find the most recent logged number NoSeriesLog.SetRange("Series Code", NoSeriesLine."Series Code"); if NoSeriesLog.FindLast() then begin LastLoggedNumber := NoSeriesLog."Generated No."; if LastLoggedNumber <> NoSeriesLine."Last No. Used" then begin AddValidationError(StrSubstNo('Last used number mismatch. Series Line: %1, Log: %2', NoSeriesLine."Last No. Used", LastLoggedNumber)); exit(false); end; end; exit(true); end; local procedure ValidateSequenceBoundaries(NoSeriesLine: Record "No. Series Line"): Boolean var BoundariesValid: Boolean; begin BoundariesValid := true; // Validate that starting number is less than ending number if (NoSeriesLine."Starting No." <> '') and (NoSeriesLine."Ending No." <> '') then begin if NoSeriesLine."Starting No." >= NoSeriesLine."Ending No." then begin AddValidationError(StrSubstNo('Starting number %1 must be less than ending number %2', NoSeriesLine."Starting No.", NoSeriesLine."Ending No.")); BoundariesValid := false; end; end; // Validate that last used number is within boundaries if NoSeriesLine."Last No. Used" <> '' then begin if (NoSeriesLine."Starting No." <> '') and (NoSeriesLine."Last No. Used" < NoSeriesLine."Starting No.") then begin AddValidationError(StrSubstNo('Last used number %1 is below starting number %2', NoSeriesLine."Last No. Used", NoSeriesLine."Starting No.")); BoundariesValid := false; end; if (NoSeriesLine."Ending No." <> '') and (NoSeriesLine."Last No. Used" > NoSeriesLine."Ending No.") then begin AddValidationError(StrSubstNo('Last used number %1 exceeds ending number %2', NoSeriesLine."Last No. Used", NoSeriesLine."Ending No.")); BoundariesValid := false; end; end; exit(BoundariesValid); end; local procedure ValidateCrossTableReferences(SeriesCode: Code[20]): Boolean var SalesHeader: Record "Sales Header"; PurchaseHeader: Record "Purchase Header"; ItemLedgerEntry: Record "Item Ledger Entry"; ReferenceValid: Boolean; OrphanCount: Integer; begin ReferenceValid := true; // Check for orphaned numbers in sales documents OrphanCount := CheckOrphanedReferences(SeriesCode, Database::"Sales Header"); if OrphanCount > 0 then begin AddValidationWarning(StrSubstNo('%1 orphaned references found in Sales Headers for series %2', OrphanCount, SeriesCode)); end; // Check for orphaned numbers in purchase documents OrphanCount := CheckOrphanedReferences(SeriesCode, Database::"Purchase Header"); if OrphanCount > 0 then begin AddValidationWarning(StrSubstNo('%1 orphaned references found in Purchase Headers for series %2', OrphanCount, SeriesCode)); end; exit(ReferenceValid); end; local procedure ValidateConcurrencyIntegrity(SeriesCode: Code[20]): Boolean var NoSeriesLock: Record "No. Series Lock"; ConcurrencyValid: Boolean; begin ConcurrencyValid := true; // Check for stale locks NoSeriesLock.SetRange("Series Code", SeriesCode); NoSeriesLock.SetFilter("Lock Time", '<%1', CurrentDateTime - 300000); // 5 minutes old if not NoSeriesLock.IsEmpty then begin AddValidationWarning(StrSubstNo('Stale locks found for series %1', SeriesCode)); // Clean up stale locks NoSeriesLock.DeleteAll(true); end; exit(ConcurrencyValid); end; local procedure ValidateBusinessRules(SeriesCode: Code[20]): Boolean var NoSeries: Record "No. Series"; BusinessRulesValid: Boolean; begin BusinessRulesValid := true; if not NoSeries.Get(SeriesCode) then begin AddValidationError(StrSubstNo('No. Series %1 not found', SeriesCode)); exit(false); end; // Validate business rule: Manual vs. Automatic if NoSeries."Manual Nos." and NoSeries."Default Nos." then begin AddValidationError(StrSubstNo('Series %1 cannot have both Manual and Default numbering enabled', SeriesCode)); BusinessRulesValid := false; end; // Validate date-based rules if not ValidateDateBasedRules(SeriesCode) then BusinessRulesValid := false; exit(BusinessRulesValid); end; local procedure ValidateSystemConsistency(SeriesCode: Code[20]): Boolean var NoSeriesLine: Record "No. Series Line"; ConsistencyValid: Boolean; OverlapCount: Integer; begin ConsistencyValid := true; // Check for overlapping date ranges OverlapCount := CheckDateRangeOverlaps(SeriesCode); if OverlapCount > 0 then begin AddValidationError(StrSubstNo('%1 overlapping date ranges found for series %2', OverlapCount, SeriesCode)); ConsistencyValid := false; end; // Check for missing date ranges if not ValidateDateRangeContinuity(SeriesCode) then ConsistencyValid := false; exit(ConsistencyValid); end; // Helper procedures... local procedure ValidatePrefix(NumberValue: Code[20]; NoSeriesLine: Record "No. Series Line"): Boolean begin // Implement prefix validation logic exit(true); end; local procedure ValidateSuffix(NumberValue: Code[20]; NoSeriesLine: Record "No. Series Line"): Boolean begin // Implement suffix validation logic exit(true); end; local procedure ValidateNumericPortion(NumberValue: Code[20]; NoSeriesLine: Record "No. Series Line"): Boolean begin // Implement numeric portion validation logic exit(true); end; local procedure CalculateIncrement(FromNumber: Code[20]; ToNumber: Code[20]): Integer begin // Implement increment calculation logic exit(1); end; local procedure IncrementNumber(Number: Code[20]; IncrementBy: Integer): Code[20] begin // Implement number increment logic exit(Number); end; local procedure CheckOrphanedReferences(SeriesCode: Code[20]; TableNo: Integer): Integer begin // Implement orphaned reference checking exit(0); end; local procedure CheckDateRangeOverlaps(SeriesCode: Code[20]): Integer begin // Implement date range overlap checking exit(0); end; local procedure ValidateDateRangeContinuity(SeriesCode: Code[20]): Boolean begin // Implement date range continuity validation exit(true); end; local procedure ValidateDateBasedRules(SeriesCode: Code[20]): Boolean begin // Implement date-based business rule validation exit(true); end; local procedure AddValidationError(ErrorText: Text) begin ValidationErrors.Add(ErrorText); end; local procedure AddValidationWarning(WarningText: Text) begin ValidationWarnings.Add(WarningText); end; local procedure LogGap(SeriesCode: Code[20]; ExpectedNumber: Code[20]; ActualNumber: Code[20]) begin // Log gap detection for analysis end; local procedure LogDuplicate(SeriesCode: Code[20]; DuplicateNumber: Code[20]; Date1: Date; Date2: Date) begin // Log duplicate detection for analysis end; } ``` ## Real-Time Validation Integration ```al // Real-time validation during number generation codeunit 50901 "Real-Time No Series Validator" { var Validator: Codeunit "No Series Validator"; [EventSubscriber(ObjectType::Codeunit, Codeunit::"No. Series Management", 'OnBeforeGetNextNo', '', false, false)] local procedure OnBeforeGetNextNo(NoSeriesCode: Code[20]; SeriesDate: Date; var NoSeriesLine: Record "No. Series Line") begin ValidateBeforeGeneration(NoSeriesCode, NoSeriesLine); end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"No. Series Management", 'OnAfterGetNextNo', '', false, false)] local procedure OnAfterGetNextNo(NoSeriesCode: Code[20]; SeriesDate: Date; var NoSeriesLine: Record "No. Series Line"; var NextNo: Code[20]) begin ValidateAfterGeneration(NoSeriesCode, NextNo, NoSeriesLine); end; local procedure ValidateBeforeGeneration(NoSeriesCode: Code[20]; NoSeriesLine: Record "No. Series Line") var ValidationErrors: List of [Text]; ErrorText: Text; begin if not Validator.ValidateSequence(NoSeriesCode) then begin ValidationErrors := Validator.GetValidationErrors(); foreach ErrorText in ValidationErrors do Error('No. Series validation failed: %1', ErrorText); end; end; local procedure ValidateAfterGeneration(NoSeriesCode: Code[20]; NextNo: Code[20]; NoSeriesLine: Record "No. Series Line") var ValidationErrors: List of [Text]; ErrorText: Text; begin if not Validator.ValidateFormat(NoSeriesCode, NextNo) then begin ValidationErrors := Validator.GetValidationErrors(); foreach ErrorText in ValidationErrors do Error('Generated number validation failed: %1', ErrorText); end; // Log successful generation LogNumberGeneration(NoSeriesCode, NextNo); 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; } ``` ## Batch Validation Tool ```al // Comprehensive batch validation for all series codeunit 50902 "Batch No Series Validation" { var Validator: Codeunit "No Series Validator"; procedure ValidateAllSeries(): Dictionary of [Code[20], List of [Text]] var NoSeries: Record "No. Series"; ValidationResults: Dictionary of [Code[20], List of [Text]]; SeriesErrors: List of [Text]; begin if NoSeries.FindSet() then repeat Clear(SeriesErrors); // Run comprehensive validation if not Validator.ValidateSequence(NoSeries.Code) then AddErrors(SeriesErrors, Validator.GetValidationErrors()); if not Validator.ValidateIntegrity(NoSeries.Code) then AddErrors(SeriesErrors, Validator.GetValidationErrors()); ValidationResults.Set(NoSeries.Code, SeriesErrors); until NoSeries.Next() = 0; exit(ValidationResults); end; procedure GenerateValidationReport(): Text var ValidationResults: Dictionary of [Code[20], List of [Text]]; ReportText: Text; SeriesCode: Code[20]; SeriesErrors: List of [Text]; ErrorText: Text; TotalSeries: Integer; SeriesWithErrors: Integer; begin ValidationResults := ValidateAllSeries(); TotalSeries := ValidationResults.Count; ReportText := StrSubstNo('No. Series Validation Report - %1\n', CurrentDateTime); ReportText += StrSubstNo('Total Series Validated: %1\n\n', TotalSeries); foreach SeriesCode in ValidationResults.Keys do begin ValidationResults.Get(SeriesCode, SeriesErrors); if SeriesErrors.Count > 0 then begin SeriesWithErrors += 1; ReportText += StrSubstNo('Series: %1 (%2 errors)\n', SeriesCode, SeriesErrors.Count); foreach ErrorText in SeriesErrors do ReportText += StrSubstNo(' - %1\n', ErrorText); ReportText += '\n'; end; end; ReportText += StrSubstNo('Summary: %1 of %2 series have validation errors.\n', SeriesWithErrors, TotalSeries); exit(ReportText); end; procedure FixCommonIssues(): Integer var ValidationResults: Dictionary of [Code[20], List of [Text]]; SeriesCode: Code[20]; SeriesErrors: List of [Text]; FixedCount: Integer; begin ValidationResults := ValidateAllSeries(); foreach SeriesCode in ValidationResults.Keys do begin ValidationResults.Get(SeriesCode, SeriesErrors); if SeriesErrors.Count > 0 then begin if FixSeriesIssues(SeriesCode, SeriesErrors) then FixedCount += 1; end; end; exit(FixedCount); end; local procedure FixSeriesIssues(SeriesCode: Code[20]; Errors: List of [Text]): Boolean var ErrorText: Text; FixedAny: Boolean; begin FixedAny := false; foreach ErrorText in Errors do begin case true of StrPos(ErrorText, 'Stale locks') > 0: begin CleanupStaleLocks(SeriesCode); FixedAny := true; end; StrPos(ErrorText, 'orphaned references') > 0: begin CleanupOrphanedReferences(SeriesCode); FixedAny := true; end; end; end; exit(FixedAny); end; local procedure AddErrors(var TargetList: List of [Text]; SourceList: List of [Text]) var ErrorText: Text; begin foreach ErrorText in SourceList do TargetList.Add(ErrorText); end; local procedure CleanupStaleLocks(SeriesCode: Code[20]) var NoSeriesLock: Record "No. Series Lock"; begin NoSeriesLock.SetRange("Series Code", SeriesCode); NoSeriesLock.SetFilter("Lock Time", '<%1', CurrentDateTime - 300000); NoSeriesLock.DeleteAll(true); end; local procedure CleanupOrphanedReferences(SeriesCode: Code[20]) begin // Implement orphaned reference cleanup end; } ``` ## Usage Example ```al // Example of using validation patterns codeunit 50903 "Validation Usage Example" { procedure DemonstrateValidation() var Validator: Codeunit "No Series Validator"; BatchValidator: Codeunit "Batch No Series Validation"; ValidationReport: Text; SeriesCode: Code[20]; TestNumber: Code[20]; ValidationErrors: List of [Text]; ErrorText: Text; FixedCount: Integer; begin SeriesCode := 'SALES-ORDER'; TestNumber := 'SO-2024-001'; // Validate specific number format if Validator.ValidateFormat(SeriesCode, TestNumber) then Message('Number format validation passed') else begin ValidationErrors := Validator.GetValidationErrors(); foreach ErrorText in ValidationErrors do Message('Format error: %1', ErrorText); end; // Validate sequence integrity if Validator.ValidateSequence(SeriesCode) then Message('Sequence validation passed') else begin ValidationErrors := Validator.GetValidationErrors(); foreach ErrorText in ValidationErrors do Message('Sequence error: %1', ErrorText); end; // Run batch validation ValidationReport := BatchValidator.GenerateValidationReport(); Message('Validation Report:\n%1', ValidationReport); // Fix common issues FixedCount := BatchValidator.FixCommonIssues(); Message('Fixed %1 series issues automatically', FixedCount); end; } ``` This comprehensive validation implementation provides robust number sequence integrity checking with real-time validation, batch processing capabilities, and automatic issue remediation for Business Central number series management.