Skip to content

[E-Documents Core] - Enabling remittance advice export via E-Documents (payment journal + posted payments)#9578

Open
AndriusAndrulevicius wants to merge 7 commits into
microsoft:mainfrom
AndriusAndrulevicius:dev/aan/remittance-advice
Open

[E-Documents Core] - Enabling remittance advice export via E-Documents (payment journal + posted payments)#9578
AndriusAndrulevicius wants to merge 7 commits into
microsoft:mainfrom
AndriusAndrulevicius:dev/aan/remittance-advice

Conversation

@AndriusAndrulevicius

Copy link
Copy Markdown
Contributor

Implementation

Adds an electronic counterpart to BC's existing paper remittance advice reports: a new "Create E-Documents" checkbox on reports 399 ("Remittance Advice - Journal") and 400 ("Remittance Advice - Entries") creates E-Document records of a new "Remittance Advice" type that flow through the standard E-Documents engine and export as UBL 2.1 RemittanceAdvice XML via the PEPPOL BIS 3.0 format.

Key additions:

  • Adds a new "Remittance Advice" E-Document type (enum 6121) with its own data-assembly, validation, and export path in E-Documents Core, reusing the existing service/format/workflow engine (no new dependencies).
  • Adds report extensions on 399 (unposted payment journal) and 400 (posted Vendor Ledger Entries) with an opt-in checkbox that collects payment/applied-document data and creates one E-Document per payment.
  • Ports report 399/400's allocation, currency-conversion, and payment-discount math into a shared assembly codeunit so the E-Document data matches the printed report exactly.
  • Adds a PEPPOL BIS 3.0 format implementation that generates UBL 2.1 RemittanceAdvice XML (header, accounting parties, payment means, one RemittanceAdviceLine per applied document).
  • Adds a non-editable "created" flag on Gen. Journal Line with a Void action (clears the flag / cancels the E-Document) and an Open E-Document action on the Payment Journal page.
  • Re-points the E-Document from the journal line to the posted payment Vendor Ledger Entry at posting time (journal lines are deleted on posting), with a delete-safety subscriber that cancels orphaned E-Documents if a flagged, un-re-pointed line is removed before posting.
  • Moves the existing UKSendRemittanceAdvice "send by email" actions natively into BaseApp (VendorLedgerEntries.Page.al, PaymentJournal.Page.al) and marks the old UKSendRemittanceAdvice page extensions obsolete — this is a code-location move only, the email-send behavior itself is unchanged and does not route through the new E-Document type.
  • Adds test coverage (EDocRemitAdviceTest, 13 scenarios) for journal/entries happy paths, multi-line/multi-vendor grouping, credit memos and payment discounts, duplicate/re-export confirms, posting re-point, void, and format extensibility.

Linked work

Fixes #

Enables creation, validation, export, and lifecycle handling for vendor remittance advice documents from payment journals and posted payments, including PEPPOL/UBL generation and related test coverage.
@AndriusAndrulevicius
AndriusAndrulevicius requested review from a team July 17, 2026 12:37
@AndriusAndrulevicius
AndriusAndrulevicius requested review from a team as code owners July 17, 2026 12:37
@github-actions github-actions Bot added AL: Apps (W1) Add-on apps for W1 From Fork Pull request is coming from a fork labels Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Could not find linked issues in the pull request description. Please make sure the pull request description contains a line that contains 'Fixes #' followed by the issue number being fixed. Use that pattern for every issue you want to link.

@github-actions github-actions Bot added the Integration GitHub request for Integration area label Jul 17, 2026
Introduces shared remittance advice buffer logic for unposted and posted payments, enabling consistent advice generation across payment workflows. Native send actions now appear in core payment and vendor ledger experiences, report selections are seeded for email delivery, and older UK-specific remittance setup hooks are marked obsolete.
# Conflicts:
#	src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al
@github-actions github-actions Bot added the needs-approval Workflow runs require maintainer approval to start label Jul 17, 2026
Reserves new identifiers for remittance advice components and related tests to prevent conflicts and keep the extension consistent.
CompanyInfo.TestField("Country/Region Code");
this.CheckCountryRegionCode(CompanyInfo."Country/Region Code");

if CompanyInfo.GLN + CompanyInfo."VAT Registration No." = '' then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Error\ Handling}$

These remittance-advice validation failures are recoverable setup issues on known records, but they raise plain Error dialogs. When Company Information lacks GLN/VAT Registration No. or the vendor lacks GLN/VAT Registration No., use ErrorInfo with navigation context so the user can jump directly to the affected setup record instead of hitting a dead-end error.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

