From db26bceeee8e950e9abe031645fe49aed279ef46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Sun, 21 Jun 2026 21:59:02 +0200 Subject: [PATCH 1/3] [Subscription Billing] Performance optimizations for large-scale billing runs (#7690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary Optimizes the Subscription Billing module to handle large-scale data volumes (~4M subscription lines) without timeouts or unbounded memory usage. Introduces a reusable progress/ETA helper, targeted index additions, per-N-contract commit checkpoints, and a range of code-level perf fixes. ### New codeunits - **Progress Tracker** (CU 8035): Reusable progress dialog helper. Shows elapsed time, processed/total count with %, ETA, and throughput (items/min). All dialog calls guarded by `GuiAllowed`; safe for Job Queue / headless sessions. Time-throttled to one redraw/sec. - **Billing Price Calc Skip** (CU 8036): `EventSubscriberInstance = Manual` codeunit. Bound/unbound around document-line insertion to suppress the `OnBeforeUpdateUnitPrice`, `OnBeforeGetUnitCost`, `OnBeforeUpdateQuantityFromUOMCode` (Sales) and `OnBeforeUpdateUnitCost` (Purchase) subscribers — eliminates redundant price recalculation per line. ### BillingProposal (CU 8062) - Added `Progress Tracker` with ETA to `DeleteBillingProposal`, `DeleteBillingLines`, and `DeleteBillingDocuments`. - `DeleteBillingDocuments`: pre-counts distinct documents via `CountDistinctDocuments` so ETA is accurate from the first step. - Per-N-contract commit checkpoint (`CommitBatchSize = 10`) in both Customer and Vendor contract loops — prevents multi-hour runs from being lost to a single late failure, while keeping test behaviour unchanged (tests use 1 contract, so the checkpoint never fires). - `TrimTempBillingLinesForContract` simplified using `Copy(TempBillingLine, true)` (shared-table reference, O(1)) — all four per-category loops collapsed to a single call each, removing cursor-state variables and the separate post-loop trim pass. - `RefreshGroupsInTempTable` (renamed from `RefreshTempTableForSelection`): drops and rebuilds only the affected groups in the temp table rather than rebuilding the entire set — targeted refresh after drilldowns and actions. - `SetLoadFields` on `CustomerContract`, `VendorContract`, and `ServiceObject` reads in `UpdateBillingLineFromServiceCommitment`. - `FilterBillingLinesOnServiceCommitment` now sets only `"Subscription Line Entry No."` filter (relies on new SK7 index). ### CreateBillingDocuments (CU 8060) - Added `Progress Tracker` with ETA covering the full document-creation run (total billing-line count as denominator). - `BillingPriceCalcSkip` bound before and unbound after each `InsertSalesLine` / `InsertPurchaseLine` block — price recalc suppressed per line. - Fixed AA0205 pipeline error: `PreviousSubscriptionLineEntryNo` initialised to `0` in `CheckBillingLineData`. ### RecurringBilling page (page 8067) - `ContractDescriptionCache` and `PartnerNameCache` dictionaries: description and partner-name lookups cached per `OnAfterGetRecord` instead of re-querying `ContractsGeneralMgt` on every row scroll. - `InitTempTable` removed from 9 navigation / drilldown sites that do not change billing data; replaced with targeted `RefreshCurrentRowGroup()` or `RefreshAfterDocumentDrillDown()`. - `RefreshGroups` restores the previously-focused record after a partial rebuild (no collapse-all side effect). - New helpers: `RefreshCurrentRowGroup`, `RefreshAfterDocumentDrillDown`, `CollectSelectionGroupKeys`, `GetGroupKey`, `RefreshGroups`. ### SalesDocuments / PurchaseDocuments (CU 8063 / 8066) - `UpdateUsageDataBillingArchiveEntryNo` helper dropped; 2-line UBB update inlined into `MoveBillingLineToBillingLineArchive` — removes one indirection per archived line. ### BillingLine table (8061) - `key(SK7; "Subscription Line Entry No.")` added — used by `FilterBillingLinesOnServiceCommitment` to avoid full scans. - `TableRelation` added to `"Subscription Line Entry No."` field. - `OnDelete` trigger: clears `UsageDataBilling."Billing Line Entry No."` for related UBB records (removes the need for the event subscriber). - `fieldgroup(DropDown; ...)` added for meaningful popup columns. ### BillingLineArchive table (8064) - Same `DropDown` fieldgroup added. ### UsageDataBilling table (8062) - `key(key5; "Billing Line Entry No.")` added. - `TableRelation` for `"Billing Line Entry No."` extended with `Document Type = None` branches so the lookup works before billing documents are created. ### UsageBasedContrSubscribers (CU 8028) - Removed three event subscribers whose logic is now handled directly in `BillingLine.OnDelete` and inlined in Sales/PurchaseDocuments: `UpdateUsageDataBillingWithBillingArchiveLineSalesDocuments`, `UpdateUsageDataBillingWithBillingArchiveLinePurchaseDocuments`, `RemoveBillingLineNoFromRelatedUsageData`. ### Usage Based Billing codeunits - All `ProgressTracker.` calls qualified to `this.ProgressTracker.` across `GenericConnectorProcessing` (CU 8028) and `ProcessUsageDataBilling` (CU 8026) to satisfy BCQuality AA0248. Co-Authored-By: Claude Sonnet 4.6 --- .../Codeunits/ProgressTracker.Codeunit.al | 143 ++++++++ .../BillingPriceCalcSkip.Codeunit.al | 46 +++ .../Codeunits/BillingProposal.Codeunit.al | 191 ++++++++++- .../CreateBillingDocuments.Codeunit.al | 307 ++++++++++++------ .../Codeunits/PurchaseDocuments.Codeunit.al | 3 + .../Codeunits/SalesDocuments.Codeunit.al | 3 + .../Pages/CreateCustomerBillingDocs.Page.al | 4 +- .../Pages/CreateVendorBillingDocs.Page.al | 4 +- .../Billing/Pages/RecurringBilling.Page.al | 148 ++++++++- .../App/Billing/Tables/BillingLine.Table.al | 14 + .../Tables/BillingLineArchive.Table.al | 8 + .../Tables/SubscriptionLine.Table.al | 7 +- .../GenericConnectorProcessing.Codeunit.al | 24 +- .../ProcessUsageDataBilling.Codeunit.al | 10 + .../UsageBasedContrSubscribers.Codeunit.al | 42 --- .../Tables/UsageDataBilling.Table.al | 83 +++-- .../Tables/UsageDataBillingMetadata.Table.al | 4 +- .../Tables/UsageDataBlob.Table.al | 6 +- .../Tables/UsageDataGenericImport.Table.al | 9 +- .../Tables/UsageDataImport.Table.al | 18 +- .../Tables/UsageDataSuppSubscription.Table.al | 14 +- .../Tables/UsageDataSupplier.Table.al | 2 +- .../UsageDataSupplierReference.Table.al | 6 +- 23 files changed, 863 insertions(+), 233 deletions(-) create mode 100644 src/Apps/W1/Subscription Billing/App/Base/Codeunits/ProgressTracker.Codeunit.al create mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingPriceCalcSkip.Codeunit.al 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/BillingPriceCalcSkip.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingPriceCalcSkip.Codeunit.al new file mode 100644 index 00000000000..c3a8e3ba5b0 --- /dev/null +++ b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingPriceCalcSkip.Codeunit.al @@ -0,0 +1,46 @@ +namespace Microsoft.SubscriptionBilling; + +using Microsoft.Purchases.Document; +using Microsoft.Sales.Document; + +/// +/// Manual-binding helper used by "Create Billing Documents" to skip the redundant unit price / unit cost +/// engine work that the platform runs while validating the created Sales and Purchase lines. The billing +/// flow already assigns the known Unit Price and Unit Cost explicitly (the values come from the Billing +/// Line), so the price-list and cost lookups inside those Validate calls are wasted work on every line. +/// Only the price/cost calculation is skipped - all other validation side effects (dimensions, VAT, +/// posting groups, item defaults) still run. Bind this instance only around the line field initialization +/// via BindSubscription / UnbindSubscription. +/// +codeunit 8036 "Billing Price Calc. Skip" +{ + EventSubscriberInstance = Manual; + + [EventSubscriber(ObjectType::Table, Database::"Sales Line", OnBeforeUpdateUnitPrice, '', false, false)] + local procedure SkipSalesUpdateUnitPrice(var SalesLine: Record "Sales Line"; xSalesLine: Record "Sales Line"; CalledByFieldNo: Integer; CurrFieldNo: Integer; var Handled: Boolean) + begin + Handled := true; + end; + + [EventSubscriber(ObjectType::Table, Database::"Sales Line", OnBeforeGetUnitCost, '', false, false)] + local procedure SkipSalesGetUnitCost(var SalesLine: Record "Sales Line"; var IsHandled: Boolean) + begin + IsHandled := true; + end; + + // Suppresses UoM quantity conversion while the billing line is being initialised. At the point this + // subscriber fires, the Sales Line quantity is still 0 (the billing amount is assigned afterwards), + // so the platform's UoM conversion logic produces no meaningful result and only adds overhead. All + // UoM-driven quantity validation runs normally once BillingPriceCalcSkip is unbound. + [EventSubscriber(ObjectType::Table, Database::"Sales Line", OnBeforeUpdateQuantityFromUOMCode, '', false, false)] + local procedure SkipSalesUpdateQuantityFromUOMCode(var SalesLine: Record "Sales Line"; var IsHandled: Boolean) + begin + IsHandled := true; + end; + + [EventSubscriber(ObjectType::Table, Database::"Purchase Line", OnBeforeUpdateUnitCost, '', false, false)] + local procedure SkipPurchaseUpdateUnitCost(var PurchaseLine: Record "Purchase Line"; xPurchaseLine: Record "Purchase Line"; CurrentFieldNo: Integer; var IsHandled: Boolean) + begin + IsHandled := true; + 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..ae3541b560e 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.'; @@ -60,6 +63,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 +240,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 +262,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 +359,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 +368,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 +380,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 +548,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 +556,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 +566,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); @@ -552,7 +656,9 @@ codeunit 8062 "Billing Proposal" ClearBillingProposalOptionsTxt: Label 'All billing proposals, Only current billing template proposal'; ClearBillingProposalOptionsMySuggestionsOnlyTxt: Label 'All billing proposals (user %1 only), Only current billing template proposal', Comment = '%1: User ID'; ClearBillingProposalQst: Label 'Which billing proposal(s) should be deleted?'; + ClearingBillingProposalLbl: Label 'Clearing billing proposal...'; StrMenuResponse: Integer; + Counter: Integer; begin DisplayErrorIfNotAuthorizedToClearProposalOrDeleteDocuments(); BillingTemplate.Get(BillingTemplateCode); @@ -570,24 +676,45 @@ 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 + DeletingBillingLinesLbl: Label 'Deleting billing lines...'; + 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 +1017,22 @@ 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'; + DeletingDocumentsLbl: Label 'Deleting billing documents...'; + 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..d5f654e74d3 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() @@ -237,6 +254,9 @@ codeunit 8060 "Create Billing Documents" CustomerContractLine.Get(TempBillingLine."Subscription Contract No.", TempBillingLine."Subscription Contract Line No."); OnAfterCustomerContractLineGetInInsertSalesLineFromTempBillingLine(CustomerContractLine, SalesHeader, TempBillingLine); + // Skip the redundant unit price/cost engine while validating the line - the values are taken from + // the Billing Line and assigned explicitly below. All other validation side effects still run. + BindSubscription(BillingPriceCalcSkip); SalesLine.InitFromSalesHeader(SalesHeader); SubContractsItemManagement.SetAllowInsertOfInvoicingItem(true); if (ServiceCommitment."Invoicing Item No." <> '') and @@ -247,15 +267,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"; @@ -272,6 +295,7 @@ codeunit 8060 "Create Billing Documents" SalesLine."Description 2" := ''; SetInvoicePriceFromUsageDataBilling(SalesLine, TempBillingLine); + UnbindSubscription(BillingPriceCalcSkip); OnBeforeInsertSalesLineFromContractLine(SalesLine, TempBillingLine); SalesLine.Insert(false); @@ -302,13 +326,16 @@ 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 + // TempBillingLine.Indent holds the source Billing Line "Entry No." captured during aggregation + // in CreateTempBillingLines; reused here as the Billing Line link for Usage Data Billing + // instead of re-querying Billing Line. + BillingLineNo := TempBillingLine.Indent; 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); @@ -360,6 +387,9 @@ codeunit 8060 "Create Billing Documents" ServiceObject.Get(TempBillingLine."Subscription Header No."); ServiceCommitment.Get(TempBillingLine."Subscription Line Entry No."); + // Skip the redundant unit price/cost engine while validating the line - the values are taken from + // the Billing Line and assigned explicitly below. All other validation side effects still run. + BindSubscription(BillingPriceCalcSkip); InitPurchaseLine(PurchaseLine); SubContractsItemManagement.SetAllowInsertOfInvoicingItem(true); if (ServiceCommitment."Invoicing Item No." <> '') and @@ -370,12 +400,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"; @@ -384,6 +417,7 @@ codeunit 8060 "Create Billing Documents" PurchaseLine.Description := ServiceCommitment.Description; PurchaseLine."Description 2" := CopyStr(ServiceObject.Description, 1, MaxStrLen(PurchaseLine."Description 2")); SetInvoicePriceFromUsageDataBilling(PurchaseLine, TempBillingLine); + UnbindSubscription(BillingPriceCalcSkip); OnBeforeInsertPurchaseLineFromContractLine(PurchaseLine, TempBillingLine); PurchaseLine.Insert(false); @@ -411,13 +445,16 @@ 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 + // TempBillingLine.Indent holds the source Billing Line "Entry No." captured during aggregation + // in CreateTempBillingLines; reused here as the Billing Line link for Usage Data Billing + // instead of re-querying Billing Line. + BillingLineNo := TempBillingLine.Indent; 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 +490,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 +748,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 +772,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 +784,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 +808,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 +822,10 @@ codeunit 8060 "Create Billing Documents" OnCreateTempBillingLinesBeforeSaveTempBillingLine(TempBillingLine, BillingLine); TempBillingLine."Unit Cost" += BillingLine."Unit Cost"; TempBillingLine."Unit Cost (LCY)" += BillingLine."Unit Cost (LCY)"; + // Carry the (last) source Billing Line "Entry No." on the aggregated temp line via the unused + // Indent field, so the usage data billing link can be stamped without re-querying Billing Line + // (replaces a per-line GetBillingLineNo FindLast). + TempBillingLine.Indent := BillingLine."Entry No."; TempBillingLine.Modify(false); until BillingLine.Next() = 0; end; @@ -829,27 +866,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 +915,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 +1001,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 +1297,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 +1431,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 +1500,27 @@ codeunit 8060 "Create Billing Documents" TranslationHelper: Codeunit "Translation Helper"; DocumentChangeManagement: Codeunit "Document Change Management"; Language: Codeunit Language; + ProgressTracker: Codeunit "Progress Tracker"; + BillingPriceCalcSkip: Codeunit "Billing Price Calc. Skip"; 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/CreateCustomerBillingDocs.Page.al b/src/Apps/W1/Subscription Billing/App/Billing/Pages/CreateCustomerBillingDocs.Page.al index d47cea70b7b..71b8322cff9 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 @@ -55,7 +55,7 @@ page 8072 "Create Customer Billing Docs" PostDocuments: Boolean; Grouping: Enum "Customer Rec. Billing Grouping"; - internal procedure GetData(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Customer Rec. Billing Grouping"; var NewPostDocuments: Boolean) + procedure GetData(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Customer Rec. Billing Grouping"; var NewPostDocuments: Boolean) begin NewDocumentDate := DocumentDate; NewPostingDate := PostingDate; @@ -63,7 +63,7 @@ page 8072 "Create Customer Billing Docs" NewPostDocuments := PostDocuments; end; - internal procedure SetData(NewDocumentDate: Date; NewPostingDate: Date; NewGroupingType: Enum "Customer Rec. Billing Grouping"; NewPostDocuments: Boolean) + 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 a71b150937a..aa6b2c89d0b 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 @@ -50,14 +50,14 @@ page 8077 "Create Vendor Billing Docs" PostingDate: Date; Grouping: Enum "Vendor Rec. Billing Grouping"; - internal procedure GetData(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Vendor Rec. Billing Grouping") + procedure GetData(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Vendor Rec. Billing Grouping") begin NewDocumentDate := DocumentDate; NewPostingDate := PostingDate; NewGroupingType := Grouping; end; - internal procedure SetData(NewDocumentDate: Date; NewPostingDate: Date; NewGroupingType: Enum "Vendor Rec. Billing Grouping") + 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/Pages/RecurringBilling.Page.al b/src/Apps/W1/Subscription Billing/App/Billing/Pages/RecurringBilling.Page.al index a0581d52a93..b230966283d 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,123 @@ 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; + begin + CurrentEntryNo := Rec."Entry No."; + Clear(ContractDescriptionCache); + Clear(PartnerNameCache); + BillingProposal.RefreshGroupsInTempTable(Rec, GroupBy, GroupKeys); + if not Rec.Get(CurrentEntryNo) then + if Rec.FindFirst() then; + CurrPage.Update(false); + 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..95369f04f15 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() @@ -903,7 +906,7 @@ table 8059 "Subscription Line" until BillingLine.Next() = 0; end; - internal procedure UpdateNextBillingDate(LastBillingToDate: Date) + procedure UpdateNextBillingDate(LastBillingToDate: Date) var NewNextBillingDate: Date; OriginalInvoicedToDate: Date; @@ -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..b845b800a0c 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() @@ -344,7 +363,7 @@ table 8006 "Usage Data Billing" ServiceCommitment.Modify(); end; - internal procedure SetReason(ReasonText: Text) + procedure SetReason(ReasonText: Text) var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -360,7 +379,7 @@ table 8006 "Usage Data Billing" end; end; - internal procedure InitFrom(UsageDataImportEntryNo: Integer; SubscriptionHeaderNo: Code[20]; ProductID: Text[80]; ProductName: Text[100]; + procedure InitFrom(UsageDataImportEntryNo: Integer; SubscriptionHeaderNo: Code[20]; ProductID: Text[80]; ProductName: Text[100]; BillingPeriodStartDate: Date; BillingPeriodEndDate: Date; NewQuantity: Decimal) begin Rec.Init(); @@ -455,23 +474,23 @@ table 8006 "Usage Data Billing" end; end; - internal procedure FilterOnUsageDataImportAndServiceCommitment(UsageDataImportEntryNo: Integer; ServiceCommitment: Record "Subscription Line") + procedure FilterOnUsageDataImportAndServiceCommitment(UsageDataImportEntryNo: Integer; ServiceCommitment: Record "Subscription Line") begin Rec.SetRange("Usage Data Import Entry No.", UsageDataImportEntryNo); Rec.FilterOnServiceCommitment(ServiceCommitment); end; - internal procedure FilterOnServiceCommitment(ServiceCommitment: Record "Subscription Line") + procedure FilterOnServiceCommitment(ServiceCommitment: Record "Subscription Line") begin Rec.SetRange("Subscription Line Entry No.", ServiceCommitment."Entry No."); end; - internal procedure UpdateChargedPeriod() + procedure UpdateChargedPeriod() begin "Charged Period (Days)" := Rec."Charge End Date" - Rec."Charge Start Date" + 1; end; - internal procedure ShowReason() + procedure ShowReason() var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -481,7 +500,7 @@ table 8006 "Usage Data Billing" TextManagement.ShowFieldText(RRef, FieldNo(Reason)); end; - internal procedure GetReason(): Text + procedure GetReason(): Text var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -491,7 +510,7 @@ table 8006 "Usage Data Billing" exit(TextManagement.ReadBlobText(RRef, FieldNo(Reason))); end; - internal procedure ShowRelatedDocuments(var UsageBasedBilling: Record "Usage Data Billing"; DocumentType: Option Contract,"Contract Invoices","Posted Contract Invoices"; ServicePartner: Enum "Service Partner") + procedure ShowRelatedDocuments(var UsageBasedBilling: Record "Usage Data Billing"; DocumentType: Option Contract,"Contract Invoices","Posted Contract Invoices"; ServicePartner: Enum "Service Partner") begin UsageBasedBilling.SetRange(Partner, ServicePartner); case DocumentType of @@ -579,7 +598,7 @@ table 8006 "Usage Data Billing" Page.Run(Page::"Posted Purchase Invoices", PurchInvHeader); end; - internal procedure MarkPurchaseHeaderFromUsageDataBilling(var UsageDataBilling: Record "Usage Data Billing"; var PurchaseHeader: Record "Purchase Header") + procedure MarkPurchaseHeaderFromUsageDataBilling(var UsageDataBilling: Record "Usage Data Billing"; var PurchaseHeader: Record "Purchase Header") begin UsageDataBilling.SetRange("Document Type", "Usage Based Billing Doc. Type"::Invoice); UsageDataBilling.SetFilter("Document No.", '<>%1', ''); @@ -627,14 +646,14 @@ table 8006 "Usage Data Billing" SalesHeader.MarkedOnly(true); end; - internal procedure FilterOnDocumentTypeAndDocumentNo(ServicePartner: Enum "Service Partner"; UsageBasedBillingDocType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]) + procedure FilterOnDocumentTypeAndDocumentNo(ServicePartner: Enum "Service Partner"; UsageBasedBillingDocType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]) begin Rec.SetRange(Partner, ServicePartner); Rec.SetRange("Document Type", UsageBasedBillingDocType); Rec.SetRange("Document No.", DocumentNo); end; - internal procedure SaveDocumentValues(UsageBasedBillingDocType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; DocumentEntryNo: Integer; BillingLineEntryNo: Integer) + procedure SaveDocumentValues(UsageBasedBillingDocType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; DocumentEntryNo: Integer; BillingLineEntryNo: Integer) begin Rec."Document Type" := UsageBasedBillingDocType; Rec."Document No." := DocumentNo; @@ -644,12 +663,12 @@ table 8006 "Usage Data Billing" OnAfterSaveDocumentValues(Rec); end; - internal procedure IsPartnerVendor(): Boolean + procedure IsPartnerVendor(): Boolean begin exit(Rec.Partner = Rec.Partner::Vendor); end; - internal procedure IsPartnerCustomer(): Boolean + procedure IsPartnerCustomer(): Boolean begin exit(Rec.Partner = Rec.Partner::Customer); end; @@ -661,7 +680,7 @@ table 8006 "Usage Data Billing" Rec.SetRange("Subscription Contract Line No.", EntryNo); end; - internal procedure FilterDocumentWithLine(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer) + procedure FilterDocumentWithLine(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer) begin Rec.FilterOnDocumentTypeAndDocumentNo(ServicePartner, DocumentType, DocumentNo); Rec.SetRange("Document Line No.", EntryNo); @@ -682,75 +701,75 @@ table 8006 "Usage Data Billing" Rec.SetRange("Subscription Line Entry No.", EntryNo); end; - internal procedure ShowForContractLine(ServicePartner: Enum "Service Partner"; ContractNo: Code[20]; EntryNo: Integer) + procedure ShowForContractLine(ServicePartner: Enum "Service Partner"; ContractNo: Code[20]; EntryNo: Integer) begin FilterContractLine(ServicePartner, ContractNo, EntryNo); Page.RunModal(Page::"Usage Data Billings", Rec); end; - internal procedure ShowForDocuments(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer) + procedure ShowForDocuments(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer) begin FilterDocumentWithLine(ServicePartner, DocumentType, DocumentNo, EntryNo); Page.RunModal(Page::"Usage Data Billings", Rec); end; - internal procedure ShowForSalesDocuments(DocumentType: Enum "Sales Document Type"; DocumentNo: Code[20]; EntryNo: Integer) + procedure ShowForSalesDocuments(DocumentType: Enum "Sales Document Type"; DocumentNo: Code[20]; EntryNo: Integer) begin ShowForDocuments(Enum::"Service Partner"::Customer, UsageBasedDocTypeConv.ConvertSalesDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo, EntryNo); end; - internal procedure ShowForPurchaseDocuments(DocumentType: Enum "Purchase Document Type"; DocumentNo: Code[20]; EntryNo: Integer) + procedure ShowForPurchaseDocuments(DocumentType: Enum "Purchase Document Type"; DocumentNo: Code[20]; EntryNo: Integer) begin ShowForDocuments(Enum::"Service Partner"::Vendor, UsageBasedDocTypeConv.ConvertPurchaseDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo, EntryNo); end; - internal procedure ShowForRecurringBilling(ServiceObjectNo: Code[20]; ServCommEntryNo: Integer; DocumentType: Enum "Rec. Billing Document Type"; DocumentNo: Code[20]) + procedure ShowForRecurringBilling(ServiceObjectNo: Code[20]; ServCommEntryNo: Integer; DocumentType: Enum "Rec. Billing Document Type"; DocumentNo: Code[20]) begin FilterBillingLine(ServiceObjectNo, ServCommEntryNo, UsageBasedDocTypeConv.ConvertRecurringBillingDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo); Page.RunModal(Page::"Usage Data Billings", Rec); end; - internal procedure ShowForServiceCommitments(ServicePartner: Enum "Service Partner"; ServiceObjectNo: Code[20]; EntryNo: Integer) + procedure ShowForServiceCommitments(ServicePartner: Enum "Service Partner"; ServiceObjectNo: Code[20]; EntryNo: Integer) begin FilterServiceCommitmentLine(ServicePartner, ServiceObjectNo, EntryNo); Page.RunModal(Page::"Usage Data Billings", Rec); end; - internal procedure ExistForContractLine(ServicePartner: Enum "Service Partner"; ContractNo: Code[20]; EntryNo: Integer): Boolean + procedure ExistForContractLine(ServicePartner: Enum "Service Partner"; ContractNo: Code[20]; EntryNo: Integer): Boolean begin FilterContractLine(ServicePartner, ContractNo, EntryNo); exit(not Rec.IsEmpty()); end; - internal procedure ExistForDocuments(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean + procedure ExistForDocuments(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean begin FilterDocumentWithLine(ServicePartner, DocumentType, DocumentNo, EntryNo); exit(not Rec.IsEmpty()); end; - internal procedure ExistForSalesDocuments(DocumentType: Enum "Sales Document Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean + procedure ExistForSalesDocuments(DocumentType: Enum "Sales Document Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean begin exit(ExistForDocuments(Enum::"Service Partner"::Customer, UsageBasedDocTypeConv.ConvertSalesDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo, EntryNo)); end; - internal procedure ExistForPurchaseDocuments(DocumentType: Enum "Purchase Document Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean + procedure ExistForPurchaseDocuments(DocumentType: Enum "Purchase Document Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean begin exit(ExistForDocuments(Enum::"Service Partner"::Vendor, UsageBasedDocTypeConv.ConvertPurchaseDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo, EntryNo)); end; - internal procedure ExistForRecurringBilling(ServiceObjectNo: Code[20]; ServCommEntryNo: Integer; DocumentType: Enum "Rec. Billing Document Type"; DocumentNo: Code[20]): Boolean + procedure ExistForRecurringBilling(ServiceObjectNo: Code[20]; ServCommEntryNo: Integer; DocumentType: Enum "Rec. Billing Document Type"; DocumentNo: Code[20]): Boolean begin FilterBillingLine(ServiceObjectNo, ServCommEntryNo, UsageBasedDocTypeConv.ConvertRecurringBillingDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo); exit(not Rec.IsEmpty()); end; - internal procedure ExistForServiceCommitments(ServicePartner: Enum "Service Partner"; ServiceObjectNo: Code[20]; EntryNo: Integer): Boolean + procedure ExistForServiceCommitments(ServicePartner: Enum "Service Partner"; ServiceObjectNo: Code[20]; EntryNo: Integer): Boolean begin FilterServiceCommitmentLine(ServicePartner, ServiceObjectNo, EntryNo); exit(not Rec.IsEmpty()); end; - internal procedure UpdateRebilling() + procedure UpdateRebilling() var UsageDataBillingMetadata: Record "Usage Data Billing Metadata"; begin @@ -762,7 +781,7 @@ table 8006 "Usage Data Billing" Rec.Rebilling := not UsageDataBillingMetadata.IsEmpty; end; - internal procedure InsertMetadata() + procedure InsertMetadata() var UsageDataBillingMetadata: Record "Usage Data Billing Metadata"; begin @@ -773,7 +792,7 @@ table 8006 "Usage Data Billing" UsageDataBillingMetadata.ModifyAll(Rebilling, Rec.Rebilling, false); end; - internal procedure SetMetadataAsInvoiced() + procedure SetMetadataAsInvoiced() var UsageDataBillingMetadata: Record "Usage Data Billing Metadata"; begin @@ -785,12 +804,12 @@ table 8006 "Usage Data Billing" UsageDataBillingMetadata.ModifyAll(Invoiced, true, false); end; - internal procedure IsInvoiced(): Boolean + procedure IsInvoiced(): Boolean begin 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/UsageDataBillingMetadata.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBillingMetadata.Table.al index 4b006806a07..8d42ea46f37 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBillingMetadata.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBillingMetadata.Table.al @@ -72,7 +72,7 @@ table 8021 "Usage Data Billing Metadata" } } - internal procedure InsertFromUsageDataBilling(UsageDataBilling: Record "Usage Data Billing") + procedure InsertFromUsageDataBilling(UsageDataBilling: Record "Usage Data Billing") var ServiceCommitment: Record "Subscription Line"; NewOriginalInvoicedToDate: Date; @@ -102,7 +102,7 @@ table 8021 "Usage Data Billing Metadata" Insert(true); end; - internal procedure FilterOnServiceCommitment(ServiceCommitmentEntryNo: Integer) + procedure FilterOnServiceCommitment(ServiceCommitmentEntryNo: Integer) begin Rec.SetRange("Subscription Line Entry No.", ServiceCommitmentEntryNo); end; diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBlob.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBlob.Table.al index 74906a6fb10..f1418467a47 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBlob.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBlob.Table.al @@ -93,7 +93,7 @@ table 8011 "Usage Data Blob" Rec.Insert(false); end; - internal procedure ImportFromFile(InStream: InStream; FileName: Text) + procedure ImportFromFile(InStream: InStream; FileName: Text) var OutStream: OutStream; begin @@ -106,7 +106,7 @@ table 8011 "Usage Data Blob" Rec.Modify(false); end; - internal procedure ShowReason() + procedure ShowReason() var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -116,7 +116,7 @@ table 8011 "Usage Data Blob" TextManagement.ShowFieldText(RRef, FieldNo(Reason)); end; - internal procedure ExportData() + procedure ExportData() var InStream: InStream; FileName: Text; 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..d738ba4abb9 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() @@ -349,7 +352,7 @@ table 8018 "Usage Data Generic Import" end; end; - internal procedure SetReason(ReasonText: Text) + procedure SetReason(ReasonText: Text) var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -365,7 +368,7 @@ table 8018 "Usage Data Generic Import" end; end; - internal procedure ShowReason() + procedure ShowReason() var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -375,7 +378,7 @@ table 8018 "Usage Data Generic Import" TextManagement.ShowFieldText(RRef, FieldNo(Reason)); end; - internal procedure GetCurrencyCode(): Code[10] + procedure GetCurrencyCode(): Code[10] var GLSetup: Record "General Ledger Setup"; begin diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataImport.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataImport.Table.al index 4e2d523fd01..43aa2b2db1a 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataImport.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataImport.Table.al @@ -151,13 +151,13 @@ table 8013 "Usage Data Import" UsageDataBlob.DeleteAll(false); end; - internal procedure SetStatus(ProcessingStatus: Enum Microsoft.SubscriptionBilling."Processing Status") + procedure SetStatus(ProcessingStatus: Enum Microsoft.SubscriptionBilling."Processing Status") begin Rec.Validate("Processing Status", ProcessingStatus); Rec.Modify(true); end; - internal procedure SetReason(ReasonText: Text) + procedure SetReason(ReasonText: Text) var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -173,7 +173,7 @@ table 8013 "Usage Data Import" end; end; - internal procedure DeleteUsageDataBillingLines() + procedure DeleteUsageDataBillingLines() var UsageDataSupplier: Record "Usage Data Supplier"; UsageDataProcessing: Interface "Usage Data Processing"; @@ -206,13 +206,13 @@ table 8013 "Usage Data Import" UsageDataBilling.DeleteAll(true); end; - internal procedure SetErrorReason(ErrorText: Text) + procedure SetErrorReason(ErrorText: Text) begin Rec.Validate("Processing Status", Enum::"Processing Status"::Error); Rec.SetReason(ErrorText); end; - internal procedure ShowReason() + procedure ShowReason() var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -255,7 +255,7 @@ table 8013 "Usage Data Import" end; end; - internal procedure ProcessUsageDataImport(var UsageDataImport: Record "Usage Data Import"; ProcessingStep: Enum "Processing Step") + procedure ProcessUsageDataImport(var UsageDataImport: Record "Usage Data Import"; ProcessingStep: Enum "Processing Step") var UsageDataImport2: Record "Usage Data Import"; begin @@ -282,7 +282,7 @@ table 8013 "Usage Data Import" until UsageDataImport.Next() = 0; end; - internal procedure CollectCustomerContractsAndCreateInvoices(var UsageDataImport: Record "Usage Data Import") + procedure CollectCustomerContractsAndCreateInvoices(var UsageDataImport: Record "Usage Data Import") var CustomerContractFilter: Text; CustomerContractLineFilter: Text; @@ -295,7 +295,7 @@ table 8013 "Usage Data Import" CreateCustomerInvoices(CustomerContractFilter, CustomerContractLineFilter); end; - internal procedure CollectVendorContractsAndCreateInvoices(var UsageDataImport: Record "Usage Data Import") + procedure CollectVendorContractsAndCreateInvoices(var UsageDataImport: Record "Usage Data Import") var VendorContractFilter: Text; VendorContractLineFilter: Text; @@ -358,7 +358,7 @@ table 8013 "Usage Data Import" UsageBasedContrSubscribers.CreateContractInvoicesFromUsageDataImport(Enum::"Service Partner"::Vendor, VendorContractFilter, VendorContractLineFilter, ''); end; - internal procedure ShowRelatedDocuments(var UsageDataImport: Record "Usage Data Import"; ServicePartner: Enum "Service Partner"; DocumentType: Option Contract,"Contract Invoices","Posted Contract Invoices") + procedure ShowRelatedDocuments(var UsageDataImport: Record "Usage Data Import"; ServicePartner: Enum "Service Partner"; DocumentType: Option Contract,"Contract Invoices","Posted Contract Invoices") var UsageBasedBilling: Record "Usage Data Billing"; begin diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSuppSubscription.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSuppSubscription.Table.al index 9e0949c1769..98f3e8026a3 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSuppSubscription.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSuppSubscription.Table.al @@ -250,13 +250,13 @@ table 8016 "Usage Data Supp. Subscription" Error(SubscriptionCannotBeConnectedErr); end; - internal procedure SetErrorReason(ErrorText: Text) + procedure SetErrorReason(ErrorText: Text) begin Rec.Validate("Processing Status", Enum::"Processing Status"::Error); Rec.SetReason(ErrorText); end; - internal procedure ShowReason() + procedure ShowReason() var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -266,7 +266,7 @@ table 8016 "Usage Data Supp. Subscription" TextManagement.ShowFieldText(RRef, FieldNo(Reason)); end; - internal procedure SetReason(ReasonText: Text) + procedure SetReason(ReasonText: Text) var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -282,7 +282,7 @@ table 8016 "Usage Data Supp. Subscription" end; end; - internal procedure ResetProcessingStatus(var UsageDataSubscription: Record "Usage Data Supp. Subscription") + procedure ResetProcessingStatus(var UsageDataSubscription: Record "Usage Data Supp. Subscription") begin if UsageDataSubscription.FindSet(true) then repeat @@ -291,7 +291,7 @@ table 8016 "Usage Data Supp. Subscription" until UsageDataSubscription.Next() = 0; end; - internal procedure ResetServiceObjectAndServiceCommitment() + procedure ResetServiceObjectAndServiceCommitment() begin if Rec."Entry No." = 0 then exit; @@ -300,14 +300,14 @@ table 8016 "Usage Data Supp. Subscription" Rec.Modify(true); end; - internal procedure FindForSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]): Boolean + procedure FindForSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]): Boolean begin Rec.SetRange("Supplier No.", SupplierNo); Rec.SetRange("Supplier Reference", SupplierReference); exit(Rec.FindFirst()); end; - internal procedure UpdateSubscriptionHeaderNoInImportedData() + procedure UpdateSubscriptionHeaderNoInImportedData() var UsageDataSupplier: Record "Usage Data Supplier"; UsageDataProcessing: Interface "Usage Data Processing"; diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplier.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplier.Table.al index b321c0409f3..3a2d8eb7f0f 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplier.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplier.Table.al @@ -100,7 +100,7 @@ table 8014 "Usage Data Supplier" UsageDataProcessing.DeleteSupplierData(Rec); end; - internal procedure OpenSupplierSettings() + procedure OpenSupplierSettings() var UsageDataProcessing: Interface "Usage Data Processing"; begin diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplierReference.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplierReference.Table.al index f1ffe3be364..b304de05339 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplierReference.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplierReference.Table.al @@ -58,7 +58,7 @@ table 8015 "Usage Data Supplier Reference" } } - internal procedure CreateSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type") + procedure CreateSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type") begin if Rec.FindSupplierReference(SupplierNo, SupplierReference, ReferenceType) then begin Rec.Reset(); @@ -73,13 +73,13 @@ table 8015 "Usage Data Supplier Reference" Rec.Reset(); end; - internal procedure FindSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type"): Boolean + procedure FindSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type"): Boolean begin Rec.FilterUsageDataSupplierReference(SupplierNo, SupplierReference, ReferenceType); exit(Rec.FindFirst()); end; - internal procedure FilterUsageDataSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type") + procedure FilterUsageDataSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type") begin Rec.SetCurrentKey("Supplier No.", "Supplier Reference", Type); Rec.SetRange("Supplier No.", SupplierNo); From b09468315ece3e17ba87c18943e8db768a0718d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Sun, 12 Jul 2026 23:26:18 +0200 Subject: [PATCH 2/3] Address PR review feedback - Move ClearingBillingProposalLbl, DeletingBillingLinesLbl, DeletingDocumentsLbl from procedure-local var blocks to the codeunit object-level var block in BillingProposal (BCQuality: labels must be declared at object scope for correct XLIFF extraction) - Replace TempBillingLine.Indent overload in CreateBillingDocuments with a dedicated BillingLineNoByTempEntryNo dictionary (Dictionary of [Integer, Integer]) keyed by temp Entry No.; avoids repurposing a display-semantics field for an unrelated value - Revert ~40 internal -> procedure visibility changes in UsageDataBilling, UsageDataImport, UsageDataBlob, UsageDataGenericImport, UsageDataSuppSubscription, UsageDataSupplier, UsageDataSupplierReference, UsageDataBillingMetadata, SubscriptionLine (UpdateNextBillingDate), and CreateCustomerBillingDocs / CreateVendorBillingDocs (GetData/SetData); these are unrelated to the performance work in this PR and will be proposed separately Co-Authored-By: Claude Sonnet 4.6 --- .../Codeunits/BillingProposal.Codeunit.al | 6 +- .../CreateBillingDocuments.Codeunit.al | 16 ++--- .../Pages/CreateCustomerBillingDocs.Page.al | 4 +- .../Pages/CreateVendorBillingDocs.Page.al | 4 +- .../Tables/SubscriptionLine.Table.al | 2 +- .../Tables/UsageDataBilling.Table.al | 60 +++++++++---------- .../Tables/UsageDataBillingMetadata.Table.al | 4 +- .../Tables/UsageDataBlob.Table.al | 6 +- .../Tables/UsageDataGenericImport.Table.al | 6 +- .../Tables/UsageDataImport.Table.al | 18 +++--- .../Tables/UsageDataSuppSubscription.Table.al | 14 ++--- .../Tables/UsageDataSupplier.Table.al | 2 +- .../UsageDataSupplierReference.Table.al | 6 +- 13 files changed, 70 insertions(+), 78 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 ae3541b560e..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 @@ -29,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 @@ -656,7 +659,6 @@ codeunit 8062 "Billing Proposal" ClearBillingProposalOptionsTxt: Label 'All billing proposals, Only current billing template proposal'; ClearBillingProposalOptionsMySuggestionsOnlyTxt: Label 'All billing proposals (user %1 only), Only current billing template proposal', Comment = '%1: User ID'; ClearBillingProposalQst: Label 'Which billing proposal(s) should be deleted?'; - ClearingBillingProposalLbl: Label 'Clearing billing proposal...'; StrMenuResponse: Integer; Counter: Integer; begin @@ -702,7 +704,6 @@ codeunit 8062 "Billing Proposal" internal procedure DeleteBillingLines(var BillingLine: Record "Billing Line") var - DeletingBillingLinesLbl: Label 'Deleting billing lines...'; Counter: Integer; begin BillingLine.SetCurrentKey("Subscription Header No.", "Subscription Line Entry No.", "Billing to"); @@ -1017,7 +1018,6 @@ 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'; - DeletingDocumentsLbl: Label 'Deleting billing documents...'; PreviousDocumentNo: Code[20]; StrMenuResponse: Integer; Counter: Integer; 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 d5f654e74d3..c73541c4bbd 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 @@ -327,10 +327,7 @@ codeunit 8060 "Create Billing Documents" UsageDataBilling.SetRange("Document Type", Enum::"Usage Based Billing Doc. Type"::None); UsageDataBilling.SetRange("Document No.", ''); if UsageDataBilling.FindSet() then begin - // TempBillingLine.Indent holds the source Billing Line "Entry No." captured during aggregation - // in CreateTempBillingLines; reused here as the Billing Line link for Usage Data Billing - // instead of re-querying Billing Line. - BillingLineNo := TempBillingLine.Indent; + BillingLineNoByTempEntryNo.Get(TempBillingLine."Entry No.", BillingLineNo); repeat UsageDataBilling.SaveDocumentValues(UsageBasedDocTypeConv.ConvertSalesDocTypeToUsageBasedBillingDocType(SalesLine."Document Type"), SalesLine."Document No.", SalesLine."Line No.", BillingLineNo); @@ -446,10 +443,7 @@ codeunit 8060 "Create Billing Documents" UsageDataBilling.SetRange("Document Type", Enum::"Usage Based Billing Doc. Type"::None); UsageDataBilling.SetRange("Document No.", ''); if UsageDataBilling.FindSet() then begin - // TempBillingLine.Indent holds the source Billing Line "Entry No." captured during aggregation - // in CreateTempBillingLines; reused here as the Billing Line link for Usage Data Billing - // instead of re-querying Billing Line. - BillingLineNo := TempBillingLine.Indent; + BillingLineNoByTempEntryNo.Get(TempBillingLine."Entry No.", BillingLineNo); repeat UsageDataBilling.SaveDocumentValues(UsageBasedDocTypeConv.ConvertPurchaseDocTypeToUsageBasedBillingDocType(PurchaseLine."Document Type"), PurchaseLine."Document No.", PurchaseLine."Line No.", BillingLineNo); @@ -822,10 +816,7 @@ codeunit 8060 "Create Billing Documents" OnCreateTempBillingLinesBeforeSaveTempBillingLine(TempBillingLine, BillingLine); TempBillingLine."Unit Cost" += BillingLine."Unit Cost"; TempBillingLine."Unit Cost (LCY)" += BillingLine."Unit Cost (LCY)"; - // Carry the (last) source Billing Line "Entry No." on the aggregated temp line via the unused - // Indent field, so the usage data billing link can be stamped without re-querying Billing Line - // (replaces a per-line GetBillingLineNo FindLast). - TempBillingLine.Indent := BillingLine."Entry No."; + BillingLineNoByTempEntryNo.Set(TempBillingLine."Entry No.", BillingLine."Entry No."); TempBillingLine.Modify(false); until BillingLine.Next() = 0; end; @@ -1502,6 +1493,7 @@ codeunit 8060 "Create Billing Documents" Language: Codeunit Language; ProgressTracker: Codeunit "Progress Tracker"; BillingPriceCalcSkip: Codeunit "Billing Price Calc. Skip"; + BillingLineNoByTempEntryNo: Dictionary of [Integer, Integer]; DocumentDate: Date; PostingDate: Date; CustomerRecurringBillingGrouping: Enum "Customer Rec. Billing Grouping"; 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 71b8322cff9..d47cea70b7b 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 @@ -55,7 +55,7 @@ page 8072 "Create Customer Billing Docs" PostDocuments: Boolean; Grouping: Enum "Customer Rec. Billing Grouping"; - procedure GetData(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Customer Rec. Billing Grouping"; var NewPostDocuments: Boolean) + internal procedure GetData(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Customer Rec. Billing Grouping"; var NewPostDocuments: Boolean) begin NewDocumentDate := DocumentDate; NewPostingDate := PostingDate; @@ -63,7 +63,7 @@ page 8072 "Create Customer Billing Docs" NewPostDocuments := PostDocuments; end; - procedure SetData(NewDocumentDate: Date; NewPostingDate: Date; NewGroupingType: Enum "Customer Rec. Billing Grouping"; 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 aa6b2c89d0b..a71b150937a 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 @@ -50,14 +50,14 @@ page 8077 "Create Vendor Billing Docs" PostingDate: Date; Grouping: Enum "Vendor Rec. Billing Grouping"; - procedure GetData(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Vendor Rec. Billing Grouping") + internal procedure GetData(var NewDocumentDate: Date; var NewPostingDate: Date; var NewGroupingType: Enum "Vendor Rec. Billing Grouping") begin NewDocumentDate := DocumentDate; NewPostingDate := PostingDate; NewGroupingType := Grouping; end; - procedure SetData(NewDocumentDate: Date; NewPostingDate: Date; 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/Service Commitments/Tables/SubscriptionLine.Table.al b/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al index 95369f04f15..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 @@ -906,7 +906,7 @@ table 8059 "Subscription Line" until BillingLine.Next() = 0; end; - procedure UpdateNextBillingDate(LastBillingToDate: Date) + internal procedure UpdateNextBillingDate(LastBillingToDate: Date) var NewNextBillingDate: Date; OriginalInvoicedToDate: Date; 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 b845b800a0c..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 @@ -363,7 +363,7 @@ table 8006 "Usage Data Billing" ServiceCommitment.Modify(); end; - procedure SetReason(ReasonText: Text) + internal procedure SetReason(ReasonText: Text) var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -379,7 +379,7 @@ table 8006 "Usage Data Billing" end; end; - procedure InitFrom(UsageDataImportEntryNo: Integer; SubscriptionHeaderNo: Code[20]; ProductID: Text[80]; ProductName: Text[100]; + internal procedure InitFrom(UsageDataImportEntryNo: Integer; SubscriptionHeaderNo: Code[20]; ProductID: Text[80]; ProductName: Text[100]; BillingPeriodStartDate: Date; BillingPeriodEndDate: Date; NewQuantity: Decimal) begin Rec.Init(); @@ -474,23 +474,23 @@ table 8006 "Usage Data Billing" end; end; - procedure FilterOnUsageDataImportAndServiceCommitment(UsageDataImportEntryNo: Integer; ServiceCommitment: Record "Subscription Line") + internal procedure FilterOnUsageDataImportAndServiceCommitment(UsageDataImportEntryNo: Integer; ServiceCommitment: Record "Subscription Line") begin Rec.SetRange("Usage Data Import Entry No.", UsageDataImportEntryNo); Rec.FilterOnServiceCommitment(ServiceCommitment); end; - procedure FilterOnServiceCommitment(ServiceCommitment: Record "Subscription Line") + internal procedure FilterOnServiceCommitment(ServiceCommitment: Record "Subscription Line") begin Rec.SetRange("Subscription Line Entry No.", ServiceCommitment."Entry No."); end; - procedure UpdateChargedPeriod() + internal procedure UpdateChargedPeriod() begin "Charged Period (Days)" := Rec."Charge End Date" - Rec."Charge Start Date" + 1; end; - procedure ShowReason() + internal procedure ShowReason() var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -500,7 +500,7 @@ table 8006 "Usage Data Billing" TextManagement.ShowFieldText(RRef, FieldNo(Reason)); end; - procedure GetReason(): Text + internal procedure GetReason(): Text var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -510,7 +510,7 @@ table 8006 "Usage Data Billing" exit(TextManagement.ReadBlobText(RRef, FieldNo(Reason))); end; - procedure ShowRelatedDocuments(var UsageBasedBilling: Record "Usage Data Billing"; DocumentType: Option Contract,"Contract Invoices","Posted Contract Invoices"; ServicePartner: Enum "Service Partner") + internal procedure ShowRelatedDocuments(var UsageBasedBilling: Record "Usage Data Billing"; DocumentType: Option Contract,"Contract Invoices","Posted Contract Invoices"; ServicePartner: Enum "Service Partner") begin UsageBasedBilling.SetRange(Partner, ServicePartner); case DocumentType of @@ -598,7 +598,7 @@ table 8006 "Usage Data Billing" Page.Run(Page::"Posted Purchase Invoices", PurchInvHeader); end; - procedure MarkPurchaseHeaderFromUsageDataBilling(var UsageDataBilling: Record "Usage Data Billing"; var PurchaseHeader: Record "Purchase Header") + internal procedure MarkPurchaseHeaderFromUsageDataBilling(var UsageDataBilling: Record "Usage Data Billing"; var PurchaseHeader: Record "Purchase Header") begin UsageDataBilling.SetRange("Document Type", "Usage Based Billing Doc. Type"::Invoice); UsageDataBilling.SetFilter("Document No.", '<>%1', ''); @@ -646,14 +646,14 @@ table 8006 "Usage Data Billing" SalesHeader.MarkedOnly(true); end; - procedure FilterOnDocumentTypeAndDocumentNo(ServicePartner: Enum "Service Partner"; UsageBasedBillingDocType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]) + internal procedure FilterOnDocumentTypeAndDocumentNo(ServicePartner: Enum "Service Partner"; UsageBasedBillingDocType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]) begin Rec.SetRange(Partner, ServicePartner); Rec.SetRange("Document Type", UsageBasedBillingDocType); Rec.SetRange("Document No.", DocumentNo); end; - procedure SaveDocumentValues(UsageBasedBillingDocType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; DocumentEntryNo: Integer; BillingLineEntryNo: Integer) + internal procedure SaveDocumentValues(UsageBasedBillingDocType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; DocumentEntryNo: Integer; BillingLineEntryNo: Integer) begin Rec."Document Type" := UsageBasedBillingDocType; Rec."Document No." := DocumentNo; @@ -663,12 +663,12 @@ table 8006 "Usage Data Billing" OnAfterSaveDocumentValues(Rec); end; - procedure IsPartnerVendor(): Boolean + internal procedure IsPartnerVendor(): Boolean begin exit(Rec.Partner = Rec.Partner::Vendor); end; - procedure IsPartnerCustomer(): Boolean + internal procedure IsPartnerCustomer(): Boolean begin exit(Rec.Partner = Rec.Partner::Customer); end; @@ -680,7 +680,7 @@ table 8006 "Usage Data Billing" Rec.SetRange("Subscription Contract Line No.", EntryNo); end; - procedure FilterDocumentWithLine(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer) + internal procedure FilterDocumentWithLine(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer) begin Rec.FilterOnDocumentTypeAndDocumentNo(ServicePartner, DocumentType, DocumentNo); Rec.SetRange("Document Line No.", EntryNo); @@ -701,75 +701,75 @@ table 8006 "Usage Data Billing" Rec.SetRange("Subscription Line Entry No.", EntryNo); end; - procedure ShowForContractLine(ServicePartner: Enum "Service Partner"; ContractNo: Code[20]; EntryNo: Integer) + internal procedure ShowForContractLine(ServicePartner: Enum "Service Partner"; ContractNo: Code[20]; EntryNo: Integer) begin FilterContractLine(ServicePartner, ContractNo, EntryNo); Page.RunModal(Page::"Usage Data Billings", Rec); end; - procedure ShowForDocuments(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer) + internal procedure ShowForDocuments(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer) begin FilterDocumentWithLine(ServicePartner, DocumentType, DocumentNo, EntryNo); Page.RunModal(Page::"Usage Data Billings", Rec); end; - procedure ShowForSalesDocuments(DocumentType: Enum "Sales Document Type"; DocumentNo: Code[20]; EntryNo: Integer) + internal procedure ShowForSalesDocuments(DocumentType: Enum "Sales Document Type"; DocumentNo: Code[20]; EntryNo: Integer) begin ShowForDocuments(Enum::"Service Partner"::Customer, UsageBasedDocTypeConv.ConvertSalesDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo, EntryNo); end; - procedure ShowForPurchaseDocuments(DocumentType: Enum "Purchase Document Type"; DocumentNo: Code[20]; EntryNo: Integer) + internal procedure ShowForPurchaseDocuments(DocumentType: Enum "Purchase Document Type"; DocumentNo: Code[20]; EntryNo: Integer) begin ShowForDocuments(Enum::"Service Partner"::Vendor, UsageBasedDocTypeConv.ConvertPurchaseDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo, EntryNo); end; - procedure ShowForRecurringBilling(ServiceObjectNo: Code[20]; ServCommEntryNo: Integer; DocumentType: Enum "Rec. Billing Document Type"; DocumentNo: Code[20]) + internal procedure ShowForRecurringBilling(ServiceObjectNo: Code[20]; ServCommEntryNo: Integer; DocumentType: Enum "Rec. Billing Document Type"; DocumentNo: Code[20]) begin FilterBillingLine(ServiceObjectNo, ServCommEntryNo, UsageBasedDocTypeConv.ConvertRecurringBillingDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo); Page.RunModal(Page::"Usage Data Billings", Rec); end; - procedure ShowForServiceCommitments(ServicePartner: Enum "Service Partner"; ServiceObjectNo: Code[20]; EntryNo: Integer) + internal procedure ShowForServiceCommitments(ServicePartner: Enum "Service Partner"; ServiceObjectNo: Code[20]; EntryNo: Integer) begin FilterServiceCommitmentLine(ServicePartner, ServiceObjectNo, EntryNo); Page.RunModal(Page::"Usage Data Billings", Rec); end; - procedure ExistForContractLine(ServicePartner: Enum "Service Partner"; ContractNo: Code[20]; EntryNo: Integer): Boolean + internal procedure ExistForContractLine(ServicePartner: Enum "Service Partner"; ContractNo: Code[20]; EntryNo: Integer): Boolean begin FilterContractLine(ServicePartner, ContractNo, EntryNo); exit(not Rec.IsEmpty()); end; - procedure ExistForDocuments(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean + internal procedure ExistForDocuments(ServicePartner: Enum "Service Partner"; DocumentType: Enum "Usage Based Billing Doc. Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean begin FilterDocumentWithLine(ServicePartner, DocumentType, DocumentNo, EntryNo); exit(not Rec.IsEmpty()); end; - procedure ExistForSalesDocuments(DocumentType: Enum "Sales Document Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean + internal procedure ExistForSalesDocuments(DocumentType: Enum "Sales Document Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean begin exit(ExistForDocuments(Enum::"Service Partner"::Customer, UsageBasedDocTypeConv.ConvertSalesDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo, EntryNo)); end; - procedure ExistForPurchaseDocuments(DocumentType: Enum "Purchase Document Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean + internal procedure ExistForPurchaseDocuments(DocumentType: Enum "Purchase Document Type"; DocumentNo: Code[20]; EntryNo: Integer): Boolean begin exit(ExistForDocuments(Enum::"Service Partner"::Vendor, UsageBasedDocTypeConv.ConvertPurchaseDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo, EntryNo)); end; - procedure ExistForRecurringBilling(ServiceObjectNo: Code[20]; ServCommEntryNo: Integer; DocumentType: Enum "Rec. Billing Document Type"; DocumentNo: Code[20]): Boolean + internal procedure ExistForRecurringBilling(ServiceObjectNo: Code[20]; ServCommEntryNo: Integer; DocumentType: Enum "Rec. Billing Document Type"; DocumentNo: Code[20]): Boolean begin FilterBillingLine(ServiceObjectNo, ServCommEntryNo, UsageBasedDocTypeConv.ConvertRecurringBillingDocTypeToUsageBasedBillingDocType(DocumentType), DocumentNo); exit(not Rec.IsEmpty()); end; - procedure ExistForServiceCommitments(ServicePartner: Enum "Service Partner"; ServiceObjectNo: Code[20]; EntryNo: Integer): Boolean + internal procedure ExistForServiceCommitments(ServicePartner: Enum "Service Partner"; ServiceObjectNo: Code[20]; EntryNo: Integer): Boolean begin FilterServiceCommitmentLine(ServicePartner, ServiceObjectNo, EntryNo); exit(not Rec.IsEmpty()); end; - procedure UpdateRebilling() + internal procedure UpdateRebilling() var UsageDataBillingMetadata: Record "Usage Data Billing Metadata"; begin @@ -781,7 +781,7 @@ table 8006 "Usage Data Billing" Rec.Rebilling := not UsageDataBillingMetadata.IsEmpty; end; - procedure InsertMetadata() + internal procedure InsertMetadata() var UsageDataBillingMetadata: Record "Usage Data Billing Metadata"; begin @@ -792,7 +792,7 @@ table 8006 "Usage Data Billing" UsageDataBillingMetadata.ModifyAll(Rebilling, Rec.Rebilling, false); end; - procedure SetMetadataAsInvoiced() + internal procedure SetMetadataAsInvoiced() var UsageDataBillingMetadata: Record "Usage Data Billing Metadata"; begin @@ -804,7 +804,7 @@ table 8006 "Usage Data Billing" UsageDataBillingMetadata.ModifyAll(Invoiced, true, false); end; - procedure IsInvoiced(): Boolean + internal procedure IsInvoiced(): Boolean begin exit((Rec."Document Type" <> "Usage Based Billing Doc. Type"::None) and (Rec."Document No." <> '')); end; diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBillingMetadata.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBillingMetadata.Table.al index 8d42ea46f37..4b006806a07 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBillingMetadata.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBillingMetadata.Table.al @@ -72,7 +72,7 @@ table 8021 "Usage Data Billing Metadata" } } - procedure InsertFromUsageDataBilling(UsageDataBilling: Record "Usage Data Billing") + internal procedure InsertFromUsageDataBilling(UsageDataBilling: Record "Usage Data Billing") var ServiceCommitment: Record "Subscription Line"; NewOriginalInvoicedToDate: Date; @@ -102,7 +102,7 @@ table 8021 "Usage Data Billing Metadata" Insert(true); end; - procedure FilterOnServiceCommitment(ServiceCommitmentEntryNo: Integer) + internal procedure FilterOnServiceCommitment(ServiceCommitmentEntryNo: Integer) begin Rec.SetRange("Subscription Line Entry No.", ServiceCommitmentEntryNo); end; diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBlob.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBlob.Table.al index f1418467a47..74906a6fb10 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBlob.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataBlob.Table.al @@ -93,7 +93,7 @@ table 8011 "Usage Data Blob" Rec.Insert(false); end; - procedure ImportFromFile(InStream: InStream; FileName: Text) + internal procedure ImportFromFile(InStream: InStream; FileName: Text) var OutStream: OutStream; begin @@ -106,7 +106,7 @@ table 8011 "Usage Data Blob" Rec.Modify(false); end; - procedure ShowReason() + internal procedure ShowReason() var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -116,7 +116,7 @@ table 8011 "Usage Data Blob" TextManagement.ShowFieldText(RRef, FieldNo(Reason)); end; - procedure ExportData() + internal procedure ExportData() var InStream: InStream; FileName: Text; 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 d738ba4abb9..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 @@ -352,7 +352,7 @@ table 8018 "Usage Data Generic Import" end; end; - procedure SetReason(ReasonText: Text) + internal procedure SetReason(ReasonText: Text) var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -368,7 +368,7 @@ table 8018 "Usage Data Generic Import" end; end; - procedure ShowReason() + internal procedure ShowReason() var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -378,7 +378,7 @@ table 8018 "Usage Data Generic Import" TextManagement.ShowFieldText(RRef, FieldNo(Reason)); end; - procedure GetCurrencyCode(): Code[10] + internal procedure GetCurrencyCode(): Code[10] var GLSetup: Record "General Ledger Setup"; begin diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataImport.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataImport.Table.al index 43aa2b2db1a..4e2d523fd01 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataImport.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataImport.Table.al @@ -151,13 +151,13 @@ table 8013 "Usage Data Import" UsageDataBlob.DeleteAll(false); end; - procedure SetStatus(ProcessingStatus: Enum Microsoft.SubscriptionBilling."Processing Status") + internal procedure SetStatus(ProcessingStatus: Enum Microsoft.SubscriptionBilling."Processing Status") begin Rec.Validate("Processing Status", ProcessingStatus); Rec.Modify(true); end; - procedure SetReason(ReasonText: Text) + internal procedure SetReason(ReasonText: Text) var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -173,7 +173,7 @@ table 8013 "Usage Data Import" end; end; - procedure DeleteUsageDataBillingLines() + internal procedure DeleteUsageDataBillingLines() var UsageDataSupplier: Record "Usage Data Supplier"; UsageDataProcessing: Interface "Usage Data Processing"; @@ -206,13 +206,13 @@ table 8013 "Usage Data Import" UsageDataBilling.DeleteAll(true); end; - procedure SetErrorReason(ErrorText: Text) + internal procedure SetErrorReason(ErrorText: Text) begin Rec.Validate("Processing Status", Enum::"Processing Status"::Error); Rec.SetReason(ErrorText); end; - procedure ShowReason() + internal procedure ShowReason() var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -255,7 +255,7 @@ table 8013 "Usage Data Import" end; end; - procedure ProcessUsageDataImport(var UsageDataImport: Record "Usage Data Import"; ProcessingStep: Enum "Processing Step") + internal procedure ProcessUsageDataImport(var UsageDataImport: Record "Usage Data Import"; ProcessingStep: Enum "Processing Step") var UsageDataImport2: Record "Usage Data Import"; begin @@ -282,7 +282,7 @@ table 8013 "Usage Data Import" until UsageDataImport.Next() = 0; end; - procedure CollectCustomerContractsAndCreateInvoices(var UsageDataImport: Record "Usage Data Import") + internal procedure CollectCustomerContractsAndCreateInvoices(var UsageDataImport: Record "Usage Data Import") var CustomerContractFilter: Text; CustomerContractLineFilter: Text; @@ -295,7 +295,7 @@ table 8013 "Usage Data Import" CreateCustomerInvoices(CustomerContractFilter, CustomerContractLineFilter); end; - procedure CollectVendorContractsAndCreateInvoices(var UsageDataImport: Record "Usage Data Import") + internal procedure CollectVendorContractsAndCreateInvoices(var UsageDataImport: Record "Usage Data Import") var VendorContractFilter: Text; VendorContractLineFilter: Text; @@ -358,7 +358,7 @@ table 8013 "Usage Data Import" UsageBasedContrSubscribers.CreateContractInvoicesFromUsageDataImport(Enum::"Service Partner"::Vendor, VendorContractFilter, VendorContractLineFilter, ''); end; - procedure ShowRelatedDocuments(var UsageDataImport: Record "Usage Data Import"; ServicePartner: Enum "Service Partner"; DocumentType: Option Contract,"Contract Invoices","Posted Contract Invoices") + internal procedure ShowRelatedDocuments(var UsageDataImport: Record "Usage Data Import"; ServicePartner: Enum "Service Partner"; DocumentType: Option Contract,"Contract Invoices","Posted Contract Invoices") var UsageBasedBilling: Record "Usage Data Billing"; begin diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSuppSubscription.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSuppSubscription.Table.al index 98f3e8026a3..9e0949c1769 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSuppSubscription.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSuppSubscription.Table.al @@ -250,13 +250,13 @@ table 8016 "Usage Data Supp. Subscription" Error(SubscriptionCannotBeConnectedErr); end; - procedure SetErrorReason(ErrorText: Text) + internal procedure SetErrorReason(ErrorText: Text) begin Rec.Validate("Processing Status", Enum::"Processing Status"::Error); Rec.SetReason(ErrorText); end; - procedure ShowReason() + internal procedure ShowReason() var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -266,7 +266,7 @@ table 8016 "Usage Data Supp. Subscription" TextManagement.ShowFieldText(RRef, FieldNo(Reason)); end; - procedure SetReason(ReasonText: Text) + internal procedure SetReason(ReasonText: Text) var TextManagement: Codeunit "Text Management"; RRef: RecordRef; @@ -282,7 +282,7 @@ table 8016 "Usage Data Supp. Subscription" end; end; - procedure ResetProcessingStatus(var UsageDataSubscription: Record "Usage Data Supp. Subscription") + internal procedure ResetProcessingStatus(var UsageDataSubscription: Record "Usage Data Supp. Subscription") begin if UsageDataSubscription.FindSet(true) then repeat @@ -291,7 +291,7 @@ table 8016 "Usage Data Supp. Subscription" until UsageDataSubscription.Next() = 0; end; - procedure ResetServiceObjectAndServiceCommitment() + internal procedure ResetServiceObjectAndServiceCommitment() begin if Rec."Entry No." = 0 then exit; @@ -300,14 +300,14 @@ table 8016 "Usage Data Supp. Subscription" Rec.Modify(true); end; - procedure FindForSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]): Boolean + internal procedure FindForSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]): Boolean begin Rec.SetRange("Supplier No.", SupplierNo); Rec.SetRange("Supplier Reference", SupplierReference); exit(Rec.FindFirst()); end; - procedure UpdateSubscriptionHeaderNoInImportedData() + internal procedure UpdateSubscriptionHeaderNoInImportedData() var UsageDataSupplier: Record "Usage Data Supplier"; UsageDataProcessing: Interface "Usage Data Processing"; diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplier.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplier.Table.al index 3a2d8eb7f0f..b321c0409f3 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplier.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplier.Table.al @@ -100,7 +100,7 @@ table 8014 "Usage Data Supplier" UsageDataProcessing.DeleteSupplierData(Rec); end; - procedure OpenSupplierSettings() + internal procedure OpenSupplierSettings() var UsageDataProcessing: Interface "Usage Data Processing"; begin diff --git a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplierReference.Table.al b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplierReference.Table.al index b304de05339..f1ffe3be364 100644 --- a/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplierReference.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Usage Based Billing/Tables/UsageDataSupplierReference.Table.al @@ -58,7 +58,7 @@ table 8015 "Usage Data Supplier Reference" } } - procedure CreateSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type") + internal procedure CreateSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type") begin if Rec.FindSupplierReference(SupplierNo, SupplierReference, ReferenceType) then begin Rec.Reset(); @@ -73,13 +73,13 @@ table 8015 "Usage Data Supplier Reference" Rec.Reset(); end; - procedure FindSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type"): Boolean + internal procedure FindSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type"): Boolean begin Rec.FilterUsageDataSupplierReference(SupplierNo, SupplierReference, ReferenceType); exit(Rec.FindFirst()); end; - procedure FilterUsageDataSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type") + internal procedure FilterUsageDataSupplierReference(SupplierNo: Code[20]; SupplierReference: Text[80]; ReferenceType: Enum "Usage Data Reference Type") begin Rec.SetCurrentKey("Supplier No.", "Supplier Reference", Type); Rec.SetRange("Supplier No.", SupplierNo); From 08c1c85401aec9c075f66078123647dfd2596c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miljan=20Milosavljevi=C4=87?= Date: Sun, 19 Jul 2026 22:41:54 +0200 Subject: [PATCH 3/3] Address PR Iteration 2 review feedback - RecurringBilling: fix position restore for group header rows after RefreshGroupsInTempTable. Header rows are reinserted with new negative Entry Nos. on every rebuild, so Rec.Get(CurrentEntryNo) always failed for them. Now the group key (contract no. or partner no.) is remembered before the rebuild and the header is re-located afterwards via a shared-table copy cursor, leaving the page record's own view filters (User ID / Partner) untouched, then positioned with Rec.Get. - Drop the "Billing Price Calc. Skip" manual-subscriber codeunit and its Bind/UnbindSubscription calls in Create Billing Documents. The codeunit was a public manual subscriber any extension could bind, and there was no guaranteed unbind if a Validate/TestField errored mid-line. Removing it eliminates both the external-binding surface and the unbind-leak risk; the platform's normal unit price/cost calculation runs again on each line. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../BillingPriceCalcSkip.Codeunit.al | 46 ------------------- .../CreateBillingDocuments.Codeunit.al | 9 ---- .../Billing/Pages/RecurringBilling.Page.al | 35 +++++++++++++- 3 files changed, 34 insertions(+), 56 deletions(-) delete mode 100644 src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingPriceCalcSkip.Codeunit.al diff --git a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingPriceCalcSkip.Codeunit.al b/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingPriceCalcSkip.Codeunit.al deleted file mode 100644 index c3a8e3ba5b0..00000000000 --- a/src/Apps/W1/Subscription Billing/App/Billing/Codeunits/BillingPriceCalcSkip.Codeunit.al +++ /dev/null @@ -1,46 +0,0 @@ -namespace Microsoft.SubscriptionBilling; - -using Microsoft.Purchases.Document; -using Microsoft.Sales.Document; - -/// -/// Manual-binding helper used by "Create Billing Documents" to skip the redundant unit price / unit cost -/// engine work that the platform runs while validating the created Sales and Purchase lines. The billing -/// flow already assigns the known Unit Price and Unit Cost explicitly (the values come from the Billing -/// Line), so the price-list and cost lookups inside those Validate calls are wasted work on every line. -/// Only the price/cost calculation is skipped - all other validation side effects (dimensions, VAT, -/// posting groups, item defaults) still run. Bind this instance only around the line field initialization -/// via BindSubscription / UnbindSubscription. -/// -codeunit 8036 "Billing Price Calc. Skip" -{ - EventSubscriberInstance = Manual; - - [EventSubscriber(ObjectType::Table, Database::"Sales Line", OnBeforeUpdateUnitPrice, '', false, false)] - local procedure SkipSalesUpdateUnitPrice(var SalesLine: Record "Sales Line"; xSalesLine: Record "Sales Line"; CalledByFieldNo: Integer; CurrFieldNo: Integer; var Handled: Boolean) - begin - Handled := true; - end; - - [EventSubscriber(ObjectType::Table, Database::"Sales Line", OnBeforeGetUnitCost, '', false, false)] - local procedure SkipSalesGetUnitCost(var SalesLine: Record "Sales Line"; var IsHandled: Boolean) - begin - IsHandled := true; - end; - - // Suppresses UoM quantity conversion while the billing line is being initialised. At the point this - // subscriber fires, the Sales Line quantity is still 0 (the billing amount is assigned afterwards), - // so the platform's UoM conversion logic produces no meaningful result and only adds overhead. All - // UoM-driven quantity validation runs normally once BillingPriceCalcSkip is unbound. - [EventSubscriber(ObjectType::Table, Database::"Sales Line", OnBeforeUpdateQuantityFromUOMCode, '', false, false)] - local procedure SkipSalesUpdateQuantityFromUOMCode(var SalesLine: Record "Sales Line"; var IsHandled: Boolean) - begin - IsHandled := true; - end; - - [EventSubscriber(ObjectType::Table, Database::"Purchase Line", OnBeforeUpdateUnitCost, '', false, false)] - local procedure SkipPurchaseUpdateUnitCost(var PurchaseLine: Record "Purchase Line"; xPurchaseLine: Record "Purchase Line"; CurrentFieldNo: Integer; var IsHandled: Boolean) - begin - IsHandled := 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 c73541c4bbd..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 @@ -254,9 +254,6 @@ codeunit 8060 "Create Billing Documents" CustomerContractLine.Get(TempBillingLine."Subscription Contract No.", TempBillingLine."Subscription Contract Line No."); OnAfterCustomerContractLineGetInInsertSalesLineFromTempBillingLine(CustomerContractLine, SalesHeader, TempBillingLine); - // Skip the redundant unit price/cost engine while validating the line - the values are taken from - // the Billing Line and assigned explicitly below. All other validation side effects still run. - BindSubscription(BillingPriceCalcSkip); SalesLine.InitFromSalesHeader(SalesHeader); SubContractsItemManagement.SetAllowInsertOfInvoicingItem(true); if (ServiceCommitment."Invoicing Item No." <> '') and @@ -295,7 +292,6 @@ codeunit 8060 "Create Billing Documents" SalesLine."Description 2" := ''; SetInvoicePriceFromUsageDataBilling(SalesLine, TempBillingLine); - UnbindSubscription(BillingPriceCalcSkip); OnBeforeInsertSalesLineFromContractLine(SalesLine, TempBillingLine); SalesLine.Insert(false); @@ -384,9 +380,6 @@ codeunit 8060 "Create Billing Documents" ServiceObject.Get(TempBillingLine."Subscription Header No."); ServiceCommitment.Get(TempBillingLine."Subscription Line Entry No."); - // Skip the redundant unit price/cost engine while validating the line - the values are taken from - // the Billing Line and assigned explicitly below. All other validation side effects still run. - BindSubscription(BillingPriceCalcSkip); InitPurchaseLine(PurchaseLine); SubContractsItemManagement.SetAllowInsertOfInvoicingItem(true); if (ServiceCommitment."Invoicing Item No." <> '') and @@ -414,7 +407,6 @@ codeunit 8060 "Create Billing Documents" PurchaseLine.Description := ServiceCommitment.Description; PurchaseLine."Description 2" := CopyStr(ServiceObject.Description, 1, MaxStrLen(PurchaseLine."Description 2")); SetInvoicePriceFromUsageDataBilling(PurchaseLine, TempBillingLine); - UnbindSubscription(BillingPriceCalcSkip); OnBeforeInsertPurchaseLineFromContractLine(PurchaseLine, TempBillingLine); PurchaseLine.Insert(false); @@ -1492,7 +1484,6 @@ codeunit 8060 "Create Billing Documents" DocumentChangeManagement: Codeunit "Document Change Management"; Language: Codeunit Language; ProgressTracker: Codeunit "Progress Tracker"; - BillingPriceCalcSkip: Codeunit "Billing Price Calc. Skip"; BillingLineNoByTempEntryNo: Dictionary of [Integer, Integer]; DocumentDate: Date; PostingDate: Date; 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 b230966283d..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 @@ -638,16 +638,49 @@ page 8067 "Recurring Billing" local procedure RefreshGroups(GroupKeys: List of [Code[20]]) var CurrentEntryNo: Integer; + SavedGroupKey: Code[20]; + IsHeaderRow: Boolean; begin - CurrentEntryNo := Rec."Entry No."; + // 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;