Skip to content

[19.0] Migrate account_invoice_en16931 and the einvoicing stack to 19.0 - #34

Draft
njeudy wants to merge 44 commits into
akretion:19.0from
Alusage:MIG-19.0-account_invoice_en16931
Draft

[19.0] Migrate account_invoice_en16931 and the einvoicing stack to 19.0#34
njeudy wants to merge 44 commits into
akretion:19.0from
Alusage:MIG-19.0-account_invoice_en16931

Conversation

@njeudy

@njeudy njeudy commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Migration of the fr-einvoicing stack to 19.0, rebased on upstream/19.0.

Ports the modules to 19.0 API changes (res.users.groups_idgroup_ids, view adaptations, dropped commercial_partner_invoice_id, journal/sale views…) and adapts the SIREN/SIRET data cleanup to the new l10n_fr_siret layout.

SIREN/SIRET on company_registry

l10n_fr_siret 19.0 no longer provides the siren / nic / siret fields: the value lives in the native company_registry field (9 digits for a SIREN, 14 for a SIRET) and the module only exposes the _get_siren() / _get_siret() / _get_nic() getters. The stack already went through those getters everywhere, so the only caller left to port was _fr_directory_check_siren_siret_vat(), added in 3e3f1ba/5b0f0ff and written against the 18.0 fields.

It is rewritten around the single field: the SIREN+NIC vs SIRET arbitration collapses into a length + checksum check (extracted into _fr_check_company_registry() to keep the method readable). One deliberate behaviour change: when the SIRET checksum is wrong but its SIREN is valid, 18.0 left the computed field rendering 792377731*****; 19.0 has no such intermediate representation, so it falls back to the 9-digit SIREN. The search domain of fr_ctc_check_siren_siret_vat_button is fixed accordingly (it still queried the two removed fields), and the tests are rewritten on company_registry.

Modules — migration status

  • account_invoice_en16931 — no code change needed, the 18.0 tax base-lines computation API is unchanged in 19.0
  • l10n_fr_account_invoice_en16931
  • l10n_fr_einvoicing
  • l10n_fr_einvoicing_import
  • l10n_fr_einvoicing_purchase
  • l10n_fr_einvoicing_sale
  • l10n_fr_einvoicing_payment_batch_oca
  • l10n_fr_einvoicing_dashboard_banner — blocked: account_dashboard_banner has no 19.0 host repository yet. The port is a two-hunk patch (the account.DashboardKanbanRenderer OWL renderer is identical in 18.0 and 19.0), but it needs a home first. Left installable: False, as you did in 74f3e9b.
  • account_invoice_en16931_py3o — out of scope, report_py3o dropped the py3o.report model in 19.0

The seven ported modules are back to installable: True.

Runtime fixes found while generating a real invoice

Installation alone did not exercise the generation path. Producing an actual EN16931 document surfaced three bugs:

  • res.partner.mobile no longer exists in 19.0 (543636a). Two readers were left: _en16931_partner_data() — hence the seller and buyer blocks of every document — and BT-42. Invisible at install time.
  • Odoo 19 breaks stdnum (4a1a117). odoo/_monkeypatches/__init__.py installs a PatchImportHook whose replacement loader has no get_resource_reader, so importlib.resources.files('stdnum') raises FileNotFoundError and any stdnum submodule reading a .dat at import time (iban, cfi, gs1_ai…) becomes unimportable inside the Odoo processfacturx.generate_xml among them. Ironically the patch itself is a no-op with python-stdnum ≥ 2.0, but it breaks the loader anyway. Still unfixed upstream on master; the workaround lives in the module for now and would be better placed elsewhere.
  • BT-152 was missing on exempt lines (a9cd80b) — this one is not a migration bug. The rate was only set for categories S, K and G, so E (exempt), Z (zero-rated) and AE (reverse charge) went out without BT-152, although BR-E-05, BR-Z-05 and BR-AE-05 require it at 0. Only category O must omit it (BR-O-05). Present identically in 18.0: any invoice carrying an exempt line — VAT franchise, reverse charge, export — is rejected by the base schematron, in CII as in UBL. Backported and validated before/after on 18.0 in [FIX] account_invoice_en16931: BT-152 is required on exempt invoice l… #39.

Also included: self._contextself.env.context (8dfdd03), deprecated in 19.0.

Rebase on upstream/19.0

The branch is rebased on the seven commits carried onto 19.0 (send/receive configuration, invoice_attachment_ids, business_process_type, the Chorus old-syntax option, two fixes). The local fix for #36 is dropped: upstream's [FIX] don't store fr_einvoicing_required removes the mass recompute at the root.