/// inject elements such as CustomizationID/ProfileID (no PEPPOL BIS profile exists for remittance advice yet).
/// </summary>
[IntegrationEvent(false, false)]
local procedure OnBeforeAddHeaderElements(var TempBuffer: Record "Remit. Advice Buffer" temporary; RootNode: XmlNode)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Events}$

The new integration event OnBeforeAddHeaderElements does not encode its real firing position. It is raised in GenerateXml after AddHeaderElements has already run and before later XML sections are added, so the name both omits the host routine and advertises the wrong boundary. Rename it to a position-encoding name such as OnGenerateXmlOnAfterAddHeaderElements (or an equivalent mid-routine On<Host>OnBefore<Context> form) so subscribers can tell where it fires without reading the publisher.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Interfaces}$

The persisted "PEPPOL 3.0 Purchase" enum now dispatches the new "PEPPOL Remit. Advice Info Provider" contract but still relies only on DefaultImplementation. Because the selected format is stored in setup, an ordinal left behind after uninstalling an enum extension will not match any declared value, and DefaultImplementation will not protect the interface assignments in the remittance-advice exporter. Add UnknownValueImplementation for this persisted enum instead of relying on DefaultImplementation alone.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

DetailedVendLedgEntry.SetRange("Document Type", DetailedVendLedgEntry."Document Type"::"Credit Memo");
if DetailedVendLedgEntry.FindSet() then
repeat
VendLedgEntry3.Get(DetailedVendLedgEntry."Applied Vend. Ledger Entry No.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Performance}$

The nested application loops call VendLedgEntry3.Get(...) once per Detailed Vendor Ledg. Entry row. That creates an N+1 lookup pattern against Vendor Ledger Entry in the hot path of remittance buffer construction; reshape this as a joined/prefetched read instead of doing a per-row Get inside the loop.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

PaymentVendLedgEntry.CalcFields(Amount, "Remaining Amount");

FindAppliedEntries(PaymentVendLedgEntry, AppliedVendLedgEntry);
if AppliedVendLedgEntry.FindSet() then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Performance}$

BuildFromPostedPayment() calls AppliedVendLedgEntry.CalcFields(Amount, "Remaining Amount") inside the FindSet() loop. Because every iterated ledger entry needs those same FlowFields for per-row calculations, this matches the per-row CalcFields anti-pattern; set SetAutoCalcFields(Amount, "Remaining Amount") before the loop and remove the repeated CalcFields call.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why


AppliedVendLedgEntry.SetCurrentKey("Closed by Entry No.");
AppliedVendLedgEntry.SetRange("Closed by Entry No.", PaymentVendLedgEntry."Entry No.");
if AppliedVendLedgEntry.FindSet() then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Performance}$

FindAppliedEntries() scans Vendor Ledger Entry and Detailed Vendor Ledg. Entry records for mark/existence work while reading only a small subset of normal fields, but it never declares SetLoadFields. On these wide ledger tables that means each FindSet()/FindFirst() still materializes full rows even though the helper only needs a few columns.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

Caption = 'Bank Payment Type';
DataClassification = SystemMetadata;
}
field(9; "Recipient Bank Account"; Code[20])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Privacy}$

The new "Recipient Bank Account" field is declared SystemMetadata, but this buffer later resolves that value to an IBAN or bank-account identifier for export. A financial account identifier must use a real data classification such as AccountData, not SystemMetadata, otherwise the platform will treat bank-account data as system metadata in privacy-sensitive surfaces.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

        field(9; "Recipient Bank Account"; Code[20])
        {
            Caption = 'Recipient Bank Account';
            DataClassification = AccountData;
        }

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

RemittanceAdviceEntries.SaveAsXml(LibraryReportDataset.GetParametersFileName(), LibraryReportDataset.GetFileName());
end;

[ConfirmHandler]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🔴\ Critical\ Severity\ —\ Testing}$

ConfirmHandlerYes and ConfirmHandlerNo hardcode their replies without dequeuing and asserting the expected confirm text. That means these tests cannot prove the intended confirm fired the expected number of times; a different confirm that happens to get the same yes/no answer can leave the suite green while exercising the wrong UI path.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

PaymentVendLedgEntry.FindFirst();
end;

local procedure RunRemitAdviceJournalReport(CreateEDocs: Boolean)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Testing}$

This codeunit calls Commit() from test code (both directly in test methods and in the report helper procedures), but none of the affected tests declare [TransactionModel(TransactionModel::AutoCommit)]. Per the rule behind this article, committing under the rollback transaction model turns the test into a transaction-runtime failure instead of letting it reach its assertions about remittance-advice behavior.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

