diff --git a/src/Business Foundation/App/NoSeries/src/Batch/NoSeriesBatchImpl.Codeunit.al b/src/Business Foundation/App/NoSeries/src/Batch/NoSeriesBatchImpl.Codeunit.al index 508b9fc4df..229a9e84e5 100644 --- a/src/Business Foundation/App/NoSeries/src/Batch/NoSeriesBatchImpl.Codeunit.al +++ b/src/Business Foundation/App/NoSeries/src/Batch/NoSeriesBatchImpl.Codeunit.al @@ -59,7 +59,7 @@ codeunit 309 "No. Series - Batch Impl." var NoSeries: Codeunit "No. Series"; begin - SetInitialState(TempNoSeriesLine); + SyncGlobalLineWithProvidedLine(TempNoSeriesLine, UsageDate); exit(NoSeries.PeekNextNo(TempGlobalNoSeriesLine, UsageDate)); end; diff --git a/src/Business Foundation/App/NoSeries/src/Legacy/NoSeriesManagement.Codeunit.al b/src/Business Foundation/App/NoSeries/src/Legacy/NoSeriesManagement.Codeunit.al index d9ed3eeb32..74fc1609c9 100644 --- a/src/Business Foundation/App/NoSeries/src/Legacy/NoSeriesManagement.Codeunit.al +++ b/src/Business Foundation/App/NoSeries/src/Legacy/NoSeriesManagement.Codeunit.al @@ -966,6 +966,12 @@ codeunit 396 NoSeriesManagement begin end; #endif + [Obsolete('This is a temporary method for compatibility only. Please use the "No. Series" codeunit instead', '24.0')] + internal procedure RaiseObsoleteOnBeforeModifyNoSeriesLine(var NoSeriesLine: Record "No. Series Line"; var IsHandled: Boolean) + begin + OnBeforeModifyNoSeriesLine(NoSeriesLine, IsHandled); + end; + [IntegrationEvent(false, false)] internal procedure OnBeforeModifyNoSeriesLine(var NoSeriesLine: Record "No. Series Line"; var IsHandled: Boolean) begin diff --git a/src/Business Foundation/App/NoSeries/src/Setup/NoSeriesLine.Table.al b/src/Business Foundation/App/NoSeries/src/Setup/NoSeriesLine.Table.al index 426ad4b9f3..b3f5c08aad 100644 --- a/src/Business Foundation/App/NoSeries/src/Setup/NoSeriesLine.Table.al +++ b/src/Business Foundation/App/NoSeries/src/Setup/NoSeriesLine.Table.al @@ -126,6 +126,12 @@ table 309 "No. Series Line" DataClassification = SystemMetadata; } + field(15; "Temp Current Sequence No."; Integer) + { + Caption = 'Temporary Sequence Number'; + DataClassification = SystemMetadata; + Access = Internal; + } } keys diff --git a/src/Business Foundation/App/NoSeries/src/Single/NoSeriesImpl.Codeunit.al b/src/Business Foundation/App/NoSeries/src/Single/NoSeriesImpl.Codeunit.al index 8209b8d91b..f6783370ee 100644 --- a/src/Business Foundation/App/NoSeries/src/Single/NoSeriesImpl.Codeunit.al +++ b/src/Business Foundation/App/NoSeries/src/Single/NoSeriesImpl.Codeunit.al @@ -152,6 +152,11 @@ codeunit 304 "No. Series - Impl." NoSeriesRec: Record "No. Series"; NoSeries: Codeunit "No. Series"; NoSeriesErrorsImpl: Codeunit "No. Series - Errors Impl."; +#if not CLEAN24 +#pragma warning disable AL0432 + NoSeriesManagement: Codeunit NoSeriesManagement; +#pragma warning restore AL0432 +#endif LineFound: Boolean; begin if UsageDate = 0D then @@ -165,12 +170,26 @@ codeunit 304 "No. Series - Impl." NoSeriesLine.SetRange(Open, true); if (NoSeriesLine."Line No." <> 0) and (NoSeriesLine."Series Code" = NoSeriesCode) then begin NoSeriesLine.SetRange("Line No.", NoSeriesLine."Line No."); +#if not CLEAN24 +#pragma warning disable AL0432 + NoSeriesManagement.RaiseObsoleteOnNoSeriesLineFilterOnBeforeFindLast(NoSeriesLine); +#pragma warning restore AL0432 +#endif LineFound := NoSeriesLine.FindLast(); if not LineFound then NoSeriesLine.SetRange("Line No."); end; +#if not CLEAN24 +#pragma warning disable AL0432 + if not LineFound then begin + NoSeriesManagement.RaiseObsoleteOnNoSeriesLineFilterOnBeforeFindLast(NoSeriesLine); + LineFound := NoSeriesLine.FindLast(); + end; +#pragma warning restore AL0432 +#else if not LineFound then LineFound := NoSeriesLine.FindLast(); +#endif if LineFound and NoSeries.MayProduceGaps(NoSeriesLine) then begin NoSeriesLine.Validate(Open); diff --git a/src/Business Foundation/App/NoSeries/src/Single/NoSeriesSequenceImpl.Codeunit.al b/src/Business Foundation/App/NoSeries/src/Single/NoSeriesSequenceImpl.Codeunit.al index fa7f3944f3..a0935fd809 100644 --- a/src/Business Foundation/App/NoSeries/src/Single/NoSeriesSequenceImpl.Codeunit.al +++ b/src/Business Foundation/App/NoSeries/src/Single/NoSeriesSequenceImpl.Codeunit.al @@ -31,11 +31,7 @@ codeunit 307 "No. Series - Sequence Impl." implements "No. Series - Single" var LastSeqNoUsed: BigInteger; begin - if not TryGetCurrentSequenceNo(NoSeriesLine."Sequence Name", LastSeqNoUsed) then begin - if not NumberSequence.Exists(NoSeriesLine."Sequence Name") then - CreateNewSequence(NoSeriesLine); - TryGetCurrentSequenceNo(NoSeriesLine."Sequence Name", LastSeqNoUsed); - end; + LastSeqNoUsed := GetCurrentSequenceNo(NoSeriesLine); if LastSeqNoUsed >= NoSeriesLine."Starting Sequence No." then exit(GetFormattedNo(NoSeriesLine, LastSeqNoUsed)); exit(''); // No. Series has not been used yet, so there is no last no. used @@ -46,6 +42,18 @@ codeunit 307 "No. Series - Sequence Impl." implements "No. Series - Single" exit(true); end; + local procedure GetCurrentSequenceNo(var NoSeriesLine: Record "No. Series Line") LastSeqNoUsed: BigInteger + begin + if NoSeriesLine."Temp Current Sequence No." <> 0 then + exit(NoSeriesLine."Temp Current Sequence No."); + + if not TryGetCurrentSequenceNo(NoSeriesLine."Sequence Name", LastSeqNoUsed) then begin + if not NumberSequence.Exists(NoSeriesLine."Sequence Name") then + CreateNewSequence(NoSeriesLine); + TryGetCurrentSequenceNo(NoSeriesLine."Sequence Name", LastSeqNoUsed); + end; + end; + [TryFunction] local procedure TryGetCurrentSequenceNo(SequenceName: Code[40]; var LastSeqNoUsed: BigInteger) begin @@ -57,13 +65,28 @@ codeunit 307 "No. Series - Sequence Impl." implements "No. Series - Single" var NoSeriesLine2: Record "No. Series Line"; NoSeriesStatelessImpl: Codeunit "No. Series - Stateless Impl."; +#if not CLEAN24 +#pragma warning disable AL0432 + NoSeriesManagement: Codeunit NoSeriesManagement; + IsHandled: Boolean; +#pragma warning restore AL0432 +#endif NewNo: BigInteger; begin - if not TryGetNextSequenceNo(NoSeriesLine, ModifySeries, NewNo) then begin - if not NumberSequence.Exists(NoSeriesLine."Sequence Name") then - CreateNewSequence(NoSeriesLine); - TryGetNextSequenceNo(NoSeriesLine, ModifySeries, NewNo); - end; + if NoSeriesLine.IsTemporary() or (NoSeriesLine."Temp Current Sequence No." <> 0) then begin // Do not update the database for temporary records, if Temp Current Sequence No. is set that means we are emulating the next numbers + if NoSeriesLine."Temp Current Sequence No." = 0 then + NoSeriesLine."Temp Current Sequence No." := GetCurrentSequenceNo(NoSeriesLine); + + NewNo := NoSeriesLine."Temp Current Sequence No." + NoSeriesLine."Increment-by No."; + + if ModifySeries then + NoSeriesLine."Temp Current Sequence No." := NewNo; + end else + if not TryGetNextSequenceNo(NoSeriesLine, ModifySeries, NewNo) then begin + if not NumberSequence.Exists(NoSeriesLine."Sequence Name") then + CreateNewSequence(NoSeriesLine); + TryGetNextSequenceNo(NoSeriesLine, ModifySeries, NewNo); + end; NoSeriesLine2 := NoSeriesLine; NoSeriesLine2."Last No. Used" := GetFormattedNo(NoSeriesLine, NewNo); @@ -72,9 +95,15 @@ codeunit 307 "No. Series - Sequence Impl." implements "No. Series - Single" if not NoSeriesStatelessImpl.EnsureLastNoUsedIsWithinValidRange(NoSeriesLine2, HideErrorsAndWarnings) then exit(''); - if ModifySeries and ((NoSeriesLine."Last Date Used" <> UsageDate) or (NoSeriesLine.Open <> NoSeriesLine2.Open)) then begin // Only modify the series if either the date or the open status has changed. Otherwise avoid locking the record. + if ModifySeries and ((NoSeriesLine."Last Date Used" <> UsageDate) or (NoSeriesLine.Open <> NoSeriesLine2.Open) or NoSeriesLine.IsTemporary()) then begin // Only modify the series if either the date or the open status has changed. Otherwise avoid locking the record. NoSeriesLine."Last Date Used" := UsageDate; NoSeriesLine.Open := NoSeriesLine2.Open; +#if not CLEAN24 +#pragma warning disable AL0432 + NoSeriesManagement.RaiseObsoleteOnBeforeModifyNoSeriesLine(NoSeriesLine, IsHandled); + if not IsHandled then +#pragma warning restore AL0432 +#endif NoSeriesLine.Modify(true); end; @@ -268,4 +297,33 @@ codeunit 307 "No. Series - Sequence Impl." implements "No. Series - Single" if NumberSequence.Exists(Rec."Sequence Name") then NumberSequence.Delete(Rec."Sequence Name"); end; + + [EventSubscriber(ObjectType::Table, Database::"No. Series Line", 'OnBeforeModifyEvent', '', false, false)] + local procedure OnModifyNoSeriesLine(var Rec: Record "No. Series Line"; RunTrigger: Boolean) + begin + if Rec.IsTemporary() then + exit; + + EnsureTempCurrentSequenceNoIsReset(Rec); + end; + + [EventSubscriber(ObjectType::Table, Database::"No. Series Line", 'OnBeforeInsertEvent', '', false, false)] + local procedure OnInsertNoSeriesLine(var Rec: Record "No. Series Line"; RunTrigger: Boolean) + begin + if Rec.IsTemporary() then + exit; + + EnsureTempCurrentSequenceNoIsReset(Rec); + end; + + local procedure EnsureTempCurrentSequenceNoIsReset(var NoSeriesLine: Record "No. Series Line") + begin + if NoSeriesLine."Temp Current Sequence No." = 0 then + exit; + + if NoSeriesLine.Implementation = "No. Series Implementation"::Sequence then + RecreateNoSeriesWithLastUsedNo(NoSeriesLine, NoSeriesLine."Temp Current Sequence No."); + + NoSeriesLine."Temp Current Sequence No." := 0; // Always reset the temporary sequence number! + end; } \ No newline at end of file diff --git a/src/Business Foundation/Test/NoSeries/src/NoSeriesBatchTests.Codeunit.al b/src/Business Foundation/Test/NoSeries/src/NoSeriesBatchTests.Codeunit.al index 47d09e540f..61051b7e12 100644 --- a/src/Business Foundation/Test/NoSeries/src/NoSeriesBatchTests.Codeunit.al +++ b/src/Business Foundation/Test/NoSeries/src/NoSeriesBatchTests.Codeunit.al @@ -570,6 +570,7 @@ codeunit 134531 "No. Series Batch Tests" NoSeriesBatch: Codeunit "No. Series - Batch"; PermissionsMock: Codeunit "Permissions Mock"; NoSeriesBatch2: Codeunit "No. Series - Batch"; + NoSeriesBatch3: Codeunit "No. Series - Batch"; NoSeriesCode: Code[20]; i: Integer; begin @@ -601,10 +602,17 @@ codeunit 134531 "No. Series Batch Tests" LibraryAssert.AreEqual('B3', NoSeriesBatch.GetNextNo(NoSeriesCode, WorkDate()), 'Getting Next No. should continue the simulation'); // [WHEN] We get the next number from the No. Series using a different batch - // [THEN] The numbers from line 1 are exhausted so we continue from line 2 - LibraryAssert.AreEqual('B4', NoSeriesBatch2.GetNextNo(NoSeriesCode, WorkDate()), 'No numbers from the sequence should have been used'); - LibraryAssert.AreEqual('B5', NoSeriesBatch2.GetNextNo(NoSeriesCode, WorkDate()), 'No numbers from the sequence should have been used'); - LibraryAssert.AreEqual('B6', NoSeriesBatch2.GetNextNo(NoSeriesCode, WorkDate()), 'No numbers from the sequence should have been used'); + // [THEN] The numbers start again from A1 + LibraryAssert.AreEqual('A1', NoSeriesBatch2.GetNextNo(NoSeriesCode, WorkDate()), 'No numbers from the sequence should have been used'); + LibraryAssert.AreEqual('A2', NoSeriesBatch2.GetNextNo(NoSeriesCode, WorkDate()), 'No numbers from the sequence should have been used'); + LibraryAssert.AreEqual('A3', NoSeriesBatch2.GetNextNo(NoSeriesCode, WorkDate()), 'No numbers from the sequence should have been used'); + + // [WHEN] We save the original batch + NoSeriesBatch.SaveState(); + // [THEN] The numbers from another batch will continue from line 2 + LibraryAssert.AreEqual('B4', NoSeriesBatch3.GetNextNo(NoSeriesCode, WorkDate()), 'No numbers from the sequence should have been used'); + LibraryAssert.AreEqual('B5', NoSeriesBatch3.GetNextNo(NoSeriesCode, WorkDate()), 'No numbers from the sequence should have been used'); + LibraryAssert.AreEqual('B6', NoSeriesBatch3.GetNextNo(NoSeriesCode, WorkDate()), 'No numbers from the sequence should have been used'); end; [Test] @@ -735,9 +743,9 @@ codeunit 134531 "No. Series Batch Tests" // [THEN] No number is returned LibraryAssert.AreEqual('', NoSeriesBatch.GetNextNo(NoSeriesCode, WorkDate(), true), 'A number was returned even though the sequence has run out'); - // [THEN] GetLastNoUsed returns blank, however new batch references will return A9 until save since the Line is not yet closed but the sequence is updated in the database. + // [THEN] GetLastNoUsed returns A9, however new batch references will return blank since no number has been saved. LibraryAssert.AreEqual('A9', NoSeriesBatch.GetLastNoUsed(NoSeriesLine), 'GetLastNoUsed Number was not as expected after getting invalid number'); - LibraryAssert.AreEqual('A9', NoSeriesBatch2.GetLastNoUsed(NoSeriesLine), 'GetLastNoUsed Number was not as expected'); + LibraryAssert.AreEqual('', NoSeriesBatch2.GetLastNoUsed(NoSeriesLine), 'GetLastNoUsed Number was not as expected'); // [GIVEN] The No. Series is saved NoSeriesBatch.SaveState(); @@ -859,6 +867,58 @@ codeunit 134531 "No. Series Batch Tests" end; #endregion + [Test] + procedure TestSequenceTempCurrentSequenceNoField() + var + NoSeriesLine: Record "No. Series Line"; + TempNoSeriesLine: Record "No. Series Line" temporary; + NoSeriesBatch: Codeunit "No. Series - Batch"; + NoSeriesBatch2: Codeunit "No. Series - Batch"; + PermissionsMock: Codeunit "Permissions Mock"; + RecordRef: RecordRef; + TempCurrentSequenceNoField: FieldRef; + NoSeriesCode: Code[20]; + begin + // [Scenario] Make sure the Temp Current Sequence No. field cannot be abused and is always set to 0 upon database modify + // Note: These scenarios are not supported, this is simply to make sure the Temp Current Sequence No. field works as expected behind the scenes. + + Initialize(); + PermissionsMock.Set('No. Series - Admin'); + + // [GIVEN] A No. Series with 10 numbers + NoSeriesCode := CopyStr(UpperCase(Any.AlphabeticText(MaxStrLen(NoSeriesCode))), 1, MaxStrLen(NoSeriesCode)); + LibraryNoSeries.CreateNoSeries(NoSeriesCode); + LibraryNoSeries.CreateSequenceNoSeriesLine(NoSeriesCode, 1, '1', '9'); + NoSeriesLine.SetRange("Series Code", NoSeriesCode); + NoSeriesLine.FindFirst(); + TempNoSeriesLine := NoSeriesLine; + TempNoSeriesLine.Insert(); + + // [GIVEN] A No. Series Line and Temporary No. Series Line with Current Sequence No. set to 5 + PermissionsMock.SetExactPermissionSet('No. Series Test'); + RecordRef.GetTable(NoSeriesLine); + TempCurrentSequenceNoField := RecordRef.Field(15); // Field "Temp Current Sequence No." + TempCurrentSequenceNoField.Value := 5; + RecordRef.SetTable(NoSeriesLine); + TempNoSeriesLine := NoSeriesLine; + + // [WHEN] Fetching the Last No. Used, both implementations return blank (batch will fetch the correct line from the database which does not contain the Temp Current Sequence No.) + LibraryAssert.AreEqual('', NoSeriesBatch.GetLastNoUsed(NoSeriesLine), 'GetLastNoUsed returned wrong value'); + LibraryAssert.AreEqual('', NoSeriesBatch2.GetLastNoUsed(TempNoSeriesLine), 'GetLastNoUsed with temporary record returned wrong value'); + + // [WHEN] We peek the next number from both No. Series, they both return 1 since no number has been used + LibraryAssert.AreEqual('1', NoSeriesBatch.PeekNextNo(NoSeriesLine, WorkDate()), 'PeekNextNo returned wrong value'); + LibraryAssert.AreEqual('1', NoSeriesBatch2.PeekNextNo(TempNoSeriesLine, WorkDate()), 'PeekNextNo with temporary record returned wrong value'); + + // [WHEN] We get the next number from both No. Series, they both return 1 + LibraryAssert.AreEqual('1', NoSeriesBatch.GetNextNo(NoSeriesLine, WorkDate()), 'GetNextNo returned wrong value'); + LibraryAssert.AreEqual('1', NoSeriesBatch2.GetNextNo(TempNoSeriesLine, WorkDate()), 'GetNextNo with temporary record returned wrong value'); + + // [THEN] Getting the next number, they again both return 7 + LibraryAssert.AreEqual('2', NoSeriesBatch.GetNextNo(NoSeriesLine, WorkDate()), 'GetNextNo returned wrong value'); + LibraryAssert.AreEqual('2', NoSeriesBatch2.GetNextNo(TempNoSeriesLine, WorkDate()), 'GetNextNo with temporary record returned wrong value'); + end; + local procedure Initialize() begin Any.SetDefaultSeed(); diff --git a/src/Business Foundation/Test/NoSeries/src/NoSeriesTests.Codeunit.al b/src/Business Foundation/Test/NoSeries/src/NoSeriesTests.Codeunit.al index 092a8ccaee..a1db8c6727 100644 --- a/src/Business Foundation/Test/NoSeries/src/NoSeriesTests.Codeunit.al +++ b/src/Business Foundation/Test/NoSeries/src/NoSeriesTests.Codeunit.al @@ -731,6 +731,60 @@ codeunit 134530 "No. Series Tests" end; #endregion + [Test] + procedure TestSequenceTempCurrentSequenceNoField() + var + NoSeriesLine: Record "No. Series Line"; + TempNoSeriesLine: Record "No. Series Line" temporary; + NoSeries: Codeunit "No. Series"; + PermissionsMock: Codeunit "Permissions Mock"; + RecordRef: RecordRef; + TempCurrentSequenceNoField: FieldRef; + NoSeriesCode: Code[20]; + begin + // [Scenario] Make sure the Temp Current Sequence No. field cannot be abused and is always set to 0 upon database modify + // Note: These scenarios are not supported, this is simply to make sure the Temp Current Sequence No. field works as expected behind the scenes. + + Initialize(); + PermissionsMock.Set('No. Series - Admin'); + + // [GIVEN] A No. Series with 10 numbers + NoSeriesCode := CopyStr(UpperCase(Any.AlphabeticText(MaxStrLen(NoSeriesCode))), 1, MaxStrLen(NoSeriesCode)); + LibraryNoSeries.CreateNoSeries(NoSeriesCode); + LibraryNoSeries.CreateSequenceNoSeriesLine(NoSeriesCode, 1, '1', '9'); + NoSeriesLine.SetRange("Series Code", NoSeriesCode); + NoSeriesLine.FindFirst(); + TempNoSeriesLine := NoSeriesLine; + TempNoSeriesLine.Insert(); + + // [GIVEN] A No. Series Line and Temporary No. Series Line with Current Sequence No. set to 5 + PermissionsMock.SetExactPermissionSet('No. Series Test'); + RecordRef.GetTable(NoSeriesLine); + TempCurrentSequenceNoField := RecordRef.Field(15); // Field "Temp Current Sequence No." + TempCurrentSequenceNoField.Value := 5; + RecordRef.SetTable(NoSeriesLine); + TempNoSeriesLine := NoSeriesLine; + + // [WHEN] Fetching the Last No. Used, both implementations return 5 + LibraryAssert.AreEqual('5', NoSeries.GetLastNoUsed(NoSeriesLine), 'GetLastNoUsed returned wrong value'); + LibraryAssert.AreEqual('5', NoSeries.GetLastNoUsed(TempNoSeriesLine), 'GetLastNoUsed with temporary record returned wrong value'); + + // [WHEN] We peek the next number from both No. Series, they both return 6 + LibraryAssert.AreEqual('6', NoSeries.PeekNextNo(NoSeriesLine, WorkDate()), 'PeekNextNo returned wrong value'); + LibraryAssert.AreEqual('6', NoSeries.PeekNextNo(TempNoSeriesLine, WorkDate()), 'PeekNextNo with temporary record returned wrong value'); + + // [WHEN] We get the next number from both No. Series, they both return 6 since that's the number set to be next number based on Temp Current Sequence No., furthermore the Temp Current Sequence No. in the normal No. Series has been saved into the sequence due to modify on Get + LibraryAssert.AreEqual('6', NoSeries.GetNextNo(NoSeriesLine, WorkDate()), 'GetNextNo returned wrong value'); + LibraryAssert.AreEqual('6', NoSeries.GetNextNo(TempNoSeriesLine, WorkDate()), 'GetNextNo with temporary record returned wrong value'); + RecordRef.GetTable(NoSeriesLine); + TempCurrentSequenceNoField := RecordRef.Field(15); // Field "Temp Current Sequence No." + LibraryAssert.AreEqual(0, TempCurrentSequenceNoField.Value, 'Temp Current Sequence No. was not as expected'); + + // [THEN] Getting the next number, they again both return 7 + LibraryAssert.AreEqual('7', NoSeries.GetNextNo(NoSeriesLine, WorkDate()), 'GetNextNo returned wrong value'); + LibraryAssert.AreEqual('7', NoSeries.GetNextNo(TempNoSeriesLine, WorkDate()), 'GetNextNo with temporary record returned wrong value'); + end; + local procedure Initialize() begin Any.SetDefaultSeed();