Two fixes carried temporarily

#46 (#43, Discount precision) and #47 (#42, EN16931 config scope) are applied here and proposed upstream on 18.0 where they belong. They will be dropped from this branch once merged there, or adjusted if a different shape is preferred.

The second one matters for this branch specifically. Running the full suite on a clean 19.0 database, the unconditional _en16931_checks() in _post() raises ... that block EN16931 e-invoicing in 390 standard Odoo tests (account 287, sale 45, account_edi_ubl_cii 18, account_payment 8) — every test that creates its own company and posts an invoice on it. With the fix, the run goes from 41 failed, 418 errors of 3902 tests to 39 failed, 51 errors of 3976 tests. Same code on 18.0.

Testing

Installed and upgraded on a fresh 19.0 database (Odoo 19.0-20260723, no demo data), cascade install of the full dependency chain: 81 modules loaded, no warning.

Unit tests: 0 failed, 0 error(s) of 15 tests on l10n_fr_einvoicing, the only module of the stack carrying tests. This includes the two edge cases only reachable through raw SQL — the SUPERPDP sandbox partner and an invalid VAT containing a valid SIREN — since base_vat refuses to write them through the ORM.

fr_ctc_check_siren_siret_vat_button was additionally exercised over nine deliberately corrupted registries — valid SIRET, wrong SIRET checksum, wrong SIREN checksum, non-digit registry, truncated registry, spaces only, inconsistent FR VAT, non-FR VAT, spaced valid VAT — all nine behave as expected.

End-to-end generation validated, on an invoice mixing a 20 % line (category S) and an exempt line (category E, VATEX-FR-FRANCHISE), with a Saxon server actually answering:

factur-x        XML file successfully validated against 2 schematron(s) in 0.177 sec
ubl-2.1-invoice XML file successfully validated against 2 schematron(s) in 0.213 sec

XSD plus the base and fr-ctc schematrons, both formats. Worth noting for anyone reproducing this: without the en16931.saxon_server_url parameter, the schematrons are silently skipped and the log still says "validated".

Test coverage remains partial: only VAT categories S and E were exercised end to end; Z, AE, K, G, refunds and down payments are not.

Two pre-existing failures surfaced by the full test run

Neither is caused by the migration, but both are worth knowing:

  • account_invoice_en16931 makes country_id required in the partner view (en16931.country.required.on.partner), which breaks three standard base tests: test_create_res_partner, test_lang_computation_form_view and test_onchange_parent_sync_user, all of which fill a partner form without a country. The view is identical in 18.0, so 18.0 fails the same way — it looks like the suite had simply never been run against it. Reporting it rather than changing it: the trade-off is deliberate and documented in the view's own comment.
  • account_invoice_import.tests.test_invoice_import fails in setUpClass on 19.0: NotNullViolation on account_tax.tax_group_id, an at_install test running against a partial registry. That is a third-party dependency, out of scope here.

Provisioning notes

Two transitive dependencies are invisible in the manifests and only surface at install time: account_payment_base_oca and pdf_xml_attachment (OCA/reporting-engine). account_payment_batch_oca lives in OCA/bank-payment-alternative, not bank-payment. account_invoice_import only exists as an open PR, OCA/edi#1363, consumed as-is for the test run.

account_tax_unece 19.0.2.0.0 (with unece_vatex_id and the 88 VATEX codes) is required and available on the official 19.0 branch since OCA/community-data-files#284 was merged.

Follows up on #7 and #35.

@ak-git-bot ak-git-bot added mod:l10n_fr_einvoicing Module l10n_fr_einvoicing mod:l10n_fr_einvoicing_import Module l10n_fr_einvoicing_import mod:l10n_fr_einvoicing_sale Module l10n_fr_einvoicing_sale mod:account_invoice_en16931_py3o Module account_invoice_en16931_py3o mod:l10n_fr_account_invoice_en16931 Module l10n_fr_account_invoice_en16931 series:19.0 mod:l10n_fr_einvoicing_payment_batch_oca Module l10n_fr_einvoicing_payment_batch_oca mod:account_invoice_en16931 Module account_invoice_en16931 mod:l10n_fr_einvoicing_purchase Module l10n_fr_einvoicing_purchase labels Jul 29, 2026
Fix crash when no last sync date on partner
Fix criteria to create flow on customer invoices
Add support for invoice attachment generation in Factur-X as attachement
to PDF.
Simplify proto of method generate_en16931_xml()
Change 'variant' by 'invoice_format' to unifiy vocabulary
[FIX] account_invoice_en16931_py3o: add support for context key-
'regular_pdf_invoice'
[IMP] l10n_fr_einvoicing: add checks on attachments for Chorus Pro
Re-organize invoice form view: all fields are now in the dedicated tab-
"e-Invoicing"
@njeudy

