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