Skip to content

Extend French Peppol exports for CTC and participant routing - #9639

Open
djukicmilica wants to merge 27 commits into
mainfrom
djukicmilica/fr-extended-ctc
Open

Extend French Peppol exports for CTC and participant routing#9639
djukicmilica wants to merge 27 commits into
mainfrom
djukicmilica/fr-extended-ctc

Conversation

@djukicmilica

@djukicmilica djukicmilica commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What & why

Extend the French Peppol BIS 3.0 format to support the Extended CTC France scenarios and France-specific invoice metadata required for compliant routing and export.

This change:

  • selects the EXTENDED-CTC-FR customization when an invoice contains multiple order or shipment references, while retaining the basic profile for repeated references;
  • exports typed French regulatory sales comments as UBL header notes;
  • allows service-specific participant identifiers and French identifier schemes to override customer and company endpoint defaults;
  • adds schemes 0223 (French VAT number) and 0225 (French routing identifier), with VAT registration number fallbacks when explicit endpoints are missing;
  • adds Purchase Credit Memo as a supported source document type for the French Peppol format.

Linked work

AB#642536

An approved GitHub issue still needs to be linked before this PR is ready for review.

How I validated this

  • I read the full diff and it contains only changes I intended.
  • I built the affected app(s) locally with no new analyzer warnings.
  • I ran the change in Business Central and confirmed it behaves as expected.
  • I added or updated tests for the new behavior, or explained below why none are needed.

What I tested and the outcome

Added focused tests in PEPPOLBIS30XMLTests.Codeunit.al covering:

  • Extended CTC selection for multiple order references and multiple shipment references;
  • retention of the basic CTC profile for repeated references;
  • export of French regulatory comments as UBL notes;
  • company and customer endpoint resolution through service participants;
  • scheme 0225 routing identifiers and scheme 0223 VAT fallbacks;
  • seller and buyer VAT fallback behavior when explicit electronic addresses are absent.

Current GitHub checks: 51 succeeded. The French default and clean app builds are failing, which also causes the aggregate Pull Request Status Check to fail.

Risk & compatibility

Endpoint selection precedence changes for the French Peppol format: a service participant identifier now takes precedence over values stored on the company or customer, and VAT registration numbers are used as fallback endpoints. Existing documents with explicit participant or electronic address configuration retain those configured identifiers.

The added fields and enum values are additive. No upgrade code or data migration is required.

@github-actions github-actions Bot added AL: Apps (W1) Add-on apps for W1 Integration GitHub request for Integration area labels Jul 21, 2026
@github-actions github-actions Bot added this to the Version 29.0 milestone Jul 21, 2026
@github-actions github-actions Bot removed the AL: Apps (W1) Add-on apps for W1 label Jul 21, 2026
@djukicmilica djukicmilica changed the title [FR] draft Extend French Peppol exports for CTC and participant routing Jul 22, 2026
@djukicmilica
djukicmilica marked this pull request as ready for review July 29, 2026 08:38
{
Extensible = true;

value(0; "EM")

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\ —\ Upgrade}$

This change inserts a new enum member value(0; " ") at ordinal 0 and renumbers the existing members from 0/1/2 to 1/2/3 (EM: 0->1, 0009: 1->2, 0002: 2->3). This enum is stored as a persisted field on multiple tables (Customer."FR Elec. Address Scheme", Vendor."FR Elec. Address Scheme", Service Participant."FR Identifier Scheme"), and BC stores enum fields by ordinal, not by name. Any existing row that currently means "EM" (0), "0009"/SIRET (1), or "0002"/SIREN (2) will be silently reinterpreted as the wrong value after upgrade (e.g. a row meaning EM will read back as blank, a row meaning SIRET will read back as EM). New enum members must be appended at the end with new ordinals; existing ordinals must never be renumbered.

Knowledge:

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

ItemNode.AddBeforeSelf(OrderLineReferenceElement);
end;

if not SalesShipmentLine.Get(SalesInvoiceLine."Shipment No.", SalesInvoiceLine."Shipment Line 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}$

When extended CTC export needs shipment-based line enrichment, a missing posted shipment line or header is silently ignored and the export continues without any diagnostic. Add a specific validation error for unresolved nonblank shipment references so users get a clear failure instead of an incomplete extended FR invoice.

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

ServiceParticipant."Participant Identifier" := EndpointId;
ServiceParticipant."FR Identifier Scheme" := ServiceParticipant."FR Identifier Scheme"::"0225";
ServiceParticipant.Insert();
SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo));

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\ —\ Testing}$

ExportSalesInvUsesServiceParticipantEndpointWithScheme0225 only checks the overridden EndpointID. It never calls CheckInvoice, and it does not assert that PartyIdentification stays empty when the customer card is 0009 but the service-participant override is 0225, so regressions in the new buyer service-participant path can slip through unnoticed. Add the validation call and an explicit absence assertion.

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

SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo));

        CheckInvoice(SalesInvoiceHeader);
        ExportInvoice(SalesInvoiceHeader, XmlDoc);

        Assert.AreEqual(EndpointId,
            GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'),
            StrSubstNo(IncorrectValueErr, 'Buyer EndpointID'));
        Assert.AreEqual('0225',
            GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'),
            StrSubstNo(IncorrectValueErr, 'Buyer EndpointID schemeID'));
        Assert.AreEqual('',
            GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID'),
            StrSubstNo(IncorrectValueErr, 'Buyer PartyIdentification ID should be empty'));

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

@github-actions

Copy link
Copy Markdown
Contributor

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

The new asserterror-based validation tests verify full inline English error sentences with Assert.ExpectedError(...). This follows the rule narrowly, but it contradicts the article's best practice to use a shared label/helper or at least a stable invariant fragment, so wording or localization changes can break the test without any behavior change.

Knowledge:

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

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


if Customer."FR Electronic Address" = '' then
Error(BuyerElectronicAddressRequiredErr, Customer."No.");
if Customer."FR Electronic Address" <> '' then begin

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}$

When a customer has an FR Electronic Address but no scheme selected, CheckBuyerElectronicAddress raises a plain Error(BuyerElectronicAddressSchemeRequiredErr, Customer."No.") even though the code already holds the exact Customer record that must be corrected. Use ErrorInfo with PageNo/RecordId and an AddNavigationAction so the user can jump straight to that customer and fill in FR Elec. Address Scheme, instead of getting a dead-end dialog with no way to act on it.

Knowledge:

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

@github-actions

Copy link
Copy Markdown
Contributor

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

The Extended CTC tests build posted shipment/order references by overwriting posted Sales Invoice Line fields directly and by Init/Insert-ing Sales Shipment Header and Sales Shipment Line with invented keys, bypassing the real sales posting flow. These tests can pass on fixture states that production posting would never create, and may miss regressions in how posted shipment/order references are actually populated during real posting.

Knowledge:

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

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

EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Purchase Invoice";
EDocServiceSupportedType.Insert();

EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Purchase Credit Memo";

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\ —\ Testing}$

The diff adds "Purchase Credit Memo" to the "Peppol BIS 3.0 FR" supported source-document types, but no companion test was added to verify that OnAfterValidateDocumentFormat actually inserts that row into E-Doc. Service Supported Type. Add a regression test around service-format initialization so this registration cannot regress silently.

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


asserterror CheckInvoice(SalesInvoiceHeader);

Assert.ExpectedError('SIRET No., VAT Registration No., or a Service Participant identifier must be specified for the company for French e-invoicing.');

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\ —\ Testing}$

The new negative tests hardcode full Assert.ExpectedError(...) sentences for the seller/service-participant failures instead of asserting through a shared Label or helper. This still verifies that an error occurred, but it makes the asserterror tests brittle to harmless wording or localization changes and duplicates the expected error text across the suite.

Knowledge:

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

{
Caption = 'French Identifier Scheme';
DataClassification = CustomerContent;
InitValue = " ";

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\ —\ Upgrade}$

The new "FR Identifier Scheme" field is added to the existing "Service Participant" table via tableextension with InitValue = " " (blank), but InitValue only applies to newly inserted rows. Existing Service Participant records will keep the column's datatype default, which is enum ordinal 0 ("EM"), not the intended blank/unset value. Combined with the new HasServiceParticipantAddress logic in EDocHelpers.Codeunit.al and GetServiceParticipantAddress in PeppolBIS30FRFormat.Codeunit.al, which both treat a non-blank scheme as a configured electronic address, upgraded rows can be silently misinterpreted as having an "EM" (email) identifier scheme configured for French e-invoicing routing when none was ever set. Ship an upgrade routine (or a per-company data upgrade codeunit) to explicitly back-fill existing Service Participant rows to the blank scheme value before this field participates in validation/export logic.

Knowledge:

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

if not FREDocHelpers.HasServiceParticipantAddress(EDocumentServiceCode, ParticipantType, ParticipantNo) then
exit(false);

ServiceParticipant.Get(EDocumentServiceCode, ParticipantType, ParticipantNo);

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\ —\ Agent}$

GetServiceParticipantAddress in PeppolBIS30FRFormat.Codeunit.al calls FREDocHelpers.HasServiceParticipantAddress (which internally does ServiceParticipant.Get) and then, once it returns true, calls ServiceParticipant.Get again on the exact same key without checking its return value. This is a redundant second database read on every export path that reaches this helper, and if the row could no longer be found on the second call (e.g. deleted between the two reads) the unchecked Get would raise an unhandled 'record not found' runtime error instead of a controlled outcome.

Recommendation:

  • have HasServiceParticipantAddress optionally return the fetched record (var parameter) so the caller reuses it, or check the second Get's boolean result explicitly.

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