@github-actions github-actions Bot removed the needs-approval Workflow runs require maintainer approval to start label Jul 20, 2026
AndriusAndrulevicius and others added 2 commits July 20, 2026 14:10
…nal.PageExt.al

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@github-actions github-actions Bot added the needs-approval Workflow runs require maintainer approval to start label Jul 20, 2026
Comment on lines +1017 to +1019
GenJournalLine := Rec;
CurrPage.SetSelectionFilter(GenJournalLine);
SendVendorRemittanceAdviceRecords(GenJournalLine);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Accessibility}$

This batch action passes the result of CurrPage.SetSelectionFilter directly to the sender without checking MarkedOnly. When the user only positions the cursor on a row, Business Central narrows the record to that single row, so Send Remittance Advice silently processes just one payment instead of the full filtered journal view. After SetSelectionFilter, copy Rec back when MarkedOnly is false before calling the sender.

Suggested change
GenJournalLine := Rec;
CurrPage.SetSelectionFilter(GenJournalLine);
SendVendorRemittanceAdviceRecords(GenJournalLine);
GenJournalLine := Rec;
CurrPage.SetSelectionFilter(GenJournalLine);
if not GenJournalLine.MarkedOnly then
GenJournalLine.Copy(Rec);
SendVendorRemittanceAdviceRecords(GenJournalLine);

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.12.4

Comment on lines +556 to +559
VendorLedgerEntry := Rec;
CurrPage.SetSelectionFilter(VendorLedgerEntry);
VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Payment);
SendVendorRecords(VendorLedgerEntry);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Accessibility}$

This action applies CurrPage.SetSelectionFilter and then continues with the filtered record without resolving the cursor-only case. If the user has not explicitly selected rows, the page passes only the current entry, so Send Remittance Advice can skip the rest of the filtered payment entries with no warning. Preserve the full page scope by copying Rec back when MarkedOnly is false, then apply the payment filter.

Suggested change
VendorLedgerEntry := Rec;
CurrPage.SetSelectionFilter(VendorLedgerEntry);
VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Payment);
SendVendorRecords(VendorLedgerEntry);
VendorLedgerEntry := Rec;
CurrPage.SetSelectionFilter(VendorLedgerEntry);
if not VendorLedgerEntry.MarkedOnly then
VendorLedgerEntry.Copy(Rec);
VendorLedgerEntry.SetRange("Document Type", VendorLedgerEntry."Document Type"::Payment);
SendVendorRecords(VendorLedgerEntry);

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.12.4

if EDocumentServiceStatus.Status in
[EDocumentServiceStatus.Status::Sent, EDocumentServiceStatus.Status::Approved, EDocumentServiceStatus.Status::"Pending Response"]
then
Error(UseCancelActionErr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Error\ Handling}$

Voiding a sent remittance-advice e-document raises a plain Error that tells the user to use the Cancel action on the E-Document page, but the code already has the target E-Document record. Raise an ErrorInfo with a navigation action to that record instead of a dead-end dialog.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.12.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Interfaces}$

The new remittance-advice provider is wired through the persisted, extensible PEPPOL 3.0 Purchase enum, but the enum still has no UnknownValueImplementation. That enum is stored in PEPPOL 3.0 Setup, so if a partner-defined format value is persisted and the extension that declared it is later removed, assigning the unknown ordinal to Interface "PEPPOL Remit. Advice Info Provider" will raise a technical runtime error instead of falling back cleanly. Mirror the enum's existing DefaultImplementation mappings under UnknownValueImplementation.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

    DefaultImplementation = "PEPPOL Purchase Attachment Provider" = "PEPPOL30",
                            "PEPPOL Purchase Delivery Info Provider" = "PEPPOL30",
                            "PEPPOL Purchase Document Info Provider" = "PEPPOL30",
                            "PEPPOL Purchase Line Info Provider" = "PEPPOL30",
                            "PEPPOL Purchase Monetary Info Provider" = "PEPPOL30",
                            "PEPPOL Purchase Party Info Provider" = "PEPPOL30",
                            "PEPPOL Purchase Payment Info Provider" = "PEPPOL30",
                            "PEPPOL Purchase Tax Info Provider" = "PEPPOL30",
                            "PEPPOL Remit. Advice Info Provider" = "PEPPOL30";
    UnknownValueImplementation = "PEPPOL Purchase Attachment Provider" = "PEPPOL30",
                                 "PEPPOL Purchase Delivery Info Provider" = "PEPPOL30",
                                 "PEPPOL Purchase Document Info Provider" = "PEPPOL30",
                                 "PEPPOL Purchase Line Info Provider" = "PEPPOL30",
                                 "PEPPOL Purchase Monetary Info Provider" = "PEPPOL30",
                                 "PEPPOL Purchase Party Info Provider" = "PEPPOL30",
                                 "PEPPOL Purchase Payment Info Provider" = "PEPPOL30",
                                 "PEPPOL Purchase Tax Info Provider" = "PEPPOL30",
                                 "PEPPOL Remit. Advice Info Provider" = "PEPPOL30";

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.12.4

