[PEPPOL] Add PEPPOL SE localization: Swedish organisation number for endpoints under scheme 0007#8646
Conversation
PEPPOL BIS 3.0 scheme 0007 (SE:ORGNR) requires the 10-digit Swedish organisation number, but the EndpointID and PartyLegalEntity CompanyID carried the full 14-char VAT (SE...01), which access points reject (PEPPOL-COMMON-R049). Extract the organisation number for SE parties whose Country/Region VAT Scheme is 0007, leaving the PartyTaxScheme VAT identifier and all other countries unchanged. Add SE coverage to the PEPPOL BIS billing tests.
The new Swedish organisation-number branches used else-if, which AA0018 rejects. Split them so the if starts its own line, and document the keep-only-digits extraction in GetSwedishOrgNo.
|
Pushed
Also replied inline on the silent-fallback point. |
Copilot PR ReviewIteration 8 · Outcome: completed Knowledge source: https://github.com/microsoft/BCQuality@186d8a131465475c79244d994acb872cd5c0d4bf Orchestrator pre-filter (2 file(s) excluded)
Findings produced by the AL review agent v1.7.3. Reply 👎 on any inline comment to flag false positives. |
…ort test Validating a changed Country/Region Code on Company Information clears City, Post Code and County, so the SE export test failed the PEPPOL company checks with "City must have a value". Set City and Post Code after switching the company to SE. Test-only change.
|
Pushed |
|
I think the proper design is to create swedish localization, that implemetns the GetAccountingSupplierPartyInfo. So should be fairly trivial change. Long term better design choice. |
…ndpoints Move the Swedish organisation number handling from the shared W1 PEPPOL codeunit into a new PEPPOL SE localization app, following the PEPPOL DE pattern. The SE party info provider delegates to the W1 implementation and reduces endpoint and legal entity identifiers to the 10-digit organisation number when they are emitted under scheme 0007 (SE:ORGNR), leaving the party tax scheme VAT identifier and all other countries untouched. Default new PEPPOL 3.0 Setup records to the SE formats, add a PEPPOL SE test app covering the provider, and wire both apps into the SE build project.
Co-authored-by: Alexander Holstrup <117829001+aholstrup1@users.noreply.github.com>
1c20d08
Head branch was pushed to by a user without write access
This reverts commit 1c20d08.
| // Keep only digits: the inner DelChr removes all digits, leaving the non-digit characters as a mask; | ||
| // the outer DelChr then removes that mask from the original, so only the digits remain. | ||
| DigitsOnly := DelChr(VATRegistrationNo, '=', DelChr(VATRegistrationNo, '=', '0123456789')); | ||
| if StrLen(DigitsOnly) >= 10 then |
There was a problem hiding this comment.
When GetSwedishOrgNo receives a value with fewer than 10 digits, it silently returns the original VAT registration number.
For scheme 0007 this can export an invalid EndpointID/CompanyID instead of failing with a clear validation error. Validate that at least 10 digits were found and raise an explicit error when the organisation number cannot be derived.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
Same consideration as in the earlier iteration 1 thread on the former W1 version of this helper: it is a formatter, mirroring the existing FormatVATRegistrationNo helpers, which format rather than raise. A value with fewer than 10 digits only occurs when a party under scheme 0007 has a malformed VAT registration, which already failed PEPPOL-COMMON-R049 at the receiving access point before this change, so passing the value through keeps behaviour no worse than today and surfaces the precise schematron error. Raising here would additionally block a sender when the malformed registration belongs to the customer, which they may not control.
| begin | ||
| if Rec.IsTemporary() then | ||
| exit; | ||
|
|
||
| Rec."PEPPOL 3.0 Sales Format" := Rec."PEPPOL 3.0 Sales Format"::"PEPPOL 3.0 - SE Sales"; | ||
| Rec."PEPPOL 3.0 Service Format" := Rec."PEPPOL 3.0 Service Format"::"PEPPOL 3.0 - SE Service"; | ||
| Rec.Modify(); | ||
| end; |
There was a problem hiding this comment.
The OnAfterInsert subscriber ignores its RunTrigger parameter and still rewrites PEPPOL 3.0 Setup.
When this table is inserted with Insert(false), the trigger event still reaches subscribers, so this localization code applies SE defaults and performs an extra Modify() even though the caller explicitly suppressed trigger logic. Exit when RunTrigger is false to keep the subscriber aligned with trigger-suppressed inserts.
| begin | |
| if Rec.IsTemporary() then | |
| exit; | |
| Rec."PEPPOL 3.0 Sales Format" := Rec."PEPPOL 3.0 Sales Format"::"PEPPOL 3.0 - SE Sales"; | |
| Rec."PEPPOL 3.0 Service Format" := Rec."PEPPOL 3.0 Service Format"::"PEPPOL 3.0 - SE Service"; | |
| Rec.Modify(); | |
| end; | |
| begin | |
| if Rec.IsTemporary() then | |
| exit; | |
| if not RunTrigger then | |
| exit; | |
| Rec."PEPPOL 3.0 Sales Format" := Rec."PEPPOL 3.0 Sales Format"::"PEPPOL 3.0 - SE Sales"; | |
| Rec."PEPPOL 3.0 Service Format" := Rec."PEPPOL 3.0 Service Format"::"PEPPOL 3.0 - SE Service"; | |
| Rec.Modify(); | |
| end; |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
Good catch, honored in 2a25684: the subscriber now exits when RunTrigger is false, so trigger-suppressed inserts (for example configuration package imports) keep their values.
| if Rec.IsTemporary() then | ||
| exit; | ||
|
|
||
| Rec."PEPPOL 3.0 Sales Format" := Rec."PEPPOL 3.0 Sales Format"::"PEPPOL 3.0 - SE Sales"; |
There was a problem hiding this comment.
The new SE formats are persisted into "PEPPOL 3.0 Setup", but "PEPPOL 3.0 Format" still relies on DefaultImplementation only.
If this localization is later uninstalled, the stored 37450/37451 ordinals become unknown and assigning the setup value to interface variables will raise a runtime error instead of a controlled fallback. Add an UnknownValueImplementation on the base enum, or avoid persisting extension-owned values.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
Fair architectural point, but the fix belongs on the base "PEPPOL 3.0 Format" enum in the W1 PEPPOL app, and this PR is deliberately scoped to the SE localization only (the earlier W1 changes were reverted per review). The same exposure already exists for the persisted DE, BE, NO and NA format values, so it seems better addressed centrally for all country extensions. @Groenbech96 happy to file a follow-up issue proposing an UnknownValueImplementation on the base enum if you agree.
| [Test] | ||
| procedure ExportXml_PEPPOL_BIS3_SalesInvoice_SEOrgNo() |
There was a problem hiding this comment.
ExportXml_PEPPOL_BIS3_SalesInvoice_SEOrgNo posts a sales invoice through LibrarySales.PostSalesDocument but has no [TransactionModel(TransactionModel::AutoCommit)] attribute.
Test methods default to AutoRollback, so the posting commit path can fail with a transaction-model error before the assertions run.
| [Test] | |
| procedure ExportXml_PEPPOL_BIS3_SalesInvoice_SEOrgNo() | |
| [Test] | |
| [TransactionModel(TransactionModel::AutoCommit)] | |
| procedure ExportXml_PEPPOL_BIS3_SalesInvoice_SEOrgNo() |
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
The W1 suite this test mirrors, PEPPOL BIS BillingTests, posts sales and service documents at 13 call sites without a TransactionModel attribute, and the OIOUBL posting tests in the DK localization do the same, so the default transaction model demonstrably handles this posting path in the test framework. Following that precedent here; if the SE pipeline shows otherwise, happy to add the attribute.
|
|
||
| // [THEN] EndpointID keeps the full VAT (scheme 9955 expects the VAT, not the organisation number) | ||
| Assert.AreEqual(GetSEVATSchemeID(), SupplierSchemeID, ''); | ||
| Assert.AreNotEqual(GetExpectedSEOrgNo(), SupplierEndpointID, 'VAT must not be stripped to the organisation number when the scheme is 9955.'); |
There was a problem hiding this comment.
GetAccountingSupplierPartyInfo_VATScheme9955_SE only checks that EndpointID is not the stripped organisation number and still contains the VAT digits.
A malformed value like a prefixed or suffixed string would still pass. Assert the exact expected VAT registration number instead.
| Assert.AreNotEqual(GetExpectedSEOrgNo(), SupplierEndpointID, 'VAT must not be stripped to the organisation number when the scheme is 9955.'); | |
| Assert.AreEqual(GetSETestVATRegNo(), SupplierEndpointID, 'EndpointID must keep the full Swedish VAT under scheme 9955.'); |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
Done in 2a25684, exact equality asserted.
| PartyInfoProvider.GetAccountingSupplierPartyTaxScheme(CompanyID, CompanyIDSchemeID, TaxSchemeID); | ||
|
|
||
| // [THEN] CompanyID keeps the full VAT, not the stripped organisation number | ||
| Assert.AreNotEqual(GetExpectedSEOrgNo(), CompanyID, 'PartyTaxScheme CompanyID must not be the stripped organisation number.'); |
There was a problem hiding this comment.
GetAccountingSupplierPartyTaxScheme_VATRegNo_SE uses a negative check plus substring match for CompanyID, so values that merely contain the VAT digits still pass.
Because the expected full VAT is known, the test should assert exact equality.
| Assert.AreNotEqual(GetExpectedSEOrgNo(), CompanyID, 'PartyTaxScheme CompanyID must not be the stripped organisation number.'); | |
| Assert.AreEqual(GetSETestVATRegNo(), CompanyID, 'PartyTaxScheme CompanyID must retain the full Swedish VAT.'); |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
Done in 2a25684, exact equality asserted.
| end; | ||
|
|
||
| [Test] | ||
| procedure PEPPOL30SetupDefaultsToSEFormats() |
There was a problem hiding this comment.
PEPPOL30SetupDefaultsToSEFormats only covers creating a missing PEPPOL 3.0 Setup record.
This PR also adds OnInstallAppPerCompany migration logic that updates an already-existing setup record only when it still uses the W1 defaults, but the new suite never exercises that upgrade path or the 'do not override a custom format' branch. Add a test for the install-migration behavior.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
Added in 2a25684: the migration procedure is exposed to the test app via internalsVisibleTo, with tests covering both the move-from-W1-defaults branch and the keep-configured-format branch.
| InherentPermissions = X; | ||
| Access = Internal; | ||
|
|
||
| trigger OnInstallAppPerCompany() |
There was a problem hiding this comment.
OnInstallAppPerCompany always calls SetSEFormatsOnExistingSetup() without first checking AppInfo.DataVersion().
Per the install guidance, this trigger also runs on reinstall, so first-install migration logic should be gated to Version.Create('0.0.0.0') or split into an explicit reinstall path. As written, reinstalling the app over preserved company data can reapply this migration and overwrite setup values that happen to still equal the W1 defaults.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
Adopted in 2a25684: the migration now runs only when DataVersion() is 0.0.0.0, per the linked guidance, so a reinstall over preserved data does not re-apply it.
Honor RunTrigger in the PEPPOL 3.0 Setup insert subscriber so trigger-suppressed inserts (for example configuration package imports) are not overwritten. Gate the install migration on a zero DataVersion so a reinstall over preserved data does not re-apply it. Assert exact values in the 9955 and tax scheme tests, and cover the install migration with tests for both the move-from-defaults and the keep-configured-format branches.
|
|
||
| using Microsoft.Peppol; | ||
|
|
||
| enumextension 37450 "PEPPOL 3.0 Format SE" extends "PEPPOL 3.0 Format" |
There was a problem hiding this comment.
This app adds new values to the persisted enum "PEPPOL 3.0 Format" and writes them into "PEPPOL 3.0 Setup", but that enum has no "UnknownValueImplementation".
If the SE app is later uninstalled, ordinals 37450/37451 can remain in setup, and later assignments such as "PEPPOL30Validation := PeppolSetup."PEPPOL 3.0 Sales Format";" can resolve an unknown enum value to an interface and fail with a technical runtime error instead of controlled handling.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
Same finding as iteration 6 on PEPPOL30SESubscribers.Codeunit.al, answered in that thread: UnknownValueImplementation can only be declared on the base enum, not on an enum extension, so it is not something an SE-scoped app can add. The persisted DE, BE, NO and NA format values carry the same exposure today, which is why this seems best addressed centrally on "PEPPOL 3.0 Format" for all country apps at once. The offer to file a follow-up issue for that still stands, @Groenbech96.
| InherentEntitlements = X; | ||
| InherentPermissions = X; | ||
|
|
||
| var |
There was a problem hiding this comment.
"StandardProvider" is hard-wired as "Codeunit "PEPPOL30"" even though the collaborator already sits behind the "PEPPOL Party Info Provider" interface.
That concrete dependency prevents this wrapper from accepting an injected interface implementation or test double for the delegated calls. Depend on an "Interface "PEPPOL Party Info Provider"" variable and inject "Codeunit "PEPPOL30"" in production instead of calling a concrete codeunit field directly.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
There was a problem hiding this comment.
If I am not mistaken this one does not apply here: this codeunit is itself the "PEPPOL Party Info Provider" implementation that the new enum values bind, so it is instantiated through enum dispatch and no caller could inject an alternative into it. The StandardProvider member is the fixed W1 base implementation being wrapped, not a swappable collaborator, and the shipped DE localization (codeunit 37403 "PEPPOL30 DE Party Info") declares the exact same member, which is the pattern this app was asked to follow. Happy to adjust if the maintainers prefer otherwise.
What and why
Swedish PEPPOL BIS 3.0 documents fail at the access point because the supplier/customer electronic address (
cbc:EndpointID) and thecac:PartyLegalEntity/cbc:CompanyIDare emitted as the full 14-character Swedish VAT number (for exampleSE733078715601) underschemeID="0007". Scheme0007is SE:ORGNR, which perPEPPOL-COMMON-R049must be exactly the 10-digit Swedish organisation number (7330787156).Per review feedback, the fix is implemented as a new PEPPOL SE localization app (
src/Apps/SE/PeppolSE), following the PEPPOL DE pattern, and the earlier changes to the shared W1 PEPPOL codeunit are fully reverted:PEPPOL 3.0 Formatenum extension addsPEPPOL 3.0 - SE SalesandPEPPOL 3.0 - SE Service, binding thePEPPOL Party Info Providerto a newPEPPOL30 SE Party Infocodeunit (standard W1 validation and iterator are reused).PEPPOL30implementation and post-processes only four of them: the endpoint identifiers are reduced to the 10-digit organisation number when the emittedschemeIDis0007, and the legal entity identifiers when the party's Country/Region VAT Scheme is0007. GLN setups (0088), Denmark (0184), and Swedish parties registered under EAS9955(SE:VAT, where the full VAT is correct) are untouched by construction.cac:PartyTaxScheme/cbc:CompanyID(BT-31/BT-48) keeps the full VAT number.PEPPOL 3.0 Setuprecords default to the SE formats on insert, mirroring PEPPOL DE. Existing setups keep their configured format; an install/upgrade step re-defaulting existing SE tenants can be added if preferred.PEPPOL SE Testsapp covers the provider through the enum interface dispatch, plus a posted sales invoice XML export.Note: for a blank bill-to country the country scheme falls back to the company's country, which is the same fallback the W1 implementation already uses to emit the endpoint
schemeID, so the value stays consistent with the declared scheme.Linked work
Fixes #7723
How I validated
EndpointIDandPartyLegalEntity/CompanyIDreturn7330787156under scheme00079955keeps the full VAT (not stripped)PartyTaxScheme/CompanyIDkeeps the full VATPEPPOL 3.0 Setupdefaults to the SE formatsPartyTaxSchemeRisk and compatibility
PEPPOL 3.0 Setup(defaulted for new setups in the SE localization).Fixes AB#641595