From 70c8f764ea6ca19611d68500eb6ba0f7f930d298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Fri, 26 Sep 2025 17:41:59 +0200 Subject: [PATCH 01/10] Initial commit --- .../SubBillingActivitiesCue.Codeunit.al | 14 -- .../Base/Pages/SubBillingActivities.Page.al | 15 +- .../Tables/SubscriptionBillingCue.Table.al | 20 +- .../Codeunits/AutoContractBilling.Codeunit.al | 16 ++ .../Codeunits/BillingProposal.Codeunit.al | 196 +++++++++------ .../CreateBillingDocuments.Codeunit.al | 228 ++++++++++++++---- .../SubBillingBackgroundJobs.Codeunit.al | 76 ++++++ .../Enums/SubBillingAutomation.Enum.al | 13 + .../PostedSalesCreditMemo.PageExt.al | 7 +- .../PostedSalesInvoice.PageExt.al | 7 +- .../SalesCreditMemo.PageExt.al | 6 +- .../Page Extensions/SalesInvoice.PageExt.al | 6 +- .../Page Extensions/UserSetup.PageExt.al | 18 ++ .../Billing/Pages/BillingTemplates.Page.al | 25 ++ .../Pages/ContractBillingErrLog.Page.al | 71 ++++++ .../Pages/CreateCustomerBillingDocs.Page.al | 14 +- .../Pages/CreateVendorBillingDocs.Page.al | 12 +- .../Billing/Pages/RecurringBilling.Page.al | 22 +- .../BatchPostSalesInvoices.ReportExt.al | 36 +++ .../PurchCrMemoHdr.TableExt.al | 1 + .../PurchInvHeader.TableExt.al | 1 + .../SalesCrMemoHeader.TableExt.al | 11 + .../SalesInvoiceHeader.TableExt.al | 11 + .../Table Extensions/UserSetup.TableExt.al | 23 ++ .../App/Billing/Tables/BillingLine.Table.al | 7 +- .../Billing/Tables/BillingTemplate.Table.al | 175 ++++++++++++++ .../Tables/ContractBillingErrLog.Table.al | 125 ++++++++++ .../PurchaseHeader.TableExt.al | 2 + .../Table Extensions/SalesHeader.TableExt.al | 10 + src/Apps/W1/Subscription Billing/App/app.json | 2 +- .../Billing/AutomatedBillingTest.Codeunit.al | 203 ++++++++++++++++ .../RecurringBillingDocsTest.Codeunit.al | 99 ++++---- .../Billing/RecurringBillingTest.Codeunit.al | 83 +++++-- .../CustomerDeferralsTest.Codeunit.al | 6 + .../Deferrals/VendorDeferralsTest.Codeunit.al | 18 +- 35 files changed, 1345 insertions(+), 234 deletions(-) create mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Codeunits/AutoContractBilling.Codeunit.al create mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al create mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Enums/SubBillingAutomation.Enum.al create mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Page Extensions/UserSetup.PageExt.al create mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Pages/ContractBillingErrLog.Page.al create mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Report Extensions/BatchPostSalesInvoices.ReportExt.al create mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Table Extensions/UserSetup.TableExt.al create mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Tables/ContractBillingErrLog.Table.al create mode 100644 src/Apps/W1/Subscription Billing/Test/Billing/AutomatedBillingTest.Codeunit.al 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 815e995501..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 @@ -2,7 +2,6 @@ namespace Microsoft.SubscriptionBilling; using Microsoft.Sales.History; using Microsoft.Purchases.History; -using Microsoft.Projects.Project.Job; 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 eeafffedbe..b3e9569072 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 @@ -171,6 +171,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; + } + } } } @@ -202,7 +210,6 @@ page 8085 "Sub. Billing Activities" trigger OnAction() begin - SetMyJobsFilter(); CurrPage.Update(); end; } @@ -238,7 +245,6 @@ page 8085 "Sub. Billing Activities" Rec.Insert(false); end; - SetMyJobsFilter(); RoleCenterNotificationMgt.ShowNotifications(); end; @@ -259,11 +265,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 17a85e9998..840e0ac694 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 = '27.0'; +#else + ObsoleteState = Removed; + ObsoleteTag = '30.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 03856479ab..2fa3b9a569 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 @@ -1,10 +1,11 @@ namespace Microsoft.SubscriptionBilling; -using System.Utilities; using Microsoft.Sales.Document; using Microsoft.Purchases.Document; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.Currency; +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,10 +145,16 @@ 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"; VendorContract: Record "Vendor Subscription Contract"; + ContractBillingErrLog: Record "Contract Billing Err Log"; FilterText: Text; BillingRhythmFilterText: Text; begin @@ -169,7 +178,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,32 +188,33 @@ 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; - case BillingTemplate.Partner of - Enum::"Service Partner"::Customer: - begin - SalesHeaderGlobal.MarkedOnly(true); - if SalesHeaderGlobal.Count <> 0 then begin - Page.Run(Page::"Sales Credit Memos", SalesHeaderGlobal); - Message(CreditMemoPreventsProposalCreationLbl); + if not AutomatedBilling then + case BillingTemplate.Partner of + Enum::"Service Partner"::Customer: + begin + SalesHeaderGlobal.MarkedOnly(true); + if SalesHeaderGlobal.Count <> 0 then begin + Page.Run(Page::"Sales Credit Memos", SalesHeaderGlobal); + Message(CreditMemoPreventsProposalCreationLbl); + end; end; - end; - Enum::"Service Partner"::Vendor: - begin - PurchaseHeaderGlobal.MarkedOnly(true); - if PurchaseHeaderGlobal.Count <> 0 then begin - Page.Run(Page::"Purchase Credit Memos", PurchaseHeaderGlobal); - Message(CreditMemoPreventsProposalCreationLbl); + Enum::"Service Partner"::Vendor: + begin + PurchaseHeaderGlobal.MarkedOnly(true); + if PurchaseHeaderGlobal.Count <> 0 then begin + Page.Run(Page::"Purchase Credit Memos", PurchaseHeaderGlobal); + Message(CreditMemoPreventsProposalCreationLbl); + end; end; - end; - end; + 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 +231,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 +257,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, + StrSubstNo(CreditMemoExistsForSubscriptionLineTxt, ServiceCommitment."Subscription Header No.", ServiceCommitment."Entry No.")) + 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, + StrSubstNo(CreditMemoExistsForSubscriptionLineTxt, ServiceCommitment."Subscription Header No.", ServiceCommitment."Entry No.")) + else + PurchaseHeaderGlobal.Mark(true); end; end; ServiceCommitment."Usage Based Billing": @@ -509,38 +532,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'; 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 +697,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 +716,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,46 +882,64 @@ 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'; + DeleteBillingDocumentOptionsTxt: Label 'All Documents,Documents from current billing template only'; + DeleteBillingDocumentOptionsMySuggestionsOnlyTxt: Label 'All Documents (user %1 only),Documents from current billing template only'; + StrMenuResponse: Integer; begin - DeleteBillingDocuments(Dialog.StrMenu(DeleteBillingDocumentOptionsTxt, 1, DeleteBillingDocumentQst), 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.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 + DeleteSalesBillingDocuments(BillingLine); + until BillingLine.Next() = 0; + end; + 2: + begin + BillingLine.SetRange("Billing Template Code", BillingTemplate.Code); + if BillingLine.FindSet() then + repeat + DeleteSalesBillingDocuments(BillingLine); + until BillingLine.Next() = 0; + end; + end; end; - internal procedure DeleteBillingDocuments(Selection: Option " ","All Documents","All Sales Invoices","All Sales Credit Memos","All Purchase Invoices","All Purchase Credit Memos"; ShowDialog: Boolean) + local procedure DeleteSalesBillingDocuments(BillingLine: Record "Billing Line") 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) + SalesHeader: Record "Sales Header"; + PurchaseHeader: Record "Purchase Header"; 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); + case BillingLine.Partner of + BillingLine.Partner::Customer: + begin + if SalesHeader.Get(BillingLine.GetSalesDocumentTypeFromBillingDocumentType(), BillingLine."Document No.") then + SalesHeader.Delete(true); + end; + BillingLine.Partner::Vendor: + begin + if PurchaseHeader.Get(BillingLine.GetPurchaseDocumentTypeFromBillingDocumentType(), BillingLine."Document No.") then + PurchaseHeader.Delete(true); + end; 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 2eec1b4930..cff9979449 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") Success: 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") Success: Boolean begin if BillingLine.FindSet() then repeat @@ -824,16 +829,158 @@ 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") Success: 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.'; + 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"; + begin + if AutomatedBilling then + ContractBillingErrLog.InsertUnspecificLog(ErrorText) + else + Error(ErrorText); + end; + + local procedure DisplayOrLogErrorFromBillingTemplate(BillingTemplateCode: Code[20]; ErrorText: Text) + var + ContractBillingErrLog: Record "Contract Billing Err Log"; + begin + if AutomatedBilling then + ContractBillingErrLog.InsertLogFromBillingTemplate( + BillingTemplateCode, + ErrorText) + else + Error(ErrorText); + 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"; + begin + if AutomatedBilling then begin + BillingLine.GetServiceCommitment(SubscriptionLine); + ContractBillingErrLog.InsertLogFromSubscriptionLine( + BillingLine."Billing Template Code", + SubscriptionLine, + ErrorText); + 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 + Error(ErrorText); end; local procedure ProcessingFinishedMessage() @@ -920,6 +1067,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 +1082,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 +1199,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 +1373,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..54c06e5725 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al @@ -0,0 +1,76 @@ +namespace Microsoft.SubscriptionBilling; + +using System.Telemetry; +using System.Threading; + +codeunit 8034 SubBillingBackgroundJobs +{ + procedure ScheduleRecurrentImportJob(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 IsRecurrentJobScheduledForAService(BillingTemplate."Batch Recurrent Job Id") then begin + JobQueueEntry.ScheduleRecurrentJobQueueEntryWithFrequency(JobQueueEntry."Object Type to Run"::Codeunit, Codeunit::"Auto Contract Billing", BillingTemplate.RecordId, BillingTemplate."Minutes between runs", BillingTemplate."Automation Start Time"); + 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.Modify(); + end else begin + JobQueueEntry.Get(BillingTemplate."Batch Recurrent Job Id"); + JobQueueEntry."Starting Time" := BillingTemplate."Automation Start Time"; + JobQueueEntry."No. of Minutes between Runs" := BillingTemplate."Minutes between runs"; + JobQueueEntry."No. of Attempts to Run" := 0; + JobQueueEntry.Modify(); + if not JobQueueEntry.IsReadyToStart() then + JobQueueEntry.Restart(); + end; + 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 HandleRecurrentImportJob(var BillingTemplate: Record "Billing Template") + begin + if BillingTemplate.Automation = BillingTemplate.Automation::"Create Billing Proposal and Documents" then begin + BillingTemplate.TestField("Minutes between runs"); + ScheduleRecurrentImportJob(BillingTemplate); + end else + RemoveJob(BillingTemplate); + end; + + procedure RemoveJob(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 IsRecurrentJobScheduledForAService(JobId: Guid): Boolean + var + JobQueueEntry: Record "Job Queue Entry"; + begin + if IsNullGuid(JobId) then + exit(false); + + exit(JobQueueEntry.Get(JobId)); + 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..710e5624d0 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,31 @@ 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("Automation Start Time"; Rec."Automation Start Time") + { + Enabled = Rec.Automation = Rec.Automation::"Create Billing Proposal and Documents"; + } + field("Minutes between runs"; Rec."Minutes between runs") + { + Enabled = Rec.Automation = Rec.Automation::"Create Billing Proposal and Documents"; + } + field("Batch Recurrent Job Id"; Rec."Batch Recurrent Job Id") + { + Enabled = Rec.Automation = Rec.Automation::"Create Billing Proposal and Documents"; + Visible = false; + } } } } 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..438f4414a4 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Pages/ContractBillingErrLog.Page.al @@ -0,0 +1,71 @@ +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.") + { + ApplicationArea = All; + ToolTip = 'Specifies the unique entry number for the error log record.'; + } + field("Billing Template Code"; Rec."Billing Template Code") + { + ApplicationArea = All; + ToolTip = 'Specifies the billing template code that was being processed when the error occurred.'; + } + field("Error Text"; Rec."Error Text") + { + ApplicationArea = All; + ToolTip = 'Specifies the error message that occurred during the auto contract billing process.'; + } + field("Subscription"; Rec."Subscription") + { + ApplicationArea = All; + ToolTip = 'Specifies the subscription number that was being processed when the error occurred.'; + } + field("Subscription Entry No."; Rec."Subscription Entry No.") + { + ApplicationArea = All; + ToolTip = 'Specifies the subscription line entry number that was being processed when the error occurred.'; + } + field("Subscription Contract No."; Rec."Subscription Contract No.") + { + ApplicationArea = All; + ToolTip = 'Specifies the subscription contract number that was being processed when the error occurred.'; + } + field("Contract Line No."; Rec."Contract Line No.") + { + ApplicationArea = All; + ToolTip = 'Specifies the contract line number that was being processed when the error occurred.'; + } + field("Contract Type"; Rec."Contract Type") + { + ApplicationArea = All; + ToolTip = 'Specifies the contract type that was being processed when the error occurred.'; + } + field("Assigned User ID"; Rec."Assigned User ID") + { + ApplicationArea = All; + ToolTip = 'Specifies the user ID assigned to handle this error.'; + } + field("Salesperson Code"; Rec."Salesperson Code") + { + ApplicationArea = All; + ToolTip = 'Specifies the salesperson code associated with the contract that had an error.'; + } + } + } + } +} 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..b2a5277234 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(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Customer Rec. Billing Grouping"; var 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..8e9db063b6 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(var NewDocumentDate: Date; var NewPostingDate: Date; var 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 7aa0107a5b..1fc8f347ee 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,19 @@ 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); + BillingTemplate.CalculateDocumentDates(PostingDate, DocumentDate, false); + if BillingTemplate.Partner = BillingTemplate.Partner::Customer then + CreateBillingDocuments.SetCustomerRecurringBillingGrouping(BillingTemplate."Customer Document per"); + CreateBillingDocuments.SetDocumentDataFromRequestPage(DocumentDate, PostingDate, false, false); + IsSuccess := CreateBillingDocuments.Run(Rec); if not IsSuccess then ErrorMessageHandler.ShowErrors(); InitTempTable(); @@ -299,7 +306,7 @@ page 8067 "Recurring Billing" trigger OnAction() begin - BillingProposal.DeleteBillingDocuments(); + BillingProposal.DeleteBillingDocuments(BillingTemplate.Code); InitTempTable(); end; } @@ -508,16 +515,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/Report Extensions/BatchPostSalesInvoices.ReportExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Report Extensions/BatchPostSalesInvoices.ReportExt.al new file mode 100644 index 0000000000..4dc887e6d6 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Report Extensions/BatchPostSalesInvoices.ReportExt.al @@ -0,0 +1,36 @@ +namespace Microsoft.SubscriptionBilling; + +using Microsoft.Sales.Document; + +reportextension 8001 BatchPostSalesInvoices extends "Batch Post Sales Invoices" +{ + dataset + { + modify("Sales Header") + { + trigger OnBeforePostDataItem() + begin + if RecurringBillingOnly then + "Sales Header".SetRange("Recurring Billing", true); + end; + } + + } + requestpage + { + layout + { + addlast(Options) + { + field(RecurringBillingOnly; RecurringBillingOnly) + { + ApplicationArea = Basic, Suite; + Caption = 'Recurring Billing only'; + ToolTip = 'Specifies if you want to post invoices automatically created from subscription billing.'; + } + } + } + } + var + RecurringBillingOnly: Boolean; +} 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..37f208573f 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 = CustomerContent; + 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..95b7fd763d 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 = CustomerContent; + 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..1446585f0b --- /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 = CustomerContent; + } + } + 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 5a7a7736bf..eb38a3bfd1 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..77165b4a5e 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.Utilities; + table 8060 "Billing Template" { DataClassification = CustomerContent; @@ -43,6 +46,94 @@ table 8060 "Billing Template" { Caption = 'Filter'; } + field(11; "Posting Date Formula"; DateFormula) + { + Caption = 'Posting Date Formula'; + ToolTip = 'Specifies the date formula, the Posting Date will be calculated with.'; + trigger OnValidate() + begin + if Format("Posting Date Formula") <> '' then + if Automation = Automation::None then + Error(CanOnlyBeSetWhenAutomatedErr, FieldCaption("Posting Date Formula"), FieldCaption(Automation), Automation::"Create Billing Proposal and Documents"); + end; + } + field(12; "Document Date Formula"; DateFormula) + { + Caption = 'Document Date Formula'; + ToolTip = 'Specifies the date formula the Document Date will be calculated with.'; + trigger OnValidate() + begin + if Format("Document Date Formula") <> '' then + if Automation = Automation::None then + Error(CanOnlyBeSetWhenAutomatedErr, FieldCaption("Document Date Formula"), FieldCaption(Automation), Automation::"Create Billing Proposal and Documents"); + end; + } + 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 + if "Customer Document per" <> "Customer Document per"::Contract then + if Automation = Automation::None then + Error(CanOnlyBeSetWhenAutomatedErr, FieldCaption("Customer Document per"), FieldCaption(Automation), Automation::"Create Billing Proposal and Documents"); + 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: + begin + "Automation Start Time" := 0T; + "Minutes between runs" := 0; + end; + Automation::"Create Billing Proposal and Documents": + begin + TestField(Partner, Partner::Customer); + "My Suggestions Only" := false; + "Automation Start Time" := 0T; + "Minutes between runs" := 60; + end; + end; + SubBillingBackgroundJobs.HandleRecurrentImportJob(Rec); + end; + } + field(16; "Automation Start Time"; Time) + { + Caption = 'Automation Start Time'; + ToolTip = 'Specifies the time of day when the billing process should start.'; + DataClassification = SystemMetadata; + NotBlank = true; + + trigger OnValidate() + begin + SubBillingBackgroundJobs.HandleRecurrentImportJob(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 + SubBillingBackgroundJobs.HandleRecurrentImportJob(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; + } } keys @@ -53,6 +144,27 @@ 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 SubBillingBackgroundJobs; + 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.'; + CanOnlyBeSetWhenAutomatedErr: Label 'You can only set the field %1 if %2 is set to %3', Comment = '%1 - Customer Document per Field Caption, %2 - Automation Field Caption, %3 - Automation Field Value'; + internal procedure EditFilter(FieldNumber: Integer): Boolean var FilterPageBuilder: FilterPageBuilder; @@ -161,4 +273,67 @@ 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; + GroupBy: Enum "Contract Billing Grouping"; + 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; } \ 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..203f81d484 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Tables/ContractBillingErrLog.Table.al @@ -0,0 +1,125 @@ +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"; + + 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; +} 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..e24aa433c9 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 = CustomerContent; + 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..65ab0b527a --- /dev/null +++ b/src/Apps/W1/Subscription Billing/Test/Billing/AutomatedBillingTest.Codeunit.al @@ -0,0 +1,203 @@ +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 a Job Queue Entry is created 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."); + 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"); + ClearAll(); + ContractTestLibrary.InitContractsApp(); + + if IsInitialized then + exit; + + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Automated Billing Test"); + IsInitialized := true; + 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..8f3f594539 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.'; 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 700a1b4e70..ab7bcdd194 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 d1aa0af8ea..eff34ddeca 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."); From 45dc28d55f44a8af6392b6dfa62e3aae762eaac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Wed, 29 Oct 2025 19:48:03 +0100 Subject: [PATCH 02/10] Adressing Feedback --- .../Tables/SubscriptionBillingCue.Table.al | 2 +- .../Codeunits/BillingProposal.Codeunit.al | 4 +- .../CreateBillingDocuments.Codeunit.al | 38 ++++++++-- .../SubBillingBackgroundJobs.Codeunit.al | 16 ++--- .../Pages/ContractBillingErrLog.Page.al | 69 +++++++++++++++---- .../BatchPostSalesInvoices.ReportExt.al | 36 ---------- .../SalesCrMemoHeader.TableExt.al | 2 +- .../SalesInvoiceHeader.TableExt.al | 2 +- .../Table Extensions/UserSetup.TableExt.al | 2 +- .../Billing/Tables/BillingTemplate.Table.al | 31 ++++++--- .../Tables/ContractBillingErrLog.Table.al | 45 +++++++++++- .../SubBillingBasic.PermissionSet.al | 3 +- .../SubBillingObjects.PermissionSet.al | 5 +- .../SubBillingUser.PermissionSet.al | 3 +- .../Table Extensions/SalesHeader.TableExt.al | 2 +- .../Billing/AutomatedBillingTest.Codeunit.al | 14 ++-- 16 files changed, 181 insertions(+), 93 deletions(-) delete mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Report Extensions/BatchPostSalesInvoices.ReportExt.al 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 840e0ac694..074cfadac2 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 @@ -144,7 +144,7 @@ table 8070 "Subscription Billing Cue" } field(30; "Errors Automated Billing"; Integer) { - CalcFormula = count("Contract Billing Err Log"); + 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; 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 2fa3b9a569..bbcfc5492b 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 @@ -154,7 +154,7 @@ codeunit 8062 "Billing Proposal" BillingTemplate: Record "Billing Template"; CustomerContract: Record "Customer Subscription Contract"; VendorContract: Record "Vendor Subscription Contract"; - ContractBillingErrLog: Record "Contract Billing Err Log"; + ContractBillingErrLog: Record "Contract Billing Err. Log"; FilterText: Text; BillingRhythmFilterText: Text; begin @@ -241,7 +241,7 @@ codeunit 8062 "Billing Proposal" 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"; + ContractBillingErrLog: Record "Contract Billing Err. Log"; SkipServiceCommitment: Boolean; BillingPeriodStart: Date; BillingPeriodEnd: Date; 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 cff9979449..57f9bd416a 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 @@ -944,31 +944,48 @@ codeunit 8060 "Create Billing Documents" local procedure DisplayOrLogUnspecificError(ErrorText: Text) var - ContractBillingErrLog: Record "Contract Billing Err Log"; + ContractBillingErrLog: Record "Contract Billing Err. Log"; + ErrorTextInfo: ErrorInfo; begin if AutomatedBilling then ContractBillingErrLog.InsertUnspecificLog(ErrorText) - else - Error(ErrorText); + 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"; + ContractBillingErrLog: Record "Contract Billing Err. Log"; + BillingTemplate: Record "Billing Template"; + ErrorTextInfo: ErrorInfo; begin if AutomatedBilling then ContractBillingErrLog.InsertLogFromBillingTemplate( BillingTemplateCode, ErrorText) - else + 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"; + ContractBillingErrLog: Record "Contract Billing Err. Log"; SubscriptionLine: Record "Subscription Line"; FilteredBillingLine: Record "Billing Line"; + ErrorTextInfo: ErrorInfo; begin if AutomatedBilling then begin BillingLine.GetServiceCommitment(SubscriptionLine); @@ -979,8 +996,15 @@ codeunit 8060 "Create Billing Documents" 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 + 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() 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 index 54c06e5725..a0aba7f94f 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al @@ -3,9 +3,9 @@ namespace Microsoft.SubscriptionBilling; using System.Telemetry; using System.Threading; -codeunit 8034 SubBillingBackgroundJobs +codeunit 8034 "Sub. Billing Background Jobs" { - procedure ScheduleRecurrentImportJob(var BillingTemplate: Record "Billing Template") + procedure ScheduleRecurrentBillingJob(var BillingTemplate: Record "Billing Template") var JobQueueEntry: Record "Job Queue Entry"; Telemetry: Codeunit Telemetry; @@ -14,7 +14,7 @@ codeunit 8034 SubBillingBackgroundJobs if BillingTemplate.Code = '' then exit; - if not IsRecurrentJobScheduledForAService(BillingTemplate."Batch Recurrent Job Id") then begin + if not IsRecurrentJobScheduledForABillingTemplate(BillingTemplate."Batch Recurrent Job Id") then begin JobQueueEntry.ScheduleRecurrentJobQueueEntryWithFrequency(JobQueueEntry."Object Type to Run"::Codeunit, Codeunit::"Auto Contract Billing", BillingTemplate.RecordId, BillingTemplate."Minutes between runs", BillingTemplate."Automation Start Time"); BillingTemplate."Batch Recurrent Job Id" := JobQueueEntry.ID; BillingTemplate.Modify(); @@ -41,16 +41,16 @@ codeunit 8034 SubBillingBackgroundJobs end; - procedure HandleRecurrentImportJob(var BillingTemplate: Record "Billing Template") + procedure HandleRecurrentBillingJob(var BillingTemplate: Record "Billing Template") begin if BillingTemplate.Automation = BillingTemplate.Automation::"Create Billing Proposal and Documents" then begin BillingTemplate.TestField("Minutes between runs"); - ScheduleRecurrentImportJob(BillingTemplate); + ScheduleRecurrentBillingJob(BillingTemplate); end else - RemoveJob(BillingTemplate); + RemovedRecurrentBillingJob(BillingTemplate); end; - procedure RemoveJob(var BillingTemplate: Record "Billing Template") + procedure RemovedRecurrentBillingJob(var BillingTemplate: Record "Billing Template") var JobQueueEntry: Record "Job Queue Entry"; begin @@ -60,7 +60,7 @@ codeunit 8034 SubBillingBackgroundJobs BillingTemplate.Modify(); end; - local procedure IsRecurrentJobScheduledForAService(JobId: Guid): Boolean + local procedure IsRecurrentJobScheduledForABillingTemplate(JobId: Guid): Boolean var JobQueueEntry: Record "Job Queue Entry"; begin 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 index 438f4414a4..a6fb548457 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Pages/ContractBillingErrLog.Page.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Pages/ContractBillingErrLog.Page.al @@ -1,11 +1,11 @@ namespace Microsoft.SubscriptionBilling; -page 8113 "Contract Billing Err Log" +page 8113 "Contract Billing Err. Log" { Caption = 'Contract Billing Error Log'; PageType = List; ApplicationArea = All; - SourceTable = "Contract Billing Err Log"; + SourceTable = "Contract Billing Err. Log"; UsageCategory = Lists; Editable = false; @@ -17,55 +17,98 @@ page 8113 "Contract Billing Err Log" { field("Entry No."; Rec."Entry No.") { - ApplicationArea = All; ToolTip = 'Specifies the unique entry number for the error log record.'; } field("Billing Template Code"; Rec."Billing Template Code") { - ApplicationArea = All; ToolTip = 'Specifies the billing template code that was being processed when the error occurred.'; } field("Error Text"; Rec."Error Text") { - ApplicationArea = All; ToolTip = 'Specifies the error message that occurred during the auto contract billing process.'; } field("Subscription"; Rec."Subscription") { - ApplicationArea = All; ToolTip = 'Specifies the subscription number that was being processed when the error occurred.'; } field("Subscription Entry No."; Rec."Subscription Entry No.") { - ApplicationArea = All; ToolTip = 'Specifies the subscription line entry number that was being processed when the error occurred.'; } field("Subscription Contract No."; Rec."Subscription Contract No.") { - ApplicationArea = All; ToolTip = 'Specifies the subscription contract number that was being processed when the error occurred.'; } field("Contract Line No."; Rec."Contract Line No.") { - ApplicationArea = All; ToolTip = 'Specifies the contract line number that was being processed when the error occurred.'; } field("Contract Type"; Rec."Contract Type") { - ApplicationArea = All; ToolTip = 'Specifies the contract type that was being processed when the error occurred.'; } field("Assigned User ID"; Rec."Assigned User ID") { - ApplicationArea = All; ToolTip = 'Specifies the user ID assigned to handle this error.'; } field("Salesperson Code"; Rec."Salesperson Code") { - ApplicationArea = All; 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/Report Extensions/BatchPostSalesInvoices.ReportExt.al b/src/Apps/W1/Subscription Billing/App/Billing/Report Extensions/BatchPostSalesInvoices.ReportExt.al deleted file mode 100644 index 4dc887e6d6..0000000000 --- a/src/Apps/W1/Subscription Billing/App/Billing/Report Extensions/BatchPostSalesInvoices.ReportExt.al +++ /dev/null @@ -1,36 +0,0 @@ -namespace Microsoft.SubscriptionBilling; - -using Microsoft.Sales.Document; - -reportextension 8001 BatchPostSalesInvoices extends "Batch Post Sales Invoices" -{ - dataset - { - modify("Sales Header") - { - trigger OnBeforePostDataItem() - begin - if RecurringBillingOnly then - "Sales Header".SetRange("Recurring Billing", true); - end; - } - - } - requestpage - { - layout - { - addlast(Options) - { - field(RecurringBillingOnly; RecurringBillingOnly) - { - ApplicationArea = Basic, Suite; - Caption = 'Recurring Billing only'; - ToolTip = 'Specifies if you want to post invoices automatically created from subscription billing.'; - } - } - } - } - var - RecurringBillingOnly: Boolean; -} 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 37f208573f..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 @@ -24,7 +24,7 @@ tableextension 8057 "Sales Cr. Memo Header" extends "Sales Cr.Memo Header" { Caption = 'Auto Contract Billing'; ToolTip = 'Specifies whether the Document has been created by an auto billing template.'; - DataClassification = CustomerContent; + DataClassification = SystemMetadata; Editable = false; } } 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 95b7fd763d..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 @@ -24,7 +24,7 @@ tableextension 8055 "Sales Invoice Header" extends "Sales Invoice Header" { 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 = CustomerContent; + DataClassification = SystemMetadata; Editable = false; } } 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 index 1446585f0b..4c8c3cc179 100644 --- 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 @@ -10,7 +10,7 @@ tableextension 8011 "User Setup" extends "User Setup" { Caption = 'Auto Contract Billing'; ToolTip = 'Specifies, whether the user can automate contract billing. It allows to work with and edit automated Billing Templates.'; - DataClassification = CustomerContent; + DataClassification = SystemMetadata; } } internal procedure AutoContractBillingAllowed(): Boolean 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 77165b4a5e..cade530ea5 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 @@ -24,6 +24,14 @@ table 8060 "Billing Template" 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) { @@ -53,8 +61,7 @@ table 8060 "Billing Template" trigger OnValidate() begin if Format("Posting Date Formula") <> '' then - if Automation = Automation::None then - Error(CanOnlyBeSetWhenAutomatedErr, FieldCaption("Posting Date Formula"), FieldCaption(Automation), Automation::"Create Billing Proposal and Documents"); + ThrowErrorIfAutomationIsNotSet(FieldCaption("Posting Date Formula")); end; } field(12; "Document Date Formula"; DateFormula) @@ -64,8 +71,7 @@ table 8060 "Billing Template" trigger OnValidate() begin if Format("Document Date Formula") <> '' then - if Automation = Automation::None then - Error(CanOnlyBeSetWhenAutomatedErr, FieldCaption("Document Date Formula"), FieldCaption(Automation), Automation::"Create Billing Proposal and Documents"); + ThrowErrorIfAutomationIsNotSet(FieldCaption("Document Date Formula")); end; } field(13; "Customer Document per"; Enum "Customer Rec. Billing Grouping") @@ -75,8 +81,7 @@ table 8060 "Billing Template" trigger OnValidate() begin if "Customer Document per" <> "Customer Document per"::Contract then - if Automation = Automation::None then - Error(CanOnlyBeSetWhenAutomatedErr, FieldCaption("Customer Document per"), FieldCaption(Automation), Automation::"Create Billing Proposal and Documents"); + ThrowErrorIfAutomationIsNotSet(FieldCaption("Customer Document per")); end; } field(15; Automation; Enum "Sub. Billing Automation") @@ -101,7 +106,7 @@ table 8060 "Billing Template" "Minutes between runs" := 60; end; end; - SubBillingBackgroundJobs.HandleRecurrentImportJob(Rec); + SubBillingBackgroundJobs.HandleRecurrentBillingJob(Rec); end; } field(16; "Automation Start Time"; Time) @@ -113,7 +118,7 @@ table 8060 "Billing Template" trigger OnValidate() begin - SubBillingBackgroundJobs.HandleRecurrentImportJob(Rec); + SubBillingBackgroundJobs.HandleRecurrentBillingJob(Rec); end; } field(17; "Minutes between runs"; Integer) @@ -124,7 +129,7 @@ table 8060 "Billing Template" trigger OnValidate() begin - SubBillingBackgroundJobs.HandleRecurrentImportJob(Rec); + SubBillingBackgroundJobs.HandleRecurrentBillingJob(Rec); end; } field(18; "Batch Recurrent Job Id"; Guid) @@ -161,7 +166,7 @@ table 8060 "Billing Template" var UserSetup: Record "User Setup"; - SubBillingBackgroundJobs: Codeunit SubBillingBackgroundJobs; + 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.'; CanOnlyBeSetWhenAutomatedErr: Label 'You can only set the field %1 if %2 is set to %3', Comment = '%1 - Customer Document per Field Caption, %2 - Automation Field Caption, %3 - Automation Field Value'; @@ -336,4 +341,10 @@ table 8060 "Billing Template" else DocumentDate := ReferenceDate; end; + + local procedure ThrowErrorIfAutomationIsNotSet(FieldCaption: Text) + begin + if Automation = Automation::None then + Error(CanOnlyBeSetWhenAutomatedErr, FieldCaption, FieldCaption(Automation), Automation::"Create Billing Proposal and Documents"); + 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 index 203f81d484..bf4111a2f5 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Tables/ContractBillingErrLog.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Tables/ContractBillingErrLog.Table.al @@ -3,12 +3,15 @@ namespace Microsoft.SubscriptionBilling; using System.Security.User; using Microsoft.CRM.Team; -table 8022 "Contract Billing Err Log" +table 8022 "Contract Billing Err. Log" { Caption = 'Contract Billing Error Log'; DataClassification = CustomerContent; - DrillDownPageId = "Contract Billing Err Log"; - LookupPageId = "Contract Billing Err Log"; + DrillDownPageId = "Contract Billing Err. Log"; + LookupPageId = "Contract Billing Err. Log"; + Permissions = + tabledata "Contract Billing Err. Log" = rmid, + tabledata "Billing Line" = rm; fields { @@ -80,6 +83,7 @@ table 8022 "Contract Billing Err Log" Clustered = true; } } + local procedure InitRecord() begin Rec.Init(); @@ -122,4 +126,39 @@ table 8022 "Contract Billing Err Log" 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/SalesHeader.TableExt.al b/src/Apps/W1/Subscription Billing/App/Sales Service Commitments/Table Extensions/SalesHeader.TableExt.al index e24aa433c9..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 @@ -23,7 +23,7 @@ tableextension 8053 "Sales Header" extends "Sales Header" { Caption = 'Auto Contract Billing'; ToolTip = 'Specifies whether the Document has been created by an auto billing template.'; - DataClassification = CustomerContent; + DataClassification = SystemMetadata; Editable = false; } } diff --git a/src/Apps/W1/Subscription Billing/Test/Billing/AutomatedBillingTest.Codeunit.al b/src/Apps/W1/Subscription Billing/Test/Billing/AutomatedBillingTest.Codeunit.al index 65ab0b527a..2a57cda94e 100644 --- a/src/Apps/W1/Subscription Billing/Test/Billing/AutomatedBillingTest.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/Test/Billing/AutomatedBillingTest.Codeunit.al @@ -83,7 +83,7 @@ codeunit 139684 "Automated Billing Test" JobQueueEntry: Record "Job Queue Entry"; SalesHeader: Record "Sales Header"; begin - // [SCENARIO] Verify that a Job Queue Entry is created correctly for a Billing Template with automation settings + // [SCENARIO] Verify that automated billing processes contracts correctly for a Billing Template with automation settings Initialize(); // [GIVEN] A Billing Template with automation settings @@ -98,6 +98,7 @@ codeunit 139684 "Automated Billing Test" BillingLine.FindSet(); BillingLine.TestField("Document No."); SalesHeader.Get(BillingLine.GetSalesDocumentTypeFromBillingDocumentType(), BillingLine."Document No."); + SalesHeader.TestField("Auto Contract Billing", true); end; [Test] @@ -108,7 +109,7 @@ codeunit 139684 "Automated Billing Test" SubscriptionHeader: Record "Subscription Header"; SubscriptionLine: Record "Subscription Line"; BillingTemplate: Record "Billing Template"; - ContractBillingErrLog: Record "Contract Billing Err Log"; + 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 @@ -140,14 +141,15 @@ codeunit 139684 "Automated Billing Test" local procedure Initialize() begin LibraryTestInitialize.OnTestInitialize(Codeunit::"Automated Billing Test"); - ClearAll(); - ContractTestLibrary.InitContractsApp(); - if IsInitialized then exit; - LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Automated Billing Test"); + + ContractTestLibrary.InitContractsApp(); + IsInitialized := true; + Commit(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Automated Billing Test"); end; From 45249fff9e7cc3d56c4dc0f63248130e9fde62aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Fri, 31 Oct 2025 15:58:26 +0100 Subject: [PATCH 03/10] Adressing Feedback --- .../Codeunits/BillingProposal.Codeunit.al | 4 +- .../CreateBillingDocuments.Codeunit.al | 6 +- .../SubBillingBackgroundJobs.Codeunit.al | 57 +++++++++++-------- .../Billing/Tables/BillingTemplate.Table.al | 10 ++-- 4 files changed, 43 insertions(+), 34 deletions(-) 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 bbcfc5492b..1c289d479d 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 @@ -261,7 +261,7 @@ codeunit 8062 "Billing Proposal" ContractBillingErrLog.InsertLogFromSubscriptionLine( BillingTemplate.Code, ServiceCommitment, - StrSubstNo(CreditMemoExistsForSubscriptionLineTxt, ServiceCommitment."Subscription Header No.", ServiceCommitment."Entry No.")) + CopyStr(StrSubstNo(CreditMemoExistsForSubscriptionLineTxt, ServiceCommitment."Subscription Header No.", ServiceCommitment."Entry No."), 1, 250)) else SalesHeaderGlobal.Mark(true); Enum::"Service Partner"::Vendor: @@ -270,7 +270,7 @@ codeunit 8062 "Billing Proposal" ContractBillingErrLog.InsertLogFromSubscriptionLine( BillingTemplate.Code, ServiceCommitment, - StrSubstNo(CreditMemoExistsForSubscriptionLineTxt, ServiceCommitment."Subscription Header No.", ServiceCommitment."Entry No.")) + CopyStr(StrSubstNo(CreditMemoExistsForSubscriptionLineTxt, ServiceCommitment."Subscription Header No.", ServiceCommitment."Entry No."), 1, 250)) else PurchaseHeaderGlobal.Mark(true); 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 57f9bd416a..4b670615e7 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 @@ -948,7 +948,7 @@ codeunit 8060 "Create Billing Documents" ErrorTextInfo: ErrorInfo; begin if AutomatedBilling then - ContractBillingErrLog.InsertUnspecificLog(ErrorText) + ContractBillingErrLog.InsertUnspecificLog(CopyStr(ErrorText, 1, 250)) else begin ErrorTextInfo.ErrorType := ErrorType::Client; ErrorTextInfo.Message := ErrorText; @@ -966,7 +966,7 @@ codeunit 8060 "Create Billing Documents" if AutomatedBilling then ContractBillingErrLog.InsertLogFromBillingTemplate( BillingTemplateCode, - ErrorText) + CopyStr(ErrorText, 1, 250)) else begin ErrorTextInfo.ErrorType := ErrorType::Client; ErrorTextInfo.Message := ErrorText; @@ -992,7 +992,7 @@ codeunit 8060 "Create Billing Documents" ContractBillingErrLog.InsertLogFromSubscriptionLine( BillingLine."Billing Template Code", SubscriptionLine, - ErrorText); + 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); 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 index a0aba7f94f..1fc56f89ce 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al @@ -5,7 +5,7 @@ using System.Threading; codeunit 8034 "Sub. Billing Background Jobs" { - procedure ScheduleRecurrentBillingJob(var BillingTemplate: Record "Billing Template") + procedure ScheduleAutomatedBillingJob(var BillingTemplate: Record "Billing Template") var JobQueueEntry: Record "Job Queue Entry"; Telemetry: Codeunit Telemetry; @@ -14,24 +14,10 @@ codeunit 8034 "Sub. Billing Background Jobs" if BillingTemplate.Code = '' then exit; - if not IsRecurrentJobScheduledForABillingTemplate(BillingTemplate."Batch Recurrent Job Id") then begin - JobQueueEntry.ScheduleRecurrentJobQueueEntryWithFrequency(JobQueueEntry."Object Type to Run"::Codeunit, Codeunit::"Auto Contract Billing", BillingTemplate.RecordId, BillingTemplate."Minutes between runs", BillingTemplate."Automation Start Time"); - 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.Modify(); - end else begin - JobQueueEntry.Get(BillingTemplate."Batch Recurrent Job Id"); - JobQueueEntry."Starting Time" := BillingTemplate."Automation Start Time"; - JobQueueEntry."No. of Minutes between Runs" := BillingTemplate."Minutes between runs"; - JobQueueEntry."No. of Attempts to Run" := 0; - JobQueueEntry.Modify(); - if not JobQueueEntry.IsReadyToStart() then - JobQueueEntry.Restart(); - end; + 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)); @@ -41,16 +27,16 @@ codeunit 8034 "Sub. Billing Background Jobs" end; - procedure HandleRecurrentBillingJob(var BillingTemplate: Record "Billing Template") + 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"); - ScheduleRecurrentBillingJob(BillingTemplate); + ScheduleAutomatedBillingJob(BillingTemplate); end else - RemovedRecurrentBillingJob(BillingTemplate); + RemoveAutomatedBillingJob(BillingTemplate); end; - procedure RemovedRecurrentBillingJob(var BillingTemplate: Record "Billing Template") + procedure RemoveAutomatedBillingJob(var BillingTemplate: Record "Billing Template") var JobQueueEntry: Record "Job Queue Entry"; begin @@ -60,7 +46,7 @@ codeunit 8034 "Sub. Billing Background Jobs" BillingTemplate.Modify(); end; - local procedure IsRecurrentJobScheduledForABillingTemplate(JobId: Guid): Boolean + local procedure IsAutomatedBillingJobScheduled(JobId: Guid): Boolean var JobQueueEntry: Record "Job Queue Entry"; begin @@ -70,6 +56,29 @@ codeunit 8034 "Sub. Billing Background Jobs" 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."Automation Start Time"); + 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.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."Starting Time" := BillingTemplate."Automation Start Time"; + JobQueueEntry."No. of Minutes between Runs" := BillingTemplate."Minutes between runs"; + JobQueueEntry."No. of Attempts to Run" := 0; + 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/Tables/BillingTemplate.Table.al b/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingTemplate.Table.al index cade530ea5..c6c6a7d0c6 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 @@ -57,7 +57,7 @@ table 8060 "Billing Template" field(11; "Posting Date Formula"; DateFormula) { Caption = 'Posting Date Formula'; - ToolTip = 'Specifies the date formula, the Posting Date will be calculated with.'; + 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.'; trigger OnValidate() begin if Format("Posting Date Formula") <> '' then @@ -67,7 +67,7 @@ table 8060 "Billing Template" field(12; "Document Date Formula"; DateFormula) { Caption = 'Document Date Formula'; - ToolTip = 'Specifies the date formula the Document Date will be calculated with.'; + 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.'; trigger OnValidate() begin if Format("Document Date Formula") <> '' then @@ -106,7 +106,7 @@ table 8060 "Billing Template" "Minutes between runs" := 60; end; end; - SubBillingBackgroundJobs.HandleRecurrentBillingJob(Rec); + SubBillingBackgroundJobs.HandleAutomatedBillingJob(Rec); end; } field(16; "Automation Start Time"; Time) @@ -118,7 +118,7 @@ table 8060 "Billing Template" trigger OnValidate() begin - SubBillingBackgroundJobs.HandleRecurrentBillingJob(Rec); + SubBillingBackgroundJobs.HandleAutomatedBillingJob(Rec); end; } field(17; "Minutes between runs"; Integer) @@ -129,7 +129,7 @@ table 8060 "Billing Template" trigger OnValidate() begin - SubBillingBackgroundJobs.HandleRecurrentBillingJob(Rec); + SubBillingBackgroundJobs.HandleAutomatedBillingJob(Rec); end; } field(18; "Batch Recurrent Job Id"; Guid) From 88e7098dae20014ba1c048bc083ec50ec4652666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Tue, 11 Nov 2025 11:41:29 +0100 Subject: [PATCH 04/10] Adressing Feedback --- .../App/Billing/Codeunits/BillingProposal.Codeunit.al | 3 ++- .../App/Billing/Codeunits/CreateBillingDocuments.Codeunit.al | 2 +- .../Test/Billing/RecurringBillingTest.Codeunit.al | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) 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 1c289d479d..529ea635e3 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 @@ -549,7 +549,7 @@ codeunit 8062 "Billing Proposal" 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'; + 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 @@ -897,6 +897,7 @@ codeunit 8062 "Billing Proposal" 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', ''); 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 4b670615e7..9d91c89021 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 @@ -902,7 +902,7 @@ codeunit 8060 "Create Billing Documents" 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.'; + 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; 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 8f3f594539..e584e35c38 100644 --- a/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingTest.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingTest.Codeunit.al @@ -1312,7 +1312,7 @@ codeunit 139688 "Recurring Billing Test" 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.'; + 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 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. From c8bc88316568372ba74400caa11603da9e61da90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Tue, 11 Nov 2025 11:42:39 +0100 Subject: [PATCH 05/10] improving readibility --- .../Codeunits/BillingProposal.Codeunit.al | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) 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 529ea635e3..6db3da07c5 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 @@ -193,25 +193,27 @@ codeunit 8062 "Billing Proposal" end; end; - if not AutomatedBilling then - case BillingTemplate.Partner of - Enum::"Service Partner"::Customer: - begin - SalesHeaderGlobal.MarkedOnly(true); - if SalesHeaderGlobal.Count <> 0 then begin - Page.Run(Page::"Sales Credit Memos", SalesHeaderGlobal); - Message(CreditMemoPreventsProposalCreationLbl); - end; + if AutomatedBilling then + exit; + + case BillingTemplate.Partner of + Enum::"Service Partner"::Customer: + begin + SalesHeaderGlobal.MarkedOnly(true); + if SalesHeaderGlobal.Count <> 0 then begin + Page.Run(Page::"Sales Credit Memos", SalesHeaderGlobal); + Message(CreditMemoPreventsProposalCreationLbl); end; - Enum::"Service Partner"::Vendor: - begin - PurchaseHeaderGlobal.MarkedOnly(true); - if PurchaseHeaderGlobal.Count <> 0 then begin - Page.Run(Page::"Purchase Credit Memos", PurchaseHeaderGlobal); - Message(CreditMemoPreventsProposalCreationLbl); - end; + end; + Enum::"Service Partner"::Vendor: + begin + PurchaseHeaderGlobal.MarkedOnly(true); + if PurchaseHeaderGlobal.Count <> 0 then begin + Page.Run(Page::"Purchase Credit Memos", PurchaseHeaderGlobal); + Message(CreditMemoPreventsProposalCreationLbl); end; - end; + end; + end; end; local procedure ProcessContractServiceCommitments(BillingTemplate: Record "Billing Template"; ContractNo: Code[20]; ContractLineFilter: Text; BillingDate: Date; BillingToDate: Date; BillingRhythmFilterText: Text; AutomatedBilling: Boolean) From e728059cf8a3de1d7819e6803589db2cb77c4e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Mon, 17 Nov 2025 15:15:23 +0100 Subject: [PATCH 06/10] Internal feedback from testing --- .../SubBillingBackgroundJobs.Codeunit.al | 3 +- .../Billing/Pages/BillingTemplates.Page.al | 9 ++--- .../Billing/Pages/RecurringBilling.Page.al | 10 +++-- .../Billing/Tables/BillingTemplate.Table.al | 39 ++++--------------- 4 files changed, 19 insertions(+), 42 deletions(-) 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 index 1fc56f89ce..d1663edf4e 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al @@ -58,7 +58,7 @@ codeunit 8034 "Sub. Billing Background Jobs" 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."Automation Start Time"); + 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(); @@ -71,7 +71,6 @@ codeunit 8034 "Sub. Billing Background Jobs" local procedure UpdateJobQueueEntryForAutomatedBilling(var BillingTemplate: Record "Billing Template"; var JobQueueEntry: Record "Job Queue Entry") begin JobQueueEntry.Get(BillingTemplate."Batch Recurrent Job Id"); - JobQueueEntry."Starting Time" := BillingTemplate."Automation Start Time"; JobQueueEntry."No. of Minutes between Runs" := BillingTemplate."Minutes between runs"; JobQueueEntry."No. of Attempts to Run" := 0; JobQueueEntry.Modify(); 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 710e5624d0..b9bc28b068 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 @@ -60,10 +60,6 @@ page 8066 "Billing Templates" field(Automation; Rec.Automation) { } - field("Automation Start Time"; Rec."Automation Start Time") - { - Enabled = Rec.Automation = Rec.Automation::"Create Billing Proposal and Documents"; - } field("Minutes between runs"; Rec."Minutes between runs") { Enabled = Rec.Automation = Rec.Automation::"Create Billing Proposal and Documents"; @@ -71,7 +67,10 @@ page 8066 "Billing Templates" field("Batch Recurrent Job Id"; Rec."Batch Recurrent Job Id") { Enabled = Rec.Automation = Rec.Automation::"Create Billing Proposal and Documents"; - Visible = false; + trigger OnAssistEdit() + begin + Rec.LookupJobEntryQueue(); + end; } } } 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 1fc8f347ee..b9caa2d9ed 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 @@ -261,10 +261,12 @@ page 8067 "Recurring Billing" ErrorMessageMgt.Activate(ErrorMessageHandler); ErrorMessageMgt.PushContext(ErrorContextElement, 0, 0, ''); Commit(); //commit to database before processing - BillingTemplate.CalculateDocumentDates(PostingDate, DocumentDate, false); - if BillingTemplate.Partner = BillingTemplate.Partner::Customer then - CreateBillingDocuments.SetCustomerRecurringBillingGrouping(BillingTemplate."Customer Document per"); - CreateBillingDocuments.SetDocumentDataFromRequestPage(DocumentDate, PostingDate, false, false); + 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(); 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 c6c6a7d0c6..9f6629145f 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,7 +1,7 @@ namespace Microsoft.SubscriptionBilling; using System.Security.User; -using System.Utilities; +using System.Threading; table 8060 "Billing Template" { @@ -58,21 +58,11 @@ table 8060 "Billing Template" { 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.'; - trigger OnValidate() - begin - if Format("Posting Date Formula") <> '' then - ThrowErrorIfAutomationIsNotSet(FieldCaption("Posting Date Formula")); - end; } 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.'; - trigger OnValidate() - begin - if Format("Document Date Formula") <> '' then - ThrowErrorIfAutomationIsNotSet(FieldCaption("Document Date Formula")); - end; } field(13; "Customer Document per"; Enum "Customer Rec. Billing Grouping") { @@ -80,8 +70,7 @@ table 8060 "Billing Template" ToolTip = 'Specifies how the Billing lines for customers are grouped in sales documents.'; trigger OnValidate() begin - if "Customer Document per" <> "Customer Document per"::Contract then - ThrowErrorIfAutomationIsNotSet(FieldCaption("Customer Document per")); + TestField(Partner, Partner::Customer); end; } field(15; Automation; Enum "Sub. Billing Automation") @@ -95,32 +84,18 @@ table 8060 "Billing Template" case Automation of Automation::None: begin - "Automation Start Time" := 0T; "Minutes between runs" := 0; end; Automation::"Create Billing Proposal and Documents": begin TestField(Partner, Partner::Customer); "My Suggestions Only" := false; - "Automation Start Time" := 0T; "Minutes between runs" := 60; end; end; SubBillingBackgroundJobs.HandleAutomatedBillingJob(Rec); end; } - field(16; "Automation Start Time"; Time) - { - Caption = 'Automation Start Time'; - ToolTip = 'Specifies the time of day when the billing process should start.'; - DataClassification = SystemMetadata; - NotBlank = true; - - trigger OnValidate() - begin - SubBillingBackgroundJobs.HandleAutomatedBillingJob(Rec); - end; - } field(17; "Minutes between runs"; Integer) { Caption = 'Minutes between runs'; @@ -168,7 +143,6 @@ table 8060 "Billing Template" 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.'; - CanOnlyBeSetWhenAutomatedErr: Label 'You can only set the field %1 if %2 is set to %3', Comment = '%1 - Customer Document per Field Caption, %2 - Automation Field Caption, %3 - Automation Field Value'; internal procedure EditFilter(FieldNumber: Integer): Boolean var @@ -342,9 +316,12 @@ table 8060 "Billing Template" DocumentDate := ReferenceDate; end; - local procedure ThrowErrorIfAutomationIsNotSet(FieldCaption: Text) + internal procedure LookupJobEntryQueue() + var + JobQueueEntry: Record "Job Queue Entry"; begin - if Automation = Automation::None then - Error(CanOnlyBeSetWhenAutomatedErr, FieldCaption, FieldCaption(Automation), Automation::"Create Billing Proposal and Documents"); + JobQueueEntry.Get("Batch Recurrent Job Id"); + JobQueueEntry.SetRecFilter(); + Page.Run(0, JobQueueEntry); end; } \ No newline at end of file From b476374d7139cf530f2581a82ee4f1d98d1db365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Tue, 18 Nov 2025 11:21:56 +0100 Subject: [PATCH 07/10] internal feedback --- .../SubBillingBackgroundJobs.Codeunit.al | 2 ++ .../Billing/Pages/BillingTemplates.Page.al | 7 +++++-- .../Billing/Tables/BillingTemplate.Table.al | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) 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 index d1663edf4e..03f7815997 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SubBillingBackgroundJobs.Codeunit.al @@ -65,6 +65,7 @@ codeunit 8034 "Sub. Billing Background Jobs" 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; @@ -73,6 +74,7 @@ codeunit 8034 "Sub. Billing Background Jobs" 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(); 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 b9bc28b068..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 @@ -62,16 +62,18 @@ page 8066 "Billing Templates" } field("Minutes between runs"; Rec."Minutes between runs") { - Enabled = Rec.Automation = Rec.Automation::"Create Billing Proposal and Documents"; } field("Batch Recurrent Job Id"; Rec."Batch Recurrent Job Id") { - Enabled = Rec.Automation = Rec.Automation::"Create Billing Proposal and Documents"; + Visible = false; trigger OnAssistEdit() begin Rec.LookupJobEntryQueue(); end; } + field("Batch Rec. Job Description"; Rec."Batch Rec. Job Description") + { + } } } } @@ -103,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/Tables/BillingTemplate.Table.al b/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingTemplate.Table.al index 9f6629145f..d95d23f462 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 @@ -20,6 +20,10 @@ table 8060 "Billing Template" field(2; Description; Text[80]) { Caption = 'Description'; + trigger OnValidate() + begin + SubBillingBackgroundJobs.HandleAutomatedBillingJob(Rec); + end; } field(3; Partner; Enum "Service Partner") { @@ -104,6 +108,7 @@ table 8060 "Billing Template" trigger OnValidate() begin + TestField(Automation, Automation::"Create Billing Proposal and Documents"); SubBillingBackgroundJobs.HandleAutomatedBillingJob(Rec); end; } @@ -114,6 +119,14 @@ table 8060 "Billing Template" 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 @@ -324,4 +337,11 @@ table 8060 "Billing Template" 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 From 67640f8b9462d12d62dea8cd971c30c424f68a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Tue, 25 Nov 2025 14:30:34 +0100 Subject: [PATCH 08/10] Code Cop fixes --- .../Codeunits/BillingProposal.Codeunit.al | 22 +++---------------- .../Pages/CreateCustomerBillingDocs.Page.al | 2 +- .../Pages/CreateVendorBillingDocs.Page.al | 2 +- .../Billing/Tables/BillingTemplate.Table.al | 7 ++---- 4 files changed, 7 insertions(+), 26 deletions(-) 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 7c5e698843..23d3bfd99b 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 @@ -913,7 +913,7 @@ codeunit 8062 "Billing Proposal" BillingLine.SetRange("User ID", UserId()); if BillingLine.FindSet() then repeat - DeleteSalesBillingDocuments(BillingLine); + DeleteBillingDocuments(BillingLine); until BillingLine.Next() = 0; end; 2: @@ -921,13 +921,13 @@ codeunit 8062 "Billing Proposal" BillingLine.SetRange("Billing Template Code", BillingTemplate.Code); if BillingLine.FindSet() then repeat - DeleteSalesBillingDocuments(BillingLine); + DeleteBillingDocuments(BillingLine); until BillingLine.Next() = 0; end; end; end; - local procedure DeleteSalesBillingDocuments(BillingLine: Record "Billing Line") + local procedure DeleteBillingDocuments(BillingLine: Record "Billing Line") var SalesHeader: Record "Sales Header"; PurchaseHeader: Record "Purchase Header"; @@ -946,22 +946,6 @@ codeunit 8062 "Billing Proposal" end; end; - local procedure DeletePurchaseBillingDocuments(DeletePurchaseInvoices: Boolean; DeletePurchaseCreditMemos: Boolean) - 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); - end; - end; - [IntegrationEvent(false, false)] local procedure OnAfterUpdateBillingLineFromSubscriptionLine(var BillingLine: Record "Billing Line"; SubscriptionLine: Record "Subscription Line") begin 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 b2a5277234..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 @@ -63,7 +63,7 @@ page 8072 "Create Customer Billing Docs" NewPostDocuments := PostDocuments; end; - internal procedure SetData(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Customer Rec. Billing Grouping"; var NewPostDocuments: Boolean) + internal procedure SetData(NewDocumentDate: Date; NewPostingDate: Date; NewGroupingType: Enum "Customer Rec. Billing Grouping"; NewPostDocuments: Boolean) begin DocumentDate := NewDocumentDate; PostingDate := NewPostingDate; 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 8e9db063b6..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 @@ -57,7 +57,7 @@ page 8077 "Create Vendor Billing Docs" NewGroupingType := Grouping; end; - internal procedure SetData(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Vendor Rec. Billing Grouping") + internal procedure SetData(NewDocumentDate: Date; NewPostingDate: Date; NewGroupingType: Enum "Vendor Rec. Billing Grouping") begin DocumentDate := NewDocumentDate; PostingDate := NewPostingDate; 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 d95d23f462..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 @@ -87,9 +87,7 @@ table 8060 "Billing Template" Error(AutoContractBillingNotAllowedErr); case Automation of Automation::None: - begin - "Minutes between runs" := 0; - end; + "Minutes between runs" := 0; Automation::"Create Billing Proposal and Documents": begin TestField(Partner, Partner::Customer); @@ -125,7 +123,7 @@ table 8060 "Billing Template" 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"))); + CalcFormula = lookup("Job Queue Entry".Description where("ID" = field("Batch Recurrent Job Id"))); } } @@ -275,7 +273,6 @@ table 8060 "Billing Template" DocumentDate: Date; BillingDate: Date; BillingToDate: Date; - GroupBy: Enum "Contract Billing Grouping"; begin CalculateBillingDates(BillingDate, BillingToDate, true); BillingProposal.CreateBillingProposal(Code, BillingDate, BillingToDate, true); From 48fd6165ea1ae43b3dfe6211881db98e9d7af0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Wed, 26 Nov 2025 12:09:45 +0100 Subject: [PATCH 09/10] Code Cop fixes --- .../Billing/Codeunits/BillingProposal.Codeunit.al | 15 +++++---------- .../Codeunits/CreateBillingDocuments.Codeunit.al | 6 +++--- 2 files changed, 8 insertions(+), 13 deletions(-) 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 23d3bfd99b..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 @@ -154,7 +154,6 @@ codeunit 8062 "Billing Proposal" BillingTemplate: Record "Billing Template"; CustomerContract: Record "Customer Subscription Contract"; VendorContract: Record "Vendor Subscription Contract"; - ContractBillingErrLog: Record "Contract Billing Err. Log"; FilterText: Text; BillingRhythmFilterText: Text; begin @@ -890,7 +889,7 @@ codeunit 8062 "Billing Proposal" BillingTemplate: Record "Billing Template"; DeleteBillingDocumentQst: Label 'Which contract billing documents should be deleted?'; DeleteBillingDocumentOptionsTxt: Label 'All Documents,Documents from current billing template only'; - DeleteBillingDocumentOptionsMySuggestionsOnlyTxt: Label 'All Documents (user %1 only),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 DisplayErrorIfNotAuthorizedToClearProposalOrDeleteDocuments(); @@ -934,15 +933,11 @@ codeunit 8062 "Billing Proposal" begin case BillingLine.Partner of BillingLine.Partner::Customer: - begin - if SalesHeader.Get(BillingLine.GetSalesDocumentTypeFromBillingDocumentType(), BillingLine."Document No.") then - SalesHeader.Delete(true); - end; + if SalesHeader.Get(BillingLine.GetSalesDocumentTypeFromBillingDocumentType(), BillingLine."Document No.") then + SalesHeader.Delete(true); BillingLine.Partner::Vendor: - begin - if PurchaseHeader.Get(BillingLine.GetPurchaseDocumentTypeFromBillingDocumentType(), BillingLine."Document No.") then - PurchaseHeader.Delete(true); - end; + 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 ab98c7e8c0..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 @@ -806,7 +806,7 @@ codeunit 8060 "Create Billing Documents" end; end; - local procedure CheckBillingLines(var BillingLine: Record "Billing Line") Success: Boolean + local procedure CheckBillingLines(var BillingLine: Record "Billing Line"): Boolean begin if not CheckOnlyOneServicePartnerType(BillingLine) then exit(false); @@ -817,7 +817,7 @@ codeunit 8060 "Create Billing Documents" exit(true); end; - local procedure CheckOnlyOneServicePartnerType(var BillingLine: Record "Billing Line") Success: Boolean + local procedure CheckOnlyOneServicePartnerType(var BillingLine: Record "Billing Line"): Boolean begin if BillingLine.FindSet() then repeat @@ -837,7 +837,7 @@ codeunit 8060 "Create Billing Documents" exit(true); end; - local procedure CheckNoUpdateRequired(var BillingLine: Record "Billing Line") Success: Boolean + local procedure CheckNoUpdateRequired(var BillingLine: Record "Billing Line"): Boolean begin BillingLine.SetRange("Update Required", true); if BillingLine.FindFirst() then begin From 910ee2edd085791e6b7271920118e329a0b19276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Wed, 26 Nov 2025 12:11:43 +0100 Subject: [PATCH 10/10] Code Cop fixes --- .../App/Base/Tables/SubscriptionBillingCue.Table.al | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 81126483d8..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 @@ -122,10 +122,10 @@ table 8070 "Subscription Billing Cue" ObsoleteReason = 'Removed as projects are not relevant in context of Subscription Billing'; #if not CLEAN27 ObsoleteState = Pending; - ObsoleteTag = '27.0'; + ObsoleteTag = '28.0'; #else ObsoleteState = Removed; - ObsoleteTag = '30.0'; + ObsoleteTag = '31.0'; #endif } #endif