Skip to content

[16.0] Migrate account_invoice_en16931 and the einvoicing stack to 16.0 - #33

Draft
njeudy wants to merge 74 commits into
akretion:16.0from
Alusage:MIG-16.0-account_invoice_en16931
Draft

[16.0] Migrate account_invoice_en16931 and the einvoicing stack to 16.0#33
njeudy wants to merge 74 commits into
akretion:16.0from
Alusage:MIG-16.0-account_invoice_en16931

Conversation

@njeudy

@njeudy njeudy commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

WIP — migration of the whole fr-einvoicing stack to 16.0, rebased on upstream/16.0.

Ports the modules to 16.0: Python API gaps (env._, read_group, button_draft, hard_lock_date, reversal_move_id…), views (<setting>o_setting_box, attrs=, <tree>), and the tax layer rewritten on the 16.0 tax engine.

Modules — all ten are installable: True

  • account_invoice_en16931 — EN16931 Factur-X core
  • l10n_fr_account_invoice_en16931 — FR EN16931 layer
  • l10n_fr_einvoicing
  • l10n_fr_einvoicing_import
  • l10n_fr_einvoicing_purchase
  • l10n_fr_einvoicing_sale
  • l10n_fr_einvoicing_dashboard_banner
  • l10n_fr_einvoicing_directory_import
  • l10n_fr_einvoicing_batch_payment (renamed from l10n_fr_einvoicing_payment_batch_oca: account_payment_batch_oca is 18.0-only, replaced by account_payment_order)
  • account_invoice_en16931_py3o

Dependencies: plain OCA 16.0 is enough

Two prerequisites were missing from OCA 16.0 when this port started and were carried on Alusage forks. Both have since landed upstream, so no fork is needed: the VATEX codes are on OCA/community-data-files 16.0, and is_france_country / _get_siren() / _get_siret() / _get_nic() are on OCA/l10n-france 16.0 since 9e662b1a (27/07). Reported by @almumu, verified here.

That same commit extended _check_siret to the vat field — a French VAT number must now end with the nine digits of the SIREN. The demo of l10n_fr_einvoicing carries an inconsistent pair (siren 892350455 / vat FR23334175221, which belongs to SIREN 334175221), so loading it aborts the database. Fixed here as FR78892350455; the same pair is still on 18.0.

Rebase on upstream/16.0

The branch is rebased on the seven commits carried onto 16.0. Four of them had been backported here beforehand and were dropped in favour of upstream's own, along with the local fix for #36 — upstream's [FIX] don't store fr_einvoicing_required removes the mass recompute at the root.

Two of those commits are written against 18.0 APIs and needed re-porting: the twelve self.env._() calls they add (before 17.0 the helper returns the string and silently drops its arguments), and BT-23, which now decides on product_id.type == "consu" — on 16.0 the stock module adds ('product', 'Storable Product') to product.type, so every physical-goods invoice would go out as a mixed process (M1/M2) instead of B1/B2.

Two fixes carried temporarily

#46 (#43, Discount precision) and #47 (#42, EN16931 config scope) are applied here so they can be tested on 16.0, and are 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.

Test run

Clean 16.0 community database, demo data, cascade install, plain OCA l10n-france 16.0 — so with _check_siret actually in place. All ten modules install, l10n_fr_einvoicing passes its 17 tests, and the demo loads. Full run: 31 failed, 9 errors of 1603 tests, all outside this stack (base, mail, sms, google_gmail, digest, base_vat, account_invoice_import).

Context and rationale: #28

@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_dashboard_banner Module l10n_fr_einvoicing_dashboard_banner mod:l10n_fr_einvoicing_sale Module l10n_fr_einvoicing_sale mod:account_invoice_en16931 Module account_invoice_en16931 mod:l10n_fr_account_invoice_en16931 Module l10n_fr_account_invoice_en16931 mod:l10n_fr_einvoicing_purchase Module l10n_fr_einvoicing_purchase mod:l10n_fr_einvoicing_batch_payment Module l10n_fr_einvoicing_batch_payment series:16.0 mod:l10n_fr_einvoicing_directory_import Module l10n_fr_einvoicing_directory_import 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.

@almumu

almumu commented Aug 3, 2026

Copy link
Copy Markdown

@njeudy First of all, thank you for this work!

We deployed this branch (f2a298a) on a 16.0 community database to validate it, and it holds up:
the nine modules in scope install with no error against our own addons path, and the suite passes 0 failed, 0 error(s) of 49 tests. Thanks for the MIGRATION-16.0.md — it made the review mucheasier.

Everything below is either a correction that removes work from your instructions, or a defect that pre-exists on 18.0 and that this port merely surfaced. None of it is caused by the backport.

One correction to MIGRATION-16.0.md, which also explains a difference between our two runs

