Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// ------------------------------------------------------------------------------------------------
// 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 687 "Paym. Prac. Reporting Scheme"
{
Extensible = true;

value(0; Standard)
{
}
value(1; "Percentiles; Modes; Pct Peppol Enabled; Pct Small Business Payments")
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,22 @@ 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');
FeatureTelemetry.LogUsage('0000KSU', 'Payment Practices', 'Period layout used.')

case ReportingScheme of
ReportingScheme::"Percentiles; Modes; Pct Peppol Enabled; Pct Small Business Payments":
begin
DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_PeriodDetailedLayout');
FeatureTelemetry.LogUsage('0000KSY', 'Payment Practices', 'Period detailed layout used.')
end;
else begin
DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_PeriodLayout');
FeatureTelemetry.LogUsage('0000KSU', 'Payment Practices', 'Period layout used.')
end;
end;
end;

procedure GenerateLines(var PaymentPracticeData: Record "Payment Practice Data"; PaymentPracticeHeader: Record "Payment Practice Header");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ codeunit 686 "Paym. Prac. Size Aggregator" implements PaymentPracticeLinesAggreg
FeatureTelemetry: Codeunit "Feature Telemetry";
WrongHeaderTypeErr: Label 'Payment Practice Header Type must be Vendor for this aggregation type.';

procedure PrepareLayout();
procedure PrepareLayout(ReportingScheme: Enum "Paym. Prac. Reporting Scheme");
var
DesignTimeReportSelection: Codeunit "Design-time Report Selection";
begin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ interface PaymentPracticeLinesAggregator
/// <summary>
/// Prepare the layout to be used for Payment Practice report that is suitable for the aggregation type of the header/lines.
/// </summary>
procedure PrepareLayout()
/// <param name="ReportingScheme">Indicates which reporting scheme should be used for the layout.</param>
procedure PrepareLayout(ReportingScheme: Enum "Paym. Prac. Reporting Scheme");

/// <summary>
/// Generate the lines for the Payment Practice report based on the Payment Practice Data raw data and Payment Practice Header fields.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ codeunit 688 "Payment Practice Builders"
Vendor.Get(VendorLedgerEntry."Vendor No.");
LastVendNo := Vendor."No.";
end;
if Vendor."Exclude from Pmt. Practices" then begin
if SkipVendor(Vendor, PaymentPracticeHeader."Only Small Businesses") then begin
// Skip all entries associated with this Vendor
VendorLedgerEntry.SetRange("Vendor No.", Vendor."No.");
VendorLedgerEntry.FindLast();
Expand All @@ -44,6 +44,22 @@ codeunit 688 "Payment Practice Builders"
until VendorLedgerEntry.Next() = 0;
end;

procedure SkipVendor(Vendor: Record Vendor; OnlySmallBusinesses: Boolean): Boolean
var
CompanySize: Record "Company Size";
begin
if Vendor."Exclude from Pmt. Practices" then
exit(true);

if not OnlySmallBusinesses then
exit(false);

if CompanySize.Get(Vendor."Company Size Code") then
exit(not CompanySize."Small Business")
else
exit(true)
end;

procedure BuildPaymentPracticeDataForCustomer(var PaymentPracticeData: Record "Payment Practice Data"; PaymentPracticeHeader: Record "Payment Practice Header")
var
Customer: Record Customer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Finance.Analysis;

using Microsoft.Purchases.Payables;
using Microsoft.Purchases.Vendor;

codeunit 693 "Payment Practice Math"
{
Access = internal;
Expand Down Expand Up @@ -72,4 +75,259 @@ 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
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;

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";
TotalAmountSmallBusinesses: Decimal;
TotalAmountAllVendors: Decimal;
begin
if PaymentPracticeData.FindSet() then
repeat
TotalAmountSmallBusinesses += PaymentPracticeData."Invoice Amount";
until PaymentPracticeData.Next() = 0;


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);
TotalAmountAllVendors += -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,27 @@ codeunit 689 "Payment Practices"
PaymentPracticeHeader."Average Actual Payment Period" := PaymentPracticeMath.GetAverageActualPaymentTime(PaymentPracticeData);
PaymentPracticeHeader."Average Agreed Payment Period" := PaymentPracticeMath.GetAverageAgreedPaymentTime(PaymentPracticeData);
PaymentPracticeHeader."Pct Paid on Time" := PaymentPracticeMath.GetPercentOfOnTimePayments(PaymentPracticeData);
PaymentPracticeHeader."Mode Payment Time" := PaymentPracticeMath.GetModePaymentTime(PaymentPracticeData);

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;

if PaymentPracticeHeader."Reporting Scheme" = PaymentPracticeHeader."Reporting Scheme"::"Percentiles; Modes; Pct Peppol Enabled; Pct Small Business Payments" then begin
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);

if PaymentPracticeHeader."Only Small Businesses" then
PaymentPracticeHeader."Pct Small Business Payments" := PaymentPracticeMath.GetPctSmallBusinessPayments(PaymentPracticeData, PaymentPracticeHeader);
end;
end;

local procedure GenerateData(var PaymentPracticeData: Record "Payment Practice Data"; PaymentPracticeHeader: Record "Payment Practice Header"; PaymentPracticeDataGenerator: Interface PaymentPracticeDataGenerator)
Expand Down
Loading
Loading