diff --git a/src/Apps/W1/Subscription Billing/App/Base/Codeunits/ProgressTracker.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Base/Codeunits/ProgressTracker.Codeunit.al new file mode 100644 index 00000000000..09e01da5d2b --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Base/Codeunits/ProgressTracker.Codeunit.al @@ -0,0 +1,143 @@ +namespace Microsoft.SubscriptionBilling; + +/// +/// Reusable progress dialog helper for long-running batch processes. +/// Shows elapsed time, progress (processed/total), estimated time remaining (ETA) and throughput. +/// All dialog interaction is guarded by GuiAllowed, so callers are safe to use it from background +/// Job Queue sessions without additional guards. +/// +codeunit 8035 "Progress Tracker" +{ + var + Window: Dialog; + StartTime: DateTime; + LastUpdateTime: DateTime; + TotalCount: Integer; + LastDetail: Text; + IsOpen: Boolean; + UpdateIntervalMs: Integer; + DialogTxt: Label '#1#################################\Processing #2###############################\Progress #3###############################\Elapsed #4###############################\Est. remaining #5############################\Throughput #6###############################', Comment = '%1 = activity caption, %2 = current item detail, %3 = progress (processed of total), %4 = elapsed time, %5 = estimated time remaining, %6 = throughput'; + ProgressLbl: Label '%1 of %2 (%3%)', Comment = '%1 = processed count, %2 = total count, %3 = percentage'; + PerMinuteLbl: Label '%1 / min', Comment = '%1 = number of items processed per minute'; + + /// + /// Opens the progress dialog (only when GuiAllowed) and starts the elapsed-time clock. + /// + /// Caption describing the running activity. + /// Total number of items to process; used for ETA and percentage. Pass 0 if unknown. + procedure StartActivity(ActivityText: Text; NewTotalCount: Integer) + begin + StartTime := CurrentDateTime(); + LastUpdateTime := StartTime; + TotalCount := NewTotalCount; + LastDetail := ''; + UpdateIntervalMs := 1000; // Redraw the dialog at most once per second. + if not GuiAllowed() then + exit; + Window.Open(DialogTxt); + Window.Update(1, ActivityText); + IsOpen := true; + end; + + /// + /// Updates the dialog. Redraws are throttled (at most once per second, plus whenever the detail + /// text changes or the last item is reached) to avoid excessive flicker. + /// + /// Number of items processed so far. + /// Detail about the current item (e.g. contract no., partner, step). + procedure UpdateProgress(ProcessedCount: Integer; DetailText: Text) + begin + if not IsOpen then + exit; + if not ShouldUpdate(ProcessedCount, DetailText) then + exit; + LastDetail := DetailText; + LastUpdateTime := CurrentDateTime(); + Window.Update(2, DetailText); + Window.Update(3, FormatProgress(ProcessedCount)); + Window.Update(4, FormatDurationText(CurrentDateTime() - StartTime)); + Window.Update(5, FormatEta(ProcessedCount)); + Window.Update(6, FormatThroughput(ProcessedCount)); + end; + + /// + /// Closes the progress dialog if it is open. + /// + procedure Finish() + begin + if IsOpen then + Window.Close(); + IsOpen := false; + end; + + local procedure ShouldUpdate(ProcessedCount: Integer; DetailText: Text): Boolean + begin + if DetailText <> LastDetail then + exit(true); + if (TotalCount > 0) and (ProcessedCount >= TotalCount) then + exit(true); + exit((CurrentDateTime() - LastUpdateTime) >= UpdateIntervalMs); + end; + + local procedure FormatProgress(ProcessedCount: Integer): Text + var + Percentage: Integer; + begin + if TotalCount <= 0 then + exit(Format(ProcessedCount)); + Percentage := Round(ProcessedCount / TotalCount * 100, 1); + exit(StrSubstNo(ProgressLbl, ProcessedCount, TotalCount, Percentage)); + end; + + local procedure FormatEta(ProcessedCount: Integer): Text + var + Elapsed: Duration; + begin + if (ProcessedCount <= 0) or (TotalCount <= 0) or (ProcessedCount >= TotalCount) then + exit('-'); + Elapsed := CurrentDateTime() - StartTime; + exit(FormatDurationText(Round(Elapsed / ProcessedCount * (TotalCount - ProcessedCount), 1))); + end; + + local procedure FormatThroughput(ProcessedCount: Integer): Text + var + Elapsed: Duration; + PerMinute: Decimal; + begin + Elapsed := CurrentDateTime() - StartTime; + if Elapsed <= 0 then + exit('-'); + PerMinute := Round(ProcessedCount / (Elapsed / 60000), 0.1); + exit(StrSubstNo(PerMinuteLbl, Format(PerMinute, 0, ''))); + end; + + local procedure FormatDurationText(DurationValue: Duration): Text + var + TotalSeconds: Integer; + Days: Integer; + Hours: Integer; + Minutes: Integer; + Seconds: Integer; + HH: Text; + MM: Text; + SS: Text; + begin + if DurationValue <= 0 then + exit('0s'); + if DurationValue < 1000 then + exit('<1s'); + TotalSeconds := Round(DurationValue / 1000, 1); + Days := TotalSeconds div 86400; + TotalSeconds := TotalSeconds mod 86400; + Hours := TotalSeconds div 3600; + TotalSeconds := TotalSeconds mod 3600; + Minutes := TotalSeconds div 60; + Seconds := TotalSeconds mod 60; + HH := PadStr('', 2 - StrLen(Format(Hours)), '0') + Format(Hours); + MM := PadStr('', 2 - StrLen(Format(Minutes)), '0') + Format(Minutes); + SS := PadStr('', 2 - StrLen(Format(Seconds)), '0') + Format(Seconds); + if Days > 0 then + exit(Format(Days) + 'd ' + HH + ':' + MM + ':' + SS); + exit(HH + ':' + MM + ':' + SS); + 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 81bfa4d6c09..e37c31c4c88 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 @@ -12,8 +12,11 @@ codeunit 8062 "Billing Proposal" var SalesHeaderGlobal: Record "Sales Header"; PurchaseHeaderGlobal: Record "Purchase Header"; + ProgressTracker: Codeunit "Progress Tracker"; LastContractNo: Code[20]; LastPartnerNo: Code[20]; + CreatingProposalLbl: Label 'Creating billing proposal...'; + ContractDetailLbl: Label 'Contract %1', Comment = '%1 = Subscription Contract No.'; BillingToChangeNotAllowedErr: Label 'A change of Billing to field from %1 to %2 for %3 and %4 is not allowed because the Subscription Line has already been calculated up to %5.', Comment = '%1: Old Billing To Date, %2: New Billing To Date, %3: Subscription Contract No., %4: Subscription Contract Line No., %5: Last Billing To Date'; 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.'; @@ -26,6 +29,9 @@ codeunit 8062 "Billing Proposal" 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.'; + ClearingBillingProposalLbl: Label 'Clearing billing proposal...'; + DeletingBillingLinesLbl: Label 'Deleting billing lines...'; + DeletingDocumentsLbl: Label 'Deleting billing documents...'; procedure InitTempTable(var TempBillingLine: Record "Billing Line" temporary; GroupBy: Enum "Contract Billing Grouping") var @@ -60,6 +66,87 @@ codeunit 8062 "Billing Proposal" OnAfterInitTempTable(TempBillingLine, GroupBy); end; + /// + /// Rebuilds only the temporary rows of the groups touched by a selection-scoped action (for example Delete + /// Billing Line or Change Billing To Date), instead of re-reading every Billing Line as InitTempTable does. + /// Each affected group is dropped and rebuilt from the database, so the result is identical to a full rebuild + /// for those groups - but the cost scales with the affected groups, not with the whole proposal. GroupKeys holds + /// the Subscription Contract No. of each affected group, or the Partner No. when grouping by "Contract Partner". + /// + internal procedure RefreshGroupsInTempTable(var TempBillingLine: Record "Billing Line" temporary; GroupBy: Enum "Contract Billing Grouping"; GroupKeys: List of [Code[20]]) + var + PageFilterBillingLine: Record "Billing Line"; + TempPageFilter: Record "Billing Line" temporary; + GroupKey: Code[20]; + NextHeaderEntryNo: Integer; + begin + if GroupKeys.Count = 0 then + exit; + + TempPageFilter.CopyFilters(TempBillingLine); // remember the page view filters so they can be restored + PageFilterBillingLine.CopyFilters(TempBillingLine); // page view filters (Partner, User ID) scope the DB read + NextHeaderEntryNo := SmallestTempBillingLineEntryNo(TempBillingLine); + + foreach GroupKey in GroupKeys do begin + DeleteTempRowsForGroup(TempBillingLine, GroupBy, GroupKey); + RebuildTempRowsForGroup(TempBillingLine, PageFilterBillingLine, GroupBy, GroupKey, NextHeaderEntryNo); + end; + + TempBillingLine.CopyFilters(TempPageFilter); + end; + + local procedure SmallestTempBillingLineEntryNo(var TempBillingLine: Record "Billing Line" temporary): Integer + begin + // Group header rows use descending negative Entry No.; continue below the current minimum to avoid collisions. + TempBillingLine.Reset(); + if TempBillingLine.FindFirst() then // primary key ascending -> smallest Entry No. is first + if TempBillingLine."Entry No." < 0 then + exit(TempBillingLine."Entry No."); + exit(0); + end; + + local procedure DeleteTempRowsForGroup(var TempBillingLine: Record "Billing Line" temporary; GroupBy: Enum "Contract Billing Grouping"; GroupKey: Code[20]) + begin + TempBillingLine.Reset(); + case GroupBy of + GroupBy::"Contract Partner": + TempBillingLine.SetRange("Partner No.", GroupKey); + else + TempBillingLine.SetRange("Subscription Contract No.", GroupKey); + end; + TempBillingLine.DeleteAll(false); + TempBillingLine.Reset(); + end; + + local procedure RebuildTempRowsForGroup(var TempBillingLine: Record "Billing Line" temporary; var PageFilterBillingLine: Record "Billing Line"; GroupBy: Enum "Contract Billing Grouping"; GroupKey: Code[20]; var NextHeaderEntryNo: Integer) + var + BillingLine: Record "Billing Line"; + TempGroupBillingLine: Record "Billing Line" temporary; + begin + BillingLine.CopyFilters(PageFilterBillingLine); + SetKeysForGrouping(BillingLine, TempBillingLine, GroupBy); // also resets Last* so the first row opens a new group + case GroupBy of + GroupBy::"Contract Partner": + BillingLine.SetRange("Partner No.", GroupKey); + else + BillingLine.SetRange("Subscription Contract No.", GroupKey); + end; + if BillingLine.FindSet() then + repeat + UpdateGroupingLine(TempGroupBillingLine, BillingLine, GroupBy); + TempBillingLine := BillingLine; + TempBillingLine.Indent := 1; + TempBillingLine.Insert(false); + until BillingLine.Next() = 0; + if TempGroupBillingLine.FindSet() then + repeat + TempBillingLine := TempGroupBillingLine; + NextHeaderEntryNo -= 1; + TempBillingLine."Entry No." := NextHeaderEntryNo; + TempBillingLine.Insert(false); + until TempGroupBillingLine.Next() = 0; + end; + local procedure SetKeysForGrouping(var BillingLine: Record "Billing Line"; var TempBillingLine: Record "Billing Line" temporary; GroupBy: Enum "Contract Billing Grouping") begin case GroupBy of @@ -156,8 +243,11 @@ codeunit 8062 "Billing Proposal" VendorContract: Record "Vendor Subscription Contract"; FilterText: Text; BillingRhythmFilterText: Text; + ContractCounter: Integer; + CommitBatchSize: Integer; begin SalesHeaderGlobal.Reset(); + CommitBatchSize := 50; // Contracts processed per transaction checkpoint BillingTemplate.Get(BillingTemplateCode); if BillingDate = 0D then Error(NoBillingDateErr); @@ -175,24 +265,38 @@ codeunit 8062 "Billing Proposal" if FilterText <> '' then CustomerContract.SetView(FilterText); BillingRhythmFilterText := CustomerContract.GetFilter("Billing Rhythm Filter"); + this.ProgressTracker.StartActivity(CreatingProposalLbl, CustomerContract.Count()); if CustomerContract.FindSet() then repeat + ContractCounter += 1; + this.ProgressTracker.UpdateProgress(ContractCounter, StrSubstNo(ContractDetailLbl, CustomerContract."No.")); ProcessContractServiceCommitments(BillingTemplate, CustomerContract."No.", '', BillingDate, BillingToDate, BillingRhythmFilterText, AutomatedBilling); + if ContractCounter mod CommitBatchSize = 0 then + Commit(); until CustomerContract.Next() = 0; + this.ProgressTracker.Finish(); end; "Service Partner"::Vendor: begin if FilterText <> '' then VendorContract.SetView(FilterText); BillingRhythmFilterText := VendorContract.GetFilter("Billing Rhythm Filter"); + this.ProgressTracker.StartActivity(CreatingProposalLbl, VendorContract.Count()); if VendorContract.FindSet() then repeat + ContractCounter += 1; + this.ProgressTracker.UpdateProgress(ContractCounter, StrSubstNo(ContractDetailLbl, VendorContract."No.")); ProcessContractServiceCommitments(BillingTemplate, VendorContract."No.", '', BillingDate, BillingToDate, BillingRhythmFilterText, AutomatedBilling); + if ContractCounter mod CommitBatchSize = 0 then + Commit(); until VendorContract.Next() = 0; + this.ProgressTracker.Finish(); end; end; - if AutomatedBilling then + // The remaining block only surfaces blocking credit memos to the user, so there is nothing to do + // on the automated/background path or when no UI is available. + if AutomatedBilling or not GuiAllowed() then exit; case BillingTemplate.Partner of @@ -258,7 +362,7 @@ codeunit 8062 "Billing Proposal" case BillingLine.Partner of Enum::"Service Partner"::Customer: if SalesHeaderGlobal.Get(SalesHeaderGlobal."Document Type"::"Credit Memo", BillingLine."Document No.") then - if AutomatedBilling then + if AutomatedBilling or not GuiAllowed() then ContractBillingErrLog.InsertLogFromSubscriptionLine( BillingTemplate.Code, ServiceCommitment, @@ -267,7 +371,7 @@ codeunit 8062 "Billing Proposal" SalesHeaderGlobal.Mark(true); Enum::"Service Partner"::Vendor: if PurchaseHeaderGlobal.Get(PurchaseHeaderGlobal."Document Type"::"Credit Memo", BillingLine."Document No.") then - if AutomatedBilling then + if AutomatedBilling or not GuiAllowed() then ContractBillingErrLog.InsertLogFromSubscriptionLine( BillingTemplate.Code, ServiceCommitment, @@ -279,9 +383,9 @@ codeunit 8062 "Billing Proposal" ServiceCommitment."Usage Based Billing": begin UsageDataBilling.Reset(); - UsageDataBilling.SetCurrentKey("Usage Data Import Entry No.", "Subscription Line Entry No.", Partner, "Document Type", "Charge End Date"); UsageDataBilling.FilterOnServiceCommitment(ServiceCommitment); UsageDataBilling.SetRange("Document Type", "Usage Based Billing Doc. Type"::None); + OnProcessSubscriptionLineOnAfterFilterUsageDataBilling(UsageDataBilling, ServiceCommitment); SkipServiceCommitment := UsageDataBilling.IsEmpty(); end; else @@ -447,6 +551,7 @@ codeunit 8062 "Billing Proposal" case ServiceCommitment.Partner of ServiceCommitment.Partner::Customer: begin + CustomerContract.SetLoadFields("Sell-to Customer No.", "Detail Overview", "Currency Code"); CustomerContract.Get(ServiceCommitment."Subscription Contract No."); BillingLine."Partner No." := CustomerContract."Sell-to Customer No."; BillingLine."Detail Overview" := CustomerContract."Detail Overview"; @@ -454,6 +559,7 @@ codeunit 8062 "Billing Proposal" end; ServiceCommitment.Partner::Vendor: begin + VendorContract.SetLoadFields("Pay-to Vendor No.", "Currency Code"); VendorContract.Get(ServiceCommitment."Subscription Contract No."); BillingLine."Partner No." := VendorContract."Pay-to Vendor No."; BillingLine."Currency Code" := VendorContract."Currency Code"; @@ -463,6 +569,7 @@ codeunit 8062 "Billing Proposal" BillingLine."Subscription Contract Line No." := ServiceCommitment."Subscription Contract Line No."; BillingLine."Discount %" := ServiceCommitment."Discount %"; BillingLine.Discount := ServiceCommitment.Discount; + ServiceObject.SetLoadFields(Quantity); ServiceObject.Get(ServiceCommitment."Subscription Header No."); BillingLine."Service Object Quantity" := BillingLine.GetSign() * ServiceObject.Quantity; OnAfterUpdateBillingLineFromSubscriptionLine(BillingLine, ServiceCommitment); @@ -553,6 +660,7 @@ codeunit 8062 "Billing 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; + Counter: Integer; begin DisplayErrorIfNotAuthorizedToClearProposalOrDeleteDocuments(); BillingTemplate.Get(BillingTemplateCode); @@ -570,24 +678,44 @@ codeunit 8062 "Billing Proposal" BillingLine.SetRange(Partner, BillingTemplate.Partner); if BillingTemplate."My Suggestions Only" then BillingLine.SetRange("User ID", UserId()); - BillingLine.DeleteAll(true); + this.ProgressTracker.StartActivity(ClearingBillingProposalLbl, BillingLine.Count()); + if BillingLine.FindSet() then + repeat + Counter += 1; + this.ProgressTracker.UpdateProgress(Counter, BillingLine."Subscription Contract No."); + BillingLine.Delete(true); + until BillingLine.Next() = 0; + this.ProgressTracker.Finish(); end; 2: begin BillingLine.SetRange("Billing Template Code", BillingTemplate.Code); - BillingLine.DeleteAll(true); + this.ProgressTracker.StartActivity(ClearingBillingProposalLbl, BillingLine.Count()); + if BillingLine.FindSet() then + repeat + Counter += 1; + this.ProgressTracker.UpdateProgress(Counter, BillingLine."Subscription Contract No."); + BillingLine.Delete(true); + until BillingLine.Next() = 0; + this.ProgressTracker.Finish(); end; end; end; internal procedure DeleteBillingLines(var BillingLine: Record "Billing Line") + var + Counter: Integer; begin BillingLine.SetCurrentKey("Subscription Header No.", "Subscription Line Entry No.", "Billing to"); BillingLine.SetAscending("Billing to", false); + this.ProgressTracker.StartActivity(DeletingBillingLinesLbl, BillingLine.Count()); if BillingLine.FindSet() then repeat - BillingLine.Delete(true) + Counter += 1; + this.ProgressTracker.UpdateProgress(Counter, BillingLine."Subscription Contract No."); + BillingLine.Delete(true); until BillingLine.Next() = 0; + this.ProgressTracker.Finish(); end; local procedure DeleteBillingLinesForServiceObject(var BillingLine: Record "Billing Line") @@ -890,17 +1018,21 @@ codeunit 8062 "Billing Proposal" 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', Comment = '%1: User ID'; + PreviousDocumentNo: Code[20]; StrMenuResponse: Integer; + Counter: Integer; begin + PreviousDocumentNo := ''; DisplayErrorIfNotAuthorizedToClearProposalOrDeleteDocuments(); BillingTemplate.Get(BillingTemplateCode); if BillingTemplate."My Suggestions Only" then StrMenuResponse := Dialog.StrMenu(StrSubstNo(DeleteBillingDocumentOptionsMySuggestionsOnlyTxt, UserId()), 1, DeleteBillingDocumentQst) else StrMenuResponse := Dialog.StrMenu(DeleteBillingDocumentOptionsTxt, 1, DeleteBillingDocumentQst); - BillingLine.SetLoadFields("Subscription Header No.", "Subscription Line Entry No.", "Billing to", "Document Type", "Document No.", "Partner", "User ID"); - BillingLine.SetCurrentKey("Subscription Header No.", "Subscription Line Entry No.", "Billing to"); - BillingLine.SetAscending("Billing to", false); + // Sort by document so same-document billing lines are adjacent; the loop skips duplicate document nos so each + // document is deleted exactly once and the progress counter reflects documents, not billing lines. + BillingLine.SetLoadFields("Document Type", "Document No.", "Partner", "User ID", "Billing Template Code"); + BillingLine.SetCurrentKey("Document Type", "Document No."); BillingLine.SetFilter("Document No.", '<>%1', ''); case StrMenuResponse of 0: @@ -910,22 +1042,52 @@ codeunit 8062 "Billing Proposal" BillingLine.SetRange(Partner, BillingTemplate.Partner); if BillingTemplate."My Suggestions Only" then BillingLine.SetRange("User ID", UserId()); + this.ProgressTracker.StartActivity(DeletingDocumentsLbl, this.CountDistinctDocuments(BillingLine)); if BillingLine.FindSet() then repeat - DeleteBillingDocuments(BillingLine); + if BillingLine."Document No." <> PreviousDocumentNo then begin + Counter += 1; + this.ProgressTracker.UpdateProgress(Counter, BillingLine."Document No."); + this.DeleteBillingDocuments(BillingLine); + PreviousDocumentNo := BillingLine."Document No."; + end; until BillingLine.Next() = 0; + this.ProgressTracker.Finish(); end; 2: begin BillingLine.SetRange("Billing Template Code", BillingTemplate.Code); + this.ProgressTracker.StartActivity(DeletingDocumentsLbl, this.CountDistinctDocuments(BillingLine)); if BillingLine.FindSet() then repeat - DeleteBillingDocuments(BillingLine); + if BillingLine."Document No." <> PreviousDocumentNo then begin + Counter += 1; + this.ProgressTracker.UpdateProgress(Counter, BillingLine."Document No."); + this.DeleteBillingDocuments(BillingLine); + PreviousDocumentNo := BillingLine."Document No."; + end; until BillingLine.Next() = 0; + this.ProgressTracker.Finish(); end; end; end; + local procedure CountDistinctDocuments(var BillingLine: Record "Billing Line"): Integer + var + PrevDocumentNo: Code[20]; + DocCount: Integer; + begin + PrevDocumentNo := ''; + if BillingLine.FindSet() then + repeat + if BillingLine."Document No." <> PrevDocumentNo then begin + DocCount += 1; + PrevDocumentNo := BillingLine."Document No."; + end; + until BillingLine.Next() = 0; + exit(DocCount); + end; + local procedure DeleteBillingDocuments(BillingLine: Record "Billing Line") var SalesHeader: Record "Sales Header"; @@ -991,6 +1153,11 @@ codeunit 8062 "Billing Proposal" begin end; + [IntegrationEvent(false, false)] + local procedure OnProcessSubscriptionLineOnAfterFilterUsageDataBilling(var UsageDataBilling: Record "Usage Data Billing"; SubscriptionLine: Record "Subscription Line") + begin + end; + [IntegrationEvent(false, false)] local procedure OnCreateBillingProposalBeforeApplyFilterToContract(var FilterText: Text; var BillingTemplate: Record "Billing Template"; BillingDate: Date; BillingToDate: Date) begin 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 f3c64b48292..aa6e1cc727a 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 @@ -34,18 +34,15 @@ codeunit 8060 "Create Billing Documents" if not RequestPageSelectionConfirmed() then exit; - 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 PostCreatedDocuments(); - if not HideProcessingFinishedMessage then + if (not HideProcessingFinishedMessage) and GuiAllowed() then ProcessingFinishedMessage(); end; @@ -53,6 +50,9 @@ codeunit 8060 "Create Billing Documents" begin OnBeforeProcessBillingLines(BillingLine, DocumentDate, PostingDate, CustomerRecurringBillingGrouping, VendorRecurringBillingGrouping, PostDocuments); CreateTempBillingLines(BillingLine); + TotalBillingLineCount := TempBillingLine.Count(); + ProcessedBillingLineCount := 0; + this.ProgressTracker.StartActivity(CreatingDocumentsLbl, TotalBillingLineCount); case BillingLine.Partner of BillingLine.Partner::Customer: case CustomerRecurringBillingGrouping of @@ -71,6 +71,7 @@ codeunit 8060 "Create Billing Documents" CreatePurchaseDocumentsPerVendor(); end; end; + this.ProgressTracker.Finish(); OnAfterProcessBillingLines(BillingLine); end; @@ -89,17 +90,21 @@ codeunit 8060 "Create Billing Documents" repeat if IsNewHeaderNeededPerContract(PreviousContractNo) then begin TestPreviousDocumentTotalInvoiceAmount(true, DiscountLineExists, PreviousContractNo); + CommitCreatedDocumentCheckpoint(); + TrimTempBillingLinesForContract(PreviousContractNo); CustomerContract.Get(TempBillingLine."Subscription Contract No."); CreateSalesHeaderFromContract(CustomerContract); InsertContractDescriptionSalesLines(TempBillingLine); PreviousContractNo := TempBillingLine."Subscription Contract No."; ContractsProcessedCount += 1; - Window.Update(1, CustomerContract."Sell-to Customer No."); - Window.Update(2, PreviousContractNo); + SetProgressPartner(CustomerContract."Sell-to Customer No."); + SetProgressContract(PreviousContractNo); end; InsertSalesLineFromTempBillingLine(); + UpdateBillingProgress(); until TempBillingLine.Next() = 0; TestPreviousDocumentTotalInvoiceAmount(true, DiscountLineExists, PreviousContractNo); + CommitCreatedDocumentCheckpoint(); end; local procedure CreatePurchaseDocumentsPerContract() @@ -117,17 +122,21 @@ codeunit 8060 "Create Billing Documents" repeat if IsNewHeaderNeededPerContract(PreviousContractNo) then begin TestPreviousDocumentTotalInvoiceAmount(false, DiscountLineExists, PreviousContractNo); + CommitCreatedDocumentCheckpoint(); + TrimTempBillingLinesForContract(PreviousContractNo); VendorContract.Get(TempBillingLine."Subscription Contract No."); CreatePurchaseHeaderFromContract(VendorContract); InsertContractDescriptionPurchaseLines(TempBillingLine); PreviousContractNo := TempBillingLine."Subscription Contract No."; ContractsProcessedCount += 1; - Window.Update(1, VendorContract."Pay-to Vendor No."); - Window.Update(2, PreviousContractNo); + SetProgressPartner(VendorContract."Pay-to Vendor No."); + SetProgressContract(PreviousContractNo); end; InsertPurchaseLineFromTempBillingLine(); + UpdateBillingProgress(); until TempBillingLine.Next() = 0; TestPreviousDocumentTotalInvoiceAmount(false, DiscountLineExists, PreviousContractNo); + CommitCreatedDocumentCheckpoint(); end; local procedure CreateSalesDocumentsPerCustomer() @@ -149,13 +158,14 @@ codeunit 8060 "Create Billing Documents" repeat if IsNewSalesHeaderNeeded(PreviousCustomerNo, LastDetailOverview, PreviousCurrencyCode, PreviousContractNo) then begin TestPreviousDocumentTotalInvoiceAmount(true, DiscountLineExists, PreviousContractNo); + CommitCreatedDocumentCheckpoint(); CreateSalesHeaderForCustomerNo(TempBillingLine."Partner No."); SalesHeader."Sub. Contract Detail Overview" := TempBillingLine."Detail Overview"; SalesHeader.Modify(false); PreviousCustomerNo := TempBillingLine."Partner No."; LastDetailOverview := TempBillingLine."Detail Overview"; PreviousCurrencyCode := TempBillingLine."Currency Code"; - Window.Update(1, PreviousCustomerNo); + SetProgressPartner(PreviousCustomerNo); FirstContractDescriptionLineInserted := false; end; if TempBillingLine."Subscription Contract No." <> PreviousContractNo then begin @@ -166,13 +176,16 @@ codeunit 8060 "Create Billing Documents" TranslationHelper.RestoreGlobalLanguage(); SalesHeader.Modify(false); end; + TrimTempBillingLinesForContract(PreviousContractNo); PreviousContractNo := TempBillingLine."Subscription Contract No."; ContractsProcessedCount += 1; - Window.Update(2, PreviousContractNo); + SetProgressContract(PreviousContractNo); end; InsertSalesLineFromTempBillingLine(); + UpdateBillingProgress(); until TempBillingLine.Next() = 0; TestPreviousDocumentTotalInvoiceAmount(true, DiscountLineExists, PreviousContractNo); + CommitCreatedDocumentCheckpoint(); end; local procedure CreatePurchaseDocumentsPerVendor() @@ -195,10 +208,11 @@ codeunit 8060 "Create Billing Documents" (TempBillingLine."Currency Code" <> PreviousCurrencyCode) then begin TestPreviousDocumentTotalInvoiceAmount(false, DiscountLineExists, PreviousContractNo); + CommitCreatedDocumentCheckpoint(); CreatePurchaseHeaderForVendorNo(TempBillingLine."Partner No."); PreviousVendorNo := TempBillingLine."Partner No."; PreviousCurrencyCode := TempBillingLine."Currency Code"; - Window.Update(1, PreviousVendorNo); + SetProgressPartner(PreviousVendorNo); FirstContractDescriptionLineInserted := false; end; if TempBillingLine."Subscription Contract No." <> PreviousContractNo then begin @@ -209,13 +223,16 @@ codeunit 8060 "Create Billing Documents" TranslationHelper.RestoreGlobalLanguage(); PurchaseHeader.Modify(false); end; + TrimTempBillingLinesForContract(PreviousContractNo); PreviousContractNo := TempBillingLine."Subscription Contract No."; ContractsProcessedCount += 1; - Window.Update(2, PreviousContractNo); + SetProgressContract(PreviousContractNo); end; InsertPurchaseLineFromTempBillingLine(); + UpdateBillingProgress(); until TempBillingLine.Next() = 0; TestPreviousDocumentTotalInvoiceAmount(false, DiscountLineExists, PreviousContractNo); + CommitCreatedDocumentCheckpoint(); end; local procedure InsertSalesLineFromTempBillingLine() @@ -247,15 +264,18 @@ codeunit 8060 "Create Billing Documents" end else begin SalesLine.Validate(Type, ServiceObject.GetSalesLineType()); SalesLine.Validate("No.", ServiceObject."Source No."); - SalesLine.Validate("Variant Code", ServiceObject."Variant Code"); if Item.Get(ServiceObject."Source No.") then if Item.IsVariantMandatory() then ServiceObject.TestField("Variant Code"); + if ServiceObject."Variant Code" <> '' then + SalesLine.Validate("Variant Code", ServiceObject."Variant Code"); end; SubContractsItemManagement.SetAllowInsertOfInvoicingItem(false); - SalesLine.Validate("Unit of Measure Code", ServiceObject."Unit of Measure"); + if SalesLine."Unit of Measure Code" <> ServiceObject."Unit of Measure" then + 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"); + // Unit Price is assigned directly; the following Validate("Line Discount %") recalculates the line amount. + SalesLine."Unit Price" := SalesLine.GetSalesDocumentSign() * TempBillingLine."Unit Price"; SalesLine.Validate("Line Discount %", TempBillingLine."Discount %"); SalesLine.Validate("Unit Cost (LCY)", TempBillingLine."Unit Cost (LCY)"); SalesLine."Recurring Billing from" := TempBillingLine."Billing from"; @@ -302,13 +322,13 @@ codeunit 8060 "Create Billing Documents" UsageDataBilling.SetRange("Subscription Contract Line No.", CustomerContractLine."Line No."); UsageDataBilling.SetRange("Document Type", Enum::"Usage Based Billing Doc. Type"::None); UsageDataBilling.SetRange("Document No.", ''); - if UsageDataBilling.FindSet() then + if UsageDataBilling.FindSet() then begin + BillingLineNoByTempEntryNo.Get(TempBillingLine."Entry No.", BillingLineNo); repeat - BillingLineNo := GetBillingLineNo(BillingLine.GetBillingDocumentTypeFromSalesDocumentType(SalesLine."Document Type"), - "Service Partner"::Customer, SalesLine."Document No.", CustomerContractLine."Subscription Contract No.", CustomerContractLine."Line No."); UsageDataBilling.SaveDocumentValues(UsageBasedDocTypeConv.ConvertSalesDocTypeToUsageBasedBillingDocType(SalesLine."Document Type"), SalesLine."Document No.", SalesLine."Line No.", BillingLineNo); until UsageDataBilling.Next() = 0; + end; end; OnAfterInsertSalesLineFromBillingLine(CustomerContractLine, SalesLine); @@ -370,12 +390,15 @@ codeunit 8060 "Create Billing Documents" end else begin PurchaseLine.Validate(Type, ServiceObject.GetPurchaseLineType()); PurchaseLine.Validate("No.", ServiceObject."Source No."); - PurchaseLine.Validate("Variant Code", ServiceObject."Variant Code"); + if ServiceObject."Variant Code" <> '' then + PurchaseLine.Validate("Variant Code", ServiceObject."Variant Code"); end; SubContractsItemManagement.SetAllowInsertOfInvoicingItem(false); - PurchaseLine.Validate("Unit of Measure Code", ServiceObject."Unit of Measure"); + if PurchaseLine."Unit of Measure Code" <> ServiceObject."Unit of Measure" then + PurchaseLine.Validate("Unit of Measure Code", ServiceObject."Unit of Measure"); PurchaseLine.Validate(Quantity, TempBillingLine.GetSign() * ServiceObject.Quantity); - PurchaseLine.Validate("Direct Unit Cost", PurchaseLine.GetPurchaseDocumentSign() * TempBillingLine."Unit Price"); + // Direct Unit Cost is assigned directly; the following Validate("Line Discount %") recalculates the line amount. + PurchaseLine."Direct Unit Cost" := PurchaseLine.GetPurchaseDocumentSign() * TempBillingLine."Unit Price"; PurchaseLine.Validate("Line Discount %", TempBillingLine."Discount %"); PurchaseLine."Recurring Billing from" := TempBillingLine."Billing from"; PurchaseLine."Recurring Billing to" := TempBillingLine."Billing to"; @@ -411,13 +434,13 @@ codeunit 8060 "Create Billing Documents" UsageDataBilling.SetRange("Subscription Contract Line No.", ServiceCommitment."Subscription Contract Line No."); UsageDataBilling.SetRange("Document Type", Enum::"Usage Based Billing Doc. Type"::None); UsageDataBilling.SetRange("Document No.", ''); - if UsageDataBilling.FindSet() then + if UsageDataBilling.FindSet() then begin + BillingLineNoByTempEntryNo.Get(TempBillingLine."Entry No.", BillingLineNo); repeat - BillingLineNo := GetBillingLineNo(BillingLine.GetBillingDocumentTypeFromPurchaseDocumentType(PurchaseLine."Document Type"), - "Service Partner"::Vendor, PurchaseLine."Document No.", ServiceCommitment."Subscription Contract No.", ServiceCommitment."Subscription Contract Line No."); UsageDataBilling.SaveDocumentValues(UsageBasedDocTypeConv.ConvertPurchaseDocTypeToUsageBasedBillingDocType(PurchaseLine."Document Type"), PurchaseLine."Document No.", PurchaseLine."Line No.", BillingLineNo); until UsageDataBilling.Next() = 0; + end; OnAfterInsertPurchaseLineFromBillingLine(ServiceCommitment, PurchaseLine); end; @@ -453,19 +476,6 @@ codeunit 8060 "Create Billing Documents" PurchLine.Validate("Line Discount %", ServiceCommitment."Discount %"); end; - local procedure GetBillingLineNo(BillingDocumentType: Enum "Rec. Billing Document Type"; ServicePartner: Enum "Service Partner"; DocumentNo: Code[20]; ContractNo: Code[20]; ContractLineNo: Integer): Integer - var - BillingLine: Record "Billing Line"; - begin - BillingLine.FilterBillingLineOnContractLine(ServicePartner, ContractNo, ContractLineNo); - BillingLine.SetRange("Document Type", BillingDocumentType); - BillingLine.SetRange("Document No.", DocumentNo); - if BillingLine.FindLast() then - exit(BillingLine."Entry No.") - else - exit(0); - end; - local procedure InitPurchaseLine(var PurchaseLine: Record "Purchase Line") begin PurchaseLine.Init(); @@ -724,16 +734,20 @@ codeunit 8060 "Create Billing Documents" var CustomerContract: Record "Customer Subscription Contract"; VendorContract: Record "Vendor Subscription Contract"; + AggregatedLineByKey: Dictionary of [Text, Integer]; CurrencyCode: Code[20]; PartnerNo: Code[20]; + LookupKey: Text; LineNo: Integer; + ExistingEntryNo: Integer; begin if BillingLine.FindSet() then repeat case BillingLine.Partner of BillingLine.Partner::Customer: begin - CustomerContract.Get(BillingLine."Subscription Contract No."); + if CustomerContract."No." <> BillingLine."Subscription Contract No." then + CustomerContract.Get(BillingLine."Subscription Contract No."); case CustomerRecurringBillingGrouping of CustomerRecurringBillingGrouping::"Sell-to Customer No.": PartnerNo := CustomerContract."Sell-to Customer No."; @@ -744,7 +758,8 @@ codeunit 8060 "Create Billing Documents" end; BillingLine.Partner::Vendor: begin - VendorContract.Get(BillingLine."Subscription Contract No."); + if VendorContract."No." <> BillingLine."Subscription Contract No." then + VendorContract.Get(BillingLine."Subscription Contract No."); case VendorRecurringBillingGrouping of VendorRecurringBillingGrouping::"Pay-to Vendor No.": PartnerNo := VendorContract."Pay-to Vendor No."; @@ -755,11 +770,13 @@ codeunit 8060 "Create Billing Documents" end; end; - TempBillingLine.SetRange("Subscription Contract No.", BillingLine."Subscription Contract No."); - TempBillingLine.SetRange("Subscription Header No.", BillingLine."Subscription Header No."); - TempBillingLine.SetRange("Subscription Line Entry No.", BillingLine."Subscription Line Entry No."); - TempBillingLine.SetRange(Rebilling, BillingLine.Rebilling); - if not TempBillingLine.FindFirst() then begin + // Aggregate by Subscription Line + Rebilling. The Subscription Line (Entry No.) already determines + // its Contract and Subscription, so those are not part of the key. Regular and rebilling charges + // for the same line stay separate because they carry different document/sign semantics. The + // in-memory index maps each group key to its temp Entry No., so the matching aggregated line is + // fetched by primary key (Get) - fast even when the temp table holds millions of rows. + LookupKey := Format(BillingLine."Subscription Line Entry No.") + '|' + Format(BillingLine.Rebilling); + if not AggregatedLineByKey.Get(LookupKey, ExistingEntryNo) then begin TempBillingLine.Init(); LineNo += 1; TempBillingLine."Entry No." := LineNo; @@ -777,7 +794,9 @@ codeunit 8060 "Create Billing Documents" TempBillingLine.Rebilling := BillingLine.Rebilling; OnBeforeInsertTempBillingLine(TempBillingLine, BillingLine); TempBillingLine.Insert(false); - end; + AggregatedLineByKey.Add(LookupKey, LineNo); + end else + TempBillingLine.Get(ExistingEntryNo); TempBillingLine."Unit Price" += BillingLine."Unit Price"; TempBillingLine.Amount += BillingLine.Amount; TempBillingLine.Discount := BillingLine.Discount; @@ -789,6 +808,7 @@ codeunit 8060 "Create Billing Documents" OnCreateTempBillingLinesBeforeSaveTempBillingLine(TempBillingLine, BillingLine); TempBillingLine."Unit Cost" += BillingLine."Unit Cost"; TempBillingLine."Unit Cost (LCY)" += BillingLine."Unit Cost (LCY)"; + BillingLineNoByTempEntryNo.Set(TempBillingLine."Entry No.", BillingLine."Entry No."); TempBillingLine.Modify(false); until BillingLine.Next() = 0; end; @@ -829,27 +849,35 @@ codeunit 8060 "Create Billing Documents" end; local procedure CheckBillingLines(var BillingLine: Record "Billing Line"): Boolean + var + ShouldCheckDataConsistency: Boolean; + ShouldCheckItemUnitOfMeasures: Boolean; begin if not CheckOnlyOneServicePartnerType(BillingLine) then exit(false); if not CheckNoUpdateRequired(BillingLine) then exit(false); - CheckServiceCommitmentDataConsistency(BillingLine); - CheckItemUnitOfMeasureForInvoicingItems(BillingLine); + ShouldCheckDataConsistency := true; + ShouldCheckItemUnitOfMeasures := true; + OnCheckBillingLinesOnBeforeDataChecks(BillingLine, ShouldCheckDataConsistency, ShouldCheckItemUnitOfMeasures); + if ShouldCheckDataConsistency or ShouldCheckItemUnitOfMeasures then begin + this.ProgressTracker.StartActivity(CheckingBillingLinesLbl, BillingLine.Count()); + CheckBillingLineData(BillingLine, ShouldCheckDataConsistency, ShouldCheckItemUnitOfMeasures); + this.ProgressTracker.Finish(); + end; exit(true); end; local procedure CheckOnlyOneServicePartnerType(var BillingLine: Record "Billing Line"): Boolean + var + PartnerBillingLine: Record "Billing Line"; begin - if BillingLine.FindSet() then - repeat - case BillingLine.Partner of - BillingLine.Partner::Customer: - CustomerBillingLinesFound := true; - BillingLine.Partner::Vendor: - VendorBillingLinesFound := true; - end; - until BillingLine.Next() = 0; + PartnerBillingLine.CopyFilters(BillingLine); + PartnerBillingLine.SetRange(Partner, Enum::"Service Partner"::Customer); + CustomerBillingLinesFound := not PartnerBillingLine.IsEmpty(); + + PartnerBillingLine.SetRange(Partner, Enum::"Service Partner"::Vendor); + VendorBillingLinesFound := not PartnerBillingLine.IsEmpty(); if (CustomerBillingLinesFound and VendorBillingLinesFound) then begin DisplayOrLogUnspecificError(OnlyOneServicePartnerErr); @@ -870,22 +898,49 @@ codeunit 8060 "Create Billing Documents" exit(true); end; - local procedure CheckServiceCommitmentDataConsistency(var BillingLine: Record "Billing Line") + local procedure CheckBillingLineData(var BillingLine: Record "Billing Line"; CheckDataConsistency: Boolean; CheckItemUnitOfMeasures: Boolean) var - CheckedServiceCommitments: List of [Text]; - begin - if BillingLine.FindSet() then + LoopBillingLine: Record "Billing Line"; + SubscriptionHeader: Record "Subscription Header"; + SubscriptionLine: Record "Subscription Line"; + CheckedItemUnitsOfMeasure: Dictionary of [Text, Boolean]; + InvoicingItemNo: Code[20]; + ItemUnitOfMeasureKey: Text; + PreviousSubscriptionLineEntryNo: Integer; + Counter: Integer; + begin + PreviousSubscriptionLineEntryNo := 0; + LoopBillingLine.CopyFilters(BillingLine); + LoopBillingLine.SetCurrentKey("Subscription Header No.", "Subscription Line Entry No.", "Billing to"); + LoopBillingLine.SetLoadFields("Subscription Header No.", "Subscription Line Entry No."); + SubscriptionLine.SetLoadFields("Invoicing Item No."); + if LoopBillingLine.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; + Counter += 1; + this.ProgressTracker.UpdateProgress(Counter, ''); + if LoopBillingLine."Subscription Line Entry No." <> PreviousSubscriptionLineEntryNo then begin + PreviousSubscriptionLineEntryNo := LoopBillingLine."Subscription Line Entry No."; + if CheckDataConsistency then + ValidateFilteredVsTotalBillingLineCount(LoopBillingLine); + if CheckItemUnitOfMeasures then begin + if SubscriptionHeader."No." <> LoopBillingLine."Subscription Header No." then + SubscriptionHeader.Get(LoopBillingLine."Subscription Header No."); + if SubscriptionHeader.Type = SubscriptionHeader.Type::Item then begin + SubscriptionLine.Get(LoopBillingLine."Subscription Line Entry No."); + if SubscriptionLine."Invoicing Item No." = '' then + InvoicingItemNo := SubscriptionHeader."Source No." + else + InvoicingItemNo := SubscriptionLine."Invoicing Item No."; + ItemUnitOfMeasureKey := InvoicingItemNo + '|' + SubscriptionHeader."Unit of Measure"; + if not CheckedItemUnitsOfMeasure.ContainsKey(ItemUnitOfMeasureKey) then begin + CheckedItemUnitsOfMeasure.Add(ItemUnitOfMeasureKey, true); + ErrorIfItemUnitOfMeasureCodeDoesNotExist(LoopBillingLine, InvoicingItemNo, SubscriptionHeader); + end; + end; + end; + end; + OnCheckBillingLineDataOnBeforeNextBillingLine(LoopBillingLine); + until LoopBillingLine.Next() = 0; end; local procedure ValidateFilteredVsTotalBillingLineCount(var BillingLine: Record "Billing Line") @@ -929,30 +984,6 @@ codeunit 8060 "Create Billing Documents" 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"; @@ -1249,6 +1280,55 @@ codeunit 8060 "Create Billing Documents" OnAfterIsNewHeaderNeededPerContract(CreateNewHeader, TempBillingLine, PreviousSubContractNo); end; + local procedure UpdateBillingProgress() + begin + ProcessedBillingLineCount += 1; + this.ProgressTracker.UpdateProgress(ProcessedBillingLineCount, CurrentDetailText); + end; + + local procedure SetProgressPartner(PartnerNo: Code[20]) + begin + CurrentPartnerNo := PartnerNo; + RefreshProgressDetail(); + end; + + local procedure SetProgressContract(ContractNo: Code[20]) + begin + CurrentContractNo := ContractNo; + RefreshProgressDetail(); + end; + + local procedure RefreshProgressDetail() + begin + CurrentDetailText := StrSubstNo(ProgressDetailLbl, CurrentPartnerNo, CurrentContractNo); + end; + + local procedure CommitCreatedDocumentCheckpoint() + begin + // Per-document checkpoint: makes each completed document durable on the non-posting path + // (incl. automated/background billing) without committing inside the per-line loop. + // When posting follows, creation and posting are kept atomic and PostCreatedDocuments + // manages its own commit/rollback. + if not PostDocuments then + Commit(); + end; + + local procedure TrimTempBillingLinesForContract(ContractNo: Code[20]) + var + TempBillingLineToDelete: Record "Billing Line" temporary; + begin + // Release a processed contract's aggregated temp lines so the temporary table stays small; otherwise it + // spills to a per-session SQL temp table and every Get/Modify becomes a round-trip. The delete runs on a + // shared-table copy, so the caller's iteration cursor (position, key, filters) is left untouched - only + // lines of an already-processed contract (behind the cursor) are removed. + if ContractNo = '' then + exit; + TempBillingLineToDelete.Copy(TempBillingLine, true); + TempBillingLineToDelete.Reset(); + TempBillingLineToDelete.SetRange("Subscription Contract No.", ContractNo); + TempBillingLineToDelete.DeleteAll(false); + end; + [IntegrationEvent(false, false)] local procedure OnAfterCreateSalesHeaderFromContract(CustomerSubscriptionContract: Record "Customer Subscription Contract"; var SalesHeader: Record "Sales Header") begin @@ -1334,6 +1414,16 @@ codeunit 8060 "Create Billing Documents" begin end; + [IntegrationEvent(false, false)] + local procedure OnCheckBillingLinesOnBeforeDataChecks(var BillingLine: Record "Billing Line"; var ShouldCheckDataConsistency: Boolean; var ShouldCheckItemUnitOfMeasures: Boolean) + begin + end; + + [IntegrationEvent(false, false)] + local procedure OnCheckBillingLineDataOnBeforeNextBillingLine(var BillingLine: Record "Billing Line") + begin + end; + [IntegrationEvent(false, false)] local procedure OnAfterIsNewSalesHeaderNeeded(var CreateNewSalesHeader: Boolean; TempBillingLine: Record "Billing Line" temporary; PreviousCustomerNo: Code[20]; LastDetailOverview: Enum "Contract Detail Overview"; PreviousCurrencyCode: Code[20]; PreviousContractNo: Code[20]) begin @@ -1393,19 +1483,27 @@ codeunit 8060 "Create Billing Documents" TranslationHelper: Codeunit "Translation Helper"; DocumentChangeManagement: Codeunit "Document Change Management"; Language: Codeunit Language; + ProgressTracker: Codeunit "Progress Tracker"; + BillingLineNoByTempEntryNo: Dictionary of [Integer, Integer]; DocumentDate: Date; PostingDate: Date; CustomerRecurringBillingGrouping: Enum "Customer Rec. Billing Grouping"; VendorRecurringBillingGrouping: Enum "Vendor Rec. Billing Grouping"; DocumentsCreatedCount: Integer; ContractsProcessedCount: Integer; + TotalBillingLineCount: Integer; + ProcessedBillingLineCount: Integer; + CurrentPartnerNo: Code[20]; + CurrentContractNo: Code[20]; + CurrentDetailText: Text; CustomerBillingLinesFound: Boolean; VendorBillingLinesFound: Boolean; FirstContractDescriptionLineInserted: Boolean; PostDocuments: Boolean; HideProcessingFinishedMessage: Boolean; - Window: Dialog; - ProgressTxt: Label 'Creating documents...\Partner No. #1#################################\Contract No. #2#################################', Comment = '%1=Partner No., %2=Contract No.'; + CreatingDocumentsLbl: Label 'Creating billing documents...'; + ProgressDetailLbl: Label 'Partner %1 | Contract %2', Comment = '%1 = Partner No., %2 = Contract No.'; + CheckingBillingLinesLbl: Label 'Checking billing lines...'; OnlyOneServicePartnerErr: Label 'You can create documents only for one type of partner at a time (Customer or Vendor). Please check your filters.'; UpdateRequiredErr: Label 'At least one Subscription Line was changed after billing proposal was created. Please check the lines marked with "Update Required" field and update the billing proposal before the billing documents can be created.'; BillingPeriodDescriptionTxt: Label 'Billing period: %1 to %2', Comment = '%1=Recurring Billing from, %2=Recurring Billing to'; diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/PurchaseDocuments.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/PurchaseDocuments.Codeunit.al index 62ebb801c8f..3d68e4483b6 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/PurchaseDocuments.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/PurchaseDocuments.Codeunit.al @@ -163,6 +163,7 @@ codeunit 8066 "Purchase Documents" local procedure MoveBillingLineToBillingLineArchive(var BillingLine: Record "Billing Line"; var PurchaseHeader: Record "Purchase Header"; var PurchaseInvoiceHeader: Record "Purch. Inv. Header"; var PurchaseCrMemoHeader: Record "Purch. Cr. Memo Hdr.") var BillingLineArchive: Record "Billing Line Archive"; + UsageDataBilling: Record "Usage Data Billing"; PostedDocumentNo: Code[20]; begin case PurchaseHeader."Document Type" of @@ -178,6 +179,8 @@ codeunit 8066 "Purchase Documents" BillingLineArchive."Document No." := PostedDocumentNo; BillingLineArchive."Entry No." := 0; BillingLineArchive.Insert(false); + UsageDataBilling.SetRange("Billing Line Entry No.", BillingLine."Entry No."); + UsageDataBilling.ModifyAll("Billing Line Entry No.", BillingLineArchive."Entry No.", false); OnAfterInsertBillingLineArchiveOnMoveBillingLineToBillingLineArchive(BillingLineArchive, BillingLine); until BillingLine.Next() = 0; end; diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SalesDocuments.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SalesDocuments.Codeunit.al index b9eae6e2197..1bbeb71dcd7 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SalesDocuments.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/SalesDocuments.Codeunit.al @@ -200,6 +200,7 @@ codeunit 8063 "Sales Documents" local procedure MoveBillingLineToBillingLineArchive(var BillingLine: Record "Billing Line"; var SalesHeader: Record "Sales Header"; var SalesInvoiceHeader: Record "Sales Invoice Header"; var SalesCrMemoHeader: Record "Sales Cr.Memo Header") var BillingLineArchive: Record "Billing Line Archive"; + UsageDataBilling: Record "Usage Data Billing"; PostedDocumentNo: Code[20]; begin case SalesHeader."Document Type" of @@ -215,6 +216,8 @@ codeunit 8063 "Sales Documents" BillingLineArchive."Document No." := PostedDocumentNo; BillingLineArchive."Entry No." := 0; BillingLineArchive.Insert(false); + UsageDataBilling.SetRange("Billing Line Entry No.", BillingLine."Entry No."); + UsageDataBilling.ModifyAll("Billing Line Entry No.", BillingLineArchive."Entry No.", false); OnAfterInsertBillingLineArchiveOnMoveBillingLineToBillingLineArchive(BillingLineArchive, BillingLine); until BillingLine.Next() = 0; 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 a0581d52a93..03c72218e6b 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 @@ -70,7 +70,7 @@ page 8067 "Recurring Billing" trigger OnDrillDown() begin ContractsGeneralMgt.OpenPartnerCard(Rec.Partner, Rec."Partner No."); - InitTempTable(); + RefreshCurrentRowGroup(); end; } field("Partner Name"; PartnerNameTxt) @@ -83,7 +83,7 @@ page 8067 "Recurring Billing" trigger OnDrillDown() begin ContractsGeneralMgt.OpenPartnerCard(Rec.Partner, Rec."Partner No."); - InitTempTable(); + RefreshCurrentRowGroup(); end; } field("Contract No."; Rec."Subscription Contract No.") @@ -94,7 +94,7 @@ page 8067 "Recurring Billing" trigger OnDrillDown() begin ContractsGeneralMgt.OpenContractCard(Rec.Partner, Rec."Subscription Contract No."); - InitTempTable(); + RefreshCurrentRowGroup(); end; } field(ContractDescriptionField; ContractDescriptionTxt) @@ -107,7 +107,7 @@ page 8067 "Recurring Billing" trigger OnDrillDown() begin ContractsGeneralMgt.OpenContractCard(Rec.Partner, Rec."Subscription Contract No."); - InitTempTable(); + RefreshCurrentRowGroup(); end; } field("Billing from"; Rec."Billing from") @@ -182,8 +182,7 @@ page 8067 "Recurring Billing" trigger OnDrillDown() begin - Rec.OpenDocumentCard(); - InitTempTable(); + RefreshAfterDocumentDrillDown(); end; } field("Service Start Date"; Rec."Subscription Line Start Date") @@ -204,7 +203,7 @@ page 8067 "Recurring Billing" trigger OnDrillDown() begin ServiceObject.OpenServiceObjectCard(Rec."Subscription Header No."); - InitTempTable(); + RefreshCurrentRowGroup(); end; } field("Service Object Description"; Rec."Subscription Description") @@ -335,6 +334,7 @@ page 8067 "Recurring Billing" BillingLine: Record "Billing Line"; ChangeDate: Page "Change Date"; NewBillingToDate: Date; + AffectedGroupKeys: List of [Code[20]]; begin if BillingDate <> 0D then ChangeDate.SetDate(BillingDate); @@ -343,8 +343,9 @@ page 8067 "Recurring Billing" else exit; CurrPage.SetSelectionFilter(BillingLine); + CollectSelectionGroupKeys(BillingLine, AffectedGroupKeys); BillingProposal.UpdateBillingToDate(BillingLine, NewBillingToDate); - InitTempTable(); + RefreshGroups(AffectedGroupKeys); end; } action(DeleteBillingLineAction) @@ -357,10 +358,12 @@ page 8067 "Recurring Billing" trigger OnAction() var BillingLine: Record "Billing Line"; + AffectedGroupKeys: List of [Code[20]]; begin CurrPage.SetSelectionFilter(BillingLine); + CollectSelectionGroupKeys(BillingLine, AffectedGroupKeys); BillingProposal.DeleteBillingLines(BillingLine); - InitTempTable(); + RefreshGroups(AffectedGroupKeys); end; } action(Refresh) @@ -390,7 +393,7 @@ page 8067 "Recurring Billing" trigger OnAction() begin ContractsGeneralMgt.OpenPartnerCard(Rec.Partner, Rec."Partner No."); - InitTempTable(); + RefreshCurrentRowGroup(); end; } action(OpenContractAction) @@ -403,7 +406,7 @@ page 8067 "Recurring Billing" trigger OnAction() begin ContractsGeneralMgt.OpenContractCard(Rec.Partner, Rec."Subscription Contract No."); - InitTempTable(); + RefreshCurrentRowGroup(); end; } action(OpenServiceObjectAction) @@ -416,7 +419,7 @@ page 8067 "Recurring Billing" trigger OnAction() begin ServiceObject.OpenServiceObjectCard(Rec."Subscription Header No."); - InitTempTable(); + RefreshCurrentRowGroup(); end; } action(Dimensions) @@ -466,8 +469,11 @@ page 8067 "Recurring Billing" trigger OnAfterGetRecord() begin - ContractDescriptionTxt := ContractsGeneralMgt.GetContractDescription(Rec.Partner, Rec."Subscription Contract No."); - PartnerNameTxt := ContractsGeneralMgt.GetPartnerName(Rec.Partner, Rec."Partner No."); + // Resolve the contract description and partner name from per-page caches: OnAfterGetRecord fires on every + // row of every render cycle, and the same contract/partner repeats across many rows, so an uncached lookup + // per row turns into thousands of redundant Get calls. The caches are reset whenever the lines are rebuilt. + ContractDescriptionTxt := GetCachedContractDescription(Rec.Partner, Rec."Subscription Contract No."); + PartnerNameTxt := GetCachedPartnerName(Rec.Partner, Rec."Partner No."); SetLineStyleExpr(); end; @@ -487,6 +493,8 @@ page 8067 "Recurring Billing" PartnerNameTxt: Text; GroupBy: Enum "Contract Billing Grouping"; UsageDataEnabled: Boolean; + ContractDescriptionCache: Dictionary of [Text, Text]; + PartnerNameCache: Dictionary of [Text, Text]; protected var BillingDate: Date; @@ -553,11 +561,156 @@ page 8067 "Recurring Billing" procedure InitTempTable() begin + // The lines are about to be rebuilt, so drop the row-render caches; contract descriptions or partner names + // may have changed since they were last read. + Clear(ContractDescriptionCache); + Clear(PartnerNameCache); BillingProposal.InitTempTable(Rec, GroupBy); if Rec.FindFirst() then; //to enable CollapseAll CurrPage.Update(false); end; + // Rebuilds the temp table group that contains the current row. Called after any drilldown or navigate action + // that opens a card which may have been edited (partner name, contract discount, subscription quantity, etc.). + local procedure RefreshCurrentRowGroup() + var + GroupKeys: List of [Code[20]]; + begin + GroupKeys.Add(GetGroupKey(Rec."Partner No.", Rec."Subscription Contract No.")); + RefreshGroups(GroupKeys); + end; + + // Special refresh for the Document No. drilldown: a document may be deleted inside the card, which clears + // Document No. on all billing lines it covered — potentially across multiple groups for collective documents. + // All affected groups are captured before the card opens, so the rebuild is correct regardless of what changes. + local procedure RefreshAfterDocumentDrillDown() + var + TempDocumentBillingLine: Record "Billing Line" temporary; + AffectedGroupKeys: List of [Code[20]]; + GroupKey: Code[20]; + begin + if Rec."Document No." = '' then + exit; + TempDocumentBillingLine.Copy(Rec, true); // shared-table copy — no data copy, cursor untouched + TempDocumentBillingLine.Reset(); + TempDocumentBillingLine.SetRange("Document Type", Rec."Document Type"); + TempDocumentBillingLine.SetRange("Document No.", Rec."Document No."); + if TempDocumentBillingLine.FindSet() then + repeat + GroupKey := GetGroupKey(TempDocumentBillingLine."Partner No.", TempDocumentBillingLine."Subscription Contract No."); + if not AffectedGroupKeys.Contains(GroupKey) then + AffectedGroupKeys.Add(GroupKey); + until TempDocumentBillingLine.Next() = 0; + + Rec.OpenDocumentCard(); + + // RefreshGroups preserves Rec's position; if the document was deleted and the billing line no longer + // exists it falls back to the first record automatically. + RefreshGroups(AffectedGroupKeys); + end; + + // Collects the group key for each line in the selection before a mutating action (Delete Billing Line, + // Change Billing To Date) runs, so only those groups need rebuilding afterwards. + local procedure CollectSelectionGroupKeys(var BillingLine: Record "Billing Line"; var GroupKeys: List of [Code[20]]) + var + GroupKey: Code[20]; + begin + if BillingLine.FindSet() then + repeat + GroupKey := GetGroupKey(BillingLine."Partner No.", BillingLine."Subscription Contract No."); + if not GroupKeys.Contains(GroupKey) then + GroupKeys.Add(GroupKey); + until BillingLine.Next() = 0; + end; + + // Returns the grouping key for a billing line: the contract no. for Contract grouping, the partner no. for + // Contract Partner grouping. Centralizes the GroupBy decision so callers do not repeat it. + local procedure GetGroupKey(PartnerNo: Code[20]; ContractNo: Code[20]): Code[20] + begin + if GroupBy = GroupBy::"Contract Partner" then + exit(PartnerNo); + exit(ContractNo); + end; + + // Common entry point for all targeted refreshes: clears the per-page render caches, rebuilds the specified + // groups in the temp table, and refreshes the page. The user's current row position is preserved; if the row + // no longer exists after the rebuild (e.g. it was deleted), the page falls back to the first record. + local procedure RefreshGroups(GroupKeys: List of [Code[20]]) + var + CurrentEntryNo: Integer; + SavedGroupKey: Code[20]; + IsHeaderRow: Boolean; + begin + // Detail rows keep their stable Billing Line Entry No. across a rebuild, but group header rows are + // reinserted with new negative Entry Nos. every time. So for a header row we remember its group key + // (contract no. or partner no.) and re-locate the rebuilt header by that key afterwards. + IsHeaderRow := Rec."Entry No." < 0; + if IsHeaderRow then + SavedGroupKey := GetGroupKey(Rec."Partner No.", Rec."Subscription Contract No.") + else + CurrentEntryNo := Rec."Entry No."; + Clear(ContractDescriptionCache); + Clear(PartnerNameCache); + BillingProposal.RefreshGroupsInTempTable(Rec, GroupBy, GroupKeys); + if IsHeaderRow then + CurrentEntryNo := FindHeaderEntryNo(SavedGroupKey); + // Get positions by primary key regardless of the page's view filters (User ID / Partner), so the + // page's own filtering is left untouched; fall back to the first visible row if the target is gone. + if not Rec.Get(CurrentEntryNo) then + if Rec.FindFirst() then; + CurrPage.Update(false); + end; + + // Locates the rebuilt group header for GroupKey and returns its (new, negative) Entry No., searching a + // shared-table copy so the page record's own filters are never disturbed. Returns 0 if no header matches. + local procedure FindHeaderEntryNo(GroupKey: Code[20]): Integer + var + TempHeaderBillingLine: Record "Billing Line" temporary; + begin + TempHeaderBillingLine.Copy(Rec, true); // shared-table copy — same temp data, independent cursor/filters + TempHeaderBillingLine.Reset(); + TempHeaderBillingLine.SetRange(Indent, 0); // header rows only; detail rows are Indent 1 + case GroupBy of + GroupBy::Contract: + TempHeaderBillingLine.SetRange("Subscription Contract No.", GroupKey); + GroupBy::"Contract Partner": + TempHeaderBillingLine.SetRange("Partner No.", GroupKey); + end; + if TempHeaderBillingLine.FindFirst() then + exit(TempHeaderBillingLine."Entry No."); + exit(0); + end; + + local procedure GetCachedContractDescription(Partner: Enum "Service Partner"; ContractNo: Code[20]): Text + var + CacheKey: Text; + CachedValue: Text; + begin + if ContractNo = '' then + exit(''); + CacheKey := Format(Partner) + '|' + ContractNo; + if ContractDescriptionCache.Get(CacheKey, CachedValue) then + exit(CachedValue); + CachedValue := ContractsGeneralMgt.GetContractDescription(Partner, ContractNo); + ContractDescriptionCache.Add(CacheKey, CachedValue); + exit(CachedValue); + end; + + local procedure GetCachedPartnerName(Partner: Enum "Service Partner"; PartnerNo: Code[20]): Text + var + CacheKey: Text; + CachedValue: Text; + begin + if PartnerNo = '' then + exit(''); + CacheKey := Format(Partner) + '|' + PartnerNo; + if PartnerNameCache.Get(CacheKey, CachedValue) then + exit(CachedValue); + CachedValue := ContractsGeneralMgt.GetPartnerName(Partner, PartnerNo); + PartnerNameCache.Add(CacheKey, CachedValue); + exit(CachedValue); + end; + local procedure UpdateServiceCommitmentDimension() var ServiceCommitment: Record "Subscription Line"; 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 cc22098ed6d..b2286f82d46 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 @@ -57,6 +57,7 @@ table 8061 "Billing Line" field(31; "Subscription Line Entry No."; Integer) { Caption = 'Subscription Line Entry No.'; + TableRelation = "Subscription Line"."Entry No."; } field(32; "Subscription Description"; Text[100]) { @@ -243,11 +244,22 @@ table 8061 "Billing Line" key(SK6; "Billing Template Code", Partner) { } + key(SK7; "Subscription Line Entry No.") + { + } + } + + fieldgroups + { + fieldgroup(DropDown; "Subscription Header No.", "Billing from", "Billing to", Amount) + { + } } trigger OnDelete() var BillingLine2: Record "Billing Line"; + UsageDataBilling: Record "Usage Data Billing"; begin if "Document No." <> '' then Error(CannotDeleteBillingLinesWithDocumentNoErr); @@ -257,6 +269,8 @@ table 8061 "Billing Line" else Error(OnlyLastServiceLineCanBeDeletedErr, "Subscription Header No."); RecalculateCustomerContractHarmonizedBillingFields(); + UsageDataBilling.SetRange("Billing Line Entry No.", "Entry No."); + UsageDataBilling.ModifyAll("Billing Line Entry No.", 0, false); end; var diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLineArchive.Table.al b/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLineArchive.Table.al index ea6f945c0df..81795965fbd 100644 --- a/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLineArchive.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Billing/Tables/BillingLineArchive.Table.al @@ -200,6 +200,14 @@ table 8064 "Billing Line Archive" { } } + + fieldgroups + { + fieldgroup(DropDown; "Subscription Header No.", "Billing from", "Billing to", Amount) + { + } + } + internal procedure PostedDocumentExist(): Boolean var SalesInvoiceHeader: Record "Sales Invoice Header"; diff --git a/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al b/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al index f5e28d8dd7f..213a03a4877 100644 --- a/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al @@ -611,6 +611,9 @@ table 8059 "Subscription Line" { } + key(Key4; "Supplier Reference Entry No.", "Subscription Line Start Date") + { + } } trigger OnInsert() @@ -1794,9 +1797,7 @@ table 8059 "Subscription Line" internal procedure SetUsageDataBillingFilters(var UsageDataBilling: Record "Usage Data Billing"; BillingFromDate: Date; BillingToDate: Date) begin - UsageDataBilling.SetRange("Subscription Header No.", Rec."Subscription Header No."); UsageDataBilling.SetRange("Subscription Line Entry No.", Rec."Entry No."); - UsageDataBilling.SetRange(Partner, Rec.Partner); UsageDataBilling.SetRange("Usage Base Pricing", Enum::"Usage Based Pricing"::"Usage Quantity", Enum::"Usage Based Pricing"::"Unit Cost Surcharge"); UsageDataBilling.SetRange("Document Type", "Usage Based Billing Doc. Type"::None); UsageDataBilling.SetFilter("Charge Start Date", '>=%1', BillingFromDate); diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/GenericConnectorProcessing.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/GenericConnectorProcessing.Codeunit.al index 0e6a2a0da74..c2c04aa19a5 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/GenericConnectorProcessing.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/GenericConnectorProcessing.Codeunit.al @@ -8,6 +8,10 @@ codeunit 8033 "Generic Connector Processing" implements "Usage Data Processing" var ImportAndProcessUsageData: Codeunit "Import And Process Usage Data"; CreateUsageDataBilling: Codeunit "Create Usage Data Billing"; + ProgressTracker: Codeunit "Progress Tracker"; + ProcessImportedLinesLbl: Label 'Processing imported usage data lines...'; + CreateBillingDataLbl: Label 'Creating usage data billing...'; + ImportEntryDetailLbl: Label 'Import Entry No. %1', Comment = '%1 = Usage Data Import Entry No.'; ProcessingSetupErr: Label 'You must specify either a reading/writing XMLport or a reading/writing codeunit.'; UsageDataLinesProcessingErr: Label 'Errors were found while processing the Usage Data Lines.'; NoDataFoundErr: Label 'No data found for processing step %1.', Comment = '%1 = Name of the processing step'; @@ -84,12 +88,18 @@ codeunit 8033 "Generic Connector Processing" implements "Usage Data Processing" GenericImportSettings: Record "Generic Import Settings"; UsageDataSupplierReference: Record "Usage Data Supplier Reference"; ErrorCount: Integer; + LineCounter: Integer; + DetailText: Text; begin UsageDataGenericImport.SetRange("Usage Data Import Entry No.", UsageDataImport."Entry No."); + this.ProgressTracker.StartActivity(ProcessImportedLinesLbl, UsageDataGenericImport.Count()); + DetailText := StrSubstNo(ImportEntryDetailLbl, UsageDataImport."Entry No."); + GenericImportSettings.Get(UsageDataImport."Supplier No."); // Loop-invariant (constant Supplier No.) - read once instead of per row. if UsageDataGenericImport.FindSet() then repeat + LineCounter += 1; + this.ProgressTracker.UpdateProgress(LineCounter, DetailText); UsageDataGenericImport.Validate("Processing Status", Enum::"Processing Status"::None); - GenericImportSettings.Get(UsageDataImport."Supplier No."); CreateUsageDataCustomers(GenericImportSettings, UsageDataGenericImport, UsageDataSupplierReference, UsageDataImport."Supplier No."); CreateUsageDataSubscriptions(GenericImportSettings, UsageDataGenericImport, UsageDataSupplierReference, UsageDataImport); @@ -109,6 +119,7 @@ codeunit 8033 "Generic Connector Processing" implements "Usage Data Processing" UsageDataImport.SetErrorReason(StrSubstNo(NoDataFoundErr, UsageDataImport."Processing Step")); UsageDataImport.Modify(false); end; + this.ProgressTracker.Finish(); if ErrorCount <> 0 then ImportAndProcessUsageData.SetError(UsageDataImport, UsageDataLinesProcessingErr); @@ -203,6 +214,8 @@ codeunit 8033 "Generic Connector Processing" implements "Usage Data Processing" UsageDataBilling: Record "Usage Data Billing"; UsageDataGenericImport: Record "Usage Data Generic Import"; ConfirmManagement: Codeunit "Confirm Management"; + LineCounter: Integer; + DetailText: Text; begin UsageDataGenericImport.SetRange("Usage Data Import Entry No.", UsageDataImport."Entry No."); UsageDataGenericImport.SetRange("Processing Status", "Processing Status"::Error); @@ -216,18 +229,25 @@ codeunit 8033 "Generic Connector Processing" implements "Usage Data Processing" UsageDataGenericImport.SetRange("Processing Status"); end; + this.ProgressTracker.StartActivity(CreateBillingDataLbl, UsageDataGenericImport.Count()); + DetailText := StrSubstNo(ImportEntryDetailLbl, UsageDataImport."Entry No."); if UsageDataGenericImport.FindSet() then repeat + LineCounter += 1; + this.ProgressTracker.UpdateProgress(LineCounter, DetailText); CreateUsageDataBilling.CollectServiceCommitments(TempServiceCommitment, UsageDataGenericImport."Subscription Header No.", UsageDataGenericImport."Supp. Subscription End Date"); SetUsageDataGenericImportError(UsageDataGenericImport, ''); - if not CheckServiceCommitments(UsageDataGenericImport, TempServiceCommitment) then + if not CheckServiceCommitments(UsageDataGenericImport, TempServiceCommitment) then begin + this.ProgressTracker.Finish(); exit; + end; CreateUsageDataBilling.CreateUsageDataBillingFromTempServiceCommitments(TempServiceCommitment, UsageDataImport."Supplier No.", UsageDataGenericImport); until UsageDataGenericImport.Next() = 0 else begin UsageDataImport.SetErrorReason(StrSubstNo(NoDataFoundErr, UsageDataImport."Processing Step")); UsageDataImport.Modify(false); end; + this.ProgressTracker.Finish(); end; procedure UpdateImportStatus(var UsageDataImport: Record "Usage Data Import") diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/ProcessUsageDataBilling.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/ProcessUsageDataBilling.Codeunit.al index 42d937ddf58..246ffa912a9 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/ProcessUsageDataBilling.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/ProcessUsageDataBilling.Codeunit.al @@ -11,6 +11,9 @@ codeunit 8026 "Process Usage Data Billing" UsageDataSupplier: Record "Usage Data Supplier"; DateTimeManagement: Codeunit "Date Time Management"; ContractItemMgt: Codeunit "Sub. Contracts Item Management"; + ProgressTracker: Codeunit "Progress Tracker"; + ProcessBillingLbl: Label 'Processing usage based billing...'; + ImportEntryDetailLbl: Label 'Import Entry No. %1', Comment = '%1 = Usage Data Import Entry No.'; DoesNotExistErr: Label 'No data found for processing step %1.', Comment = '%1=Name of the processing step'; ProcessServiceCommitmentProcedureNameLbl: Label 'ProcessServiceCommitment', Locked = true; UsageBasedPricingOptionNotImplementedErr: Label 'Unknown option %1 for %2.\\Object Type: %3 Object Name: %4, Procedure: %5', Comment = '%1=Format("Calculation Base Type"), %2 = Fieldcaption for "Calculation Base Type", %3 = Object Type, %4 = Object Name, %5 = Procedure Name'; @@ -30,6 +33,8 @@ codeunit 8026 "Process Usage Data Billing" var UsageDataBilling: Record "Usage Data Billing"; SubscriptionLineEntryNoList: List of [Integer]; + Counter: Integer; + DetailText: Text; begin OnBeforeProcessUsageDataBilling(UsageDataImport); if UsageDataImport."Processing Status" = Enum::"Processing Status"::Closed then @@ -46,13 +51,18 @@ codeunit 8026 "Process Usage Data Billing" until UsageDataBilling.Next() = 0; UsageDataBilling.SetRange(Partner); + this.ProgressTracker.StartActivity(ProcessBillingLbl, UsageDataBilling.Count()); + DetailText := StrSubstNo(ImportEntryDetailLbl, UsageDataImport."Entry No."); if UsageDataBilling.FindSet() then repeat + Counter += 1; + this.ProgressTracker.UpdateProgress(Counter, DetailText); if SubscriptionLineEntryNoList.IndexOf(UsageDataBilling."Subscription Line Entry No.") = 0 then begin SubscriptionLineEntryNoList.Add(UsageDataBilling."Subscription Line Entry No."); ProcessServiceCommitment(UsageDataBilling); end; until UsageDataBilling.Next() = 0; + this.ProgressTracker.Finish(); end else begin UsageDataBilling.SetRange("Subscription Contract No."); if UsageDataBilling.FindFirst() then begin diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/UsageBasedContrSubscribers.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/UsageBasedContrSubscribers.Codeunit.al index 96c800ef1a1..224e3113741 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/UsageBasedContrSubscribers.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Codeunits/UsageBasedContrSubscribers.Codeunit.al @@ -240,34 +240,6 @@ codeunit 8028 "Usage Based Contr. Subscribers" until UsageDataBilling.Next() = 0; end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales Documents", OnAfterInsertBillingLineArchiveOnMoveBillingLineToBillingLineArchive, '', false, false)] - local procedure UpdateUsageDataBillingWithBillingArchiveLineSalesDocuments(var BillingLineArchive: Record "Billing Line Archive"; BillingLine: Record "Billing Line") - var - UsageDataBilling: Record "Usage Data Billing"; - ServiceCommitment: Record "Subscription Line"; - begin - if not ServiceCommitment.Get(BillingLine."Subscription Line Entry No.") then - exit; - if not ServiceCommitment."Usage Based Billing" then - exit; - UsageDataBilling.SetRange("Billing Line Entry No.", BillingLine."Entry No."); - UsageDataBilling.ModifyAll("Billing Line Entry No.", BillingLineArchive."Entry No.", false); - end; - - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purchase Documents", OnAfterInsertBillingLineArchiveOnMoveBillingLineToBillingLineArchive, '', false, false)] - local procedure UpdateUsageDataBillingWithBillingArchiveLinePurchaseDocuments(var BillingLineArchive: Record "Billing Line Archive"; BillingLine: Record "Billing Line") - var - UsageDataBilling: Record "Usage Data Billing"; - ServiceCommitment: Record "Subscription Line"; - begin - if not ServiceCommitment.Get(BillingLine."Subscription Line Entry No.") then - exit; - if not ServiceCommitment."Usage Based Billing" then - exit; - UsageDataBilling.SetRange("Billing Line Entry No.", BillingLine."Entry No."); - UsageDataBilling.ModifyAll("Billing Line Entry No.", BillingLineArchive."Entry No.", false); - end; - [EventSubscriber(ObjectType::Table, Database::"Purchase Line", OnAfterDeleteEvent, '', false, false)] local procedure DeleteOrResetRelatedUsageBillingLinesOnAfterDeletePurchaseLineEvent(Rec: Record "Purchase Line"; RunTrigger: Boolean) var @@ -330,18 +302,4 @@ codeunit 8028 "Usage Based Contr. Subscribers" until UsageDataBilling.Next() = 0; end; - [EventSubscriber(ObjectType::Table, Database::"Billing Line", OnAfterDeleteEvent, '', false, false)] - local procedure RemoveBillingLineNoFromRelatedUsageData(Rec: Record "Billing Line"; RunTrigger: Boolean) - var - UsageDataBilling: Record "Usage Data Billing"; - begin - if not RunTrigger then - exit; - if Rec.IsTemporary then - exit; - - UsageDataBilling.SetRange("Billing Line Entry No.", Rec."Entry No."); - UsageDataBilling.ModifyAll("Billing Line Entry No.", 0, false); - end; - } diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBilling.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBilling.Table.al index 760ebec406c..d9e6c64fa2b 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBilling.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBilling.Table.al @@ -205,6 +205,9 @@ table 8006 "Usage Data Billing" { Caption = 'Billing Line Entry No.'; TableRelation = + if (Partner = const(Customer), "Document Type" = filter(None)) "Billing Line"."Entry No." + where(Partner = const(Customer)) + else if (Partner = const(Customer), "Document Type" = filter(Invoice)) "Billing Line"."Entry No." where(Partner = const(Customer), "Document Type" = const(Invoice), "Document No." = field("Document No.")) else @@ -217,6 +220,9 @@ table 8006 "Usage Data Billing" if (Partner = const(Customer), "Document Type" = filter("Posted Credit Memo")) "Billing Line Archive"."Entry No." where(Partner = const(Customer), "Document Type" = const("Credit Memo"), "Document No." = field("Document No.")) else + if (Partner = const(Vendor), "Document Type" = filter(None)) "Billing Line"."Entry No." + where(Partner = const(Vendor)) + else if (Partner = const(Vendor), "Document Type" = filter(Invoice)) "Billing Line"."Entry No." where(Partner = const(Vendor), "Document Type" = const(Invoice), "Document No." = field("Document No.")) else @@ -300,6 +306,19 @@ table 8006 "Usage Data Billing" SumIndexFields = Quantity, Amount; MaintainSiftIndex = true; } + key(key2; "Subscription Contract No.", "Subscription Contract Line No.") + { + } + key(key3; "Subscription Line Entry No.", "Document Type", "Charge End Date") + { + SumIndexFields = Quantity, Amount, "Cost Amount"; + } + key(key4; "Document No.", "Document Type") + { + } + key(key5; "Billing Line Entry No.") + { + } } trigger OnInsert() @@ -790,7 +809,7 @@ table 8006 "Usage Data Billing" exit((Rec."Document Type" <> "Usage Based Billing Doc. Type"::None) and (Rec."Document No." <> '')); end; - internal procedure GetPrintoutDescription() Description: Text[100] + procedure GetPrintoutDescription() Description: Text[100] begin if ShouldPrintProductName() then Description := "Product Name" @@ -799,7 +818,7 @@ table 8006 "Usage Data Billing" OnAfterGetPrintoutDescription(Rec, Description); end; - internal procedure ShouldPrintProductName() Result: Boolean + procedure ShouldPrintProductName() Result: Boolean var ServiceContractSetup: Record "Subscription Contract Setup"; begin diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataGenericImport.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataGenericImport.Table.al index d4c67323915..52290198a5c 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataGenericImport.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataGenericImport.Table.al @@ -306,6 +306,9 @@ table 8018 "Usage Data Generic Import" { Clustered = true; } + key(Key2; "Usage Data Import Entry No.", "Processing Status") + { + } } trigger OnInsert()