njeudy commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Pushed the fix for #36 on this branch as well — the MemoryError was found while migrating, and the code is identical across 16.0 / 18.0 / 19.0. Standalone PR against 18.0: #37.

alexis-via and others added 15 commits July 30, 2026 11:32
Move France-specific code for BT-23 from account_invoice_en16931 to
l10n_fr_account_invoice_en16931
…ction_confirm

When sale_subscription is installed, action_confirm() splits self into
recurring/non-recurring subsets and calls super().action_confirm() on
each, including empty ones. self.ensure_one() at the top of
_action_confirm() then raises on the empty subset, blocking
confirmation of any non-subscription order. Moving ensure_one() inside
the loop lets empty recordsets pass through as a no-op, matching how
the rest of the method already handles batches.

Fixes akretion#40
Bump manifest to 19.0.1.0.0. Core l10n_fr renamed the partner-level flag
from res.partner.is_france_country to res.partner.l10n_fr_is_french in 19.0
(res.company.is_france_country is unchanged). Adapt the buyer-side check
accordingly.
…ework

report_py3o removed the py3o.report model in 19.0 and moved rendering into
ir.actions.report._render_py3o. The _postprocess_report hook this glue
overrides no longer exists. Bump the manifest to 19.0.1.0.0 but keep the
module uninstallable until the hook is reimplemented against the new flow.
Bump manifest to 19.0.1.2.0 and adapt to the 19.0 breaking changes:

- _sql_constraints is no longer supported by the ORM (it only logs a warning
  and the constraints are never created): declare models.Constraint instead.
- Core l10n_fr renamed the partner-level flag is_france_country to
  l10n_fr_is_french (res.company.is_france_country is unchanged).
- l10n_fr_siret dropped the stored siren/siret/nic fields in favor of the core
  company_registry field plus the _get_siren()/_get_siret()/_get_nic() getters.
  Adapt the compute dependencies, the partner search domains and the demo data
  accordingly. Searching a partner by SIREN now uses the upstream pattern
  company_registry =like '<siren>%'.
- l10n_fr_siret_account no longer exists in 19.0 (it only added tracking on the
  dropped siren/nic fields): depend on l10n_fr_siret directly.
- Demo partner Burger Queen had a VAT number inconsistent with its SIREN, which
  19.0 now rejects in _check_siret: align the VAT on the SIREN.
Bump the five layered modules to 19.0.1.0.0:
dashboard_banner, import, payment_batch_oca, purchase and sale.

- l10n_fr_einvoicing_sale: partner-level is_france_country was renamed to
  l10n_fr_is_french in core l10n_fr 19.0.
- res.company._sql_constraints is no longer supported by the ORM: declare a
  models.Constraint instead.

All overridden core methods (_prepare_invoice, _get_invoice_grouping_keys,
_action_confirm, generated2uploaded, _prepare_create_invoice_vals) keep the
same signature in 19.0.
19.0 merged the base partner form's warning_company banner into warning_tax:
anchor the directory warnings on the remaining one.
19.0 replaced the account.journal form's group_alias_ro/group_alias_ro_general
groups by a single group_email_alias in the Advanced Settings page.
sale_commercial_partner dropped its sale.order form view in 19.0 and only keeps
the search filter, so inherit sale.view_order_form directly. The 'done' state of
sale.order no longer exists either: the order is locked instead.
…_invoice_id

sale_commercial_partner no longer provides commercial_partner_invoice_id in
19.0. Declare the related invoicing entity in the module's own namespace and
expose it in the form view, so the directory line domain keeps working.
…egistry

_fr_directory_check_siren_siret_vat() was written against the 18.0 layout
where l10n_fr_siret provided three fields: siren and nic (stored) plus a
siret computed from them. In 19.0 these fields are gone: the value lives in
the native company_registry field of res.partner (9 digits for a SIREN, 14
for a SIRET) and l10n_fr_siret only exposes the _get_siren()/_get_siret()/
_get_nic() getters, which the rest of the module already uses.

