From fc0689be30075c0f43bbc1251e5b68d2752664de Mon Sep 17 00:00:00 2001 From: Aleksandr Gladkov Date: Mon, 27 Apr 2026 12:06:59 +0200 Subject: [PATCH 01/12] GB + AU + tests --- src/Apps/W1/PaymentPractices/App/app.json | 11 +- .../Enums/PaymPracReportingScheme.Enum.al | 23 + .../PaymPracDisputeRetHdlr.Codeunit.al | 72 +++ .../PaymPracPeriodAggregator.Codeunit.al | 11 +- .../PaymPracSizeAggregator.Codeunit.al | 7 +- .../PaymPracSmallBusHandler.Codeunit.al | 27 + .../PaymPracStandardHandler.Codeunit.al | 30 ++ .../PaymentPracticeSchemeHandler.Interface.al | 36 ++ .../App/src/Core/PaymentPeriodMgt.Codeunit.al | 35 ++ .../Core/PaymentPracticeBuilders.Codeunit.al | 14 +- .../App/src/Core/PaymentPractices.Codeunit.al | 8 + .../PaymPracObjects.PermissionSet.al | 4 + .../Core/UpgradePaymentPractices.Codeunit.al | 32 ++ .../src/Pages/PaymPracVendLedgEntr.PageExt.al | 22 + .../App/src/Pages/PaymentPeriods.Page.al | 34 +- .../App/src/Pages/PaymentPracticeCard.Page.al | 26 +- .../src/Pages/PaymentPracticeDataList.Page.al | 16 +- .../src/Pages/PaymentPracticeLines.Page.al | 8 + .../Tables/PaymPracVendLedgEntry.TableExt.al | 20 + .../App/src/Tables/PaymentPeriod.Table.al | 11 - .../src/Tables/PaymentPracticeData.Table.al | 18 + .../src/Tables/PaymentPracticeHeader.Table.al | 38 ++ .../src/Tables/PaymentPracticeLine.Table.al | 14 +- .../Test Library/ExtensionLogo.png | Bin 0 -> 5446 bytes .../W1/PaymentPractices/Test Library/app.json | 46 ++ .../src/PaymentPracticesLibrary.Codeunit.al | 298 +++++++++++ src/Apps/W1/PaymentPractices/Test/app.json | 8 +- .../src/PaymentPracticesLibrary.Codeunit.al | 165 ------- .../Test/src/PaymentPracticesUT.Codeunit.al | 465 +++++++++++++----- 29 files changed, 1185 insertions(+), 314 deletions(-) create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/Enums/PaymPracReportingScheme.Enum.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracStandardHandler.Codeunit.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Tables/PaymPracVendLedgEntry.TableExt.al create mode 100644 src/Apps/W1/PaymentPractices/Test Library/ExtensionLogo.png create mode 100644 src/Apps/W1/PaymentPractices/Test Library/app.json create mode 100644 src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al delete mode 100644 src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesLibrary.Codeunit.al diff --git a/src/Apps/W1/PaymentPractices/App/app.json b/src/Apps/W1/PaymentPractices/App/app.json index 6f4428a507..72e77013eb 100644 --- a/src/Apps/W1/PaymentPractices/App/app.json +++ b/src/Apps/W1/PaymentPractices/App/app.json @@ -16,8 +16,8 @@ "platform": "29.0.0.0", "idRanges": [ { - "from": 685, - "to": 694 + "from": 680, + "to": 698 } ], "resourceExposurePolicy": { @@ -29,5 +29,12 @@ "target": "Cloud", "features": [ "TranslationFile" + ], + "internalsVisibleTo": [ + { + "id": "cc329ed7-8840-45f6-860b-3eb99c408998", + "name": "Payment Practices Test Library", + "publisher": "Microsoft" + } ] } diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Enums/PaymPracReportingScheme.Enum.al b/src/Apps/W1/PaymentPractices/App/src/Core/Enums/PaymPracReportingScheme.Enum.al new file mode 100644 index 0000000000..65d275c006 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Enums/PaymPracReportingScheme.Enum.al @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +enum 680 "Paym. Prac. Reporting Scheme" implements PaymentPracticeSchemeHandler +{ + Extensible = true; + + value(0; Standard) + { + Implementation = PaymentPracticeSchemeHandler = "Paym. Prac. Standard Handler"; + } + value(1; "Dispute & Retention") + { + Implementation = PaymentPracticeSchemeHandler = "Paym. Prac. Dispute Ret. Hdlr"; + } + value(2; "Small Business") + { + Implementation = PaymentPracticeSchemeHandler = "Paym. Prac. Small Bus. Handler"; + } +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al new file mode 100644 index 0000000000..0f964a0d73 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al @@ -0,0 +1,72 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +using Microsoft.Purchases.Payables; + +codeunit 681 "Paym. Prac. Dispute Ret. Hdlr" implements PaymentPracticeSchemeHandler +{ + Access = Internal; + + procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") + begin + // Dispute & Retention: no additional header type restrictions + end; + + procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean + var + VendorLedgerEntry: Record "Vendor Ledger Entry"; + begin + if PaymentPracticeData."Source Type" <> PaymentPracticeData."Source Type"::Vendor then + exit(true); + + VendorLedgerEntry.SetLoadFields("SCF Payment Date", "Dispute Status"); + if VendorLedgerEntry.Get(PaymentPracticeData."Invoice Entry No.") then begin + PaymentPracticeData."SCF Payment Date" := VendorLedgerEntry."SCF Payment Date"; + PaymentPracticeData."Dispute Status" := VendorLedgerEntry."Dispute Status"; + + if PaymentPracticeData."SCF Payment Date" <> 0D then + PaymentPracticeData."Actual Payment Days" := PaymentPracticeData."SCF Payment Date" - PaymentPracticeData."Invoice Received Date"; + end; + + exit(true); + end; + + procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + var + TotalPayments: Integer; + TotalAmount: Decimal; + TotalOverdueAmount: Decimal; + OverdueCount: Integer; + OverdueDueToDisputeCount: Integer; + begin + if PaymentPracticeData.FindSet() then + repeat + if not PaymentPracticeData."Invoice Is Open" then begin + TotalPayments += 1; + TotalAmount += PaymentPracticeData."Invoice Amount"; + if PaymentPracticeData."Actual Payment Days" > PaymentPracticeData."Agreed Payment Days" then begin + OverdueCount += 1; + TotalOverdueAmount += PaymentPracticeData."Invoice Amount"; + PaymentPracticeData."Overdue Due to Dispute" := PaymentPracticeData."Dispute Status" <> ''; + PaymentPracticeData.Modify(); + if PaymentPracticeData."Dispute Status" <> '' then + OverdueDueToDisputeCount += 1; + end; + end; + until PaymentPracticeData.Next() = 0; + + PaymentPracticeHeader."Total Number of Payments" := TotalPayments; + PaymentPracticeHeader."Total Amount of Payments" := TotalAmount; + PaymentPracticeHeader."Total Amt. of Overdue Payments" := TotalOverdueAmount; + if OverdueCount > 0 then + PaymentPracticeHeader."Pct Overdue Due to Dispute" := OverdueDueToDisputeCount / OverdueCount * 100; + end; + + procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") + begin + // Dispute & Retention: no additional line totals + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al index 9c8f0aa311..5f4fa8b16e 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al @@ -26,10 +26,12 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr var PaymentPracticeLine: Record "Payment Practice Line"; PaymentPeriod: Record "Payment Period"; + SchemeHandler: Interface PaymentPracticeSchemeHandler; SourceType: Integer; NextLineNo: Integer; begin NextLineNo := 1; + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; PaymentPeriod.SetCurrentKey("Days From"); PaymentPeriod.SetAscending("Days From", true); foreach SourceType in PaymentPracticeData."Source Type".Ordinals() do begin @@ -37,7 +39,10 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr if not PaymentPracticeData.IsEmpty() then if PaymentPeriod.FindSet() then repeat - InsertPeriodLine(PaymentPracticeLine, PaymentPracticeData, PaymentPeriod, PaymentPracticeHeader."No.", NextLineNo); + InsertPeriodLine(PaymentPracticeLine, PaymentPracticeData, PaymentPeriod, PaymentPracticeHeader."No.", NextLineNo, SourceType); + SchemeHandler.CalculateLineTotals(PaymentPracticeLine, PaymentPracticeData); + if (PaymentPracticeLine."Invoice Count" <> 0) or (PaymentPracticeLine."Invoice Value" <> 0) then + PaymentPracticeLine.Modify(); until PaymentPeriod.Next() = 0; end; end; @@ -47,7 +52,7 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr end; - local procedure InsertPeriodLine(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data"; PaymentPeriod: Record "Payment Period"; HeaderNo: Integer; var NextLineNo: Integer) + local procedure InsertPeriodLine(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data"; PaymentPeriod: Record "Payment Period"; HeaderNo: Integer; var NextLineNo: Integer; SourceType: Integer) begin PaymentPracticeLine.Init(); PaymentPracticeLine."Header No." := HeaderNo; @@ -57,7 +62,7 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr PaymentPracticeLine."Payment Period Code" := PaymentPeriod.Code; PaymentPracticeLine."Payment Period Description" := PaymentPeriod.Description; SetPercentPaidInPeriod(PaymentPracticeData, PaymentPeriod."Days From", PaymentPeriod."Days To", PaymentPracticeLine."Pct Paid in Period", PaymentPracticeLine."Pct Paid in Period (Amount)"); - PaymentPracticeLine."Source Type" := PaymentPracticeData."Source Type"; + PaymentPracticeLine."Source Type" := "Paym. Prac. Header Type".FromInteger(SourceType); PaymentPracticeLine.Insert(); end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al index 6b47f4f6a5..b2cae99983 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al @@ -28,9 +28,11 @@ codeunit 686 "Paym. Prac. Size Aggregator" implements PaymentPracticeLinesAggreg var PaymentPracticeLine: Record "Payment Practice Line"; CompanySize: Record "Company Size"; + SchemeHandler: Interface PaymentPracticeSchemeHandler; NextLineNo: Integer; begin NextLineNo := 1; + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; if CompanySize.FindSet() then repeat PaymentPracticeLine.Init(); @@ -45,9 +47,12 @@ codeunit 686 "Paym. Prac. Size Aggregator" implements PaymentPracticeLinesAggreg PaymentPracticeLine."Average Actual Payment Period" := PaymentPracticeMath.GetAverageActualPaymentTime(PaymentPracticeData); PaymentPracticeLine."Average Agreed Payment Period" := PaymentPracticeMath.GetAverageAgreedPaymentTime(PaymentPracticeData); PaymentPracticeLine."Pct Paid on Time" := PaymentPracticeMath.GetPercentOfOnTimePayments(PaymentPracticeData); - PaymentPracticeData.SetRange("Company Size Code"); PaymentPracticeLine.Insert(); + SchemeHandler.CalculateLineTotals(PaymentPracticeLine, PaymentPracticeData); + if (PaymentPracticeLine."Invoice Count" <> 0) or (PaymentPracticeLine."Invoice Value" <> 0) then + PaymentPracticeLine.Modify(); + PaymentPracticeData.SetRange("Company Size Code"); until CompanySize.Next() = 0; end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al new file mode 100644 index 0000000000..d3e1c84df5 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHandler +{ + Access = Internal; + + procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") + begin + end; + + procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean + begin + exit(true); + end; + + procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + begin + end; + + procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") + begin + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracStandardHandler.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracStandardHandler.Codeunit.al new file mode 100644 index 0000000000..67e0b376c8 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracStandardHandler.Codeunit.al @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +codeunit 680 "Paym. Prac. Standard Handler" implements PaymentPracticeSchemeHandler +{ + Access = Internal; + + procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") + begin + // Standard scheme: no additional validation + end; + + procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean + begin + exit(true); + end; + + procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + begin + // Standard scheme: no additional header totals + end; + + procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") + begin + // Standard scheme: no additional line totals + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al b/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al new file mode 100644 index 0000000000..ca2d0b15f3 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +interface PaymentPracticeSchemeHandler +{ + /// + /// Validates the Payment Practice Header before data generation. + /// + /// The header to validate. + procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") + + /// + /// Enriches or filters a Payment Practice Data row before insertion. + /// Returns true to include the row, false to skip it. + /// + /// The data row to enrich/filter. + /// True to include the row, false to skip. + procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean + + /// + /// Calculates scheme-specific header totals after standard totals are generated. + /// + /// The header to update with totals. + /// The data to aggregate from. + procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + + /// + /// Calculates scheme-specific line totals for each generated line. + /// + /// The line to update with totals. + /// The data to aggregate from. + procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al new file mode 100644 index 0000000000..9807f45823 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +using System.Environment; + +codeunit 695 "Payment Period Mgt." +{ + Access = Internal; + + procedure DetectReportingScheme(): Enum "Paym. Prac. Reporting Scheme" + var + EnvironmentInformation: Codeunit "Environment Information"; + ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; + IsHandled: Boolean; + begin + OnBeforeDetectReportingScheme(ReportingScheme, IsHandled); + if IsHandled then + exit(ReportingScheme); + + case EnvironmentInformation.GetApplicationFamily() of + 'GB': + exit("Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + else + exit("Paym. Prac. Reporting Scheme"::Standard); + end; + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeDetectReportingScheme(var ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; var IsHandled: Boolean) + begin + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al index fc3d2ab1bf..8f56133732 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al @@ -17,9 +17,13 @@ codeunit 688 "Payment Practice Builders" var Vendor: Record Vendor; VendorLedgerEntry: Record "Vendor Ledger Entry"; + SchemeHandler: Interface PaymentPracticeSchemeHandler; LastVendNo: Code[20]; begin + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; LastVendNo := ''; + Vendor.SetLoadFields("No.", "Exclude from Pmt. Practices", "Company Size Code"); + VendorLedgerEntry.SetLoadFields("Entry No.", "Vendor No.", "External Document No.", "Document No.", "Posting Date", "Invoice Received Date", "Document Date", "Due Date", Open, "Closed at Date", "Closed by Entry No.", "SCF Payment Date"); VendorLedgerEntry.SetCurrentKey("Vendor No."); VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); VendorLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); @@ -39,7 +43,8 @@ codeunit 688 "Payment Practice Builders" PaymentPracticeData."Header No." := PaymentPracticeHeader."No."; PaymentPracticeData.CopyFromInvoiceVendLedgEntry(VendorLedgerEntry); PaymentPracticeData."Company Size Code" := Vendor."Company Size Code"; - PaymentPracticeData.Insert(); + if SchemeHandler.UpdatePaymentPracData(PaymentPracticeData) then + PaymentPracticeData.Insert(); end; until VendorLedgerEntry.Next() = 0; end; @@ -48,9 +53,13 @@ codeunit 688 "Payment Practice Builders" var Customer: Record Customer; CustLedgerEntry: Record "Cust. Ledger Entry"; + SchemeHandler: Interface PaymentPracticeSchemeHandler; LastCustNo: Code[20]; begin + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; LastCustNo := ''; + Customer.SetLoadFields("No.", "Exclude from Pmt. Practices"); + CustLedgerEntry.SetLoadFields("Entry No.", "Customer No.", "External Document No.", "Document No.", "Posting Date", "Document Date", "Due Date", Open, "Closed at Date", "Closed by Entry No."); CustLedgerEntry.SetCurrentKey("Customer No."); CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); CustLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); @@ -69,7 +78,8 @@ codeunit 688 "Payment Practice Builders" PaymentPracticeData.Init(); PaymentPracticeData."Header No." := PaymentPracticeHeader."No."; PaymentPracticeData.CopyFromInvoiceCustLedgEntry(CustLedgerEntry); - PaymentPracticeData.Insert(); + if SchemeHandler.UpdatePaymentPracData(PaymentPracticeData) then + PaymentPracticeData.Insert(); end; until CustLedgerEntry.Next() = 0; end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al index 1298d87e81..d8539907db 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al @@ -12,9 +12,14 @@ codeunit 689 "Payment Practices" procedure Generate(var PaymentPracticeHeader: Record "Payment Practice Header") DataIsNotEmpty: Boolean var PaymentPracticeData: Record "Payment Practice Data"; + SchemeHandler: Interface PaymentPracticeSchemeHandler; begin PaymentPracticeHeader.TestField("Starting Date"); PaymentPracticeHeader.TestField("Ending Date"); + + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; + SchemeHandler.ValidateHeader(PaymentPracticeHeader); + PaymentPracticeData.Reset(); PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); PaymentPracticeData.DeleteAll(); @@ -23,6 +28,9 @@ codeunit 689 "Payment Practices" PaymentPracticeHeader."Generated On" := CurrentDateTime(); PaymentPracticeHeader."Generated By" := CopyStr(UserId(), 1, MaxStrLen(PaymentPracticeHeader."Generated By")); GenerateTotals(PaymentPracticeData, PaymentPracticeHeader); + PaymentPracticeData.Reset(); + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + SchemeHandler.CalculateHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); GenerateLines(PaymentPracticeHeader."Aggregation Type", PaymentPracticeData, PaymentPracticeHeader); PaymentPracticeHeader."Modified Manually" := false; PaymentPracticeHeader.Modify(false); diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al b/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al index 1971adb6ff..20fcdac349 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al @@ -25,6 +25,10 @@ permissionset 685 "Paym. Prac. Objects" codeunit "Paym. Prac. Period Aggregator" = X, codeunit "Paym. Prac. Size Aggregator" = X, codeunit "Paym. Prac. Vendor Generator" = X, + codeunit "Paym. Prac. Standard Handler" = X, + codeunit "Paym. Prac. Dispute Ret. Hdlr" = X, + codeunit "Paym. Prac. Small Bus. Handler" = X, + codeunit "Upgrade Payment Practices" = X, codeunit "Install Payment Practices" = X, codeunit "Payment Practice Builders" = X, codeunit "Payment Practice Math" = X, diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al new file mode 100644 index 0000000000..6d8849d42f --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +codeunit 683 "Upgrade Payment Practices" +{ + Access = Internal; + Subtype = Upgrade; + + trigger OnUpgradePerCompany() + begin + BackfillReportingScheme(); + end; + + local procedure BackfillReportingScheme() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPeriodMgt: Codeunit "Payment Period Mgt."; + ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; + begin + ReportingScheme := PaymentPeriodMgt.DetectReportingScheme(); + + PaymentPracticeHeader.SetRange("Reporting Scheme", 0); + if PaymentPracticeHeader.FindSet() then + repeat + PaymentPracticeHeader."Reporting Scheme" := ReportingScheme; + PaymentPracticeHeader.Modify(); + until PaymentPracticeHeader.Next() = 0; + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al new file mode 100644 index 0000000000..a9f37a91a6 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +using Microsoft.Purchases.Payables; + +pageextension 681 "Paym. Prac. Vend. Ledg. Entr." extends "Vendor Ledger Entries" +{ + layout + { + addafter("Invoice Received Date") + { + field("SCF Payment Date"; Rec."SCF Payment Date") + { + ApplicationArea = Basic, Suite; + Visible = false; + } + } + } +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPeriods.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPeriods.Page.al index 1fdd261dcb..9c7826b096 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPeriods.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPeriods.Page.al @@ -4,6 +4,8 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; +using System.Utilities; + page 685 "Payment Periods" { ApplicationArea = Basic, Suite; @@ -42,6 +44,36 @@ page 685 "Payment Periods" actions { + area(Processing) + { + action(RestoreDefaults) + { + Caption = 'Restore Default Periods'; + ToolTip = 'Deletes all payment periods and restores the default periods for the current environment.'; + Image = Restore; + + trigger OnAction() + var + PaymentPeriod: Record "Payment Period"; + ConfirmManagement: Codeunit "Confirm Management"; + begin + if not ConfirmManagement.GetResponseOrDefault(RestoreDefaultsQst, false) then + exit; + + PaymentPeriod.DeleteAll(); + PaymentPeriod.SetupDefaults(); + CurrPage.Update(false); + end; + } + } + area(Promoted) + { + actionref(RestoreDefaults_Promoted; RestoreDefaults) + { + } + } } -} + var + RestoreDefaultsQst: Label 'This will replace all payment periods with the default periods for your environment. Do you want to continue?'; +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al index c899463801..d9b7970a9b 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al @@ -25,6 +25,11 @@ page 687 "Payment Practice Card" { ToolTip = 'Specifies the number of the payment practice header.'; } + field("Reporting Scheme"; Rec."Reporting Scheme") + { + Visible = false; + Editable = false; + } field("Aggregation Type"; Rec."Aggregation Type") { ToolTip = 'Specifies the aggregation type of the payment practice.'; @@ -35,6 +40,7 @@ page 687 "Payment Practice Card" } field("Startind Date"; Rec."Starting Date") { + Caption = 'Starting Date'; ToolTip = 'Specifies the starting date of the payment practice.'; } field("Ending Date"; Rec."Ending Date") @@ -65,7 +71,7 @@ page 687 "Payment Practice Card" } group("Statistics") { - Caption = 'Statistics'; + Caption = 'Payment Statistics'; field("Average Agreed Payment Period"; Rec."Average Agreed Payment Period") { ToolTip = 'Specifies the average agreed payment period.'; @@ -93,6 +99,18 @@ page 687 "Payment Practice Card" ShowHeaderDataLines(); end; } + field("Total Number of Payments"; Rec."Total Number of Payments") + { + } + field("Total Amount of Payments"; Rec."Total Amount of Payments") + { + } + field("Total Amt. of Overdue Payments"; Rec."Total Amt. of Overdue Payments") + { + } + field("Pct Overdue Due to Dispute"; Rec."Pct Overdue Due to Dispute") + { + } } part(Lines; "Payment Practice Lines") { @@ -158,7 +176,12 @@ page 687 "Payment Practice Card" trigger OnOpenPage() begin + UpdateVisibility(); CurrPage.Update(); + end; + + trigger OnAfterGetCurrRecord() + begin UpdateVisibility(); end; @@ -183,6 +206,5 @@ page 687 "Payment Practice Card" local procedure UpdateVisibility() begin CurrPage.Lines.Page.UpdateVisibility(Rec."Aggregation Type", Rec."Header Type"); - CurrPage.Update(); end; } diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al index b3c4ef342b..3beb3021d4 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al @@ -25,7 +25,7 @@ page 686 "Payment Practice Data List" } field("Payment Entry No."; Rec."Pmt. Entry No.") { - ToolTip = 'Specifies the closing payment entry number that is associated with the source invoicy entry, if any was applied.'; + ToolTip = 'Specifies the closing payment entry number that is associated with the source invoice entry, if any was applied.'; } field("Invoice Posting Date"; Rec."Invoice Posting Date") { @@ -41,7 +41,7 @@ page 686 "Payment Practice Data List" } field("Pmt. Posting Date"; Rec."Pmt. Posting Date") { - ToolTip = 'Specifies the posting date of the payment entry that is associated with the source invoicy entry, if any was applied.'; + ToolTip = 'Specifies the posting date of the payment entry that is associated with the source invoice entry, if any was applied.'; } field("Invoice Is Open"; Rec."Invoice Is Open") { @@ -65,6 +65,18 @@ page 686 "Payment Practice Data List" Style = Unfavorable; StyleExpr = Rec."Actual Payment Days" > Rec."Agreed Payment Days"; } + field("Dispute Status"; Rec."Dispute Status") + { + Visible = false; + } + field("Overdue Due to Dispute"; Rec."Overdue Due to Dispute") + { + Visible = false; + } + field("SCF Payment Date"; Rec."SCF Payment Date") + { + Visible = false; + } } } } diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeLines.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeLines.Page.al index 90f6b89a53..59de6bb343 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeLines.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeLines.Page.al @@ -93,6 +93,14 @@ page 688 "Payment Practice Lines" Editable = false; ToolTip = 'Specifies whether the line has been modified manually.'; } + field("Invoice Count"; Rec."Invoice Count") + { + Editable = false; + } + field("Invoice Value"; Rec."Invoice Value") + { + Editable = false; + } } } } diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymPracVendLedgEntry.TableExt.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymPracVendLedgEntry.TableExt.al new file mode 100644 index 0000000000..fcba3ab6cd --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymPracVendLedgEntry.TableExt.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +using Microsoft.Purchases.Payables; + +tableextension 681 "Paym. Prac. Vend. Ledg. Entry" extends "Vendor Ledger Entry" +{ + fields + { + field(680; "SCF Payment Date"; Date) + { + Caption = 'SCF Payment Date'; + ToolTip = 'Specifies when the supplier received payment from a finance provider under a supply chain finance arrangement. When filled in, replaces the payment posting date for calculating actual payment days.'; + DataClassification = CustomerContent; + } + } +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al index 8dd0d1f413..1db7f8166f 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al @@ -87,8 +87,6 @@ table 685 "Payment Period" InsertDefaultPeriods_GB(); 'FR': InsertDefaultPeriods_FR(); - 'AU', 'NZ': - InsertDefaultPeriods_AUNZ(); else InsertDefaultPeriods(); end; @@ -142,15 +140,6 @@ table 685 "Payment Period" InsertPeriod('P121+', 121, 0); end; - local procedure InsertDefaultPeriods_AUNZ() - begin - InsertPeriod('P0_21', 0, 21); - InsertPeriod('P22_30', 22, 30); - InsertPeriod('P31_60', 31, 60); - InsertPeriod('P61_90', 61, 120); - InsertPeriod('P121+', 121, 0); - end; - [IntegrationEvent(false, false)] local procedure OnBeforeSetupDefaults(var IsHandled: Boolean) begin diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeData.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeData.Table.al index 6e5640a5d8..0adcdd0dde 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeData.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeData.Table.al @@ -5,6 +5,7 @@ namespace Microsoft.Finance.Analysis; using Microsoft.Purchases.Payables; +using Microsoft.Sales.Customer; using Microsoft.Sales.Receivables; table 686 "Payment Practice Data" @@ -78,6 +79,20 @@ table 686 "Payment Practice Data" AutoFormatType = 1; AutoFormatExpression = ''; } + field(20; "Dispute Status"; Code[10]) + { + TableRelation = "Dispute Status"; + ToolTip = 'Specifies whether the invoice is flagged as disputed. Copied from the vendor ledger entry during data generation.'; + } + field(21; "Overdue Due to Dispute"; Boolean) + { + Editable = false; + ToolTip = 'Specifies whether the invoice is overdue due to a dispute. This field is automatically calculated based on whether the invoice is overdue and has a dispute status.'; + } + field(22; "SCF Payment Date"; Date) + { + ToolTip = 'Specifies when the supplier received payment under a supply chain finance arrangement. When filled in, replaces the payment posting date for calculating actual payment days.'; + } } @@ -112,6 +127,8 @@ table 686 "Payment Practice Data" "Invoice Amount" := -VendorLedgerEntry.Amount; "Pmt. Posting Date" := VendorLedgerEntry."Closed at Date"; "Pmt. Entry No." := VendorLedgerEntry."Closed by Entry No."; + "SCF Payment Date" := VendorLedgerEntry."SCF Payment Date"; + "Dispute Status" := VendorLedgerEntry."Dispute Status"; if "Invoice Posting Date" <> 0D then "Agreed Payment Days" := "Due Date" - "Invoice Received Date"; if "Pmt. Posting Date" <> 0D then @@ -133,6 +150,7 @@ table 686 "Payment Practice Data" "Invoice Amount" := -CustLedgerEntry.Amount; "Pmt. Posting Date" := CustLedgerEntry."Closed at Date"; "Pmt. Entry No." := CustLedgerEntry."Closed by Entry No."; + "Dispute Status" := CustLedgerEntry."Dispute Status"; if "Invoice Posting Date" <> 0D then "Agreed Payment Days" := "Due Date" - "Invoice Received Date"; if "Pmt. Posting Date" <> 0D then diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al index b97262898e..18177c5dd7 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al @@ -105,6 +105,35 @@ table 687 "Payment Practice Header" { } + field(15; "Reporting Scheme"; Enum "Paym. Prac. Reporting Scheme") + { + ToolTip = 'Specifies which reporting scheme is used, such as Standard, Dispute & Retention, or Small Business. Controls which fields and calculations apply.'; + } + field(20; "Total Number of Payments"; Integer) + { + Editable = false; + ToolTip = 'Specifies the total number of payments made during the reporting period.'; + } + field(21; "Total Amount of Payments"; Decimal) + { + Editable = false; + AutoFormatType = 1; + AutoFormatExpression = ''; + ToolTip = 'Specifies the total value of payments made during the reporting period.'; + } + field(22; "Total Amt. of Overdue Payments"; Decimal) + { + Editable = false; + AutoFormatType = 1; + AutoFormatExpression = ''; + ToolTip = 'Specifies the total value of payments not made within the agreed payment terms.'; + } + field(23; "Pct Overdue Due to Dispute"; Decimal) + { + Editable = false; + AutoFormatType = 0; + ToolTip = 'Specifies the percentage of payments not made within agreed terms that are due to disputes.'; + } } keys @@ -118,6 +147,7 @@ table 687 "Payment Practice Header" trigger OnInsert() begin UpdateNo(); + DetectReportingScheme(); end; trigger OnDelete() @@ -168,4 +198,12 @@ table 687 "Payment Practice Header" if Rec."Starting Date" > Rec."Ending Date" then Error(DateValidationErr); end; + + local procedure DetectReportingScheme() + var + PaymentPeriodMgt: Codeunit "Payment Period Mgt."; + begin + "Reporting Scheme" := PaymentPeriodMgt.DetectReportingScheme(); + end; + } diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al index c7fbf37242..9fbff11903 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al @@ -84,7 +84,19 @@ table 688 "Payment Practice Line" } field(13; "Modified Manually"; Boolean) { - + } + field(15; "Invoice Count"; Integer) + { + ToolTip = 'Specifies the number of invoices in this period.'; + } + field(16; "Invoice Value"; Decimal) + { + AutoFormatType = 1; + AutoFormatExpression = ''; + ToolTip = 'Specifies the total value of invoices in this period.'; + } + field(20; "Payment Period Line No."; Integer) + { } } diff --git a/src/Apps/W1/PaymentPractices/Test Library/ExtensionLogo.png b/src/Apps/W1/PaymentPractices/Test Library/ExtensionLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..4d2c9a626cb9617350617c40cd73904129d4c108 GIT binary patch literal 5446 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 literal 0 HcmV?d00001 diff --git a/src/Apps/W1/PaymentPractices/Test Library/app.json b/src/Apps/W1/PaymentPractices/Test Library/app.json new file mode 100644 index 0000000000..3302636634 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/Test Library/app.json @@ -0,0 +1,46 @@ +{ + "id": "cc329ed7-8840-45f6-860b-3eb99c408998", + "name": "Payment Practices Test Library", + "publisher": "Microsoft", + "brief": "Test library for Payment Practices app.", + "description": "Test library for Payment Practices app.", + "version": "29.0.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?LinkId=724011", + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2204541", + "url": "https://go.microsoft.com/fwlink/?LinkId=724011", + "logo": "ExtensionLogo.png", + "dependencies": [ + { + "id": "64977288-facd-4b48-aaaa-bb0e288edfb3", + "name": "Payment Practices", + "publisher": "Microsoft", + "version": "29.0.0.0" + }, + { + "id": "5d86850b-0d76-4eca-bd7b-951ad998e997", + "name": "Tests-TestLibraries", + "publisher": "Microsoft", + "version": "29.0.0.0" + } + ], + "screenshots": [], + "platform": "29.0.0.0", + "idRanges": [ + { + "from": 134196, + "to": 134196 + } + ], + "resourceExposurePolicy": { + "allowDebugging": false, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "application": "29.0.0.0", + "target": "Cloud", + "features": [ + "TranslationFile" + ] +} diff --git a/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al b/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al new file mode 100644 index 0000000000..3276733227 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al @@ -0,0 +1,298 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Test.Finance.Analysis; + +using Microsoft.Finance.Analysis; +using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Purchases.Payables; +using Microsoft.Purchases.Vendor; +using Microsoft.Sales.Customer; +using Microsoft.Sales.Receivables; + +codeunit 134196 "Payment Practices Library" +{ + Subtype = Test; + + var + Assert: Codeunit Assert; + LibraryUtility: Codeunit "Library - Utility"; + LibraryPurchase: Codeunit "Library - Purchase"; + LibraryRandom: Codeunit "Library - Random"; + + procedure CreatePaymentPracticeHeader(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type"; StartingDate: Date; EndingDate: Date) + begin + PaymentPracticeHeader.Init(); + PaymentPracticeHeader."Header Type" := HeaderType; + PaymentPracticeHeader."Aggregation Type" := AggregationType; + PaymentPracticeHeader."Starting Date" := StartingDate; + PaymentPracticeHeader."Ending Date" := EndingDate; + PaymentPracticeHeader.Insert(); + end; + + procedure CreatePaymentPracticeHeader(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type"; ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; StartingDate: Date; EndingDate: Date) + begin + PaymentPracticeHeader.Init(); + PaymentPracticeHeader."Header Type" := HeaderType; + PaymentPracticeHeader."Aggregation Type" := AggregationType; + PaymentPracticeHeader."Reporting Scheme" := ReportingScheme; + PaymentPracticeHeader."Starting Date" := StartingDate; + PaymentPracticeHeader."Ending Date" := EndingDate; + PaymentPracticeHeader.Insert(); + end; + + procedure CreatePaymentPracticeHeaderSimple(var PaymentPracticeHeader: Record "Payment Practice Header") + begin + CreatePaymentPracticeHeader(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::"Company Size", WorkDate() - 180, WorkDate() + 180); + end; + + procedure CreatePaymentPracticeHeaderSimple(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type") + begin + CreatePaymentPracticeHeader(PaymentPracticeHeader, HeaderType, AggregationType, WorkDate() - 180, WorkDate() + 180); + end; + + procedure CreateCompanySizeCode(): Code[20] + var + CompanySize: Record "Company Size"; + begin + CompanySize.Init(); + CompanySize.Code := LibraryUtility.GenerateGUID(); + CompanySize.Description := CompanySize.Code; + CompanySize.Insert(); + exit(CompanySize.Code); + end; + + procedure CreateVendorNoWithSizeAndExcl(CompanySizeCode: Code[20]; ExclFromPaymentPractice: Boolean) VendorNo: Code[20] + var + Vendor: Record Vendor; + begin + LibraryPurchase.CreateVendor(Vendor); + SetCompanySize(Vendor, CompanySizeCode); + SetExcludeFromPaymentPractices(Vendor, ExclFromPaymentPractice); + exit(Vendor."No."); + end; + + procedure InitializeCompanySizes(var CompanySizeCodes: array[3] of Code[20]) + var + i: Integer; + begin + for i := 1 to ArrayLen(CompanySizeCodes) do + CompanySizeCodes[i] := CreateCompanySizeCode(); + end; + + procedure InitializePaymentPeriods(var PaymentPeriods: array[3] of Record "Payment Period") + var + PaymentPeriod: Record "Payment Period"; + i: Integer; + begin + PaymentPeriod.SetCurrentKey("Days From"); + PaymentPeriod.FindSet(); + for i := 1 to ArrayLen(PaymentPeriods) do begin + PaymentPeriods[i] := PaymentPeriod; + PaymentPeriod.Next(); + end; + end; + + procedure InitAndGetLastPaymentPeriod(var PaymentPeriod: Record "Payment Period") + begin + PaymentPeriod.SetRange("Days To", 0); + PaymentPeriod.FindLast(); + end; + + procedure SetCompanySize(var Vendor: Record Vendor; CompanySizeCode: Code[20]) + begin + Vendor."Company Size Code" := CompanySizeCode; + Vendor.Modify(); + end; + + procedure SetExcludeFromPaymentPractices(var Vendor: Record Vendor; NewExcludeFromPaymentPractice: Boolean) + begin + Vendor."Exclude from Pmt. Practices" := NewExcludeFromPaymentPractice; + Vendor.Modify(); + end; + + procedure SetExcludeFromPaymentPractices(var Customer: Record Customer; NewExcludeFromPaymentPractice: Boolean) + begin + Customer."Exclude from Pmt. Practices" := NewExcludeFromPaymentPractice; + Customer.Modify(); + end; + + procedure SetExcludeFromPaymentPracticesOnAllVendorsAndCustomers() + var + Vendor: Record Vendor; + Customer: Record Customer; + begin + Vendor.ModifyAll("Exclude from Pmt. Practices", true); + Customer.ModifyAll("Exclude from Pmt. Practices", true); + end; + + procedure VerifyLinesCount(PaymentPracticeHeader: Record "Payment Practice Header"; NumberOfLines: Integer) + var + PaymentPracticeLine: Record "Payment Practice Line"; + begin + PaymentPracticeLine.SetRange("Header No.", PaymentPracticeHeader."No."); + Assert.RecordCount(PaymentPracticeLine, NumberOfLines); + end; + + procedure VerifyPeriodLine(PaymentPracticeHeaderNo: Integer; SourceType: Enum "Paym. Prac. Header Type"; PaymentPeriodDescription: Text[250]; PctInPeriodExpected: Decimal; PctInPeriodAmountExpected: Decimal) + var + PaymentPracticeLine: Record "Payment Practice Line"; + begin + PaymentPracticeLine.SetRange("Header No.", PaymentPracticeHeaderNo); +#pragma warning disable AA0210 + PaymentPracticeLine.SetRange("Payment Period Description", PaymentPeriodDescription); + PaymentPracticeLine.SetRange("Source Type", SourceType); +#pragma warning restore AA0210 + PaymentPracticeLine.FindFirst(); + Assert.AreNearlyEqual(PctInPeriodExpected, PaymentPracticeLine."Pct Paid in Period", 0.1, '"Pct Paid in Period" is not as expected'); + Assert.AreNearlyEqual(PctInPeriodAmountExpected, PaymentPracticeLine."Pct Paid in Period (Amount)", 0.1, '"Pct Paid in Period (Amount)" is not as expected'); + end; + + + procedure VerifyBufferCount(PaymentPracticeHeader: Record "Payment Practice Header"; NumberOfLines: Integer; SourceType: Enum "Paym. Prac. Header Type") + var + PaymentPracticeData: Record "Payment Practice Data"; + begin + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + PaymentPracticeData.SetRange("Source Type", SourceType); + Assert.RecordCount(PaymentPracticeData, NumberOfLines); + end; + + procedure CreateDefaultPaymentPeriodTemplates() + var + PaymentPeriod: Record "Payment Period"; + begin + PaymentPeriod.DeleteAll(); + PaymentPeriod.SetupDefaults(); + end; + + + + + procedure CleanupPaymentPracticeHeaders() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeData: Record "Payment Practice Data"; + begin + PaymentPracticeData.DeleteAll(); + PaymentPracticeHeader.DeleteAll(); + end; + + + procedure CreatePaymentPracticeHeaderWithScheme(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type"; ReportingScheme: Enum "Paym. Prac. Reporting Scheme") + begin + PaymentPracticeHeader.Init(); + PaymentPracticeHeader."Header Type" := HeaderType; + PaymentPracticeHeader."Aggregation Type" := AggregationType; + PaymentPracticeHeader."Reporting Scheme" := ReportingScheme; + PaymentPracticeHeader."Starting Date" := WorkDate() - 180; + PaymentPracticeHeader."Ending Date" := WorkDate() + 180; + PaymentPracticeHeader.Insert(); + end; + + + procedure DisputeRetCalcHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + var + SchemeHandler: Interface PaymentPracticeSchemeHandler; + begin + SchemeHandler := "Paym. Prac. Reporting Scheme"::"Dispute & Retention"; + SchemeHandler.CalculateHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); + end; + + + procedure MockVendLedgerEntry(VendorNo: Code[20]; var VendorLedgerEntry: Record "Vendor Ledger Entry"; DocType: Enum "Gen. Journal Document Type"; PostingDate: Date; DueDate: Date; PmtPostingDate: Date; IsOpen: Boolean) + begin + VendorLedgerEntry.Init(); + VendorLedgerEntry."Entry No." := LibraryUtility.GetNewRecNo(VendorLedgerEntry, VendorLedgerEntry.FieldNo("Entry No.")); + VendorLedgerEntry."Document Type" := DocType; + VendorLedgerEntry."Posting Date" := PostingDate; + VendorLedgerEntry."Document Date" := PostingDate; + VendorLedgerEntry."Vendor No." := VendorNo; + VendorLedgerEntry."Due Date" := DueDate; + VendorLedgerEntry.Open := IsOpen; + VendorLedgerEntry."Closed at Date" := PmtPostingDate; + VendorLedgerEntry.Amount := LibraryRandom.RandDec(1000, 2); + VendorLedgerEntry.Insert(); + end; + + procedure MockVendorInvoice(VendorNo: Code[20]; PostingDate: Date; DueDate: Date) InvoiceAmount: Decimal; + var + VendorLedgerEntry: Record "Vendor Ledger Entry"; + begin + MockVendLedgerEntry(VendorNo, VendorLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, 0D, true); + VendorLedgerEntry.CalcFields("Amount (LCY)"); + InvoiceAmount := VendorLedgerEntry."Amount (LCY)"; + end; + + procedure MockVendorInvoiceAndPayment(VendorNo: Code[20]; PostingDate: Date; DueDate: Date; PaymentPostingDate: Date) InvoiceAmount: Decimal; + var + VendorLedgerEntry: Record "Vendor Ledger Entry"; + begin + MockVendLedgerEntry(VendorNo, VendorLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, PaymentPostingDate, false); + VendorLedgerEntry.CalcFields("Amount (LCY)"); + InvoiceAmount := VendorLedgerEntry."Amount (LCY)"; + end; + + procedure MockVendorInvoiceAndPaymentInPeriod(VendorNo: Code[20]; StartingDate: Date; PaidInDays_min: Integer; PaidInDays_max: Integer) InvoiceAmount: Decimal; + var + PostingDate: Date; + DueDate: Date; + PaymentPostingDate: Date; + begin + PostingDate := StartingDate; + DueDate := StartingDate; + if PaidInDays_max <> 0 then + PaymentPostingDate := PostingDate + LibraryRandom.RandIntInRange(PaidInDays_min, PaidInDays_max) + else + PaymentPostingDate := PostingDate + PaidInDays_min + LibraryRandom.RandInt(10); + InvoiceAmount := MockVendorInvoiceAndPayment(VendorNo, PostingDate, DueDate, PaymentPostingDate); + end; + + procedure MockCustLedgerEntry(CustomerNo: Code[20]; var CustLedgerEntry: Record "Cust. Ledger Entry"; DocType: Enum "Gen. Journal Document Type"; PostingDate: Date; DueDate: Date; PmtPostingDate: Date; IsOpen: Boolean) + begin + CustLedgerEntry.Init(); + CustLedgerEntry."Entry No." := LibraryUtility.GetNewRecNo(CustLedgerEntry, CustLedgerEntry.FieldNo("Entry No.")); + CustLedgerEntry."Document Type" := DocType; + CustLedgerEntry."Posting Date" := PostingDate; + CustLedgerEntry."Document Date" := PostingDate; + CustLedgerEntry."Customer No." := CustomerNo; + CustLedgerEntry."Due Date" := DueDate; + CustLedgerEntry.Open := IsOpen; + CustLedgerEntry."Closed at Date" := PmtPostingDate; + CustLedgerEntry.Amount := LibraryRandom.RandDec(1000, 2); + CustLedgerEntry.Insert(); + end; + + procedure MockCustomerInvoice(CustomerNo: Code[20]; PostingDate: Date; DueDate: Date) InvoiceAmount: Decimal; + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + MockCustLedgerEntry(CustomerNo, CustLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, 0D, true); + CustLedgerEntry.CalcFields("Amount (LCY)"); + InvoiceAmount := CustLedgerEntry."Amount (LCY)"; + end; + + procedure MockCustomerInvoiceAndPayment(CustomerNo: Code[20]; PostingDate: Date; DueDate: Date; PaymentPostingDate: Date) InvoiceAmount: Decimal; + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + MockCustLedgerEntry(CustomerNo, CustLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, PaymentPostingDate, false); + CustLedgerEntry.CalcFields("Amount (LCY)"); + InvoiceAmount := CustLedgerEntry."Amount (LCY)"; + end; + + procedure MockCustomerInvoiceAndPaymentInPeriod(CustomerNo: Code[20]; StartingDate: Date; PaidInDays_min: Integer; PaidInDays_max: Integer) InvoiceAmount: Decimal; + var + PostingDate: Date; + DueDate: Date; + PaymentPostingDate: Date; + begin + PostingDate := StartingDate; + DueDate := StartingDate + LibraryRandom.RandIntInRange(1, 5); + PaymentPostingDate := PostingDate + LibraryRandom.RandIntInRange(PaidInDays_min, PaidInDays_max); + InvoiceAmount := MockCustomerInvoiceAndPayment(CustomerNo, PostingDate, DueDate, PaymentPostingDate); + end; + + +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/Test/app.json b/src/Apps/W1/PaymentPractices/Test/app.json index 0badc2cc17..2267bedd31 100644 --- a/src/Apps/W1/PaymentPractices/Test/app.json +++ b/src/Apps/W1/PaymentPractices/Test/app.json @@ -18,6 +18,12 @@ "publisher": "Microsoft", "version": "29.0.0.0" }, + { + "id": "cc329ed7-8840-45f6-860b-3eb99c408998", + "name": "Payment Practices Test Library", + "publisher": "Microsoft", + "version": "29.0.0.0" + }, { "id": "5d86850b-0d76-4eca-bd7b-951ad998e997", "name": "Tests-TestLibraries", @@ -41,7 +47,7 @@ "platform": "29.0.0.0", "idRanges": [ { - "from": 134196, + "from": 134197, "to": 134197 } ], diff --git a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesLibrary.Codeunit.al b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesLibrary.Codeunit.al deleted file mode 100644 index 384fef369a..0000000000 --- a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesLibrary.Codeunit.al +++ /dev/null @@ -1,165 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.Test.Finance.Analysis; - -using Microsoft.Finance.Analysis; -using Microsoft.Purchases.Vendor; -using Microsoft.Sales.Customer; - -codeunit 134196 "Payment Practices Library" -{ - Subtype = Test; - - var - Assert: Codeunit Assert; - LibraryUtility: Codeunit "Library - Utility"; - LibraryPurchase: Codeunit "Library - Purchase"; - - procedure CreatePaymentPracticeHeader(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type"; StartingDate: Date; EndingDate: Date) - begin - PaymentPracticeHeader.Init(); - PaymentPracticeHeader."Header Type" := HeaderType; - PaymentPracticeHeader."Aggregation Type" := AggregationType; - PaymentPracticeHeader."Starting Date" := StartingDate; - PaymentPracticeHeader."Ending Date" := EndingDate; - PaymentPracticeHeader.Insert(); - end; - - procedure CreatePaymentPracticeHeaderSimple(var PaymentPracticeHeader: Record "Payment Practice Header") - begin - CreatePaymentPracticeHeader(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::"Company Size", WorkDate() - 180, WorkDate() + 180); - end; - - procedure CreatePaymentPracticeHeaderSimple(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type") - begin - CreatePaymentPracticeHeader(PaymentPracticeHeader, HeaderType, AggregationType, WorkDate() - 180, WorkDate() + 180); - end; - - procedure CreateCompanySizeCode(): Code[20] - var - CompanySize: Record "Company Size"; - begin - CompanySize.Init(); - CompanySize.Code := LibraryUtility.GenerateGUID(); - CompanySize.Description := CompanySize.Code; - CompanySize.Insert(); - exit(CompanySize.Code); - end; - - procedure CreatePaymentPeriod(DaysFrom: Integer; DaysTo: Integer) - var - PaymentPeriod: Record "Payment Period"; - begin - PaymentPeriod.Init(); - PaymentPeriod."Days From" := DaysFrom; - PaymentPeriod."Days To" := DaysTo; - PaymentPeriod.Insert(); - end; - - procedure CreateVendorNoWithSizeAndExcl(CompanySizeCode: Code[20]; ExclFromPaymentPractice: Boolean) VendorNo: Code[20] - var - Vendor: Record Vendor; - begin - LibraryPurchase.CreateVendor(Vendor); - SetCompanySize(Vendor, CompanySizeCode); - SetExcludeFromPaymentPractices(Vendor, ExclFromPaymentPractice); - exit(Vendor."No."); - end; - - procedure InitializeCompanySizes(var CompanySizeCodes: array[3] of Code[20]) - var - i: Integer; - begin - for i := 1 to ArrayLen(CompanySizeCodes) do - CompanySizeCodes[i] := CreateCompanySizeCode(); - end; - - procedure InitializePaymentPeriods(var PaymentPeriods: array[3] of Record "Payment Period") - var - PaymentPeriod: Record "Payment Period"; - i: Integer; - begin - PaymentPeriod.SetupDefaults(); - PaymentPeriod.SetCurrentKey("Days From"); - PaymentPeriod.FindSet(); - for i := 1 to ArrayLen(PaymentPeriods) do begin - PaymentPeriods[i] := PaymentPeriod; - PaymentPeriod.Next(); - end; - end; - - procedure InitAndGetLastPaymentPeriod(var PaymentPeriod: Record "Payment Period") - begin - PaymentPeriod.SetupDefaults(); - PaymentPeriod.SetRange("Days To", 0); - PaymentPeriod.FindLast(); - end; - - procedure SetCompanySize(var Vendor: Record Vendor; CompanySizeCode: Code[20]) - begin - Vendor."Company Size Code" := CompanySizeCode; - Vendor.Modify(); - end; - - procedure SetExcludeFromPaymentPractices(var Vendor: Record Vendor; NewExcludeFromPaymentPractice: Boolean) - begin - Vendor."Exclude from Pmt. Practices" := NewExcludeFromPaymentPractice; - Vendor.Modify(); - end; - - procedure SetExcludeFromPaymentPractices(var Customer: Record Customer; NewExcludeFromPaymentPractice: Boolean) - begin - Customer."Exclude from Pmt. Practices" := NewExcludeFromPaymentPractice; - Customer.Modify(); - end; - - procedure SetExcludeFromPaymentPracticesOnAllVendorsAndCustomers() - var - Vendor: Record Vendor; - Customer: Record Customer; - begin - if Vendor.FindSet(true) then - repeat - SetExcludeFromPaymentPractices(Vendor, true); - until Vendor.Next() = 0; - if Customer.FindSet(true) then - repeat - SetExcludeFromPaymentPractices(Customer, true); - until Customer.Next() = 0; - end; - - procedure VerifyLinesCount(PaymentPracticeHeader: Record "Payment Practice Header"; NumberOfLines: Integer) - var - PaymentPracticeLine: Record "Payment Practice Line"; - begin - PaymentPracticeLine.SetRange("Header No.", PaymentPracticeHeader."No."); - Assert.RecordCount(PaymentPracticeLine, NumberOfLines); - end; - - procedure VerifyPeriodLine(PaymentPracticeHeaderNo: Integer; SourceType: Enum "Paym. Prac. Header Type"; PaymentPeriodCode: Code[20]; PctInPeriodExpected: Decimal; PctInPeriodAmountExpected: Decimal) - var - PaymentPracticeLine: Record "Payment Practice Line"; - begin - PaymentPracticeLine.SetRange("Header No.", PaymentPracticeHeaderNo); -#pragma warning disable AA0210 - PaymentPracticeLine.SetRange("Payment Period Code", PaymentPeriodCode); - PaymentPracticeLine.SetRange("Source Type", SourceType); -#pragma warning restore AA0210 - PaymentPracticeLine.FindFirst(); - Assert.AreNearlyEqual(PctInPeriodExpected, PaymentPracticeLine."Pct Paid in Period", 0.1, '"Pct Paid in Period" is not as expected'); - Assert.AreNearlyEqual(PctInPeriodAmountExpected, PaymentPracticeLine."Pct Paid in Period (Amount)", 0.1, '"Pct Paid in Period (Amount)" is not as expected'); - end; - - - procedure VerifyBufferCount(PaymentPracticeHeader: Record "Payment Practice Header"; NumberOfLines: Integer; SourceType: Enum "Paym. Prac. Header Type") - var - PaymentPracticeData: Record "Payment Practice Data"; - begin - PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); - PaymentPracticeData.SetRange("Source Type", SourceType); - Assert.RecordCount(PaymentPracticeData, NumberOfLines); - end; - -} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al index bd9c6ef507..6b9a6ecf46 100644 --- a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al @@ -3,13 +3,12 @@ // Licensed under the MIT License. See License.txt in the project root for license information. // ------------------------------------------------------------------------------------------------ +#pragma warning disable AA0210 // table does not contain key with field A namespace Microsoft.Test.Finance.Analysis; using Microsoft.Finance.Analysis; -using Microsoft.Finance.GeneralLedger.Journal; -using Microsoft.Purchases.Payables; using Microsoft.Sales.Customer; -using Microsoft.Sales.Receivables; +using System.Environment; codeunit 134197 "Payment Practices UT" { @@ -28,7 +27,6 @@ codeunit 134197 "Payment Practices UT" PaymentPracticesLibrary: Codeunit "Payment Practices Library"; PaymentPractices: Codeunit "Payment Practices"; LibraryPurchase: Codeunit "Library - Purchase"; - LibraryUtility: Codeunit "Library - Utility"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; LibraryRandom: Codeunit "Library - Random"; LibrarySales: Codeunit "Library - Sales"; @@ -69,11 +67,11 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Vendor with company size and an entry in the period VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); // [GIVEN]Vendor with company size and an entry in the period, but with Excl. from Payment Practice = true VendorExcludedNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[2], true); - MockVendorInvoice(VendorExcludedNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorExcludedNo, WorkDate(), WorkDate()); // [WHEN] Generate payment practices for vendors by size PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader); @@ -95,12 +93,12 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Customer with an entry in the period LibrarySales.CreateCustomer(Customer); - MockCustomerInvoice(Customer."No.", WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockCustomerInvoice(Customer."No.", WorkDate(), WorkDate()); // [GIVEN] Customer with an entry in the period, but with Excl. from Payment Practice = true LibrarySales.CreateCustomer(CustomerExcluded); PaymentPracticesLibrary.SetExcludeFromPaymentPractices(CustomerExcluded, true); - MockCustomerInvoice(CustomerExcluded."No.", WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockCustomerInvoice(CustomerExcluded."No.", WorkDate(), WorkDate()); // [WHEN] Generate payment practices for cust+vendors PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::"Vendor+Customer", "Paym. Prac. Aggregation Type"::Period); @@ -111,7 +109,7 @@ codeunit 134197 "Payment Practices UT" end; [Test] - [HandlerFunctions('ConfirmHandler_Yes')] + [HandlerFunctions('ConfirmHandlerYes')] procedure ConfirmToCleanUpOnAggrValidation_Yes() var PaymentPracticeHeader: Record "Payment Practice Header"; @@ -122,7 +120,7 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Vendor with company size and an entry in the period VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); // [GIVEN] Lines were generated for Header PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader); @@ -137,7 +135,7 @@ codeunit 134197 "Payment Practices UT" end; [Test] - [HandlerFunctions('ConfirmHandler_No')] + [HandlerFunctions('ConfirmHandlerNo')] procedure ConfirmToCleanUpOnAggrValidation_No() var PaymentPracticeHeader: Record "Payment Practice Header"; @@ -148,7 +146,7 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Vendor with company size and an entry in the period VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); // [GIVEN] Lines were generated for Header PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader); @@ -164,7 +162,7 @@ codeunit 134197 "Payment Practices UT" end; [Test] - [HandlerFunctions('ConfirmHandler_Yes')] + [HandlerFunctions('ConfirmHandlerYes')] procedure ConfirmToCleanUpOnTypeValidation() var PaymentPracticeHeader: Record "Payment Practice Header"; @@ -175,7 +173,7 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Vendor with company size and an entry in the period VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); // [GIVEN] Lines were generated for Header PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); @@ -218,7 +216,7 @@ codeunit 134197 "Payment Practices UT" for j := 1 to LibraryRandom.RandInt(10) do begin PeriodCounts[i] += 1; TotalCount += 1; - Amount := MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); + Amount := PaymentPracticesLibrary.MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); PeriodAmounts[i] += Amount; TotalAmount += Amount; end; @@ -228,9 +226,9 @@ codeunit 134197 "Payment Practices UT" // [THEN] Check that report dataset contains correct percentages for each period PrepareExpectedPeriodPcts(ExpectedPeriodPcts, ExpectedPeriodAmountPcts, PeriodCounts, TotalCount, PeriodAmounts, TotalAmount); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[1].Code, ExpectedPeriodPcts[1], ExpectedPeriodAmountPcts[1]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[2].Code, ExpectedPeriodPcts[2], ExpectedPeriodAmountPcts[2]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[3].Code, ExpectedPeriodPcts[3], ExpectedPeriodAmountPcts[3]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[1].Description, ExpectedPeriodPcts[1], ExpectedPeriodAmountPcts[1]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[2].Description, ExpectedPeriodPcts[2], ExpectedPeriodAmountPcts[2]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[3].Description, ExpectedPeriodPcts[3], ExpectedPeriodAmountPcts[3]); end; [Test] @@ -262,7 +260,7 @@ codeunit 134197 "Payment Practices UT" for j := 1 to LibraryRandom.RandInt(10) do begin PeriodCounts[i] += 1; TotalCount += 1; - Amount := MockCustomerInvoiceAndPaymentInPeriod(CustomerNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); + Amount := PaymentPracticesLibrary.MockCustomerInvoiceAndPaymentInPeriod(CustomerNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); PeriodAmounts[i] += Amount; TotalAmount += Amount; end; @@ -272,9 +270,9 @@ codeunit 134197 "Payment Practices UT" // [THEN] Check that report dataset contains correct percentages for each period PrepareExpectedPeriodPcts(ExpectedPeriodPcts, ExpectedPeriodAmountPcts, PeriodCounts, TotalCount, PeriodAmounts, TotalAmount); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[1].Code, ExpectedPeriodPcts[1], ExpectedPeriodAmountPcts[1]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[2].Code, ExpectedPeriodPcts[2], ExpectedPeriodAmountPcts[2]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[3].Code, ExpectedPeriodPcts[3], ExpectedPeriodAmountPcts[3]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[1].Description, ExpectedPeriodPcts[1], ExpectedPeriodAmountPcts[1]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[2].Description, ExpectedPeriodPcts[2], ExpectedPeriodAmountPcts[2]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[3].Description, ExpectedPeriodPcts[3], ExpectedPeriodAmountPcts[3]); end; [Test] @@ -316,7 +314,7 @@ codeunit 134197 "Payment Practices UT" for j := 1 to LibraryRandom.RandInt(10) do begin Vendor_PeriodCounts[i] += 1; Vendor_TotalCount += 1; - Amount := MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); + Amount := PaymentPracticesLibrary.MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); Vendor_PeriodAmounts[i] += Amount; Vendor_TotalAmount += Amount; end; @@ -326,7 +324,7 @@ codeunit 134197 "Payment Practices UT" for j := 1 to LibraryRandom.RandInt(10) do begin Customer_PeriodCounts[i] += 1; Customer_TotalCount += 1; - Amount := MockCustomerInvoiceAndPaymentInPeriod(CustomerNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); + Amount := PaymentPracticesLibrary.MockCustomerInvoiceAndPaymentInPeriod(CustomerNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); Customer_PeriodAmounts[i] += Amount; Customer_TotalAmount += Amount; end; @@ -336,15 +334,15 @@ codeunit 134197 "Payment Practices UT" // [THEN] Check that report dataset contains correct percentages for each period for vendors PrepareExpectedPeriodPcts(Vendor_ExpectedPeriodPcts, Vendor_ExpectedPeriodAmountPcts, Vendor_PeriodCounts, Vendor_TotalCount, Vendor_PeriodAmounts, Vendor_TotalAmount); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[1].Code, Vendor_ExpectedPeriodPcts[1], Vendor_ExpectedPeriodAmountPcts[1]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[2].Code, Vendor_ExpectedPeriodPcts[2], Vendor_ExpectedPeriodAmountPcts[2]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[3].Code, Vendor_ExpectedPeriodPcts[3], Vendor_ExpectedPeriodAmountPcts[3]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[1].Description, Vendor_ExpectedPeriodPcts[1], Vendor_ExpectedPeriodAmountPcts[1]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[2].Description, Vendor_ExpectedPeriodPcts[2], Vendor_ExpectedPeriodAmountPcts[2]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[3].Description, Vendor_ExpectedPeriodPcts[3], Vendor_ExpectedPeriodAmountPcts[3]); // [THEN] Check that report dataset contains correct percentages for each period for customers PrepareExpectedPeriodPcts(Customer_ExpectedPeriodPcts, Customer_ExpectedPeriodAmountPcts, Customer_PeriodCounts, Customer_TotalCount, Customer_PeriodAmounts, Customer_TotalAmount); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[1].Code, Customer_ExpectedPeriodPcts[1], Customer_ExpectedPeriodAmountPcts[1]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[2].Code, Customer_ExpectedPeriodPcts[2], Customer_ExpectedPeriodAmountPcts[2]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[3].Code, Customer_ExpectedPeriodPcts[3], Customer_ExpectedPeriodAmountPcts[3]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[1].Description, Customer_ExpectedPeriodPcts[1], Customer_ExpectedPeriodAmountPcts[1]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[2].Description, Customer_ExpectedPeriodPcts[2], Customer_ExpectedPeriodAmountPcts[2]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[3].Description, Customer_ExpectedPeriodPcts[3], Customer_ExpectedPeriodAmountPcts[3]); end; [Test] @@ -370,21 +368,21 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Post several entries paid on time, this will affect total entries considered and total entries paid on time. PaidOnTimeCount := LibraryRandom.RandInt(20); for i := 1 to PaidOnTimeCount do - MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate() + LibraryRandom.RandInt(10), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate() + LibraryRandom.RandInt(10), WorkDate()); // [GIVEN] Post several entries paid late, this will affect total entries considered. PaidLateCount := LibraryRandom.RandInt(20); for i := 1 to PaidLateCount do - MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + LibraryRandom.RandInt(10)); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + LibraryRandom.RandInt(10)); // [GIVEN] Post several entries unpaid overdue, this will affect total entries considered. UnpaidOverdueCount := LibraryRandom.RandInt(20); for i := 1 to UnpaidOverdueCount do - MockVendorInvoice(VendorNo, WorkDate() - 50, WorkDate() - LibraryRandom.RandInt(40)); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate() - 50, WorkDate() - LibraryRandom.RandInt(40)); // [GIVEN] Post several entries unpaid not overdue, these will not affect count for i := 1 to LibraryRandom.RandInt(20) do - MockVendorInvoice(VendorNo, WorkDate(), WorkDate() + LibraryRandom.RandInt(10)); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate() + LibraryRandom.RandInt(10)); // [WHEN] Lines were generated for Header PaymentPractices.Generate(PaymentPracticeHeader); @@ -419,7 +417,7 @@ codeunit 134197 "Payment Practices UT" TotalEntries := LibraryRandom.RandInt(100); for i := 1 to TotalEntries do begin ActualPaymentTime := LibraryRandom.RandInt(30); - MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + ActualPaymentTime); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + ActualPaymentTime); ActualPaymentTimeSum += ActualPaymentTime; end; @@ -457,7 +455,7 @@ codeunit 134197 "Payment Practices UT" TotalPaidEntries := LibraryRandom.RandInt(100); for i := 1 to TotalPaidEntries do begin AgreedPaymentTime := LibraryRandom.RandInt(30); - MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate() + AgreedPaymentTime, WorkDate() + AgreedPaymentTime); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate() + AgreedPaymentTime, WorkDate() + AgreedPaymentTime); AgreedPaymentTimeSum += AgreedPaymentTime; end; @@ -465,7 +463,7 @@ codeunit 134197 "Payment Practices UT" TotalUnpaidEntries += LibraryRandom.RandInt(100); for i := 1 to TotalUnpaidEntries do begin AgreedPaymentTime := LibraryRandom.RandInt(30); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate() + AgreedPaymentTime); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate() + AgreedPaymentTime); AgreedPaymentTimeSum += AgreedPaymentTime; end; // [WHEN] Lines were generated for Header @@ -498,13 +496,13 @@ codeunit 134197 "Payment Practices UT" PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); // [GIVEN] Post an entry for the vendor in the period - MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriod."Days From", PaymentPeriod."Days To"); + PaymentPracticesLibrary.MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriod."Days From", PaymentPeriod."Days To"); // [WHEN] Lines were generated for Header PaymentPractices.Generate(PaymentPracticeHeader); // [THEN] Check that report dataset contains the line for the period correcly - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriod.Code, 100, 0); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriod.Description, 100, 0); end; [Test] @@ -584,7 +582,7 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Vendor "V" with company size and an entry in the period VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); // [GIVEN] A payment practice header "PPH" PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader); @@ -605,116 +603,303 @@ codeunit 134197 "Payment Practices UT" PaymentPracticeCard.Close(); end; - local procedure Initialize() + [Test] + procedure StandardSchemeGenerateProducesSameResults() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + VendorNo: Code[20]; begin - LibraryTestInitialize.OnTestInitialize(Codeunit::"Payment Practices UT"); + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] Generating payment practices with Standard reporting scheme produces correct data for vendor entries + Initialize(); - // This is so demodata and previous tests doesn't influence the tests - PaymentPracticesLibrary.SetExcludeFromPaymentPracticesOnAllVendorsAndCustomers(); + // [GIVEN] Vendor with entry + VendorNo := LibraryPurchase.CreateVendorNo(); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate() + 30, WorkDate() + 10); + + // [WHEN] Generate with Standard scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderWithScheme( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::Period, + "Paym. Prac. Reporting Scheme"::Standard); + PaymentPractices.Generate(PaymentPracticeHeader); - if Initialized then - exit; + // [THEN] Data is generated correctly + PaymentPracticesLibrary.VerifyBufferCount(PaymentPracticeHeader, 1, "Paym. Prac. Header Type"::Vendor); + end; - LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Payment Practices UT"); + [Test] + procedure ReportingSchemeAutoDetectionOnInsert() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + EnvironmentInformation: Codeunit "Environment Information"; + ExpectedScheme: Enum "Paym. Prac. Reporting Scheme"; + begin + // [FEATURE] [AI test 0.3] + // [SCENARIO 629871] Inserting a Payment Practice Header auto-detects the Reporting Scheme based on environment + Initialize(); - PaymentPracticesLibrary.InitializeCompanySizes(CompanySizeCodes); - PaymentPracticesLibrary.InitializePaymentPeriods(PaymentPeriods); - Initialized := true; + // [WHEN] Insert a new Payment Practice Header via Insert(true) + PaymentPracticeHeader.Init(); + PaymentPracticeHeader.Insert(true); - LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Payment Practices UT"); + // [THEN] Reporting Scheme is auto-detected based on environment application family + case EnvironmentInformation.GetApplicationFamily() of + 'GB': + ExpectedScheme := "Paym. Prac. Reporting Scheme"::"Dispute & Retention"; + else + ExpectedScheme := "Paym. Prac. Reporting Scheme"::Standard; + end; + Assert.AreEqual( + ExpectedScheme, + PaymentPracticeHeader."Reporting Scheme", + 'Reporting Scheme should be auto-detected based on environment application family.'); end; - local procedure MockVendLedgerEntry(VendorNo: Code[20]; var VendorLedgerEntry: Record "Vendor Ledger Entry"; DocType: Enum "Gen. Journal Document Type"; PostingDate: Date; DueDate: Date; PmtPostingDate: Date; IsOpen: Boolean) + [Test] + procedure DisputeRetCalcHeaderTotalsAllPaidOnTime() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeData: Record "Payment Practice Data"; + InvoiceAmount1: Decimal; + InvoiceAmount2: Decimal; begin - VendorLedgerEntry.Init(); - VendorLedgerEntry."Entry No." := LibraryUtility.GetNewRecNo(VendorLedgerEntry, VendorLedgerEntry.FieldNo("Entry No.")); - VendorLedgerEntry."Document Type" := DocType; - VendorLedgerEntry."Posting Date" := PostingDate; - VendorLedgerEntry."Document Date" := PostingDate; - VendorLedgerEntry."Vendor No." := VendorNo; - VendorLedgerEntry."Due Date" := DueDate; - VendorLedgerEntry.Open := IsOpen; - VendorLedgerEntry."Closed at Date" := PmtPostingDate; - VendorLedgerEntry.Amount := LibraryRandom.RandDec(1000, 2); - VendorLedgerEntry.Insert(); + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] CalculateHeaderTotals counts all closed invoices paid on time with zero overdue totals + Initialize(); + + // [GIVEN] Payment Practice Header "PPH" with Dispute and Retention scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderWithScheme( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::Period, + "Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + + // [GIVEN] Two closed invoices paid on time (Actual <= Agreed) + InvoiceAmount1 := 500; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 1, false, 10, 30, InvoiceAmount1, false); + InvoiceAmount2 := 300; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 2, false, 20, 30, InvoiceAmount2, false); + + // [WHEN] CalculateHeaderTotals is called + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + PaymentPracticesLibrary.DisputeRetCalcHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); + + // [THEN] Total payments = 2, total amount = sum of both, overdue = 0, dispute pct = 0 + Assert.AreEqual(2, PaymentPracticeHeader."Total Number of Payments", 'Total Number of Payments'); + Assert.AreEqual(InvoiceAmount1 + InvoiceAmount2, PaymentPracticeHeader."Total Amount of Payments", 'Total Amount of Payments'); + Assert.AreEqual(0, PaymentPracticeHeader."Total Amt. of Overdue Payments", 'Total Amt. of Overdue Payments'); + Assert.AreEqual(0, PaymentPracticeHeader."Pct Overdue Due to Dispute", 'Pct Overdue Due to Dispute'); end; - local procedure MockVendorInvoice(VendorNo: Code[20]; PostingDate: Date; DueDate: Date) InvoiceAmount: Decimal; + [Test] + procedure DisputeRetCalcHeaderTotalsOverdueNoDispute() var - VendorLedgerEntry: Record "Vendor Ledger Entry"; + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeData: Record "Payment Practice Data"; + InvoiceAmount: Decimal; begin - MockVendLedgerEntry(VendorNo, VendorLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, 0D, true); - VendorLedgerEntry.CalcFields("Amount (LCY)"); - InvoiceAmount := VendorLedgerEntry."Amount (LCY)"; + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] CalculateHeaderTotals calculates overdue amounts correctly when no invoices are disputed + Initialize(); + + // [GIVEN] Payment Practice Header "PPH" with Dispute and Retention scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderWithScheme( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::Period, + "Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + + // [GIVEN] Closed overdue invoice without dispute (Actual > Agreed, Dispute = false) + InvoiceAmount := 1000; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 1, false, 45, 30, InvoiceAmount, false); + + // [WHEN] CalculateHeaderTotals is called + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + PaymentPracticesLibrary.DisputeRetCalcHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); + + // [THEN] Total payments = 1, overdue amount = invoice amount, dispute pct = 0 + Assert.AreEqual(1, PaymentPracticeHeader."Total Number of Payments", 'Total Number of Payments'); + Assert.AreEqual(InvoiceAmount, PaymentPracticeHeader."Total Amt. of Overdue Payments", 'Total Amt. of Overdue Payments'); + Assert.AreEqual(0, PaymentPracticeHeader."Pct Overdue Due to Dispute", 'Pct Overdue Due to Dispute'); + + // [THEN] Data record has "Overdue Due to Dispute" = false + PaymentPracticeData.FindFirst(); + Assert.IsFalse(PaymentPracticeData."Overdue Due to Dispute", 'Overdue Due to Dispute should be false'); end; - local procedure MockVendorInvoiceAndPayment(VendorNo: Code[20]; PostingDate: Date; DueDate: Date; PaymentPostingDate: Date) InvoiceAmount: Decimal; + [Test] + procedure DisputeRetCalcHeaderTotalsMixedOverdueDispute() var - VendorLedgerEntry: Record "Vendor Ledger Entry"; + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeData: Record "Payment Practice Data"; + OverdueAmount1: Decimal; + OverdueAmount2: Decimal; + OverdueAmount3: Decimal; begin - MockVendLedgerEntry(VendorNo, VendorLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, PaymentPostingDate, false); - VendorLedgerEntry.CalcFields("Amount (LCY)"); - InvoiceAmount := VendorLedgerEntry."Amount (LCY)"; + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] CalculateHeaderTotals calculates correct dispute percentage when some overdue invoices are disputed + Initialize(); + + // [GIVEN] Payment Practice Header "PPH" with Dispute and Retention scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderWithScheme( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::Period, + "Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + + // [GIVEN] Three overdue invoices: one disputed, two not + OverdueAmount1 := 100; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 1, false, 45, 30, OverdueAmount1, true); + OverdueAmount2 := 200; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 2, false, 50, 30, OverdueAmount2, false); + OverdueAmount3 := 300; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 3, false, 60, 30, OverdueAmount3, false); + + // [WHEN] CalculateHeaderTotals is called + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + PaymentPracticesLibrary.DisputeRetCalcHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); + + // [THEN] Dispute pct = 1/3 * 100 ≈ 33.33 + Assert.AreEqual(3, PaymentPracticeHeader."Total Number of Payments", 'Total Number of Payments'); + Assert.AreEqual(OverdueAmount1 + OverdueAmount2 + OverdueAmount3, PaymentPracticeHeader."Total Amt. of Overdue Payments", 'Total Amt. of Overdue Payments'); + Assert.AreNearlyEqual(33.33, PaymentPracticeHeader."Pct Overdue Due to Dispute", 0.01, 'Pct Overdue Due to Dispute'); end; - local procedure MockVendorInvoiceAndPaymentInPeriod(VendorNo: Code[20]; StartingDate: Date; PaidInDays_min: Integer; PaidInDays_max: Integer) InvoiceAmount: Decimal; - var - PostingDate: Date; - DueDate: Date; - PaymentPostingDate: Date; - begin - PostingDate := StartingDate; - DueDate := StartingDate; - if PaidInDays_max <> 0 then - PaymentPostingDate := PostingDate + LibraryRandom.RandIntInRange(PaidInDays_min, PaidInDays_max) - else - PaymentPostingDate := PostingDate + PaidInDays_min + LibraryRandom.RandInt(10); - InvoiceAmount := MockVendorInvoiceAndPayment(VendorNo, PostingDate, DueDate, PaymentPostingDate); - end; - - local procedure MockCustLedgerEntry(CustomerNo: Code[20]; var CustLedgerEntry: Record "Cust. Ledger Entry"; DocType: Enum "Gen. Journal Document Type"; PostingDate: Date; DueDate: Date; PmtPostingDate: Date; IsOpen: Boolean) - begin - CustLedgerEntry.Init(); - CustLedgerEntry."Entry No." := LibraryUtility.GetNewRecNo(CustLedgerEntry, CustLedgerEntry.FieldNo("Entry No.")); - CustLedgerEntry."Document Type" := DocType; - CustLedgerEntry."Posting Date" := PostingDate; - CustLedgerEntry."Document Date" := PostingDate; - CustLedgerEntry."Customer No." := CustomerNo; - CustLedgerEntry."Due Date" := DueDate; - CustLedgerEntry.Open := IsOpen; - CustLedgerEntry."Closed at Date" := PmtPostingDate; - CustLedgerEntry.Amount := LibraryRandom.RandDec(1000, 2); - CustLedgerEntry.Insert(); - end; - - local procedure MockCustomerInvoice(CustomerNo: Code[20]; PostingDate: Date; DueDate: Date) InvoiceAmount: Decimal; + [Test] + procedure DisputeRetCalcHeaderTotalsMixOnTimeAndOverdue() var - CustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeData: Record "Payment Practice Data"; + OnTimeAmount: Decimal; + OverdueDisputedAmount: Decimal; + OverdueNotDisputedAmount: Decimal; + OpenAmount: Decimal; begin - MockCustLedgerEntry(CustomerNo, CustLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, 0D, true); - CustLedgerEntry.CalcFields("Amount (LCY)"); - InvoiceAmount := CustLedgerEntry."Amount (LCY)"; + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] CalculateHeaderTotals correctly handles a mix of on-time, overdue, and open invoices + Initialize(); + + // [GIVEN] Payment Practice Header "PPH" with Dispute and Retention scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderWithScheme( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::Period, + "Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + + // [GIVEN] One on-time closed invoice + OnTimeAmount := 400; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 1, false, 10, 30, OnTimeAmount, false); + + // [GIVEN] One overdue disputed invoice + OverdueDisputedAmount := 600; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 2, false, 45, 30, OverdueDisputedAmount, true); + + // [GIVEN] One overdue non-disputed invoice + OverdueNotDisputedAmount := 200; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 3, false, 50, 30, OverdueNotDisputedAmount, false); + + // [GIVEN] One open invoice (should be skipped) + OpenAmount := 999; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 4, true, 0, 30, OpenAmount, false); + + // [WHEN] CalculateHeaderTotals is called + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + PaymentPracticesLibrary.DisputeRetCalcHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); + + // [THEN] Total payments = 3 (open skipped), total amount = on-time + both overdue + Assert.AreEqual(3, PaymentPracticeHeader."Total Number of Payments", 'Total Number of Payments'); + Assert.AreEqual(OnTimeAmount + OverdueDisputedAmount + OverdueNotDisputedAmount, PaymentPracticeHeader."Total Amount of Payments", 'Total Amount of Payments'); + + // [THEN] Overdue amount = only overdue invoices + Assert.AreEqual(OverdueDisputedAmount + OverdueNotDisputedAmount, PaymentPracticeHeader."Total Amt. of Overdue Payments", 'Total Amt. of Overdue Payments'); + + // [THEN] Dispute pct = 1/2 * 100 = 50 (1 disputed out of 2 overdue) + Assert.AreEqual(50, PaymentPracticeHeader."Pct Overdue Due to Dispute", 'Pct Overdue Due to Dispute'); end; - local procedure MockCustomerInvoiceAndPayment(CustomerNo: Code[20]; PostingDate: Date; DueDate: Date; PaymentPostingDate: Date) InvoiceAmount: Decimal; + [Test] + procedure CompanySizeGenerationSucceedsWithBlankPeriodCode() var - CustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentPracticeHeader: Record "Payment Practice Header"; + VendorNo: Code[20]; begin - MockCustLedgerEntry(CustomerNo, CustLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, PaymentPostingDate, false); - CustLedgerEntry.CalcFields("Amount (LCY)"); - InvoiceAmount := CustLedgerEntry."Amount (LCY)"; + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] Company Size aggregation succeeds + Initialize(); + + // [GIVEN] Vendor "V" with company size and an entry in the period + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + + // [GIVEN] Header with Company Size aggregation + PaymentPracticesLibrary.CreatePaymentPracticeHeader( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::"Company Size", + WorkDate() - 180, WorkDate() + 180); + + // [WHEN] Generate payment practices + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Lines are created (one per company size code) + PaymentPracticesLibrary.VerifyLinesCount(PaymentPracticeHeader, 3); + + // [THEN] Data rows include the vendor invoice + PaymentPracticesLibrary.VerifyBufferCount(PaymentPracticeHeader, 1, "Paym. Prac. Header Type"::Vendor); end; - local procedure MockCustomerInvoiceAndPaymentInPeriod(CustomerNo: Code[20]; StartingDate: Date; PaidInDays_min: Integer; PaidInDays_max: Integer) InvoiceAmount: Decimal; + [Test] + procedure CompanySizeStandardLeavesInvoiceCountAndValueZero() var - PostingDate: Date; - DueDate: Date; - PaymentPostingDate: Date; + PaymentPracticeHeader: Record "Payment Practice Header"; + VendorNo: Code[20]; + begin + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] Standard scheme with Company Size aggregation leaves Invoice Count and Invoice Value at zero + Initialize(); + + // [GIVEN] Vendor "V" with company size and a closed invoice in the period + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 10); + + // [GIVEN] Header with Standard scheme and Company Size aggregation + PaymentPracticesLibrary.CreatePaymentPracticeHeader( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::"Company Size", + "Paym. Prac. Reporting Scheme"::Standard, + WorkDate() - 180, WorkDate() + 180); + + // [WHEN] Generate payment practices + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Lines exist + PaymentPracticesLibrary.VerifyLinesCount(PaymentPracticeHeader, 3); + + // [THEN] All lines have Invoice Count = 0 and Invoice Value = 0 + VerifyAllLinesInvoiceCountAndValueZero(PaymentPracticeHeader."No."); + end; + + local procedure Initialize() begin - PostingDate := StartingDate; - DueDate := StartingDate + LibraryRandom.RandIntInRange(1, 5); - PaymentPostingDate := PostingDate + LibraryRandom.RandIntInRange(PaidInDays_min, PaidInDays_max); - InvoiceAmount := MockCustomerInvoiceAndPayment(CustomerNo, PostingDate, DueDate, PaymentPostingDate); + LibraryTestInitialize.OnTestInitialize(Codeunit::"Payment Practices UT"); + + // This is so demodata and previous tests doesn't influence the tests + PaymentPracticesLibrary.SetExcludeFromPaymentPracticesOnAllVendorsAndCustomers(); + + if Initialized then + exit; + + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Payment Practices UT"); + + PaymentPracticesLibrary.InitializeCompanySizes(CompanySizeCodes); + PaymentPracticesLibrary.CreateDefaultPaymentPeriodTemplates(); + PaymentPracticesLibrary.InitializePaymentPeriods(PaymentPeriods); + Initialized := true; + + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Payment Practices UT"); end; local procedure PrepareExpectedPeriodPcts(var ExpectedPeriodPcts: array[3] of Decimal; var ExpectedPeriodAmountPcts: array[3] of Decimal; PeriodCounts: array[3] of Integer; TotalCount: Integer; PeriodAmounts: array[3] of Decimal; TotalAmount: Decimal) @@ -729,15 +914,49 @@ codeunit 134197 "Payment Practices UT" end; end; + local procedure MockPaymentPracticeData(HeaderNo: Integer; EntryNo: Integer; IsOpen: Boolean; ActualPaymentDays: Integer; AgreedPaymentDays: Integer; InvoiceAmount: Decimal; IsDisputed: Boolean) + var + PaymentPracticeData: Record "Payment Practice Data"; + begin + PaymentPracticeData.Init(); + PaymentPracticeData."Header No." := HeaderNo; + PaymentPracticeData."Invoice Entry No." := EntryNo; + PaymentPracticeData."Source Type" := "Paym. Prac. Header Type"::Vendor; + PaymentPracticeData."Invoice Is Open" := IsOpen; + PaymentPracticeData."Actual Payment Days" := ActualPaymentDays; + PaymentPracticeData."Agreed Payment Days" := AgreedPaymentDays; + PaymentPracticeData."Invoice Amount" := InvoiceAmount; + if IsDisputed then + PaymentPracticeData."Dispute Status" := 'DISPUTED'; + PaymentPracticeData.Insert(); + end; + + local procedure VerifyAllLinesInvoiceCountAndValueZero(HeaderNo: Integer) + var + PaymentPracticeLine: Record "Payment Practice Line"; + begin + PaymentPracticeLine.SetRange("Header No.", HeaderNo); + PaymentPracticeLine.FindSet(); + repeat + Assert.AreEqual(0, PaymentPracticeLine."Invoice Count", 'Invoice Count should be 0 for Standard scheme'); + Assert.AreEqual(0, PaymentPracticeLine."Invoice Value", 'Invoice Value should be 0 for Standard scheme'); + until PaymentPracticeLine.Next() = 0; + end; + [ConfirmHandler] - procedure ConfirmHandler_Yes(Question: Text[1024]; var Reply: Boolean) + procedure ConfirmHandlerYes(Question: Text[1024]; var Reply: Boolean) begin Reply := true; end; [ConfirmHandler] - procedure ConfirmHandler_No(Question: Text[1024]; var Reply: Boolean) + procedure ConfirmHandlerNo(Question: Text[1024]; var Reply: Boolean) begin Reply := false; end; + + [MessageHandler] + procedure MessageHandler(Message: Text[1024]) + begin + end; } From f12e66bbae03c6f70f6d2f14e2730e93b782ffb2 Mon Sep 17 00:00:00 2001 From: Aleksandr Gladkov Date: Mon, 27 Apr 2026 14:58:02 +0200 Subject: [PATCH 02/12] GB + AU + tests --- src/Apps/W1/PaymentPractices/App/app.json | 11 +- .../Enums/PaymPracReportingScheme.Enum.al | 23 + .../PaymPracDisputeRetHdlr.Codeunit.al | 72 +++ .../PaymPracPeriodAggregator.Codeunit.al | 11 +- .../PaymPracSizeAggregator.Codeunit.al | 7 +- .../PaymPracSmallBusHandler.Codeunit.al | 27 + .../PaymPracStandardHandler.Codeunit.al | 30 ++ .../PaymentPracticeSchemeHandler.Interface.al | 36 ++ .../App/src/Core/PaymentPeriodMgt.Codeunit.al | 35 ++ .../Core/PaymentPracticeBuilders.Codeunit.al | 14 +- .../App/src/Core/PaymentPractices.Codeunit.al | 8 + .../PaymPracObjects.PermissionSet.al | 4 + .../Core/UpgradePaymentPractices.Codeunit.al | 32 ++ .../src/Pages/PaymPracVendLedgEntr.PageExt.al | 22 + .../App/src/Pages/PaymentPeriods.Page.al | 34 +- .../App/src/Pages/PaymentPracticeCard.Page.al | 26 +- .../src/Pages/PaymentPracticeDataList.Page.al | 16 +- .../src/Pages/PaymentPracticeLines.Page.al | 8 + .../Tables/PaymPracVendLedgEntry.TableExt.al | 20 + .../App/src/Tables/PaymentPeriod.Table.al | 11 - .../src/Tables/PaymentPracticeData.Table.al | 18 + .../src/Tables/PaymentPracticeHeader.Table.al | 38 ++ .../src/Tables/PaymentPracticeLine.Table.al | 14 +- .../Test Library/ExtensionLogo.png | Bin 0 -> 5446 bytes .../W1/PaymentPractices/Test Library/app.json | 46 ++ .../src/PaymentPracticesLibrary.Codeunit.al | 298 +++++++++++ src/Apps/W1/PaymentPractices/Test/app.json | 8 +- .../src/PaymentPracticesLibrary.Codeunit.al | 165 ------- .../Test/src/PaymentPracticesUT.Codeunit.al | 465 +++++++++++++----- 29 files changed, 1185 insertions(+), 314 deletions(-) create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/Enums/PaymPracReportingScheme.Enum.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracStandardHandler.Codeunit.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al create mode 100644 src/Apps/W1/PaymentPractices/App/src/Tables/PaymPracVendLedgEntry.TableExt.al create mode 100644 src/Apps/W1/PaymentPractices/Test Library/ExtensionLogo.png create mode 100644 src/Apps/W1/PaymentPractices/Test Library/app.json create mode 100644 src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al delete mode 100644 src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesLibrary.Codeunit.al diff --git a/src/Apps/W1/PaymentPractices/App/app.json b/src/Apps/W1/PaymentPractices/App/app.json index 6f4428a507..72e77013eb 100644 --- a/src/Apps/W1/PaymentPractices/App/app.json +++ b/src/Apps/W1/PaymentPractices/App/app.json @@ -16,8 +16,8 @@ "platform": "29.0.0.0", "idRanges": [ { - "from": 685, - "to": 694 + "from": 680, + "to": 698 } ], "resourceExposurePolicy": { @@ -29,5 +29,12 @@ "target": "Cloud", "features": [ "TranslationFile" + ], + "internalsVisibleTo": [ + { + "id": "cc329ed7-8840-45f6-860b-3eb99c408998", + "name": "Payment Practices Test Library", + "publisher": "Microsoft" + } ] } diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Enums/PaymPracReportingScheme.Enum.al b/src/Apps/W1/PaymentPractices/App/src/Core/Enums/PaymPracReportingScheme.Enum.al new file mode 100644 index 0000000000..65d275c006 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Enums/PaymPracReportingScheme.Enum.al @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +enum 680 "Paym. Prac. Reporting Scheme" implements PaymentPracticeSchemeHandler +{ + Extensible = true; + + value(0; Standard) + { + Implementation = PaymentPracticeSchemeHandler = "Paym. Prac. Standard Handler"; + } + value(1; "Dispute & Retention") + { + Implementation = PaymentPracticeSchemeHandler = "Paym. Prac. Dispute Ret. Hdlr"; + } + value(2; "Small Business") + { + Implementation = PaymentPracticeSchemeHandler = "Paym. Prac. Small Bus. Handler"; + } +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al new file mode 100644 index 0000000000..0f964a0d73 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al @@ -0,0 +1,72 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +using Microsoft.Purchases.Payables; + +codeunit 681 "Paym. Prac. Dispute Ret. Hdlr" implements PaymentPracticeSchemeHandler +{ + Access = Internal; + + procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") + begin + // Dispute & Retention: no additional header type restrictions + end; + + procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean + var + VendorLedgerEntry: Record "Vendor Ledger Entry"; + begin + if PaymentPracticeData."Source Type" <> PaymentPracticeData."Source Type"::Vendor then + exit(true); + + VendorLedgerEntry.SetLoadFields("SCF Payment Date", "Dispute Status"); + if VendorLedgerEntry.Get(PaymentPracticeData."Invoice Entry No.") then begin + PaymentPracticeData."SCF Payment Date" := VendorLedgerEntry."SCF Payment Date"; + PaymentPracticeData."Dispute Status" := VendorLedgerEntry."Dispute Status"; + + if PaymentPracticeData."SCF Payment Date" <> 0D then + PaymentPracticeData."Actual Payment Days" := PaymentPracticeData."SCF Payment Date" - PaymentPracticeData."Invoice Received Date"; + end; + + exit(true); + end; + + procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + var + TotalPayments: Integer; + TotalAmount: Decimal; + TotalOverdueAmount: Decimal; + OverdueCount: Integer; + OverdueDueToDisputeCount: Integer; + begin + if PaymentPracticeData.FindSet() then + repeat + if not PaymentPracticeData."Invoice Is Open" then begin + TotalPayments += 1; + TotalAmount += PaymentPracticeData."Invoice Amount"; + if PaymentPracticeData."Actual Payment Days" > PaymentPracticeData."Agreed Payment Days" then begin + OverdueCount += 1; + TotalOverdueAmount += PaymentPracticeData."Invoice Amount"; + PaymentPracticeData."Overdue Due to Dispute" := PaymentPracticeData."Dispute Status" <> ''; + PaymentPracticeData.Modify(); + if PaymentPracticeData."Dispute Status" <> '' then + OverdueDueToDisputeCount += 1; + end; + end; + until PaymentPracticeData.Next() = 0; + + PaymentPracticeHeader."Total Number of Payments" := TotalPayments; + PaymentPracticeHeader."Total Amount of Payments" := TotalAmount; + PaymentPracticeHeader."Total Amt. of Overdue Payments" := TotalOverdueAmount; + if OverdueCount > 0 then + PaymentPracticeHeader."Pct Overdue Due to Dispute" := OverdueDueToDisputeCount / OverdueCount * 100; + end; + + procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") + begin + // Dispute & Retention: no additional line totals + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al index 9c8f0aa311..5f4fa8b16e 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al @@ -26,10 +26,12 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr var PaymentPracticeLine: Record "Payment Practice Line"; PaymentPeriod: Record "Payment Period"; + SchemeHandler: Interface PaymentPracticeSchemeHandler; SourceType: Integer; NextLineNo: Integer; begin NextLineNo := 1; + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; PaymentPeriod.SetCurrentKey("Days From"); PaymentPeriod.SetAscending("Days From", true); foreach SourceType in PaymentPracticeData."Source Type".Ordinals() do begin @@ -37,7 +39,10 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr if not PaymentPracticeData.IsEmpty() then if PaymentPeriod.FindSet() then repeat - InsertPeriodLine(PaymentPracticeLine, PaymentPracticeData, PaymentPeriod, PaymentPracticeHeader."No.", NextLineNo); + InsertPeriodLine(PaymentPracticeLine, PaymentPracticeData, PaymentPeriod, PaymentPracticeHeader."No.", NextLineNo, SourceType); + SchemeHandler.CalculateLineTotals(PaymentPracticeLine, PaymentPracticeData); + if (PaymentPracticeLine."Invoice Count" <> 0) or (PaymentPracticeLine."Invoice Value" <> 0) then + PaymentPracticeLine.Modify(); until PaymentPeriod.Next() = 0; end; end; @@ -47,7 +52,7 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr end; - local procedure InsertPeriodLine(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data"; PaymentPeriod: Record "Payment Period"; HeaderNo: Integer; var NextLineNo: Integer) + local procedure InsertPeriodLine(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data"; PaymentPeriod: Record "Payment Period"; HeaderNo: Integer; var NextLineNo: Integer; SourceType: Integer) begin PaymentPracticeLine.Init(); PaymentPracticeLine."Header No." := HeaderNo; @@ -57,7 +62,7 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr PaymentPracticeLine."Payment Period Code" := PaymentPeriod.Code; PaymentPracticeLine."Payment Period Description" := PaymentPeriod.Description; SetPercentPaidInPeriod(PaymentPracticeData, PaymentPeriod."Days From", PaymentPeriod."Days To", PaymentPracticeLine."Pct Paid in Period", PaymentPracticeLine."Pct Paid in Period (Amount)"); - PaymentPracticeLine."Source Type" := PaymentPracticeData."Source Type"; + PaymentPracticeLine."Source Type" := "Paym. Prac. Header Type".FromInteger(SourceType); PaymentPracticeLine.Insert(); end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al index 6b47f4f6a5..b2cae99983 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al @@ -28,9 +28,11 @@ codeunit 686 "Paym. Prac. Size Aggregator" implements PaymentPracticeLinesAggreg var PaymentPracticeLine: Record "Payment Practice Line"; CompanySize: Record "Company Size"; + SchemeHandler: Interface PaymentPracticeSchemeHandler; NextLineNo: Integer; begin NextLineNo := 1; + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; if CompanySize.FindSet() then repeat PaymentPracticeLine.Init(); @@ -45,9 +47,12 @@ codeunit 686 "Paym. Prac. Size Aggregator" implements PaymentPracticeLinesAggreg PaymentPracticeLine."Average Actual Payment Period" := PaymentPracticeMath.GetAverageActualPaymentTime(PaymentPracticeData); PaymentPracticeLine."Average Agreed Payment Period" := PaymentPracticeMath.GetAverageAgreedPaymentTime(PaymentPracticeData); PaymentPracticeLine."Pct Paid on Time" := PaymentPracticeMath.GetPercentOfOnTimePayments(PaymentPracticeData); - PaymentPracticeData.SetRange("Company Size Code"); PaymentPracticeLine.Insert(); + SchemeHandler.CalculateLineTotals(PaymentPracticeLine, PaymentPracticeData); + if (PaymentPracticeLine."Invoice Count" <> 0) or (PaymentPracticeLine."Invoice Value" <> 0) then + PaymentPracticeLine.Modify(); + PaymentPracticeData.SetRange("Company Size Code"); until CompanySize.Next() = 0; end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al new file mode 100644 index 0000000000..d3e1c84df5 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHandler +{ + Access = Internal; + + procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") + begin + end; + + procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean + begin + exit(true); + end; + + procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + begin + end; + + procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") + begin + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracStandardHandler.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracStandardHandler.Codeunit.al new file mode 100644 index 0000000000..67e0b376c8 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracStandardHandler.Codeunit.al @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +codeunit 680 "Paym. Prac. Standard Handler" implements PaymentPracticeSchemeHandler +{ + Access = Internal; + + procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") + begin + // Standard scheme: no additional validation + end; + + procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean + begin + exit(true); + end; + + procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + begin + // Standard scheme: no additional header totals + end; + + procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") + begin + // Standard scheme: no additional line totals + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al b/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al new file mode 100644 index 0000000000..ca2d0b15f3 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +interface PaymentPracticeSchemeHandler +{ + /// + /// Validates the Payment Practice Header before data generation. + /// + /// The header to validate. + procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") + + /// + /// Enriches or filters a Payment Practice Data row before insertion. + /// Returns true to include the row, false to skip it. + /// + /// The data row to enrich/filter. + /// True to include the row, false to skip. + procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean + + /// + /// Calculates scheme-specific header totals after standard totals are generated. + /// + /// The header to update with totals. + /// The data to aggregate from. + procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + + /// + /// Calculates scheme-specific line totals for each generated line. + /// + /// The line to update with totals. + /// The data to aggregate from. + procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al new file mode 100644 index 0000000000..9807f45823 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +using System.Environment; + +codeunit 695 "Payment Period Mgt." +{ + Access = Internal; + + procedure DetectReportingScheme(): Enum "Paym. Prac. Reporting Scheme" + var + EnvironmentInformation: Codeunit "Environment Information"; + ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; + IsHandled: Boolean; + begin + OnBeforeDetectReportingScheme(ReportingScheme, IsHandled); + if IsHandled then + exit(ReportingScheme); + + case EnvironmentInformation.GetApplicationFamily() of + 'GB': + exit("Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + else + exit("Paym. Prac. Reporting Scheme"::Standard); + end; + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeDetectReportingScheme(var ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; var IsHandled: Boolean) + begin + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al index fc3d2ab1bf..8f56133732 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al @@ -17,9 +17,13 @@ codeunit 688 "Payment Practice Builders" var Vendor: Record Vendor; VendorLedgerEntry: Record "Vendor Ledger Entry"; + SchemeHandler: Interface PaymentPracticeSchemeHandler; LastVendNo: Code[20]; begin + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; LastVendNo := ''; + Vendor.SetLoadFields("No.", "Exclude from Pmt. Practices", "Company Size Code"); + VendorLedgerEntry.SetLoadFields("Entry No.", "Vendor No.", "External Document No.", "Document No.", "Posting Date", "Invoice Received Date", "Document Date", "Due Date", Open, "Closed at Date", "Closed by Entry No.", "SCF Payment Date"); VendorLedgerEntry.SetCurrentKey("Vendor No."); VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); VendorLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); @@ -39,7 +43,8 @@ codeunit 688 "Payment Practice Builders" PaymentPracticeData."Header No." := PaymentPracticeHeader."No."; PaymentPracticeData.CopyFromInvoiceVendLedgEntry(VendorLedgerEntry); PaymentPracticeData."Company Size Code" := Vendor."Company Size Code"; - PaymentPracticeData.Insert(); + if SchemeHandler.UpdatePaymentPracData(PaymentPracticeData) then + PaymentPracticeData.Insert(); end; until VendorLedgerEntry.Next() = 0; end; @@ -48,9 +53,13 @@ codeunit 688 "Payment Practice Builders" var Customer: Record Customer; CustLedgerEntry: Record "Cust. Ledger Entry"; + SchemeHandler: Interface PaymentPracticeSchemeHandler; LastCustNo: Code[20]; begin + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; LastCustNo := ''; + Customer.SetLoadFields("No.", "Exclude from Pmt. Practices"); + CustLedgerEntry.SetLoadFields("Entry No.", "Customer No.", "External Document No.", "Document No.", "Posting Date", "Document Date", "Due Date", Open, "Closed at Date", "Closed by Entry No."); CustLedgerEntry.SetCurrentKey("Customer No."); CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); CustLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); @@ -69,7 +78,8 @@ codeunit 688 "Payment Practice Builders" PaymentPracticeData.Init(); PaymentPracticeData."Header No." := PaymentPracticeHeader."No."; PaymentPracticeData.CopyFromInvoiceCustLedgEntry(CustLedgerEntry); - PaymentPracticeData.Insert(); + if SchemeHandler.UpdatePaymentPracData(PaymentPracticeData) then + PaymentPracticeData.Insert(); end; until CustLedgerEntry.Next() = 0; end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al index 1298d87e81..d8539907db 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al @@ -12,9 +12,14 @@ codeunit 689 "Payment Practices" procedure Generate(var PaymentPracticeHeader: Record "Payment Practice Header") DataIsNotEmpty: Boolean var PaymentPracticeData: Record "Payment Practice Data"; + SchemeHandler: Interface PaymentPracticeSchemeHandler; begin PaymentPracticeHeader.TestField("Starting Date"); PaymentPracticeHeader.TestField("Ending Date"); + + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; + SchemeHandler.ValidateHeader(PaymentPracticeHeader); + PaymentPracticeData.Reset(); PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); PaymentPracticeData.DeleteAll(); @@ -23,6 +28,9 @@ codeunit 689 "Payment Practices" PaymentPracticeHeader."Generated On" := CurrentDateTime(); PaymentPracticeHeader."Generated By" := CopyStr(UserId(), 1, MaxStrLen(PaymentPracticeHeader."Generated By")); GenerateTotals(PaymentPracticeData, PaymentPracticeHeader); + PaymentPracticeData.Reset(); + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + SchemeHandler.CalculateHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); GenerateLines(PaymentPracticeHeader."Aggregation Type", PaymentPracticeData, PaymentPracticeHeader); PaymentPracticeHeader."Modified Manually" := false; PaymentPracticeHeader.Modify(false); diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al b/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al index 1971adb6ff..20fcdac349 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al @@ -25,6 +25,10 @@ permissionset 685 "Paym. Prac. Objects" codeunit "Paym. Prac. Period Aggregator" = X, codeunit "Paym. Prac. Size Aggregator" = X, codeunit "Paym. Prac. Vendor Generator" = X, + codeunit "Paym. Prac. Standard Handler" = X, + codeunit "Paym. Prac. Dispute Ret. Hdlr" = X, + codeunit "Paym. Prac. Small Bus. Handler" = X, + codeunit "Upgrade Payment Practices" = X, codeunit "Install Payment Practices" = X, codeunit "Payment Practice Builders" = X, codeunit "Payment Practice Math" = X, diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al new file mode 100644 index 0000000000..6d8849d42f --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +codeunit 683 "Upgrade Payment Practices" +{ + Access = Internal; + Subtype = Upgrade; + + trigger OnUpgradePerCompany() + begin + BackfillReportingScheme(); + end; + + local procedure BackfillReportingScheme() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPeriodMgt: Codeunit "Payment Period Mgt."; + ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; + begin + ReportingScheme := PaymentPeriodMgt.DetectReportingScheme(); + + PaymentPracticeHeader.SetRange("Reporting Scheme", 0); + if PaymentPracticeHeader.FindSet() then + repeat + PaymentPracticeHeader."Reporting Scheme" := ReportingScheme; + PaymentPracticeHeader.Modify(); + until PaymentPracticeHeader.Next() = 0; + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al new file mode 100644 index 0000000000..a9f37a91a6 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +using Microsoft.Purchases.Payables; + +pageextension 681 "Paym. Prac. Vend. Ledg. Entr." extends "Vendor Ledger Entries" +{ + layout + { + addafter("Invoice Received Date") + { + field("SCF Payment Date"; Rec."SCF Payment Date") + { + ApplicationArea = Basic, Suite; + Visible = false; + } + } + } +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPeriods.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPeriods.Page.al index 1fdd261dcb..9c7826b096 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPeriods.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPeriods.Page.al @@ -4,6 +4,8 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; +using System.Utilities; + page 685 "Payment Periods" { ApplicationArea = Basic, Suite; @@ -42,6 +44,36 @@ page 685 "Payment Periods" actions { + area(Processing) + { + action(RestoreDefaults) + { + Caption = 'Restore Default Periods'; + ToolTip = 'Deletes all payment periods and restores the default periods for the current environment.'; + Image = Restore; + + trigger OnAction() + var + PaymentPeriod: Record "Payment Period"; + ConfirmManagement: Codeunit "Confirm Management"; + begin + if not ConfirmManagement.GetResponseOrDefault(RestoreDefaultsQst, false) then + exit; + + PaymentPeriod.DeleteAll(); + PaymentPeriod.SetupDefaults(); + CurrPage.Update(false); + end; + } + } + area(Promoted) + { + actionref(RestoreDefaults_Promoted; RestoreDefaults) + { + } + } } -} + var + RestoreDefaultsQst: Label 'This will replace all payment periods with the default periods for your environment. Do you want to continue?'; +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al index c899463801..d9b7970a9b 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al @@ -25,6 +25,11 @@ page 687 "Payment Practice Card" { ToolTip = 'Specifies the number of the payment practice header.'; } + field("Reporting Scheme"; Rec."Reporting Scheme") + { + Visible = false; + Editable = false; + } field("Aggregation Type"; Rec."Aggregation Type") { ToolTip = 'Specifies the aggregation type of the payment practice.'; @@ -35,6 +40,7 @@ page 687 "Payment Practice Card" } field("Startind Date"; Rec."Starting Date") { + Caption = 'Starting Date'; ToolTip = 'Specifies the starting date of the payment practice.'; } field("Ending Date"; Rec."Ending Date") @@ -65,7 +71,7 @@ page 687 "Payment Practice Card" } group("Statistics") { - Caption = 'Statistics'; + Caption = 'Payment Statistics'; field("Average Agreed Payment Period"; Rec."Average Agreed Payment Period") { ToolTip = 'Specifies the average agreed payment period.'; @@ -93,6 +99,18 @@ page 687 "Payment Practice Card" ShowHeaderDataLines(); end; } + field("Total Number of Payments"; Rec."Total Number of Payments") + { + } + field("Total Amount of Payments"; Rec."Total Amount of Payments") + { + } + field("Total Amt. of Overdue Payments"; Rec."Total Amt. of Overdue Payments") + { + } + field("Pct Overdue Due to Dispute"; Rec."Pct Overdue Due to Dispute") + { + } } part(Lines; "Payment Practice Lines") { @@ -158,7 +176,12 @@ page 687 "Payment Practice Card" trigger OnOpenPage() begin + UpdateVisibility(); CurrPage.Update(); + end; + + trigger OnAfterGetCurrRecord() + begin UpdateVisibility(); end; @@ -183,6 +206,5 @@ page 687 "Payment Practice Card" local procedure UpdateVisibility() begin CurrPage.Lines.Page.UpdateVisibility(Rec."Aggregation Type", Rec."Header Type"); - CurrPage.Update(); end; } diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al index b3c4ef342b..3beb3021d4 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al @@ -25,7 +25,7 @@ page 686 "Payment Practice Data List" } field("Payment Entry No."; Rec."Pmt. Entry No.") { - ToolTip = 'Specifies the closing payment entry number that is associated with the source invoicy entry, if any was applied.'; + ToolTip = 'Specifies the closing payment entry number that is associated with the source invoice entry, if any was applied.'; } field("Invoice Posting Date"; Rec."Invoice Posting Date") { @@ -41,7 +41,7 @@ page 686 "Payment Practice Data List" } field("Pmt. Posting Date"; Rec."Pmt. Posting Date") { - ToolTip = 'Specifies the posting date of the payment entry that is associated with the source invoicy entry, if any was applied.'; + ToolTip = 'Specifies the posting date of the payment entry that is associated with the source invoice entry, if any was applied.'; } field("Invoice Is Open"; Rec."Invoice Is Open") { @@ -65,6 +65,18 @@ page 686 "Payment Practice Data List" Style = Unfavorable; StyleExpr = Rec."Actual Payment Days" > Rec."Agreed Payment Days"; } + field("Dispute Status"; Rec."Dispute Status") + { + Visible = false; + } + field("Overdue Due to Dispute"; Rec."Overdue Due to Dispute") + { + Visible = false; + } + field("SCF Payment Date"; Rec."SCF Payment Date") + { + Visible = false; + } } } } diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeLines.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeLines.Page.al index 90f6b89a53..59de6bb343 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeLines.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeLines.Page.al @@ -93,6 +93,14 @@ page 688 "Payment Practice Lines" Editable = false; ToolTip = 'Specifies whether the line has been modified manually.'; } + field("Invoice Count"; Rec."Invoice Count") + { + Editable = false; + } + field("Invoice Value"; Rec."Invoice Value") + { + Editable = false; + } } } } diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymPracVendLedgEntry.TableExt.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymPracVendLedgEntry.TableExt.al new file mode 100644 index 0000000000..fcba3ab6cd --- /dev/null +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymPracVendLedgEntry.TableExt.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.Analysis; + +using Microsoft.Purchases.Payables; + +tableextension 681 "Paym. Prac. Vend. Ledg. Entry" extends "Vendor Ledger Entry" +{ + fields + { + field(680; "SCF Payment Date"; Date) + { + Caption = 'SCF Payment Date'; + ToolTip = 'Specifies when the supplier received payment from a finance provider under a supply chain finance arrangement. When filled in, replaces the payment posting date for calculating actual payment days.'; + DataClassification = CustomerContent; + } + } +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al index 8dd0d1f413..1db7f8166f 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al @@ -87,8 +87,6 @@ table 685 "Payment Period" InsertDefaultPeriods_GB(); 'FR': InsertDefaultPeriods_FR(); - 'AU', 'NZ': - InsertDefaultPeriods_AUNZ(); else InsertDefaultPeriods(); end; @@ -142,15 +140,6 @@ table 685 "Payment Period" InsertPeriod('P121+', 121, 0); end; - local procedure InsertDefaultPeriods_AUNZ() - begin - InsertPeriod('P0_21', 0, 21); - InsertPeriod('P22_30', 22, 30); - InsertPeriod('P31_60', 31, 60); - InsertPeriod('P61_90', 61, 120); - InsertPeriod('P121+', 121, 0); - end; - [IntegrationEvent(false, false)] local procedure OnBeforeSetupDefaults(var IsHandled: Boolean) begin diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeData.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeData.Table.al index 6e5640a5d8..0adcdd0dde 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeData.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeData.Table.al @@ -5,6 +5,7 @@ namespace Microsoft.Finance.Analysis; using Microsoft.Purchases.Payables; +using Microsoft.Sales.Customer; using Microsoft.Sales.Receivables; table 686 "Payment Practice Data" @@ -78,6 +79,20 @@ table 686 "Payment Practice Data" AutoFormatType = 1; AutoFormatExpression = ''; } + field(20; "Dispute Status"; Code[10]) + { + TableRelation = "Dispute Status"; + ToolTip = 'Specifies whether the invoice is flagged as disputed. Copied from the vendor ledger entry during data generation.'; + } + field(21; "Overdue Due to Dispute"; Boolean) + { + Editable = false; + ToolTip = 'Specifies whether the invoice is overdue due to a dispute. This field is automatically calculated based on whether the invoice is overdue and has a dispute status.'; + } + field(22; "SCF Payment Date"; Date) + { + ToolTip = 'Specifies when the supplier received payment under a supply chain finance arrangement. When filled in, replaces the payment posting date for calculating actual payment days.'; + } } @@ -112,6 +127,8 @@ table 686 "Payment Practice Data" "Invoice Amount" := -VendorLedgerEntry.Amount; "Pmt. Posting Date" := VendorLedgerEntry."Closed at Date"; "Pmt. Entry No." := VendorLedgerEntry."Closed by Entry No."; + "SCF Payment Date" := VendorLedgerEntry."SCF Payment Date"; + "Dispute Status" := VendorLedgerEntry."Dispute Status"; if "Invoice Posting Date" <> 0D then "Agreed Payment Days" := "Due Date" - "Invoice Received Date"; if "Pmt. Posting Date" <> 0D then @@ -133,6 +150,7 @@ table 686 "Payment Practice Data" "Invoice Amount" := -CustLedgerEntry.Amount; "Pmt. Posting Date" := CustLedgerEntry."Closed at Date"; "Pmt. Entry No." := CustLedgerEntry."Closed by Entry No."; + "Dispute Status" := CustLedgerEntry."Dispute Status"; if "Invoice Posting Date" <> 0D then "Agreed Payment Days" := "Due Date" - "Invoice Received Date"; if "Pmt. Posting Date" <> 0D then diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al index b97262898e..18177c5dd7 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al @@ -105,6 +105,35 @@ table 687 "Payment Practice Header" { } + field(15; "Reporting Scheme"; Enum "Paym. Prac. Reporting Scheme") + { + ToolTip = 'Specifies which reporting scheme is used, such as Standard, Dispute & Retention, or Small Business. Controls which fields and calculations apply.'; + } + field(20; "Total Number of Payments"; Integer) + { + Editable = false; + ToolTip = 'Specifies the total number of payments made during the reporting period.'; + } + field(21; "Total Amount of Payments"; Decimal) + { + Editable = false; + AutoFormatType = 1; + AutoFormatExpression = ''; + ToolTip = 'Specifies the total value of payments made during the reporting period.'; + } + field(22; "Total Amt. of Overdue Payments"; Decimal) + { + Editable = false; + AutoFormatType = 1; + AutoFormatExpression = ''; + ToolTip = 'Specifies the total value of payments not made within the agreed payment terms.'; + } + field(23; "Pct Overdue Due to Dispute"; Decimal) + { + Editable = false; + AutoFormatType = 0; + ToolTip = 'Specifies the percentage of payments not made within agreed terms that are due to disputes.'; + } } keys @@ -118,6 +147,7 @@ table 687 "Payment Practice Header" trigger OnInsert() begin UpdateNo(); + DetectReportingScheme(); end; trigger OnDelete() @@ -168,4 +198,12 @@ table 687 "Payment Practice Header" if Rec."Starting Date" > Rec."Ending Date" then Error(DateValidationErr); end; + + local procedure DetectReportingScheme() + var + PaymentPeriodMgt: Codeunit "Payment Period Mgt."; + begin + "Reporting Scheme" := PaymentPeriodMgt.DetectReportingScheme(); + end; + } diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al index c7fbf37242..9fbff11903 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al @@ -84,7 +84,19 @@ table 688 "Payment Practice Line" } field(13; "Modified Manually"; Boolean) { - + } + field(15; "Invoice Count"; Integer) + { + ToolTip = 'Specifies the number of invoices in this period.'; + } + field(16; "Invoice Value"; Decimal) + { + AutoFormatType = 1; + AutoFormatExpression = ''; + ToolTip = 'Specifies the total value of invoices in this period.'; + } + field(20; "Payment Period Line No."; Integer) + { } } diff --git a/src/Apps/W1/PaymentPractices/Test Library/ExtensionLogo.png b/src/Apps/W1/PaymentPractices/Test Library/ExtensionLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..4d2c9a626cb9617350617c40cd73904129d4c108 GIT binary patch literal 5446 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 literal 0 HcmV?d00001 diff --git a/src/Apps/W1/PaymentPractices/Test Library/app.json b/src/Apps/W1/PaymentPractices/Test Library/app.json new file mode 100644 index 0000000000..3302636634 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/Test Library/app.json @@ -0,0 +1,46 @@ +{ + "id": "cc329ed7-8840-45f6-860b-3eb99c408998", + "name": "Payment Practices Test Library", + "publisher": "Microsoft", + "brief": "Test library for Payment Practices app.", + "description": "Test library for Payment Practices app.", + "version": "29.0.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?LinkId=724011", + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2204541", + "url": "https://go.microsoft.com/fwlink/?LinkId=724011", + "logo": "ExtensionLogo.png", + "dependencies": [ + { + "id": "64977288-facd-4b48-aaaa-bb0e288edfb3", + "name": "Payment Practices", + "publisher": "Microsoft", + "version": "29.0.0.0" + }, + { + "id": "5d86850b-0d76-4eca-bd7b-951ad998e997", + "name": "Tests-TestLibraries", + "publisher": "Microsoft", + "version": "29.0.0.0" + } + ], + "screenshots": [], + "platform": "29.0.0.0", + "idRanges": [ + { + "from": 134196, + "to": 134196 + } + ], + "resourceExposurePolicy": { + "allowDebugging": false, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "application": "29.0.0.0", + "target": "Cloud", + "features": [ + "TranslationFile" + ] +} diff --git a/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al b/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al new file mode 100644 index 0000000000..3276733227 --- /dev/null +++ b/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al @@ -0,0 +1,298 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Test.Finance.Analysis; + +using Microsoft.Finance.Analysis; +using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Purchases.Payables; +using Microsoft.Purchases.Vendor; +using Microsoft.Sales.Customer; +using Microsoft.Sales.Receivables; + +codeunit 134196 "Payment Practices Library" +{ + Subtype = Test; + + var + Assert: Codeunit Assert; + LibraryUtility: Codeunit "Library - Utility"; + LibraryPurchase: Codeunit "Library - Purchase"; + LibraryRandom: Codeunit "Library - Random"; + + procedure CreatePaymentPracticeHeader(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type"; StartingDate: Date; EndingDate: Date) + begin + PaymentPracticeHeader.Init(); + PaymentPracticeHeader."Header Type" := HeaderType; + PaymentPracticeHeader."Aggregation Type" := AggregationType; + PaymentPracticeHeader."Starting Date" := StartingDate; + PaymentPracticeHeader."Ending Date" := EndingDate; + PaymentPracticeHeader.Insert(); + end; + + procedure CreatePaymentPracticeHeader(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type"; ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; StartingDate: Date; EndingDate: Date) + begin + PaymentPracticeHeader.Init(); + PaymentPracticeHeader."Header Type" := HeaderType; + PaymentPracticeHeader."Aggregation Type" := AggregationType; + PaymentPracticeHeader."Reporting Scheme" := ReportingScheme; + PaymentPracticeHeader."Starting Date" := StartingDate; + PaymentPracticeHeader."Ending Date" := EndingDate; + PaymentPracticeHeader.Insert(); + end; + + procedure CreatePaymentPracticeHeaderSimple(var PaymentPracticeHeader: Record "Payment Practice Header") + begin + CreatePaymentPracticeHeader(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::"Company Size", WorkDate() - 180, WorkDate() + 180); + end; + + procedure CreatePaymentPracticeHeaderSimple(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type") + begin + CreatePaymentPracticeHeader(PaymentPracticeHeader, HeaderType, AggregationType, WorkDate() - 180, WorkDate() + 180); + end; + + procedure CreateCompanySizeCode(): Code[20] + var + CompanySize: Record "Company Size"; + begin + CompanySize.Init(); + CompanySize.Code := LibraryUtility.GenerateGUID(); + CompanySize.Description := CompanySize.Code; + CompanySize.Insert(); + exit(CompanySize.Code); + end; + + procedure CreateVendorNoWithSizeAndExcl(CompanySizeCode: Code[20]; ExclFromPaymentPractice: Boolean) VendorNo: Code[20] + var + Vendor: Record Vendor; + begin + LibraryPurchase.CreateVendor(Vendor); + SetCompanySize(Vendor, CompanySizeCode); + SetExcludeFromPaymentPractices(Vendor, ExclFromPaymentPractice); + exit(Vendor."No."); + end; + + procedure InitializeCompanySizes(var CompanySizeCodes: array[3] of Code[20]) + var + i: Integer; + begin + for i := 1 to ArrayLen(CompanySizeCodes) do + CompanySizeCodes[i] := CreateCompanySizeCode(); + end; + + procedure InitializePaymentPeriods(var PaymentPeriods: array[3] of Record "Payment Period") + var + PaymentPeriod: Record "Payment Period"; + i: Integer; + begin + PaymentPeriod.SetCurrentKey("Days From"); + PaymentPeriod.FindSet(); + for i := 1 to ArrayLen(PaymentPeriods) do begin + PaymentPeriods[i] := PaymentPeriod; + PaymentPeriod.Next(); + end; + end; + + procedure InitAndGetLastPaymentPeriod(var PaymentPeriod: Record "Payment Period") + begin + PaymentPeriod.SetRange("Days To", 0); + PaymentPeriod.FindLast(); + end; + + procedure SetCompanySize(var Vendor: Record Vendor; CompanySizeCode: Code[20]) + begin + Vendor."Company Size Code" := CompanySizeCode; + Vendor.Modify(); + end; + + procedure SetExcludeFromPaymentPractices(var Vendor: Record Vendor; NewExcludeFromPaymentPractice: Boolean) + begin + Vendor."Exclude from Pmt. Practices" := NewExcludeFromPaymentPractice; + Vendor.Modify(); + end; + + procedure SetExcludeFromPaymentPractices(var Customer: Record Customer; NewExcludeFromPaymentPractice: Boolean) + begin + Customer."Exclude from Pmt. Practices" := NewExcludeFromPaymentPractice; + Customer.Modify(); + end; + + procedure SetExcludeFromPaymentPracticesOnAllVendorsAndCustomers() + var + Vendor: Record Vendor; + Customer: Record Customer; + begin + Vendor.ModifyAll("Exclude from Pmt. Practices", true); + Customer.ModifyAll("Exclude from Pmt. Practices", true); + end; + + procedure VerifyLinesCount(PaymentPracticeHeader: Record "Payment Practice Header"; NumberOfLines: Integer) + var + PaymentPracticeLine: Record "Payment Practice Line"; + begin + PaymentPracticeLine.SetRange("Header No.", PaymentPracticeHeader."No."); + Assert.RecordCount(PaymentPracticeLine, NumberOfLines); + end; + + procedure VerifyPeriodLine(PaymentPracticeHeaderNo: Integer; SourceType: Enum "Paym. Prac. Header Type"; PaymentPeriodDescription: Text[250]; PctInPeriodExpected: Decimal; PctInPeriodAmountExpected: Decimal) + var + PaymentPracticeLine: Record "Payment Practice Line"; + begin + PaymentPracticeLine.SetRange("Header No.", PaymentPracticeHeaderNo); +#pragma warning disable AA0210 + PaymentPracticeLine.SetRange("Payment Period Description", PaymentPeriodDescription); + PaymentPracticeLine.SetRange("Source Type", SourceType); +#pragma warning restore AA0210 + PaymentPracticeLine.FindFirst(); + Assert.AreNearlyEqual(PctInPeriodExpected, PaymentPracticeLine."Pct Paid in Period", 0.1, '"Pct Paid in Period" is not as expected'); + Assert.AreNearlyEqual(PctInPeriodAmountExpected, PaymentPracticeLine."Pct Paid in Period (Amount)", 0.1, '"Pct Paid in Period (Amount)" is not as expected'); + end; + + + procedure VerifyBufferCount(PaymentPracticeHeader: Record "Payment Practice Header"; NumberOfLines: Integer; SourceType: Enum "Paym. Prac. Header Type") + var + PaymentPracticeData: Record "Payment Practice Data"; + begin + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + PaymentPracticeData.SetRange("Source Type", SourceType); + Assert.RecordCount(PaymentPracticeData, NumberOfLines); + end; + + procedure CreateDefaultPaymentPeriodTemplates() + var + PaymentPeriod: Record "Payment Period"; + begin + PaymentPeriod.DeleteAll(); + PaymentPeriod.SetupDefaults(); + end; + + + + + procedure CleanupPaymentPracticeHeaders() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeData: Record "Payment Practice Data"; + begin + PaymentPracticeData.DeleteAll(); + PaymentPracticeHeader.DeleteAll(); + end; + + + procedure CreatePaymentPracticeHeaderWithScheme(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type"; ReportingScheme: Enum "Paym. Prac. Reporting Scheme") + begin + PaymentPracticeHeader.Init(); + PaymentPracticeHeader."Header Type" := HeaderType; + PaymentPracticeHeader."Aggregation Type" := AggregationType; + PaymentPracticeHeader."Reporting Scheme" := ReportingScheme; + PaymentPracticeHeader."Starting Date" := WorkDate() - 180; + PaymentPracticeHeader."Ending Date" := WorkDate() + 180; + PaymentPracticeHeader.Insert(); + end; + + + procedure DisputeRetCalcHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + var + SchemeHandler: Interface PaymentPracticeSchemeHandler; + begin + SchemeHandler := "Paym. Prac. Reporting Scheme"::"Dispute & Retention"; + SchemeHandler.CalculateHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); + end; + + + procedure MockVendLedgerEntry(VendorNo: Code[20]; var VendorLedgerEntry: Record "Vendor Ledger Entry"; DocType: Enum "Gen. Journal Document Type"; PostingDate: Date; DueDate: Date; PmtPostingDate: Date; IsOpen: Boolean) + begin + VendorLedgerEntry.Init(); + VendorLedgerEntry."Entry No." := LibraryUtility.GetNewRecNo(VendorLedgerEntry, VendorLedgerEntry.FieldNo("Entry No.")); + VendorLedgerEntry."Document Type" := DocType; + VendorLedgerEntry."Posting Date" := PostingDate; + VendorLedgerEntry."Document Date" := PostingDate; + VendorLedgerEntry."Vendor No." := VendorNo; + VendorLedgerEntry."Due Date" := DueDate; + VendorLedgerEntry.Open := IsOpen; + VendorLedgerEntry."Closed at Date" := PmtPostingDate; + VendorLedgerEntry.Amount := LibraryRandom.RandDec(1000, 2); + VendorLedgerEntry.Insert(); + end; + + procedure MockVendorInvoice(VendorNo: Code[20]; PostingDate: Date; DueDate: Date) InvoiceAmount: Decimal; + var + VendorLedgerEntry: Record "Vendor Ledger Entry"; + begin + MockVendLedgerEntry(VendorNo, VendorLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, 0D, true); + VendorLedgerEntry.CalcFields("Amount (LCY)"); + InvoiceAmount := VendorLedgerEntry."Amount (LCY)"; + end; + + procedure MockVendorInvoiceAndPayment(VendorNo: Code[20]; PostingDate: Date; DueDate: Date; PaymentPostingDate: Date) InvoiceAmount: Decimal; + var + VendorLedgerEntry: Record "Vendor Ledger Entry"; + begin + MockVendLedgerEntry(VendorNo, VendorLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, PaymentPostingDate, false); + VendorLedgerEntry.CalcFields("Amount (LCY)"); + InvoiceAmount := VendorLedgerEntry."Amount (LCY)"; + end; + + procedure MockVendorInvoiceAndPaymentInPeriod(VendorNo: Code[20]; StartingDate: Date; PaidInDays_min: Integer; PaidInDays_max: Integer) InvoiceAmount: Decimal; + var + PostingDate: Date; + DueDate: Date; + PaymentPostingDate: Date; + begin + PostingDate := StartingDate; + DueDate := StartingDate; + if PaidInDays_max <> 0 then + PaymentPostingDate := PostingDate + LibraryRandom.RandIntInRange(PaidInDays_min, PaidInDays_max) + else + PaymentPostingDate := PostingDate + PaidInDays_min + LibraryRandom.RandInt(10); + InvoiceAmount := MockVendorInvoiceAndPayment(VendorNo, PostingDate, DueDate, PaymentPostingDate); + end; + + procedure MockCustLedgerEntry(CustomerNo: Code[20]; var CustLedgerEntry: Record "Cust. Ledger Entry"; DocType: Enum "Gen. Journal Document Type"; PostingDate: Date; DueDate: Date; PmtPostingDate: Date; IsOpen: Boolean) + begin + CustLedgerEntry.Init(); + CustLedgerEntry."Entry No." := LibraryUtility.GetNewRecNo(CustLedgerEntry, CustLedgerEntry.FieldNo("Entry No.")); + CustLedgerEntry."Document Type" := DocType; + CustLedgerEntry."Posting Date" := PostingDate; + CustLedgerEntry."Document Date" := PostingDate; + CustLedgerEntry."Customer No." := CustomerNo; + CustLedgerEntry."Due Date" := DueDate; + CustLedgerEntry.Open := IsOpen; + CustLedgerEntry."Closed at Date" := PmtPostingDate; + CustLedgerEntry.Amount := LibraryRandom.RandDec(1000, 2); + CustLedgerEntry.Insert(); + end; + + procedure MockCustomerInvoice(CustomerNo: Code[20]; PostingDate: Date; DueDate: Date) InvoiceAmount: Decimal; + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + MockCustLedgerEntry(CustomerNo, CustLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, 0D, true); + CustLedgerEntry.CalcFields("Amount (LCY)"); + InvoiceAmount := CustLedgerEntry."Amount (LCY)"; + end; + + procedure MockCustomerInvoiceAndPayment(CustomerNo: Code[20]; PostingDate: Date; DueDate: Date; PaymentPostingDate: Date) InvoiceAmount: Decimal; + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + MockCustLedgerEntry(CustomerNo, CustLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, PaymentPostingDate, false); + CustLedgerEntry.CalcFields("Amount (LCY)"); + InvoiceAmount := CustLedgerEntry."Amount (LCY)"; + end; + + procedure MockCustomerInvoiceAndPaymentInPeriod(CustomerNo: Code[20]; StartingDate: Date; PaidInDays_min: Integer; PaidInDays_max: Integer) InvoiceAmount: Decimal; + var + PostingDate: Date; + DueDate: Date; + PaymentPostingDate: Date; + begin + PostingDate := StartingDate; + DueDate := StartingDate + LibraryRandom.RandIntInRange(1, 5); + PaymentPostingDate := PostingDate + LibraryRandom.RandIntInRange(PaidInDays_min, PaidInDays_max); + InvoiceAmount := MockCustomerInvoiceAndPayment(CustomerNo, PostingDate, DueDate, PaymentPostingDate); + end; + + +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/Test/app.json b/src/Apps/W1/PaymentPractices/Test/app.json index 0badc2cc17..2267bedd31 100644 --- a/src/Apps/W1/PaymentPractices/Test/app.json +++ b/src/Apps/W1/PaymentPractices/Test/app.json @@ -18,6 +18,12 @@ "publisher": "Microsoft", "version": "29.0.0.0" }, + { + "id": "cc329ed7-8840-45f6-860b-3eb99c408998", + "name": "Payment Practices Test Library", + "publisher": "Microsoft", + "version": "29.0.0.0" + }, { "id": "5d86850b-0d76-4eca-bd7b-951ad998e997", "name": "Tests-TestLibraries", @@ -41,7 +47,7 @@ "platform": "29.0.0.0", "idRanges": [ { - "from": 134196, + "from": 134197, "to": 134197 } ], diff --git a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesLibrary.Codeunit.al b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesLibrary.Codeunit.al deleted file mode 100644 index 384fef369a..0000000000 --- a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesLibrary.Codeunit.al +++ /dev/null @@ -1,165 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.Test.Finance.Analysis; - -using Microsoft.Finance.Analysis; -using Microsoft.Purchases.Vendor; -using Microsoft.Sales.Customer; - -codeunit 134196 "Payment Practices Library" -{ - Subtype = Test; - - var - Assert: Codeunit Assert; - LibraryUtility: Codeunit "Library - Utility"; - LibraryPurchase: Codeunit "Library - Purchase"; - - procedure CreatePaymentPracticeHeader(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type"; StartingDate: Date; EndingDate: Date) - begin - PaymentPracticeHeader.Init(); - PaymentPracticeHeader."Header Type" := HeaderType; - PaymentPracticeHeader."Aggregation Type" := AggregationType; - PaymentPracticeHeader."Starting Date" := StartingDate; - PaymentPracticeHeader."Ending Date" := EndingDate; - PaymentPracticeHeader.Insert(); - end; - - procedure CreatePaymentPracticeHeaderSimple(var PaymentPracticeHeader: Record "Payment Practice Header") - begin - CreatePaymentPracticeHeader(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::"Company Size", WorkDate() - 180, WorkDate() + 180); - end; - - procedure CreatePaymentPracticeHeaderSimple(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type") - begin - CreatePaymentPracticeHeader(PaymentPracticeHeader, HeaderType, AggregationType, WorkDate() - 180, WorkDate() + 180); - end; - - procedure CreateCompanySizeCode(): Code[20] - var - CompanySize: Record "Company Size"; - begin - CompanySize.Init(); - CompanySize.Code := LibraryUtility.GenerateGUID(); - CompanySize.Description := CompanySize.Code; - CompanySize.Insert(); - exit(CompanySize.Code); - end; - - procedure CreatePaymentPeriod(DaysFrom: Integer; DaysTo: Integer) - var - PaymentPeriod: Record "Payment Period"; - begin - PaymentPeriod.Init(); - PaymentPeriod."Days From" := DaysFrom; - PaymentPeriod."Days To" := DaysTo; - PaymentPeriod.Insert(); - end; - - procedure CreateVendorNoWithSizeAndExcl(CompanySizeCode: Code[20]; ExclFromPaymentPractice: Boolean) VendorNo: Code[20] - var - Vendor: Record Vendor; - begin - LibraryPurchase.CreateVendor(Vendor); - SetCompanySize(Vendor, CompanySizeCode); - SetExcludeFromPaymentPractices(Vendor, ExclFromPaymentPractice); - exit(Vendor."No."); - end; - - procedure InitializeCompanySizes(var CompanySizeCodes: array[3] of Code[20]) - var - i: Integer; - begin - for i := 1 to ArrayLen(CompanySizeCodes) do - CompanySizeCodes[i] := CreateCompanySizeCode(); - end; - - procedure InitializePaymentPeriods(var PaymentPeriods: array[3] of Record "Payment Period") - var - PaymentPeriod: Record "Payment Period"; - i: Integer; - begin - PaymentPeriod.SetupDefaults(); - PaymentPeriod.SetCurrentKey("Days From"); - PaymentPeriod.FindSet(); - for i := 1 to ArrayLen(PaymentPeriods) do begin - PaymentPeriods[i] := PaymentPeriod; - PaymentPeriod.Next(); - end; - end; - - procedure InitAndGetLastPaymentPeriod(var PaymentPeriod: Record "Payment Period") - begin - PaymentPeriod.SetupDefaults(); - PaymentPeriod.SetRange("Days To", 0); - PaymentPeriod.FindLast(); - end; - - procedure SetCompanySize(var Vendor: Record Vendor; CompanySizeCode: Code[20]) - begin - Vendor."Company Size Code" := CompanySizeCode; - Vendor.Modify(); - end; - - procedure SetExcludeFromPaymentPractices(var Vendor: Record Vendor; NewExcludeFromPaymentPractice: Boolean) - begin - Vendor."Exclude from Pmt. Practices" := NewExcludeFromPaymentPractice; - Vendor.Modify(); - end; - - procedure SetExcludeFromPaymentPractices(var Customer: Record Customer; NewExcludeFromPaymentPractice: Boolean) - begin - Customer."Exclude from Pmt. Practices" := NewExcludeFromPaymentPractice; - Customer.Modify(); - end; - - procedure SetExcludeFromPaymentPracticesOnAllVendorsAndCustomers() - var - Vendor: Record Vendor; - Customer: Record Customer; - begin - if Vendor.FindSet(true) then - repeat - SetExcludeFromPaymentPractices(Vendor, true); - until Vendor.Next() = 0; - if Customer.FindSet(true) then - repeat - SetExcludeFromPaymentPractices(Customer, true); - until Customer.Next() = 0; - end; - - procedure VerifyLinesCount(PaymentPracticeHeader: Record "Payment Practice Header"; NumberOfLines: Integer) - var - PaymentPracticeLine: Record "Payment Practice Line"; - begin - PaymentPracticeLine.SetRange("Header No.", PaymentPracticeHeader."No."); - Assert.RecordCount(PaymentPracticeLine, NumberOfLines); - end; - - procedure VerifyPeriodLine(PaymentPracticeHeaderNo: Integer; SourceType: Enum "Paym. Prac. Header Type"; PaymentPeriodCode: Code[20]; PctInPeriodExpected: Decimal; PctInPeriodAmountExpected: Decimal) - var - PaymentPracticeLine: Record "Payment Practice Line"; - begin - PaymentPracticeLine.SetRange("Header No.", PaymentPracticeHeaderNo); -#pragma warning disable AA0210 - PaymentPracticeLine.SetRange("Payment Period Code", PaymentPeriodCode); - PaymentPracticeLine.SetRange("Source Type", SourceType); -#pragma warning restore AA0210 - PaymentPracticeLine.FindFirst(); - Assert.AreNearlyEqual(PctInPeriodExpected, PaymentPracticeLine."Pct Paid in Period", 0.1, '"Pct Paid in Period" is not as expected'); - Assert.AreNearlyEqual(PctInPeriodAmountExpected, PaymentPracticeLine."Pct Paid in Period (Amount)", 0.1, '"Pct Paid in Period (Amount)" is not as expected'); - end; - - - procedure VerifyBufferCount(PaymentPracticeHeader: Record "Payment Practice Header"; NumberOfLines: Integer; SourceType: Enum "Paym. Prac. Header Type") - var - PaymentPracticeData: Record "Payment Practice Data"; - begin - PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); - PaymentPracticeData.SetRange("Source Type", SourceType); - Assert.RecordCount(PaymentPracticeData, NumberOfLines); - end; - -} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al index bd9c6ef507..6b9a6ecf46 100644 --- a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al @@ -3,13 +3,12 @@ // Licensed under the MIT License. See License.txt in the project root for license information. // ------------------------------------------------------------------------------------------------ +#pragma warning disable AA0210 // table does not contain key with field A namespace Microsoft.Test.Finance.Analysis; using Microsoft.Finance.Analysis; -using Microsoft.Finance.GeneralLedger.Journal; -using Microsoft.Purchases.Payables; using Microsoft.Sales.Customer; -using Microsoft.Sales.Receivables; +using System.Environment; codeunit 134197 "Payment Practices UT" { @@ -28,7 +27,6 @@ codeunit 134197 "Payment Practices UT" PaymentPracticesLibrary: Codeunit "Payment Practices Library"; PaymentPractices: Codeunit "Payment Practices"; LibraryPurchase: Codeunit "Library - Purchase"; - LibraryUtility: Codeunit "Library - Utility"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; LibraryRandom: Codeunit "Library - Random"; LibrarySales: Codeunit "Library - Sales"; @@ -69,11 +67,11 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Vendor with company size and an entry in the period VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); // [GIVEN]Vendor with company size and an entry in the period, but with Excl. from Payment Practice = true VendorExcludedNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[2], true); - MockVendorInvoice(VendorExcludedNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorExcludedNo, WorkDate(), WorkDate()); // [WHEN] Generate payment practices for vendors by size PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader); @@ -95,12 +93,12 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Customer with an entry in the period LibrarySales.CreateCustomer(Customer); - MockCustomerInvoice(Customer."No.", WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockCustomerInvoice(Customer."No.", WorkDate(), WorkDate()); // [GIVEN] Customer with an entry in the period, but with Excl. from Payment Practice = true LibrarySales.CreateCustomer(CustomerExcluded); PaymentPracticesLibrary.SetExcludeFromPaymentPractices(CustomerExcluded, true); - MockCustomerInvoice(CustomerExcluded."No.", WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockCustomerInvoice(CustomerExcluded."No.", WorkDate(), WorkDate()); // [WHEN] Generate payment practices for cust+vendors PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::"Vendor+Customer", "Paym. Prac. Aggregation Type"::Period); @@ -111,7 +109,7 @@ codeunit 134197 "Payment Practices UT" end; [Test] - [HandlerFunctions('ConfirmHandler_Yes')] + [HandlerFunctions('ConfirmHandlerYes')] procedure ConfirmToCleanUpOnAggrValidation_Yes() var PaymentPracticeHeader: Record "Payment Practice Header"; @@ -122,7 +120,7 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Vendor with company size and an entry in the period VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); // [GIVEN] Lines were generated for Header PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader); @@ -137,7 +135,7 @@ codeunit 134197 "Payment Practices UT" end; [Test] - [HandlerFunctions('ConfirmHandler_No')] + [HandlerFunctions('ConfirmHandlerNo')] procedure ConfirmToCleanUpOnAggrValidation_No() var PaymentPracticeHeader: Record "Payment Practice Header"; @@ -148,7 +146,7 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Vendor with company size and an entry in the period VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); // [GIVEN] Lines were generated for Header PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader); @@ -164,7 +162,7 @@ codeunit 134197 "Payment Practices UT" end; [Test] - [HandlerFunctions('ConfirmHandler_Yes')] + [HandlerFunctions('ConfirmHandlerYes')] procedure ConfirmToCleanUpOnTypeValidation() var PaymentPracticeHeader: Record "Payment Practice Header"; @@ -175,7 +173,7 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Vendor with company size and an entry in the period VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); // [GIVEN] Lines were generated for Header PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); @@ -218,7 +216,7 @@ codeunit 134197 "Payment Practices UT" for j := 1 to LibraryRandom.RandInt(10) do begin PeriodCounts[i] += 1; TotalCount += 1; - Amount := MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); + Amount := PaymentPracticesLibrary.MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); PeriodAmounts[i] += Amount; TotalAmount += Amount; end; @@ -228,9 +226,9 @@ codeunit 134197 "Payment Practices UT" // [THEN] Check that report dataset contains correct percentages for each period PrepareExpectedPeriodPcts(ExpectedPeriodPcts, ExpectedPeriodAmountPcts, PeriodCounts, TotalCount, PeriodAmounts, TotalAmount); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[1].Code, ExpectedPeriodPcts[1], ExpectedPeriodAmountPcts[1]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[2].Code, ExpectedPeriodPcts[2], ExpectedPeriodAmountPcts[2]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[3].Code, ExpectedPeriodPcts[3], ExpectedPeriodAmountPcts[3]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[1].Description, ExpectedPeriodPcts[1], ExpectedPeriodAmountPcts[1]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[2].Description, ExpectedPeriodPcts[2], ExpectedPeriodAmountPcts[2]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[3].Description, ExpectedPeriodPcts[3], ExpectedPeriodAmountPcts[3]); end; [Test] @@ -262,7 +260,7 @@ codeunit 134197 "Payment Practices UT" for j := 1 to LibraryRandom.RandInt(10) do begin PeriodCounts[i] += 1; TotalCount += 1; - Amount := MockCustomerInvoiceAndPaymentInPeriod(CustomerNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); + Amount := PaymentPracticesLibrary.MockCustomerInvoiceAndPaymentInPeriod(CustomerNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); PeriodAmounts[i] += Amount; TotalAmount += Amount; end; @@ -272,9 +270,9 @@ codeunit 134197 "Payment Practices UT" // [THEN] Check that report dataset contains correct percentages for each period PrepareExpectedPeriodPcts(ExpectedPeriodPcts, ExpectedPeriodAmountPcts, PeriodCounts, TotalCount, PeriodAmounts, TotalAmount); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[1].Code, ExpectedPeriodPcts[1], ExpectedPeriodAmountPcts[1]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[2].Code, ExpectedPeriodPcts[2], ExpectedPeriodAmountPcts[2]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[3].Code, ExpectedPeriodPcts[3], ExpectedPeriodAmountPcts[3]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[1].Description, ExpectedPeriodPcts[1], ExpectedPeriodAmountPcts[1]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[2].Description, ExpectedPeriodPcts[2], ExpectedPeriodAmountPcts[2]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[3].Description, ExpectedPeriodPcts[3], ExpectedPeriodAmountPcts[3]); end; [Test] @@ -316,7 +314,7 @@ codeunit 134197 "Payment Practices UT" for j := 1 to LibraryRandom.RandInt(10) do begin Vendor_PeriodCounts[i] += 1; Vendor_TotalCount += 1; - Amount := MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); + Amount := PaymentPracticesLibrary.MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); Vendor_PeriodAmounts[i] += Amount; Vendor_TotalAmount += Amount; end; @@ -326,7 +324,7 @@ codeunit 134197 "Payment Practices UT" for j := 1 to LibraryRandom.RandInt(10) do begin Customer_PeriodCounts[i] += 1; Customer_TotalCount += 1; - Amount := MockCustomerInvoiceAndPaymentInPeriod(CustomerNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); + Amount := PaymentPracticesLibrary.MockCustomerInvoiceAndPaymentInPeriod(CustomerNo, WorkDate(), PaymentPeriods[i]."Days From", PaymentPeriods[i]."Days To"); Customer_PeriodAmounts[i] += Amount; Customer_TotalAmount += Amount; end; @@ -336,15 +334,15 @@ codeunit 134197 "Payment Practices UT" // [THEN] Check that report dataset contains correct percentages for each period for vendors PrepareExpectedPeriodPcts(Vendor_ExpectedPeriodPcts, Vendor_ExpectedPeriodAmountPcts, Vendor_PeriodCounts, Vendor_TotalCount, Vendor_PeriodAmounts, Vendor_TotalAmount); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[1].Code, Vendor_ExpectedPeriodPcts[1], Vendor_ExpectedPeriodAmountPcts[1]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[2].Code, Vendor_ExpectedPeriodPcts[2], Vendor_ExpectedPeriodAmountPcts[2]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[3].Code, Vendor_ExpectedPeriodPcts[3], Vendor_ExpectedPeriodAmountPcts[3]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[1].Description, Vendor_ExpectedPeriodPcts[1], Vendor_ExpectedPeriodAmountPcts[1]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[2].Description, Vendor_ExpectedPeriodPcts[2], Vendor_ExpectedPeriodAmountPcts[2]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriods[3].Description, Vendor_ExpectedPeriodPcts[3], Vendor_ExpectedPeriodAmountPcts[3]); // [THEN] Check that report dataset contains correct percentages for each period for customers PrepareExpectedPeriodPcts(Customer_ExpectedPeriodPcts, Customer_ExpectedPeriodAmountPcts, Customer_PeriodCounts, Customer_TotalCount, Customer_PeriodAmounts, Customer_TotalAmount); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[1].Code, Customer_ExpectedPeriodPcts[1], Customer_ExpectedPeriodAmountPcts[1]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[2].Code, Customer_ExpectedPeriodPcts[2], Customer_ExpectedPeriodAmountPcts[2]); - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[3].Code, Customer_ExpectedPeriodPcts[3], Customer_ExpectedPeriodAmountPcts[3]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[1].Description, Customer_ExpectedPeriodPcts[1], Customer_ExpectedPeriodAmountPcts[1]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[2].Description, Customer_ExpectedPeriodPcts[2], Customer_ExpectedPeriodAmountPcts[2]); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Customer, PaymentPeriods[3].Description, Customer_ExpectedPeriodPcts[3], Customer_ExpectedPeriodAmountPcts[3]); end; [Test] @@ -370,21 +368,21 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Post several entries paid on time, this will affect total entries considered and total entries paid on time. PaidOnTimeCount := LibraryRandom.RandInt(20); for i := 1 to PaidOnTimeCount do - MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate() + LibraryRandom.RandInt(10), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate() + LibraryRandom.RandInt(10), WorkDate()); // [GIVEN] Post several entries paid late, this will affect total entries considered. PaidLateCount := LibraryRandom.RandInt(20); for i := 1 to PaidLateCount do - MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + LibraryRandom.RandInt(10)); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + LibraryRandom.RandInt(10)); // [GIVEN] Post several entries unpaid overdue, this will affect total entries considered. UnpaidOverdueCount := LibraryRandom.RandInt(20); for i := 1 to UnpaidOverdueCount do - MockVendorInvoice(VendorNo, WorkDate() - 50, WorkDate() - LibraryRandom.RandInt(40)); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate() - 50, WorkDate() - LibraryRandom.RandInt(40)); // [GIVEN] Post several entries unpaid not overdue, these will not affect count for i := 1 to LibraryRandom.RandInt(20) do - MockVendorInvoice(VendorNo, WorkDate(), WorkDate() + LibraryRandom.RandInt(10)); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate() + LibraryRandom.RandInt(10)); // [WHEN] Lines were generated for Header PaymentPractices.Generate(PaymentPracticeHeader); @@ -419,7 +417,7 @@ codeunit 134197 "Payment Practices UT" TotalEntries := LibraryRandom.RandInt(100); for i := 1 to TotalEntries do begin ActualPaymentTime := LibraryRandom.RandInt(30); - MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + ActualPaymentTime); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + ActualPaymentTime); ActualPaymentTimeSum += ActualPaymentTime; end; @@ -457,7 +455,7 @@ codeunit 134197 "Payment Practices UT" TotalPaidEntries := LibraryRandom.RandInt(100); for i := 1 to TotalPaidEntries do begin AgreedPaymentTime := LibraryRandom.RandInt(30); - MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate() + AgreedPaymentTime, WorkDate() + AgreedPaymentTime); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate() + AgreedPaymentTime, WorkDate() + AgreedPaymentTime); AgreedPaymentTimeSum += AgreedPaymentTime; end; @@ -465,7 +463,7 @@ codeunit 134197 "Payment Practices UT" TotalUnpaidEntries += LibraryRandom.RandInt(100); for i := 1 to TotalUnpaidEntries do begin AgreedPaymentTime := LibraryRandom.RandInt(30); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate() + AgreedPaymentTime); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate() + AgreedPaymentTime); AgreedPaymentTimeSum += AgreedPaymentTime; end; // [WHEN] Lines were generated for Header @@ -498,13 +496,13 @@ codeunit 134197 "Payment Practices UT" PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); // [GIVEN] Post an entry for the vendor in the period - MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriod."Days From", PaymentPeriod."Days To"); + PaymentPracticesLibrary.MockVendorInvoiceAndPaymentInPeriod(VendorNo, WorkDate(), PaymentPeriod."Days From", PaymentPeriod."Days To"); // [WHEN] Lines were generated for Header PaymentPractices.Generate(PaymentPracticeHeader); // [THEN] Check that report dataset contains the line for the period correcly - PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriod.Code, 100, 0); + PaymentPracticesLibrary.VerifyPeriodLine(PaymentPracticeHeader."No.", "Paym. Prac. Header Type"::Vendor, PaymentPeriod.Description, 100, 0); end; [Test] @@ -584,7 +582,7 @@ codeunit 134197 "Payment Practices UT" // [GIVEN] Vendor "V" with company size and an entry in the period VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); - MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); // [GIVEN] A payment practice header "PPH" PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader); @@ -605,116 +603,303 @@ codeunit 134197 "Payment Practices UT" PaymentPracticeCard.Close(); end; - local procedure Initialize() + [Test] + procedure StandardSchemeGenerateProducesSameResults() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + VendorNo: Code[20]; begin - LibraryTestInitialize.OnTestInitialize(Codeunit::"Payment Practices UT"); + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] Generating payment practices with Standard reporting scheme produces correct data for vendor entries + Initialize(); - // This is so demodata and previous tests doesn't influence the tests - PaymentPracticesLibrary.SetExcludeFromPaymentPracticesOnAllVendorsAndCustomers(); + // [GIVEN] Vendor with entry + VendorNo := LibraryPurchase.CreateVendorNo(); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate() + 30, WorkDate() + 10); + + // [WHEN] Generate with Standard scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderWithScheme( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::Period, + "Paym. Prac. Reporting Scheme"::Standard); + PaymentPractices.Generate(PaymentPracticeHeader); - if Initialized then - exit; + // [THEN] Data is generated correctly + PaymentPracticesLibrary.VerifyBufferCount(PaymentPracticeHeader, 1, "Paym. Prac. Header Type"::Vendor); + end; - LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Payment Practices UT"); + [Test] + procedure ReportingSchemeAutoDetectionOnInsert() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + EnvironmentInformation: Codeunit "Environment Information"; + ExpectedScheme: Enum "Paym. Prac. Reporting Scheme"; + begin + // [FEATURE] [AI test 0.3] + // [SCENARIO 629871] Inserting a Payment Practice Header auto-detects the Reporting Scheme based on environment + Initialize(); - PaymentPracticesLibrary.InitializeCompanySizes(CompanySizeCodes); - PaymentPracticesLibrary.InitializePaymentPeriods(PaymentPeriods); - Initialized := true; + // [WHEN] Insert a new Payment Practice Header via Insert(true) + PaymentPracticeHeader.Init(); + PaymentPracticeHeader.Insert(true); - LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Payment Practices UT"); + // [THEN] Reporting Scheme is auto-detected based on environment application family + case EnvironmentInformation.GetApplicationFamily() of + 'GB': + ExpectedScheme := "Paym. Prac. Reporting Scheme"::"Dispute & Retention"; + else + ExpectedScheme := "Paym. Prac. Reporting Scheme"::Standard; + end; + Assert.AreEqual( + ExpectedScheme, + PaymentPracticeHeader."Reporting Scheme", + 'Reporting Scheme should be auto-detected based on environment application family.'); end; - local procedure MockVendLedgerEntry(VendorNo: Code[20]; var VendorLedgerEntry: Record "Vendor Ledger Entry"; DocType: Enum "Gen. Journal Document Type"; PostingDate: Date; DueDate: Date; PmtPostingDate: Date; IsOpen: Boolean) + [Test] + procedure DisputeRetCalcHeaderTotalsAllPaidOnTime() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeData: Record "Payment Practice Data"; + InvoiceAmount1: Decimal; + InvoiceAmount2: Decimal; begin - VendorLedgerEntry.Init(); - VendorLedgerEntry."Entry No." := LibraryUtility.GetNewRecNo(VendorLedgerEntry, VendorLedgerEntry.FieldNo("Entry No.")); - VendorLedgerEntry."Document Type" := DocType; - VendorLedgerEntry."Posting Date" := PostingDate; - VendorLedgerEntry."Document Date" := PostingDate; - VendorLedgerEntry."Vendor No." := VendorNo; - VendorLedgerEntry."Due Date" := DueDate; - VendorLedgerEntry.Open := IsOpen; - VendorLedgerEntry."Closed at Date" := PmtPostingDate; - VendorLedgerEntry.Amount := LibraryRandom.RandDec(1000, 2); - VendorLedgerEntry.Insert(); + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] CalculateHeaderTotals counts all closed invoices paid on time with zero overdue totals + Initialize(); + + // [GIVEN] Payment Practice Header "PPH" with Dispute and Retention scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderWithScheme( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::Period, + "Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + + // [GIVEN] Two closed invoices paid on time (Actual <= Agreed) + InvoiceAmount1 := 500; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 1, false, 10, 30, InvoiceAmount1, false); + InvoiceAmount2 := 300; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 2, false, 20, 30, InvoiceAmount2, false); + + // [WHEN] CalculateHeaderTotals is called + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + PaymentPracticesLibrary.DisputeRetCalcHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); + + // [THEN] Total payments = 2, total amount = sum of both, overdue = 0, dispute pct = 0 + Assert.AreEqual(2, PaymentPracticeHeader."Total Number of Payments", 'Total Number of Payments'); + Assert.AreEqual(InvoiceAmount1 + InvoiceAmount2, PaymentPracticeHeader."Total Amount of Payments", 'Total Amount of Payments'); + Assert.AreEqual(0, PaymentPracticeHeader."Total Amt. of Overdue Payments", 'Total Amt. of Overdue Payments'); + Assert.AreEqual(0, PaymentPracticeHeader."Pct Overdue Due to Dispute", 'Pct Overdue Due to Dispute'); end; - local procedure MockVendorInvoice(VendorNo: Code[20]; PostingDate: Date; DueDate: Date) InvoiceAmount: Decimal; + [Test] + procedure DisputeRetCalcHeaderTotalsOverdueNoDispute() var - VendorLedgerEntry: Record "Vendor Ledger Entry"; + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeData: Record "Payment Practice Data"; + InvoiceAmount: Decimal; begin - MockVendLedgerEntry(VendorNo, VendorLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, 0D, true); - VendorLedgerEntry.CalcFields("Amount (LCY)"); - InvoiceAmount := VendorLedgerEntry."Amount (LCY)"; + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] CalculateHeaderTotals calculates overdue amounts correctly when no invoices are disputed + Initialize(); + + // [GIVEN] Payment Practice Header "PPH" with Dispute and Retention scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderWithScheme( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::Period, + "Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + + // [GIVEN] Closed overdue invoice without dispute (Actual > Agreed, Dispute = false) + InvoiceAmount := 1000; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 1, false, 45, 30, InvoiceAmount, false); + + // [WHEN] CalculateHeaderTotals is called + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + PaymentPracticesLibrary.DisputeRetCalcHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); + + // [THEN] Total payments = 1, overdue amount = invoice amount, dispute pct = 0 + Assert.AreEqual(1, PaymentPracticeHeader."Total Number of Payments", 'Total Number of Payments'); + Assert.AreEqual(InvoiceAmount, PaymentPracticeHeader."Total Amt. of Overdue Payments", 'Total Amt. of Overdue Payments'); + Assert.AreEqual(0, PaymentPracticeHeader."Pct Overdue Due to Dispute", 'Pct Overdue Due to Dispute'); + + // [THEN] Data record has "Overdue Due to Dispute" = false + PaymentPracticeData.FindFirst(); + Assert.IsFalse(PaymentPracticeData."Overdue Due to Dispute", 'Overdue Due to Dispute should be false'); end; - local procedure MockVendorInvoiceAndPayment(VendorNo: Code[20]; PostingDate: Date; DueDate: Date; PaymentPostingDate: Date) InvoiceAmount: Decimal; + [Test] + procedure DisputeRetCalcHeaderTotalsMixedOverdueDispute() var - VendorLedgerEntry: Record "Vendor Ledger Entry"; + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeData: Record "Payment Practice Data"; + OverdueAmount1: Decimal; + OverdueAmount2: Decimal; + OverdueAmount3: Decimal; begin - MockVendLedgerEntry(VendorNo, VendorLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, PaymentPostingDate, false); - VendorLedgerEntry.CalcFields("Amount (LCY)"); - InvoiceAmount := VendorLedgerEntry."Amount (LCY)"; + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] CalculateHeaderTotals calculates correct dispute percentage when some overdue invoices are disputed + Initialize(); + + // [GIVEN] Payment Practice Header "PPH" with Dispute and Retention scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderWithScheme( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::Period, + "Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + + // [GIVEN] Three overdue invoices: one disputed, two not + OverdueAmount1 := 100; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 1, false, 45, 30, OverdueAmount1, true); + OverdueAmount2 := 200; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 2, false, 50, 30, OverdueAmount2, false); + OverdueAmount3 := 300; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 3, false, 60, 30, OverdueAmount3, false); + + // [WHEN] CalculateHeaderTotals is called + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + PaymentPracticesLibrary.DisputeRetCalcHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); + + // [THEN] Dispute pct = 1/3 * 100 ≈ 33.33 + Assert.AreEqual(3, PaymentPracticeHeader."Total Number of Payments", 'Total Number of Payments'); + Assert.AreEqual(OverdueAmount1 + OverdueAmount2 + OverdueAmount3, PaymentPracticeHeader."Total Amt. of Overdue Payments", 'Total Amt. of Overdue Payments'); + Assert.AreNearlyEqual(33.33, PaymentPracticeHeader."Pct Overdue Due to Dispute", 0.01, 'Pct Overdue Due to Dispute'); end; - local procedure MockVendorInvoiceAndPaymentInPeriod(VendorNo: Code[20]; StartingDate: Date; PaidInDays_min: Integer; PaidInDays_max: Integer) InvoiceAmount: Decimal; - var - PostingDate: Date; - DueDate: Date; - PaymentPostingDate: Date; - begin - PostingDate := StartingDate; - DueDate := StartingDate; - if PaidInDays_max <> 0 then - PaymentPostingDate := PostingDate + LibraryRandom.RandIntInRange(PaidInDays_min, PaidInDays_max) - else - PaymentPostingDate := PostingDate + PaidInDays_min + LibraryRandom.RandInt(10); - InvoiceAmount := MockVendorInvoiceAndPayment(VendorNo, PostingDate, DueDate, PaymentPostingDate); - end; - - local procedure MockCustLedgerEntry(CustomerNo: Code[20]; var CustLedgerEntry: Record "Cust. Ledger Entry"; DocType: Enum "Gen. Journal Document Type"; PostingDate: Date; DueDate: Date; PmtPostingDate: Date; IsOpen: Boolean) - begin - CustLedgerEntry.Init(); - CustLedgerEntry."Entry No." := LibraryUtility.GetNewRecNo(CustLedgerEntry, CustLedgerEntry.FieldNo("Entry No.")); - CustLedgerEntry."Document Type" := DocType; - CustLedgerEntry."Posting Date" := PostingDate; - CustLedgerEntry."Document Date" := PostingDate; - CustLedgerEntry."Customer No." := CustomerNo; - CustLedgerEntry."Due Date" := DueDate; - CustLedgerEntry.Open := IsOpen; - CustLedgerEntry."Closed at Date" := PmtPostingDate; - CustLedgerEntry.Amount := LibraryRandom.RandDec(1000, 2); - CustLedgerEntry.Insert(); - end; - - local procedure MockCustomerInvoice(CustomerNo: Code[20]; PostingDate: Date; DueDate: Date) InvoiceAmount: Decimal; + [Test] + procedure DisputeRetCalcHeaderTotalsMixOnTimeAndOverdue() var - CustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeData: Record "Payment Practice Data"; + OnTimeAmount: Decimal; + OverdueDisputedAmount: Decimal; + OverdueNotDisputedAmount: Decimal; + OpenAmount: Decimal; begin - MockCustLedgerEntry(CustomerNo, CustLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, 0D, true); - CustLedgerEntry.CalcFields("Amount (LCY)"); - InvoiceAmount := CustLedgerEntry."Amount (LCY)"; + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] CalculateHeaderTotals correctly handles a mix of on-time, overdue, and open invoices + Initialize(); + + // [GIVEN] Payment Practice Header "PPH" with Dispute and Retention scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderWithScheme( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::Period, + "Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + + // [GIVEN] One on-time closed invoice + OnTimeAmount := 400; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 1, false, 10, 30, OnTimeAmount, false); + + // [GIVEN] One overdue disputed invoice + OverdueDisputedAmount := 600; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 2, false, 45, 30, OverdueDisputedAmount, true); + + // [GIVEN] One overdue non-disputed invoice + OverdueNotDisputedAmount := 200; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 3, false, 50, 30, OverdueNotDisputedAmount, false); + + // [GIVEN] One open invoice (should be skipped) + OpenAmount := 999; + MockPaymentPracticeData(PaymentPracticeHeader."No.", 4, true, 0, 30, OpenAmount, false); + + // [WHEN] CalculateHeaderTotals is called + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); + PaymentPracticesLibrary.DisputeRetCalcHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); + + // [THEN] Total payments = 3 (open skipped), total amount = on-time + both overdue + Assert.AreEqual(3, PaymentPracticeHeader."Total Number of Payments", 'Total Number of Payments'); + Assert.AreEqual(OnTimeAmount + OverdueDisputedAmount + OverdueNotDisputedAmount, PaymentPracticeHeader."Total Amount of Payments", 'Total Amount of Payments'); + + // [THEN] Overdue amount = only overdue invoices + Assert.AreEqual(OverdueDisputedAmount + OverdueNotDisputedAmount, PaymentPracticeHeader."Total Amt. of Overdue Payments", 'Total Amt. of Overdue Payments'); + + // [THEN] Dispute pct = 1/2 * 100 = 50 (1 disputed out of 2 overdue) + Assert.AreEqual(50, PaymentPracticeHeader."Pct Overdue Due to Dispute", 'Pct Overdue Due to Dispute'); end; - local procedure MockCustomerInvoiceAndPayment(CustomerNo: Code[20]; PostingDate: Date; DueDate: Date; PaymentPostingDate: Date) InvoiceAmount: Decimal; + [Test] + procedure CompanySizeGenerationSucceedsWithBlankPeriodCode() var - CustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentPracticeHeader: Record "Payment Practice Header"; + VendorNo: Code[20]; begin - MockCustLedgerEntry(CustomerNo, CustLedgerEntry, "Gen. Journal Document Type"::Invoice, PostingDate, DueDate, PaymentPostingDate, false); - CustLedgerEntry.CalcFields("Amount (LCY)"); - InvoiceAmount := CustLedgerEntry."Amount (LCY)"; + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] Company Size aggregation succeeds + Initialize(); + + // [GIVEN] Vendor "V" with company size and an entry in the period + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); + PaymentPracticesLibrary.MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + + // [GIVEN] Header with Company Size aggregation + PaymentPracticesLibrary.CreatePaymentPracticeHeader( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::"Company Size", + WorkDate() - 180, WorkDate() + 180); + + // [WHEN] Generate payment practices + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Lines are created (one per company size code) + PaymentPracticesLibrary.VerifyLinesCount(PaymentPracticeHeader, 3); + + // [THEN] Data rows include the vendor invoice + PaymentPracticesLibrary.VerifyBufferCount(PaymentPracticeHeader, 1, "Paym. Prac. Header Type"::Vendor); end; - local procedure MockCustomerInvoiceAndPaymentInPeriod(CustomerNo: Code[20]; StartingDate: Date; PaidInDays_min: Integer; PaidInDays_max: Integer) InvoiceAmount: Decimal; + [Test] + procedure CompanySizeStandardLeavesInvoiceCountAndValueZero() var - PostingDate: Date; - DueDate: Date; - PaymentPostingDate: Date; + PaymentPracticeHeader: Record "Payment Practice Header"; + VendorNo: Code[20]; + begin + // [FEATURE] [AI test 0.3] + // [SCENARIO 597313] Standard scheme with Company Size aggregation leaves Invoice Count and Invoice Value at zero + Initialize(); + + // [GIVEN] Vendor "V" with company size and a closed invoice in the period + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 10); + + // [GIVEN] Header with Standard scheme and Company Size aggregation + PaymentPracticesLibrary.CreatePaymentPracticeHeader( + PaymentPracticeHeader, + "Paym. Prac. Header Type"::Vendor, + "Paym. Prac. Aggregation Type"::"Company Size", + "Paym. Prac. Reporting Scheme"::Standard, + WorkDate() - 180, WorkDate() + 180); + + // [WHEN] Generate payment practices + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Lines exist + PaymentPracticesLibrary.VerifyLinesCount(PaymentPracticeHeader, 3); + + // [THEN] All lines have Invoice Count = 0 and Invoice Value = 0 + VerifyAllLinesInvoiceCountAndValueZero(PaymentPracticeHeader."No."); + end; + + local procedure Initialize() begin - PostingDate := StartingDate; - DueDate := StartingDate + LibraryRandom.RandIntInRange(1, 5); - PaymentPostingDate := PostingDate + LibraryRandom.RandIntInRange(PaidInDays_min, PaidInDays_max); - InvoiceAmount := MockCustomerInvoiceAndPayment(CustomerNo, PostingDate, DueDate, PaymentPostingDate); + LibraryTestInitialize.OnTestInitialize(Codeunit::"Payment Practices UT"); + + // This is so demodata and previous tests doesn't influence the tests + PaymentPracticesLibrary.SetExcludeFromPaymentPracticesOnAllVendorsAndCustomers(); + + if Initialized then + exit; + + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Payment Practices UT"); + + PaymentPracticesLibrary.InitializeCompanySizes(CompanySizeCodes); + PaymentPracticesLibrary.CreateDefaultPaymentPeriodTemplates(); + PaymentPracticesLibrary.InitializePaymentPeriods(PaymentPeriods); + Initialized := true; + + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Payment Practices UT"); end; local procedure PrepareExpectedPeriodPcts(var ExpectedPeriodPcts: array[3] of Decimal; var ExpectedPeriodAmountPcts: array[3] of Decimal; PeriodCounts: array[3] of Integer; TotalCount: Integer; PeriodAmounts: array[3] of Decimal; TotalAmount: Decimal) @@ -729,15 +914,49 @@ codeunit 134197 "Payment Practices UT" end; end; + local procedure MockPaymentPracticeData(HeaderNo: Integer; EntryNo: Integer; IsOpen: Boolean; ActualPaymentDays: Integer; AgreedPaymentDays: Integer; InvoiceAmount: Decimal; IsDisputed: Boolean) + var + PaymentPracticeData: Record "Payment Practice Data"; + begin + PaymentPracticeData.Init(); + PaymentPracticeData."Header No." := HeaderNo; + PaymentPracticeData."Invoice Entry No." := EntryNo; + PaymentPracticeData."Source Type" := "Paym. Prac. Header Type"::Vendor; + PaymentPracticeData."Invoice Is Open" := IsOpen; + PaymentPracticeData."Actual Payment Days" := ActualPaymentDays; + PaymentPracticeData."Agreed Payment Days" := AgreedPaymentDays; + PaymentPracticeData."Invoice Amount" := InvoiceAmount; + if IsDisputed then + PaymentPracticeData."Dispute Status" := 'DISPUTED'; + PaymentPracticeData.Insert(); + end; + + local procedure VerifyAllLinesInvoiceCountAndValueZero(HeaderNo: Integer) + var + PaymentPracticeLine: Record "Payment Practice Line"; + begin + PaymentPracticeLine.SetRange("Header No.", HeaderNo); + PaymentPracticeLine.FindSet(); + repeat + Assert.AreEqual(0, PaymentPracticeLine."Invoice Count", 'Invoice Count should be 0 for Standard scheme'); + Assert.AreEqual(0, PaymentPracticeLine."Invoice Value", 'Invoice Value should be 0 for Standard scheme'); + until PaymentPracticeLine.Next() = 0; + end; + [ConfirmHandler] - procedure ConfirmHandler_Yes(Question: Text[1024]; var Reply: Boolean) + procedure ConfirmHandlerYes(Question: Text[1024]; var Reply: Boolean) begin Reply := true; end; [ConfirmHandler] - procedure ConfirmHandler_No(Question: Text[1024]; var Reply: Boolean) + procedure ConfirmHandlerNo(Question: Text[1024]; var Reply: Boolean) begin Reply := false; end; + + [MessageHandler] + procedure MessageHandler(Message: Text[1024]) + begin + end; } From f2d919a1db6037e4369bbdba332101ab3fe2b61a Mon Sep 17 00:00:00 2001 From: AndreasHans Date: Thu, 7 May 2026 11:20:37 +0200 Subject: [PATCH 03/12] Changes for AU --- .../PaymPracPeriodAggregator.Codeunit.al | 8 +- .../PaymPracSizeAggregator.Codeunit.al | 7 +- .../PaymPracSmallBusHandler.Codeunit.al | 62 ++- ...aymentPracticeLinesAggregator.Interface.al | 5 +- .../src/Core/PaymentPracticeMath.Codeunit.al | 263 ++++++++++- .../App/src/Core/PaymentPractices.Codeunit.al | 17 +- .../src/Pages/PaymPracVendLedgEntr.PageExt.al | 27 +- .../App/src/Pages/PaymentPracticeCard.Page.al | 83 +++- .../src/Pages/PaymentPracticeDataList.Page.al | 32 +- .../Payment Practice Small Business.docx | Bin 0 -> 31172 bytes .../Reports/Payment Practice by Period.docx | Bin 25645 -> 28449 bytes .../Payment Practice by Vendor Size.docx | Bin 25077 -> 27874 bytes .../App/src/Reports/PaymentPractice.Report.al | 25 +- .../App/src/Tables/PaymentPeriod.Table.al | 10 +- .../src/Tables/PaymentPracticeHeader.Table.al | 80 +++- .../src/PaymentPracticesLibrary.Codeunit.al | 7 +- .../Test/src/PaymentPracticesUT.Codeunit.al | 436 ++++++++++++++++++ 17 files changed, 1040 insertions(+), 22 deletions(-) create mode 100644 src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice Small Business.docx diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al index 5f4fa8b16e..c5ead4c9d5 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al @@ -14,11 +14,15 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr var FeatureTelemetry: Codeunit "Feature Telemetry"; - procedure PrepareLayout(); + procedure PrepareLayout(ReportingScheme: Enum "Paym. Prac. Reporting Scheme"); var DesignTimeReportSelection: Codeunit "Design-time Report Selection"; begin - DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_PeriodLayout'); + + if ReportingScheme = "Paym. Prac. Reporting Scheme"::"Small Business" then + DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_SmallBusinessLayout') + else + DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_PeriodLayout'); FeatureTelemetry.LogUsage('0000KSU', 'Payment Practices', 'Period layout used.') end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al index b2cae99983..a68f781640 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al @@ -15,8 +15,9 @@ codeunit 686 "Paym. Prac. Size Aggregator" implements PaymentPracticeLinesAggreg PaymentPracticeMath: Codeunit "Payment Practice Math"; FeatureTelemetry: Codeunit "Feature Telemetry"; WrongHeaderTypeErr: Label 'Payment Practice Header Type must be Vendor for this aggregation type.'; + WrongHeaderAggErr: Label 'Payment Practice Aggregation Type must be Period for the Small Business reporting scheme.'; - procedure PrepareLayout(); + procedure PrepareLayout(ReportingScheme: Enum "Paym. Prac. Reporting Scheme"); var DesignTimeReportSelection: Codeunit "Design-time Report Selection"; begin @@ -60,5 +61,7 @@ codeunit 686 "Paym. Prac. Size Aggregator" implements PaymentPracticeLinesAggreg begin if PaymentPracticeHeader."Header Type" in [PaymentPracticeHeader."Header Type"::Customer, PaymentPracticeHeader."Header Type"::"Vendor+Customer"] then Error(WrongHeaderTypeErr); + if PaymentPracticeHeader."Aggregation Type" = PaymentPracticeHeader."Aggregation Type"::"Company Size" then + Error(WrongHeaderAggErr); end; -} +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al index d3e1c84df5..f071d63512 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al @@ -4,24 +4,82 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; +using Microsoft.Purchases.Vendor; + codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHandler { Access = Internal; + var + PaymentPracticeMath: Codeunit "Payment Practice Math"; + WrongHeaderTypeErr: Label 'Payment Practice Header Type must be Vendor for the Small Business reporting scheme.'; + WrongHeaderAggErr: Label 'Payment Practice Aggregation Type must be Period for the Small Business reporting scheme.'; + procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") begin + if PaymentPracticeHeader."Header Type" in [PaymentPracticeHeader."Header Type"::Customer, PaymentPracticeHeader."Header Type"::"Vendor+Customer"] then + Error(WrongHeaderTypeErr); + if PaymentPracticeHeader."Aggregation Type" = PaymentPracticeHeader."Aggregation Type"::"Company Size" then + Error(WrongHeaderAggErr); end; procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean + var + Vendor: Record Vendor; + CompanySize: Record "Company Size"; begin - exit(true); + if PaymentPracticeData."Source Type" <> PaymentPracticeData."Source Type"::Vendor then + exit(false); + + Vendor.SetLoadFields("Company Size Code"); + if not Vendor.Get(PaymentPracticeData."CV No.") then + exit(false); + + if CompanySize.Get(Vendor."Company Size Code") then + exit(CompanySize."Small Business") + else + exit(false); end; procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") + var + TotalCount: Integer; + TotalValue: Decimal; begin + PaymentPracticeData.SetRange("Invoice Is Open", false); + if PaymentPracticeData.FindSet() then + repeat + TotalCount += 1; + TotalValue += PaymentPracticeData."Invoice Amount"; + until PaymentPracticeData.Next() = 0; + PaymentPracticeData.SetRange("Invoice Is Open"); + + PaymentPracticeHeader."Total Number of Payments" := TotalCount; + PaymentPracticeHeader."Total Amount of Payments" := TotalValue; + PaymentPracticeHeader."Mode Payment Time" := PaymentPracticeMath.GetModePaymentTime(PaymentPracticeData); + PaymentPracticeHeader."Mode Payment Time Min." := PaymentPracticeMath.GetModePaymentTimeMin(PaymentPracticeData); + PaymentPracticeHeader."Mode Payment Time Max." := PaymentPracticeMath.GetModePaymentTimeMax(PaymentPracticeData); + PaymentPracticeHeader."Median Payment Time" := PaymentPracticeMath.GetMedianPaymentTime(PaymentPracticeData); + PaymentPracticeHeader."80th Percentile Payment Time" := PaymentPracticeMath.Get80thPercentilePaymentTime(PaymentPracticeData); + PaymentPracticeHeader."95th Percentile Payment Time" := PaymentPracticeMath.Get95thPercentilePaymentTime(PaymentPracticeData); + PaymentPracticeHeader."Pct Peppol Enabled" := PaymentPracticeMath.GetPctPeppolEnabled(PaymentPracticeData); + PaymentPracticeHeader."Pct Small Business Payments" := PaymentPracticeMath.GetPctSmallBusinessPayments(PaymentPracticeData, PaymentPracticeHeader); end; procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") + var + InvoiceCount: Integer; + InvoiceValue: Decimal; begin + PaymentPracticeData.SetRange("Invoice Is Open", false); + if PaymentPracticeData.FindSet() then + repeat + InvoiceCount += 1; + InvoiceValue += PaymentPracticeData."Invoice Amount"; + until PaymentPracticeData.Next() = 0; + PaymentPracticeData.SetRange("Invoice Is Open"); + + PaymentPracticeLine."Invoice Count" := InvoiceCount; + PaymentPracticeLine."Invoice Value" := InvoiceValue; end; -} +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeLinesAggregator.Interface.al b/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeLinesAggregator.Interface.al index 4484e3bc06..18b879db29 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeLinesAggregator.Interface.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeLinesAggregator.Interface.al @@ -9,7 +9,8 @@ interface PaymentPracticeLinesAggregator /// /// Prepare the layout to be used for Payment Practice report that is suitable for the aggregation type of the header/lines. /// - procedure PrepareLayout() + /// The reporting scheme to be used for the layout. + procedure PrepareLayout(ReportingScheme: Enum "Paym. Prac. Reporting Scheme") /// /// Generate the lines for the Payment Practice report based on the Payment Practice Data raw data and Payment Practice Header fields. @@ -23,4 +24,4 @@ interface PaymentPracticeLinesAggregator /// /// The document header that needs checking. procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") -} +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al index f410a7c11c..366049e617 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al @@ -4,6 +4,9 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; +using Microsoft.Purchases.Payables; +using Microsoft.Purchases.Vendor; + codeunit 693 "Payment Practice Math" { Access = internal; @@ -72,4 +75,262 @@ codeunit 693 "Payment Practice Math" foreach Number in List do Total += Number; end; -} + + procedure GetModePaymentTime(var PaymentPracticeData: Record "Payment Practice Data"): Integer + var + ActualPaymentTimes: List of [Integer]; + begin + PaymentPracticeData.SetRange("Invoice Is Open", false); + if PaymentPracticeData.FindSet() then + repeat + ActualPaymentTimes.Add(PaymentPracticeData."Actual Payment Days"); + until PaymentPracticeData.Next() = 0; + PaymentPracticeData.SetRange("Invoice Is Open"); + exit(Mode(ActualPaymentTimes)); + end; + + procedure GetModePaymentTimeMin(var PaymentPracticeData: Record "Payment Practice Data"): Integer + var + ModesPerVendor: List of [Integer]; + begin + GetModesPerVendor(PaymentPracticeData, ModesPerVendor); + exit(MinOfList(ModesPerVendor)); + end; + + procedure GetModePaymentTimeMax(var PaymentPracticeData: Record "Payment Practice Data"): Integer + var + ModesPerVendor: List of [Integer]; + begin + GetModesPerVendor(PaymentPracticeData, ModesPerVendor); + exit(MaxOfList(ModesPerVendor)); + end; + + procedure GetMedianPaymentTime(var PaymentPracticeData: Record "Payment Practice Data"): Decimal + var + ActualPaymentTimes: List of [Integer]; + MiddleIndex: Integer; + begin + PaymentPracticeData.SetRange("Invoice Is Open", false); + if PaymentPracticeData.FindSet() then + repeat + ActualPaymentTimes.Add(PaymentPracticeData."Actual Payment Days"); + until PaymentPracticeData.Next() = 0; + PaymentPracticeData.SetRange("Invoice Is Open"); + + if ActualPaymentTimes.Count() = 0 then + exit(0); + + SortIntegerList(ActualPaymentTimes); + + MiddleIndex := ActualPaymentTimes.Count() div 2; + if ActualPaymentTimes.Count() mod 2 = 0 then + exit((ActualPaymentTimes.Get(MiddleIndex) + ActualPaymentTimes.Get(MiddleIndex + 1)) / 2) + else + exit(ActualPaymentTimes.Get(MiddleIndex + 1)); + end; + + procedure Get80thPercentilePaymentTime(var PaymentPracticeData: Record "Payment Practice Data"): Integer + var + ActualPaymentTimes: List of [Integer]; + begin + GetClosedInvoicePaymentTimes(PaymentPracticeData, ActualPaymentTimes); + exit(Percentile(ActualPaymentTimes, 80)); + end; + + procedure Get95thPercentilePaymentTime(var PaymentPracticeData: Record "Payment Practice Data"): Integer + var + ActualPaymentTimes: List of [Integer]; + begin + GetClosedInvoicePaymentTimes(PaymentPracticeData, ActualPaymentTimes); + exit(Percentile(ActualPaymentTimes, 95)); + end; + + procedure GetPctPeppolEnabled(var PaymentPracticeData: Record "Payment Practice Data"): Decimal + var + Vendor: Record Vendor; + VendorGLNCache: Dictionary of [Code[20], Boolean]; + HasGLN: Boolean; + Total: Integer; + PeppolCount: Integer; + begin + PaymentPracticeData.SetRange("Invoice Is Open", false); + if PaymentPracticeData.FindSet() then + repeat + + Total += 1; + if not VendorGLNCache.Get(PaymentPracticeData."CV No.", HasGLN) then begin + HasGLN := Vendor.Get(PaymentPracticeData."CV No.") and (Vendor.GLN <> ''); + VendorGLNCache.Add(PaymentPracticeData."CV No.", HasGLN); + end; + + if HasGLN then + PeppolCount += 1; + until PaymentPracticeData.Next() = 0; + + PaymentPracticeData.SetRange("Invoice Is Open"); + if Total = 0 then + exit(0); + + exit(PeppolCount / Total * 100); + end; + + procedure GetPctSmallBusinessPayments(var PaymentPracticeData: Record "Payment Practice Data"; PaymentPracticeHeader: Record "Payment Practice Header"): Decimal + var + VendorLedgerEntry: Record "Vendor Ledger Entry"; + CompanySize: Record "Company Size"; + Vendor: Record Vendor; + TotalAmountSmallBusinesses: Decimal; + TotalAmountAllVendors: Decimal; + begin + VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); + VendorLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); + if VendorLedgerEntry.FindSet() then + repeat + VendorLedgerEntry.CalcFields(Amount, "Remaining Amount"); + Vendor.Get(VendorLedgerEntry."Vendor No."); + if CompanySize.Get(Vendor."Company Size Code") then + if CompanySize."Small Business" then + TotalAmountSmallBusinesses += VendorLedgerEntry."Remaining Amount" - VendorLedgerEntry.Amount; + TotalAmountAllVendors += VendorLedgerEntry."Remaining Amount" - VendorLedgerEntry.Amount; + until VendorLedgerEntry.Next() = 0; + + if TotalAmountAllVendors = 0 then + exit(0); + + exit(TotalAmountSmallBusinesses / TotalAmountAllVendors * 100); + end; + + local procedure GetClosedInvoicePaymentTimes(var PaymentPracticeData: Record "Payment Practice Data"; var ActualPaymentTimes: List of [Integer]) + begin + PaymentPracticeData.SetRange("Invoice Is Open", false); + if PaymentPracticeData.FindSet() then + repeat + ActualPaymentTimes.Add(PaymentPracticeData."Actual Payment Days"); + until PaymentPracticeData.Next() = 0; + PaymentPracticeData.SetRange("Invoice Is Open"); + end; + + local procedure Percentile(var List: List of [Integer]; P: Integer): Integer + var + Index: Integer; + begin + if List.Count() = 0 then + exit(0); + + SortIntegerList(List); + + Index := List.Count() * P div 100; + if Index < 1 then + Index := 1; + if Index > List.Count() then + Index := List.Count(); + + exit(List.Get(Index)); + end; + + local procedure GetModesPerVendor(var PaymentPracticeData: Record "Payment Practice Data"; var ModesPerVendor: List of [Integer]) + var + ActualPaymentTimes: List of [Integer]; + CurrentVendor: Code[20]; + begin + PaymentPracticeData.SetRange("Invoice Is Open", false); + PaymentPracticeData.SetCurrentKey("CV No."); + if PaymentPracticeData.FindSet() then begin + CurrentVendor := PaymentPracticeData."CV No."; + repeat + if PaymentPracticeData."CV No." <> CurrentVendor then begin + ModesPerVendor.Add(Mode(ActualPaymentTimes)); + Clear(ActualPaymentTimes); + CurrentVendor := PaymentPracticeData."CV No."; + end; + ActualPaymentTimes.Add(PaymentPracticeData."Actual Payment Days"); + until PaymentPracticeData.Next() = 0; + ModesPerVendor.Add(Mode(ActualPaymentTimes)); + end; + PaymentPracticeData.SetRange("Invoice Is Open"); + PaymentPracticeData.SetCurrentKey("Header No.", "Invoice Entry No.", "Source Type"); + end; + + local procedure MinOfList(var List: List of [Integer]): Integer + var + Value: Integer; + MinValue: Integer; + IsFirst: Boolean; + begin + if List.Count() = 0 then + exit(0); + + IsFirst := true; + foreach Value in List do + if IsFirst then begin + MinValue := Value; + IsFirst := false; + end else + if Value < MinValue then + MinValue := Value; + + exit(MinValue); + end; + + local procedure MaxOfList(var List: List of [Integer]): Integer + var + Value: Integer; + MaxValue: Integer; + begin + if List.Count() = 0 then + exit(0); + + MaxValue := List.Get(1); + + foreach Value in List do + if Value > MaxValue then + MaxValue := Value; + + exit(MaxValue); + end; + + local procedure Mode(var List: List of [Integer]): Integer + var + Frequencies: Dictionary of [Integer, Integer]; + Value: Integer; + Frequency: Integer; + MaxFrequency: Integer; + ModeValue: Integer; + begin + if List.Count() = 0 then + exit(0); + + foreach Value in List do + if Frequencies.ContainsKey(Value) then + Frequencies.Set(Value, Frequencies.Get(Value) + 1) + else + Frequencies.Add(Value, 1); + + MaxFrequency := 0; + ModeValue := 0; + foreach Value in Frequencies.Keys() do begin + Frequency := Frequencies.Get(Value); + if (Frequency > MaxFrequency) or ((Frequency = MaxFrequency) and (Value < ModeValue)) then begin + MaxFrequency := Frequency; + ModeValue := Value; + end; + end; + + exit(ModeValue); + end; + + local procedure SortIntegerList(var List: List of [Integer]) + var + i: Integer; + j: Integer; + Temp: Integer; + begin + for i := 1 to List.Count() - 1 do + for j := 1 to List.Count() - i do + if List.Get(j) > List.Get(j + 1) then begin + Temp := List.Get(j); + List.Set(j, List.Get(j + 1)); + List.Set(j + 1, Temp); + end; + end; +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al index d8539907db..a32177449b 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al @@ -37,10 +37,25 @@ codeunit 689 "Payment Practices" end; local procedure GenerateTotals(var PaymentPracticeData: Record "Payment Practice Data"; var PaymentPracticeHeader: Record "Payment Practice Header") + var + SchemeHandler: Interface PaymentPracticeSchemeHandler; begin PaymentPracticeHeader."Average Actual Payment Period" := PaymentPracticeMath.GetAverageActualPaymentTime(PaymentPracticeData); PaymentPracticeHeader."Average Agreed Payment Period" := PaymentPracticeMath.GetAverageAgreedPaymentTime(PaymentPracticeData); PaymentPracticeHeader."Pct Paid on Time" := PaymentPracticeMath.GetPercentOfOnTimePayments(PaymentPracticeData); + + // Reset fields before calculating scheme-specific totals + PaymentPracticeHeader."Mode Payment Time" := 0; + PaymentPracticeHeader."Mode Payment Time Min." := 0; + PaymentPracticeHeader."Mode Payment Time Max." := 0; + PaymentPracticeHeader."Median Payment Time" := 0; + PaymentPracticeHeader."80th Percentile Payment Time" := 0; + PaymentPracticeHeader."95th Percentile Payment Time" := 0; + PaymentPracticeHeader."Pct Peppol Enabled" := 0; + PaymentPracticeHeader."Pct Small Business Payments" := 0; + + SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; + SchemeHandler.CalculateHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); end; local procedure GenerateData(var PaymentPracticeData: Record "Payment Practice Data"; PaymentPracticeHeader: Record "Payment Practice Header"; PaymentPracticeDataGenerator: Interface PaymentPracticeDataGenerator) @@ -52,4 +67,4 @@ codeunit 689 "Payment Practices" begin PaymentPracticeLinesAggregator.GenerateLines(PaymentPracticeData, PaymentPracticeHeader); end; -} +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al index a9f37a91a6..14bbdf7120 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al @@ -5,6 +5,7 @@ namespace Microsoft.Finance.Analysis; using Microsoft.Purchases.Payables; +using Microsoft.Purchases.Vendor; pageextension 681 "Paym. Prac. Vend. Ledg. Entr." extends "Vendor Ledger Entries" { @@ -18,5 +19,29 @@ pageextension 681 "Paym. Prac. Vend. Ledg. Entr." extends "Vendor Ledger Entries Visible = false; } } + addafter("Vendor No.") + { + field(SmallBusiness; IsSmallBusiness) + { + ApplicationArea = All; + Caption = 'Small Business'; + ToolTip = 'Specifies whether the vendor is classified as a small business.'; + Editable = false; + } + } } -} + + trigger OnAfterGetRecord() + var + Vendor: Record Vendor; + CompanySize: Record "Company Size"; + begin + IsSmallBusiness := false; + if Vendor.Get(Rec."Vendor No.") then + if CompanySize.Get(Vendor."Company Size Code") then + IsSmallBusiness := CompanySize."Small Business"; + end; + + var + IsSmallBusiness: Boolean; +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al index d9b7970a9b..c10d4bb2f4 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al @@ -4,6 +4,7 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; +using Microsoft.Purchases.Payables; using System.Telemetry; using System.Utilities; @@ -27,8 +28,6 @@ page 687 "Payment Practice Card" } field("Reporting Scheme"; Rec."Reporting Scheme") { - Visible = false; - Editable = false; } field("Aggregation Type"; Rec."Aggregation Type") { @@ -111,6 +110,69 @@ page 687 "Payment Practice Card" field("Pct Overdue Due to Dispute"; Rec."Pct Overdue Due to Dispute") { } + group("Small Business Scheme") + { + Caption = 'Small Business Scheme'; + Visible = Rec."Reporting Scheme" = Rec."Reporting Scheme"::"Small Business"; + + field("Mode Payment Time"; Rec."Mode Payment Time") + { + trigger OnDrillDown() + begin + ShowHeaderDataLines(); + end; + } + + field("Mode Payment Time Min."; Rec."Mode Payment Time Min.") + { + trigger OnDrillDown() + begin + ShowHeaderDataLines(); + end; + } + field("Mode Payment Time Max."; Rec."Mode Payment Time Max.") + { + trigger OnDrillDown() + begin + ShowHeaderDataLines(); + end; + } + field("Median Payment Time"; Rec."Median Payment Time") + { + trigger OnDrillDown() + begin + ShowHeaderDataLines(); + end; + } + field("80th Percentile Payment Time"; Rec."80th Percentile Payment Time") + { + trigger OnDrillDown() + begin + ShowHeaderDataLines(); + end; + } + field("95th Percentile Payment Time"; Rec."95th Percentile Payment Time") + { + trigger OnDrillDown() + begin + ShowHeaderDataLines(); + end; + } + field("Pct Peppol Enabled"; Rec."Pct Peppol Enabled") + { + trigger OnDrillDown() + begin + ShowHeaderDataLines(); + end; + } + field("Pct Small Business Payments"; Rec."Pct Small Business Payments") + { + trigger OnDrillDown() + begin + ShowSmallBusinessVendorLedgerEntries(); + end; + } + } } part(Lines; "Payment Practice Lines") { @@ -156,7 +218,7 @@ page 687 "Payment Practice Card" trigger OnAction() begin - PrepareLayout(Rec."Aggregation Type"); + PrepareLayout(Rec."Aggregation Type", Rec."Reporting Scheme"); Rec.SetRecFilter(); Report.Run(Report::"Payment Practice", false, true, Rec); FeatureTelemetry.LogUptake('0000KSV', 'Payment Practices', "Feature Uptake Status"::Used); @@ -190,9 +252,9 @@ page 687 "Payment Practice Card" LinesWillBeDeletedQst: Label 'All previously generated lines will be deleted. Do you want to continue?'; NoEntriesFoundMsg: Label 'The payment practice generator found no entries corresponding to the header type, starting and ending date.'; - local procedure PrepareLayout(PaymentPracticeLinesAggregator: Interface PaymentPracticeLinesAggregator) + local procedure PrepareLayout(PaymentPracticeLinesAggregator: Interface PaymentPracticeLinesAggregator; ReportingScheme: Enum "Paym. Prac. Reporting Scheme") begin - PaymentPracticeLinesAggregator.PrepareLayout(); + PaymentPracticeLinesAggregator.PrepareLayout(ReportingScheme); end; local procedure ShowHeaderDataLines() @@ -203,8 +265,17 @@ page 687 "Payment Practice Card" Page.RunModal(Page::"Payment Practice Data List", PaymentPracticeData); end; + local procedure ShowSmallBusinessVendorLedgerEntries() + var + VendorLedgerEntry: Record "Vendor Ledger Entry"; + begin + VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); + VendorLedgerEntry.SetRange("Posting Date", Rec."Starting Date", Rec."Ending Date"); + Page.RunModal(Page::"Vendor Ledger Entries", VendorLedgerEntry); + end; + local procedure UpdateVisibility() begin CurrPage.Lines.Page.UpdateVisibility(Rec."Aggregation Type", Rec."Header Type"); end; -} +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al index 3beb3021d4..a8eb0fe955 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al @@ -4,6 +4,8 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; +using Microsoft.Purchases.Vendor; + page 686 "Payment Practice Data List" { ApplicationArea = All; @@ -55,6 +57,16 @@ page 686 "Payment Practice Data List" { ToolTip = 'Specifies the company size code of the vendor that is the source for this entry.'; } + field(SmallBusiness; IsSmallBusiness) + { + Caption = 'Small Business'; + ToolTip = 'Specifies whether the vendor is classified as a small business.'; + } + field(PeppolEnabled; IsPeppolEnabled) + { + Caption = 'PEPPOL Enabled'; + ToolTip = 'Specifies whether the vendor has a GLN and is PEPPOL enabled.'; + } field("Agreed Payment Days"; Rec."Agreed Payment Days") { ToolTip = 'Specifies the number of days that was the agreed period for payment for the invoice.'; @@ -80,4 +92,22 @@ page 686 "Payment Practice Data List" } } } -} + + trigger OnAfterGetRecord() + var + CompanySize: Record "Company Size"; + Vendor: Record Vendor; + begin + IsSmallBusiness := false; + if CompanySize.Get(Rec."Company Size Code") then + IsSmallBusiness := CompanySize."Small Business"; + + IsPeppolEnabled := false; + if Vendor.Get(Rec."CV No.") then + IsPeppolEnabled := Vendor.GLN <> ''; + end; + + var + IsSmallBusiness: Boolean; + IsPeppolEnabled: Boolean; +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice Small Business.docx b/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice Small Business.docx new file mode 100644 index 0000000000000000000000000000000000000000..9015ca74adb26118a841fbad028330c994ee4253 GIT binary patch literal 31172 zcmeFYbyQqWx;Ba@XmEFTYup_g3+~!L)3|#eXa{#|B*8tnySoPh0fIXOPl6=m>)*_o zIp@rod)B)D-L<~ESMPW2s@hWZRCU!`&r_wPhKxdpfQIlA0Re#y;R$A^tc{3(u#JR( zfQj%D$(QUjk_9sLZI% zzMhy?YtzTT_W0DIrNhb%oK^!)Pt2*c=vS$888o)-9W{N)!=cwBik@ei<569B9R}r+TFKsa*Kh@UYKM4)n8a9khSW+8 zlLs>(Gav^eGc2VSe8@{PGRuQy!mu>0MPq=Kq7~Qsn3xhY$$9n`CQ{RikE|z)I0#z* z?H^bY(kiP+jDWCTf`EVrZ)bjPp0+%;ZZ1%{(9fY5{ydy}Rc7YDdq%qO^=#E;7; z7-cfVwKq}K1_gjE#KvU^e(%Q*7o{#?M3O-nDL_?jm2P1U$#`UFWaP&bWFT@o^+s** zBdwve|7;{@1Qs@LzOCweppk^V(tDs%wkto&9HWhT8s|6wok8>f#C-KDe5pHjBd_^a z(Z+BpsOMLa_wSA2qVq+hulv6?!iQ6*OQ!RtQ&r)hBLj@8)TgJXn1>W$0bkq+JQH+P zga*u{g{7GU$3#%Us=Xg|9JJpP3Dlhq-m;IM7$kRNlx~aZcttmT9`B#mk!~qsEN@3C zYkMirl35?IdWytl)L&R;nVyqolnVP`n3rOVz(eIZA;=}%z=(WU!v6+QD>D~a*U-XB z#nxa*zqvyOgDo;_ScJx;@!(Kq5Ybkss9r&MVIJXwJ?)MoDnHT%8YmB8F|?lGFm+>i zTB4nA9~E0?n;hTxK6C?#{UD)#8Lbf!JW;7&K+Fr^EC3&~FtEpY$~Wz+(>D=KQv%vW zVOP%jt}3=jt85(I3I+VA@`^QlZWs)u2tzl^&uz2QVz-6iX3i7(`VHq7N z9!ANUIefSu=u_K+v_l*aC9W{|*#I*H+O}B{Ihu^kiC9)Z%$;n|Bqc#!icxY^Npi09 z0qlL2WqNjcnq#$|E2K!v6;g$eh=>czgCP~kh={fzDTFP`prxl|B7;iO*+XL$n6wyp z*`!PH6|xu)WGX}HSsB_(_*wJ|H>x=K#thg{Y(E6yvF0Aj9}E#qew=j4z)B~a{HV%~ zspFD?ucmgBL1=ZO`Jl*FsW@V_{D8_EadUI4BP*7A>va=nWit6Owz`0sh=h^3Jp7B5 zm#5Ar)$ygAbW5Go4{BwHMbmd!o?Nrj9JA`c@3f!5(HFdp-e*~+8H@+CFW%@Rz77cy zn&w3&PtO;)ghxLP9`n7KyLyV|;fA~iOKmr!!+3p<#6 zZ8+6u`cLUx!$b>9RN#fL5CJf;7K|HA^!G3J&#x|`?912F#wHiW=*xn%i|k`RrWRH= z#idlIrKQSsz+-`qis&j!loo_|RiS)ZLhGkCyaWZT zf?Q35!VLwq0G^&z1N24xX%3-!{TD5gI~h5ys&*D2c&FLmY5{_UXgkV=XagHUWI#+6 z5o9!4>DgvTr(!uK|ANW|nH1erlYWb9Mr%)^l;SvUUc$Iutm$?l_h> z!Lwg+2C;G|Iy*mfgg~56oR7ih2AG&w{&z&>=^{r*86}5dHZ6=~gl$X{$s$}pDDfL+ zV(4}=GTW05JO03^48FI!EQngdpm3KA##Y%Iqxhwu1VJ@R>sE$|LF55XGi|$>A~aQf zKu(ss(jlf)l!=vJ1%0)De0+6=Fx}Igc^fJENT$>cY|$y#p|8nYF$&;sL5X6ECeXvE za!-!Eud&U3H4w$Llr^8OW<2?VNj*9O-u2#oGK-TTBf*~?L9G-O9bk%4Wa7naICRj8 z*B)UzFktenIGIKx4DhAJ&%3Vgniu3jJ(o&;-R~$HqXMrdQ9Ido+N{OydtRY70UdPMgrD7L{TAz#0DSelP3{81 zBYPaFy3CxwFo-+W;2^4=TpE>FvYGnUJRdKgU$X!|e^J13XtyG8SHMG1NZ3ozV`xvS zs7k4Z3+$yUkBO|Nt7fD|(NZZ+evPpseePdfomrmW$jdQKHPPqNDJu#D7d00N3Ija(S_KA3Tx|`T%gU6> z&_+iyi?QwR6_RZN1GOE+{9)Q}RO|*ID?(9PE1&0w2bUH`D7W$salftYs$%N#FkI=k zLo+j(Goi##Xfce?o|)>SkH>N+_^^x{P08QsRVsj`#nJrkT=BcJqQkP2v-8QZGdSLD zKJmQDaAjkDX?kgX8p7)5F~h0uQ2#Q*s>SJW7h-*QdRpb=0CFgF0&PR8oUG5COPtR4 z55bNQ@cBOE#0=BXKKwIFnXS>liie1O39~&(*5MlaK^cljm)P1`_!tRo(@}DR&c3nF zygD&tI^~>HOV2vK6s#6JJ7FbxlEoYPBvcltPYzh zRyr(Gs;y0gEXRkzimRJSs{sK4=_O+1>F7IZX)+!0ryX&NIjjBC)2oXMlv|6)H%=gV zc`sy4FPqzi2ZrH5m9fU9qT#zG|FOHGrKNQL;J zk|Rmy^h#bgr^bw9R&!M1xDTH;dc^6$i_Nt!OZ|iWStv}Yi7&rw# zejdIedQz#*O;F$9OfWMuuYkD#t71G3r|`wQwN<}kDLX{DT!0-wAjo+<3*$sxJ(mXO zG&UAiAq7Cm{#_h4E}2wEi2$E{bFb1?vb>dPL<20Pv@{A4f=RIaISi}uk=$~yAn-b@ zu2%@aFTls&DiDgD7;mt0?{x|BNpNys-x!`)TIYvDBLI@#1DT}JxbEo#Lv$oXahs#GiAftkS3I?h3q%)&`aKvu9Lf$wDq(gf4L zxvX!r1i>LyUvBmeQw(CH#>IL|bHUZGiNUPxfSxYUyFd`)xeu%UW0zn;sVk zudNo=5LQ`P7=x&e@wN$J?LDT2vyHW0ah(YCmoqYFV5PI zYR2zz|8O#=pwRIh-z(Xd1ujSP`9oLBcjJ{68yN3h$3Ce@IFpc+)zTr-7J+}2)N zXtSD1nc(AKSpb8)8oGUy52>mVW=;?XTZpTxBZ}B3jJ)!LtO#@CP3L<*G54=$LnQE<@DpLkb_=_04 z==}t6p4NyNGH9a!Jpk@*3?}BiK_Uh_Gh_lAdraZG$Iq)HtE%-)m@3nYN>O4BGcPj06Mc~_LK9EX zkOKtd>g1!1P|GtW!&k)6;i2&a6r z|I#z*{kaF7g71Q08$$k$jiXA}HLNh#oKr(t&X>>E2jC;*E}zRN*dhQgB%fe`$GhbH z^KgLXajH31LP9*YMuNu9JHRdt2^o3|CM1!*rGQqNWmE8#Hwx#oU8DjI1gBSQ1gY6z znMe!;gWPdMWXJ1@r~Sp8mQ?~r`ZTiG62UqwmXw89r;EE$03a;D&&NmQ(m60Iq`@8Y zYF1-dF!#IK0RQ-Ns2 z&`f4mVtKoM{PgcHhZ(=$(E5u}FMTL>_oQj`Byc9T4M0#8r|Mly&5jmCKptQWlQ!lJ@T>4x7%c1APo*63(7a(fZMSj9Xp{F7t^D#bh)F4j0e?WbCG7L3h$jdt zZB}8qbnM}7p{)F7NoZ|lLk#?#rRe93-oFwI+VIBxlOrY1dtXc&mD); z{nKMdTctzV7pyvTQ7m{Jgz2Md=k?Pc(sB?n`vcH|gi2@$3X}A7(xb36ur#JM;^U(w zs3-atzD$n~j==rpnd#xB`Q?T2`N74BrR7EV#WZ1OF)HDjyf9|r3!bk4TxZAJ3M>@e z_@x?w`ucjf(=SW$gSUxSP{>Ds-iwEXRTVfRbBySK?C2KOgyQdv5~Ru4#Gxcd6?swP zZ#kc#m0XnAonbYKHcDIu{|)E!x>+B>JxfPh=L=hF=Uwp0KIHTeWOWE}IE0*ntY1>S zMU47+H6d3(TNWf#)J)Cq%O@xx=&qRinV(OP-&e>>fZ6R1m+)gcm_f@5XRRjPNLJg$ z)){Q&?Cc1VG5a%u>iK=_%3L?bSdYnIUYurt#2HtY>aRbEG8B9`>q^WGIaeB=c6MsAy8Z`Gw0$CYK|djZVrSP?;8*0I#A1?D*+uN^&Z6 zHd31qFQZaZ05X8Yo`j{DDx4WQ%FSHnN|-cYbJ<;l^&{L<3EdW##14$lB&i#qgkRY& z)KJeWEGSHOfjf(ry5TXci=FBHx(lXc!=lWh>$N(lF(1thN!^-WTpF2xZA95b%jK0! zhce()z&N(k0z{D0OUR_!K_!|AVQDZJEDq3n+cREEB#VgYS`x-ADr*ByR>ha{i0ByO zLiw(0$mo}2ECQntB`Xq?)}Hl|qlsazAAY4hd`wPmYD8+1ZGzuHzdW%pJv=x#vN5u_ zIy}5OJw5Sd;>-B>{P4oyaR2xsQKROo{ebkay28&5VH-~v>yf8vuq=$OMxx=wSF=1^ zs+vr1bc9m?0(!@np*m#8+gd6SSiP?|=Qjui?ofL+$vs9m3T9X8>v02TOB^4p- zAny<iepkH&@I9UAggWADGGHD zaulQaEp5i767*tg6!xOz;X#LV_YvfkG%#6B*1nRLn?k*4c=I zTUSll#zyT;8$gDGyZZ}rx&kEm%geFpg2^ByDDfe2Cip67UHKMHxuAI*Nov&*c;6_4 zsX6~(&r=;LL6r(J{73`1dsqRe4>(?{W+-+eyux5NB&g zuoVb`<{*i0axG-?`=E4mXeEQL(W=|VHN?RJ_$zez?O|rY16_Q#N`!>{J_KYHx=EmP zf{R@%@s3(hZ|=<%HFF?E`n@2L5k|1PeB(y!v`_FM6bdC~h7u=7cubxwz9?DK7BP}9 zrfi6r|1@GynKS-ORRMH^IO?aHB%*IEZLx! zvS3p@NMn0;N48jJdrk+uskFD}m>A`V{8jFa_fg@yGWbZDJIwK_2biA&` z$vwuz!LRUt-a}*HJ!y3VS??idAVe#@=KI1^Bw?u+yhCNcWRRDi_YqbY9z8}-wP3$N zm_$fg-#wnvcH2gI<4yEt7^V9;eBLOwvNE^2u#{oV|3geQ{5p=qo3r%gIx#D!F3B-4 z|5WhRVPUo2c<%fn$>$E4(+z@reT|*+Y1)juGAJ7HXqi&MlVkLF)!{6Zc(%5d+Z8$r zW>(Uy&CEm_O;m!8LNkeLM>A_{t=g
    vP5*9*w(2XegL4c}vgNyL22wTD>kJ0I`I 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 literal 0 HcmV?d00001 diff --git a/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Period.docx b/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Period.docx index 8bcbadc6848dddf747227330b5027f16a8ad0b2b..381118eff44d80645d0123e0df9cebeb5fab3b3e 100644 GIT binary patch delta 4855 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 S@t-2Tzg_U}i1PHG?0*10jdsic delta 2059 zcmZ9NX*86L8^)i9r(0g)w87ER8jL zmKJM7%VZ}hTPR5quipC9Isg0mbf4>->s+6HSKcJZ=>{v4d3W0~U2^>40FYS2pAt>O#}tSIl?gb#`N$Z*h_`q~$CwP3eiALN?DX6fn(MB3pO zyRK^^SCVB9M(nF==P_|Kb09s`sn9w_drBAX&#e!X7doQKUn249OaV?gDDSAv#{+Je2iK8bbQKB z9vJ=Ih{w0P^Mi`8!ErBkrsqBY(uh6ZVY$z|`AM5TWqttxdq#_?{Duq#q%fve0m%YQ z#Kv8EWOeqt)9`)1PG!5U-+Cgqg?39cRW7& z&EA9gr5*MkR+WlQqnZ`DB9S~vYTfp`e*TkCxCETG1d zE+b$~A7aOhQPxEi&fo(8_F(`J`yK2X8XOWxBhaWC=R(fVwAC-tsI{3MfirsW8*Otu z|B6~nI)y7VDGLrI=uGC~W|EzAcwFGBT!X=b9Z$LxddO{Vtb7rCimD~5&D3tqLNz8+@JQnuV}_F8N`do^kA z)K_6#`SFNZQ5R~)R~aeF=A)TYt06emsnqB>_HtAp?!~Di%(eDd2@e;e>KV3*#)rb2 zjYsFcE;B)v;IFi(%C<`J4lHWC68SMIi*R<&#pk!T+aqhD-^aut+s?bazGV5L;0h@< zzLwEzdoGVtipc36PSKLMGTKhwzT8?VMzhSW$k3`zRPsch?at+*Vu)Oek55+eGHNfY z&~Qc*m2=tr!F5JGw$)oY7s5X4wZi3cC}qRtc}Z1UfnPIPKb2YH3o(vw2c9)H_C6Zg zEag_u*iQFCrP(;&DaDrVY>;+JMT%WXq3R`l zrCf{zQJpfjmcG{K*%W$hsfk9?y-lnn`>G7>U zFB;eB$tY|Q-mYZcSUS7hx@eI8Sam9>L?=qU=`~uLrt$wK8mnkO*0C1PiPK_UBcgAvtR5dpFQC0FZRLF5iyvFmmvaq$`=IwgzIuO4E~ib+b4e<6 z*g#l+;0Zb1u{nupT8kUV=vlbrGM(*=DHP4y@Kvy55A9Abe4tj<=^j(8#c0vNFE*!M z`>y0^a}uvX=)P4D!RtWiC~TTpsB_<3$li*7>=NDD(9|L6rQaf`RdtKwQOq$eIzMo= z1MXPw)Y#N9U&g|eNd(HHx+XDXDUFlidI@lnlf7j5M1@8xSwXL=+^r?O@c}Wr+)Zkm z+~>!&+3z~?a(M>jKPKW z!11l)%i?ES)Hp+HZfYiRG|S3i z{*pLyY2r;ISu9Yl%tU>twUoIjQd3+evi^!#qPB4#&E9uBM_2N>jL156YP+yu={wq? zR%ISr^qR82bI5phxJhpyiClEGUhuKLufXaczMxrs_4_LNTh2z@l9ydXbIB*u#lG{l zo<;FHhdz-^6?_#^!Z|h<*6Jja)=R=CaK51(+$Rjv<@X~JUCYxgZA~2fl-Un>s6Fk- zYBn*VS-4ujVYZp8_lwfWs8}D%XgjLR8vG?Jy=wXgjL>bmbWJp|1%%cDtp9Ewggd7v z{}+`2Ap4tfe@{{z03dU5SxAxshsF?ap}!UfnmlI#Rzpa#38)HrldX9MP>_(4EYv|( z1xKJSWJgd6!cojXVwT0eQo2oq`W{noHf|zMbsoH)-?ZzltD^~4U)QX~J z?6fwm)u<8w-p~KVeLdIpJmL>-RA{}X4zJ$Jec0$kRj^G*kd^K!e71^2d1;a+khNE1=!QEr$KXuQ18SiEaDW$-g zPR~q8O?t2l|IH}&SOrO7!8=*Ju6bKo40Z<&80~V^A1$bO8cPq?7M1}on_@AXHictf zyT?Cujxi}$8JYxs%C3yDn))=gP;%P?#bv{l#g%1a6`ywllpWeR-W;X-JT3?s(#^)% zqw1g4)SUH(g9DSR;<~!!?&-Nl`Gk=v97ng($-|Y&+{UkK9Z`4Zj{F@3IXT|k!W({x zWRR1XVEVh#azWTTGMe^7j_&SHT=$#|=9<}j=pQhszjssK_(@fykl-6833uFhfnqr^ z;n)BVlu}KtY=GbH_1xP7ceY&9*>*OVx3wLyH5}8ELn^R;t(E*J_Rvt02E=PaYjYo@ zQjwYL3?fhX>sbypES+3grSb-Z1JSCmaR;ICGhVyNJh~zkllf9Qj2zkQee-ic>Ny!y zSi_(EOHLIlQt9=dNsP4nWfki;R9$e?-HqjtC{N0hE1pnc|DU2*izmN-3zup7z(bzU zyt$^bB(0=Qb@wU-GqeSCEZ)nE;HuVV zIonl9jq(HZphx=TS6D!l)YKplNC1=~^2GTm7zB!-0D*Ww)Zid*zsFMcwSvs|$mmSs zS7_f*zveb@-=ypWd|0`K?$YsgOUnKoXkC-yQe$nC>he1CrLL17lj87^v7WKY|F8j^ zEmtj;wM+e~S1arR@(*`e3J5djLfPQqcr8ZAD8(s5)DldXfoA-^`LdO$(+&CfIYYe2 z^9J|K(g$VD9D(ZX60T?#snBfLxKwnjivQa1uwP){z$J;7Aee{kW}?YUc8ufoD>q(t z%UPC)=;Y41@pm*C(aJfIhk{Ktw3ClPu@~CfPow7a;>SL3U*rAPBIk*6sD?KL%f~Z6 ze?UkM`qMhUJBsRT9V}P9jF&~qlL>s#a%>M5!wiANzxC}s8{DqX;3dQ{%8MKobG@*` zp*NoRW`zq{)YzXVCma8hkN+pVU`|>*vk(V?83(nMdbIo;Rh3*LuUS0x$i^^1|M84q zZ_f9L#PZb3c(<8N-fA+O{jtSoa1YO?*Z+DA}kd6VlK(Xz^OnvOdIPpq%sg z+7GI`_7^uVLcXp^OnS{}-}*RmnPxv?4G}MV)`}pKLBmCwzVHRiZ$`JY-Ptv%+uqnO z>XWV-C8~A~Dsczd7<0>aWU7(T!xAP458n2anWr4%p%*N44!2OD#XZ z`CR+wl(F%a3%R-)FFw?!49xDHo_kC=!lvXt{F2FN7#e4t;!;c4EY%x-X>N%)Y)pKD zCm%LxZ*1IZW5JF@K6FXb-#R)vh7yxylqMBEqZpl$8;hSd2ZB!t+M|JMvAIU?wgkg< zFb#(LS0__^xA{vrB~w@y51Y}+sGU9*IP_>6jo$$0ySV-g+5RS6SH6-Y}@O)iIk!bS7_ zE!<9?DTLJ$<^X58bSbPwjHCD9#2uaR*0`a`BqV#Lc*na1?$}Ak8GV1&#IOA-_LcAx z7{dZ#bKUHWN80a+p4Z0Cw;DJ-Np7nd1(nsc~Y|+(>qYhR}qTyBB$^1 zie24fB_zE*m-M1$$)EOB2<-{xM4zcCL{#-mv8Mkz)7nD#o%`ot4o8G^ns;AbY2#P1 zgm>3#%*OQ2WynDmWfJ5)%F;;&Fl>^>tNq8Hc?_CLO1!@x-0B!U6|kG-pxre^h3!2? zUWC(+co6gcUTB>O6psy?riy*p&a)6qGtEGBJZT(7UfS%llZjmdi`3E)o42Eha}m+cr)n&C*KpJSa2#cFTFAXh$w)!sbvS&!}KzAesq2%leT!B|0ke zhZ+9jU>;R@yuGoso}74ec1lA=qNT)gf|BE38(ZSin!h5yFwxK@!G$*E!=(<0y3lscw`4<~-=CLF&ay}0ravEcBW zH?Wb3m)O`#D~>2~kz(^YLID(Ng8d8#|mLXUxD$;>I`f44To3K z568L$y}VvraXaA~l?`nFMET?UA(t$t;e#Z>ucb$?@~b=fWQU6{Z%*tw&Fr&hUQuE4&Q{=#DO`E*Z!H9eT!D z*f|LTXerzRnq#wOq`C@Cu_*66TS!MRNL9Ld>4!S)oi&2LXa-nY8 zw|*UB)I_^vy|l~>*K?DDymo^OR=b5KN=!9g_f#uB8d4jbv$*?C@3hH>NGTrc+UxIf zmOcU#!s;>CAN&qZ@b*8h7bd;VMNSaP*K{qMTk<8gZ5LDiyd%2w_oNC#vesiaWTqDf zbtJi5hET$UD&H8+%3>oD&G*?hPhIt*8mjjC=@_e2xUP&>8cem4gU)#jIsl{vRiAy3 zMmY$~;wO4Ua3Nt^e8<2>bw5>WAMX^h#Q9)9IQ0o;xIlW36DKD-0hyGA)_kafZY41> z%)r%r&r$nNQtFTJ@q)JaqsuhdrbVc^GI%>Ri9(Z&^@enhMcoZ4c(-h)$&mnv2211gH&L2EpyQU4UXKL22Q`jw&v7c~OA&!sTx7RyKCaXhw z$(L%D!6S#^mV^GTezvh{eXAWU6_x#aZ&5)`=JAI+Yd|Im+WsQc^ZV?e073WN+^5;f zD`UZl0>qf|MV(?H*R`Sh1JY9qaDBI5ZcX*WmYvqwP8GV}*=FVBj~OcC=X$Dt_2dpW z-^As-O)|~hY~kq2T9T{$zR>t^*Wz7_(hwpzZxw;%8%{XmaC#y6@CzWD@XbNnh7Sc{ zC^jAvYcqpkTa8L9OjY}76=Kk94YIfop6My1E5LocRPnnK+_SKq(e15B?TGkq&eNmZpQ{2(lJ*3u2DTeJkJ^su=xL?%R@fWd-P|^3ka`9??b0J}cJwI5C z-au$u@P`(7IL<{=7SS{NzHwA%Q62>iovR(M8mm3ng?C%pk1V`8O-zh&2y2&-zoSQo zVAB}2&c_Dqx<`AnI&&@!D_!0-=l14vZ0g~3eoIUz8TVDBD8*u-=eM7VC#LdiU~8to z+@EefJ02EH#^G1-h8-j4jzvpxl^+*eLdtAHeM6fPs;~)1?MdH$lr6T z!_BR}W!oRAV#7kFG%yLJ6*!1lwC?B*f9TfcPLi>l|9hN}(=tKX>+slb zk}Kwy0Dox1lY z2-S{EN6|AQ(;soH9j_R+xtU>IxmUian$KO2d#{FJ`HjIjC z{APX^z*SqF;`_)$Fd_i~dzI)Os1(DK1hJ)iUYopOZ~9#uPICmT(%#jQ8;z3!zC z)a_I(`=;e*wk3skba74<_kKgI+HNug8EKkJeO1?L^wkZ!<{bVxA)Ui-uDdZe=Y?8y zNzqZ<@E51*_JLfq{5N!!&M%eHc>&3NB1SXZ^e@Ws;m84x5U#ZzO`UA(+FO<$nVMD2 zC{we!nHcE{jal)}EwyR?+o9kFRLl9Yp zqKIL|KWGg4%4z&r$Tj8L#LRYLNV==_?fLL6tNFg5RVy0P>(6uctCnK6Le`$^noFo( ze#q3EJl|1BzUXlAj4-bwT(lGz?ehbawz~i3Yyc};dA|SslmFv#|CT>y2kF;ySvf0E#mWv`gNcE$fILi%?3Mr^F2Dms*fIhIFfOnt&<4{cQx*jvq9T7biGf1^ zE4zE(JRs9f6Fd(r*{Oo903Q3l!Hs~Oy*hXX$g|fZJJkU$boc>17$bnxW4!k7>5+hx zgATYHKsu;_Zvh`2AY}3;fRYI}Q1|CLmZtQ8lp`(J2~c&E=J_{c@LwJM7ZPp`JTqqn z@*KtBe?dVYsD?Kj;Q~k4n+5y0_{0A3)JOVYG8WMPueSfPE^Yr$(S4*(LHW-j_CJ;D LPpiiDpWc4}%_@4r delta 2102 zcmZ9Nc{tSj7stQTNa7mAl%*k8SwomRvNcK>k-f>-m25MHY$H;`6=g}~TgEWW5J|=` z_IW9yasmM4%#m&@ey!N@UQbi-G#>`FaPt-Ce z$7#pM$)}}8;}B`H=tcQIW@gcirLmQ$*4N!F+%B#Z%Pu}Ln zY^*L)ALw3piK9H;PRh#0f7^07X+zG!Sj59vn0DV-+vaMl?jD9X;b8{DGu=gCu7Hve zTgR?IfGc69N_M^8UtdIRP4i1wKseJ~U#=7lcWCEfQ&` zZvD8e0rCJlE2w}JASfgR0DvNJ`_)*q0|WqO1OVV5AOwjF4!x}H8Cr2f61_`w7fvTp z&+AXspkujDAeHb++uYw8x51Pzua~I9r?xP(B0LkrX-VtHp!9*M}P&**p zhp4A@l^9r+IHJ+x3swsR_qOg?d%t!4%}gB`8nuBp5WRVwMDNE7WY@2xM=4 zC%(w_akt=KQqzvJrW=I$Cd#IVZfzRQAUtv9hdxkKMO|l$+c&6s6JHUNVdmsh=5L`I z1zjD~4jvtGm$s5vL*u7OOfHL>m7I;56}u3axhy9`*l3)#<%gT8Po~Gu(U#(Z&EDD` zrLH_rgu7laE}y2UY0u=}s?(fXUZO&>g@0wlv76aaEk?>?Y{idpk8r+!Tzzx*=kwT# z_^F#W6`QX(zl%QeHZR5_J-Le9X&I2qD%#IzAG)s#j~RK6|9OqWmL#5`muBjgr>eN= z__pV8m2bLpPkns(IXAQFni|o}c$_^)-yK$M++kV1i3}lqL2<+p3<7hgBsZ;WGk7_Z z^ND!|n{Vha(DS;kuCsA)qljBRZ9O6K*|`IZZUbk^@lkCwgh-j@eR79HHd5-r)hCKH zBM&F#H6iSiKE*6ykI3C8?v;6UV-`qcxreH=pweS!NicUpUr^#vUWxVfty?BCqZtwPyk2reckAbiF*qtVUtW(J0sGc5K#hJSXLl1J<-;;_C+U`H4A5l#^U z#hr&9Yjro}u$nd8crp<~$3^vfUg9YZ4QW(#m03?_$3pal5A^ef`C_^2-U?Q}|2{Z3vK)E?K8Uq8K2?Xs~z-%NgSA)ZrP-y-d) z-z2PCc8BF!$TBIo(sR8<+@Z#?uD)fSnQh2~2g@tBO&H>3v~40$DdHZE*3u>8rP>_4 z0;;UUxrtI&>`pInmidYAy3DmW;56K~G_CA6D(^+cY%EVLuIHvkAoTkqS34iQQP(6T zO|8zGsZQR=I9^(?h)WU2a>m9wO|XwncKH=ww}~P-C;m9bP4FI<{YiKkLGQ2mr}|m~ zJv)5l)uvb9I_1RsMwQqdY8=3-CaV(b|t^#$$D+L1^DR%4or(}l$^ zmdayf-MPu>)R9N#_VdwFioEgnb$H2O1oMQ}Ag74B0jnrv!q)oTpJ}W=)uHV>%lJ$B zjT~&vX!2)%E$^F-eU;k0QNcUHf!0Bj*`az=Pa3}9dX4Z4eQzOtFE+11i~o(Uv&vXc z;(1y{H9Y%-UhKMJ=~j@urS!=It>CS2Kayn;vQjObw)QM?+{`UYZ()w=I2W zYOZHtCr!VLM_3b&^6BnT4Wi{j_OlIK)GtCSxpZwbvsqI$yZ4uthu?Nqy9IYj_iw-9-2foy HwiEpq*?-JG diff --git a/src/Apps/W1/PaymentPractices/App/src/Reports/PaymentPractice.Report.al b/src/Apps/W1/PaymentPractices/App/src/Reports/PaymentPractice.Report.al index 26b02209ed..310eda630e 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Reports/PaymentPractice.Report.al +++ b/src/Apps/W1/PaymentPractices/App/src/Reports/PaymentPractice.Report.al @@ -29,6 +29,22 @@ report 685 "Payment Practice" column(Average_Actual_Payment_Period_Caption; FieldCaption("Average Actual Payment Period")) { } column(Pct_Paid_on_Time; "Pct Paid on Time") { } column(Pct_Paid_on_Time_Caption; FieldCaption("Pct Paid on Time")) { } + column(Mode_Payment_Time; "Mode Payment Time") { } + column(Mode_Payment_Time_Caption; FieldCaption("Mode Payment Time")) { } + column(Mode_Payment_Time_Min; "Mode Payment Time Min.") { } + column(Mode_Payment_Time_Min_Caption; FieldCaption("Mode Payment Time Min.")) { } + column(Mode_Payment_Time_Max; "Mode Payment Time Max.") { } + column(Mode_Payment_Time_Max_Caption; FieldCaption("Mode Payment Time Max.")) { } + column(Median_Payment_Time; "Median Payment Time") { } + column(Median_Payment_Time_Caption; FieldCaption("Median Payment Time")) { } + column(Percentile_80th_Payment_Time; "80th Percentile Payment Time") { } + column(Percentile_80th_Payment_Time_Caption; FieldCaption("80th Percentile Payment Time")) { } + column(Percentile_95th_Payment_Time; "95th Percentile Payment Time") { } + column(Percentile_95th_Payment_Time_Caption; FieldCaption("95th Percentile Payment Time")) { } + column(Pct_Peppol_Enabled; "Pct Peppol Enabled") { } + column(Pct_Peppol_Enabled_Caption; FieldCaption("Pct Peppol Enabled")) { } + column(Pct_Small_Business_Payments; "Pct Small Business Payments") { } + column(Pct_Small_Business_Payments_Caption; FieldCaption("Pct Small Business Payments")) { } dataitem(PaymentPracticeLine; "Payment Practice Line") { @@ -67,6 +83,13 @@ report 685 "Payment Practice" Summary = 'Payment Practice by Period'; LayoutFile = 'src/Reports/Payment Practice by Period.docx'; } + layout(PaymentPractice_SmallBusinessLayout) + { + Type = Word; + Caption = 'Payment Practice Small Business'; + Summary = 'Payment Practice Small Business'; + LayoutFile = 'src/Reports/Payment Practice Small Business.docx'; + } layout(PaymentPractice_VendorSizeLayout) { Type = Word; @@ -75,4 +98,4 @@ report 685 "Payment Practice" LayoutFile = 'src/Reports/Payment Practice by Vendor Size.docx'; } } -} +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al index 1db7f8166f..b01503de5c 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPeriod.Table.al @@ -87,6 +87,8 @@ table 685 "Payment Period" InsertDefaultPeriods_GB(); 'FR': InsertDefaultPeriods_FR(); + 'AU', 'NZ': + InsertDefaultPeriods_AUNZ(); else InsertDefaultPeriods(); end; @@ -140,9 +142,15 @@ table 685 "Payment Period" InsertPeriod('P121+', 121, 0); end; + local procedure InsertDefaultPeriods_AUNZ() + begin + InsertPeriod('P0_30', 0, 30); + InsertPeriod('P31_60', 31, 60); + InsertPeriod('P61+', 61, 0); + end; + [IntegrationEvent(false, false)] local procedure OnBeforeSetupDefaults(var IsHandled: Boolean) begin end; } - diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al index 18177c5dd7..cfa424a9f4 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al @@ -134,6 +134,84 @@ table 687 "Payment Practice Header" AutoFormatType = 0; ToolTip = 'Specifies the percentage of payments not made within agreed terms that are due to disputes.'; } + field(24; "Mode Payment Time"; Integer) + { + ToolTip = 'Specifies the mode payment time.'; + + trigger OnValidate() + begin + Rec."Modified Manually" := true; + end; + } + field(25; "Mode Payment Time Min."; Integer) + { + ToolTip = 'Specifies the minimum per vendor mode payment time.'; + + trigger OnValidate() + begin + Rec."Modified Manually" := true; + end; + } + field(26; "Mode Payment Time Max."; Integer) + { + ToolTip = 'Specifies the maximum per vendor mode payment time.'; + + trigger OnValidate() + begin + Rec."Modified Manually" := true; + end; + } + field(27; "Median Payment Time"; Decimal) + { + AutoFormatType = 0; + DecimalPlaces = 2; + ToolTip = 'Specifies the median payment time.'; + + trigger OnValidate() + begin + Rec."Modified Manually" := true; + end; + } + field(28; "80th Percentile Payment Time"; Integer) + { + ToolTip = 'Specifies the 80th percentile payment time.'; + + trigger OnValidate() + begin + Rec."Modified Manually" := true; + end; + } + field(29; "95th Percentile Payment Time"; Integer) + { + ToolTip = 'Specifies the 95th percentile payment time.'; + + trigger OnValidate() + begin + Rec."Modified Manually" := true; + end; + } + field(30; "Pct Peppol Enabled"; Decimal) + { + AutoFormatType = 0; + DecimalPlaces = 2; + ToolTip = 'Specifies the percentage of invoices that are PEPPOL enabled.'; + + trigger OnValidate() + begin + Rec."Modified Manually" := true; + end; + } + field(31; "Pct Small Business Payments"; Decimal) + { + AutoFormatType = 0; + DecimalPlaces = 2; + ToolTip = 'Specifies small business payments as a percentage of total payments. This includes the value of partial payments.'; + + trigger OnValidate() + begin + Rec."Modified Manually" := true; + end; + } } keys @@ -206,4 +284,4 @@ table 687 "Payment Practice Header" "Reporting Scheme" := PaymentPeriodMgt.DetectReportingScheme(); end; -} +} \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al b/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al index 3276733227..c8818b05c1 100644 --- a/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al @@ -53,12 +53,18 @@ codeunit 134196 "Payment Practices Library" end; procedure CreateCompanySizeCode(): Code[20] + begin + exit(CreateCompanySizeCode(false)); + end; + + procedure CreateCompanySizeCode(IsSmallBusiness: Boolean): Code[20] var CompanySize: Record "Company Size"; begin CompanySize.Init(); CompanySize.Code := LibraryUtility.GenerateGUID(); CompanySize.Description := CompanySize.Code; + CompanySize."Small Business" := IsSmallBusiness; CompanySize.Insert(); exit(CompanySize.Code); end; @@ -179,7 +185,6 @@ codeunit 134196 "Payment Practices Library" PaymentPracticeHeader.DeleteAll(); end; - procedure CreatePaymentPracticeHeaderWithScheme(var PaymentPracticeHeader: Record "Payment Practice Header"; HeaderType: Enum "Paym. Prac. Header Type"; AggregationType: Enum "Paym. Prac. Aggregation Type"; ReportingScheme: Enum "Paym. Prac. Reporting Scheme") begin PaymentPracticeHeader.Init(); diff --git a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al index 6b9a6ecf46..69ed3708b1 100644 --- a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al @@ -7,6 +7,7 @@ namespace Microsoft.Test.Finance.Analysis; using Microsoft.Finance.Analysis; +using Microsoft.Purchases.Vendor; using Microsoft.Sales.Customer; using System.Environment; @@ -882,6 +883,441 @@ codeunit 134197 "Payment Practices UT" VerifyAllLinesInvoiceCountAndValueZero(PaymentPracticeHeader."No."); end; + [Test] + procedure ModePaymentTimeCalculation() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + VendorNo: Code[20]; + CompanySizeCode: Code[20]; + begin + // [SCENARIO] Check mode payment time calculation in header + Initialize(); + + // [GIVEN] Create a vendor + CompanySizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + + // [GIVEN] Create a payment practice header with Extra Fields enabled + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [GIVEN] Post entries with payment times: 5, 5, 5, 10, 10, 15 (mode = 5) + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 5); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 5); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 5); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 10); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 10); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 15); + + // [WHEN] Lines were generated for Header + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Mode Payment Time = 5 + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + Assert.AreEqual(5, PaymentPracticeHeader."Mode Payment Time", 'Mode Payment Time is not equal to expected.'); + end; + + [Test] + procedure ModePaymentTimeMinCalculation() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + CompanySizeCode: Code[20]; + VendorNo1: Code[20]; + VendorNo2: Code[20]; + begin + // [SCENARIO] Check mode payment time min is the minimum of per-vendor modes + Initialize(); + + // [GIVEN] Create two vendors with small business company size + CompanySizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + VendorNo1 := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + VendorNo2 := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + + // [GIVEN] Create a payment practice header with Extra Fields enabled + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [GIVEN] Vendor 1: payment times 5, 5, 10 (mode = 5) + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo1, WorkDate(), WorkDate(), WorkDate() + 5); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo1, WorkDate(), WorkDate(), WorkDate() + 5); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo1, WorkDate(), WorkDate(), WorkDate() + 10); + + // [GIVEN] Vendor 2: payment times 8, 8, 12 (mode = 8) + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo2, WorkDate(), WorkDate(), WorkDate() + 8); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo2, WorkDate(), WorkDate(), WorkDate() + 8); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo2, WorkDate(), WorkDate(), WorkDate() + 12); + + // [WHEN] Lines were generated for Header + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Mode Payment Time Min = 5 (minimum of per-vendor modes 5 and 8) + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + Assert.AreEqual(5, PaymentPracticeHeader."Mode Payment Time Min.", 'Mode Payment Time Min. is not equal to expected.'); + end; + + [Test] + procedure ModePaymentTimeMaxCalculation() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + CompanySizeCode: Code[20]; + VendorNo1: Code[20]; + VendorNo2: Code[20]; + begin + // [SCENARIO] Check mode payment time max is the maximum of per-vendor modes + Initialize(); + + // [GIVEN] Create two vendors with small business company size + CompanySizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + VendorNo1 := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + VendorNo2 := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + + // [GIVEN] Create a payment practice header with Extra Fields enabled + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [GIVEN] Vendor 1: payment times 5, 5, 10 (mode = 5) + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo1, WorkDate(), WorkDate(), WorkDate() + 5); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo1, WorkDate(), WorkDate(), WorkDate() + 5); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo1, WorkDate(), WorkDate(), WorkDate() + 10); + + // [GIVEN] Vendor 2: payment times 8, 8, 12 (mode = 8) + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo2, WorkDate(), WorkDate(), WorkDate() + 8); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo2, WorkDate(), WorkDate(), WorkDate() + 8); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo2, WorkDate(), WorkDate(), WorkDate() + 12); + + // [WHEN] Lines were generated for Header + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Mode Payment Time Max = 8 (maximum of per-vendor modes 5 and 8) + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + Assert.AreEqual(8, PaymentPracticeHeader."Mode Payment Time Max.", 'Mode Payment Time Max. is not equal to expected.'); + end; + + [Test] + procedure MedianPaymentTimeCalculation() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + CompanySizeCode: Code[20]; + VendorNo: Code[20]; + begin + // [SCENARIO] Check median payment time calculation with odd number of entries + Initialize(); + + // [GIVEN] Create a vendor with small business company size + CompanySizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + + // [GIVEN] Create a payment practice header with Extra Fields enabled + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [GIVEN] Post entries with payment times: 3, 7, 5, 11, 9 (sorted: 3, 5, 7, 9, 11; median = 7) + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 3); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 7); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 5); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 11); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 9); + + // [WHEN] Lines were generated for Header + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Median Payment Time = 7 + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + Assert.AreEqual(7, PaymentPracticeHeader."Median Payment Time", 'Median Payment Time is not equal to expected.'); + end; + + [Test] + procedure MedianPaymentTimeCalculation_EvenCount() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + CompanySizeCode: Code[20]; + VendorNo: Code[20]; + begin + // [SCENARIO] Check median payment time calculation with even number of entries + Initialize(); + + // [GIVEN] Create a vendor with small business company size + CompanySizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + + // [GIVEN] Create a payment practice header with Extra Fields enabled + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [GIVEN] Post entries with payment times: 4, 10, 2, 8 (sorted: 2, 4, 8, 10; median = (4 + 8) / 2 = 6) + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 4); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 10); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 2); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + 8); + + // [WHEN] Lines were generated for Header + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Median Payment Time = 6 (average of two middle values) + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + Assert.AreEqual(6, PaymentPracticeHeader."Median Payment Time", 'Median Payment Time is not equal to expected.'); + end; + + [Test] + procedure Percentile80thPaymentTimeCalculation() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + CompanySizeCode: Code[20]; + VendorNo: Code[20]; + i: Integer; + begin + // [SCENARIO] Check 80th percentile payment time calculation + Initialize(); + + // [GIVEN] Create a vendor with small business company size + CompanySizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + + // [GIVEN] Create a payment practice header with Extra Fields enabled + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [GIVEN] Post 10 entries with payment times 1 through 10 + for i := 1 to 10 do + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + i); + + // [WHEN] Lines were generated for Header + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] 80th percentile = 8 (index = 10 * 80 div 100 = 8) + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + Assert.AreEqual(8, PaymentPracticeHeader."80th Percentile Payment Time", '80th Percentile Payment Time is not equal to expected.'); + end; + + [Test] + procedure Percentile95thPaymentTimeCalculation() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + CompanySizeCode: Code[20]; + VendorNo: Code[20]; + i: Integer; + begin + // [SCENARIO] Check 95th percentile payment time calculation + Initialize(); + + // [GIVEN] Create a vendor with small business company size + CompanySizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + + // [GIVEN] Create a payment practice header with Extra Fields enabled + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [GIVEN] Post 20 entries with payment times 1 through 20 + for i := 1 to 20 do + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + i); + + // [WHEN] Lines were generated for Header + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] 95th percentile = 19 (index = 20 * 95 div 100 = 19) + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + Assert.AreEqual(19, PaymentPracticeHeader."95th Percentile Payment Time", '95th Percentile Payment Time is not equal to expected.'); + end; + + [Test] + procedure Percentile80thPaymentTime_FractionalIndex() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + CompanySizeCode: Code[20]; + VendorNo: Code[20]; + i: Integer; + begin + // [SCENARIO] Check 80th percentile when index is not a whole number (7 * 80 / 100 = 5.6, truncated to 5) + Initialize(); + + // [GIVEN] Create a vendor with small business company size + CompanySizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + + // [GIVEN] Create a payment practice header with Extra Fields enabled + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [GIVEN] Post 7 entries with payment times 1 through 7 + for i := 1 to 7 do + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + i); + + // [WHEN] Lines were generated for Header + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] 80th percentile = 5 (index = 7 * 80 div 100 = 5, no interpolation) + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + Assert.AreEqual(5, PaymentPracticeHeader."80th Percentile Payment Time", '80th Percentile Payment Time is not equal to expected.'); + end; + + [Test] + procedure Percentile95thPaymentTime_FractionalIndex() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + CompanySizeCode: Code[20]; + VendorNo: Code[20]; + i: Integer; + begin + // [SCENARIO] Check 95th percentile when index is not a whole number (13 * 95 / 100 = 12.35, truncated to 12) + Initialize(); + + // [GIVEN] Create a vendor with small business company size + CompanySizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + + // [GIVEN] Create a payment practice header with Extra Fields enabled + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [GIVEN] Post 13 entries with payment times 1 through 13 + for i := 1 to 13 do + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorNo, WorkDate(), WorkDate(), WorkDate() + i); + + // [WHEN] Lines were generated for Header + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] 95th percentile = 12 (index = 13 * 95 div 100 = 12, no interpolation) + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + Assert.AreEqual(12, PaymentPracticeHeader."95th Percentile Payment Time", '95th Percentile Payment Time is not equal to expected.'); + end; + + [Test] + procedure PctPeppolEnabledCalculation() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + Vendor: Record Vendor; + CompanySizeCode: Code[20]; + VendorWithGLN: Code[20]; + VendorWithoutGLN: Code[20]; + PeppolInvoiceCount: Integer; + NonPeppolInvoiceCount: Integer; + ExpectedPctPeppol: Decimal; + i: Integer; + begin + // [SCENARIO] Check Pct Peppol Enabled calculation with one vendor that has GLN and one that does not. + Initialize(); + + // [GIVEN] Create a company size marked as small business + CompanySizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + + // [GIVEN] Create a vendor with a GLN value (Peppol enabled) and small business company size + VendorWithGLN := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + Vendor.Get(VendorWithGLN); + Vendor.GLN := '1234567890123'; + Vendor.Modify(); + + // [GIVEN] Create a vendor without a GLN value (not Peppol enabled) with small business company size + VendorWithoutGLN := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCode, false); + + // [GIVEN] Create a payment practice header with Extra Fields enabled + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [GIVEN] Post 3 paid invoices for the GLN vendor + PeppolInvoiceCount := 3; + for i := 1 to PeppolInvoiceCount do + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorWithGLN, WorkDate(), WorkDate(), WorkDate() + 5); + + // [GIVEN] Post 2 paid invoices for the non-GLN vendor + NonPeppolInvoiceCount := 2; + for i := 1 to NonPeppolInvoiceCount do + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(VendorWithoutGLN, WorkDate(), WorkDate(), WorkDate() + 10); + + // [WHEN] Generate payment practices + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Pct Peppol Enabled = 3 / 5 * 100 = 60 (percentage of invoices from vendors with GLN) + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + ExpectedPctPeppol := PeppolInvoiceCount / (PeppolInvoiceCount + NonPeppolInvoiceCount) * 100; + Assert.AreNearlyEqual(ExpectedPctPeppol, PaymentPracticeHeader."Pct Peppol Enabled", 0.01, 'Pct Peppol Enabled is not equal to expected.'); + end; + + [Test] + procedure OnlySmallBusinesses_StatisticsAndPctSmallBusinessPayments() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + SmallBizSizeCode: Code[20]; + NonSmallBizSizeCode: Code[20]; + SmallBizVendor1: Code[20]; + SmallBizVendor2: Code[20]; + NonSmallBizVendor1: Code[20]; + NonSmallBizVendor2: Code[20]; + begin + // [SCENARIO] Generate payment practices with "Only Small Businesses" enabled. Only small business vendors should be included in the statistics (median, mode, percentiles) . + Initialize(); + + // [GIVEN] Create a company size marked as "Small Business" and one that is not + SmallBizSizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(true); + NonSmallBizSizeCode := PaymentPracticesLibrary.CreateCompanySizeCode(false); + + // [GIVEN] Create 2 vendors with the small business company size + SmallBizVendor1 := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(SmallBizSizeCode, false); + SmallBizVendor2 := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(SmallBizSizeCode, false); + + // [GIVEN] Create 2 vendors with the non-small business company size + NonSmallBizVendor1 := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(NonSmallBizSizeCode, false); + NonSmallBizVendor2 := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(NonSmallBizSizeCode, false); + + // [GIVEN] Post paid invoices for small business vendor 1 with payment times 5, 5, 10 + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(SmallBizVendor1, WorkDate(), WorkDate(), WorkDate() + 5); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(SmallBizVendor1, WorkDate(), WorkDate(), WorkDate() + 5); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(SmallBizVendor1, WorkDate(), WorkDate(), WorkDate() + 10); + + // [GIVEN] Post paid invoices for small business vendor 2 with payment times 8, 8 + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(SmallBizVendor2, WorkDate(), WorkDate(), WorkDate() + 8); + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(SmallBizVendor2, WorkDate(), WorkDate(), WorkDate() + 8); + + // [GIVEN] Post paid invoices for non-small business vendor 1 with payment time 20 + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(NonSmallBizVendor1, WorkDate(), WorkDate(), WorkDate() + 20); + + // [GIVEN] Post paid invoices for non-small business vendor 2 with payment time 30 + PaymentPracticesLibrary.MockVendorInvoiceAndPayment(NonSmallBizVendor2, WorkDate(), WorkDate(), WorkDate() + 30); + + // [GIVEN] Create a payment practice header with "Small Businesses" reporting scheme + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader, "Paym. Prac. Header Type"::Vendor, "Paym. Prac. Aggregation Type"::Period); + PaymentPracticeHeader."Reporting Scheme" := PaymentPracticeHeader."Reporting Scheme"::"Small Business"; + PaymentPracticeHeader.Modify(); + + // [WHEN] Generate payment practices + PaymentPractices.Generate(PaymentPracticeHeader); + + // [THEN] Only 5 entries should be in the buffer (only small business vendors) + PaymentPracticesLibrary.VerifyBufferCount(PaymentPracticeHeader, 5, "Paym. Prac. Header Type"::Vendor); + + // [THEN] Check header statistics - computed only from small business vendor data + // Payment times: 5, 5, 8, 8, 10 (sorted) + PaymentPracticeHeader.Get(PaymentPracticeHeader."No."); + + // Mode = 5 (most frequent across all data: 5 appears twice, 8 appears twice - tie broken by smallest) + Assert.AreEqual(5, PaymentPracticeHeader."Mode Payment Time", 'Mode Payment Time is not equal to expected.'); + + // Mode Min = minimum of per-vendor modes: vendor1 mode = 5, vendor2 mode = 8 → min = 5 + Assert.AreEqual(5, PaymentPracticeHeader."Mode Payment Time Min.", 'Mode Payment Time Min. is not equal to expected.'); + + // Mode Max = maximum of per-vendor modes: vendor1 mode = 5, vendor2 mode = 8 → max = 8 + Assert.AreEqual(8, PaymentPracticeHeader."Mode Payment Time Max.", 'Mode Payment Time Max. is not equal to expected.'); + + // Median of 5, 5, 8, 8, 10 (odd count = 5) → middle value = 8 + Assert.AreEqual(8, PaymentPracticeHeader."Median Payment Time", 'Median Payment Time is not equal to expected.'); + + // 80th percentile: index = 5 * 80 div 100 = 4 → sorted[4] = 8 + Assert.AreEqual(8, PaymentPracticeHeader."80th Percentile Payment Time", '80th Percentile Payment Time is not equal to expected.'); + + // 95th percentile: index = 5 * 95 div 100 = 4 → sorted[4] = 8 + Assert.AreEqual(8, PaymentPracticeHeader."95th Percentile Payment Time", '95th Percentile Payment Time is not equal to expected.'); + end; + local procedure Initialize() begin LibraryTestInitialize.OnTestInitialize(Codeunit::"Payment Practices UT"); From ed429cb58d0e2c9cd0f9bcbbf78a0638bf07de80 Mon Sep 17 00:00:00 2001 From: Aleksandr Gladkov Date: Wed, 13 May 2026 18:05:35 +0200 Subject: [PATCH 04/12] fix review comments --- .../PaymPracPeriodAggregator.Codeunit.al | 8 ++---- .../PaymPracSizeAggregator.Codeunit.al | 4 +-- .../PaymPracSmallBusHandler.Codeunit.al | 18 +++++-------- ...aymentPracticeLinesAggregator.Interface.al | 5 ++-- .../App/src/Core/PaymentPeriodMgt.Codeunit.al | 4 +++ .../src/Core/PaymentPracticeMath.Codeunit.al | 14 +++++----- .../App/src/Core/PaymentPractices.Codeunit.al | 5 ++-- .../Core/UpgradePaymentPractices.Codeunit.al | 27 +++++++++++++++---- .../src/Pages/PaymPracVendLedgEntr.PageExt.al | 25 ----------------- .../App/src/Pages/PaymentPracticeCard.Page.al | 10 ++++++- .../src/Pages/PaymentPracticeDataList.Page.al | 9 +++++-- .../src/Tables/PaymentPracticeHeader.Table.al | 1 + .../src/PaymentPracticesLibrary.Codeunit.al | 2 -- .../Test/src/PaymentPracticesUT.Codeunit.al | 24 ++++++++--------- 14 files changed, 76 insertions(+), 80 deletions(-) diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al index c5ead4c9d5..5f4fa8b16e 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al @@ -14,15 +14,11 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr var FeatureTelemetry: Codeunit "Feature Telemetry"; - procedure PrepareLayout(ReportingScheme: Enum "Paym. Prac. Reporting Scheme"); + procedure PrepareLayout(); var DesignTimeReportSelection: Codeunit "Design-time Report Selection"; begin - - if ReportingScheme = "Paym. Prac. Reporting Scheme"::"Small Business" then - DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_SmallBusinessLayout') - else - DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_PeriodLayout'); + DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_PeriodLayout'); FeatureTelemetry.LogUsage('0000KSU', 'Payment Practices', 'Period layout used.') end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al index a68f781640..750c2b8352 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSizeAggregator.Codeunit.al @@ -17,7 +17,7 @@ codeunit 686 "Paym. Prac. Size Aggregator" implements PaymentPracticeLinesAggreg WrongHeaderTypeErr: Label 'Payment Practice Header Type must be Vendor for this aggregation type.'; WrongHeaderAggErr: Label 'Payment Practice Aggregation Type must be Period for the Small Business reporting scheme.'; - procedure PrepareLayout(ReportingScheme: Enum "Paym. Prac. Reporting Scheme"); + procedure PrepareLayout(); var DesignTimeReportSelection: Codeunit "Design-time Report Selection"; begin @@ -61,7 +61,7 @@ codeunit 686 "Paym. Prac. Size Aggregator" implements PaymentPracticeLinesAggreg begin if PaymentPracticeHeader."Header Type" in [PaymentPracticeHeader."Header Type"::Customer, PaymentPracticeHeader."Header Type"::"Vendor+Customer"] then Error(WrongHeaderTypeErr); - if PaymentPracticeHeader."Aggregation Type" = PaymentPracticeHeader."Aggregation Type"::"Company Size" then + if PaymentPracticeHeader."Reporting Scheme" = PaymentPracticeHeader."Reporting Scheme"::"Small Business" then Error(WrongHeaderAggErr); end; } \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al index f071d63512..e7da3b4cfd 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al @@ -17,7 +17,7 @@ codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHa procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") begin - if PaymentPracticeHeader."Header Type" in [PaymentPracticeHeader."Header Type"::Customer, PaymentPracticeHeader."Header Type"::"Vendor+Customer"] then + if PaymentPracticeHeader."Header Type" <> PaymentPracticeHeader."Header Type"::Vendor then Error(WrongHeaderTypeErr); if PaymentPracticeHeader."Aggregation Type" = PaymentPracticeHeader."Aggregation Type"::"Company Size" then Error(WrongHeaderAggErr); @@ -47,11 +47,9 @@ codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHa TotalValue: Decimal; begin PaymentPracticeData.SetRange("Invoice Is Open", false); - if PaymentPracticeData.FindSet() then - repeat - TotalCount += 1; - TotalValue += PaymentPracticeData."Invoice Amount"; - until PaymentPracticeData.Next() = 0; + TotalCount := PaymentPracticeData.Count(); + PaymentPracticeData.CalcSums("Invoice Amount"); + TotalValue := PaymentPracticeData."Invoice Amount"; PaymentPracticeData.SetRange("Invoice Is Open"); PaymentPracticeHeader."Total Number of Payments" := TotalCount; @@ -72,11 +70,9 @@ codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHa InvoiceValue: Decimal; begin PaymentPracticeData.SetRange("Invoice Is Open", false); - if PaymentPracticeData.FindSet() then - repeat - InvoiceCount += 1; - InvoiceValue += PaymentPracticeData."Invoice Amount"; - until PaymentPracticeData.Next() = 0; + InvoiceCount := PaymentPracticeData.Count(); + PaymentPracticeData.CalcSums("Invoice Amount"); + InvoiceValue := PaymentPracticeData."Invoice Amount"; PaymentPracticeData.SetRange("Invoice Is Open"); PaymentPracticeLine."Invoice Count" := InvoiceCount; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeLinesAggregator.Interface.al b/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeLinesAggregator.Interface.al index 18b879db29..4484e3bc06 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeLinesAggregator.Interface.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeLinesAggregator.Interface.al @@ -9,8 +9,7 @@ interface PaymentPracticeLinesAggregator ///

    /// Prepare the layout to be used for Payment Practice report that is suitable for the aggregation type of the header/lines. /// - /// The reporting scheme to be used for the layout. - procedure PrepareLayout(ReportingScheme: Enum "Paym. Prac. Reporting Scheme") + procedure PrepareLayout() /// /// Generate the lines for the Payment Practice report based on the Payment Practice Data raw data and Payment Practice Header fields. @@ -24,4 +23,4 @@ interface PaymentPracticeLinesAggregator /// /// The document header that needs checking. procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header") -} \ No newline at end of file +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al index 9807f45823..de3c08396a 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al @@ -9,6 +9,8 @@ using System.Environment; codeunit 695 "Payment Period Mgt." { Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; procedure DetectReportingScheme(): Enum "Paym. Prac. Reporting Scheme" var @@ -23,6 +25,8 @@ codeunit 695 "Payment Period Mgt." case EnvironmentInformation.GetApplicationFamily() of 'GB': exit("Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + 'AU', 'NZ': + exit("Paym. Prac. Reporting Scheme"::"Small Business"); else exit("Paym. Prac. Reporting Scheme"::Standard); end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al index 366049e617..6f7aec3ef7 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al @@ -159,6 +159,7 @@ codeunit 693 "Payment Practice Math" Total += 1; if not VendorGLNCache.Get(PaymentPracticeData."CV No.", HasGLN) then begin + Vendor.SetLoadFields(GLN); HasGLN := Vendor.Get(PaymentPracticeData."CV No.") and (Vendor.GLN <> ''); VendorGLNCache.Add(PaymentPracticeData."CV No.", HasGLN); end; @@ -182,11 +183,13 @@ codeunit 693 "Payment Practice Math" TotalAmountSmallBusinesses: Decimal; TotalAmountAllVendors: Decimal; begin + VendorLedgerEntry.SetLoadFields("Vendor No.", "Document Type", "Posting Date", Amount, "Remaining Amount"); VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); VendorLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); + VendorLedgerEntry.SetAutoCalcFields(Amount, "Remaining Amount"); if VendorLedgerEntry.FindSet() then repeat - VendorLedgerEntry.CalcFields(Amount, "Remaining Amount"); + Vendor.SetLoadFields("Company Size Code"); Vendor.Get(VendorLedgerEntry."Vendor No."); if CompanySize.Get(Vendor."Company Size Code") then if CompanySize."Small Business" then @@ -255,19 +258,14 @@ codeunit 693 "Payment Practice Math" var Value: Integer; MinValue: Integer; - IsFirst: Boolean; begin if List.Count() = 0 then exit(0); - IsFirst := true; + MinValue := List.Get(1); foreach Value in List do - if IsFirst then begin + if Value < MinValue then MinValue := Value; - IsFirst := false; - end else - if Value < MinValue then - MinValue := Value; exit(MinValue); end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al index a32177449b..f6e8d14ca3 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al @@ -28,9 +28,6 @@ codeunit 689 "Payment Practices" PaymentPracticeHeader."Generated On" := CurrentDateTime(); PaymentPracticeHeader."Generated By" := CopyStr(UserId(), 1, MaxStrLen(PaymentPracticeHeader."Generated By")); GenerateTotals(PaymentPracticeData, PaymentPracticeHeader); - PaymentPracticeData.Reset(); - PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); - SchemeHandler.CalculateHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); GenerateLines(PaymentPracticeHeader."Aggregation Type", PaymentPracticeData, PaymentPracticeHeader); PaymentPracticeHeader."Modified Manually" := false; PaymentPracticeHeader.Modify(false); @@ -54,6 +51,8 @@ codeunit 689 "Payment Practices" PaymentPracticeHeader."Pct Peppol Enabled" := 0; PaymentPracticeHeader."Pct Small Business Payments" := 0; + PaymentPracticeData.Reset(); + PaymentPracticeData.SetRange("Header No.", PaymentPracticeHeader."No."); SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; SchemeHandler.CalculateHeaderTotals(PaymentPracticeHeader, PaymentPracticeData); end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al index 6d8849d42f..81d3eca4c6 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al @@ -4,11 +4,16 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; +using System.Upgrade; + codeunit 683 "Upgrade Payment Practices" { Access = Internal; Subtype = Upgrade; + var + UpgradeTag: Codeunit "Upgrade Tag"; + trigger OnUpgradePerCompany() begin BackfillReportingScheme(); @@ -20,13 +25,25 @@ codeunit 683 "Upgrade Payment Practices" PaymentPeriodMgt: Codeunit "Payment Period Mgt."; ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; begin + if UpgradeTag.HasUpgradeTag(GetReportingSchemeUpgradeTag()) then + exit; + ReportingScheme := PaymentPeriodMgt.DetectReportingScheme(); PaymentPracticeHeader.SetRange("Reporting Scheme", 0); - if PaymentPracticeHeader.FindSet() then - repeat - PaymentPracticeHeader."Reporting Scheme" := ReportingScheme; - PaymentPracticeHeader.Modify(); - until PaymentPracticeHeader.Next() = 0; + PaymentPracticeHeader.ModifyAll("Reporting Scheme", ReportingScheme); + + UpgradeTag.SetUpgradeTag(GetReportingSchemeUpgradeTag()); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", 'OnGetPerCompanyUpgradeTags', '', false, false)] + local procedure RegisterPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]]) + begin + PerCompanyUpgradeTags.Add(GetReportingSchemeUpgradeTag()); + end; + + procedure GetReportingSchemeUpgradeTag(): Code[250] + begin + exit('MS-597313-PaymPracReportingScheme-20260513'); end; } diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al index 14bbdf7120..2221689cc0 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymPracVendLedgEntr.PageExt.al @@ -5,7 +5,6 @@ namespace Microsoft.Finance.Analysis; using Microsoft.Purchases.Payables; -using Microsoft.Purchases.Vendor; pageextension 681 "Paym. Prac. Vend. Ledg. Entr." extends "Vendor Ledger Entries" { @@ -19,29 +18,5 @@ pageextension 681 "Paym. Prac. Vend. Ledg. Entr." extends "Vendor Ledger Entries Visible = false; } } - addafter("Vendor No.") - { - field(SmallBusiness; IsSmallBusiness) - { - ApplicationArea = All; - Caption = 'Small Business'; - ToolTip = 'Specifies whether the vendor is classified as a small business.'; - Editable = false; - } - } } - - trigger OnAfterGetRecord() - var - Vendor: Record Vendor; - CompanySize: Record "Company Size"; - begin - IsSmallBusiness := false; - if Vendor.Get(Rec."Vendor No.") then - if CompanySize.Get(Vendor."Company Size Code") then - IsSmallBusiness := CompanySize."Small Business"; - end; - - var - IsSmallBusiness: Boolean; } \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al index c10d4bb2f4..8b841d4d93 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al @@ -4,6 +4,7 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; +using Microsoft.Foundation.Reporting; using Microsoft.Purchases.Payables; using System.Telemetry; using System.Utilities; @@ -28,6 +29,7 @@ page 687 "Payment Practice Card" } field("Reporting Scheme"; Rec."Reporting Scheme") { + Visible = false; } field("Aggregation Type"; Rec."Aggregation Type") { @@ -253,8 +255,14 @@ page 687 "Payment Practice Card" NoEntriesFoundMsg: Label 'The payment practice generator found no entries corresponding to the header type, starting and ending date.'; local procedure PrepareLayout(PaymentPracticeLinesAggregator: Interface PaymentPracticeLinesAggregator; ReportingScheme: Enum "Paym. Prac. Reporting Scheme") + var + DesignTimeReportSelection: Codeunit "Design-time Report Selection"; begin - PaymentPracticeLinesAggregator.PrepareLayout(ReportingScheme); + PaymentPracticeLinesAggregator.PrepareLayout(); + if ReportingScheme = ReportingScheme::"Small Business" then begin + DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_SmallBusinessLayout'); + FeatureTelemetry.LogUsage('0000KSU', 'Payment Practices', 'Small Business layout used.'); + end; end; local procedure ShowHeaderDataLines() diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al index a8eb0fe955..06ab981cd2 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al @@ -57,14 +57,18 @@ page 686 "Payment Practice Data List" { ToolTip = 'Specifies the company size code of the vendor that is the source for this entry.'; } - field(SmallBusiness; IsSmallBusiness) + field("Small Business"; IsSmallBusiness) { Caption = 'Small Business'; + Visible = false; + Editable = false; ToolTip = 'Specifies whether the vendor is classified as a small business.'; } - field(PeppolEnabled; IsPeppolEnabled) + field("PEPPOL Enabled"; IsPeppolEnabled) { Caption = 'PEPPOL Enabled'; + Visible = false; + Editable = false; ToolTip = 'Specifies whether the vendor has a GLN and is PEPPOL enabled.'; } field("Agreed Payment Days"; Rec."Agreed Payment Days") @@ -103,6 +107,7 @@ page 686 "Payment Practice Data List" IsSmallBusiness := CompanySize."Small Business"; IsPeppolEnabled := false; + Vendor.SetLoadFields(GLN); if Vendor.Get(Rec."CV No.") then IsPeppolEnabled := Vendor.GLN <> ''; end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al index cfa424a9f4..eda56b235c 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al @@ -107,6 +107,7 @@ table 687 "Payment Practice Header" } field(15; "Reporting Scheme"; Enum "Paym. Prac. Reporting Scheme") { + Editable = false; ToolTip = 'Specifies which reporting scheme is used, such as Standard, Dispute & Retention, or Small Business. Controls which fields and calculations apply.'; } field(20; "Total Number of Payments"; Integer) diff --git a/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al b/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al index c8818b05c1..bae148f35e 100644 --- a/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/Test Library/src/PaymentPracticesLibrary.Codeunit.al @@ -13,8 +13,6 @@ using Microsoft.Sales.Receivables; codeunit 134196 "Payment Practices Library" { - Subtype = Test; - var Assert: Codeunit Assert; LibraryUtility: Codeunit "Library - Utility"; diff --git a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al index 69ed3708b1..5c8d470a82 100644 --- a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al @@ -883,14 +883,14 @@ codeunit 134197 "Payment Practices UT" VerifyAllLinesInvoiceCountAndValueZero(PaymentPracticeHeader."No."); end; - [Test] + [Test] procedure ModePaymentTimeCalculation() var PaymentPracticeHeader: Record "Payment Practice Header"; VendorNo: Code[20]; CompanySizeCode: Code[20]; begin - // [SCENARIO] Check mode payment time calculation in header + // [SCENARIO 568642] Check mode payment time calculation in header Initialize(); // [GIVEN] Create a vendor @@ -926,7 +926,7 @@ codeunit 134197 "Payment Practices UT" VendorNo1: Code[20]; VendorNo2: Code[20]; begin - // [SCENARIO] Check mode payment time min is the minimum of per-vendor modes + // [SCENARIO 568642] Check mode payment time min is the minimum of per-vendor modes Initialize(); // [GIVEN] Create two vendors with small business company size @@ -965,7 +965,7 @@ codeunit 134197 "Payment Practices UT" VendorNo1: Code[20]; VendorNo2: Code[20]; begin - // [SCENARIO] Check mode payment time max is the maximum of per-vendor modes + // [SCENARIO 568642] Check mode payment time max is the maximum of per-vendor modes Initialize(); // [GIVEN] Create two vendors with small business company size @@ -1003,7 +1003,7 @@ codeunit 134197 "Payment Practices UT" CompanySizeCode: Code[20]; VendorNo: Code[20]; begin - // [SCENARIO] Check median payment time calculation with odd number of entries + // [SCENARIO 568642] Check median payment time calculation with odd number of entries Initialize(); // [GIVEN] Create a vendor with small business company size @@ -1037,7 +1037,7 @@ codeunit 134197 "Payment Practices UT" CompanySizeCode: Code[20]; VendorNo: Code[20]; begin - // [SCENARIO] Check median payment time calculation with even number of entries + // [SCENARIO 568642] Check median payment time calculation with even number of entries Initialize(); // [GIVEN] Create a vendor with small business company size @@ -1071,7 +1071,7 @@ codeunit 134197 "Payment Practices UT" VendorNo: Code[20]; i: Integer; begin - // [SCENARIO] Check 80th percentile payment time calculation + // [SCENARIO 568642] Check 80th percentile payment time calculation Initialize(); // [GIVEN] Create a vendor with small business company size @@ -1103,7 +1103,7 @@ codeunit 134197 "Payment Practices UT" VendorNo: Code[20]; i: Integer; begin - // [SCENARIO] Check 95th percentile payment time calculation + // [SCENARIO 568642] Check 95th percentile payment time calculation Initialize(); // [GIVEN] Create a vendor with small business company size @@ -1135,7 +1135,7 @@ codeunit 134197 "Payment Practices UT" VendorNo: Code[20]; i: Integer; begin - // [SCENARIO] Check 80th percentile when index is not a whole number (7 * 80 / 100 = 5.6, truncated to 5) + // [SCENARIO 568642] Check 80th percentile when index is not a whole number (7 * 80 / 100 = 5.6, truncated to 5) Initialize(); // [GIVEN] Create a vendor with small business company size @@ -1167,7 +1167,7 @@ codeunit 134197 "Payment Practices UT" VendorNo: Code[20]; i: Integer; begin - // [SCENARIO] Check 95th percentile when index is not a whole number (13 * 95 / 100 = 12.35, truncated to 12) + // [SCENARIO 568642] Check 95th percentile when index is not a whole number (13 * 95 / 100 = 12.35, truncated to 12) Initialize(); // [GIVEN] Create a vendor with small business company size @@ -1204,7 +1204,7 @@ codeunit 134197 "Payment Practices UT" ExpectedPctPeppol: Decimal; i: Integer; begin - // [SCENARIO] Check Pct Peppol Enabled calculation with one vendor that has GLN and one that does not. + // [SCENARIO 568642] Check Pct Peppol Enabled calculation with one vendor that has GLN and one that does not. Initialize(); // [GIVEN] Create a company size marked as small business @@ -1254,7 +1254,7 @@ codeunit 134197 "Payment Practices UT" NonSmallBizVendor1: Code[20]; NonSmallBizVendor2: Code[20]; begin - // [SCENARIO] Generate payment practices with "Only Small Businesses" enabled. Only small business vendors should be included in the statistics (median, mode, percentiles) . + // [SCENARIO 568642] Generate payment practices with "Only Small Businesses" enabled. Only small business vendors should be included in the statistics (median, mode, percentiles) . Initialize(); // [GIVEN] Create a company size marked as "Small Business" and one that is not From 016d56df8c666e683c23a493d9ebc07af846ec5c Mon Sep 17 00:00:00 2001 From: Aleksandr Gladkov Date: Thu, 14 May 2026 12:53:11 +0200 Subject: [PATCH 05/12] more fixes for copilot bot comments --- .../PaymPracDisputeRetHdlr.Codeunit.al | 2 +- .../PaymPracPeriodAggregator.Codeunit.al | 15 +++ .../PaymPracSmallBusHandler.Codeunit.al | 10 +- .../PaymentPracticeSchemeHandler.Interface.al | 6 +- .../App/src/Core/PaymentPeriodMgt.Codeunit.al | 39 -------- .../src/Core/PaymentPracticeMath.Codeunit.al | 98 ++++++++----------- .../App/src/Core/PaymentPractices.Codeunit.al | 29 +++++- .../PaymPracObjects.PermissionSet.al | 1 - .../Core/UpgradePaymentPractices.Codeunit.al | 9 +- .../App/src/Pages/PaymentPracticeCard.Page.al | 26 +++-- .../src/Pages/PaymentPracticeDataList.Page.al | 18 +++- .../src/Tables/PaymentPracticeHeader.Table.al | 4 +- .../src/Tables/PaymentPracticeLine.Table.al | 3 - 13 files changed, 134 insertions(+), 126 deletions(-) delete mode 100644 src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al index 0f964a0d73..157820329e 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al @@ -42,7 +42,7 @@ codeunit 681 "Paym. Prac. Dispute Ret. Hdlr" implements PaymentPracticeSchemeHan OverdueCount: Integer; OverdueDueToDisputeCount: Integer; begin - if PaymentPracticeData.FindSet() then + if PaymentPracticeData.FindSet(true) then repeat if not PaymentPracticeData."Invoice Is Open" then begin TotalPayments += 1; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al index 5f4fa8b16e..146f4a4f1e 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracPeriodAggregator.Codeunit.al @@ -40,7 +40,9 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr if PaymentPeriod.FindSet() then repeat InsertPeriodLine(PaymentPracticeLine, PaymentPracticeData, PaymentPeriod, PaymentPracticeHeader."No.", NextLineNo, SourceType); + ApplyPeriodFilter(PaymentPracticeData, PaymentPeriod); SchemeHandler.CalculateLineTotals(PaymentPracticeLine, PaymentPracticeData); + ResetPeriodFilter(PaymentPracticeData); if (PaymentPracticeLine."Invoice Count" <> 0) or (PaymentPracticeLine."Invoice Value" <> 0) then PaymentPracticeLine.Modify(); until PaymentPeriod.Next() = 0; @@ -66,6 +68,19 @@ codeunit 685 "Paym. Prac. Period Aggregator" implements PaymentPracticeLinesAggr PaymentPracticeLine.Insert(); end; + local procedure ApplyPeriodFilter(var PaymentPracticeData: Record "Payment Practice Data"; PaymentPeriod: Record "Payment Period") + begin + if PaymentPeriod."Days To" = 0 then + PaymentPracticeData.SetFilter("Actual Payment Days", '>=%1', PaymentPeriod."Days From") + else + PaymentPracticeData.SetRange("Actual Payment Days", PaymentPeriod."Days From", PaymentPeriod."Days To"); + end; + + local procedure ResetPeriodFilter(var PaymentPracticeData: Record "Payment Practice Data") + begin + PaymentPracticeData.SetRange("Actual Payment Days"); + end; + local procedure SetPercentPaidInPeriod(var PaymentPracticeData: Record "Payment Practice Data"; DaysFrom: Integer; DaysTo: Integer; var PercentPaidInPeriodByNumber: Decimal; var PercentPaidInPeriodByAmount: Decimal) var Total: Integer; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al index e7da3b4cfd..c6b7648df7 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al @@ -45,6 +45,9 @@ codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHa var TotalCount: Integer; TotalValue: Decimal; + MedianPaymentTime: Decimal; + P80PaymentTime: Integer; + P95PaymentTime: Integer; begin PaymentPracticeData.SetRange("Invoice Is Open", false); TotalCount := PaymentPracticeData.Count(); @@ -57,9 +60,10 @@ codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHa PaymentPracticeHeader."Mode Payment Time" := PaymentPracticeMath.GetModePaymentTime(PaymentPracticeData); PaymentPracticeHeader."Mode Payment Time Min." := PaymentPracticeMath.GetModePaymentTimeMin(PaymentPracticeData); PaymentPracticeHeader."Mode Payment Time Max." := PaymentPracticeMath.GetModePaymentTimeMax(PaymentPracticeData); - PaymentPracticeHeader."Median Payment Time" := PaymentPracticeMath.GetMedianPaymentTime(PaymentPracticeData); - PaymentPracticeHeader."80th Percentile Payment Time" := PaymentPracticeMath.Get80thPercentilePaymentTime(PaymentPracticeData); - PaymentPracticeHeader."95th Percentile Payment Time" := PaymentPracticeMath.Get95thPercentilePaymentTime(PaymentPracticeData); + PaymentPracticeMath.GetPaymentTimeStatistics(PaymentPracticeData, MedianPaymentTime, P80PaymentTime, P95PaymentTime); + PaymentPracticeHeader."Median Payment Time" := MedianPaymentTime; + PaymentPracticeHeader."80th Percentile Payment Time" := P80PaymentTime; + PaymentPracticeHeader."95th Percentile Payment Time" := P95PaymentTime; PaymentPracticeHeader."Pct Peppol Enabled" := PaymentPracticeMath.GetPctPeppolEnabled(PaymentPracticeData); PaymentPracticeHeader."Pct Small Business Payments" := PaymentPracticeMath.GetPctSmallBusinessPayments(PaymentPracticeData, PaymentPracticeHeader); end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al b/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al index ca2d0b15f3..70e7964d12 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Interfaces/PaymentPracticeSchemeHandler.Interface.al @@ -28,9 +28,11 @@ interface PaymentPracticeSchemeHandler procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") /// - /// Calculates scheme-specific line totals for each generated line. + /// Calculates scheme-specific line totals for the currently visible slice of data. + /// The caller is responsible for applying any filters (period, company size, etc.) on + /// PaymentPracticeData before invoking this method, and for restoring them afterwards. /// /// The line to update with totals. - /// The data to aggregate from. + /// The data to aggregate from. Filters set by the caller define the slice. procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") } diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al deleted file mode 100644 index de3c08396a..0000000000 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPeriodMgt.Codeunit.al +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.Finance.Analysis; - -using System.Environment; - -codeunit 695 "Payment Period Mgt." -{ - Access = Internal; - InherentEntitlements = X; - InherentPermissions = X; - - procedure DetectReportingScheme(): Enum "Paym. Prac. Reporting Scheme" - var - EnvironmentInformation: Codeunit "Environment Information"; - ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; - IsHandled: Boolean; - begin - OnBeforeDetectReportingScheme(ReportingScheme, IsHandled); - if IsHandled then - exit(ReportingScheme); - - case EnvironmentInformation.GetApplicationFamily() of - 'GB': - exit("Paym. Prac. Reporting Scheme"::"Dispute & Retention"); - 'AU', 'NZ': - exit("Paym. Prac. Reporting Scheme"::"Small Business"); - else - exit("Paym. Prac. Reporting Scheme"::Standard); - end; - end; - - [IntegrationEvent(false, false)] - local procedure OnBeforeDetectReportingScheme(var ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; var IsHandled: Boolean) - begin - end; -} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al index 6f7aec3ef7..5edacb1bf0 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al @@ -105,44 +105,22 @@ codeunit 693 "Payment Practice Math" exit(MaxOfList(ModesPerVendor)); end; - procedure GetMedianPaymentTime(var PaymentPracticeData: Record "Payment Practice Data"): Decimal + procedure GetPaymentTimeStatistics(var PaymentPracticeData: Record "Payment Practice Data"; var MedianPaymentTime: Decimal; var P80PaymentTime: Integer; var P95PaymentTime: Integer) var ActualPaymentTimes: List of [Integer]; - MiddleIndex: Integer; begin - PaymentPracticeData.SetRange("Invoice Is Open", false); - if PaymentPracticeData.FindSet() then - repeat - ActualPaymentTimes.Add(PaymentPracticeData."Actual Payment Days"); - until PaymentPracticeData.Next() = 0; - PaymentPracticeData.SetRange("Invoice Is Open"); + MedianPaymentTime := 0; + P80PaymentTime := 0; + P95PaymentTime := 0; + GetClosedInvoicePaymentTimes(PaymentPracticeData, ActualPaymentTimes); if ActualPaymentTimes.Count() = 0 then - exit(0); + exit; SortIntegerList(ActualPaymentTimes); - - MiddleIndex := ActualPaymentTimes.Count() div 2; - if ActualPaymentTimes.Count() mod 2 = 0 then - exit((ActualPaymentTimes.Get(MiddleIndex) + ActualPaymentTimes.Get(MiddleIndex + 1)) / 2) - else - exit(ActualPaymentTimes.Get(MiddleIndex + 1)); - end; - - procedure Get80thPercentilePaymentTime(var PaymentPracticeData: Record "Payment Practice Data"): Integer - var - ActualPaymentTimes: List of [Integer]; - begin - GetClosedInvoicePaymentTimes(PaymentPracticeData, ActualPaymentTimes); - exit(Percentile(ActualPaymentTimes, 80)); - end; - - procedure Get95thPercentilePaymentTime(var PaymentPracticeData: Record "Payment Practice Data"): Integer - var - ActualPaymentTimes: List of [Integer]; - begin - GetClosedInvoicePaymentTimes(PaymentPracticeData, ActualPaymentTimes); - exit(Percentile(ActualPaymentTimes, 95)); + MedianPaymentTime := MedianFromSorted(ActualPaymentTimes); + P80PaymentTime := PercentileFromSorted(ActualPaymentTimes, 80); + P95PaymentTime := PercentileFromSorted(ActualPaymentTimes, 95); end; procedure GetPctPeppolEnabled(var PaymentPracticeData: Record "Payment Practice Data"): Decimal @@ -187,14 +165,14 @@ codeunit 693 "Payment Practice Math" VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); VendorLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); VendorLedgerEntry.SetAutoCalcFields(Amount, "Remaining Amount"); + Vendor.SetLoadFields("Company Size Code"); if VendorLedgerEntry.FindSet() then repeat - Vendor.SetLoadFields("Company Size Code"); - Vendor.Get(VendorLedgerEntry."Vendor No."); - if CompanySize.Get(Vendor."Company Size Code") then - if CompanySize."Small Business" then - TotalAmountSmallBusinesses += VendorLedgerEntry."Remaining Amount" - VendorLedgerEntry.Amount; - TotalAmountAllVendors += VendorLedgerEntry."Remaining Amount" - VendorLedgerEntry.Amount; + if Vendor.Get(VendorLedgerEntry."Vendor No.") then + if CompanySize.Get(Vendor."Company Size Code") then + if CompanySize."Small Business" then + TotalAmountSmallBusinesses += VendorLedgerEntry.Amount - VendorLedgerEntry."Remaining Amount"; + TotalAmountAllVendors += VendorLedgerEntry.Amount - VendorLedgerEntry."Remaining Amount"; until VendorLedgerEntry.Next() = 0; if TotalAmountAllVendors = 0 then @@ -213,22 +191,27 @@ codeunit 693 "Payment Practice Math" PaymentPracticeData.SetRange("Invoice Is Open"); end; - local procedure Percentile(var List: List of [Integer]; P: Integer): Integer + local procedure MedianFromSorted(var SortedList: List of [Integer]): Decimal var - Index: Integer; + MiddleIndex: Integer; begin - if List.Count() = 0 then - exit(0); - - SortIntegerList(List); + MiddleIndex := SortedList.Count() div 2; + if SortedList.Count() mod 2 = 0 then + exit((SortedList.Get(MiddleIndex) + SortedList.Get(MiddleIndex + 1)) / 2) + else + exit(SortedList.Get(MiddleIndex + 1)); + end; - Index := List.Count() * P div 100; + local procedure PercentileFromSorted(var SortedList: List of [Integer]; P: Integer): Integer + var + Index: Integer; + begin + Index := SortedList.Count() * P div 100; if Index < 1 then Index := 1; - if Index > List.Count() then - Index := List.Count(); - - exit(List.Get(Index)); + if Index > SortedList.Count() then + Index := SortedList.Count(); + exit(SortedList.Get(Index)); end; local procedure GetModesPerVendor(var PaymentPracticeData: Record "Payment Practice Data"; var ModesPerVendor: List of [Integer]) @@ -321,14 +304,17 @@ codeunit 693 "Payment Practice Math" var i: Integer; j: Integer; - Temp: Integer; + CurrentValue: Integer; begin - for i := 1 to List.Count() - 1 do - for j := 1 to List.Count() - i do - if List.Get(j) > List.Get(j + 1) then begin - Temp := List.Get(j); - List.Set(j, List.Get(j + 1)); - List.Set(j + 1, Temp); - end; + // Insertion sort, O(n^2) worst case + for i := 2 to List.Count() do begin + CurrentValue := List.Get(i); + j := i - 1; + while (j >= 1) and (List.Get(j) > CurrentValue) do begin + List.Set(j + 1, List.Get(j)); + j -= 1; + end; + List.Set(j + 1, CurrentValue); + end; end; } \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al index f6e8d14ca3..05ab1297c4 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPractices.Codeunit.al @@ -4,6 +4,8 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; +using System.Environment; + codeunit 689 "Payment Practices" { var @@ -66,4 +68,29 @@ codeunit 689 "Payment Practices" begin PaymentPracticeLinesAggregator.GenerateLines(PaymentPracticeData, PaymentPracticeHeader); end; -} \ No newline at end of file + + procedure DetectReportingScheme(): Enum "Paym. Prac. Reporting Scheme" + var + EnvironmentInformation: Codeunit "Environment Information"; + ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; + IsHandled: Boolean; + begin + OnBeforeDetectReportingScheme(ReportingScheme, IsHandled); + if IsHandled then + exit(ReportingScheme); + + case EnvironmentInformation.GetApplicationFamily() of + 'GB': + exit("Paym. Prac. Reporting Scheme"::"Dispute & Retention"); + 'AU', 'NZ': + exit("Paym. Prac. Reporting Scheme"::"Small Business"); + else + exit("Paym. Prac. Reporting Scheme"::Standard); + end; + end; + + [IntegrationEvent(false, false)] + local procedure OnBeforeDetectReportingScheme(var ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; var IsHandled: Boolean) + begin + end; +} diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al b/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al index 20fcdac349..e927dc130d 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Permissions/PaymPracObjects.PermissionSet.al @@ -28,7 +28,6 @@ permissionset 685 "Paym. Prac. Objects" codeunit "Paym. Prac. Standard Handler" = X, codeunit "Paym. Prac. Dispute Ret. Hdlr" = X, codeunit "Paym. Prac. Small Bus. Handler" = X, - codeunit "Upgrade Payment Practices" = X, codeunit "Install Payment Practices" = X, codeunit "Payment Practice Builders" = X, codeunit "Payment Practice Math" = X, diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al index 81d3eca4c6..354fe5907f 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/UpgradePaymentPractices.Codeunit.al @@ -10,6 +10,9 @@ codeunit 683 "Upgrade Payment Practices" { Access = Internal; Subtype = Upgrade; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "Payment Practice Header" = RM; var UpgradeTag: Codeunit "Upgrade Tag"; @@ -22,13 +25,13 @@ codeunit 683 "Upgrade Payment Practices" local procedure BackfillReportingScheme() var PaymentPracticeHeader: Record "Payment Practice Header"; - PaymentPeriodMgt: Codeunit "Payment Period Mgt."; + PaymentPractices: Codeunit "Payment Practices"; ReportingScheme: Enum "Paym. Prac. Reporting Scheme"; begin if UpgradeTag.HasUpgradeTag(GetReportingSchemeUpgradeTag()) then exit; - ReportingScheme := PaymentPeriodMgt.DetectReportingScheme(); + ReportingScheme := PaymentPractices.DetectReportingScheme(); PaymentPracticeHeader.SetRange("Reporting Scheme", 0); PaymentPracticeHeader.ModifyAll("Reporting Scheme", ReportingScheme); @@ -42,7 +45,7 @@ codeunit 683 "Upgrade Payment Practices" PerCompanyUpgradeTags.Add(GetReportingSchemeUpgradeTag()); end; - procedure GetReportingSchemeUpgradeTag(): Code[250] + local procedure GetReportingSchemeUpgradeTag(): Code[250] begin exit('MS-597313-PaymPracReportingScheme-20260513'); end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al index 8b841d4d93..e0f50b16d2 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al @@ -100,17 +100,23 @@ page 687 "Payment Practice Card" ShowHeaderDataLines(); end; } - field("Total Number of Payments"; Rec."Total Number of Payments") - { - } - field("Total Amount of Payments"; Rec."Total Amount of Payments") - { - } - field("Total Amt. of Overdue Payments"; Rec."Total Amt. of Overdue Payments") - { - } - field("Pct Overdue Due to Dispute"; Rec."Pct Overdue Due to Dispute") + group("Dispute and Retention") { + Caption = 'Dispute and Retention'; + Visible = Rec."Reporting Scheme" = Rec."Reporting Scheme"::"Dispute & Retention"; + + field("Total Number of Payments"; Rec."Total Number of Payments") + { + } + field("Total Amount of Payments"; Rec."Total Amount of Payments") + { + } + field("Total Amt. of Overdue Payments"; Rec."Total Amt. of Overdue Payments") + { + } + field("Pct Overdue Due to Dispute"; Rec."Pct Overdue Due to Dispute") + { + } } group("Small Business Scheme") { diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al index 06ab981cd2..7e0f4f2a2d 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeDataList.Page.al @@ -103,16 +103,24 @@ page 686 "Payment Practice Data List" Vendor: Record Vendor; begin IsSmallBusiness := false; - if CompanySize.Get(Rec."Company Size Code") then - IsSmallBusiness := CompanySize."Small Business"; + if not CompanySizeCache.Get(Rec."Company Size Code", IsSmallBusiness) then begin + if CompanySize.Get(Rec."Company Size Code") then + IsSmallBusiness := CompanySize."Small Business"; + CompanySizeCache.Add(Rec."Company Size Code", IsSmallBusiness); + end; IsPeppolEnabled := false; - Vendor.SetLoadFields(GLN); - if Vendor.Get(Rec."CV No.") then - IsPeppolEnabled := Vendor.GLN <> ''; + if not VendorGLNCache.Get(Rec."CV No.", IsPeppolEnabled) then begin + Vendor.SetLoadFields(GLN); + if Vendor.Get(Rec."CV No.") then + IsPeppolEnabled := Vendor.GLN <> ''; + VendorGLNCache.Add(Rec."CV No.", IsPeppolEnabled); + end; end; var + CompanySizeCache: Dictionary of [Code[20], Boolean]; + VendorGLNCache: Dictionary of [Code[20], Boolean]; IsSmallBusiness: Boolean; IsPeppolEnabled: Boolean; } \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al index eda56b235c..adb39bf95b 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al @@ -280,9 +280,9 @@ table 687 "Payment Practice Header" local procedure DetectReportingScheme() var - PaymentPeriodMgt: Codeunit "Payment Period Mgt."; + PaymentPractices: Codeunit "Payment Practices"; begin - "Reporting Scheme" := PaymentPeriodMgt.DetectReportingScheme(); + "Reporting Scheme" := PaymentPractices.DetectReportingScheme(); end; } \ No newline at end of file diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al index 9fbff11903..aa7d2b5438 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeLine.Table.al @@ -95,9 +95,6 @@ table 688 "Payment Practice Line" AutoFormatExpression = ''; ToolTip = 'Specifies the total value of invoices in this period.'; } - field(20; "Payment Period Line No."; Integer) - { - } } keys From 09c9eacecb6af24a41af6d750f8a0c16f8df14cf Mon Sep 17 00:00:00 2001 From: Aleksandr Gladkov Date: Thu, 14 May 2026 15:35:32 +0200 Subject: [PATCH 06/12] fix for non-existing library procedure --- .../Test/src/Codeunits/Tests/SubcPurchSubcontTest.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Apps/W1/Subcontracting/Test/src/Codeunits/Tests/SubcPurchSubcontTest.Codeunit.al b/src/Apps/W1/Subcontracting/Test/src/Codeunits/Tests/SubcPurchSubcontTest.Codeunit.al index 271412ea4b..24884d92f5 100644 --- a/src/Apps/W1/Subcontracting/Test/src/Codeunits/Tests/SubcPurchSubcontTest.Codeunit.al +++ b/src/Apps/W1/Subcontracting/Test/src/Codeunits/Tests/SubcPurchSubcontTest.Codeunit.al @@ -472,7 +472,7 @@ codeunit 139991 "Subc. Purch. Subcont. Test" ProductionOrder."Source Type"::Item, FinishedItem."No.", Qty, HomeLocation.Code); // [GIVEN] Requisition worksheet template for subcontracting - LibraryMfgManagement.CreateLaborReqWkshTemplateAndNameAndUpdateSetup(); + LibraryMfgManagement.CreateSubcontractingReqWkshTemplateAndNameAndUpdateSetup(); // [WHEN] Create subcontracting purchase order from Prod. Order Routing ProdOrderRtngLine.SetRange("Routing No.", RoutingHeader."No."); From 5db3eb98a8f8544151ab084aaa1e4465d3bda9e0 Mon Sep 17 00:00:00 2001 From: Aleksandr Gladkov Date: Thu, 14 May 2026 19:20:55 +0200 Subject: [PATCH 07/12] fixed bug in sorting list procedure --- .../App/src/Core/PaymentPracticeMath.Codeunit.al | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al index 5edacb1bf0..5d38055af1 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al @@ -310,7 +310,9 @@ codeunit 693 "Payment Practice Math" for i := 2 to List.Count() do begin CurrentValue := List.Get(i); j := i - 1; - while (j >= 1) and (List.Get(j) > CurrentValue) do begin + while j >= 1 do begin + if List.Get(j) <= CurrentValue then + break; List.Set(j + 1, List.Get(j)); j -= 1; end; From 0bd53d52d8996505d1f9eda654d427ee478f17a5 Mon Sep 17 00:00:00 2001 From: AndreasHans Date: Fri, 15 May 2026 10:36:15 +0200 Subject: [PATCH 08/12] improve tooltips, add xml tags, some renaming --- .../PaymPracSmallBusHandler.Codeunit.al | 2 +- .../src/Core/PaymentPracticeMath.Codeunit.al | 121 ++++++++++++++---- .../App/src/Pages/PaymentPracticeCard.Page.al | 4 +- .../src/Tables/PaymentPracticeHeader.Table.al | 16 +-- 4 files changed, 110 insertions(+), 33 deletions(-) diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al index c6b7648df7..e48155bdc8 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al @@ -19,7 +19,7 @@ codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHa begin if PaymentPracticeHeader."Header Type" <> PaymentPracticeHeader."Header Type"::Vendor then Error(WrongHeaderTypeErr); - if PaymentPracticeHeader."Aggregation Type" = PaymentPracticeHeader."Aggregation Type"::"Company Size" then + if PaymentPracticeHeader."Aggregation Type" <> PaymentPracticeHeader."Aggregation Type"::Period then Error(WrongHeaderAggErr); end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al index 5d38055af1..089ac59770 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al @@ -76,6 +76,11 @@ codeunit 693 "Payment Practice Math" Total += Number; end; + /// + /// Calculates the mode (most frequent value) of the actual payment times across all closed invoices in the provided dataset. + /// + /// The payment practice data to evaluate. Filters are temporarily adjusted but restored before returning. + /// The most frequently occurring number of actual payment days, or 0 if no closed invoices exist. procedure GetModePaymentTime(var PaymentPracticeData: Record "Payment Practice Data"): Integer var ActualPaymentTimes: List of [Integer]; @@ -86,9 +91,14 @@ codeunit 693 "Payment Practice Math" ActualPaymentTimes.Add(PaymentPracticeData."Actual Payment Days"); until PaymentPracticeData.Next() = 0; PaymentPracticeData.SetRange("Invoice Is Open"); - exit(Mode(ActualPaymentTimes)); + exit(MostFrequentValue(ActualPaymentTimes)); end; + /// + /// Calculates the smallest per-vendor mode of actual payment times across all vendors in the provided dataset. + /// + /// The payment practice data to evaluate, grouped by vendor. + /// The minimum of all per-vendor modes, or 0 if no closed invoices exist. procedure GetModePaymentTimeMin(var PaymentPracticeData: Record "Payment Practice Data"): Integer var ModesPerVendor: List of [Integer]; @@ -97,6 +107,11 @@ codeunit 693 "Payment Practice Math" exit(MinOfList(ModesPerVendor)); end; + /// + /// Calculates the largest per-vendor mode of actual payment times across all vendors in the provided dataset. + /// + /// The payment practice data to evaluate, grouped by vendor. + /// The maximum of all per-vendor modes, or 0 if no closed invoices exist. procedure GetModePaymentTimeMax(var PaymentPracticeData: Record "Payment Practice Data"): Integer var ModesPerVendor: List of [Integer]; @@ -105,6 +120,13 @@ codeunit 693 "Payment Practice Math" exit(MaxOfList(ModesPerVendor)); end; + /// + /// Calculates the median, 80th percentile, and 95th percentile of actual payment times across all closed invoices in the provided dataset. + /// + /// The payment practice data to evaluate. Filters are temporarily adjusted but restored before returning. + /// Output parameter that receives the median number of actual payment days, or 0 if no closed invoices exist. + /// Output parameter that receives the 80th percentile of actual payment days, or 0 if no closed invoices exist. + /// Output parameter that receives the 95th percentile of actual payment days, or 0 if no closed invoices exist. procedure GetPaymentTimeStatistics(var PaymentPracticeData: Record "Payment Practice Data"; var MedianPaymentTime: Decimal; var P80PaymentTime: Integer; var P95PaymentTime: Integer) var ActualPaymentTimes: List of [Integer]; @@ -123,6 +145,11 @@ codeunit 693 "Payment Practice Math" P95PaymentTime := PercentileFromSorted(ActualPaymentTimes, 95); end; + /// + /// Calculates the percentage of closed-invoice transactions whose vendor is Peppol enabled (i.e. has a non-empty GLN). + /// + /// The payment practice data to evaluate. A per-vendor GLN cache is used to avoid repeated lookups. + /// The percentage (0-100) of closed-invoice rows from Peppol-enabled vendors, or 0 when there are no closed invoices. procedure GetPctPeppolEnabled(var PaymentPracticeData: Record "Payment Practice Data"): Decimal var Vendor: Record Vendor; @@ -153,6 +180,12 @@ codeunit 693 "Payment Practice Math" exit(PeppolCount / Total * 100); end; + /// + /// Calculates the percentage of total vendor invoice value (within the header period) that is paid to small-business vendors. + /// + /// The payment practice data context (currently unused for filtering but retained for signature consistency). + /// The payment practice header providing the reporting period (Starting/Ending Date). + /// The percentage (0-100) of paid invoice value attributable to vendors flagged as Small Business, or 0 when no invoice value exists. procedure GetPctSmallBusinessPayments(var PaymentPracticeData: Record "Payment Practice Data"; PaymentPracticeHeader: Record "Payment Practice Header"): Decimal var VendorLedgerEntry: Record "Vendor Ledger Entry"; @@ -181,6 +214,11 @@ codeunit 693 "Payment Practice Math" exit(TotalAmountSmallBusinesses / TotalAmountAllVendors * 100); end; + /// + /// Collects the actual payment days for all closed invoices in the dataset into a list. Filters are temporarily adjusted but restored before returning. + /// + /// The payment practice data to scan. + /// Output list that will receive one integer per closed invoice. local procedure GetClosedInvoicePaymentTimes(var PaymentPracticeData: Record "Payment Practice Data"; var ActualPaymentTimes: List of [Integer]) begin PaymentPracticeData.SetRange("Invoice Is Open", false); @@ -191,6 +229,12 @@ codeunit 693 "Payment Practice Math" PaymentPracticeData.SetRange("Invoice Is Open"); end; + /// + /// Calculates the median value from a list of integers that has already been sorted in ascending order. + /// For lists with an even number of elements, returns the average of the two middle values. + /// + /// The sorted list of integers to evaluate. Must be sorted in ascending order and contain at least one element. + /// The median value as a decimal. local procedure MedianFromSorted(var SortedList: List of [Integer]): Decimal var MiddleIndex: Integer; @@ -202,6 +246,13 @@ codeunit 693 "Payment Practice Math" exit(SortedList.Get(MiddleIndex + 1)); end; + /// + /// Returns the value at the specified percentile from a list of integers that has already been sorted in ascending order. + /// The index is clamped to the valid range [1, Count]. + /// + /// The sorted list of integers to evaluate. Must be sorted in ascending order and contain at least one element. + /// The percentile to compute (e.g. 80 for the 80th percentile). + /// The integer value at the specified percentile. local procedure PercentileFromSorted(var SortedList: List of [Integer]; P: Integer): Integer var Index: Integer; @@ -214,6 +265,12 @@ codeunit 693 "Payment Practice Math" exit(SortedList.Get(Index)); end; + /// + /// Computes the mode of actual payment times for each vendor in the dataset and returns one mode value per vendor. + /// Relies on the data being sorted by "CV No." which is set as the current key inside the procedure and restored before returning. + /// + /// The payment practice data to scan. + /// Output list that will receive one mode value per vendor that has at least one closed invoice. local procedure GetModesPerVendor(var PaymentPracticeData: Record "Payment Practice Data"; var ModesPerVendor: List of [Integer]) var ActualPaymentTimes: List of [Integer]; @@ -225,18 +282,23 @@ codeunit 693 "Payment Practice Math" CurrentVendor := PaymentPracticeData."CV No."; repeat if PaymentPracticeData."CV No." <> CurrentVendor then begin - ModesPerVendor.Add(Mode(ActualPaymentTimes)); + ModesPerVendor.Add(MostFrequentValue(ActualPaymentTimes)); Clear(ActualPaymentTimes); CurrentVendor := PaymentPracticeData."CV No."; end; ActualPaymentTimes.Add(PaymentPracticeData."Actual Payment Days"); until PaymentPracticeData.Next() = 0; - ModesPerVendor.Add(Mode(ActualPaymentTimes)); + ModesPerVendor.Add(MostFrequentValue(ActualPaymentTimes)); end; PaymentPracticeData.SetRange("Invoice Is Open"); PaymentPracticeData.SetCurrentKey("Header No.", "Invoice Entry No.", "Source Type"); end; + /// + /// Returns the smallest value in the supplied integer list. + /// + /// The list of integers to evaluate. + /// The minimum value in the list, or 0 if the list is empty. local procedure MinOfList(var List: List of [Integer]): Integer var Value: Integer; @@ -253,6 +315,11 @@ codeunit 693 "Payment Practice Math" exit(MinValue); end; + /// + /// Returns the largest value in the supplied integer list. + /// + /// The list of integers to evaluate. + /// The maximum value in the list, or 0 if the list is empty. local procedure MaxOfList(var List: List of [Integer]): Integer var Value: Integer; @@ -270,36 +337,46 @@ codeunit 693 "Payment Practice Math" exit(MaxValue); end; - local procedure Mode(var List: List of [Integer]): Integer + /// + /// Returns the most frequently occurring value (statistical mode) in the supplied integer list. + /// When several values share the highest frequency, the smallest of those values is returned for deterministic behavior. + /// + /// The list of integers to evaluate. + /// The most frequent value, or 0 if the list is empty. + local procedure MostFrequentValue(var List: List of [Integer]): Integer var - Frequencies: Dictionary of [Integer, Integer]; - Value: Integer; - Frequency: Integer; - MaxFrequency: Integer; - ModeValue: Integer; + ValueFrequencies: Dictionary of [Integer, Integer]; + CurrentValue: Integer; + CurrentFrequency: Integer; + HighestFrequency: Integer; + MostFrequent: Integer; begin if List.Count() = 0 then exit(0); - foreach Value in List do - if Frequencies.ContainsKey(Value) then - Frequencies.Set(Value, Frequencies.Get(Value) + 1) + foreach CurrentValue in List do + if ValueFrequencies.ContainsKey(CurrentValue) then + ValueFrequencies.Set(CurrentValue, ValueFrequencies.Get(CurrentValue) + 1) else - Frequencies.Add(Value, 1); - - MaxFrequency := 0; - ModeValue := 0; - foreach Value in Frequencies.Keys() do begin - Frequency := Frequencies.Get(Value); - if (Frequency > MaxFrequency) or ((Frequency = MaxFrequency) and (Value < ModeValue)) then begin - MaxFrequency := Frequency; - ModeValue := Value; + ValueFrequencies.Add(CurrentValue, 1); + + HighestFrequency := 0; + MostFrequent := 0; + foreach CurrentValue in ValueFrequencies.Keys() do begin + CurrentFrequency := ValueFrequencies.Get(CurrentValue); + if (CurrentFrequency > HighestFrequency) or ((CurrentFrequency = HighestFrequency) and (CurrentValue < MostFrequent)) then begin + HighestFrequency := CurrentFrequency; + MostFrequent := CurrentValue; end; end; - exit(ModeValue); + exit(MostFrequent); end; + /// + /// Sorts the supplied integer list in ascending order in place using a simple bubble sort. + /// + /// The list of integers to sort. Modified in place. local procedure SortIntegerList(var List: List of [Integer]) var i: Integer; diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al index e0f50b16d2..51334192cd 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al @@ -177,7 +177,7 @@ page 687 "Payment Practice Card" { trigger OnDrillDown() begin - ShowSmallBusinessVendorLedgerEntries(); + ShowVendorInvoicesInReportPeriod(); end; } } @@ -279,7 +279,7 @@ page 687 "Payment Practice Card" Page.RunModal(Page::"Payment Practice Data List", PaymentPracticeData); end; - local procedure ShowSmallBusinessVendorLedgerEntries() + local procedure ShowVendorInvoicesInReportPeriod() var VendorLedgerEntry: Record "Vendor Ledger Entry"; begin diff --git a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al index adb39bf95b..c7c2e3a73d 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al +++ b/src/Apps/W1/PaymentPractices/App/src/Tables/PaymentPracticeHeader.Table.al @@ -137,7 +137,7 @@ table 687 "Payment Practice Header" } field(24; "Mode Payment Time"; Integer) { - ToolTip = 'Specifies the mode payment time.'; + ToolTip = 'Specifies the most frequently occurring number of days taken to pay invoices during the reporting period.'; trigger OnValidate() begin @@ -146,7 +146,7 @@ table 687 "Payment Practice Header" } field(25; "Mode Payment Time Min."; Integer) { - ToolTip = 'Specifies the minimum per vendor mode payment time.'; + ToolTip = 'Specifies the lowest mode payment time, in days, found across all vendors in the reporting period. The value is calculated by determining the mode payment time for each vendor, which is the most frequently occurring number of days taken to pay invoices for that vendor during the reporting period, and then finding the lowest value among those.'; trigger OnValidate() begin @@ -155,7 +155,7 @@ table 687 "Payment Practice Header" } field(26; "Mode Payment Time Max."; Integer) { - ToolTip = 'Specifies the maximum per vendor mode payment time.'; + ToolTip = 'Specifies the highest mode payment time, in days, found across all vendors in the reporting period. The value is calculated by determining the mode payment time for each vendor, which is the most frequently occurring number of days taken to pay invoices for that vendor during the reporting period, and then finding the highest value among those.'; trigger OnValidate() begin @@ -166,7 +166,7 @@ table 687 "Payment Practice Header" { AutoFormatType = 0; DecimalPlaces = 2; - ToolTip = 'Specifies the median payment time.'; + ToolTip = 'Specifies the median number of days taken to pay invoices during the reporting period.'; trigger OnValidate() begin @@ -175,7 +175,7 @@ table 687 "Payment Practice Header" } field(28; "80th Percentile Payment Time"; Integer) { - ToolTip = 'Specifies the 80th percentile payment time.'; + ToolTip = 'Specifies the number of days within which 80 percent of invoices were paid during the reporting period.'; trigger OnValidate() begin @@ -184,7 +184,7 @@ table 687 "Payment Practice Header" } field(29; "95th Percentile Payment Time"; Integer) { - ToolTip = 'Specifies the 95th percentile payment time.'; + ToolTip = 'Specifies the number of days within which 95 percent of invoices were paid during the reporting period.'; trigger OnValidate() begin @@ -195,7 +195,7 @@ table 687 "Payment Practice Header" { AutoFormatType = 0; DecimalPlaces = 2; - ToolTip = 'Specifies the percentage of invoices that are PEPPOL enabled.'; + ToolTip = 'Specifies the percentage of invoices that were PEPPOL enabled. An invoice is considered PEPPOL enabled if the vendor has a GLN.'; trigger OnValidate() begin @@ -206,7 +206,7 @@ table 687 "Payment Practice Header" { AutoFormatType = 0; DecimalPlaces = 2; - ToolTip = 'Specifies small business payments as a percentage of total payments. This includes the value of partial payments.'; + ToolTip = 'Specifies the value of payments made to small business vendors as a percentage of total payments in the reporting period, including partial payments. A vendor is classified as a small business based on the company size code on the vendor record.'; trigger OnValidate() begin From ba3a425a40aac957d283f4534232ba307630ac5d Mon Sep 17 00:00:00 2001 From: AndreasHans Date: Fri, 15 May 2026 12:20:11 +0200 Subject: [PATCH 09/12] resolve some pr comments and fix a test --- .../PaymPracSmallBusHandler.Codeunit.al | 2 +- .../src/Core/PaymentPracticeMath.Codeunit.al | 77 +++++++++++++----- .../Payment Practice Small Business.docx | Bin 31172 -> 31480 bytes .../Reports/Payment Practice by Period.docx | Bin 28449 -> 28757 bytes .../Payment Practice by Vendor Size.docx | Bin 27874 -> 28182 bytes .../Test/src/PaymentPracticesUT.Codeunit.al | 2 + 6 files changed, 59 insertions(+), 22 deletions(-) diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al index e48155bdc8..f5c9bd3fde 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al @@ -65,7 +65,7 @@ codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHa PaymentPracticeHeader."80th Percentile Payment Time" := P80PaymentTime; PaymentPracticeHeader."95th Percentile Payment Time" := P95PaymentTime; PaymentPracticeHeader."Pct Peppol Enabled" := PaymentPracticeMath.GetPctPeppolEnabled(PaymentPracticeData); - PaymentPracticeHeader."Pct Small Business Payments" := PaymentPracticeMath.GetPctSmallBusinessPayments(PaymentPracticeData, PaymentPracticeHeader); + PaymentPracticeHeader."Pct Small Business Payments" := PaymentPracticeMath.GetPctSmallBusinessPayments(PaymentPracticeHeader); end; procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al index 089ac59770..7b0da46c42 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al @@ -183,29 +183,63 @@ codeunit 693 "Payment Practice Math" /// /// Calculates the percentage of total vendor invoice value (within the header period) that is paid to small-business vendors. /// - /// The payment practice data context (currently unused for filtering but retained for signature consistency). /// The payment practice header providing the reporting period (Starting/Ending Date). /// The percentage (0-100) of paid invoice value attributable to vendors flagged as Small Business, or 0 when no invoice value exists. - procedure GetPctSmallBusinessPayments(var PaymentPracticeData: Record "Payment Practice Data"; PaymentPracticeHeader: Record "Payment Practice Header"): Decimal + procedure GetPctSmallBusinessPayments(PaymentPracticeHeader: Record "Payment Practice Header"): Decimal var VendorLedgerEntry: Record "Vendor Ledger Entry"; + VendorLedgerEntryForSum: Record "Vendor Ledger Entry"; CompanySize: Record "Company Size"; Vendor: Record Vendor; + VendorCompanySizeCache: Dictionary of [Code[20], Code[10]]; + VendorExcludedCache: Dictionary of [Code[20], Boolean]; + SmallBusinessCache: Dictionary of [Code[10], Boolean]; + CompanySizeCode: Code[10]; + PaidAmount: Decimal; TotalAmountSmallBusinesses: Decimal; TotalAmountAllVendors: Decimal; begin - VendorLedgerEntry.SetLoadFields("Vendor No.", "Document Type", "Posting Date", Amount, "Remaining Amount"); + // Walk the filtered Vendor Ledger Entries grouped by vendor. For each distinct vendor we + // (a) look up "Company Size Code" / "Exclude from Pmt. Practices" once (cached in a Dictionary), and + // (b) aggregate Amount / "Remaining Amount" with a single CalcSums call, instead of + // forcing the FlowFields to evaluate on every row via SetAutoCalcFields. + VendorLedgerEntry.SetCurrentKey("Vendor No.", "Posting Date"); + VendorLedgerEntry.SetLoadFields("Vendor No."); VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); VendorLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); - VendorLedgerEntry.SetAutoCalcFields(Amount, "Remaining Amount"); - Vendor.SetLoadFields("Company Size Code"); if VendorLedgerEntry.FindSet() then repeat - if Vendor.Get(VendorLedgerEntry."Vendor No.") then - if CompanySize.Get(Vendor."Company Size Code") then - if CompanySize."Small Business" then - TotalAmountSmallBusinesses += VendorLedgerEntry.Amount - VendorLedgerEntry."Remaining Amount"; - TotalAmountAllVendors += VendorLedgerEntry.Amount - VendorLedgerEntry."Remaining Amount"; + if not VendorCompanySizeCache.ContainsKey(VendorLedgerEntry."Vendor No.") then begin + Vendor.SetLoadFields("Company Size Code", "Exclude from Pmt. Practices"); + if Vendor.Get(VendorLedgerEntry."Vendor No.") then begin + CompanySizeCode := Vendor."Company Size Code"; + VendorExcludedCache.Add(VendorLedgerEntry."Vendor No.", Vendor."Exclude from Pmt. Practices"); + end else begin + CompanySizeCode := ''; + VendorExcludedCache.Add(VendorLedgerEntry."Vendor No.", false); + end; + VendorCompanySizeCache.Add(VendorLedgerEntry."Vendor No.", CompanySizeCode); + + if not SmallBusinessCache.ContainsKey(CompanySizeCode) then + SmallBusinessCache.Add(CompanySizeCode, (CompanySizeCode <> '') and CompanySize.Get(CompanySizeCode) and CompanySize."Small Business"); + + if not VendorExcludedCache.Get(VendorLedgerEntry."Vendor No.") then begin + VendorLedgerEntryForSum.SetCurrentKey("Vendor No.", "Posting Date"); + VendorLedgerEntryForSum.SetRange("Vendor No.", VendorLedgerEntry."Vendor No."); + VendorLedgerEntryForSum.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); + VendorLedgerEntryForSum.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); + VendorLedgerEntryForSum.SetAutoCalcFields(Amount, "Remaining Amount"); + PaidAmount := 0; + if VendorLedgerEntryForSum.FindSet() then + repeat + PaidAmount += VendorLedgerEntryForSum.Amount - VendorLedgerEntryForSum."Remaining Amount"; + until VendorLedgerEntryForSum.Next() = 0; + + if SmallBusinessCache.Get(CompanySizeCode) then + TotalAmountSmallBusinesses += PaidAmount; + TotalAmountAllVendors += PaidAmount; + end; + end; until VendorLedgerEntry.Next() = 0; if TotalAmountAllVendors = 0 then @@ -273,25 +307,26 @@ codeunit 693 "Payment Practice Math" /// Output list that will receive one mode value per vendor that has at least one closed invoice. local procedure GetModesPerVendor(var PaymentPracticeData: Record "Payment Practice Data"; var ModesPerVendor: List of [Integer]) var + LocalPaymentPracticeData: Record "Payment Practice Data"; ActualPaymentTimes: List of [Integer]; CurrentVendor: Code[20]; begin - PaymentPracticeData.SetRange("Invoice Is Open", false); - PaymentPracticeData.SetCurrentKey("CV No."); - if PaymentPracticeData.FindSet() then begin - CurrentVendor := PaymentPracticeData."CV No."; + // Use a separate record variable so we don't mutate the caller's filters or current key. + LocalPaymentPracticeData.CopyFilters(PaymentPracticeData); + LocalPaymentPracticeData.SetRange("Invoice Is Open", false); + LocalPaymentPracticeData.SetCurrentKey("CV No."); + if LocalPaymentPracticeData.FindSet() then begin + CurrentVendor := LocalPaymentPracticeData."CV No."; repeat - if PaymentPracticeData."CV No." <> CurrentVendor then begin + if LocalPaymentPracticeData."CV No." <> CurrentVendor then begin ModesPerVendor.Add(MostFrequentValue(ActualPaymentTimes)); Clear(ActualPaymentTimes); - CurrentVendor := PaymentPracticeData."CV No."; + CurrentVendor := LocalPaymentPracticeData."CV No."; end; - ActualPaymentTimes.Add(PaymentPracticeData."Actual Payment Days"); - until PaymentPracticeData.Next() = 0; + ActualPaymentTimes.Add(LocalPaymentPracticeData."Actual Payment Days"); + until LocalPaymentPracticeData.Next() = 0; ModesPerVendor.Add(MostFrequentValue(ActualPaymentTimes)); end; - PaymentPracticeData.SetRange("Invoice Is Open"); - PaymentPracticeData.SetCurrentKey("Header No.", "Invoice Entry No.", "Source Type"); end; /// @@ -374,7 +409,7 @@ codeunit 693 "Payment Practice Math" end; /// - /// Sorts the supplied integer list in ascending order in place using a simple bubble sort. + /// Sorts the supplied integer list in ascending order in place using a simple insertion sort. /// /// The list of integers to sort. Modified in place. local procedure SortIntegerList(var List: List of [Integer]) diff --git a/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice Small Business.docx b/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice Small Business.docx index 9015ca74adb26118a841fbad028330c994ee4253..d04a6fccd51c8b4f6449e9902fa4f5c1e278c48d 100644 GIT binary patch delta 2820 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 W`>3`G008D(lZSaX2FP>(0000TM8OLH delta 2510 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 Yw3NIgf(ZZs*i@6ddN&4?a{vGU084c}6aWAK diff --git a/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Period.docx b/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Period.docx index 381118eff44d80645d0123e0df9cebeb5fab3b3e..f7bb36940fe232059accd49f2727a666597e017a 100644 GIT binary patch delta 2835 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 CL&k9c delta 2525 zcmV<32_p8E+yUa*0k92Sf3%dmB!US50N7Lj022TJ0AqD?bZ>1~ZEP=TbY*QcE_iKh z)m+1Z<21rw*PSf^DP%Om}s*7vMM$(UOi!&sp-6eOI z+~qpyLkwj}4rdO}&J2f?{{80+-{B+HSRuhZ7P!R@OKh>hw|Imuf1cn3kC0%24X&`o z97}9)Evod~@?(hyPEV07}Su^*Xc0c#*E_ptByv@7;e{LPJecbcs<=As)*}@e` zKRK?S-LG8M5%;XT^lG{L+j5`b9k%k8zrhE=GClGQme^w@V~;(PL*FE)!LdJe@RuE~ zmwL~AwNkx(bTV?wSKvo1u)+;qi3j@AS_#vh9D3x}#Fmit8=U~pk=2c#|H7SkOt&Sp zlKztZ@5RBNe=6ps+mci(**X`!%0|21zYXrO6i>f7c-^$uakb|?Pq_@=S>NCjZc1l0 zJ&I>N%CKjBjxE->5o|uC^vgca+Ez5CO8U8Q_XaD8X2o-5`<2aIB(X{pwetCSjc;@H zwI%+Fop{sZtma8pW4oU%>s;QE1)fO`Krf?un&*k=e=^tJ%1AC6Wy^mh@Z#}GqwF~$TFoMMF2OwL`Tg^ALM_$sqgq3p-_6_+C8QoQsPmUxe) zV3i2Z`2Mf8&WZS?qt5K1bbg{EooiNmb+Y~9eB)Bqus+P ze@-yK029sThd|rt{AHuf3LH^o_QL#Sg{kbpW>V$hyI23_!+Ns_4R1$=fM6_;>${q|2|iF&GUU%%5!FN zT!}u$;=jACuE*}6cVbnH^X%r6nf_b8e}a2#<=M(1<4LC4#M!O{(Rq#`_lKb!?5riVGpba|fQbg>@Hjr$m4f-X+blQWIvU`JXWJrAW(%PPH8U9r4Rf2gff zJjV<-hnD8HGtR4Qm684glS11srKYFOfPC!F>*|9?evFNeI;m3c`O`W2uP8oUGZXM$Qa{7q`1E(_o?Hp#;UH~3bneU|s_m%!p#;uV#iLqP!ENUx`k{?5{ve(c;*Sh=l%p;J_+v3|z zzs>w%q~D9Kx7}rWsakt+d%2CTv5oY;kL2&E-n$F~JgBXaZ4;lCTM{qFf29Qjv4t#E z(ZMInb?al>Qk2oUd$sZY(L&nCl`31SavqjtuYNuA2>4YxW2~^NBI?HAT<@0E4w)$U zX{k=Je0tVHp^d1m&OKCyNzd#bd+O=zk5M&qs?H>0;Nv=N^YwDo8k+Om@?PPVw-L#d zr{_M8Z_hY@C!yN@T?~4_` zh!5)<6J5cb1mrwb+7XMYKA{%3k>Ri!Ig#xCMAs*Mc@nCv4)YVS06k#|5$Oq6!a3rvu`fElTnRgajI&#|&niRt6Q*?^pI*t&XZ>U2vn>W(I{^9x38UAr`j>nZnYa|Zm)|C{=MxttL%=E`rg|j+WjI^ zS>Rj4bGf3Mm1f@QXT4#<+?@HQI;}6;v)EoUh6U;yO3mf8e}!;cXZk9eB3H;3-#4ZW zi(;)n`CC}MA`f>@MO810SrJ zn5<)^lE0S2C-L{Fyl2YW^Tf6oQ}eBb5Io&_hoI?}5Fg7n*vh)8`i2B|bc5(@^|Pco z+M!I&pE9ZQ#@N}X+Ha|oqLBMiVVdUAs~*uDopw%``kRwDqrpYFGo3{|S(_y5E!N2O88udq)BY3HQ1&0;exOdbUQOG1_ zk>u0ZIO~pjm1bEi+NL--qgY=7C97EkFOOnMKw7>^G48zh!KG nRkY*%yP{9=AG79X=>-b3l)NN@2><}tRFm0lHwLn100000b Date: Fri, 15 May 2026 13:08:42 +0200 Subject: [PATCH 10/12] fix overflow of company size --- .../App/src/Core/PaymentPracticeMath.Codeunit.al | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al index 7b0da46c42..c5ea87d875 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al @@ -191,10 +191,10 @@ codeunit 693 "Payment Practice Math" VendorLedgerEntryForSum: Record "Vendor Ledger Entry"; CompanySize: Record "Company Size"; Vendor: Record Vendor; - VendorCompanySizeCache: Dictionary of [Code[20], Code[10]]; + VendorCompanySizeCache: Dictionary of [Code[20], Code[20]]; VendorExcludedCache: Dictionary of [Code[20], Boolean]; - SmallBusinessCache: Dictionary of [Code[10], Boolean]; - CompanySizeCode: Code[10]; + SmallBusinessCache: Dictionary of [Code[20], Boolean]; + CompanySizeCode: Code[20]; PaidAmount: Decimal; TotalAmountSmallBusinesses: Decimal; TotalAmountAllVendors: Decimal; From 5fc29069d933da881db1dbf6f76ec1937da0f805 Mon Sep 17 00:00:00 2001 From: Aleksandr Gladkov Date: Fri, 15 May 2026 13:12:29 +0200 Subject: [PATCH 11/12] fix review comments, part 2 --- .../PaymPracDisputeRetHdlr.Codeunit.al | 30 ++++++++----------- .../Core/PaymentPracticeBuilders.Codeunit.al | 8 +++-- .../Test/src/PaymentPracticesUT.Codeunit.al | 2 ++ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al index 157820329e..e8b59c3e1f 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracDisputeRetHdlr.Codeunit.al @@ -4,8 +4,6 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; -using Microsoft.Purchases.Payables; - codeunit 681 "Paym. Prac. Dispute Ret. Hdlr" implements PaymentPracticeSchemeHandler { Access = Internal; @@ -16,20 +14,18 @@ codeunit 681 "Paym. Prac. Dispute Ret. Hdlr" implements PaymentPracticeSchemeHan end; procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean - var - VendorLedgerEntry: Record "Vendor Ledger Entry"; begin - if PaymentPracticeData."Source Type" <> PaymentPracticeData."Source Type"::Vendor then - exit(true); - - VendorLedgerEntry.SetLoadFields("SCF Payment Date", "Dispute Status"); - if VendorLedgerEntry.Get(PaymentPracticeData."Invoice Entry No.") then begin - PaymentPracticeData."SCF Payment Date" := VendorLedgerEntry."SCF Payment Date"; - PaymentPracticeData."Dispute Status" := VendorLedgerEntry."Dispute Status"; - - if PaymentPracticeData."SCF Payment Date" <> 0D then + if PaymentPracticeData."Source Type" = PaymentPracticeData."Source Type"::Vendor then + if PaymentPracticeData."SCF Payment Date" <> 0D then begin PaymentPracticeData."Actual Payment Days" := PaymentPracticeData."SCF Payment Date" - PaymentPracticeData."Invoice Received Date"; - end; + if PaymentPracticeData."Actual Payment Days" < 0 then + PaymentPracticeData."Actual Payment Days" := 0; + end; + + if (not PaymentPracticeData."Invoice Is Open") and + (PaymentPracticeData."Actual Payment Days" > PaymentPracticeData."Agreed Payment Days") + then + PaymentPracticeData."Overdue Due to Dispute" := PaymentPracticeData."Dispute Status" <> ''; exit(true); end; @@ -42,7 +38,7 @@ codeunit 681 "Paym. Prac. Dispute Ret. Hdlr" implements PaymentPracticeSchemeHan OverdueCount: Integer; OverdueDueToDisputeCount: Integer; begin - if PaymentPracticeData.FindSet(true) then + if PaymentPracticeData.FindSet() then repeat if not PaymentPracticeData."Invoice Is Open" then begin TotalPayments += 1; @@ -50,9 +46,7 @@ codeunit 681 "Paym. Prac. Dispute Ret. Hdlr" implements PaymentPracticeSchemeHan if PaymentPracticeData."Actual Payment Days" > PaymentPracticeData."Agreed Payment Days" then begin OverdueCount += 1; TotalOverdueAmount += PaymentPracticeData."Invoice Amount"; - PaymentPracticeData."Overdue Due to Dispute" := PaymentPracticeData."Dispute Status" <> ''; - PaymentPracticeData.Modify(); - if PaymentPracticeData."Dispute Status" <> '' then + if PaymentPracticeData."Overdue Due to Dispute" then OverdueDueToDisputeCount += 1; end; end; diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al index 8f56133732..8b73e380e4 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeBuilders.Codeunit.al @@ -23,7 +23,9 @@ codeunit 688 "Payment Practice Builders" SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; LastVendNo := ''; Vendor.SetLoadFields("No.", "Exclude from Pmt. Practices", "Company Size Code"); - VendorLedgerEntry.SetLoadFields("Entry No.", "Vendor No.", "External Document No.", "Document No.", "Posting Date", "Invoice Received Date", "Document Date", "Due Date", Open, "Closed at Date", "Closed by Entry No.", "SCF Payment Date"); + VendorLedgerEntry.SetLoadFields( + "Entry No.", "Vendor No.", "External Document No.", "Document No.", "Posting Date", "Invoice Received Date", "Document Date", + "Due Date", Open, "Closed at Date", "Closed by Entry No.", "SCF Payment Date", "Dispute Status"); VendorLedgerEntry.SetCurrentKey("Vendor No."); VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); VendorLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); @@ -59,7 +61,9 @@ codeunit 688 "Payment Practice Builders" SchemeHandler := PaymentPracticeHeader."Reporting Scheme"; LastCustNo := ''; Customer.SetLoadFields("No.", "Exclude from Pmt. Practices"); - CustLedgerEntry.SetLoadFields("Entry No.", "Customer No.", "External Document No.", "Document No.", "Posting Date", "Document Date", "Due Date", Open, "Closed at Date", "Closed by Entry No."); + CustLedgerEntry.SetLoadFields( + "Entry No.", "Customer No.", "External Document No.", "Document No.", "Posting Date", "Document Date", + "Due Date", Open, "Closed at Date", "Closed by Entry No.", "Dispute Status"); CustLedgerEntry.SetCurrentKey("Customer No."); CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); CustLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); diff --git a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al index 89b26f2a01..f570262fff 100644 --- a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al @@ -1366,6 +1366,8 @@ codeunit 134197 "Payment Practices UT" PaymentPracticeData."Invoice Amount" := InvoiceAmount; if IsDisputed then PaymentPracticeData."Dispute Status" := 'DISPUTED'; + if (not IsOpen) and (ActualPaymentDays > AgreedPaymentDays) then + PaymentPracticeData."Overdue Due to Dispute" := IsDisputed; PaymentPracticeData.Insert(); end; From 6258c2711289cbaf35036435ff80596185a668ac Mon Sep 17 00:00:00 2001 From: AndreasHans Date: Fri, 15 May 2026 14:12:01 +0200 Subject: [PATCH 12/12] more caching --- .../PaymPracSmallBusHandler.Codeunit.al | 42 ++- .../src/Core/PaymentPracticeMath.Codeunit.al | 274 ++++++------------ 2 files changed, 109 insertions(+), 207 deletions(-) diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al index f5c9bd3fde..c086efb75d 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/Implementations/PaymPracSmallBusHandler.Codeunit.al @@ -12,6 +12,7 @@ codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHa var PaymentPracticeMath: Codeunit "Payment Practice Math"; + SmallBusinessCache: Dictionary of [Code[20], Boolean]; WrongHeaderTypeErr: Label 'Payment Practice Header Type must be Vendor for the Small Business reporting scheme.'; WrongHeaderAggErr: Label 'Payment Practice Aggregation Type must be Period for the Small Business reporting scheme.'; @@ -27,45 +28,58 @@ codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHa var Vendor: Record Vendor; CompanySize: Record "Company Size"; + IsSmallBusiness: Boolean; begin if PaymentPracticeData."Source Type" <> PaymentPracticeData."Source Type"::Vendor then exit(false); + if SmallBusinessCache.Get(PaymentPracticeData."CV No.", IsSmallBusiness) then + exit(IsSmallBusiness); + Vendor.SetLoadFields("Company Size Code"); - if not Vendor.Get(PaymentPracticeData."CV No.") then + if not Vendor.Get(PaymentPracticeData."CV No.") then begin + SmallBusinessCache.Add(PaymentPracticeData."CV No.", false); exit(false); + end; if CompanySize.Get(Vendor."Company Size Code") then - exit(CompanySize."Small Business") + IsSmallBusiness := CompanySize."Small Business" else - exit(false); + IsSmallBusiness := false; + + SmallBusinessCache.Add(PaymentPracticeData."CV No.", IsSmallBusiness); + exit(IsSmallBusiness); end; procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data") var TotalCount: Integer; TotalValue: Decimal; + ModePaymentTime: Integer; + ModePaymentTimeMin: Integer; + ModePaymentTimeMax: Integer; MedianPaymentTime: Decimal; P80PaymentTime: Integer; P95PaymentTime: Integer; + PctPeppolEnabled: Decimal; + PctSmallBusinessPayments: Decimal; begin - PaymentPracticeData.SetRange("Invoice Is Open", false); - TotalCount := PaymentPracticeData.Count(); - PaymentPracticeData.CalcSums("Invoice Amount"); - TotalValue := PaymentPracticeData."Invoice Amount"; - PaymentPracticeData.SetRange("Invoice Is Open"); + PaymentPracticeMath.CalculateHeaderStatistics( + PaymentPracticeData, TotalCount, TotalValue, + ModePaymentTime, ModePaymentTimeMin, ModePaymentTimeMax, + MedianPaymentTime, P80PaymentTime, P95PaymentTime, + PctPeppolEnabled, PctSmallBusinessPayments); PaymentPracticeHeader."Total Number of Payments" := TotalCount; PaymentPracticeHeader."Total Amount of Payments" := TotalValue; - PaymentPracticeHeader."Mode Payment Time" := PaymentPracticeMath.GetModePaymentTime(PaymentPracticeData); - PaymentPracticeHeader."Mode Payment Time Min." := PaymentPracticeMath.GetModePaymentTimeMin(PaymentPracticeData); - PaymentPracticeHeader."Mode Payment Time Max." := PaymentPracticeMath.GetModePaymentTimeMax(PaymentPracticeData); - PaymentPracticeMath.GetPaymentTimeStatistics(PaymentPracticeData, MedianPaymentTime, P80PaymentTime, P95PaymentTime); + PaymentPracticeHeader."Mode Payment Time" := ModePaymentTime; + PaymentPracticeHeader."Mode Payment Time Min." := ModePaymentTimeMin; + PaymentPracticeHeader."Mode Payment Time Max." := ModePaymentTimeMax; PaymentPracticeHeader."Median Payment Time" := MedianPaymentTime; PaymentPracticeHeader."80th Percentile Payment Time" := P80PaymentTime; PaymentPracticeHeader."95th Percentile Payment Time" := P95PaymentTime; - PaymentPracticeHeader."Pct Peppol Enabled" := PaymentPracticeMath.GetPctPeppolEnabled(PaymentPracticeData); - PaymentPracticeHeader."Pct Small Business Payments" := PaymentPracticeMath.GetPctSmallBusinessPayments(PaymentPracticeHeader); + PaymentPracticeHeader."Pct Peppol Enabled" := PctPeppolEnabled; + PaymentPracticeHeader."Pct Small Business Payments" := PctSmallBusinessPayments; end; procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data") diff --git a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al index c5ea87d875..7af4253f14 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/App/src/Core/PaymentPracticeMath.Codeunit.al @@ -4,7 +4,6 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.Finance.Analysis; -using Microsoft.Purchases.Payables; using Microsoft.Purchases.Vendor; codeunit 693 "Payment Practice Math" @@ -77,190 +76,109 @@ codeunit 693 "Payment Practice Math" end; /// - /// Calculates the mode (most frequent value) of the actual payment times across all closed invoices in the provided dataset. + /// Aggregates all closed-invoice statistics required for a Small Business reporting-scheme header in a single + /// pass over the filtered set. This consolidates what previously required + /// 5+ independent full-table scans (mode, per-vendor mode min/max, median/P80/P95, % Peppol enabled, % small + /// business payments) into one iteration plus one small in-memory post-processing step. /// /// The payment practice data to evaluate. Filters are temporarily adjusted but restored before returning. - /// The most frequently occurring number of actual payment days, or 0 if no closed invoices exist. - procedure GetModePaymentTime(var PaymentPracticeData: Record "Payment Practice Data"): Integer - var - ActualPaymentTimes: List of [Integer]; - begin - PaymentPracticeData.SetRange("Invoice Is Open", false); - if PaymentPracticeData.FindSet() then - repeat - ActualPaymentTimes.Add(PaymentPracticeData."Actual Payment Days"); - until PaymentPracticeData.Next() = 0; - PaymentPracticeData.SetRange("Invoice Is Open"); - exit(MostFrequentValue(ActualPaymentTimes)); - end; - - /// - /// Calculates the smallest per-vendor mode of actual payment times across all vendors in the provided dataset. - /// - /// The payment practice data to evaluate, grouped by vendor. - /// The minimum of all per-vendor modes, or 0 if no closed invoices exist. - procedure GetModePaymentTimeMin(var PaymentPracticeData: Record "Payment Practice Data"): Integer - var - ModesPerVendor: List of [Integer]; - begin - GetModesPerVendor(PaymentPracticeData, ModesPerVendor); - exit(MinOfList(ModesPerVendor)); - end; - - /// - /// Calculates the largest per-vendor mode of actual payment times across all vendors in the provided dataset. - /// - /// The payment practice data to evaluate, grouped by vendor. - /// The maximum of all per-vendor modes, or 0 if no closed invoices exist. - procedure GetModePaymentTimeMax(var PaymentPracticeData: Record "Payment Practice Data"): Integer + /// Output: number of closed invoice rows. + /// Output: sum of "Invoice Amount" across closed invoices. + /// Output: most frequent "Actual Payment Days" across all closed invoices. + /// Output: smallest per-vendor mode of "Actual Payment Days". + /// Output: largest per-vendor mode of "Actual Payment Days". + /// Output: median of "Actual Payment Days" across closed invoices. + /// Output: 80th percentile of "Actual Payment Days". + /// Output: 95th percentile of "Actual Payment Days". + /// Output: percentage of closed-invoice rows whose vendor has a GLN. + /// Output: percentage of closed-invoice value attributable to small-business vendors. + procedure CalculateHeaderStatistics(var PaymentPracticeData: Record "Payment Practice Data"; var TotalCount: Integer; var TotalValue: Decimal; var ModePaymentTime: Integer; var ModePaymentTimeMin: Integer; var ModePaymentTimeMax: Integer; var MedianPaymentTime: Decimal; var P80PaymentTime: Integer; var P95PaymentTime: Integer; var PctPeppolEnabled: Decimal; var PctSmallBusinessPayments: Decimal) var + Vendor: Record Vendor; + CompanySize: Record "Company Size"; + AllPaymentTimes: List of [Integer]; + PerVendorTimes: Dictionary of [Code[20], List of [Integer]]; + VendorTimes: List of [Integer]; ModesPerVendor: List of [Integer]; + VendorGLNCache: Dictionary of [Code[20], Boolean]; + SmallBusinessCache: Dictionary of [Code[20], Boolean]; + HasGLN: Boolean; + IsSmallBusiness: Boolean; + PeppolCount: Integer; + SmallBusinessValue: Decimal; + PaymentTime: Integer; + CVNo: Code[20]; + CompanySizeCode: Code[20]; begin - GetModesPerVendor(PaymentPracticeData, ModesPerVendor); - exit(MaxOfList(ModesPerVendor)); - end; - - /// - /// Calculates the median, 80th percentile, and 95th percentile of actual payment times across all closed invoices in the provided dataset. - /// - /// The payment practice data to evaluate. Filters are temporarily adjusted but restored before returning. - /// Output parameter that receives the median number of actual payment days, or 0 if no closed invoices exist. - /// Output parameter that receives the 80th percentile of actual payment days, or 0 if no closed invoices exist. - /// Output parameter that receives the 95th percentile of actual payment days, or 0 if no closed invoices exist. - procedure GetPaymentTimeStatistics(var PaymentPracticeData: Record "Payment Practice Data"; var MedianPaymentTime: Decimal; var P80PaymentTime: Integer; var P95PaymentTime: Integer) - var - ActualPaymentTimes: List of [Integer]; - begin + TotalCount := 0; + TotalValue := 0; + ModePaymentTime := 0; + ModePaymentTimeMin := 0; + ModePaymentTimeMax := 0; MedianPaymentTime := 0; P80PaymentTime := 0; P95PaymentTime := 0; + PctPeppolEnabled := 0; + PctSmallBusinessPayments := 0; - GetClosedInvoicePaymentTimes(PaymentPracticeData, ActualPaymentTimes); - if ActualPaymentTimes.Count() = 0 then - exit; - - SortIntegerList(ActualPaymentTimes); - MedianPaymentTime := MedianFromSorted(ActualPaymentTimes); - P80PaymentTime := PercentileFromSorted(ActualPaymentTimes, 80); - P95PaymentTime := PercentileFromSorted(ActualPaymentTimes, 95); - end; - - /// - /// Calculates the percentage of closed-invoice transactions whose vendor is Peppol enabled (i.e. has a non-empty GLN). - /// - /// The payment practice data to evaluate. A per-vendor GLN cache is used to avoid repeated lookups. - /// The percentage (0-100) of closed-invoice rows from Peppol-enabled vendors, or 0 when there are no closed invoices. - procedure GetPctPeppolEnabled(var PaymentPracticeData: Record "Payment Practice Data"): Decimal - var - Vendor: Record Vendor; - VendorGLNCache: Dictionary of [Code[20], Boolean]; - HasGLN: Boolean; - Total: Integer; - PeppolCount: Integer; - begin PaymentPracticeData.SetRange("Invoice Is Open", false); if PaymentPracticeData.FindSet() then repeat + TotalCount += 1; + TotalValue += PaymentPracticeData."Invoice Amount"; + + PaymentTime := PaymentPracticeData."Actual Payment Days"; + AllPaymentTimes.Add(PaymentTime); + + CVNo := PaymentPracticeData."CV No."; + if PerVendorTimes.Get(CVNo, VendorTimes) then begin + VendorTimes.Add(PaymentTime); + PerVendorTimes.Set(CVNo, VendorTimes); + end else begin + Clear(VendorTimes); + VendorTimes.Add(PaymentTime); + PerVendorTimes.Add(CVNo, VendorTimes); + end; - Total += 1; - if not VendorGLNCache.Get(PaymentPracticeData."CV No.", HasGLN) then begin + // Peppol enabled (vendor GLN), cached per vendor + if not VendorGLNCache.Get(CVNo, HasGLN) then begin Vendor.SetLoadFields(GLN); - HasGLN := Vendor.Get(PaymentPracticeData."CV No.") and (Vendor.GLN <> ''); - VendorGLNCache.Add(PaymentPracticeData."CV No.", HasGLN); + HasGLN := Vendor.Get(CVNo) and (Vendor.GLN <> ''); + VendorGLNCache.Add(CVNo, HasGLN); end; - if HasGLN then PeppolCount += 1; - until PaymentPracticeData.Next() = 0; - - PaymentPracticeData.SetRange("Invoice Is Open"); - if Total = 0 then - exit(0); - - exit(PeppolCount / Total * 100); - end; - /// - /// Calculates the percentage of total vendor invoice value (within the header period) that is paid to small-business vendors. - /// - /// The payment practice header providing the reporting period (Starting/Ending Date). - /// The percentage (0-100) of paid invoice value attributable to vendors flagged as Small Business, or 0 when no invoice value exists. - procedure GetPctSmallBusinessPayments(PaymentPracticeHeader: Record "Payment Practice Header"): Decimal - var - VendorLedgerEntry: Record "Vendor Ledger Entry"; - VendorLedgerEntryForSum: Record "Vendor Ledger Entry"; - CompanySize: Record "Company Size"; - Vendor: Record Vendor; - VendorCompanySizeCache: Dictionary of [Code[20], Code[20]]; - VendorExcludedCache: Dictionary of [Code[20], Boolean]; - SmallBusinessCache: Dictionary of [Code[20], Boolean]; - CompanySizeCode: Code[20]; - PaidAmount: Decimal; - TotalAmountSmallBusinesses: Decimal; - TotalAmountAllVendors: Decimal; - begin - // Walk the filtered Vendor Ledger Entries grouped by vendor. For each distinct vendor we - // (a) look up "Company Size Code" / "Exclude from Pmt. Practices" once (cached in a Dictionary), and - // (b) aggregate Amount / "Remaining Amount" with a single CalcSums call, instead of - // forcing the FlowFields to evaluate on every row via SetAutoCalcFields. - VendorLedgerEntry.SetCurrentKey("Vendor No.", "Posting Date"); - VendorLedgerEntry.SetLoadFields("Vendor No."); - VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); - VendorLedgerEntry.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); - if VendorLedgerEntry.FindSet() then - repeat - if not VendorCompanySizeCache.ContainsKey(VendorLedgerEntry."Vendor No.") then begin - Vendor.SetLoadFields("Company Size Code", "Exclude from Pmt. Practices"); - if Vendor.Get(VendorLedgerEntry."Vendor No.") then begin - CompanySizeCode := Vendor."Company Size Code"; - VendorExcludedCache.Add(VendorLedgerEntry."Vendor No.", Vendor."Exclude from Pmt. Practices"); - end else begin - CompanySizeCode := ''; - VendorExcludedCache.Add(VendorLedgerEntry."Vendor No.", false); - end; - VendorCompanySizeCache.Add(VendorLedgerEntry."Vendor No.", CompanySizeCode); - - if not SmallBusinessCache.ContainsKey(CompanySizeCode) then - SmallBusinessCache.Add(CompanySizeCode, (CompanySizeCode <> '') and CompanySize.Get(CompanySizeCode) and CompanySize."Small Business"); - - if not VendorExcludedCache.Get(VendorLedgerEntry."Vendor No.") then begin - VendorLedgerEntryForSum.SetCurrentKey("Vendor No.", "Posting Date"); - VendorLedgerEntryForSum.SetRange("Vendor No.", VendorLedgerEntry."Vendor No."); - VendorLedgerEntryForSum.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); - VendorLedgerEntryForSum.SetRange("Posting Date", PaymentPracticeHeader."Starting Date", PaymentPracticeHeader."Ending Date"); - VendorLedgerEntryForSum.SetAutoCalcFields(Amount, "Remaining Amount"); - PaidAmount := 0; - if VendorLedgerEntryForSum.FindSet() then - repeat - PaidAmount += VendorLedgerEntryForSum.Amount - VendorLedgerEntryForSum."Remaining Amount"; - until VendorLedgerEntryForSum.Next() = 0; - - if SmallBusinessCache.Get(CompanySizeCode) then - TotalAmountSmallBusinesses += PaidAmount; - TotalAmountAllVendors += PaidAmount; - end; + // Small business value, using "Company Size Code" already stored on the data row (cached per code) + CompanySizeCode := PaymentPracticeData."Company Size Code"; + if not SmallBusinessCache.Get(CompanySizeCode, IsSmallBusiness) then begin + IsSmallBusiness := (CompanySizeCode <> '') and CompanySize.Get(CompanySizeCode) and CompanySize."Small Business"; + SmallBusinessCache.Add(CompanySizeCode, IsSmallBusiness); end; - until VendorLedgerEntry.Next() = 0; + if IsSmallBusiness then + SmallBusinessValue += PaymentPracticeData."Invoice Amount"; + until PaymentPracticeData.Next() = 0; + PaymentPracticeData.SetRange("Invoice Is Open"); - if TotalAmountAllVendors = 0 then - exit(0); + if AllPaymentTimes.Count() > 0 then begin + ModePaymentTime := MostFrequentValue(AllPaymentTimes); + SortIntegerList(AllPaymentTimes); + MedianPaymentTime := MedianFromSorted(AllPaymentTimes); + P80PaymentTime := PercentileFromSorted(AllPaymentTimes, 80); + P95PaymentTime := PercentileFromSorted(AllPaymentTimes, 95); + end; - exit(TotalAmountSmallBusinesses / TotalAmountAllVendors * 100); - end; + foreach CVNo in PerVendorTimes.Keys() do begin + VendorTimes := PerVendorTimes.Get(CVNo); + ModesPerVendor.Add(MostFrequentValue(VendorTimes)); + end; + ModePaymentTimeMin := MinOfList(ModesPerVendor); + ModePaymentTimeMax := MaxOfList(ModesPerVendor); - /// - /// Collects the actual payment days for all closed invoices in the dataset into a list. Filters are temporarily adjusted but restored before returning. - /// - /// The payment practice data to scan. - /// Output list that will receive one integer per closed invoice. - local procedure GetClosedInvoicePaymentTimes(var PaymentPracticeData: Record "Payment Practice Data"; var ActualPaymentTimes: List of [Integer]) - begin - PaymentPracticeData.SetRange("Invoice Is Open", false); - if PaymentPracticeData.FindSet() then - repeat - ActualPaymentTimes.Add(PaymentPracticeData."Actual Payment Days"); - until PaymentPracticeData.Next() = 0; - PaymentPracticeData.SetRange("Invoice Is Open"); + if TotalCount > 0 then + PctPeppolEnabled := PeppolCount / TotalCount * 100; + if TotalValue <> 0 then + PctSmallBusinessPayments := SmallBusinessValue / TotalValue * 100; end; /// @@ -299,36 +217,6 @@ codeunit 693 "Payment Practice Math" exit(SortedList.Get(Index)); end; - /// - /// Computes the mode of actual payment times for each vendor in the dataset and returns one mode value per vendor. - /// Relies on the data being sorted by "CV No." which is set as the current key inside the procedure and restored before returning. - /// - /// The payment practice data to scan. - /// Output list that will receive one mode value per vendor that has at least one closed invoice. - local procedure GetModesPerVendor(var PaymentPracticeData: Record "Payment Practice Data"; var ModesPerVendor: List of [Integer]) - var - LocalPaymentPracticeData: Record "Payment Practice Data"; - ActualPaymentTimes: List of [Integer]; - CurrentVendor: Code[20]; - begin - // Use a separate record variable so we don't mutate the caller's filters or current key. - LocalPaymentPracticeData.CopyFilters(PaymentPracticeData); - LocalPaymentPracticeData.SetRange("Invoice Is Open", false); - LocalPaymentPracticeData.SetCurrentKey("CV No."); - if LocalPaymentPracticeData.FindSet() then begin - CurrentVendor := LocalPaymentPracticeData."CV No."; - repeat - if LocalPaymentPracticeData."CV No." <> CurrentVendor then begin - ModesPerVendor.Add(MostFrequentValue(ActualPaymentTimes)); - Clear(ActualPaymentTimes); - CurrentVendor := LocalPaymentPracticeData."CV No."; - end; - ActualPaymentTimes.Add(LocalPaymentPracticeData."Actual Payment Days"); - until LocalPaymentPracticeData.Next() = 0; - ModesPerVendor.Add(MostFrequentValue(ActualPaymentTimes)); - end; - end; - /// /// Returns the smallest value in the supplied integer list. ///