diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLine.Table.al b/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLine.Table.al
index b2286f82d46..f7eb0ec8a68 100644
--- a/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLine.Table.al
+++ b/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLine.Table.al
@@ -470,6 +470,11 @@ table 8061 "Billing Line"
Rec.SetRange("Subscription Contract Line No.", ContractLineNo);
end;
+ internal procedure FilterBillingLineOnServiceCommitment(ServiceCommitmentEntryNo: Integer)
+ begin
+ Rec.SetRange("Subscription Line Entry No.", ServiceCommitmentEntryNo);
+ end;
+
local procedure RecalculateCustomerContractHarmonizedBillingFields()
var
CustomerContract: Record "Customer Subscription Contract";
diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLineArchive.Table.al b/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLineArchive.Table.al
index 81795965fbd..476623793d7 100644
--- a/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLineArchive.Table.al
+++ b/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLineArchive.Table.al
@@ -199,6 +199,9 @@ table 8064 "Billing Line Archive"
key(SK1; "Subscription Contract No.", "Subscription Contract Line No.", "Billing from")
{
}
+ key(SK2; "Subscription Line Entry No.")
+ {
+ }
}
fieldgroups
diff --git a/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al b/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al
index 6973af99a6e..ed768f06751 100644
--- a/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al
+++ b/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al
@@ -56,6 +56,7 @@ table 8059 "Subscription Line"
trigger OnValidate()
begin
+ CheckSubscriptionLineStartDateChangeAllowed();
DateFormulaManagement.ErrorIfDateEmpty("Subscription Line Start Date", FieldCaption("Subscription Line Start Date"));
UpdateNextBillingDate("Subscription Line Start Date" - 1);
CheckServiceDates();
@@ -1022,11 +1023,7 @@ table 8059 "Subscription Line"
FieldNo("Invoicing Item No."):
Validate("Invoicing Item No.", "Invoicing Item No.");
FieldNo("Subscription Line Start Date"):
- begin
- Rec.ErrorIfBillingLineArchiveForServiceCommitmentExist();
- Rec.ErrorIfBillingLineForServiceCommitmentExist();
- Validate("Subscription Line Start Date", "Subscription Line Start Date");
- end;
+ Validate("Subscription Line Start Date", "Subscription Line Start Date");
FieldNo("Subscription Line End Date"):
Validate("Subscription Line End Date", "Subscription Line End Date");
FieldNo(Quantity):
@@ -1475,11 +1472,42 @@ table 8059 "Subscription Line"
end;
internal procedure ErrorIfBillingLineForServiceCommitmentExist()
+ var
+ BillingLine: Record "Billing Line";
begin
- if BillingLineExists() then
+ BillingLine.FilterBillingLineOnServiceCommitment(Rec."Entry No.");
+ if not BillingLine.IsEmpty() then
Error(BillingLineForServiceCommitmentExistErr);
end;
+ ///
+ /// Checks whether the Subscription Line Start Date may still be changed.
+ /// The change is allowed as long as no billing has taken place for the Subscription Line, or as long as the
+ /// Next Billing Date is still on the Subscription Line Start Date, which is the state after a cancellation
+ /// or a credit memo and the case in which the billing period legitimately has to be corrected.
+ ///
+ local procedure CheckSubscriptionLineStartDateChangeAllowed()
+ begin
+ CheckBillingStateAllowsSubscriptionLineStartDateChange();
+ OnAfterCheckSubscriptionLineStartDateChangeAllowed(Rec);
+ end;
+
+ local procedure CheckBillingStateAllowsSubscriptionLineStartDateChange()
+ var
+ SubscriptionLine: Record "Subscription Line";
+ begin
+ if Rec.IsTemporary() then
+ exit;
+ SubscriptionLine.SetLoadFields("Subscription Line Start Date", "Next Billing Date");
+ if not SubscriptionLine.Get(Rec."Entry No.") then
+ exit;
+ if SubscriptionLine."Next Billing Date" = SubscriptionLine."Subscription Line Start Date" then
+ exit;
+
+ ErrorIfBillingLineArchiveForServiceCommitmentExist();
+ ErrorIfBillingLineForServiceCommitmentExist();
+ end;
+
internal procedure GetPartnerNoFromContract(): Code[20]
var
CustomerContract: Record "Customer Subscription Contract";
@@ -1504,8 +1532,7 @@ table 8059 "Subscription Line"
BillingLineArchive: Record "Billing Line Archive";
begin
BillingLineArchive.FilterBillingLineArchiveOnServiceCommitment(Rec."Entry No.");
- BillingLineArchive.CalcSums(Amount);
- if BillingLineArchive.Amount <> 0 then
+ if not BillingLineArchive.IsEmpty() then
Error(BillingLineArchiveForServiceCommitmentExistErr);
end;
@@ -2111,6 +2138,16 @@ table 8059 "Subscription Line"
begin
end;
+ ///
+ /// Raised after the Subscription Line Start Date change has passed the billing state check.
+ /// Subscribers can only tighten the rule, by raising an error of their own.
+ ///
+ /// The Subscription Line carrying the new Subscription Line Start Date.
+ [IntegrationEvent(false, false)]
+ local procedure OnAfterCheckSubscriptionLineStartDateChangeAllowed(SubscriptionLine: Record "Subscription Line")
+ begin
+ end;
+
[IntegrationEvent(false, false)]
local procedure OnAfterCalculateServiceAmount(var SubscriptionLine: Record "Subscription Line"; CalledByFieldNo: Integer)
begin
diff --git a/src/Apps/W1/Subscription Billing/Test/Service Commitments/ServiceCommitmentTest.Codeunit.al b/src/Apps/W1/Subscription Billing/Test/Service Commitments/ServiceCommitmentTest.Codeunit.al
index 585a7cbcd77..3653fbb4f4b 100644
--- a/src/Apps/W1/Subscription Billing/Test/Service Commitments/ServiceCommitmentTest.Codeunit.al
+++ b/src/Apps/W1/Subscription Billing/Test/Service Commitments/ServiceCommitmentTest.Codeunit.al
@@ -29,6 +29,8 @@ codeunit 148156 "Service Commitment Test"
DiscountCanBeInvoicedViaContractErr: Label 'Recurring discounts can only be granted for Invoicing via Contract.', Locked = true;
DiscountCannotBeAssignedErr: Label 'Subscription Package Lines, which are discounts, can only be assigned to Subscription Items.', Locked = true;
RecurringDiscountCannotBeGrantedErr: Label 'Recurring discounts cannot be granted in conjunction with Usage Based Billing', Locked = true;
+ BillingLineForServiceCommitmentExistErr: Label 'The contract line is in the current billing. Delete the billing line to be able to adjust the Subscription Line start date.', Locked = true;
+ BillingLineArchiveForServiceCommitmentExistErr: Label 'The contract line has already been billed. The Subscription Line start date can no longer be changed.', Locked = true;
#region Tests
@@ -597,6 +599,139 @@ codeunit 148156 "Service Commitment Test"
Assert.ExpectedError(RecurringDiscountCannotBeGrantedErr);
end;
+ [Test]
+ [HandlerFunctions('CreateCustomerBillingDocsPageHandler,ExchangeRateSelectionModalPageHandler,MessageHandler')]
+ procedure PreventStartDateChangeOnSubscriptionLinesPageAfterBilling()
+ var
+ BillingLine: Record "Billing Line";
+ BillingTemplate: Record "Billing Template";
+ SalesHeader: Record "Sales Header";
+ SubscriptionLine: Record "Subscription Line";
+ ServiceCommitmentsPage: TestPage "Service Commitments";
+ begin
+ // [SCENARIO] The Subscription Line Start Date can no longer be changed on the Subscription Lines page after the Subscription Line has been billed
+
+ // [GIVEN] A Customer Subscription Contract with a Subscription Line for which an invoice has been posted
+ Initialize();
+ BillContractAndPostInvoice(BillingTemplate, BillingLine, SalesHeader);
+ SubscriptionLine.Get(BillingLine."Subscription Line Entry No.");
+ Assert.AreNotEqual(SubscriptionLine."Subscription Line Start Date", SubscriptionLine."Next Billing Date", 'The Subscription Line should have been billed.');
+ Commit(); // retain data after asserterror
+
+ // [WHEN] The Subscription Line Start Date is changed on the Subscription Lines page
+ ServiceCommitmentsPage.OpenEdit();
+ ServiceCommitmentsPage.GoToRecord(SubscriptionLine);
+
+ // [THEN] The change is rejected with the same error as on the contract line list
+ asserterror ServiceCommitmentsPage."Service Start Date".SetValue(GetDifferentDateAllowedByLicense(SubscriptionLine."Subscription Line Start Date"));
+ Assert.ExpectedError(BillingLineArchiveForServiceCommitmentExistErr);
+ end;
+
+ [Test]
+ procedure UT_PreventStartDateChangeWhenBilledSubscriptionLineHasZeroAmount()
+ var
+ SubscriptionLine: Record "Subscription Line";
+ begin
+ // [SCENARIO] The Subscription Line Start Date can no longer be changed for a Subscription Line which has been billed at zero value
+
+ // [GIVEN] A billed Subscription Line whose archived billing lines add up to zero
+ Initialize();
+ MockBilledSubscriptionLine(SubscriptionLine);
+ MockBillingLineArchive(SubscriptionLine."Entry No.", 0);
+
+ // [WHEN] The Subscription Line Start Date is changed
+ // [THEN] The change is rejected
+ asserterror SubscriptionLine.Validate("Subscription Line Start Date", CalcDate('<+2M>', SubscriptionLine."Subscription Line Start Date"));
+ Assert.ExpectedError(BillingLineArchiveForServiceCommitmentExistErr);
+ end;
+
+ [Test]
+ procedure UT_PreventStartDateChangeWhenSubscriptionLineIsInCurrentBilling()
+ var
+ SubscriptionLine: Record "Subscription Line";
+ begin
+ // [SCENARIO] The Subscription Line Start Date cannot be changed as long as the Subscription Line is part of the current billing
+
+ // [GIVEN] A Subscription Line with an open Billing Line
+ Initialize();
+ MockBilledSubscriptionLine(SubscriptionLine);
+ MockBillingLine(SubscriptionLine."Entry No.");
+
+ // [WHEN] The Subscription Line Start Date is changed
+ // [THEN] The change is rejected
+ asserterror SubscriptionLine.Validate("Subscription Line Start Date", CalcDate('<+2M>', SubscriptionLine."Subscription Line Start Date"));
+ Assert.ExpectedError(BillingLineForServiceCommitmentExistErr);
+ end;
+
+ [Test]
+ procedure UT_AllowStartDateChangeWhenNextBillingDateIsOnStartDate()
+ var
+ SubscriptionLine: Record "Subscription Line";
+ NewStartDate: Date;
+ begin
+ // [SCENARIO] The Subscription Line Start Date may be corrected as long as the Next Billing Date is back on the Subscription Line Start Date
+
+ // [GIVEN] A billed Subscription Line whose Next Billing Date has been reset to the Subscription Line Start Date by a credit memo
+ Initialize();
+ MockBilledSubscriptionLine(SubscriptionLine);
+ MockBillingLineArchive(SubscriptionLine."Entry No.", LibraryRandom.RandDec(100, 2));
+ SubscriptionLine."Next Billing Date" := SubscriptionLine."Subscription Line Start Date";
+ SubscriptionLine.Modify(false);
+
+ // [WHEN] The Subscription Line Start Date is changed
+ NewStartDate := CalcDate('<+2M>', SubscriptionLine."Subscription Line Start Date");
+ SubscriptionLine.Validate("Subscription Line Start Date", NewStartDate);
+
+ // [THEN] The change is accepted and the Next Billing Date follows the new Subscription Line Start Date
+ SubscriptionLine.TestField("Subscription Line Start Date", NewStartDate);
+ SubscriptionLine.TestField("Next Billing Date", NewStartDate);
+ end;
+
+ [Test]
+ procedure UT_AllowStartDateChangeWhenSubscriptionLineHasNotBeenBilled()
+ var
+ SubscriptionLine: Record "Subscription Line";
+ NewStartDate: Date;
+ begin
+ // [SCENARIO] The Subscription Line Start Date may be changed as long as no billing has been performed for the Subscription Line
+
+ // [GIVEN] A Subscription Line without any Billing Line and without any Billing Line Archive
+ Initialize();
+ MockBilledSubscriptionLine(SubscriptionLine);
+
+ // [WHEN] The Subscription Line Start Date is changed
+ NewStartDate := CalcDate('<+2M>', SubscriptionLine."Subscription Line Start Date");
+ SubscriptionLine.Validate("Subscription Line Start Date", NewStartDate);
+
+ // [THEN] The change is accepted and the Next Billing Date follows the new Subscription Line Start Date
+ SubscriptionLine.TestField("Subscription Line Start Date", NewStartDate);
+ SubscriptionLine.TestField("Next Billing Date", NewStartDate);
+ end;
+
+ [Test]
+ procedure UT_AllowStartDateChangeOnTemporarySubscriptionLine()
+ var
+ SubscriptionLine: Record "Subscription Line";
+ TempSubscriptionLine: Record "Subscription Line" temporary;
+ NewStartDate: Date;
+ begin
+ // [SCENARIO] Buffering a billed Subscription Line in a temporary record, as the contract renewal does, is not blocked by the start date check
+
+ // [GIVEN] A billed Subscription Line and a temporary Subscription Line carrying its Entry No.
+ Initialize();
+ MockBilledSubscriptionLine(SubscriptionLine);
+ MockBillingLineArchive(SubscriptionLine."Entry No.", LibraryRandom.RandDec(100, 2));
+ TempSubscriptionLine.Init();
+ TempSubscriptionLine."Entry No." := SubscriptionLine."Entry No.";
+
+ // [WHEN] The Subscription Line Start Date is set on the temporary record
+ NewStartDate := CalcDate('<+2M>', SubscriptionLine."Subscription Line Start Date");
+ TempSubscriptionLine.Validate("Subscription Line Start Date", NewStartDate);
+
+ // [THEN] The value is accepted
+ TempSubscriptionLine.TestField("Subscription Line Start Date", NewStartDate);
+ end;
+
#endregion Tests
#region Procedures
@@ -637,6 +772,58 @@ codeunit 148156 "Service Commitment Test"
until ServiceCommitment.Next() = 0;
end;
+ local procedure BillContractAndPostInvoice(var BillingTemplate: Record "Billing Template"; var BillingLine: Record "Billing Line"; var SalesHeader: Record "Sales Header")
+ begin
+ ContractTestLibrary.CreateCustomerContractAndCreateContractLinesForItems(CustomerContract, ServiceObject, '');
+ ContractTestLibrary.DisableDeferralsForCustomerContract(CustomerContract, false);
+ ContractTestLibrary.CreateBillingProposal(BillingTemplate, Enum::"Service Partner"::Customer);
+ BillingLine.SetRange("Billing Template Code", BillingTemplate.Code);
+ BillingLine.SetRange(Partner, BillingLine.Partner::Customer);
+ Codeunit.Run(Codeunit::"Create Billing Documents", BillingLine);
+ BillingLine.FindLast();
+ SalesHeader.Get(SalesHeader."Document Type"::Invoice, BillingLine."Document No.");
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+ end;
+
+ local procedure GetDifferentDateAllowedByLicense(ReferenceDate: Date) NewDate: Date
+ begin
+ // the date filter of the demo license only allows dates in November, December, January and February
+ NewDate := DMY2Date(1, 2, Date2DMY(ReferenceDate, 3));
+ if NewDate = ReferenceDate then
+ NewDate := DMY2Date(1, 1, Date2DMY(ReferenceDate, 3));
+ end;
+
+ local procedure MockBilledSubscriptionLine(var SubscriptionLine: Record "Subscription Line")
+ begin
+ SubscriptionLine.Init();
+ SubscriptionLine."Entry No." := 0;
+ SubscriptionLine."Invoicing via" := SubscriptionLine."Invoicing via"::Contract;
+ SubscriptionLine."Subscription Line Start Date" := WorkDate();
+ SubscriptionLine."Next Billing Date" := CalcDate('<+1M>', WorkDate());
+ SubscriptionLine.Insert(false);
+ end;
+
+ local procedure MockBillingLine(SubscriptionLineEntryNo: Integer)
+ var
+ BillingLine: Record "Billing Line";
+ begin
+ BillingLine.Init();
+ BillingLine."Entry No." := 0;
+ BillingLine."Subscription Line Entry No." := SubscriptionLineEntryNo;
+ BillingLine.Insert(false);
+ end;
+
+ local procedure MockBillingLineArchive(SubscriptionLineEntryNo: Integer; ArchivedAmount: Decimal)
+ var
+ BillingLineArchive: Record "Billing Line Archive";
+ begin
+ BillingLineArchive.Init();
+ BillingLineArchive."Entry No." := 0;
+ BillingLineArchive."Subscription Line Entry No." := SubscriptionLineEntryNo;
+ BillingLineArchive.Amount := ArchivedAmount;
+ BillingLineArchive.Insert(false);
+ end;
+
local procedure MockSubscriptionLine(var SubscriptionLine: Record "Subscription Line")
begin
SubscriptionLine.Init();
@@ -681,6 +868,12 @@ codeunit 148156 "Service Commitment Test"
Reply := true;
end;
+ [ModalPageHandler]
+ procedure CreateCustomerBillingDocsPageHandler(var CreateCustomerBillingDocs: TestPage "Create Customer Billing Docs")
+ begin
+ CreateCustomerBillingDocs.OK().Invoke();
+ end;
+
[ModalPageHandler]
procedure ExchangeRateSelectionModalPageHandler(var ExchangeRateSelectionPage: TestPage "Exchange Rate Selection")
begin