Extend French Peppol exports for CTC and participant routing - #9639
Extend French Peppol exports for CTC and participant routing#9639djukicmilica wants to merge 27 commits into
Conversation
| { | ||
| Extensible = true; | ||
|
|
||
| value(0; "EM") |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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
|
The new 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 |
There was a problem hiding this comment.
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
|
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"; |
There was a problem hiding this comment.
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.'); |
There was a problem hiding this comment.
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 = " "; |
There was a problem hiding this comment.
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:
- microsoft/knowledge/upgrade/initvalue-does-not-update-existing-rows.md
- microsoft/knowledge/upgrade/datatransfer-for-bulk-init.md
👍 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); |
There was a problem hiding this comment.
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")); |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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.
| 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
|
The new negative tests still hardcode full English error sentences in 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; |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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
|
Several newly added negative tests in PEPPOLBIS30XMLTests.Codeunit.al pin 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 |
Pull request was closed
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:
EXTENDED-CTC-FRcustomization when an invoice contains multiple order or shipment references, while retaining the basic profile for repeated references;0223(French VAT number) and0225(French routing identifier), with VAT registration number fallbacks when explicit endpoints are missing;Linked work
AB#642536
An approved GitHub issue still needs to be linked before this PR is ready for review.
How I validated this
What I tested and the outcome
Added focused tests in
PEPPOLBIS30XMLTests.Codeunit.alcovering:0225routing identifiers and scheme0223VAT fallbacks;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.