Deliverable 629871: [master] [UK] Payment Practices updates#7624
Deliverable 629871: [master] [UK] Payment Practices updates#7624AleksanderGladkov wants to merge 19 commits into
Conversation
There was a problem hiding this comment.
AL Documentation Audit
Documentation gaps were detected in the following apps:
- Payment-Practices-Test-Library: 0% documentation coverage
- Payment-Practices-Tests: 0% documentation coverage
- Payment-Practices: 10% documentation coverage
To generate documentation, run /al-docs init or /al-docs update using GitHub Copilot CLI or Claude Code.
This review is for awareness to help keep documentation in sync with code changes. It is okay to dismiss this request.
636e30c to
fc0689b
Compare
fc0689b to
f12e66b
Compare
https://github.com/microsoft/BCApps into features/629871-master-Payment-Practices-W1-GB-Simple
…res/629871-master-Payment-Practices-W1-GB-Simple
https://github.com/microsoft/BCApps into features/629871-master-Payment-Practices-W1-GB-Simple
…res/629871-master-Payment-Practices-W1-GB-Simple
…res/629871-master-Payment-Practices-W1-GB-Simple
|
…res/629871-master-Payment-Practices-W1-GB-Simple
https://github.com/microsoft/BCApps into features/629871-master-Payment-Practices-W1-GB-Simple
ventselartur
left a comment
There was a problem hiding this comment.
Please, review my comments
…res/629871-master-Payment-Practices-W1-GB-Simple
| ShowHeaderDataLines(); | ||
| end; | ||
| } | ||
| group("Dispute and Retention") |
There was a problem hiding this comment.
Dispute & Retention page fields missing ToolTip
The four new fields inside the "Dispute and Retention" group ("Total Number of Payments", "Total Amount of Payments", "Total Amt. of Overdue Payments", "Pct Overdue Due to Dispute") are added to the page without explicit ToolTip properties. AL code-quality rules require every visible page field to declare a ToolTip; absence causes build warnings and degrades discoverability in the UI.
Recommendation:
- Add a
ToolTipproperty to each of the four new page fields, either matching the table-field tooltip or providing a page-context-specific description.
| group("Dispute and Retention") | |
| field("Total Number of Payments"; Rec."Total Number of Payments") | |
| { | |
| ToolTip = 'Specifies the total number of payments made during the reporting period.'; | |
| } | |
| field("Total Amount of Payments"; Rec."Total Amount of Payments") | |
| { | |
| ToolTip = 'Specifies the total value of payments made during the reporting period.'; | |
| } | |
| field("Total Amt. of Overdue Payments"; Rec."Total Amt. of Overdue Payments") | |
| { | |
| ToolTip = 'Specifies the total value of payments not made within the agreed payment terms.'; | |
| } | |
| field("Pct Overdue Due to Dispute"; Rec."Pct Overdue Due to Dispute") | |
| { | |
| ToolTip = 'Specifies the percentage of late payments attributable to disputes.'; | |
| } |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
|
|
||
| local procedure ShowSmallBusinessVendorLedgerEntries() | ||
| var | ||
| VendorLedgerEntry: Record "Vendor Ledger Entry"; |
There was a problem hiding this comment.
Drill-down shows all invoices, not small business ones
ShowSmallBusinessVendorLedgerEntries filters Vendor Ledger Entries only by document type and posting date, showing all vendor invoices in the period. A user drilling down from the "Pct Small Business Payments" field reasonably expects to see only small-business vendor entries, but will instead see every invoice in the period.
Recommendation:
- Apply an additional filter to restrict the page to vendors whose associated
Company Sizehas"Small Business" = true, or open thePayment Practice Data Listpage pre-filtered on the small-business data rows (as the other drill-downs do).
| VendorLedgerEntry: Record "Vendor Ledger Entry"; | |
| local procedure ShowSmallBusinessVendorLedgerEntries() | |
| var | |
| PaymentPracticeData: Record "Payment Practice Data"; | |
| begin | |
| PaymentPracticeData.SetRange("Header No.", Rec."No."); | |
| PaymentPracticeData.SetRange("Source Type", PaymentPracticeData."Source Type"::Vendor); | |
| // Show data already filtered to small-business vendors (see SmallBusHandler.UpdatePaymentPracData) | |
| Page.RunModal(Page::"Payment Practice Data List", PaymentPracticeData); | |
| end; |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
Invoice Count and Value page fields missing ToolTipThe two new fields Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
|
|
||
| PaymentPracticeHeader.SetRange("Reporting Scheme", 0); | ||
| PaymentPracticeHeader.ModifyAll("Reporting Scheme", ReportingScheme); | ||
|
|
There was a problem hiding this comment.
Backfill overwrites intentionally-Standard scheme headers
BackfillReportingScheme filters on "Reporting Scheme" = 0 (Standard) and overwrites them all with the environment-detected scheme. On a non-GB tenant, DetectReportingScheme() returns Standard (0), so ModifyAll is a no-op. On a GB tenant it returns Dispute & Retention, which incorrectly overwrites any header that was explicitly set to Standard — but since the field is Editable = false, all pre-upgrade records have value 0 by default, so the intent seems correct. However, the filter comment and intent is misleading and fragile for future enum additions where 0 might be legitimately used.
Recommendation:
- Add a comment explaining the intent, or use a dedicated sentinel value / a Boolean flag
"Reporting Scheme Initialised"to distinguish unset from explicitly-set-to-Standard.
| // Only backfill headers that were inserted before the Reporting Scheme field existed | |
| // (all pre-upgrade headers have value 0 = Standard because the field did not exist). | |
| PaymentPracticeHeader.SetRange("Reporting Scheme", PaymentPracticeHeader."Reporting Scheme"::Standard); | |
| PaymentPracticeHeader.ModifyAll("Reporting Scheme", ReportingScheme); |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
AUNZ default periods changed without data migrationThe AUNZ default payment periods were consolidated from five bands (0–21, 22–30, 31–60, 61–120, 121+) to three (0–30, 31–60, 61+). Existing AUNZ tenants who have already generated Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
|
|
||
| PaymentPracticeHeader."Total Number of Payments" := TotalCount; | ||
| PaymentPracticeHeader."Total Amount of Payments" := TotalValue; | ||
| PaymentPracticeHeader."Mode Payment Time" := PaymentPracticeMath.GetModePaymentTime(PaymentPracticeData); |
There was a problem hiding this comment.
Five+ sequential full-table scans per report run
CalculateHeaderTotals makes at least five independent calls that each iterate PaymentPracticeData in full: GetModePaymentTime, GetModePaymentTimeMin, GetModePaymentTimeMax, GetPaymentTimeStatistics, and GetPctPeppolEnabled. For large reporting periods this multiplies the DB I/O by 5×.
Recommendation:
- Consolidate into a single pass that accumulates counts, sums, the sorted list for statistics, per-vendor mode values, and the PEPPOL GLN dictionary simultaneously. The
GetPctSmallBusinessPaymentscall to raw VLE should be collapsed into the same pass usingPaymentPracticeData.
| PaymentPracticeHeader."Mode Payment Time" := PaymentPracticeMath.GetModePaymentTime(PaymentPracticeData); | |
| // Combine all metrics into a single FindSet loop or split into at most two passes | |
| // (one for mode/statistics over PaymentPracticeData, one for PEPPOL GLN lookups). |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
AU/NZ period codes changed, existing lines orphaned
Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
Reporting Scheme field visible=false but no ToolTipThe new 'Reporting Scheme' field on line 28 has no Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
ShowVendorInvoicesInReportPeriod exposes unfiltered entries
Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
| ActualPaymentTimes: List of [Integer]; | ||
| begin | ||
| PaymentPracticeData.SetRange("Invoice Is Open", false); | ||
| if PaymentPracticeData.FindSet() then |
There was a problem hiding this comment.
Statistical methods load all rows into memory
GetModePaymentTime, GetClosedInvoicePaymentTimes, and GetModesPerVendor iterate every closed invoice and accumulate values into AL List of [Integer] in memory. For high-volume companies this may exhaust available memory or exceed runtime limits.
Recommendation:
- Consider computing mode/median/percentiles incrementally or via sorted key traversal rather than loading the entire dataset into an in-memory list. At minimum, document the expected scale limitations.
| if PaymentPracticeData.FindSet() then | |
| // For large datasets, consider computing these statistics with a sorted key scan | |
| // instead of loading all records into a List of [Integer]. |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| Vendor.SetLoadFields("Company Size Code"); | ||
| if VendorLedgerEntry.FindSet() then | ||
| repeat | ||
| if Vendor.Get(VendorLedgerEntry."Vendor No.") then |
There was a problem hiding this comment.
Vendor.Get loads all fields unnecessarily
Vendor.Get() inside the GetPctSmallBusinessPayments loop reads all vendor fields from the database. Only Company Size Code is consumed.
Recommendation:
- Call
Vendor.SetLoadFields('Company Size Code')before the loop so only the required field is fetched.
| if Vendor.Get(VendorLedgerEntry."Vendor No.") then | |
| Vendor.SetLoadFields("Company Size Code"); | |
| if VendorLedgerEntry.FindSet() then | |
| repeat | |
| if Vendor.Get(VendorLedgerEntry."Vendor No.") then | |
| ... |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| var | ||
| VendorLedgerEntry: Record "Vendor Ledger Entry"; | ||
| begin | ||
| VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); |
There was a problem hiding this comment.
ShowVendorInvoicesInReportPeriod not scoped to header vendors
ShowVendorInvoicesInReportPeriod opens the Vendor Ledger Entries page filtered only by Document Type and Posting Date range. It shows invoices from ALL vendors in the period, not just the vendors included in the current Payment Practice Header. A user drilling down from a Payment Practice scoped to specific vendors may inadvertently see invoices from unrelated vendors.
Recommendation:
- Filter the Vendor Ledger Entry list to the vendors actually included in the Payment Practice Header's generated data by joining through
PaymentPracticeData.
| VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Invoice); | |
| local procedure ShowVendorInvoicesInReportPeriod() | |
| var | |
| VendorLedgerEntry: Record "Vendor Ledger Entry"; | |
| PaymentPracticeData: Record "Payment Practice Data"; | |
| begin | |
| PaymentPracticeData.SetRange("Header No.", Rec."No."); | |
| PaymentPracticeData.SetRange("Source Type", PaymentPracticeData."Source Type"::Vendor); | |
| if PaymentPracticeData.FindSet() then | |
| repeat | |
| VendorLedgerEntry.SetRange("Entry No.", PaymentPracticeData."Invoice Entry No."); | |
| until PaymentPracticeData.Next() = 0; | |
| 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; |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| 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") |
There was a problem hiding this comment.
PrepareLayout never resets to default report layout
PrepareLayout only sets the layout when the scheme is Small Business. If a user previously ran a Small Business report, DesignTimeReportSelection has set the layout to PaymentPractice_SmallBusinessLayout. On the next run with a Standard or Dispute & Retention scheme, the layout is not reset, so the wrong template may be applied.
Recommendation:
- Use an
elsebranch (or acasestatement) to explicitly set the default layout for Standard and Dispute & Retention schemes.
| local procedure PrepareLayout(PaymentPracticeLinesAggregator: Interface PaymentPracticeLinesAggregator; ReportingScheme: Enum "Paym. Prac. Reporting Scheme") | |
| local procedure PrepareLayout(PaymentPracticeLinesAggregator: Interface PaymentPracticeLinesAggregator; ReportingScheme: Enum "Paym. Prac. Reporting Scheme") | |
| var | |
| DesignTimeReportSelection: Codeunit "Design-time Report Selection"; | |
| begin | |
| PaymentPracticeLinesAggregator.PrepareLayout(); | |
| case ReportingScheme of | |
| ReportingScheme::"Small Business": | |
| begin | |
| DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_SmallBusinessLayout'); | |
| FeatureTelemetry.LogUsage('0000KSU', 'Payment Practices', 'Small Business layout used.'); | |
| end; | |
| else | |
| DesignTimeReportSelection.SetSelectedLayout('PaymentPractice_StandardLayout'); | |
| end; | |
| end; |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
AU/NZ period consolidation has no data upgrade
Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
| var | ||
| PaymentPractices: Codeunit "Payment Practices"; | ||
| begin | ||
| "Reporting Scheme" := PaymentPractices.DetectReportingScheme(); |
There was a problem hiding this comment.
OnInsert unconditionally overwrites Reporting Scheme
DetectReportingScheme() is called in OnInsert and always sets "Reporting Scheme" from the environment. Any code that assigns the scheme before calling Insert() — including the new test library's CreatePaymentPracticeHeader and CreatePaymentPracticeHeaderWithScheme overloads — will silently have its value overwritten, making explicit scheme assignments impossible.
Recommendation:
- Only auto-detect when the field has not been set explicitly, i.e. guard with
if "Reporting Scheme" = "Reporting Scheme"::Standard then.
| "Reporting Scheme" := PaymentPractices.DetectReportingScheme(); | |
| local procedure DetectReportingScheme() | |
| var | |
| PaymentPractices: Codeunit "Payment Practices"; | |
| begin | |
| if "Reporting Scheme" <> "Reporting Scheme"::Standard then | |
| exit; // Already set explicitly by the caller | |
| "Reporting Scheme" := PaymentPractices.DetectReportingScheme(); | |
| end; |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
What was done
Reporting Schemedimension (Standard / Dispute & Retention / Small Business) so the app can support country-specific payment practice rules without affecting existing W1/FR users.Fixes AB#629871
Fixes AB#626294