HasIdentifier := ServiceParticipant."Participant Identifier" <> '';
HasScheme := ServiceParticipant."FR Identifier Scheme" <> ServiceParticipant."FR Identifier Scheme"::" ";
if HasIdentifier <> HasScheme then
Error(ServiceParticipantAddressIncompleteErr, ServiceParticipant.FieldCaption("Participant Identifier"), ServiceParticipant.FieldCaption("FR Identifier Scheme"));

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}$

This incomplete service-participant setup is surfaced with a plain Error even though the specific Service Participant record is already known. Use an ErrorInfo with a Show-it navigation action so the user can open that record directly and complete the missing identifier/scheme pair.

Knowledge:

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

Comment on lines +224 to +228
SalesCommentLine.SetRange("Document Type", DocumentType);
SalesCommentLine.SetRange("No.", DocumentNo);
SalesCommentLine.SetRange("Document Line No.", 0);
SalesCommentLine.SetFilter("FR Regulatory Comment Type", '<>%1', SalesCommentLine."FR Regulatory Comment Type"::None);
if SalesCommentLine.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}$

The regulatory-comment loop reads only Comment and FR Regulatory Comment Type from Sales Comment Line without SetLoadFields, so each matching row loads the full record payload during export.

Suggested change
SalesCommentLine.SetRange("Document Type", DocumentType);
SalesCommentLine.SetRange("No.", DocumentNo);
SalesCommentLine.SetRange("Document Line No.", 0);
SalesCommentLine.SetFilter("FR Regulatory Comment Type", '<>%1', SalesCommentLine."FR Regulatory Comment Type"::None);
if SalesCommentLine.FindSet() then
SalesCommentLine.SetRange("Document Type", DocumentType);
SalesCommentLine.SetRange("No.", DocumentNo);
SalesCommentLine.SetRange("Document Line No.", 0);
SalesCommentLine.SetFilter("FR Regulatory Comment Type", '<>%1', SalesCommentLine."FR Regulatory Comment Type"::None);
SalesCommentLine.SetLoadFields("FR Regulatory Comment Type", Comment);
if SalesCommentLine.FindSet() then

Knowledge:

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

@github-actions

Copy link
Copy Markdown
Contributor

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

The new negative tests still hardcode full English error sentences in Assert.ExpectedError (for example at lines 682, 747, and 772). The asserterror guidance prefers a shared label/assert helper (and ExpectedErrorCode where known) so the test stays pinned to the intended failure without becoming brittle to wording or localization changes.

Knowledge:

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

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


enum 10971 "FR Regulatory Comment Type"
{
Extensible = true;

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\ —\ Data\ Modeling}$

The new "FR Regulatory Comment Type" enum models a closed, regulator-defined export code list, and its values are persisted on "Sales Comment Line" and later serialized into the invoice payload by enum member name/code. Leaving the enum extensible allows downstream extensions to store and export arbitrary non-standard codes instead of the regulated set. Prefer a non-extensible enum, or if extensibility is required for a legitimate reason, add explicit validation that only supported codes can be stored and exported.

Agent judgement — not directly backed by a BCQuality knowledge article.

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

EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Purchase Invoice";
EDocServiceSupportedType.Insert();

EDocServiceSupportedType."Source Document Type" := EDocServiceSupportedType."Source Document Type"::"Purchase Credit Memo";

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\ —\ Interfaces}$

This interface implementation now registers "Purchase Credit Memo" as a supported source document type, but its Check/Create methods still delegate to codeunit "EDoc PEPPOL BIS 3.0", whose implementation does not handle EDocument."Document Type"::"Purchase Credit Memo". The service can therefore advertise support for a purchase-credit-memo export path that ends in an unsupported-document error at runtime. Either remove this supported-type registration or add matching Purchase Credit Memo handling in the formatter implementation.

Agent judgement — not directly backed by a BCQuality knowledge article.

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

@github-actions

Copy link
Copy Markdown
Contributor

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

Several newly added negative tests in PEPPOLBIS30XMLTests.Codeunit.al pin asserterror/Assert.ExpectedError(...) against full inline English sentences instead of a shared label/helper or a stable invariant fragment. That makes the tests brittle to harmless wording, punctuation, or translation changes to the underlying labels even when the validation behavior is still correct, and duplicates the expected message text across the suite. Prefer label-backed assertions (or a shared helper) that assert on a stable substring rather than the full literal message for these seller/buyer/participant validation checks.

Knowledge:

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

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

djukicmilica added 2 commits July 30, 2026 14:10
auto-merge was automatically disabled July 30, 2026 14:58

Pull request was closed

@djukicmilica djukicmilica reopened this Jul 30, 2026
@djukicmilica
djukicmilica enabled auto-merge July 30, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Integration GitHub request for Integration area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants