diff --git a/src/Apps/W1/Subscription Billing/App/Base/Codeunits/SubBillingActivitiesCue.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Base/Codeunits/SubBillingActivitiesCue.Codeunit.al index 88299196f1..6895123927 100644 --- a/src/Apps/W1/Subscription Billing/App/Base/Codeunits/SubBillingActivitiesCue.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Base/Codeunits/SubBillingActivitiesCue.Codeunit.al @@ -1,8 +1,7 @@ namespace Microsoft.SubscriptionBilling; -using Microsoft.Projects.Project.Job; -using Microsoft.Purchases.History; using Microsoft.Sales.History; +using Microsoft.Purchases.History; codeunit 8071 "Sub. Billing Activities Cue" { @@ -81,19 +80,6 @@ codeunit 8071 "Sub. Billing Activities Cue" Page.Run(Page::"Overdue Service Commitments", TemporaryOverdueServiceCommitments); end; - internal procedure GetMyJobsFilter() FilterText: Text - var - MyJobs: Record "My Job"; - begin - MyJobs.SetRange("User ID", UserId); - if MyJobs.FindSet() then - repeat - if FilterText <> '' then - FilterText += '|'; - FilterText += MyJobs."Job No."; - until MyJobs.Next() = 0; - end; - local procedure RevenueCurrentMonth(): Decimal begin exit(GetRevenue(true)); diff --git a/src/Apps/W1/Subscription Billing/App/Base/Pages/SubBillingActivities.Page.al b/src/Apps/W1/Subscription Billing/App/Base/Pages/SubBillingActivities.Page.al index 06ce0458e5..506664041c 100644 --- a/src/Apps/W1/Subscription Billing/App/Base/Pages/SubBillingActivities.Page.al +++ b/src/Apps/W1/Subscription Billing/App/Base/Pages/SubBillingActivities.Page.al @@ -169,6 +169,14 @@ page 8085 "Sub. Billing Activities" ToolTip = 'Specifies the Balance between posted Contract Invoices and Contract Credit Memos for Vendor Subscription Contracts in previous Month.'; } } + cuegroup(Errors) + { + Caption = 'Errors'; + field("Errors Automated Billing"; Rec."Errors Automated Billing") + { + Image = Info; + } + } } } @@ -200,7 +208,6 @@ page 8085 "Sub. Billing Activities" trigger OnAction() begin - SetMyJobsFilter(); CurrPage.Update(); end; } @@ -236,7 +243,6 @@ page 8085 "Sub. Billing Activities" Rec.Insert(false); end; - SetMyJobsFilter(); RoleCenterNotificationMgt.ShowNotifications(); end; @@ -257,11 +263,6 @@ page 8085 "Sub. Billing Activities" CurrPage.Update(); end; - local procedure SetMyJobsFilter() - begin - Rec.SetFilter("Job No. Filter", SubBillingActivitiesCue.GetMyJobsFilter()); - end; - var SubBillingActivitiesCue: Codeunit "Sub. Billing Activities Cue"; CuesAndKpisCodeunit: Codeunit "Cues And KPIs"; diff --git a/src/Apps/W1/Subscription Billing/App/Base/Tables/SubscriptionBillingCue.Table.al b/src/Apps/W1/Subscription Billing/App/Base/Tables/SubscriptionBillingCue.Table.al index fea8b18f20..3e9132f15b 100644 --- a/src/Apps/W1/Subscription Billing/App/Base/Tables/SubscriptionBillingCue.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Base/Tables/SubscriptionBillingCue.Table.al @@ -113,12 +113,22 @@ table 8070 "Subscription Billing Cue" Editable = false; FieldClass = FlowFilter; } +#if not CLEANSCHEMA30 field(21; "Job No. Filter"; Code[20]) { Caption = 'Date Filter'; Editable = false; FieldClass = FlowFilter; - } + ObsoleteReason = 'Removed as projects are not relevant in context of Subscription Billing'; +#if not CLEAN27 + ObsoleteState = Pending; + ObsoleteTag = '28.0'; +#else + ObsoleteState = Removed; + ObsoleteTag = '31.0'; +#endif + } +#endif field(22; Overdue; Integer) { Caption = 'Overdue'; @@ -132,6 +142,14 @@ table 8070 "Subscription Billing Cue" Editable = false; FieldClass = FlowField; } + field(30; "Errors Automated Billing"; Integer) + { + CalcFormula = count("Contract Billing Err. Log"); + Caption = 'Errors Automated Contract Billing'; + ToolTip = 'Specifies the number of errors that occurred during the automated contract billing process.'; + Editable = false; + FieldClass = FlowField; + } } keys { diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/AutoContractBilling.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/AutoContractBilling.Codeunit.al new file mode 100644 index 0000000000..724fe89e98 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/AutoContractBilling.Codeunit.al @@ -0,0 +1,16 @@ +namespace Microsoft.SubscriptionBilling; + +using System.Threading; + +codeunit 8014 "Auto Contract Billing" +{ + TableNo = "Job Queue Entry"; + + trigger OnRun() + var + BillingTemplate: Record "Billing Template"; + begin + BillingTemplate.Get(Rec."Record ID to Process"); + BillingTemplate.BillContractsAutomatically(); + end; +} diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingProposal.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingProposal.Codeunit.al index a9b2b97d73..7c99bb7681 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingProposal.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingProposal.Codeunit.al @@ -4,6 +4,7 @@ using Microsoft.Finance.Currency; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Purchases.Document; using Microsoft.Sales.Document; +using System.Security.User; using System.Utilities; codeunit 8062 "Billing Proposal" @@ -17,12 +18,14 @@ codeunit 8062 "Billing Proposal" NoBillingDateErr: Label 'Please enter the Billing Date.'; BillingToChangeNotAllowedDocNoExistsErr: Label 'Billing to field is not allowed to change because an unposted invoice or credit memo exists.'; CreditMemoPreventsProposalCreationLbl: Label 'The credit memos listed here must be posted or deleted before further billing lines can be created.'; + CreditMemoExistsForSubscriptionLineTxt: Label 'There is a credit memo that needs to be posted or deleted before an invoice can be created. Subscription: %1, Subscription Line Entry No.: %2', Comment = '%1: Subscription Header No., %2: Subscription Line Entry No.'; BillingLineWithoutInvoiceExistsQst: Label 'Billing lines without invoice exists. Contract line with existing billing line will not be considered when creating an invoice from the contract. Do you want to continue?'; BillingLineWithUnpostedSalesInvoiceExistsQst: Label 'Billing line with unposted Sales Invoice exists. New invoices cannot be created until the current invoice is posted. Do you want to open the invoice?'; BillingLineWithUnpostedPurchaseInvoiceExistsQst: Label 'Billing line with unposted Purchase Invoice exists. New invoices cannot be created until the current invoice is posted. Do you want to open the invoice?'; SalesCreditMemoExistsForBillingLineQst: Label 'There is a sales credit memo that needs to be posted before an invoice can be created. Do you want to open the credit memo?'; PurchaseCreditMemoExistsForBillingLineQst: Label 'There is a purchase credit memo that needs to be posted before an invoice can be created. Do you want to open the credit memo?'; BillingLinesForAllContractLinesExistsErr: Label 'There are billing lines for all contract lines. For contract lines with billing lines, the invoice must be created in recurring billing.'; + NotAuthorizedToClearOrDeleteDocumentErr: Label 'You are not authorized to clear billing templates or delete billing documents. To perform these actions, you must be set up as an Auto Contract Billing user in the User Setup.'; procedure InitTempTable(var TempBillingLine: Record "Billing Line" temporary; GroupBy: Enum "Contract Billing Grouping") var @@ -142,6 +145,11 @@ codeunit 8062 "Billing Proposal" end; procedure CreateBillingProposal(BillingTemplateCode: Code[20]; BillingDate: Date; BillingToDate: Date) + begin + CreateBillingProposal(BillingTemplateCode, BillingDate, BillingToDate, false); + end; + + procedure CreateBillingProposal(BillingTemplateCode: Code[20]; BillingDate: Date; BillingToDate: Date; AutomatedBilling: Boolean) var BillingTemplate: Record "Billing Template"; CustomerContract: Record "Customer Subscription Contract"; @@ -169,7 +177,7 @@ codeunit 8062 "Billing Proposal" BillingRhythmFilterText := CustomerContract.GetFilter("Billing Rhythm Filter"); if CustomerContract.FindSet() then repeat - ProcessContractServiceCommitments(BillingTemplate, CustomerContract."No.", '', BillingDate, BillingToDate, BillingRhythmFilterText); + ProcessContractServiceCommitments(BillingTemplate, CustomerContract."No.", '', BillingDate, BillingToDate, BillingRhythmFilterText, AutomatedBilling); until CustomerContract.Next() = 0; end; "Service Partner"::Vendor: @@ -179,11 +187,14 @@ codeunit 8062 "Billing Proposal" BillingRhythmFilterText := VendorContract.GetFilter("Billing Rhythm Filter"); if VendorContract.FindSet() then repeat - ProcessContractServiceCommitments(BillingTemplate, VendorContract."No.", '', BillingDate, BillingToDate, BillingRhythmFilterText); + ProcessContractServiceCommitments(BillingTemplate, VendorContract."No.", '', BillingDate, BillingToDate, BillingRhythmFilterText, AutomatedBilling); until VendorContract.Next() = 0; end; end; + if AutomatedBilling then + exit; + case BillingTemplate.Partner of Enum::"Service Partner"::Customer: begin @@ -204,7 +215,7 @@ codeunit 8062 "Billing Proposal" end; end; - local procedure ProcessContractServiceCommitments(BillingTemplate: Record "Billing Template"; ContractNo: Code[20]; ContractLineFilter: Text; BillingDate: Date; BillingToDate: Date; BillingRhythmFilterText: Text) + local procedure ProcessContractServiceCommitments(BillingTemplate: Record "Billing Template"; ContractNo: Code[20]; ContractLineFilter: Text; BillingDate: Date; BillingToDate: Date; BillingRhythmFilterText: Text; AutomatedBilling: Boolean) var ServiceCommitment: Record "Subscription Line"; BillingLine: Record "Billing Line"; @@ -221,16 +232,17 @@ codeunit 8062 "Billing Proposal" OnBeforeProcessContractSubscriptionLines(ServiceCommitment, BillingDate, BillingToDate, BillingRhythmFilterText, BillingTemplate); if ServiceCommitment.FindSet() then repeat - ProcessServiceCommitment(ServiceCommitment, BillingLine, BillingTemplate, BillingDate, BillingToDate); + ProcessServiceCommitment(ServiceCommitment, BillingLine, BillingTemplate, BillingDate, BillingToDate, AutomatedBilling); until ServiceCommitment.Next() = 0; OnAfterProcessContractSubscriptionLines(ServiceCommitment, BillingDate, BillingToDate, BillingRhythmFilterText); if BillingTemplate.IsPartnerCustomer() then RecalculateHarmonizedBillingFieldsBasedOnNextBillingDate(BillingLine, ContractNo); end; - local procedure ProcessServiceCommitment(var ServiceCommitment: Record "Subscription Line"; var BillingLine: Record "Billing Line"; BillingTemplate: Record "Billing Template"; BillingDate: Date; BillingToDate: Date) + local procedure ProcessServiceCommitment(var ServiceCommitment: Record "Subscription Line"; var BillingLine: Record "Billing Line"; BillingTemplate: Record "Billing Template"; BillingDate: Date; BillingToDate: Date; AutomatedBilling: Boolean) var UsageDataBilling: Record "Usage Data Billing"; + ContractBillingErrLog: Record "Contract Billing Err. Log"; SkipServiceCommitment: Boolean; BillingPeriodStart: Date; BillingPeriodEnd: Date; @@ -246,10 +258,22 @@ codeunit 8062 "Billing Proposal" case BillingLine.Partner of Enum::"Service Partner"::Customer: if SalesHeaderGlobal.Get(SalesHeaderGlobal."Document Type"::"Credit Memo", BillingLine."Document No.") then - SalesHeaderGlobal.Mark(true); + if AutomatedBilling then + ContractBillingErrLog.InsertLogFromSubscriptionLine( + BillingTemplate.Code, + ServiceCommitment, + CopyStr(StrSubstNo(CreditMemoExistsForSubscriptionLineTxt, ServiceCommitment."Subscription Header No.", ServiceCommitment."Entry No."), 1, 250)) + else + SalesHeaderGlobal.Mark(true); Enum::"Service Partner"::Vendor: if PurchaseHeaderGlobal.Get(PurchaseHeaderGlobal."Document Type"::"Credit Memo", BillingLine."Document No.") then - PurchaseHeaderGlobal.Mark(true); + if AutomatedBilling then + ContractBillingErrLog.InsertLogFromSubscriptionLine( + BillingTemplate.Code, + ServiceCommitment, + CopyStr(StrSubstNo(CreditMemoExistsForSubscriptionLineTxt, ServiceCommitment."Subscription Header No.", ServiceCommitment."Entry No."), 1, 250)) + else + PurchaseHeaderGlobal.Mark(true); end; end; ServiceCommitment."Usage Based Billing": @@ -509,38 +533,49 @@ codeunit 8062 "Billing Proposal" OnAfterCalculateNextBillingToDateForSubscriptionLine(NextBillingToDate, ServiceCommitment, BillingFromDate); end; + procedure DisplayErrorIfNotAuthorizedToClearProposalOrDeleteDocuments() + var + BillingTemplate: Record "Billing Template"; + UserSetup: Record "User Setup"; + begin + BillingTemplate.SetFilter(Automation, '<>%1', BillingTemplate.Automation::None); + if BillingTemplate.IsEmpty() then + exit; + if not UserSetup.AutoContractBillingAllowed() then + Error(NotAuthorizedToClearOrDeleteDocumentErr); + end; + internal procedure DeleteBillingProposal(BillingTemplateCode: Code[20]) var BillingLine: Record "Billing Line"; BillingTemplate: Record "Billing Template"; ClearBillingProposalOptionsTxt: Label 'All billing proposals, Only current billing template proposal'; + ClearBillingProposalOptionsMySuggestionsOnlyTxt: Label 'All billing proposals (user %1 only), Only current billing template proposal', Comment = '%1: User ID'; ClearBillingProposalQst: Label 'Which billing proposal(s) should be deleted?'; StrMenuResponse: Integer; begin - StrMenuResponse := Dialog.StrMenu(ClearBillingProposalOptionsTxt, 1, ClearBillingProposalQst); + DisplayErrorIfNotAuthorizedToClearProposalOrDeleteDocuments(); BillingTemplate.Get(BillingTemplateCode); + if BillingTemplate."My Suggestions Only" then + StrMenuResponse := Dialog.StrMenu(StrSubstNo(ClearBillingProposalOptionsMySuggestionsOnlyTxt, UserId()), 1, ClearBillingProposalQst) + else + StrMenuResponse := Dialog.StrMenu(ClearBillingProposalOptionsTxt, 1, ClearBillingProposalQst); + BillingLine.SetCurrentKey("Subscription Header No.", "Subscription Line Entry No.", "Billing to"); + BillingLine.SetAscending("Billing to", false); case StrMenuResponse of 0: Error(''); 1: begin - BillingLine.SetCurrentKey("Subscription Header No.", "Subscription Line Entry No.", "Billing to"); - BillingLine.SetAscending("Billing to", false); BillingLine.SetRange(Partner, BillingTemplate.Partner); - if BillingLine.FindSet() then - repeat - BillingLine.Delete(true); - until BillingLine.Next() = 0; + if BillingTemplate."My Suggestions Only" then + BillingLine.SetRange("User ID", UserId()); + BillingLine.DeleteAll(true); end; 2: begin - BillingLine.SetCurrentKey("Subscription Header No.", "Subscription Line Entry No.", "Billing to"); - BillingLine.SetAscending("Billing to", false); BillingLine.SetRange("Billing Template Code", BillingTemplate.Code); - if BillingLine.FindSet() then - repeat - BillingLine.Delete(true); - until BillingLine.Next() = 0; + BillingLine.DeleteAll(true); end; end; end; @@ -663,7 +698,7 @@ codeunit 8062 "Billing Proposal" TempBillingTemplate: Record "Billing Template" temporary; begin CreateTempBillingTemplate(TempBillingTemplate, ServicePartner); - ProcessContractServiceCommitments(TempBillingTemplate, ContractNo, ContractLineFilter, BillingDate, BillingToDate, BillingRhythmFilter); + ProcessContractServiceCommitments(TempBillingTemplate, ContractNo, ContractLineFilter, BillingDate, BillingToDate, BillingRhythmFilter, false); end; internal procedure CreateBillingProposalForPurchaseHeader(ServicePartner: Enum "Service Partner"; var TempServiceCommitment: Record "Subscription Line" temporary; BillingDate: Date; BillingToDate: Date) @@ -682,7 +717,7 @@ codeunit 8062 "Billing Proposal" CreateTempBillingTemplate(TempBillingTemplate, ServicePartner); if TempServiceCommitment.FindSet() then repeat - ProcessServiceCommitment(TempServiceCommitment, BillingLine, TempBillingTemplate, BillingDate, BillingToDate); + ProcessServiceCommitment(TempServiceCommitment, BillingLine, TempBillingTemplate, BillingDate, BillingToDate, false); if PurchaseLine."Document No." <> '' then SyncPurchaseLineAndBillingLine(BillingLine, PurchaseLine); if ServiceCommitment.Get(TempServiceCommitment."Entry No.") then begin @@ -848,62 +883,61 @@ codeunit 8062 "Billing Proposal" exit(not BillingLine.IsEmpty()); end; - internal procedure DeleteBillingDocuments() + internal procedure DeleteBillingDocuments(BillingTemplateCode: Code[20]) var + BillingLine: Record "Billing Line"; + BillingTemplate: Record "Billing Template"; DeleteBillingDocumentQst: Label 'Which contract billing documents should be deleted?'; - DeleteBillingDocumentOptionsTxt: Label 'All Documents,All Sales Invoices,All Sales Credit Memos,All Purchase Invoices,All Purchase Credit Memos'; - begin - DeleteBillingDocuments(Dialog.StrMenu(DeleteBillingDocumentOptionsTxt, 1, DeleteBillingDocumentQst), true); - end; - - internal procedure DeleteBillingDocuments(Selection: Option " ","All Documents","All Sales Invoices","All Sales Credit Memos","All Purchase Invoices","All Purchase Credit Memos"; ShowDialog: Boolean) - var - Window: Dialog; - ProgressTxt: Label 'Deleting Billing Documents ...'; - begin - if Selection = Selection::" " then - exit; - if ShowDialog and GuiAllowed() then - Window.Open(ProgressTxt); - DeleteSalesBillingDocuments( - Selection in [Selection::"All Documents", Selection::"All Sales Invoices"], - Selection in [Selection::"All Documents", Selection::"All Sales Credit Memos"]); - DeletePurchaseBillingDocuments( - Selection in [Selection::"All Documents", Selection::"All Purchase Invoices"], - Selection in [Selection::"All Documents", Selection::"All Purchase Credit Memos"]); - if ShowDialog and GuiAllowed() then - Window.Close(); - end; - - local procedure DeleteSalesBillingDocuments(DeleteSalesInvoices: Boolean; DeleteSalesCreditMemos: Boolean) + DeleteBillingDocumentOptionsTxt: Label 'All Documents,Documents from current billing template only'; + DeleteBillingDocumentOptionsMySuggestionsOnlyTxt: Label 'All Documents (user %1 only),Documents from current billing template only', Comment = '%1: User ID'; + StrMenuResponse: Integer; begin - SalesHeaderGlobal.Reset(); - SalesHeaderGlobal.SetRange("Recurring Billing", true); - if DeleteSalesCreditMemos then begin - SalesHeaderGlobal.SetRange("Document Type", SalesHeaderGlobal."Document Type"::"Credit Memo"); - if not SalesHeaderGlobal.IsEmpty() then - SalesHeaderGlobal.DeleteAll(true); - end; - if DeleteSalesInvoices then begin - SalesHeaderGlobal.SetRange("Document Type", SalesHeaderGlobal."Document Type"::Invoice); - if not SalesHeaderGlobal.IsEmpty() then - SalesHeaderGlobal.DeleteAll(true); + DisplayErrorIfNotAuthorizedToClearProposalOrDeleteDocuments(); + BillingTemplate.Get(BillingTemplateCode); + if BillingTemplate."My Suggestions Only" then + StrMenuResponse := Dialog.StrMenu(StrSubstNo(DeleteBillingDocumentOptionsMySuggestionsOnlyTxt, UserId()), 1, DeleteBillingDocumentQst) + else + StrMenuResponse := Dialog.StrMenu(DeleteBillingDocumentOptionsTxt, 1, DeleteBillingDocumentQst); + BillingLine.SetLoadFields("Subscription Header No.", "Subscription Line Entry No.", "Billing to", "Document Type", "Document No.", "Partner", "User ID"); + BillingLine.SetCurrentKey("Subscription Header No.", "Subscription Line Entry No.", "Billing to"); + BillingLine.SetAscending("Billing to", false); + BillingLine.SetFilter("Document No.", '<>%1', ''); + case StrMenuResponse of + 0: + Error(''); + 1: + begin + BillingLine.SetRange(Partner, BillingTemplate.Partner); + if BillingTemplate."My Suggestions Only" then + BillingLine.SetRange("User ID", UserId()); + if BillingLine.FindSet() then + repeat + DeleteBillingDocuments(BillingLine); + until BillingLine.Next() = 0; + end; + 2: + begin + BillingLine.SetRange("Billing Template Code", BillingTemplate.Code); + if BillingLine.FindSet() then + repeat + DeleteBillingDocuments(BillingLine); + until BillingLine.Next() = 0; + end; end; end; - local procedure DeletePurchaseBillingDocuments(DeletePurchaseInvoices: Boolean; DeletePurchaseCreditMemos: Boolean) + local procedure DeleteBillingDocuments(BillingLine: Record "Billing Line") + var + SalesHeader: Record "Sales Header"; + PurchaseHeader: Record "Purchase Header"; begin - PurchaseHeaderGlobal.Reset(); - PurchaseHeaderGlobal.SetRange("Recurring Billing", true); - if DeletePurchaseCreditMemos then begin - PurchaseHeaderGlobal.SetRange("Document Type", PurchaseHeaderGlobal."Document Type"::"Credit Memo"); - if not PurchaseHeaderGlobal.IsEmpty() then - PurchaseHeaderGlobal.DeleteAll(true); - end; - if DeletePurchaseInvoices then begin - PurchaseHeaderGlobal.SetRange("Document Type", PurchaseHeaderGlobal."Document Type"::Invoice); - if not PurchaseHeaderGlobal.IsEmpty() then - PurchaseHeaderGlobal.DeleteAll(true); + case BillingLine.Partner of + BillingLine.Partner::Customer: + if SalesHeader.Get(BillingLine.GetSalesDocumentTypeFromBillingDocumentType(), BillingLine."Document No.") then + SalesHeader.Delete(true); + BillingLine.Partner::Vendor: + if PurchaseHeader.Get(BillingLine.GetPurchaseDocumentTypeFromBillingDocumentType(), BillingLine."Document No.") then + PurchaseHeader.Delete(true); end; end; diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/CreateBillingDocuments.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/CreateBillingDocuments.Codeunit.al index 3c3718207b..f969ff2b7c 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/CreateBillingDocuments.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/CreateBillingDocuments.Codeunit.al @@ -14,26 +14,19 @@ codeunit 8060 "Create Billing Documents" trigger OnRun() var BillingLine: Record "Billing Line"; - PartnerFilter: Text; - ShowNotification: Boolean; begin BillingLine.Copy(Rec); - PartnerFilter := BillingLine.GetFilter(Partner); - ShowNotification := PartnerFilter <> BillingLine.GetFilters(); - BillingLine.Reset(); - BillingLine.SetFilter(Partner, PartnerFilter); BillingLine.SetRange("Document Type", Enum::"Rec. Billing Document Type"::None); if CreateContractInvoice then BillingLine.SetRange("Billing Template Code", ''); CreateBillingDocuments(BillingLine); - if ShowNotification and (not CreateContractInvoice) then - ShowFiltersIgnoredNotification(); end; local procedure CreateBillingDocuments(var BillingLine: Record "Billing Line") begin OnBeforeCreateBillingDocuments(BillingLine); - CheckBillingLines(BillingLine); + if not CheckBillingLines(BillingLine) then + exit; if not SkipRequestPageSelection then if not RequestPageSelectionConfirmed() then @@ -41,6 +34,11 @@ codeunit 8060 "Create Billing Documents" Window.Open(ProgressTxt); Window.Update(); + if AutomatedBilling then + BillingLine.SetRange("Billing Error Log Entry No.", 0) + else + BillingLine.ModifyAll("Billing Error Log Entry No.", 0); + ProcessBillingLines(BillingLine); Window.Close(); if PostDocuments then @@ -252,8 +250,6 @@ codeunit 8060 "Create Billing Documents" ServiceObject.TestField("Variant Code"); end; SubContractsItemManagement.SetAllowInsertOfInvoicingItem(false); - if SalesLine.Type = SalesLine.Type::Item then - ErrorIfItemUnitOfMeasureCodeDoesNotExist(SalesLine."No.", ServiceObject); SalesLine.Validate("Unit of Measure Code", ServiceObject."Unit of Measure"); SalesLine.Validate(Quantity, TempBillingLine.GetSign() * ServiceObject.Quantity); SalesLine.Validate("Unit Price", SalesLine.GetSalesDocumentSign() * TempBillingLine."Unit Price"); @@ -594,6 +590,7 @@ codeunit 8060 "Create Billing Documents" SalesHeader."Posting Description" := CustomerContractLbl + ' ' + CustomerContract."No."; TranslationHelper.RestoreGlobalLanguage(); DocumentChangeManagement.SetSkipContractSalesHeaderModifyCheck(true); + SalesHeader."Auto Contract Billing" := AutomatedBilling; OnAfterCreateSalesHeaderFromContract(CustomerContract, SalesHeader); SalesHeader.Modify(false); if PostDocuments then begin @@ -744,6 +741,7 @@ codeunit 8060 "Create Billing Documents" TempBillingLine.Init(); LineNo += 1; TempBillingLine."Entry No." := LineNo; + TempBillingLine."Billing Template Code" := BillingLine."Billing Template Code"; TempBillingLine."Partner No." := PartnerNo; TempBillingLine.Partner := BillingLine.Partner; TempBillingLine."Subscription Contract No." := BillingLine."Subscription Contract No."; @@ -793,26 +791,33 @@ codeunit 8060 "Create Billing Documents" CreateVendorBillingDocs: Page "Create Vendor Billing Docs"; begin if CustomerBillingLinesFound then begin + CreateCustomerBillingDocs.SetData(DocumentDate, PostingDate, CustomerRecurringBillingGrouping, PostDocuments); if CreateCustomerBillingDocs.RunModal() = Action::OK then begin CreateCustomerBillingDocs.GetData(DocumentDate, PostingDate, CustomerRecurringBillingGrouping, PostDocuments); exit(true); end; - end - else - if VendorBillingLinesFound then + end else + if VendorBillingLinesFound then begin + CreateVendorBillingDocs.SetData(DocumentDate, PostingDate, VendorRecurringBillingGrouping); if CreateVendorBillingDocs.RunModal() = Action::OK then begin CreateVendorBillingDocs.GetData(DocumentDate, PostingDate, VendorRecurringBillingGrouping); exit(true); end; + end; end; - local procedure CheckBillingLines(var BillingLine: Record "Billing Line") + local procedure CheckBillingLines(var BillingLine: Record "Billing Line"): Boolean begin - CheckNoUpdateRequired(BillingLine); - CheckOnlyOneServicePartnerType(BillingLine); + if not CheckOnlyOneServicePartnerType(BillingLine) then + exit(false); + if not CheckNoUpdateRequired(BillingLine) then + exit(false); + CheckServiceCommitmentDataConsistency(BillingLine); + CheckItemUnitOfMeasureForInvoicingItems(BillingLine); + exit(true); end; - local procedure CheckOnlyOneServicePartnerType(var BillingLine: Record "Billing Line") + local procedure CheckOnlyOneServicePartnerType(var BillingLine: Record "Billing Line"): Boolean begin if BillingLine.FindSet() then repeat @@ -824,16 +829,182 @@ codeunit 8060 "Create Billing Documents" end; until BillingLine.Next() = 0; - if (CustomerBillingLinesFound and VendorBillingLinesFound) then - Error(OnlyOneServicePartnerErr); + if (CustomerBillingLinesFound and VendorBillingLinesFound) then begin + DisplayOrLogUnspecificError(OnlyOneServicePartnerErr); + exit(false); + end; + + exit(true); end; - local procedure CheckNoUpdateRequired(var BillingLine: Record "Billing Line") + local procedure CheckNoUpdateRequired(var BillingLine: Record "Billing Line"): Boolean begin BillingLine.SetRange("Update Required", true); - if not BillingLine.IsEmpty() then - Error(UpdateRequiredErr); + if BillingLine.FindFirst() then begin + DisplayOrLogErrorFromBillingTemplate(BillingLine."Billing Template Code", UpdateRequiredErr); + exit(false); + end; BillingLine.SetRange("Update Required"); + exit(true); + end; + + local procedure CheckServiceCommitmentDataConsistency(var BillingLine: Record "Billing Line") + var + CheckedServiceCommitments: List of [Text]; + begin + if BillingLine.FindSet() then + repeat + ValidateServiceCommitmentConsistency(BillingLine, CheckedServiceCommitments); + until BillingLine.Next() = 0; + end; + + local procedure ValidateServiceCommitmentConsistency(var BillingLine: Record "Billing Line"; var CheckedServiceCommitments: List of [Text]) + begin + if not CheckedServiceCommitments.Contains(Format(BillingLine."Subscription Line Entry No.")) then begin + CheckedServiceCommitments.Add(Format(BillingLine."Subscription Line Entry No.")); + ValidateFilteredVsTotalBillingLineCount(BillingLine); + end; + end; + + local procedure ValidateFilteredVsTotalBillingLineCount(var BillingLine: Record "Billing Line") + var + FilteredCount: Integer; + TotalCount: Integer; + begin + FilteredCount := GetFilteredBillingLineCount(BillingLine); + TotalCount := GetTotalBillingLineCount(BillingLine); + + if FilteredCount <> TotalCount then + ThrowSubscriptionLineConsistencyError(BillingLine, FilteredCount, TotalCount); + end; + + local procedure GetFilteredBillingLineCount(var BillingLine: Record "Billing Line"): Integer + var + FilteredBillingLine: Record "Billing Line"; + begin + FilteredBillingLine.CopyFilters(BillingLine); + FilteredBillingLine.FilterGroup(2); + FilteredBillingLine.SetCurrentKey("Subscription Header No.", "Subscription Line Entry No.", "Billing to"); + FilteredBillingLine.SetRange("Subscription Header No.", BillingLine."Subscription Header No."); + FilteredBillingLine.SetRange("Subscription Line Entry No.", BillingLine."Subscription Line Entry No."); + exit(FilteredBillingLine.Count()); + end; + + local procedure GetTotalBillingLineCount(var BillingLine: Record "Billing Line"): Integer + var + AllBillingLine: Record "Billing Line"; + begin + AllBillingLine.SetCurrentKey("Subscription Header No.", "Subscription Line Entry No.", "Billing to"); + AllBillingLine.SetRange("Subscription Header No.", BillingLine."Subscription Header No."); + AllBillingLine.SetRange("Subscription Line Entry No.", BillingLine."Subscription Line Entry No."); + exit(AllBillingLine.Count()); + end; + + local procedure ThrowSubscriptionLineConsistencyError(var BillingLine: Record "Billing Line"; FilteredCount: Integer; TotalCount: Integer) + var + ConsistencyErr: Label 'The number of filtered billing lines for Subscription Line %1 %2 (%3) does not match the total number of billing lines for this Subscription Line (%4). Adjust the page filters so that there are no gaps in the billing period.', Comment = '%1 = Subscription Header No., %2 = Subscription Line Entry No., %3 = Filtered Count, %4 = Total Count'; + begin + DisplayOrLogErrorFromBillingLine(BillingLine, StrSubstNo(ConsistencyErr, BillingLine."Subscription Header No.", BillingLine."Subscription Line Entry No.", FilteredCount, TotalCount)); + end; + + local procedure CheckItemUnitOfMeasureForInvoicingItems(var BillingLine: Record "Billing Line") + var + SubscriptionHeader: Record "Subscription Header"; + SubscriptionLine: Record "Subscription Line"; + CheckedServiceCommitments: List of [Text]; + InvoicingItemNo: Code[20]; + begin + if BillingLine.FindSet() then + repeat + if not CheckedServiceCommitments.Contains(Format(BillingLine."Subscription Line Entry No.")) then begin + CheckedServiceCommitments.Add(Format(BillingLine."Subscription Line Entry No.")); + SubscriptionHeader.Get(BillingLine."Subscription Header No."); + if SubscriptionHeader.Type = SubscriptionHeader.Type::Item then begin + SubscriptionLine.Get(BillingLine."Subscription Line Entry No."); + if SubscriptionLine."Invoicing Item No." = '' then + InvoicingItemNo := SubscriptionHeader."Source No." + else + InvoicingItemNo := SubscriptionLine."Invoicing Item No."; + ErrorIfItemUnitOfMeasureCodeDoesNotExist(BillingLine, InvoicingItemNo, SubscriptionHeader); + end; + end; + until BillingLine.Next() = 0; + end; + + internal procedure ErrorIfItemUnitOfMeasureCodeDoesNotExist(BillingLine: Record "Billing Line"; InvoicingItemNo: Code[20]; SubscriptionHeader: Record "Subscription Header") + var + ItemUnitOfMeasure: Record "Item Unit of Measure"; + ItemUOMDoesNotExistErr: Label 'The Unit of Measure of the Subscription (%1) contains a value (%2) that cannot be found in the Item Unit of Measure of the corresponding Invoicing Item (%3).', Comment = '%1 = Subscription No., %2 = Unit Of Measure Code, %3 = Item No.'; + begin + ItemUnitOfMeasure.SetRange("Item No.", InvoicingItemNo); + ItemUnitOfMeasure.SetRange(Code, SubscriptionHeader."Unit of Measure"); + if ItemUnitOfMeasure.IsEmpty() then + DisplayOrLogErrorFromBillingLine(BillingLine, StrSubstNo(ItemUOMDoesNotExistErr, SubscriptionHeader."No.", SubscriptionHeader."Unit of Measure", InvoicingItemNo)); + end; + + local procedure DisplayOrLogUnspecificError(ErrorText: Text) + var + ContractBillingErrLog: Record "Contract Billing Err. Log"; + ErrorTextInfo: ErrorInfo; + begin + if AutomatedBilling then + ContractBillingErrLog.InsertUnspecificLog(CopyStr(ErrorText, 1, 250)) + else begin + ErrorTextInfo.ErrorType := ErrorType::Client; + ErrorTextInfo.Message := ErrorText; + ErrorTextInfo.Verbosity := Verbosity::Error; + Error(ErrorTextInfo); + end; + end; + + local procedure DisplayOrLogErrorFromBillingTemplate(BillingTemplateCode: Code[20]; ErrorText: Text) + var + ContractBillingErrLog: Record "Contract Billing Err. Log"; + BillingTemplate: Record "Billing Template"; + ErrorTextInfo: ErrorInfo; + begin + if AutomatedBilling then + ContractBillingErrLog.InsertLogFromBillingTemplate( + BillingTemplateCode, + CopyStr(ErrorText, 1, 250)) + else begin + ErrorTextInfo.ErrorType := ErrorType::Client; + ErrorTextInfo.Message := ErrorText; + if BillingTemplate.Get(BillingTemplateCode) then begin + ErrorTextInfo.RecordId := BillingTemplate.RecordId; + ErrorTextInfo.SystemId := BillingTemplate.SystemId; + ErrorTextInfo.TableId := Database::"Billing Template"; + end; + ErrorTextInfo.Verbosity := Verbosity::Error; + Error(ErrorText); + end; + end; + + local procedure DisplayOrLogErrorFromBillingLine(BillingLine: Record "Billing Line"; ErrorText: Text) + var + ContractBillingErrLog: Record "Contract Billing Err. Log"; + SubscriptionLine: Record "Subscription Line"; + FilteredBillingLine: Record "Billing Line"; + ErrorTextInfo: ErrorInfo; + begin + if AutomatedBilling then begin + BillingLine.GetServiceCommitment(SubscriptionLine); + ContractBillingErrLog.InsertLogFromSubscriptionLine( + BillingLine."Billing Template Code", + SubscriptionLine, + CopyStr(ErrorText, 1, 250)); + FilteredBillingLine.SetRange("Subscription Header No.", BillingLine."Subscription Header No."); + FilteredBillingLine.SetRange("Subscription Line Entry No.", BillingLine."Subscription Line Entry No."); + FilteredBillingLine.ModifyAll("Billing Error Log Entry No.", ContractBillingErrLog."Entry No.", false); + end else begin + ErrorTextInfo.ErrorType := ErrorType::Client; + ErrorTextInfo.Message := ErrorText; + ErrorTextInfo.RecordId := BillingLine.RecordId; + ErrorTextInfo.SystemId := BillingLine.SystemId; + ErrorTextInfo.TableId := Database::"Billing Line"; + ErrorTextInfo.Verbosity := Verbosity::Error; + Error(ErrorText); + end; end; local procedure ProcessingFinishedMessage() @@ -920,6 +1091,13 @@ codeunit 8060 "Create Billing Documents" CreateContractInvoice := CreateContractInvoiceValue; end; + internal procedure SetAutomatedBilling(NewAutomatedBilling: Boolean) + begin + AutomatedBilling := NewAutomatedBilling; + SetHideProcessingFinishedMessage(); + SetSkipRequestPageSelection(true); + end; + procedure SetBillingGroupingPerContract(ServicePartner: Enum "Service Partner") begin if ServicePartner = "Service Partner"::Vendor then @@ -928,6 +1106,11 @@ codeunit 8060 "Create Billing Documents" CustomerRecurringBillingGrouping := "Customer Rec. Billing Grouping"::Contract; end; + procedure SetCustomerRecurringBillingGrouping(NewCustomerRecurringBillingGrouping: Enum "Customer Rec. Billing Grouping") + begin + CustomerRecurringBillingGrouping := NewCustomerRecurringBillingGrouping; + end; + procedure GetBillingPeriodDescriptionTxt() DescriptionText: Text begin DescriptionText := ServicePeriodDescriptionTxt; @@ -1040,28 +1223,6 @@ codeunit 8060 "Create Billing Documents" OnAfterIsNewHeaderNeededPerContract(CreateNewHeader, TempBillingLine, PreviousSubContractNo); end; - local procedure ShowFiltersIgnoredNotification() - var - FiltersIgnoredNotification: Notification; - FiltersIgnoredMsg: Label 'You have set filters on the Recurring Billing page. The filters were ignored to maintain data consistency.'; - begin - FiltersIgnoredNotification.Message(FiltersIgnoredMsg); - FiltersIgnoredNotification.Scope := NotificationScope::LocalScope; - FiltersIgnoredNotification.Send(); - end; - - - internal procedure ErrorIfItemUnitOfMeasureCodeDoesNotExist(ItemNo: Code[20]; ServiceObject: Record "Subscription Header") - var - ItemUnitOfMeasure: Record "Item Unit of Measure"; - ItemUOMDoesNotExistErr: Label 'The Unit of Measure of the Subscription (%1) contains a value (%2) that cannot be found in the Item Unit of Measure of the corresponding Invoicing Item (%3).', Comment = '%1 = Subscription No., %2 = Unit Of Measure Code, %3 = Item No.'; - begin - ItemUnitOfMeasure.SetRange("Item No.", ItemNo); - ItemUnitOfMeasure.SetRange(Code, ServiceObject."Unit of Measure"); - if ItemUnitOfMeasure.IsEmpty() then - Error(ItemUOMDoesNotExistErr, ServiceObject."No.", ServiceObject."Unit of Measure", ItemNo); - end; - [IntegrationEvent(false, false)] local procedure OnAfterCreateSalesHeaderFromContract(CustomerSubscriptionContract: Record "Customer Subscription Contract"; var SalesHeader: Record "Sales Header") begin @@ -1236,4 +1397,5 @@ codeunit 8060 "Create Billing Documents" CreateContractInvoice: Boolean; ServiceContractSetupFetched: Boolean; CreateOnlyPurchaseInvoiceLines: Boolean; + AutomatedBilling: Boolean; } diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al new file mode 100644 index 0000000000..03f7815997 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al @@ -0,0 +1,86 @@ +namespace Microsoft.SubscriptionBilling; + +using System.Telemetry; +using System.Threading; + +codeunit 8034 "Sub. Billing Background Jobs" +{ + procedure ScheduleAutomatedBillingJob(var BillingTemplate: Record "Billing Template") + var + JobQueueEntry: Record "Job Queue Entry"; + Telemetry: Codeunit Telemetry; + TelemetryDimensions: Dictionary of [Text, Text]; + begin + if BillingTemplate.Code = '' then + exit; + + if not IsAutomatedBillingJobScheduled(BillingTemplate."Batch Recurrent Job Id") then + CreateJobQueueEntryForAutomatedBilling(BillingTemplate, JobQueueEntry) + else + UpdateJobQueueEntryForAutomatedBilling(BillingTemplate, JobQueueEntry); + TelemetryDimensions.Add('Job Queue Id', JobQueueEntry.ID); + TelemetryDimensions.Add('Codeunit Id', Format(Codeunit::"Auto Contract Billing")); + TelemetryDimensions.Add('Record Id', Format(BillingTemplate.RecordId)); + TelemetryDimensions.Add('User Session ID', Format(JobQueueEntry."User Session ID")); + TelemetryDimensions.Add('Earliest Start Date/Time', Format(JobQueueEntry."Earliest Start Date/Time")); + Telemetry.LogMessage('0000LC5', SubBillingJobTelemetryLbl, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::All, TelemetryDimensions); + end; + + + procedure HandleAutomatedBillingJob(var BillingTemplate: Record "Billing Template") + begin + if BillingTemplate.Automation = BillingTemplate.Automation::"Create Billing Proposal and Documents" then begin + BillingTemplate.TestField("Minutes between runs"); + ScheduleAutomatedBillingJob(BillingTemplate); + end else + RemoveAutomatedBillingJob(BillingTemplate); + end; + + procedure RemoveAutomatedBillingJob(var BillingTemplate: Record "Billing Template") + var + JobQueueEntry: Record "Job Queue Entry"; + begin + if JobQueueEntry.Get(BillingTemplate."Batch Recurrent Job Id") then + JobQueueEntry.Delete(); + Clear(BillingTemplate."Batch Recurrent Job Id"); + BillingTemplate.Modify(); + end; + + local procedure IsAutomatedBillingJobScheduled(JobId: Guid): Boolean + var + JobQueueEntry: Record "Job Queue Entry"; + begin + if IsNullGuid(JobId) then + exit(false); + + exit(JobQueueEntry.Get(JobId)); + end; + + local procedure CreateJobQueueEntryForAutomatedBilling(var BillingTemplate: Record "Billing Template"; var JobQueueEntry: Record "Job Queue Entry") + begin + JobQueueEntry.ScheduleRecurrentJobQueueEntryWithFrequency(JobQueueEntry."Object Type to Run"::Codeunit, Codeunit::"Auto Contract Billing", BillingTemplate.RecordId, BillingTemplate."Minutes between runs"); + BillingTemplate."Batch Recurrent Job Id" := JobQueueEntry.ID; + BillingTemplate.Modify(); + + JobQueueEntry."Rerun Delay (sec.)" := 600; + JobQueueEntry."No. of Attempts to Run" := 0; + JobQueueEntry."Job Queue Category Code" := JobQueueCategoryTok; + JobQueueEntry.Description := BillingTemplate.Description; + JobQueueEntry.Modify(); + end; + + local procedure UpdateJobQueueEntryForAutomatedBilling(var BillingTemplate: Record "Billing Template"; var JobQueueEntry: Record "Job Queue Entry") + begin + JobQueueEntry.Get(BillingTemplate."Batch Recurrent Job Id"); + JobQueueEntry."No. of Minutes between Runs" := BillingTemplate."Minutes between runs"; + JobQueueEntry."No. of Attempts to Run" := 0; + JobQueueEntry.Description := BillingTemplate.Description; + JobQueueEntry.Modify(); + if not JobQueueEntry.IsReadyToStart() then + JobQueueEntry.Restart(); + end; + + var + JobQueueCategoryTok: Label 'SubBilling', Locked = true, Comment = 'Max Length 10'; + SubBillingJobTelemetryLbl: Label 'Subscription Billing Background Job Scheduled', Locked = true; +} diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Enums/SubBillingAutomation.Enum.al b/src/Apps/W1/Subscription Billing/App/Billing/Enums/SubBillingAutomation.Enum.al new file mode 100644 index 0000000000..f65de5db49 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Enums/SubBillingAutomation.Enum.al @@ -0,0 +1,13 @@ +namespace Microsoft.SubscriptionBilling; + +enum 8022 "Sub. Billing Automation" +{ + value(0; None) + { + Caption = ' ', Locked = true; + } + value(1; "Create Billing Proposal and Documents") + { + Caption = 'Create Billing Proposal and Documents'; + } +} diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/PostedSalesCreditMemo.PageExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/PostedSalesCreditMemo.PageExt.al index 35e00930a4..35d0997b0b 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/PostedSalesCreditMemo.PageExt.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/PostedSalesCreditMemo.PageExt.al @@ -11,8 +11,11 @@ pageextension 8069 "Posted Sales Credit Memo" extends "Posted Sales Credit Memo" field("Contract Detail Overview"; Rec."Sub. Contract Detail Overview") { ApplicationArea = Basic, Suite; - Editable = false; - ToolTip = 'Specifies whether to automatically print the billing details for this document. This is only relevant if you are using Subscription Billing functionalities.'; + } + field("Auto Contract Billing"; Rec."Auto Contract Billing") + { + ApplicationArea = Basic, Suite; + Visible = false; } } } diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/PostedSalesInvoice.PageExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/PostedSalesInvoice.PageExt.al index 2e63ddcf83..d7b69272ad 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/PostedSalesInvoice.PageExt.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/PostedSalesInvoice.PageExt.al @@ -11,8 +11,11 @@ pageextension 8068 "Posted Sales Invoice" extends "Posted Sales Invoice" field("Contract Detail Overview"; Rec."Sub. Contract Detail Overview") { ApplicationArea = Basic, Suite; - Editable = false; - ToolTip = 'Specifies whether to automatically print the billing details for this document. This is only relevant if you are using Subscription Billing functionalities.'; + } + field("Auto Contract Billing"; Rec."Auto Contract Billing") + { + ApplicationArea = Basic, Suite; + Visible = false; } } } diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/SalesCreditMemo.PageExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/SalesCreditMemo.PageExt.al index a6977bbc5a..e99e94494f 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/SalesCreditMemo.PageExt.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/SalesCreditMemo.PageExt.al @@ -12,7 +12,11 @@ pageextension 8067 "Sales Credit Memo" extends "Sales Credit Memo" { ApplicationArea = Basic, Suite; Enabled = Rec."Recurring Billing"; - ToolTip = 'Specifies whether to automatically print the billing details for this document. This is only relevant if you are using Subscription Billing functionalities.'; + } + field("Auto Contract Billing"; Rec."Auto Contract Billing") + { + ApplicationArea = Basic, Suite; + Visible = false; } } } diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/SalesInvoice.PageExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/SalesInvoice.PageExt.al index 2b60f70eee..e5c25715aa 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/SalesInvoice.PageExt.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/SalesInvoice.PageExt.al @@ -12,7 +12,11 @@ pageextension 8066 "Sales Invoice" extends "Sales Invoice" { ApplicationArea = Basic, Suite; Enabled = Rec."Recurring Billing"; - ToolTip = 'Specifies whether to automatically print the billing details for this document. This is only relevant if you are using Subscription Billing functionalities.'; + } + field("Auto Contract Billing"; Rec."Auto Contract Billing") + { + ApplicationArea = Basic, Suite; + Visible = false; } } } diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/UserSetup.PageExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/UserSetup.PageExt.al new file mode 100644 index 0000000000..a8ea49d948 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/UserSetup.PageExt.al @@ -0,0 +1,18 @@ +namespace Microsoft.SubscriptionBilling; + +using System.Security.User; + +pageextension 8015 "User Setup" extends "User Setup" +{ + layout + { + addafter("Time Sheet Admin.") + { + field("Auto Contract Billing"; Rec."Auto Contract Billing") + { + ApplicationArea = All; + Caption = 'Auto Contract Billing'; + } + } + } +} diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Pages/BillingTemplates.Page.al b/src/Apps/W1/Subscription Billing/App/Billing/Pages/BillingTemplates.Page.al index f0666ea1f3..ad552cf463 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Pages/BillingTemplates.Page.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Pages/BillingTemplates.Page.al @@ -48,6 +48,32 @@ page 8066 "Billing Templates" { ToolTip = 'Specifies the option for grouping contract billing lines.'; } + field("Customer Document per"; Rec."Customer Document per") + { + } + field("Posting Date Formula"; Rec."Posting Date Formula") + { + } + field("Document Date Formula"; Rec."Document Date Formula") + { + } + field(Automation; Rec.Automation) + { + } + field("Minutes between runs"; Rec."Minutes between runs") + { + } + field("Batch Recurrent Job Id"; Rec."Batch Recurrent Job Id") + { + Visible = false; + trigger OnAssistEdit() + begin + Rec.LookupJobEntryQueue(); + end; + } + field("Batch Rec. Job Description"; Rec."Batch Rec. Job Description") + { + } } } } @@ -79,6 +105,7 @@ page 8066 "Billing Templates" BillingTemplate := Rec; BillingTemplate.Code := NewCode; BillingTemplate.Description := CopyStr(Rec.Description, 1, MaxStrLen(Rec.Description) - StrLen(CopyTxt)) + CopyTxt; + BillingTemplate.ClearAutomationFields(); BillingTemplate.Insert(false); Rec := BillingTemplate; end; diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Pages/ContractBillingErrLog.Page.al b/src/Apps/W1/Subscription Billing/App/Billing/Pages/ContractBillingErrLog.Page.al new file mode 100644 index 0000000000..a6fb548457 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Pages/ContractBillingErrLog.Page.al @@ -0,0 +1,114 @@ +namespace Microsoft.SubscriptionBilling; + +page 8113 "Contract Billing Err. Log" +{ + Caption = 'Contract Billing Error Log'; + PageType = List; + ApplicationArea = All; + SourceTable = "Contract Billing Err. Log"; + UsageCategory = Lists; + Editable = false; + + layout + { + area(content) + { + repeater(Group) + { + field("Entry No."; Rec."Entry No.") + { + ToolTip = 'Specifies the unique entry number for the error log record.'; + } + field("Billing Template Code"; Rec."Billing Template Code") + { + ToolTip = 'Specifies the billing template code that was being processed when the error occurred.'; + } + field("Error Text"; Rec."Error Text") + { + ToolTip = 'Specifies the error message that occurred during the auto contract billing process.'; + } + field("Subscription"; Rec."Subscription") + { + ToolTip = 'Specifies the subscription number that was being processed when the error occurred.'; + } + field("Subscription Entry No."; Rec."Subscription Entry No.") + { + ToolTip = 'Specifies the subscription line entry number that was being processed when the error occurred.'; + } + field("Subscription Contract No."; Rec."Subscription Contract No.") + { + ToolTip = 'Specifies the subscription contract number that was being processed when the error occurred.'; + } + field("Contract Line No."; Rec."Contract Line No.") + { + ToolTip = 'Specifies the contract line number that was being processed when the error occurred.'; + } + field("Contract Type"; Rec."Contract Type") + { + ToolTip = 'Specifies the contract type that was being processed when the error occurred.'; + } + field("Assigned User ID"; Rec."Assigned User ID") + { + ToolTip = 'Specifies the user ID assigned to handle this error.'; + } + field("Salesperson Code"; Rec."Salesperson Code") + { + ToolTip = 'Specifies the salesperson code associated with the contract that had an error.'; + } + field(SystemCreatedAt; Rec.SystemCreatedAt) + { + Caption = 'Created On'; + ToolTip = 'Specifies when the entry was created.'; + } + field(SystemCreatedBy; Rec.SystemCreatedBy) + { + Caption = 'Created By'; + ToolTip = 'Specifies the users who has created this entry.'; + } + } + } + } + actions + { + area(processing) + { + action(Delete7days) + { + ApplicationArea = Basic, Suite; + Caption = 'Delete Entries Older Than 7 Days'; + Image = ClearLog; + ToolTip = 'Clear the list of log entries that are older than 7 days.'; + + trigger OnAction() + begin + Rec.DeleteEntries(7); + end; + } + action(Delete0days) + { + ApplicationArea = Basic, Suite; + Caption = 'Delete All Entries'; + Image = Delete; + ToolTip = 'Clear the list of all log entries.'; + + trigger OnAction() + begin + Rec.DeleteEntries(0); + end; + } + } + area(Promoted) + { + group(Category_Process) + { + Caption = 'Process'; + actionref(Delete0days_Promoted; Delete0days) + { + } + actionref(Delete7days_Promoted; Delete7days) + { + } + } + } + } +} \ No newline at end of file diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Pages/CreateCustomerBillingDocs.Page.al b/src/Apps/W1/Subscription Billing/App/Billing/Pages/CreateCustomerBillingDocs.Page.al index 259e3e2f72..d47cea70b7 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Pages/CreateCustomerBillingDocs.Page.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Pages/CreateCustomerBillingDocs.Page.al @@ -43,8 +43,10 @@ page 8072 "Create Customer Billing Docs" trigger OnOpenPage() begin - DocumentDate := WorkDate(); - PostingDate := WorkDate(); + if DocumentDate = 0D then + DocumentDate := WorkDate(); + if PostingDate = 0D then + PostingDate := WorkDate(); end; var @@ -61,4 +63,12 @@ page 8072 "Create Customer Billing Docs" NewPostDocuments := PostDocuments; end; + internal procedure SetData(NewDocumentDate: Date; NewPostingDate: Date; NewGroupingType: Enum "Customer Rec. Billing Grouping"; NewPostDocuments: Boolean) + begin + DocumentDate := NewDocumentDate; + PostingDate := NewPostingDate; + Grouping := NewGroupingType; + PostDocuments := NewPostDocuments; + end; + } \ No newline at end of file diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Pages/CreateVendorBillingDocs.Page.al b/src/Apps/W1/Subscription Billing/App/Billing/Pages/CreateVendorBillingDocs.Page.al index 905f0e808c..a71b150937 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Pages/CreateVendorBillingDocs.Page.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Pages/CreateVendorBillingDocs.Page.al @@ -39,8 +39,10 @@ page 8077 "Create Vendor Billing Docs" trigger OnOpenPage() begin - DocumentDate := WorkDate(); - PostingDate := WorkDate(); + if DocumentDate = 0D then + DocumentDate := WorkDate(); + if PostingDate = 0D then + PostingDate := WorkDate(); end; var @@ -55,4 +57,10 @@ page 8077 "Create Vendor Billing Docs" NewGroupingType := Grouping; end; + internal procedure SetData(NewDocumentDate: Date; NewPostingDate: Date; NewGroupingType: Enum "Vendor Rec. Billing Grouping") + begin + DocumentDate := NewDocumentDate; + PostingDate := NewPostingDate; + Grouping := NewGroupingType; + end; } \ No newline at end of file diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Pages/RecurringBilling.Page.al b/src/Apps/W1/Subscription Billing/App/Billing/Pages/RecurringBilling.Page.al index 1cfe3bb107..49e3fec6f2 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Pages/RecurringBilling.Page.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Pages/RecurringBilling.Page.al @@ -253,12 +253,21 @@ page 8067 "Recurring Billing" ErrorMessageMgt: Codeunit "Error Message Management"; ErrorMessageHandler: Codeunit "Error Message Handler"; ErrorContextElement: Codeunit "Error Context Element"; + CreateBillingDocuments: Codeunit "Create Billing Documents"; + DocumentDate: Date; + PostingDate: Date; IsSuccess: Boolean; begin ErrorMessageMgt.Activate(ErrorMessageHandler); ErrorMessageMgt.PushContext(ErrorContextElement, 0, 0, ''); Commit(); //commit to database before processing - IsSuccess := Codeunit.Run(Codeunit::"Create Billing Documents", Rec); + if BillingTemplate.Get(BillingTemplate.Code) then begin + BillingTemplate.CalculateDocumentDates(PostingDate, DocumentDate, false); + if BillingTemplate.Partner = BillingTemplate.Partner::Customer then + CreateBillingDocuments.SetCustomerRecurringBillingGrouping(BillingTemplate."Customer Document per"); + CreateBillingDocuments.SetDocumentDataFromRequestPage(DocumentDate, PostingDate, false, false); + end; + IsSuccess := CreateBillingDocuments.Run(Rec); if not IsSuccess then ErrorMessageHandler.ShowErrors(); InitTempTable(); @@ -299,7 +308,7 @@ page 8067 "Recurring Billing" trigger OnAction() begin - BillingProposal.DeleteBillingDocuments(); + BillingProposal.DeleteBillingDocuments(BillingTemplate.Code); InitTempTable(); end; } @@ -508,16 +517,7 @@ page 8067 "Recurring Billing" local procedure ApplyBillingTemplateFilter(var BillingTemplate2: Record "Billing Template") begin - if Format(BillingTemplate2."Billing Date Formula") <> '' then - BillingDate := CalcDate(BillingTemplate2."Billing Date Formula", WorkDate()) - else - BillingDate := WorkDate(); - - if Format(BillingTemplate2."Billing to Date Formula") <> '' then - BillingToDate := CalcDate(BillingTemplate2."Billing to Date Formula", WorkDate()) - else - BillingToDate := 0D; - + BillingTemplate2.CalculateBillingDates(BillingDate, BillingToDate, false); if BillingTemplate2."My Suggestions Only" then Rec.SetRange("User ID", UserId()) else diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/PurchCrMemoHdr.TableExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/PurchCrMemoHdr.TableExt.al index 10e9e98ab7..e5ceb1dd5d 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/PurchCrMemoHdr.TableExt.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/PurchCrMemoHdr.TableExt.al @@ -10,6 +10,7 @@ tableextension 8063 "Purch. Cr. Memo Hdr." extends "Purch. Cr. Memo Hdr." { DataClassification = CustomerContent; Caption = 'Recurring Billing'; + ToolTip = 'Specifies whether the document was created by Subscription Billing.'; } } } diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/PurchInvHeader.TableExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/PurchInvHeader.TableExt.al index 9162566d90..4d3b0e9679 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/PurchInvHeader.TableExt.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/PurchInvHeader.TableExt.al @@ -10,6 +10,7 @@ tableextension 8062 "Purch. Inv. Header" extends "Purch. Inv. Header" { DataClassification = CustomerContent; Caption = 'Recurring Billing'; + ToolTip = 'Specifies whether the document was created by Subscription Billing.'; } } } diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/SalesCrMemoHeader.TableExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/SalesCrMemoHeader.TableExt.al index 0eab2ecdb5..b7a2e6aa84 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/SalesCrMemoHeader.TableExt.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/SalesCrMemoHeader.TableExt.al @@ -10,11 +10,22 @@ tableextension 8057 "Sales Cr. Memo Header" extends "Sales Cr.Memo Header" { DataClassification = CustomerContent; Caption = 'Recurring Billing'; + ToolTip = 'Specifies whether the document was created by Subscription Billing.'; + Editable = false; } field(8052; "Sub. Contract Detail Overview"; Enum "Contract Detail Overview") { Caption = 'Subscription Contract Detail Overview'; + ToolTip = 'Specifies whether to automatically print the billing details for this document. This is only relevant if you are using Subscription Billing functionalities.'; DataClassification = CustomerContent; + Editable = false; + } + field(8053; "Auto Contract Billing"; Boolean) + { + Caption = 'Auto Contract Billing'; + ToolTip = 'Specifies whether the Document has been created by an auto billing template.'; + DataClassification = SystemMetadata; + Editable = false; } } } \ No newline at end of file diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/SalesInvoiceHeader.TableExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/SalesInvoiceHeader.TableExt.al index 4826b92130..7649a1b1cd 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/SalesInvoiceHeader.TableExt.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/SalesInvoiceHeader.TableExt.al @@ -10,11 +10,22 @@ tableextension 8055 "Sales Invoice Header" extends "Sales Invoice Header" { DataClassification = CustomerContent; Caption = 'Recurring Billing'; + ToolTip = 'Specifies whether the document was created by Subscription Billing.'; + Editable = false; } field(8052; "Sub. Contract Detail Overview"; Enum "Contract Detail Overview") { Caption = 'Subscription Contract Detail Overview'; + ToolTip = 'Specifies whether to automatically print the billing details for this document. This is only relevant if you are using Subscription Billing functionalities.'; DataClassification = CustomerContent; + Editable = false; + } + field(8053; "Auto Contract Billing"; Boolean) + { + Caption = 'Auto Contract Billing'; + ToolTip = 'Specifies whether the Document has been created by an auto billing template. This is only relevant if you are using Subscription Billing functionalities.'; + DataClassification = SystemMetadata; + Editable = false; } } } \ No newline at end of file diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/UserSetup.TableExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/UserSetup.TableExt.al new file mode 100644 index 0000000000..4c8c3cc179 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/UserSetup.TableExt.al @@ -0,0 +1,23 @@ +namespace Microsoft.SubscriptionBilling; + +using System.Security.User; + +tableextension 8011 "User Setup" extends "User Setup" +{ + fields + { + field(8000; "Auto Contract Billing"; Boolean) + { + Caption = 'Auto Contract Billing'; + ToolTip = 'Specifies, whether the user can automate contract billing. It allows to work with and edit automated Billing Templates.'; + DataClassification = SystemMetadata; + } + } + internal procedure AutoContractBillingAllowed(): Boolean + begin + if Get(UserId()) then + exit("Auto Contract Billing") + else + exit(false); + end; +} 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 c810096aca..9b1c84fcef 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 @@ -177,6 +177,11 @@ table 8061 "Billing Line" Caption = 'Billing Reference Date Changed'; ToolTip = 'Specifies whether the billing period has been adjusted manually. This is taken into account by the period calculation and may have an effect on the creation of future billing proposals.'; } + field(70; "Billing Error Log Entry No."; Integer) + { + Caption = 'Billing Error Log Entry No.'; + ToolTip = 'Specifies the entry number of the related billing error log, if any.'; + } field(100; "Billing Template Code"; Code[20]) { Caption = 'Code'; @@ -495,7 +500,7 @@ table 8061 "Billing Line" begin end; - local procedure GetServiceCommitment(var ServiceCommitment: Record "Subscription Line") + internal procedure GetServiceCommitment(var ServiceCommitment: Record "Subscription Line") begin ServiceCommitment.Get("Subscription Line Entry No."); end; diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingTemplate.Table.al b/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingTemplate.Table.al index 2366a09f6f..2c678fc4c6 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingTemplate.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingTemplate.Table.al @@ -1,5 +1,8 @@ namespace Microsoft.SubscriptionBilling; +using System.Security.User; +using System.Threading; + table 8060 "Billing Template" { DataClassification = CustomerContent; @@ -17,10 +20,22 @@ table 8060 "Billing Template" field(2; Description; Text[80]) { Caption = 'Description'; + trigger OnValidate() + begin + SubBillingBackgroundJobs.HandleAutomatedBillingJob(Rec); + end; } field(3; Partner; Enum "Service Partner") { Caption = 'Partner'; + + trigger OnValidate() + begin + if Partner = Partner::Vendor then + TestField(Automation, Automation::None); + if Partner <> xRec.Partner then + Clear(Filter); + end; } field(5; "Billing Date Formula"; DateFormula) { @@ -43,6 +58,73 @@ table 8060 "Billing Template" { Caption = 'Filter'; } + field(11; "Posting Date Formula"; DateFormula) + { + Caption = 'Posting Date Formula'; + ToolTip = 'Specifies the date formula used to calculate the Posting Date. If the field is left empty, the Posting Date is prefilled with workdate, or with today when the process runs automatically.'; + } + field(12; "Document Date Formula"; DateFormula) + { + Caption = 'Document Date Formula'; + ToolTip = 'Specifies the date formula used to calculate the Document Date. If the field is left empty, the Document Date is prefilled with workdate, or with today when the process runs automatically.'; + } + field(13; "Customer Document per"; Enum "Customer Rec. Billing Grouping") + { + Caption = 'Customer Document per'; + ToolTip = 'Specifies how the Billing lines for customers are grouped in sales documents.'; + trigger OnValidate() + begin + TestField(Partner, Partner::Customer); + end; + } + field(15; Automation; Enum "Sub. Billing Automation") + { + Caption = 'Automation'; + ToolTip = 'Specifies if the billing process is automated.'; + trigger OnValidate() + begin + if not UserSetup.AutoContractBillingAllowed() then + Error(AutoContractBillingNotAllowedErr); + case Automation of + Automation::None: + "Minutes between runs" := 0; + Automation::"Create Billing Proposal and Documents": + begin + TestField(Partner, Partner::Customer); + "My Suggestions Only" := false; + "Minutes between runs" := 60; + end; + end; + SubBillingBackgroundJobs.HandleAutomatedBillingJob(Rec); + end; + } + field(17; "Minutes between runs"; Integer) + { + Caption = 'Minutes between runs'; + ToolTip = 'Specifies the frequency, in minutes, for running the automation.'; + DataClassification = SystemMetadata; + + trigger OnValidate() + begin + TestField(Automation, Automation::"Create Billing Proposal and Documents"); + SubBillingBackgroundJobs.HandleAutomatedBillingJob(Rec); + end; + } + field(18; "Batch Recurrent Job Id"; Guid) + { + Caption = 'Batch Recurrent Job Id'; + ToolTip = 'Specifies the ID of the job queue entry that runs the billing process in the background.'; + Editable = false; + DataClassification = SystemMetadata; + } + field(19; "Batch Rec. Job Description"; Text[250]) + { + Caption = 'Batch Recurrent Job Description'; + ToolTip = 'Specifies the description of the job queue entry that runs the billing process in the background.'; + Editable = false; + FieldClass = FlowField; + CalcFormula = lookup("Job Queue Entry".Description where("ID" = field("Batch Recurrent Job Id"))); + } } keys @@ -53,6 +135,26 @@ table 8060 "Billing Template" } } + trigger OnModify() + begin + if Automation <> Automation::None then + if not UserSetup.AutoContractBillingAllowed() then + Error(AutoContractBillingNotAllowedErr); + end; + + trigger OnDelete() + begin + if Automation <> Automation::None then + if not UserSetup.AutoContractBillingAllowed() then + Error(AutoContractBillingNotAllowedErr); + + end; + + var + UserSetup: Record "User Setup"; + SubBillingBackgroundJobs: Codeunit "Sub. Billing Background Jobs"; + AutoContractBillingNotAllowedErr: Label 'You cannot change the auto billing templates because you are not set up as an Auto Contract Billing user in the User Setup.'; + internal procedure EditFilter(FieldNumber: Integer): Boolean var FilterPageBuilder: FilterPageBuilder; @@ -161,4 +263,82 @@ table 8060 "Billing Template" begin exit(Rec.Partner = Rec.Partner::Customer); end; + + internal procedure BillContractsAutomatically() + var + BillingLine: Record "Billing Line"; + CreateBillingDocuments: Codeunit "Create Billing Documents"; + BillingProposal: Codeunit "Billing Proposal"; + PostingDate: Date; + DocumentDate: Date; + BillingDate: Date; + BillingToDate: Date; + begin + CalculateBillingDates(BillingDate, BillingToDate, true); + BillingProposal.CreateBillingProposal(Code, BillingDate, BillingToDate, true); + BillingLine.Reset(); + BillingLine.SetRange("Billing Template Code", Code); + if not BillingLine.IsEmpty then begin + CalculateDocumentDates(PostingDate, DocumentDate, true); + CreateBillingDocuments.SetCustomerRecurringBillingGrouping("Customer Document per"); + CreateBillingDocuments.SetDocumentDataFromRequestPage(DocumentDate, PostingDate, false, false); + CreateBillingDocuments.SetAutomatedBilling(true); + CreateBillingDocuments.Run(BillingLine); + end; + end; + + internal procedure CalculateBillingDates(var BillingDate: Date; var BillingToDate: Date; BackgroundProcess: Boolean) + var + ReferenceDate: Date; + begin + if BackgroundProcess then + ReferenceDate := Today() + else + ReferenceDate := WorkDate(); + + if Format("Billing Date Formula") <> '' then + BillingDate := CalcDate("Billing Date Formula", ReferenceDate) + else + BillingDate := ReferenceDate; + + if Format("Billing to Date Formula") <> '' then + BillingToDate := CalcDate("Billing to Date Formula", ReferenceDate) + else + BillingToDate := 0D; + end; + + internal procedure CalculateDocumentDates(var PostingDate: Date; var DocumentDate: Date; BackgroundProcess: Boolean) + var + ReferenceDate: Date; + begin + if BackgroundProcess then + ReferenceDate := Today() + else + ReferenceDate := WorkDate(); + + if Format("Posting Date Formula") <> '' then + PostingDate := CalcDate("Posting Date Formula", ReferenceDate) + else + PostingDate := ReferenceDate; + if Format("Document Date Formula") <> '' then + DocumentDate := CalcDate("Document Date Formula", ReferenceDate) + else + DocumentDate := ReferenceDate; + end; + + internal procedure LookupJobEntryQueue() + var + JobQueueEntry: Record "Job Queue Entry"; + begin + JobQueueEntry.Get("Batch Recurrent Job Id"); + JobQueueEntry.SetRecFilter(); + Page.Run(0, JobQueueEntry); + end; + + internal procedure ClearAutomationFields() + begin + "Automation" := "Automation"::None; + Clear("Batch Recurrent Job Id"); + "Minutes between runs" := 0; + end; } \ No newline at end of file diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Tables/ContractBillingErrLog.Table.al b/src/Apps/W1/Subscription Billing/App/Billing/Tables/ContractBillingErrLog.Table.al new file mode 100644 index 0000000000..bf4111a2f5 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Tables/ContractBillingErrLog.Table.al @@ -0,0 +1,164 @@ +namespace Microsoft.SubscriptionBilling; + +using System.Security.User; +using Microsoft.CRM.Team; + +table 8022 "Contract Billing Err. Log" +{ + Caption = 'Contract Billing Error Log'; + DataClassification = CustomerContent; + DrillDownPageId = "Contract Billing Err. Log"; + LookupPageId = "Contract Billing Err. Log"; + Permissions = + tabledata "Contract Billing Err. Log" = rmid, + tabledata "Billing Line" = rm; + + fields + { + field(1; "Entry No."; Integer) + { + Caption = 'Entry No.'; + ToolTip = 'Specifies the unique entry number for the error log record.'; + AutoIncrement = true; + } + field(3; "Billing Template Code"; Code[20]) + { + Caption = 'Billing Template Code'; + ToolTip = 'Specifies the billing template code that was being processed when the error occurred.'; + TableRelation = "Billing Template".Code; + } + field(4; "Error Text"; Text[250]) + { + Caption = 'Error Text'; + ToolTip = 'Specifies the error message that occurred during the auto contract billing process.'; + } + field(5; "Subscription"; Code[20]) + { + Caption = 'Subscription'; + ToolTip = 'Specifies the subscription number that was being processed when the error occurred.'; + TableRelation = "Subscription Header"."No."; + } + field(6; "Subscription Entry No."; Integer) + { + Caption = 'Subscription Entry No.'; + ToolTip = 'Specifies the subscription line entry number that was being processed when the error occurred.'; + TableRelation = "Subscription Line"."Entry No."; + } + field(7; "Subscription Contract No."; Code[20]) + { + Caption = 'Subscription Contract No.'; + ToolTip = 'Specifies the subscription contract number that was being processed when the error occurred.'; + TableRelation = "Customer Subscription Contract"."No."; + } + field(8; "Contract Line No."; Integer) + { + Caption = 'Contract Line No.'; + ToolTip = 'Specifies the contract line number that was being processed when the error occurred.'; + TableRelation = "Cust. Sub. Contract Line"."Line No." where("Subscription Contract No." = field("Subscription Contract No.")); + } + field(9; "Contract Type"; Code[20]) + { + Caption = 'Contract Type'; + ToolTip = 'Specifies the contract type that was being processed when the error occurred.'; + TableRelation = "Subscription Contract Type".Code; + } + field(10; "Assigned User ID"; Code[50]) + { + Caption = 'Assigned User ID'; + ToolTip = 'Specifies the user ID assigned to handle this error.'; + DataClassification = EndUserIdentifiableInformation; + TableRelation = "User Setup"."User ID"; + } + field(11; "Salesperson Code"; Code[20]) + { + Caption = 'Salesperson Code'; + ToolTip = 'Specifies the salesperson code associated with the contract that had an error.'; + TableRelation = "Salesperson/Purchaser"; + } + } + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + } + + local procedure InitRecord() + begin + Rec.Init(); + Rec."Entry No." := 0; + end; + + internal procedure InsertUnspecificLog(ErrorText: Text[250]) + begin + InitRecord(); + Rec."Error Text" := ErrorText; + Rec.Insert(); + end; + + internal procedure InsertLogFromBillingTemplate(BillingTemplateCode: Code[20]; ErrorText: Text[250]) + begin + InitRecord(); + Rec."Billing Template Code" := BillingTemplateCode; + Rec."Error Text" := ErrorText; + Rec.Insert(); + end; + + internal procedure InsertLogFromSubscriptionLine(BillingTemplateCode: Code[20]; SubscriptionLine: Record "Subscription Line"; ErrorText: Text[250]) + var + CustomerSubscriptionContract: Record "Customer Subscription Contract"; + begin + InitRecord(); + Rec."Billing Template Code" := BillingTemplateCode; + Rec."Error Text" := ErrorText; + Rec."Subscription" := SubscriptionLine."Subscription Header No."; + Rec."Subscription Entry No." := SubscriptionLine."Entry No."; + Rec."Subscription Contract No." := SubscriptionLine."Subscription Contract No."; + Rec."Contract Line No." := SubscriptionLine."Subscription Contract Line No."; + case SubscriptionLine.Partner of + SubscriptionLine.Partner::Customer: + if CustomerSubscriptionContract.Get(SubscriptionLine."Subscription Contract No.") then begin + Rec."Contract Type" := CustomerSubscriptionContract."Contract Type"; + Rec."Assigned User ID" := CustomerSubscriptionContract."Assigned User ID"; + Rec."Salesperson Code" := CustomerSubscriptionContract."Salesperson Code"; + end; + end; + Rec.Insert(); + end; + + procedure DeleteEntries(DaysOld: Integer) + var + BillingLine: Record "Billing Line"; + Window: Dialog; + ConfirmDeletingAllEntriesQst: Label 'Are you sure that you want to delete all entries?'; + ConfirmDeletingEntriesQst: Label 'Are you sure that you want to delete log entries older than %1 days?', Comment = '%1 = Days Old'; + DeletingMsg: Label 'Deleting Entries...'; + DeletedMsg: Label 'Entries have been deleted.'; + begin + if DaysOld = 0 then begin + if not Confirm(ConfirmDeletingAllEntriesQst) then + exit; + end else + if not Confirm(StrSubstNo(ConfirmDeletingEntriesQst, DaysOld)) then + exit; + + Window.Open(DeletingMsg); + Rec.Reset(); + if DaysOld = 0 then begin + BillingLine.ModifyAll("Billing Error Log Entry No.", 0); + Rec.DeleteAll(); + end else begin + Rec.SetFilter(SystemCreatedAt, '<=%1', CreateDateTime(Today - DaysOld, Time)); + if Rec.FindSet() then + repeat + BillingLine.SetRange("Billing Error Log Entry No.", Rec."Entry No."); + BillingLine.ModifyAll("Billing Error Log Entry No.", 0); + until Rec.Next() = 0; + Rec.DeleteAll(); + end; + + Window.Close(); + Message(DeletedMsg); + end; +} diff --git a/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingBasic.PermissionSet.al b/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingBasic.PermissionSet.al index 56603f486b..74427f010c 100644 --- a/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingBasic.PermissionSet.al +++ b/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingBasic.PermissionSet.al @@ -50,5 +50,6 @@ permissionset 8054 "Sub. Billing Basic" tabledata "Usage Data Supplier" = R, tabledata "Vend. Sub. Contract Deferral" = R, tabledata "Vend. Sub. Contract Line" = R, - tabledata "Vendor Subscription Contract" = R; + tabledata "Vendor Subscription Contract" = R, + tabledata "Contract Billing Err. Log" = R; } diff --git a/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingObjects.PermissionSet.al b/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingObjects.PermissionSet.al index c1a9e00787..f607846052 100644 --- a/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingObjects.PermissionSet.al +++ b/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingObjects.PermissionSet.al @@ -63,6 +63,8 @@ permissionset 8001 "Sub. Billing Objects" codeunit "Vendor Deferrals Mngmt." = X, codeunit "Vendor Management" = X, codeunit TableAndFieldManagement = X, + codeunit "Auto Contract Billing" = X, + codeunit "Sub. Billing Background Jobs" = X, page "Archived Billing Lines API" = X, page "Archived Billing Lines List" = X, page "Archived Billing Lines" = X, @@ -211,5 +213,6 @@ permissionset 8001 "Sub. Billing Objects" table "Usage Data Supplier" = X, table "Vend. Sub. Contract Deferral" = X, table "Vend. Sub. Contract Line" = X, - table "Vendor Subscription Contract" = X; + table "Vendor Subscription Contract" = X, + table "Contract Billing Err. Log" = X; } \ No newline at end of file diff --git a/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingUser.PermissionSet.al b/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingUser.PermissionSet.al index 18d93d7b69..33744c1a33 100644 --- a/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingUser.PermissionSet.al +++ b/src/Apps/W1/Subscription Billing/App/Permission Sets/SubBillingUser.PermissionSet.al @@ -43,5 +43,6 @@ permissionset 8053 "Sub. Billing User" tabledata "Usage Data Supplier" = IMD, tabledata "Vend. Sub. Contract Deferral" = IMD, tabledata "Vend. Sub. Contract Line" = IMD, - tabledata "Vendor Subscription Contract" = IMD; + tabledata "Vendor Subscription Contract" = IMD, + tabledata "Contract Billing Err. Log" = IMD; } diff --git a/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/PurchaseHeader.TableExt.al b/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/PurchaseHeader.TableExt.al index 86cc176cc2..3d83960bf1 100644 --- a/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/PurchaseHeader.TableExt.al +++ b/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/PurchaseHeader.TableExt.al @@ -10,6 +10,8 @@ tableextension 8061 "Purchase Header" extends "Purchase Header" { DataClassification = CustomerContent; Caption = 'Recurring Billing'; + ToolTip = 'Specifies whether the document was created by Subscription Billing.'; + Editable = false; } } diff --git a/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/SalesHeader.TableExt.al b/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/SalesHeader.TableExt.al index 950232462d..566a4f9b77 100644 --- a/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/SalesHeader.TableExt.al +++ b/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/SalesHeader.TableExt.al @@ -10,12 +10,22 @@ tableextension 8053 "Sales Header" extends "Sales Header" { DataClassification = CustomerContent; Caption = 'Recurring Billing'; + ToolTip = 'Specifies whether the document was created by Subscription Billing.'; + Editable = false; } field(8052; "Sub. Contract Detail Overview"; Enum "Contract Detail Overview") { Caption = 'Subscription Contract Detail Overview'; + ToolTip = 'Specifies whether to automatically print the billing details for this document. This is only relevant if you are using Subscription Billing functionalities.'; DataClassification = CustomerContent; } + field(8053; "Auto Contract Billing"; Boolean) + { + Caption = 'Auto Contract Billing'; + ToolTip = 'Specifies whether the Document has been created by an auto billing template.'; + DataClassification = SystemMetadata; + Editable = false; + } } local procedure GetLastLineNo(): Integer diff --git a/src/Apps/W1/Subscription Billing/App/app.json b/src/Apps/W1/Subscription Billing/App/app.json index 7aba01ba5d..0b4fad079f 100644 --- a/src/Apps/W1/Subscription Billing/App/app.json +++ b/src/Apps/W1/Subscription Billing/App/app.json @@ -37,7 +37,7 @@ "idRanges": [ { "from": 8000, - "to": 8112 + "to": 8113 } ], "resourceExposurePolicy": { diff --git a/src/Apps/W1/Subscription Billing/Test/Billing/AutomatedBillingTest.Codeunit.al b/src/Apps/W1/Subscription Billing/Test/Billing/AutomatedBillingTest.Codeunit.al new file mode 100644 index 0000000000..2a57cda94e --- /dev/null +++ b/src/Apps/W1/Subscription Billing/Test/Billing/AutomatedBillingTest.Codeunit.al @@ -0,0 +1,205 @@ +namespace Microsoft.SubscriptionBilling; + +using Microsoft.Inventory.Item; +using Microsoft.Sales.Document; +using System.Security.User; +using System.Threading; + +codeunit 139684 "Automated Billing Test" +{ + Subtype = Test; + TestType = IntegrationTest; + TestPermissions = Disabled; + + var + ContractTestLibrary: Codeunit "Contract Test Library"; + Assert: Codeunit Assert; + LibraryRandom: Codeunit "Library - Random"; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + IsInitialized: Boolean; + + #region Test + + [Test] + procedure VerifyAutomatedBillingAuthorization() + var + UserSetup: Record "User Setup"; + BillingTemplate: Record "Billing Template"; + AutoContractBillingNotAllowedErr: Label 'You cannot change the auto billing templates because you are not set up as an Auto Contract Billing user in the User Setup.', Locked = true; + begin + // [SCENARIO] Verify that only users with Auto Contract Billing permission can set automation on billing templates + Initialize(); + + // [GIVEN] A user without Auto Contract Billing permission + InitUserSetupWithoutAuthorizationForAutomatedBilling(UserSetup); + + // [WHEN] User tries to create a billing template with automation + ContractTestLibrary.CreateDefaultRecurringBillingTemplateForServicePartner(BillingTemplate, BillingTemplate.Partner::Customer); + Commit(); // The Billing Template must be committed before validating the Automation field. + + // [THEN] Setting automation should fail + asserterror BillingTemplate.Validate(Automation, BillingTemplate.Automation::"Create Billing Proposal and Documents"); + Assert.ExpectedError(AutoContractBillingNotAllowedErr); + + // [GIVEN] A user with Auto Contract Billing permission + InitUserSetupWithAuthorizationForAutomatedBilling(UserSetup); + + // [WHEN] User tries to set automation + BillingTemplate.Validate(Automation, BillingTemplate.Automation::"Create Billing Proposal and Documents"); + BillingTemplate.Modify(true); + + // [THEN] Automation should be set successfully + BillingTemplate.TestField(Automation, BillingTemplate.Automation::"Create Billing Proposal and Documents"); + end; + + [Test] + procedure CreateJobQueueEntryForBillingTemplate() + var + JobQueueEntry: Record "Job Queue Entry"; + BillingTemplate: Record "Billing Template"; + begin + // [SCENARIO] Verify that a Job Queue Entry is created correctly for a Billing Template with automation settings + Initialize(); + + // [WHEN] Create a Billing Template with automation settings + CreateBillingTemplateWithAutomation(BillingTemplate); + + // [THEN] Verify Job Queue Entry is created with correct parameters + JobQueueEntry.Get(BillingTemplate."Batch Recurrent Job Id"); + Assert.AreEqual(JobQueueEntry."Object Type to Run"::Codeunit, JobQueueEntry."Object Type to Run", 'Object Type should be Codeunit.'); + Assert.AreEqual(Codeunit::"Auto Contract Billing", JobQueueEntry."Object ID to Run", 'Object ID should be Auto Contract Billing codeunit.'); + Assert.IsTrue(JobQueueEntry."Recurring Job", 'Job should be recurring'); + Assert.AreEqual(BillingTemplate."Minutes between runs", JobQueueEntry."No. of Minutes between Runs", 'Minutes between runs should match.'); + end; + + [Test] + [HandlerFunctions('MessageHandler,ExchangeRateSelectionModalPageHandler')] + procedure RunAutomatedProcessingForNewBillingTemplate() + var + CustomerSubscriptionContract: Record "Customer Subscription Contract"; + SubscriptionHeader: Record "Subscription Header"; + BillingTemplate: Record "Billing Template"; + BillingLine: Record "Billing Line"; + JobQueueEntry: Record "Job Queue Entry"; + SalesHeader: Record "Sales Header"; + begin + // [SCENARIO] Verify that automated billing processes contracts correctly for a Billing Template with automation settings + Initialize(); + + // [GIVEN] A Billing Template with automation settings + CreateBillingTemplateWithAutomation(BillingTemplate); + + // [WHEN] Bill the contracts automatically + ContractTestLibrary.CreateCustomerContractAndCreateContractLinesForItems(CustomerSubscriptionContract, SubscriptionHeader, ''); + BillingTemplate.BillContractsAutomatically(); + + // [THEN] Verify that Sales Header is created for the billed contract + BillingLine.SetRange("Subscription Contract No.", CustomerSubscriptionContract."No."); + BillingLine.FindSet(); + BillingLine.TestField("Document No."); + SalesHeader.Get(BillingLine.GetSalesDocumentTypeFromBillingDocumentType(), BillingLine."Document No."); + SalesHeader.TestField("Auto Contract Billing", true); + end; + + [Test] + [HandlerFunctions('MessageHandler,ExchangeRateSelectionModalPageHandler')] + procedure TestCreationOfContractBillingErrorLog() + var + CustomerSubscriptionContract: Record "Customer Subscription Contract"; + SubscriptionHeader: Record "Subscription Header"; + SubscriptionLine: Record "Subscription Line"; + BillingTemplate: Record "Billing Template"; + ContractBillingErrLog: Record "Contract Billing Err. Log"; + ItemUnitOfMeasure: Record "Item Unit of Measure"; + ItemUOMDoesNotExistErr: Label 'The Unit of Measure of the Subscription (%1) contains a value (%2) that cannot be found in the Item Unit of Measure of the corresponding Invoicing Item (%3).', Comment = '%1 = Subscription No., %2 = Unit Of Measure Code, %3 = Item No.', Locked = true; + begin + // [SCENARIO] Verify that a Contract Billing Error Log is created when an error occurs during automated billing + Initialize(); + + // [GIVEN] A Billing Template with automation settings and a contract that will produce an error + CreateBillingTemplateWithAutomation(BillingTemplate); + ContractTestLibrary.CreateCustomerContractAndCreateContractLinesForItems(CustomerSubscriptionContract, SubscriptionHeader, ''); + + // [GIVEN]Remove Item UOM to cause error during billing + SubscriptionLine.SetRange("Subscription Header No.", SubscriptionHeader."No."); + SubscriptionLine.FindLast(); + ItemUnitOfMeasure.Get(SubscriptionLine."Invoicing Item No.", SubscriptionHeader."Unit of Measure"); + ItemUnitOfMeasure.Delete(); + + // [WHEN] Bill the contracts automatically + BillingTemplate.BillContractsAutomatically(); + + // [THEN] Verify error log is created with correct details + ContractBillingErrLog.FindLast(); + Assert.AreEqual(StrSubstNo(ItemUOMDoesNotExistErr, SubscriptionHeader."No.", SubscriptionHeader."Unit of Measure", SubscriptionLine."Invoicing Item No."), ContractBillingErrLog."Error Text", 'Error message should match'); + end; + + #endregion + + #region Procedures + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Automated Billing Test"); + if IsInitialized then + exit; + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Automated Billing Test"); + + ContractTestLibrary.InitContractsApp(); + + IsInitialized := true; + Commit(); + + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Automated Billing Test"); + end; + + local procedure CreateBillingTemplateWithAutomation(var BillingTemplate: Record "Billing Template") + var + UserSetup: Record "User Setup"; + begin + ContractTestLibrary.CreateDefaultRecurringBillingTemplateForServicePartner(BillingTemplate, BillingTemplate.Partner::Customer); + InitUserSetupWithAuthorizationForAutomatedBilling(UserSetup); + BillingTemplate.Validate(Automation, BillingTemplate.Automation::"Create Billing Proposal and Documents"); + BillingTemplate.Modify(true); + end; + + local procedure InitUserSetup(var UserSetup: Record "User Setup") + begin + if not UserSetup.Get(UserId()) then begin + UserSetup.Init(); + UserSetup."User ID" := CopyStr(UserId(), 1, MaxStrLen(UserSetup."User ID")); + UserSetup.Insert(true); + end; + end; + + local procedure InitUserSetupWithAuthorizationForAutomatedBilling(var UserSetup: Record "User Setup") + begin + InitUserSetup(UserSetup); + UserSetup."Auto Contract Billing" := true; + UserSetup.Modify(true); + end; + + local procedure InitUserSetupWithoutAuthorizationForAutomatedBilling(var UserSetup: Record "User Setup") + begin + InitUserSetup(UserSetup); + UserSetup."Auto Contract Billing" := false; + UserSetup.Modify(true); + end; + + #endregion + + #region Handlers + + [ModalPageHandler] + procedure ExchangeRateSelectionModalPageHandler(var ExchangeRateSelectionPage: TestPage "Exchange Rate Selection") + begin + ExchangeRateSelectionPage.OK().Invoke(); + end; + + [MessageHandler] + procedure MessageHandler(Message: Text[1024]) + begin + end; + + #endregion +} diff --git a/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingDocsTest.Codeunit.al b/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingDocsTest.Codeunit.al index 63c6ec464b..42fd908828 100644 --- a/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingDocsTest.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingDocsTest.Codeunit.al @@ -67,6 +67,7 @@ codeunit 139687 "Recurring Billing Docs Test" LibraryVariableStorage: Codeunit "Library - Variable Storage"; IsInitialized: Boolean; NoContractLinesFoundErr: Label 'No contract lines were found that can be billed with the specified parameters.', Locked = true; + StrMenuHandlerStep: Integer; #region Tests @@ -117,40 +118,36 @@ codeunit 139687 "Recurring Billing Docs Test" end; [Test] - [HandlerFunctions('CreateCustomerBillingDocsContractPageHandler,CreateVendorBillingDocsContractPageHandler,ExchangeRateSelectionModalPageHandler,MessageHandler')] + [HandlerFunctions('CreateCustomerBillingDocsContractPageHandler,CreateVendorBillingDocsContractPageHandler,ExchangeRateSelectionModalPageHandler,MessageHandler,StrMenuHandlerDeleteDocuments')] procedure CheckBatchDeleteAllContractDocuments() begin + // [SCENARIO] multiple Sales and Purchase Contract Documents can be batch-deleted by using the function from the recurring billing page Initialize(); LibrarySetupStorage.Save(Database::"Subscription Contract Setup"); - // [SCENARIO] multiple Sales- and Purchase-Contract Documents can be batch-deleted by using the function from the recurring billing page + // [GIVEN] Two Sales Contract Documents exist + DeleteSalesContractDocuments(); InitAndCreateBillingDocument(Enum::"Service Partner"::Customer); InitAndCreateBillingDocument(Enum::"Service Partner"::Customer); - InitAndCreateBillingDocument(Enum::"Service Partner"::Vendor); - InitAndCreateBillingDocument(Enum::"Service Partner"::Vendor); - BillingProposal.DeleteBillingDocuments(1, false); // Selection: 1 = "All Documents" + // [WHEN] Delete all Sales Contract Documents from Recurring Billing Page + StrMenuHandlerStep := 1; + BillingProposal.DeleteBillingDocuments(BillingTemplate.Code); - Assert.AreEqual(0, GetNumberOfContractDocumentsSales(Enum::"Sales Document Type"::Invoice), 'Failed to delete all Sales Contract Invoices'); - Assert.AreEqual(0, GetNumberOfContractDocumentsSales(Enum::"Sales Document Type"::"Credit Memo"), 'Failed to delete all Sales Contract Credit Memos'); - Assert.AreEqual(0, GetNumberOfContractDocumentsPurchase(Enum::"Purchase Document Type"::Invoice), 'Failed to delete all Purchase Contract Invoices'); - Assert.AreEqual(0, GetNumberOfContractDocumentsPurchase(Enum::"Purchase Document Type"::"Credit Memo"), 'Failed to delete all Purchase Contract Credit Memos'); - end; + // [THEN] Verify that all Sales Contract Documents have been deleted + Assert.AreEqual(0, GetNumberOfContractDocumentsSales(), 'Failed to delete all Sales Documents'); - [Test] - procedure CheckBatchDeleteSelectedContractDocuments() - begin - Initialize(); + // [GIVEN] Two Purchase Contract Documents exist + DeletePurchaseContractDocuments(); + InitAndCreateBillingDocument(Enum::"Service Partner"::Vendor); + InitAndCreateBillingDocument(Enum::"Service Partner"::Vendor); - // [SCENARIO] multiple Sales- and Purchase-Contract Invoices can be batch-deleted depending on the selected document type - // Selection: 2 = "All Sales Invoices" - CreateAndDeleteDummyContractDocuments(2, 0, 2, 2, 2); - // Selection: 3 = "All Sales Credit Memos" - CreateAndDeleteDummyContractDocuments(3, 2, 0, 2, 2); - // Selection: 4 = "All Purchase Invoices" - CreateAndDeleteDummyContractDocuments(4, 2, 2, 0, 2); - // Selection: 5 = "All Purchase Credit Memos" - CreateAndDeleteDummyContractDocuments(5, 2, 2, 2, 0); + // [WHEN] Delete all Purchase Contract Documents from Recurring Billing Page + StrMenuHandlerStep := 1; + BillingProposal.DeleteBillingDocuments(BillingTemplate.Code); + + // [THEN] Verify that all Purchase Contract Documents have been deleted + Assert.AreEqual(0, GetNumberOfContractDocumentsPurchase(), 'Failed to delete all Purchase Documents'); end; [Test] @@ -2081,6 +2078,7 @@ codeunit 139687 "Recurring Billing Docs Test" procedure UT_ExpectErrorWhenItemUnitOfMeasureDoesNotExist() var Item: Record Item; + BillingLine: Record "Billing Line"; MockServiceObject: Record "Subscription Header"; UnitOfMeasure: Record "Unit of Measure"; CreateBillingDocumentsCodeunit: Codeunit "Create Billing Documents"; @@ -2095,7 +2093,7 @@ codeunit 139687 "Recurring Billing Docs Test" MockServiceObject."Unit of Measure" := UnitOfMeasure.Code; // [THEN] Throw error if Item Unit of Measure for Invoicing Item No. does not exist - asserterror CreateBillingDocumentsCodeunit.ErrorIfItemUnitOfMeasureCodeDoesNotExist(Item."No.", MockServiceObject); + asserterror CreateBillingDocumentsCodeunit.ErrorIfItemUnitOfMeasureCodeDoesNotExist(BillingLine, Item."No.", MockServiceObject); Assert.ExpectedError(StrSubstNo(ItemUOMDoesNotExistErr, MockServiceObject."No.", MockServiceObject."Unit of Measure", Item."No.")); end; @@ -2203,26 +2201,6 @@ codeunit 139687 "Recurring Billing Docs Test" exit(BillingArchiveLine.Count()); end; - local procedure CreateAndDeleteDummyContractDocuments(Selection: Integer; NoOfSalesInvoices: Integer; NoOfSalesCrMemos: Integer; NoOfPurchaseInvoices: Integer; NoOfPurchaseCrMemos: Integer) - begin - SalesHeader.Reset(); - SalesHeader.SetFilter("Document Type", '%1|%2', SalesHeader."Document Type"::Invoice, SalesHeader."Document Type"::"Credit Memo"); - if not SalesHeader.IsEmpty() then - SalesHeader.ModifyAll("Recurring Billing", false, false); - PurchaseHeader.Reset(); - PurchaseHeader.SetFilter("Document Type", '%1|%2', PurchaseHeader."Document Type"::Invoice, PurchaseHeader."Document Type"::"Credit Memo"); - if not PurchaseHeader.IsEmpty() then - PurchaseHeader.ModifyAll("Recurring Billing", false, false); - - CreateDummyContractDocumentsSales(); - CreateDummyContractDocumentsPurchase(); - BillingProposal.DeleteBillingDocuments(Selection, false); - - Assert.AreEqual(NoOfSalesInvoices, GetNumberOfContractDocumentsSales(Enum::"Sales Document Type"::Invoice), 'Unexpected No. of Sales Invoices after batch-deletion'); - Assert.AreEqual(NoOfSalesCrMemos, GetNumberOfContractDocumentsSales(Enum::"Sales Document Type"::"Credit Memo"), 'Unexpected No. of Sales Credit Memos after batch-deletion'); - Assert.AreEqual(NoOfPurchaseInvoices, GetNumberOfContractDocumentsPurchase(Enum::"Purchase Document Type"::Invoice), 'Unexpected No. of Purchase Invoices after batch-deletion'); - Assert.AreEqual(NoOfPurchaseCrMemos, GetNumberOfContractDocumentsPurchase(Enum::"Purchase Document Type"::"Credit Memo"), 'Unexpected No. of Purchase Credit Memos after batch-deletion'); - end; local procedure CreateAndPostSimpleSalesDocument(ItemNo: Code[20]) begin @@ -2305,6 +2283,20 @@ codeunit 139687 "Recurring Billing Docs Test" end; end; + local procedure DeletePurchaseContractDocuments(): Integer + begin + PurchaseHeader.Reset(); + PurchaseHeader.SetRange("Recurring Billing", true); + PurchaseHeader.DeleteAll(); + end; + + local procedure DeleteSalesContractDocuments(): Integer + begin + SalesHeader.Reset(); + SalesHeader.SetRange("Recurring Billing", true); + SalesHeader.DeleteAll(); + end; + local procedure FilterPurchaseLineOnDocumentLine(PurchaseDocumentType: Enum "Purchase Document Type"; DocumentNo: Code[20]; LineNo: Integer) begin PurchaseLine.SetRange("Document Type", PurchaseDocumentType); @@ -2340,18 +2332,16 @@ codeunit 139687 "Recurring Billing Docs Test" exit(SalesLine.Count()); end; - local procedure GetNumberOfContractDocumentsPurchase(DocumentType: Enum "Purchase Document Type"): Integer + local procedure GetNumberOfContractDocumentsPurchase(): Integer begin PurchaseHeader.Reset(); - PurchaseHeader.SetRange("Document Type", DocumentType); PurchaseHeader.SetRange("Recurring Billing", true); exit(PurchaseHeader.Count()); end; - local procedure GetNumberOfContractDocumentsSales(DocumentType: Enum "Sales Document Type"): Integer + local procedure GetNumberOfContractDocumentsSales(): Integer begin SalesHeader.Reset(); - SalesHeader.SetRange("Document Type", DocumentType); SalesHeader.SetRange("Recurring Billing", true); exit(SalesHeader.Count()); end; @@ -2828,6 +2818,19 @@ codeunit 139687 "Recurring Billing Docs Test" begin end; + [StrMenuHandler] + procedure StrMenuHandlerDeleteDocuments(Option: Text[1024]; var Choice: Integer; Instruction: Text[1024]) + begin + case StrMenuHandlerStep of + 1: + Choice := 1; + 2: + Choice := 2; + else + Choice := 0; + end; + end; + #endregion Handlers } #pragma warning restore AA0210 \ No newline at end of file diff --git a/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingTest.Codeunit.al b/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingTest.Codeunit.al index b7e1810eb8..e584e35c38 100644 --- a/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingTest.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingTest.Codeunit.al @@ -9,6 +9,8 @@ using Microsoft.Purchases.Vendor; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; using Microsoft.Sales.History; +using System.Utilities; +using System.TestLibraries.Utilities; #pragma warning disable AA0210 codeunit 139688 "Recurring Billing Test" @@ -58,6 +60,7 @@ codeunit 139688 "Recurring Billing Test" LibrarySales: Codeunit "Library - Sales"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; LibraryUtility: Codeunit "Library - Utility"; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; BillingRhythm: DateFormula; IsInitialized: Boolean; PostedDocumentNo: Code[20]; @@ -1303,36 +1306,51 @@ codeunit 139688 "Recurring Billing Test" end; [Test] - [HandlerFunctions('CreateBillingDocsCustomerPageHandler,ExchangeRateSelectionModalPageHandler,MessageHandler,BillingTemplateModalPageHandler')] - procedure ClearCustomFilterWhenCreateDocuments() + [HandlerFunctions('ExchangeRateSelectionModalPageHandler,MessageHandler,BillingTemplateModalPageHandler,ErrorMessagesHandler')] + procedure CheckServiceCommitmentConsistencyWhenCreatingDocuments() var - EntryNo: Integer; + SubscriptionLineEntryNo: Integer; + FilteredEntryNo: Integer; + ExpectedErrorMsg: Text; + ConsistencyErr: Label 'The number of filtered billing lines for Subscription Line %1 %2 (%3) does not match the total number of billing lines for this Subscription Line (%4). Adjust the page filters so that there are no gaps in the billing period.', Comment = '%1 = Subscription Header No., %2 = Subscription Line Entry No., %3 = Filtered Count, %4 = Total Count'; begin - // [SCENARIO] When document is created and posted from DYCE Recurring billing page, make sure that filters are cleared in the background; - // [SCENARIO] otherwise, all billing lines will not be included in the document which will lead to wrong invoices and corrupted billing lines - Initialize(); + // [SCENARIO] When creating documents from DYCE Recurring billing page with filters that exclude some billing lines for the same Service Commitment, + // [SCENARIO] the system should validate that all billing lines for each Service Commitment are included and throw a consistency error if not. - RecurringBillingPageSetupForCustomer(); + Initialize(); + RecurringBillingPageSetupForCustomer(); //ExchangeRateSelectionModalPageHandler,MessageHandler,BillingTemplateModalPageHandler - // [GIVEN] Get the last billing line to exclude it from posting + // [GIVEN] Find a Service Commitment that has multiple billing lines and get one line to exclude BillingLine.Reset(); BillingLine.SetRange("Billing Template Code", BillingTemplate.Code); BillingLine.FindLast(); - EntryNo := BillingLine."Entry No."; + SubscriptionLineEntryNo := BillingLine."Subscription Line Entry No."; + + // Create an additional billing line for the same Service Commitment to ensure we have multiple lines + CreateAdditionalBillingLineForSameServiceCommitment(BillingLine); + BillingLine.SetRange("Subscription Line Entry No.", SubscriptionLineEntryNo); + BillingLine.FindLast(); + FilteredEntryNo := BillingLine."Entry No."; - // [WHEN] Filter billing lines to exclude the last line and create/post documents + // [GIVEN] Prepare expected error message + BillingLine.Reset(); + BillingLine.SetRange("Subscription Header No.", BillingLine."Subscription Header No."); + BillingLine.SetRange("Subscription Line Entry No.", SubscriptionLineEntryNo); + ExpectedErrorMsg := StrSubstNo(ConsistencyErr, + BillingLine."Subscription Header No.", SubscriptionLineEntryNo, BillingLine.Count() - 1, BillingLine.Count()); + + // [GIVEN] Store expected error message for handler verification + LibraryVariableStorage.Enqueue(ExpectedErrorMsg); + + // [WHEN] Filter billing lines to exclude one line for a Service Commitment that has multiple lines and try to create documents RecurringBillingPage.OpenEdit(); RecurringBillingPage.BillingTemplateField.Lookup(); PostDocuments := true; RecurringBillingPage.CreateBillingProposalAction.Invoke(); - RecurringBillingPage.Filter.SetFilter("Entry No.", '<>' + Format(EntryNo)); - RecurringBillingPage.CreateDocuments.Invoke(); - Commit(); + RecurringBillingPage.Filter.SetFilter("Entry No.", '<>' + Format(FilteredEntryNo)); - // [THEN] All billing lines should be cleared from the proposal after posting - BillingLine.Reset(); - BillingLine.SetRange("Billing Template Code", BillingTemplate.Code); - Assert.RecordIsEmpty(BillingLine); + // [THEN] Creating documents should fail with Service Commitment consistency error (shown in ErrorMessages page) + RecurringBillingPage.CreateDocuments.Invoke(); // ErrorMessagesHandler end; [Test] @@ -1551,6 +1569,25 @@ codeunit 139688 "Recurring Billing Test" until BillingLine.Next() = 0; end; + local procedure CreateAdditionalBillingLineForSameServiceCommitment(var OriginalBillingLine: Record "Billing Line") + var + NewBillingLine: Record "Billing Line"; + begin + NewBillingLine.InitNewBillingLine(); + NewBillingLine."Billing Template Code" := OriginalBillingLine."Billing Template Code"; + NewBillingLine."Subscription Header No." := OriginalBillingLine."Subscription Header No."; + NewBillingLine."Subscription Line Entry No." := OriginalBillingLine."Subscription Line Entry No."; + NewBillingLine."Subscription Contract No." := OriginalBillingLine."Subscription Contract No."; + NewBillingLine."Subscription Contract Line No." := OriginalBillingLine."Subscription Contract Line No."; + NewBillingLine.Partner := OriginalBillingLine.Partner; + NewBillingLine."Partner No." := OriginalBillingLine."Partner No."; + NewBillingLine."Billing from" := CalcDate('<+1D>', OriginalBillingLine."Billing to"); + NewBillingLine."Billing to" := CalcDate('<+1M>', NewBillingLine."Billing from"); + NewBillingLine.Amount := OriginalBillingLine.Amount; + NewBillingLine."Unit Price" := OriginalBillingLine."Unit Price"; + NewBillingLine.Insert(false); + end; + local procedure CreateBillingProposalForCustomerContractUsingRealTemplate() begin CreateCustomerContract('<1M>', '<12M>'); @@ -1802,6 +1839,18 @@ codeunit 139688 "Recurring Billing Test" CreateBillingDocsVendorPage.OK().Invoke(); end; + [PageHandler] + procedure ErrorMessagesHandler(var ErrorMessages: TestPage "Error Messages") + var + ExpectedErrorMsg: Text; + ActualErrorMsg: Text; + begin + ExpectedErrorMsg := LibraryVariableStorage.DequeueText(); + ActualErrorMsg := ErrorMessages.Description.Value(); + Assert.AreEqual(ExpectedErrorMsg, ActualErrorMsg, 'Error message does not match expected value'); + ErrorMessages.OK().Invoke(); + end; + [ModalPageHandler] procedure ExchangeRateSelectionModalPageHandler(var ExchangeRateSelectionPage: TestPage "Exchange Rate Selection") begin diff --git a/src/Apps/W1/Subscription Billing/Test/Deferrals/CustomerDeferralsTest.Codeunit.al b/src/Apps/W1/Subscription Billing/Test/Deferrals/CustomerDeferralsTest.Codeunit.al index 71c23176d3..e4ca028654 100644 --- a/src/Apps/W1/Subscription Billing/Test/Deferrals/CustomerDeferralsTest.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/Test/Deferrals/CustomerDeferralsTest.Codeunit.al @@ -296,6 +296,7 @@ codeunit 139912 "Customer Deferrals Test" GetGLEntryAmountFromAccountNo(GLAmountAfterInvoicing, GeneralPostingSetup."Cust. Sub. Contr. Def Account"); // Expect Amount on GL Account to be decreased by Released Customer Deferral + Commit(); // close transaction before report is called ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler GetGLEntryAmountFromAccountNo(GLAmountAfterRelease, GeneralPostingSetup."Cust. Sub. Contr. Def Account"); Assert.AreEqual(GLAmountAfterInvoicing - CustomerContractDeferral.Amount, GLAmountAfterRelease, 'Amount was not moved from Deferrals Account to Contract Account'); @@ -350,6 +351,7 @@ codeunit 139912 "Customer Deferrals Test" Assert.AreEqual(0, GLLineDiscountAmountAfterInvoicing, 'There should not be amount posted into Sales Line Discount Account.'); // Expect Amount on GL Account to be decreased by Released Customer Deferral + Commit(); // close transaction before report is called ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler GetGLEntryAmountFromAccountNo(GLAmountAfterRelease, GeneralPostingSetup."Cust. Sub. Contr. Def Account"); Assert.AreEqual(GLAmountAfterInvoicing - CustomerContractDeferral.Amount, GLAmountAfterRelease, 'Amount was not moved from Deferrals Account to Contract Account'); @@ -375,6 +377,7 @@ codeunit 139912 "Customer Deferrals Test" // Release only first Customer Subscription Contract Deferral PostSalesDocumentAndFetchDeferrals(); PostingDate := CustomerContractDeferral."Posting Date"; + Commit(); // close transaction before report is called ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler SalesInvoiceHeader.Get(PostedDocumentNo); @@ -505,6 +508,7 @@ codeunit 139912 "Customer Deferrals Test" PostSalesDocumentAndFetchDeferrals(); PostingDate := CustomerContractDeferral."Posting Date"; // Used in request page handler + Commit(); // close transaction before report is called ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler SalesInvoiceHeader.Get(PostedDocumentNo); CorrectPostedSalesInvoice.CreateCreditMemoCopyDocument(SalesInvoiceHeader, SalesCrMemoHeader); @@ -585,6 +589,7 @@ codeunit 139912 "Customer Deferrals Test" // [THEN] Releasing each deferral entry should be correct repeat PostingDate := CustomerContractDeferral."Posting Date"; + Commit(); // close transaction before report is called ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler CustomerContractDeferral.Get(CustomerContractDeferral."Entry No."); GLEntry.Get(CustomerContractDeferral."G/L Entry No."); @@ -614,6 +619,7 @@ codeunit 139912 "Customer Deferrals Test" // [THEN] Releasing each deferral entry should be correct repeat PostingDate := CustomerContractDeferral."Posting Date"; + Commit(); // close transaction before report is called ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler CustomerContractDeferral.Get(CustomerContractDeferral."Entry No."); GLEntry.Get(CustomerContractDeferral."G/L Entry No."); diff --git a/src/Apps/W1/Subscription Billing/Test/Deferrals/VendorDeferralsTest.Codeunit.al b/src/Apps/W1/Subscription Billing/Test/Deferrals/VendorDeferralsTest.Codeunit.al index 47bd658f0a..ac5df83204 100644 --- a/src/Apps/W1/Subscription Billing/Test/Deferrals/VendorDeferralsTest.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/Test/Deferrals/VendorDeferralsTest.Codeunit.al @@ -304,7 +304,8 @@ codeunit 139913 "Vendor Deferrals Test" GetGLEntryAmountFromAccountNo(GLAmountAfterInvoicing, GeneralPostingSetup."Vend. Sub. Contr. Def. Account"); // Expect Amount on GL Account to be decreased by Released Vendor Deferral - ContractDeferralsRelease.Run(); + Commit(); // close transaction before report is called + ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler GetGLEntryAmountFromAccountNo(GLAmountAfterRelease, GeneralPostingSetup."Vend. Sub. Contr. Def. Account"); Assert.AreEqual(GLAmountAfterInvoicing - VendorContractDeferral.Amount, GLAmountAfterRelease, 'Amount was not moved from Deferrals Account to Contract Account'); @@ -357,7 +358,8 @@ codeunit 139913 "Vendor Deferrals Test" Assert.AreEqual(0, GLLineDiscountAmountAfterInvoicing, 'There should not be amount posted into Purchase Line Discount Account.'); // Expect Amount on GL Account to be decreased by Released Vendor Deferral - ContractDeferralsRelease.Run(); + Commit(); // close transaction before report is called + ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler GetGLEntryAmountFromAccountNo(GLAmountAfterRelease, GeneralPostingSetup."Vend. Sub. Contr. Def. Account"); Assert.AreEqual(GLAmountAfterInvoicing - VendorContractDeferral.Amount, GLAmountAfterRelease, 'Amount was not moved from Deferrals Account to Contract Account'); @@ -381,7 +383,8 @@ codeunit 139913 "Vendor Deferrals Test" PostPurchDocumentAndFetchDeferrals(); // Release only first Vendor Subscription Contract Deferral PostingDate := VendorContractDeferral."Posting Date"; - ContractDeferralsRelease.Run(); + Commit(); // close transaction before report is called + ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler PurchaseInvoiceHeader.Get(PostedDocumentNo); PostPurchCreditMemo(); @@ -511,7 +514,8 @@ codeunit 139913 "Vendor Deferrals Test" PostPurchDocumentAndFetchDeferrals(); PostingDate := VendorContractDeferral."Posting Date"; // Used in request page handler - ContractDeferralsRelease.Run(); + Commit(); // close transaction before report is called + ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler PurchaseInvoiceHeader.Get(PostedDocumentNo); CorrectPostedPurchaseInvoice.CreateCreditMemoCopyDocument(PurchaseInvoiceHeader, PurchaseCrMemoHeader); PurchaseCrMemoHeader.Validate("Vendor Cr. Memo No.", LibraryUtility.GenerateGUID()); @@ -664,7 +668,8 @@ codeunit 139913 "Vendor Deferrals Test" // [THEN] Releasing each deferral entry should be correct repeat PostingDate := VendorContractDeferral."Posting Date"; - ContractDeferralsRelease.Run(); + Commit(); // close transaction before report is called + ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler VendorContractDeferral.Get(VendorContractDeferral."Entry No."); GLEntry.Get(VendorContractDeferral."G/L Entry No."); GLEntry.TestField("Subscription Contract No.", VendorContractDeferral."Subscription Contract No."); @@ -693,7 +698,8 @@ codeunit 139913 "Vendor Deferrals Test" // [THEN] Releasing each deferral entry should be correct repeat PostingDate := VendorContractDeferral."Posting Date"; - ContractDeferralsRelease.Run(); + Commit(); // close transaction before report is called + ContractDeferralsRelease.Run(); // ContractDeferralsReleaseRequestPageHandler VendorContractDeferral.Get(VendorContractDeferral."Entry No."); GLEntry.Get(VendorContractDeferral."G/L Entry No."); GLEntry.TestField("Subscription Contract No.", VendorContractDeferral."Subscription Contract No.");