The document says the l10n_fr_siret helpers (_get_siren(), _get_siret(), is_france_country) exist on the 18.0 branch only, and backports them on Alusage/l10n-france. But all three are on OCA/l10n-france 16.0is_france_country at res_partner.py:159, _get_siren at :197, _get_siret at :228. They landed on 27/07 with 9e662b1a ("[IMP] l10n_fr_siret: add get methods for siren/siret/nic"); the previous commit touching that file dates from May 2024. So the backport is not needed any more — plain OCA 16.0 is enough, one less fork to provision.

But that same commit did not only add the three helpers: it also added a constraint.

# l10n_fr_siret/models/res_partner.py:42 (OCA l10n-france 16.0, added by 9e662b1a)
    @api.constrains("siren", "nic", "vat")
    def _check_siret(self):

Among other things it now requires a French VAT number to end with the 9 digits of the SIREN. Before 9e662b1a the constraint did not watch the vat field at all.

That is where our two runs diverge, and why installing this branch with demo data aborts the database for us and not for you: your instance consumes a reduced copy of l10n_fr_siret carrying the helpers without the constraint, while we consume the whole OCA 16.0 branch, so we get both.
l10n_fr_einvoicing's demo creates Burger Queen with siren = 892350455 and vat = FR23334175221 — that VAT belongs to SIREN 334175221, so the pair is inconsistent and the constraint rejects it during the demo load. The consistent value for that SIREN would be FR78892350455.

The same pair is on the 18.0 branch, so it is not a backport artefact; it will surface there too as soon as an instance runs a l10n_fr_siret newer than 27/07 with demo data.

Second correction, unrelated

The VATEX codes are also already on OCA/community-data-files 16.0, so that backport is not needed either.

Three things that block installing the stack on a multi-company database

We then installed it on a real multi-company database (one French company subject to the reform, alongside companies in two other countries (Italy and Spain). Three defects block that, none caused by the backport and all three present on 18.0 as well:

And one that is already reported by someone else: l10n_fr_einvoicing_sale._action_confirm() calls ensure_one() before its own loop, which breaks confirming sale orders in batch or on an empty recordset, for every company — and the module is auto_install: True. #41 fixes it on 18.0; it would be worth carrying into this branch.

We have the 16.0 environment set up and will keep it, so if any of this is useful to test — a change on this branch, a fix on 18.0 you want checked against 16.0, or a case you cannot reproduce — we are happy to run it and report back. Thanks again for opening the branch up; it is what made it possible for us to evaluate the reform stack on 16.0 at all.

alexis-via and others added 3 commits August 4, 2026 16:51
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
@njeudy

njeudy commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

thanks for your tests, will check this and update this PR (and resolv conflicts too) :)

njeudy added 3 commits August 7, 2026 10:35
…backport analysis

Retarget the three in-scope modules to 16.0.1.0.0 and mark them not installable:
the code is still the 18.0 code and would not install on 16.0.

Document the gap analysis in MIGRATION-16.0.md: the module is built on the generic
tax engine introduced in Odoo 18, which does not exist on 16.0, and two upstream
dependencies must be backported first (VATEX codes on account_tax_unece, still an
open OCA pull request, and the _get_siren/_get_siret/is_france_country additions to
l10n_fr_siret, available on the 18.0 branch only).

No migration code at this stage: specs are validated first.
Everything that does not touch the tax computation layer, so that what is left
is the rewrite of the tax engine itself.

* Translations: self.env._() only exists as of 17.0, and the 16.0 _() takes no
  interpolation argument. Rewrite the 41 calls as _("...") % {...} (or % value),
  and import _ from odoo. Done with an AST-based script, so multi-line calls and
  the two chained .format(**format_vals) cases are preserved. The literal %% in
  the VAT rate message still renders as %, as before.
* res_company._compute_no_vat_taxes(): _read_group(groupby=, aggregates=) and its
  recordset results are 18.0 API. Rewritten on the 16.0 read_group(), which
  returns dicts and exposes the count as <groupby>_count.
* Views: the 17.0+ attribute syntax (invisible="expr") is not supported on 16.0,
  rewritten with attrs=. Applies to account_move.xml, the generate wizard and
  l10n_fr_account_invoice_en16931.
* res_config_settings_view.xml: rewritten, not converted. The <block>/<setting>
  tags do not exist on 16.0, where settings are built with div.o_settings_container
  and div.o_setting_box. The anchor default_taxes_setting_container does exist,
  so the view still hooks in the same place.
* _prepare_bt23(): on 16.0 the stock module adds ('product', 'Storable Product')
  to product.type, while 18.0 only has consu/service/combo and carries storability
  in is_storable. Testing == "consu" alone silently classified goods as a mixed
  business process (BT-23 = M1/M2 instead of B1/B2). Match consu and product.

Not touched: account_move_line.py line 182 exceeds E501 upstream already; fixing
it here would only add noise to the diff with 18.0.

The tax computation layer is untouched, so the modules stay installable: False.
…ython 3.9

The target instance runs Odoo 16.0 on Python 3.9.2, where the PEP 604 syntax
raises TypeError: unsupported operand type(s) for |: 'type' and 'type'. Upstream
targets Python 3.10+ (see target-version = "py310" in .ruff.toml), which is
correct for 18.0 but not for us.

Use the (int, float) tuple form, which is valid on every version.

Found by running the code against the real image: it is valid syntax, so it
passes both review and a py_compile on a 3.12 host. Validate with:

    docker run --rm -v "$PWD/code:/src:ro" python:3.9-slim sh -c \
      'cp -r /src/<module> /tmp/ && cd /tmp && python3 -m compileall -q <module>'
@ak-git-bot ak-git-bot added the mod:account_invoice_en16931_py3o Module account_invoice_en16931_py3o label Aug 7, 2026
njeudy added 2 commits August 7, 2026 11:21
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 in a multi-company database a single company subject to
the reform makes every other company unable to post customer invoices until
its own taxes are UNECE-coded -- including companies that will never emit an
EN16931 document.

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 added a commit to Alusage/fr-einvoicing that referenced this pull request Aug 7, 2026
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.
njeudy added a commit to Alusage/fr-einvoicing that referenced this pull request Aug 7, 2026
…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 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for running it on a real 16.0 database — the branch is updated, and all four points are addressed.

The two backports are gone. You're right on both, and I checked before removing them: is_france_country, _get_siren(), _get_siret() and _get_nic() are on OCA/l10n-france 16.0 since 9e662b1a (27/07), and unece_tax_vatex.xml plus unece_vatex_id are on OCA/community-data-files 16.0. MIGRATION-16.0.md no longer instructs anyone to provision those forks — two repositories fewer.

The demo VAT number is fixed, and your diagnosis of the divergence was exactly right. FR23334175221 is the VAT number of SIREN 334175221, while the partner carries SIREN 892350455 — the consistent value is FR78892350455 (key = (12 + 3 × (siren mod 97)) mod 97 = 78). Worth noting for @alexis-via that the same pair is still on 18.0, so any 18.0 instance running a post-27/07 l10n_fr_siret with demo data will hit it too.

#41 is already on the 16.0 branch upstream, so the rebase brought it in.

#42 and #43: both patched on this branch, and proposed upstream on 18.0 where they belong — #47 and #46. They are kept here so you can test them on 16.0 straight away, and will be dropped from this branch once merged upstream (or adjusted, if Alexis prefers a different shape). On #42 I went with your option 2 rather than option 1, plus an on-demand Check Configuration button so issuing companies keep an early warning.

On that one, running the suite turned up an argument stronger than the multi-company case. On a clean 19.0 database with the whole stack installed, the unconditional _post() check 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. The full run goes from 41 failed, 418 errors of 3902 tests down to 39 failed, 51 errors of 3976 tests with the patch. Same code on 18.0, so the effect should be identical there. Details in #47.

#44: commented on the issue with what we measured on our own deployment, including a Saxon configuration that fixes the codedb lookups without --insecure, and the image we run.

The branch is also rebased on upstream 16.0, which had grown seven commits meanwhile — four of them were backports carried here and are now dropped in favour of upstream's own. Two of those commits needed re-porting: twelve self.env._() calls (which silently drop their arguments before 17.0) and BT-23, which now decides on product_id.type == "consu" — on 16.0 a storable product is "product", so every physical-goods invoice would have gone out as a mixed process (M1/M2) instead of B1/B2.

We ran the cascade install ourselves this time, on a clean 16.0 database with demo data and plain OCA l10n-france 16.0 — so with the _check_siret constraint actually in place. All eight modules install, l10n_fr_einvoicing passes its 17 tests, the demo loads, and Burger Queen lands with FR78892350455. The remaining failures are all outside the stack: base, mail, sms, google_gmail, digest, base_vat, and account_invoice_import.

@njeudy njeudy changed the title [WIP][16.0] Migrate account_invoice_en16931 and the einvoicing stack to 16.0 [16.0] Migrate account_invoice_en16931 and the einvoicing stack to 16.0 Aug 8, 2026
alexis-via and others added 19 commits August 9, 2026 08:01
(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)
Two calls introduced by the cherry-picks of the upstream 18.0 commits. Before
17.0 the translation helper only returns the string and silently drops its
arguments, so the message would have been rendered with its raw placeholders.
… 16.0

Five messages were written with the 18.0 signature _("… %(x)s", x=value). Before
17.0 the helper only returns the string and silently ignores what follows, so
those UserError messages reached the user with their raw placeholders instead of
the SIREN, the row number or the counters.

Surfaced while cherry-picking the upstream pre-commit reformatting, which moved
those calls around without changing them; the defect predates it and came from
the initial 16.0 port.
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_batch_payment Module l10n_fr_einvoicing_batch_payment mod:l10n_fr_einvoicing_dashboard_banner Module l10n_fr_einvoicing_dashboard_banner 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_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:16.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants