Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
e56719f
Initialize 16.0 branch
alexis-via Jul 28, 2026
68535f8
[UPD] addons table in README.md
Jul 28, 2026
ab8d232
[ADD] setup.py
Jul 28, 2026
96e37ec
[IMP] Configuration to send/receive invoices on accounting config page
alexis-via Jul 29, 2026
97d10bd
[IMP] account_invoice_en16931: add invoice_attachment_ids
alexis-via Jul 29, 2026
169ca28
[IMP] Option to generate factur-x with old Chorus XML syntax in the w…
alexis-via Jul 30, 2026
5720633
[FIX] Don't validate schematron fr_ctc for Chorus old XML syntax
alexis-via Jul 30, 2026
20a4af4
[IMP] Add field business_process_type (BT-23)
alexis-via Aug 4, 2026
75eab98
[FIX] l10n_fr_einvoicing_sale: ensure_one on empty recordset breaks a…
arpol-dev Aug 2, 2026
1c5a7ed
[FIX] l10n_fr_einvoicing: don't store fr_einvoicing_required on accou…
alexis-via Aug 5, 2026
9dd7926
[MIG] account_invoice_en16931: retarget manifests to 16.0 and anchor …
njeudy Jul 16, 2026
85b6d5d
[MIG] account_invoice_en16931: port the mechanical 18.0 -> 16.0 gaps
njeudy Jul 16, 2026
b79cdba
[FIX] account_invoice_en16931: isinstance(x, int | float) breaks on P…
njeudy Jul 16, 2026
b9993c8
[MIG] account_invoice_en16931: rewrite the tax layer on the 16.0 tax …
njeudy Jul 17, 2026
5f248bf
[MIG] account_invoice_en16931: payment mean via the OCA payment mode …
njeudy Jul 17, 2026
2e96b6f
[FIX] account_invoice_en16931: BG-23 for 0% VAT taxes (exemptions)
njeudy Jul 17, 2026
98b4665
[IMP] account_invoice_en16931: make the Factur-X PDF a valid PDF/A-3 …
njeudy Jul 17, 2026
05f2fcd
[IMP] account_invoice_en16931: declare fonttools for PDF/A glyph widths
njeudy Jul 17, 2026
926982a
[FIX] account_invoice_en16931: pin fonttools<4.34 for convert_to_pdfa
njeudy Jul 17, 2026
a3e935b
[MIG] l10n_fr_einvoicing: port Python to 16.0 (env._ + core API gaps)
njeudy Jul 18, 2026
2f01933
[MIG] l10n_fr_einvoicing: port views to 16.0 (attrs, <tree>, settings)
njeudy Jul 18, 2026
ea15eb4
[MIG] l10n_fr_einvoicing satellites: port the 5 companion modules to …
njeudy Jul 18, 2026
4ef41f7
[FIX] l10n_fr_einvoicing: install-time view fixes for 16.0
njeudy Jul 18, 2026
92c0b9b
[FIX] l10n_fr_einvoicing satellites: install-time fixes for 16.0
njeudy Jul 18, 2026
f2d785f
[FIX] l10n_fr_einvoicing_import: 16.0 journal view anchor
njeudy Jul 18, 2026
e5a8b13
[IMP] account_invoice_en16931: make account_payment_unece optional
njeudy Jul 23, 2026
052fb43
[DOC] account_invoice_en16931: note account_payment_unece as optional
njeudy Jul 23, 2026
ccd5743
[FIX] account_invoice_en16931: port self.env._ to 16.0 in invoice_typ…
njeudy Jul 29, 2026
43a628d
[FIX] l10n_fr_einvoicing: port the remaining self.env._ calls to 16.0
njeudy Jul 29, 2026
3ff2fb5
[IMP] l10n_fr_einvoicing: activate the reform stack on 16.0
njeudy Jul 29, 2026
fa799f0
[DOC] MIGRATION-16.0: the whole stack is ported and installable
njeudy Jul 29, 2026
3316fff
[MIG] l10n_fr_einvoicing_directory_import: migration to 16.0
njeudy Jul 29, 2026
82a32f1
[IMP] l10n_fr_einvoicing_directory_import: batch the CSV import
njeudy Jul 29, 2026
5fba7f6
[FIX] l10n_fr_einvoicing_directory_import: index account_move.commerc…
njeudy Jul 29, 2026
5fd4702
[FIX] l10n_fr_einvoicing_directory_import: ensure the index at every …
njeudy Jul 29, 2026
a65a591
[IMP] l10n_fr_einvoicing_directory_import: import the directory in ba…
njeudy Jul 29, 2026
26553a4
[FIX] account_invoice_en16931: resolve the invoice report by XML ID
njeudy Jul 29, 2026
7a6145d
[ADD] l10n_fr_einvoicing_directory_import: tests
njeudy Jul 29, 2026
04ee855
[FIX] l10n_fr_einvoicing_directory_import: neutralise the batch commi…
njeudy Jul 29, 2026
ddd49f0
[FIX] l10n_fr_einvoicing_directory_import: assert on the SIREN, not o…
njeudy Jul 29, 2026
56644bf
[FIX] account_invoice_en16931: don't crash on a line with several VAT…
njeudy Jul 29, 2026
4ae413d
[FIX] l10n_fr_einvoicing: port the demo data to 16.0
njeudy Jul 30, 2026
e629db9
[DOC] MIGRATION-16.0: record the install and test run on 16.0
njeudy Jul 30, 2026
98021b1
[FIX] l10n_fr_einvoicing: pass a company recordset to try_loading in …
njeudy Aug 1, 2026
be3f6fe
[DOC] MIGRATION-16.0: list what is left to port from 18.0
njeudy Aug 1, 2026
87547aa
[IMP] account_invoice_en16931_py3o: mark the module installable in 16.0
njeudy Aug 1, 2026
04a4026
[DOC] MIGRATION-16.0: record the upstream backports and the full-casc…
njeudy Aug 1, 2026
605dffe
[DOC] MIGRATION-16.0: l10n_fr_account_tax_unece must be installed exp…
njeudy Aug 1, 2026
b50e153
[DOC] MIGRATION-16.0: the UNECE prerequisite takes two steps, not one
njeudy Aug 1, 2026
e92f4f8
[DOC] MIGRATION-16.0: record the tax configuration state and the test…
njeudy Aug 1, 2026
45ad1d2
[MIG] fr-einvoicing: port the new upstream commits to 16.0
njeudy Aug 7, 2026
fa9caca
[FIX] l10n_fr_einvoicing: make the demo VAT number match the demo SIREN
njeudy Aug 7, 2026
533f5f9
[DOC] MIGRATION-16.0: the two OCA prerequisites landed upstream
njeudy Aug 7, 2026
ab1907a
[FIX] account_invoice_en16931: drop the Discount decimal precision check
njeudy Aug 7, 2026
d58ede0
[FIX] account_invoice_en16931: only check the EN16931 config of issui…
njeudy Aug 7, 2026
949f111
[FIX] bug #39 BT-152 is required for all VAT categorie except O
alexis-via Aug 7, 2026
6679d51
Update pyfrctc min version to 0.15
alexis-via Aug 7, 2026
ac32cc8
[IMP] l10n_fr_einvoicing: add a condition on button_cancel()
alexis-via Aug 7, 2026
3c2ad85
[IMP] l10n_fr_einvoicing: more flexibility for reset to draft
alexis-via Aug 7, 2026
c828594
Fix strings and update fr translation
alexis-via Aug 7, 2026
729991f
[IMP] l10n_fr_account_invoice_en16931: improve official strings (!)
alexis-via Aug 7, 2026
fd23938
[IMP] Add support for pre-payment in invoice_type_code and business_p…
alexis-via Aug 7, 2026
31eed37
[IMP] account_invoice_en16931: add option to raise an error if saxon …
alexis-via Aug 8, 2026
a43690e
[IMP] account_invoice_en16931: split inherit of _post() on account.mo…
alexis-via Aug 8, 2026
c5e7e86
[IMP] l10n_fr_einvoicing: add info logs for out flows
alexis-via Aug 8, 2026
31265a4
[FIX] auto dir sync upon invoice confirmation when customer has no di…
alexis-via Aug 8, 2026
07f5c9c
[IMP] account_invoice_en16931: new method to get vat taxes on invoice…
alexis-via Aug 8, 2026
05be595
[FIX] account_invoice_en16931: skip en16931 checks upon invoice confi…
alexis-via Aug 8, 2026
81fbdc0
[IMP] l10n_fr_einvoicing_import: add option no_vat_deduction on compa…
alexis-via Aug 8, 2026
ba5fefb
[IMP] l10n_fr_einvoicing_import: update fr translation
alexis-via Aug 8, 2026
3d334e2
[FIX] account_invoice_en16931: env._ does not exist on 16.0
njeudy Aug 9, 2026
5993890
[IMP] l10n_fr_einvoicing_directory_import: pre-commit fixes
alexis-via Aug 7, 2026
e9690fd
[FIX] l10n_fr_einvoicing_directory_import: _() drops its arguments on…
njeudy Aug 9, 2026
f94b3ed
[IMP] l10n_fr_einvoicing_directory_import: avoid pre-commit warning
alexis-via Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .ruff.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@

target-version = "py310"
# 16.0 runs on Python 3.9: keeping py310 here makes pyupgrade (UP038) rewrite
# isinstance(x, (int, float)) into isinstance(x, int | float), which raises
# TypeError at runtime on 3.9. Do not raise this without checking the Python
# version of the target image.
target-version = "py39"
fix = true

[lint]
Expand Down
227 changes: 227 additions & 0 deletions MIGRATION-16.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# Backport EN16931 → Odoo 16.0

Working branch: `MIG-16.0-account_invoice_en16931`, rebased on `upstream/16.0`.

**Status: the whole stack is ported and installable on 16.0.** All ten modules below are back to
`installable: True` and install on a 16.0 community database.

## Scope

| Module | Status |
|---|---|
| `account_invoice_en16931` | Ported. Tax layer rewritten on the 16.0 tax engine. |
| `l10n_fr_account_invoice_en16931` | Ported, on OCA `l10n_fr_siret` 16.0. |
| `l10n_fr_einvoicing` | Ported (Python, views, `env._`). |
| `l10n_fr_einvoicing_import` | Ported. |
| `l10n_fr_einvoicing_purchase` | Ported. |
| `l10n_fr_einvoicing_sale` | Ported. |
| `l10n_fr_einvoicing_dashboard_banner` | Ported. |
| `l10n_fr_einvoicing_batch_payment` | Ported. Renamed from `l10n_fr_einvoicing_payment_batch_oca`: `account_payment_batch_oca` is 18.0-only, the overridden method lives on `account.payment.order` (OCA `account_payment_order`). |
| `l10n_fr_einvoicing_directory_import` | Ported (Sudokeys module, not upstream). Identical to the 18.0 branch, plus the tests. |
| `account_invoice_en16931_py3o` | Ported and `installable: True`. Its glue already fit 16.0; `report_py3o` exists in OCA `reporting-engine` 16.0 (`16.0.1.0.6`) with the same `py3o.report` API. |

## Why this was not a mechanical backport

The 18.0 code is built on the **generic tax engine introduced in Odoo 18** (`base_line` dicts +
`tax_details`). None of the methods it relies on exist on 16.0, which only has `compute_all()`:

- `_prepare_product_base_line_for_taxes_computation`, `_prepare_base_line_for_taxes_computation`
- `_add_tax_details_in_base_line`, `_add_tax_details_in_base_lines`
- `_round_base_lines_tax_details`, `_aggregate_base_lines_tax_details`,
`_aggregate_base_lines_aggregated_values`, `_prepare_tax_line_for_taxes_computation`

This affects `_check_en16931`, `_prepare_bg25_single_line`, `_prepare_bg20_single_line` and
`_prepare_bg23` (VAT breakdown: `BT-116`/`BT-117`/`BT-118`/`BT-119`). Rounding must match 18.0
exactly, or the EN16931 schematron fails.

Two more 18.0-only idioms had to be undone everywhere: `self.env._("…", key=value)`, which only
returns the string on 16.0 and silently drops its arguments (rewritten as `_("…") % (…)`), and the
view syntax (`<setting>` → `o_setting_box`, `invisible="…"` → `attrs=`, `<list>` → `<tree>`).

## 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 — the forks are no longer needed:**

1. **VATEX codes** on `account_tax_unece`: `data/unece_tax_vatex.xml`, the `unece_vatex_id` field
and `_compute_unece_vatex_id()` are on `OCA/community-data-files` `16.0`.
2. **`l10n_fr_siret` helpers**: `is_france_country`, `_get_siren()`, `_get_siret()` and `_get_nic()`
are on `OCA/l10n-france` `16.0`, added on 27/07 by
[`9e662b1a`](https://github.com/OCA/l10n-france/commit/9e662b1a).

⚠️ That same commit also extended `_check_siret` to the `vat` field: a French VAT number must now
end with the nine digits of the SIREN. Any partner data carrying an inconsistent pair — including
demo data — is rejected on load. This is what made the demo of `l10n_fr_einvoicing` abort the
database on instances running a post-27/07 `l10n_fr_siret`; fixed on this branch, and the same pair
is still on `18.0`.

Activating `account_invoice_en16931_py3o` adds one more repository to provision: `report_py3o` from
OCA `reporting-engine` 16.0, whose `external_dependencies` are `py3o.template` and `py3o.formats`
(picked up by jarvis `update_requirements`) plus the `libreoffice` deb. Odoo does not check the `deb`
key at install time, so the module installs without it — but no py3o report can actually be
rendered, hence no end-to-end test of the glue.

## 🔴 A French database is not EN16931-ready out of the box

`account_invoice_en16931._post()` calls `_en16931_checks()`, which refuses to post **any** customer
invoice while the company's tax configuration is incomplete. On a fresh French database that takes
two separate steps, and neither happens on its own.

**Step 1 — install `l10n_fr_account_tax_unece`** (OCA `l10n-france`, `post_init_hook`
`set_unece_on_taxes`). Without it, every post fails with *"Tax 'TVA 20% (Goods)' has no UNECE Tax
Type"*. The module is not a dependency of anything in this stack, and it looks self-installing — its
manifest says `"auto_installable": True` — but **that key does not exist in Odoo**. The real one is
`auto_install`, and unknown manifest keys are ignored silently, so it never auto-installs. Upstream
OCA typo, identical on 16.0, 17.0 and 18.0, so not a backport artefact.

**Step 2 — set a VAT exemption reason on the exempt taxes.** With step 1 done, the check moves on to
*"VAT tax 'TVA 0% EXO (Goods)' has UNECE Tax Category '[E] Exempt from tax' so it should have a VAT
Exemption Reason"* — on `TVA 0% EXO` for both goods and services. `l10n_fr_account_tax_unece` sets
`unece_type_id` and `unece_categ_id` only; it never sets `unece_vatex_id` (zero occurrences in its
data file), and `account_tax_unece._compute_unece_vatex_id()` auto-maps categories K and G
(intra-EU, export) but not E. That is deliberate: a national exemption reason is a legal choice
(which CGI article), so no module can guess it — it has to be configured.

Both steps were verified on a demo database: with the stack alone, the demo invoices and seven
`account_payment_partner` tests fail on step 1; installing the module moves them to step 2 rather
than fixing them — the failure count does not budge. What the database looks like after step 1, which
shows the auto-mapping covering K and G but not E:

| company | sale tax | type | categ | vatex |
|---|---|---|---|---|
| Burger Queen | TVA 0% EU M | VAT | K | `VATEX-EU-IC` |
| Burger Queen | TVA 0% EXPORT | VAT | G | `VATEX-EU-G` |
| Burger Queen | **TVA 0% EXO** (goods, services) | VAT | **E** | **missing** |
| Burger Queen | TVA 20% / 10% | VAT | S | n/a |

Invoice-wise: `Burger Queen → 4 draft`, `Tricatel → 4 posted`. The draft ones cannot be rescued by
configuring the taxes afterwards — they are posted during install — so on a fresh demo database
expect four invoices left in draft on the French company. Note that `account`'s own demo does post
fine, because the dependency graph installs `account` well before `account_invoice_en16931`, while
`_post()` is not yet overridden.

### Running the test suite in the jarvis dev container

Pass `--http-port` on a free port (e.g. `--http-port=8899`). The container's entrypoint runs a
**resident Odoo** on 8069; sharing that port makes every core `HttpCase` hit the resident server
instead of the test one — a parasitised run showed 84 failures + 46 errors, with `web`, `payment`,
`portal`, `bus`, `web_editor` and nine `TestXMLRPC` failing, all of which vanish once the port is
isolated. Worse, on an empty database the resident server may start initialising it concurrently with
the run, which breaks the registry (`duplicate key … pg_type_typname_nsp_index`) and stops the
container.

## Install and test run on 16.0

Run on a jarvis 16.0 worktree (`fr-einvoicing-erp16-16`, branch `dev`), on a database created with
demo data:

```
odoo-dev -d odoo-dev -i account_invoice_en16931,l10n_fr_account_invoice_en16931,l10n_fr_einvoicing,\
l10n_fr_einvoicing_import,l10n_fr_einvoicing_purchase,l10n_fr_einvoicing_sale,\
l10n_fr_einvoicing_dashboard_banner,l10n_fr_einvoicing_batch_payment \
--test-enable --test-tags '/account_invoice_en16931,/l10n_fr_account_invoice_en16931,…' \
--stop-after-init
```

The eight modules install (80 modules loaded, none in error) and the suite passes:
`0 failed, 0 error(s) of 15 tests`. The 15 tests are
`l10n_fr_einvoicing/tests/test_partner_check_siren_siret_vat.py`, which exercises
`_fr_directory_check_siren_siret_vat()` — exactly the method whose `self.env._()` calls were
rewritten, so the port of those messages is covered.

The only warning was the demo data, which needed its own port (see the `[FIX] … port the demo data
to 16.0` commit): `l10n_fr` declares the French demo company under `base.partner_demo_company_fr` on
18.0 but under `l10n_fr.partner_demo_company_fr` on 16.0, and `try_loading()` loses its
`template_code` argument on 16.0.

### Full-cascade run, after the upstream backports

Same worktree, database recreated with demo data, all **ten** modules installed and `--test-enable`
left unrestricted, so the whole dependency cascade is tested too (83 modules loaded, none in error):

```
odoo -d odoo-dev -i account_invoice_en16931,account_invoice_en16931_py3o,\
l10n_fr_account_invoice_en16931,l10n_fr_einvoicing,l10n_fr_einvoicing_batch_payment,\
l10n_fr_einvoicing_dashboard_banner,l10n_fr_einvoicing_directory_import,\
l10n_fr_einvoicing_import,l10n_fr_einvoicing_purchase,l10n_fr_einvoicing_sale \
--test-enable --stop-after-init
```

**Our ten modules: 49 tests, 0 failures, 0 errors** (34 in `l10n_fr_einvoicing_directory_import`,
15 in `l10n_fr_einvoicing`) — confirmed twice, with and without `l10n_fr_account_tax_unece`, the
second time with the port isolated (`33 failed, 14 error(s) of 1646`). Testing the cascade also
surfaces 47 failures in modules we do not own: `mail` 11, `account_invoice_import` 8, `base` 7,
`account_payment_partner` 7, `sms` 5, `google_gmail` 3, `sale` 2, and one each in `report_py3o`,
`base_vat` and `account_dashboard_banner`. None is caused by this backport, and three are worth
knowing about:

- `account_invoice_import` (8) — from the `Alusage/edi` fork. Reproduced **on a clean database with
that module alone**, no reform stack installed: same `2 failures, 6 errors of 11 tests`. The cause
is a `payment_term` line inserted without `account_id`
(`account_move_line_check_accountable_required_fields`). Pre-existing, and worth checking against
[OCA/edi#1379](https://github.com/OCA/edi/pull/1379).
- `account_payment_partner` (7) — these do go through our `_post`, but the check that blocks them
(`_en16931_checks()`, in `account_invoice_en16931._post`) is unchanged from before the backport:
posting any customer invoice is refused while the company's taxes have no UNECE tax type, which is
the module's intended behaviour. It does mean **installing `account_invoice_en16931` breaks any
third-party test that posts a customer invoice on a company without UNECE-coded taxes** — an
upstream design point, not a 16.0 one. `4e97b69` narrows exactly this kind of check to
`fr_einvoicing_required` in `l10n_fr_einvoicing`; `account_invoice_en16931` still checks
unconditionally.
- `account_dashboard_banner` (1) — `isinstance(x, (int | float))` needs Python 3.10; this image runs
3.9. A bug in the OCA 16.0 module, unrelated to the reform stack.

The rest are core modules failing on this from-source image (`base`, `mail`, `sms`, `google_gmail`,
`sale`, `base_vat` — RTC, VIES SOAP, sanitizer), plus `report_py3o`'s `test_py3o_report_availability`
(no LibreOffice in the image, so `lo_bin_path` is empty).

## Rebase on `upstream/16.0`

Akretion opened its own `16.0` branch and carried seven commits onto it (the send/receive
configuration, `invoice_attachment_ids`, `business_process_type` (BT-23), the Chorus old-syntax
option and its schematron exemption, and two fixes). Four of them had been backported on this
branch beforehand; those backports were dropped on the rebase, upstream's own versions being
authoritative.

Those commits are written against 18.0 APIs, so one commit re-ports them to 16.0:

| What upstream added | What 16.0 required |
|---|---|
| Twelve `self.env._("…", key=value)` calls | Rewritten as `_("…") % {…}`: `env._` does not exist before 17.0 and silently drops its arguments. |
| BT-23 moved to `l10n_fr_account_invoice_en16931`, deciding on `product_id.type == "consu"` | On 16.0 the `stock` module adds `('product', 'Storable Product')` to `product.type`, so goods are `'product'` and every physical-goods invoice would be reported as a mixed process (M1/M2) instead of B1/B2. Uses the `_EN16931_GOODS_TYPES` tuple carried by `account_invoice_en16931`. |
| The shared `e-Invoicing` notebook page and the new settings, in 17+ view syntax | Converted to `attrs=` and to `o_setting_box`. |

Upstream's `[FIX] don't store fr_einvoicing_required` supersedes the fix that was carried here for
[#36](https://github.com/akretion/fr-einvoicing/issues/36) (MemoryError when the issuer's entity
type is set on a database with a real invoicing history): dropping `store=True` removes the mass
recompute at the root, so the local fix was dropped on the rebase.

## What is left to port

Nothing. Every upstream module has a 16.0 counterpart, all ten are `installable: True`, no 18.0-only
idiom is left in the ported code (`self.env._(`, `<list>`, dynamic `invisible=`/`readonly=` are all
at zero), and the whole stack installs and passes its tests on a demo database.

Two follow-ups, both outside the 16.0 stack itself:

1. **`account_invoice_en16931_py3o` has no end-to-end run.** It installs, but exercising the glue
means rendering a py3o report, which needs LibreOffice in the image — absent here, which is also
why `report_py3o`'s own `test_py3o_report_availability` fails (`lo_bin_path` empty). Nothing
points at the port; it is untested past install.
2. **The 16.0 tests of `l10n_fr_einvoicing_directory_import` are forward-ported to 18.0** on the
local branch `18.0-directory-import-tests` (off `origin/18.0-add-directory-import`, where that
module lives — it is absent from `upstream/18.0`). Not pushed, not run against an 18.0 database.

Nothing to take from `origin/18.0-add-directory-import` (the module is byte-identical here, minus the
version and the wizard view idiom) nor from `origin/18.0-tmp_hack_chorus` (a temporary Chorus hack).
`origin/18.0` itself is ten commits behind `upstream/18.0`.

## Full analysis

The method-by-method matrix, the tax verdict, effort estimate, risks and the ordered migration plan
live in the spec (Obsidian vault):

`projets/alusage/fr-einvoicing-erp16/specs/2026-07-16-backport-account_invoice_en16931-16.0.md`

Odoo tasks: <https://nicolas.alusage.fr/odoo/project/153/4468> (backport),
<https://nicolas.alusage.fr/odoo/project/153/4525> (reform stack activation).
Loading