Rewrite the cleanup around that single field. The siren/nic cross-checks
collapse into a length + checksum check, split out into
_fr_check_company_registry() to keep the method readable, and an invalid
SIRET now falls back to its SIREN instead of the '<siren>*****' placeholder
that 18.0 used to render. Fix the search domain of the settings button
accordingly and rewrite the tests on company_registry.
The 19.0 branch was opened with every manifest set to installable: False.
The port is complete for the seven deliverable modules, so mark them
installable. Two stay out: account_invoice_en16931_py3o (report_py3o
dropped the py3o.report model in 19.0) and l10n_fr_einvoicing_dashboard_banner
(its account_dashboard_banner dependency has no 19.0 host repository yet).
njeudy added 4 commits August 7, 2026 11:04
… 19.0

Odoo 19 dropped the mobile field from res.partner and kept phone alone
(odoo/addons/base/models/res_partner.py). Both remaining readers of it raised
an AttributeError as soon as an EN16931 document was generated, which is why
the install-only recipe never caught them: _en16931_partner_data() feeding the
seller and buyer blocks, and BT-42 for the sales contact.
…hook

Importing facturx died on "Can't open orphan path" and, since the failure was
only logged at debug level, the module loaded fine and blew up much later with
a NameError on generate_xml that pointed nowhere.

The cause is in Odoo itself: odoo/_monkeypatches registers an import hook on
stdnum and replaces the package loader with a SimpleNamespace carrying only
create_module and exec_module. That object has no get_resource_reader, so
importlib.resources can no longer open the .dat files stdnum.iban and its
siblings read at import time, and every library importing them breaks. The
patch this hook installs is a documented no-op from python-stdnum 2.0 on, so
dropping the hook and importing again is safe. Still present on odoo master.

Also raise the final failure to warning: a silent debug on this import costs
hours to trace back from the NameError it produces.
…ines

The line VAT rate was only emitted for categories S, K and G, so an exempt
line (E), a zero-rated one (Z) or a reverse charge (AE) went out without
BT-152 at all. BR-E-05 and its siblings require the rate to be there and to
be 0; only category O, out of scope, may omit it (BR-O-05).

The knock-on effect is worse than the missing element: with no rate on the
line, the schematron matches no line against the E VAT breakdown, so
BR-FXEXT-E-08 (and BR-FREXT-E-08 on UBL) compares a 300 taxable amount
against a sum of zero lines and fails too. An invoice carrying a single
exempt line was rejected by the base schematron in both CII and UBL.

Present in 18.0 as well: not a migration regression.
@njeudy
njeudy force-pushed the MIG-19.0-account_invoice_en16931 branch from 3f46186 to 8dfdd03 Compare August 7, 2026 09:05
njeudy added 2 commits August 7, 2026 12:06
decimal.precision is database-global, so this check forces every company of a
multi-company database down to 2 decimals on discounts, and it does not protect
the generated document: the discount percentage is never exported, and
disc_prec has a single consumer, the float_compare deciding whether a line
carries a discount. At 2 decimals a 0.001 discount compares equal to zero and
BT-147 is silently omitted, so lowering the precision produces a less faithful
document, not a more compliant one.

Reported by @almumu in akretion#33. Same change proposed upstream on 18.0 for akretion#43; to
be dropped from this branch if it is merged there.
…ng companies

_post() runs the company-level configuration check for every sale document,
with no condition, so every company of the database has to be UNECE-coded
before it can post a customer invoice -- including companies that will never
emit an EN16931 document.

Measured on a clean 19.0 database with the whole stack installed: the
unconditional check raises "The following errors have been detected in company
... that block EN16931 e-invoicing" in **390 standard Odoo tests** (account 287,
sale 45, account_edi_ubl_cii 18, account_payment 8), which all create their own
test company and post invoices on it. With this commit, that count drops to
zero.

The document itself is not at risk: the same checks run from
_en16931_checks_upon_invoice_generation(), so an incomplete configuration still
cannot produce an EN16931 document. Add en16931_issuer on res.company, unset by
default, gate the _post() check on it, and keep the configuration verifiable on
demand from the accounting settings.

Reported by @almumu in akretion#33. Same change proposed upstream on 18.0 for akretion#42; to
be dropped from this branch if it is merged there.
@njeudy njeudy changed the title [WIP][19.0] Migrate account_invoice_en16931 and the einvoicing stack to 19.0 [19.0] Migrate account_invoice_en16931 and the einvoicing stack to 19.0 Aug 8, 2026
alexis-via and others added 15 commits August 9, 2026 07:57
(cherry picked from commit 7ff630e)
…server is down

Fixes bug akretion#44 the schematron validation is silently skipped when no Saxon server is reachable
Update and improve fr translation

(cherry picked from commit f8a72c6)
…ve (new method dedicated to the check on the ivoice lines)