// JnlLineRemainingAmount by InsertTempAppliedEntry above) and AppliedDebitAmounts
// (PrintLoop OnPreDataItem).
JnlLineRemainingAmount += GenJournalLine.Amount + AppliedDebitAmounts;
if TempAppliedVendLedgEntry.FindSet() then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Performance}$

The buffer builders call CalcFields inside per-row loops (TempAppliedVendLedgEntry at line 137 and AppliedVendLedgEntry at line 267) even though every row needs the same FlowFields. Call SetAutoCalcFields(...) before each FindSet and remove the per-iteration CalcFields calls to avoid one extra FlowField query per row.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.12.4

end;
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterVendLedgEntryInsert', '', false, false)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Performance}$

Both remittance-advice subscribers query E-Document before checking a cheap in-memory remittance marker (GenJournalLine."Remit. Advice E-Doc. Created" / Rec."Remit. Advice E-Doc. Created"). OnAfterVendLedgEntryInsert and OnAfterDeleteEventGenJournalLine therefore pay an unnecessary lookup for every non-remittance journal posting or delete; add the flag guard before FindFirst().

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.12.4

/// vendor payment journal line and the other lines in the same payment group.
/// Ports the allocation/currency/discount math from report 399 "Remittance Advice - Journal".
/// </summary>
procedure BuildFromJournalPayment(AnchorGenJnlLine: Record "Gen. Journal Line"; var TempBuffer: Record "Remit. Advice Buffer" temporary)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Security}$

The new public remittance-buffer builders return a caller-supplied temporary "Remit. Advice Buffer" by var and populate it directly. Because that buffer carries vendor/payment data upward, an error after partial population can leave copied data in the caller's temporary record. Build into a local temporary buffer and copy it to the out parameter only after the routine completes successfully, or clear the caller buffer on every error path.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.12.4

local procedure VoidSelectedRemittanceAdviceEDocs()
var
SelectedGenJournalLine: Record "Gen. Journal Line";
ProcessedGroup: Record "Gen. Journal Line" temporary;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Style}$

ProcessedGroup: Record "Gen. Journal Line" temporary in VoidSelectedRemittanceAdviceEDocs, and the matching ordinary procedure parameters on lines 100 and 109, do not use the required Temp prefix. Because these variables are in-memory buffers rather than persisted journal lines, naming them TempProcessedGroup is the convention that makes every SetRange/Insert/IsEmpty call site read correctly during review.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.12.4

/// the Select Sending Options dialog - re-resolving the vendor default here could contradict
/// that choice.
/// </summary>
internal procedure ExportFromJournalLine(AnchorGenJnlLine: Record "Gen. Journal Line"; DocumentSendingProfile: Record "Document Sending Profile"; AllowReExport: Boolean): Boolean

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Telemetry}$

The new remittance-advice export flow introduces new user entry points and success paths, but it never emits a FeatureTelemetry uptake funnel for the feature. In this PR the lifecycle is visible end-to-end (new Payment Journal / Vendor Ledger Entry surfaces plus the new export codeunit), yet neither discover/setup nor used transitions are logged with a stable feature name, so adoption telemetry for this feature will remain empty.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.12.4


// [WHEN] The journal is posted in preview mode
GLPostingPreview.Trap();
asserterror GenJnlPost.Preview(GenJournalLine);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Testing}$

The preview-posting test uses asserterror GenJnlPost.Preview(GenJournalLine); and then only checks that GetLastErrorText() is empty. That still allows any unrelated empty-text failure to satisfy the negative path, so the test can pass without proving the specific preview error/sentinel it expects. Follow the asserterror with a specific Assert.ExpectedError... helper or error-code check for this preview case.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.12.4

@github-actions github-actions Bot removed the needs-approval Workflow runs require maintainer approval to start label Jul 20, 2026
@github-actions github-actions Bot added the needs-approval Workflow runs require maintainer approval to start label Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AL: Apps (W1) Add-on apps for W1 From Fork Pull request is coming from a fork Integration GitHub request for Integration area needs-approval Workflow runs require maintainer approval to start

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant