Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fc0689b
GB + AU + tests
Apr 27, 2026
f12e66b
GB + AU + tests
Apr 27, 2026
7f473e0
Merge branch 'features/629871-master-Payment-Practices-W1-GB-Simple' …
Apr 27, 2026
3ddd9d5
Merge branch 'main' of https://github.com/microsoft/BCApps into featu…
Apr 28, 2026
7a426db
Merge branch 'features/629871-master-Payment-Practices-W1-GB-Simple' …
Apr 28, 2026
64360f7
Merge branch 'main' of https://github.com/microsoft/BCApps into featu…
Apr 29, 2026
335a48a
Merge branch 'main' of https://github.com/microsoft/BCApps into featu…
Apr 29, 2026
f2d919a
Changes for AU
AndreasHans May 7, 2026
2b8f36d
Merge branch 'main' of https://github.com/microsoft/BCApps into featu…
May 8, 2026
e9eb12f
Merge branch 'features/629871-master-Payment-Practices-W1-GB-Simple' …
May 8, 2026
37f2ca4
Merge branch 'main' of https://github.com/microsoft/BCApps into featu…
May 11, 2026
ed429cb
fix review comments
May 13, 2026
f1d7c4a
Merge branch 'main' of https://github.com/microsoft/BCApps into featu…
May 14, 2026
016d56d
more fixes for copilot bot comments
May 14, 2026
09c9eac
fix for non-existing library procedure
May 14, 2026
5db3eb9
fixed bug in sorting list procedure
May 14, 2026
0bd53d5
improve tooltips, add xml tags, some renaming
AndreasHans May 15, 2026
a64bf43
Merge branch 'main' into features/629871-master-Payment-Practices-W1-…
AndreasHans May 15, 2026
ba3a425
resolve some pr comments and fix a test
AndreasHans May 15, 2026
4d94c07
fix overflow of company size
AndreasHans May 15, 2026
5fc2906
fix review comments, part 2
May 15, 2026
6258c27
more caching
AndreasHans May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/Apps/W1/PaymentPractices/App/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"platform": "29.0.0.0",
"idRanges": [
{
"from": 685,
"to": 694
"from": 680,
"to": 698
}
],
"resourceExposurePolicy": {
Expand All @@ -29,5 +29,12 @@
"target": "Cloud",
"features": [
"TranslationFile"
],
"internalsVisibleTo": [
{
"id": "cc329ed7-8840-45f6-860b-3eb99c408998",
"name": "Payment Practices Test Library",
"publisher": "Microsoft"
}
]
}
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// ------------------------------------------------------------------------------------------------
// 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 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
Comment thread
AleksanderGladkov marked this conversation as resolved.
begin
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";
Comment thread
AleksanderGladkov marked this conversation as resolved.
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;

Comment thread
AleksanderGladkov marked this conversation as resolved.
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
Comment thread
AleksanderGladkov marked this conversation as resolved.
TotalPayments += 1;
TotalAmount += PaymentPracticeData."Invoice Amount";
if PaymentPracticeData."Actual Payment Days" > PaymentPracticeData."Agreed Payment Days" then begin
OverdueCount += 1;
TotalOverdueAmount += PaymentPracticeData."Invoice Amount";
if PaymentPracticeData."Overdue Due to Dispute" then
OverdueDueToDisputeCount += 1;
Comment thread
AleksanderGladkov marked this conversation as resolved.
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,25 @@ 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
PaymentPracticeData.SetRange("Source Type", SourceType);
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);
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;
end;
end;
Expand All @@ -47,7 +54,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;
Expand All @@ -57,10 +64,23 @@ 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;

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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.';
Comment thread
AleksanderGladkov marked this conversation as resolved.

procedure PrepareLayout();
var
Expand All @@ -28,9 +29,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();
Expand All @@ -45,15 +48,20 @@ 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;

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."Reporting Scheme" = PaymentPracticeHeader."Reporting Scheme"::"Small Business" then
Error(WrongHeaderAggErr);
Comment thread
AleksanderGladkov marked this conversation as resolved.
end;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// ------------------------------------------------------------------------------------------------
// 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.Vendor;

codeunit 682 "Paym. Prac. Small Bus. Handler" implements PaymentPracticeSchemeHandler
{
Access = Internal;

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.';

procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header")
begin
if PaymentPracticeHeader."Header Type" <> PaymentPracticeHeader."Header Type"::Vendor then
Error(WrongHeaderTypeErr);
if PaymentPracticeHeader."Aggregation Type" <> PaymentPracticeHeader."Aggregation Type"::Period then
Error(WrongHeaderAggErr);
end;

procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean
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 begin
SmallBusinessCache.Add(PaymentPracticeData."CV No.", false);
exit(false);
end;

if CompanySize.Get(Vendor."Company Size Code") then
IsSmallBusiness := CompanySize."Small Business"
else
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
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" := 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" := PctPeppolEnabled;
PaymentPracticeHeader."Pct Small Business Payments" := PctSmallBusinessPayments;
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);
Comment thread
AleksanderGladkov marked this conversation as resolved.
InvoiceCount := PaymentPracticeData.Count();
PaymentPracticeData.CalcSums("Invoice Amount");
InvoiceValue := PaymentPracticeData."Invoice Amount";
PaymentPracticeData.SetRange("Invoice Is Open");

PaymentPracticeLine."Invoice Count" := InvoiceCount;
PaymentPracticeLine."Invoice Value" := InvoiceValue;
end;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// ------------------------------------------------------------------------------------------------
// 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
{
/// <summary>
/// Validates the Payment Practice Header before data generation.
/// </summary>
/// <param name="PaymentPracticeHeader">The header to validate.</param>
procedure ValidateHeader(var PaymentPracticeHeader: Record "Payment Practice Header")

/// <summary>
/// Enriches or filters a Payment Practice Data row before insertion.
/// Returns true to include the row, false to skip it.
/// </summary>
/// <param name="PaymentPracticeData">The data row to enrich/filter.</param>
/// <returns>True to include the row, false to skip.</returns>
procedure UpdatePaymentPracData(var PaymentPracticeData: Record "Payment Practice Data"): Boolean

/// <summary>
/// Calculates scheme-specific header totals after standard totals are generated.
/// </summary>
/// <param name="PaymentPracticeHeader">The header to update with totals.</param>
/// <param name="PaymentPracticeData">The data to aggregate from.</param>
procedure CalculateHeaderTotals(var PaymentPracticeHeader: Record "Payment Practice Header"; var PaymentPracticeData: Record "Payment Practice Data")

/// <summary>
/// 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.
/// </summary>
/// <param name="PaymentPracticeLine">The line to update with totals.</param>
/// <param name="PaymentPracticeData">The data to aggregate from. Filters set by the caller define the slice.</param>
procedure CalculateLineTotals(var PaymentPracticeLine: Record "Payment Practice Line"; var PaymentPracticeData: Record "Payment Practice Data")
}
Loading
Loading