Inspired by bug akretion#38

(cherry picked from commit 1757cae)
Improve display_name of flows: add syntax

(cherry picked from commit b7df7da)
…rectory status

FIX l10n_fr_einvoicing_sale: activate checks only for VAT registered companies

Mutualize code between l10n_fr_einvoicing_sale and l10n_fr_einvoicing:
new method _fr_directory_should_sync_upon_confirmation() on res.partner

Block back to draft when the invoice is not linked to a flow but would
have to be sent to AP after re-confirmation

(cherry picked from commit fcf1cad)
…rmation for unittests

(cherry picked from commit 7399781)
…ny directory lines

This new feature require PR 1386 from OCA/edi
Remove field company_partner_id from account.journal because it is a native field!

(cherry picked from commit aba1448)
The module was missing from the 19.0 branch: it lives on 18.0 and on the 16.0
backport, but was never forward-ported.

Almost nothing had to change. The two 19.0 traps this module could have hit do
not apply: it never touches res.partner.siren/siret/nic -- dropped by
l10n_fr_siret 19.0 -- because it goes through the _get_siren() getter and builds
its matching index by iterating over partners rather than searching that field;
and it declares no _sql_constraints, which 19.0 would have ignored silently.
tools.create_index, used to index account_move.commercial_partner_id, is
unchanged in 19.0.

Ported from the 18.0 branch, whose view syntax is already the 19.0 one, plus the
tests carried on the 16.0 branch (they are not on 18.0).
@ak-git-bot ak-git-bot added the mod:l10n_fr_einvoicing_directory_import Module l10n_fr_einvoicing_directory_import label Aug 9, 2026
njeudy added 4 commits August 9, 2026 09:54
…_registry

l10n_fr_siret 19.0 no longer exposes siren/nic on res.partner -- the value lives
in the native company_registry field. The six partners created by the test suite
still used the 16.0 fields, so both setUpClass raised
"ValueError: Invalid field 'siren' in 'res.partner'" and the whole suite errored
out.

The module code itself needed nothing: it reads SIREN through the _get_siren()
getter. Only the fixtures did.

Note that fr.directory.line keeps its own siren field, which is unrelated and
left as is.
…rtners

On 19.0, _get_siren() goes through _common_get_siren_siret(), which returns None
when the partner has no country_id or a country that is not France. The 16.0
version read the siren field directly, so the fixtures never needed a country
and the whole export/import suite found no SIREN at all.
…e instance

19.0 installs its own guard by patching commit/rollback/close on the cursor
*instance* (odoo/tests/common.py), and an instance attribute shadows the class
one, so patching type(self.env.cr) no longer has any effect: the nine import
tests all died on the AssertionError raised by that guard.

Patch the instance instead. The intent is unchanged -- the batched commits of
_directory_import_csv() must not release the savepoint TransactionCase rolls
back to.
factur-x writes the pdfaid:part=3 XMP but converts nothing: the PDF still comes
from wkhtmltopdf, without sRGB OutputIntent, with glyph widths inconsistent with
the embedded fonts and a pre-1.7 header. veraPDF therefore rejects a file that
claims PDF/A-3 — measured on 19.0: FAIL 3b.

Run the already-Factur-X PDF through OdooPdfFileWriter.convert_to_pdfa(), which
account_edi_ubl_cii uses for the same purpose; cloneReaderDocumentRoot keeps the
embedded XML and the XMP. Same treatment for the UBL-in-PDF flavour.

fonttools is declared: convert_to_pdfa() needs it to rebuild the width arrays,
and only logs a warning when it is missing.
@alexis-via
alexis-via force-pushed the 19.0 branch 2 times, most recently from fd33528 to ceb2f36 Compare August 18, 2026 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod:account_invoice_en16931_py3o Module account_invoice_en16931_py3o mod:account_invoice_en16931 Module account_invoice_en16931 mod:l10n_fr_account_invoice_en16931 Module l10n_fr_account_invoice_en16931 mod:l10n_fr_einvoicing_directory_import Module l10n_fr_einvoicing_directory_import mod:l10n_fr_einvoicing_import Module l10n_fr_einvoicing_import mod:l10n_fr_einvoicing_payment_batch_oca Module l10n_fr_einvoicing_payment_batch_oca mod:l10n_fr_einvoicing_purchase Module l10n_fr_einvoicing_purchase mod:l10n_fr_einvoicing_sale Module l10n_fr_einvoicing_sale mod:l10n_fr_einvoicing Module l10n_fr_einvoicing series:19.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants