Skip to content

[17.0][IMP] account_invoice_facturx: comply with Factur-X 4.x schematron#1340

Open
odoo-service wants to merge 5 commits into
OCA:17.0from
odoo-service:17.0
Open

[17.0][IMP] account_invoice_facturx: comply with Factur-X 4.x schematron#1340
odoo-service wants to merge 5 commits into
OCA:17.0from
odoo-service:17.0

Conversation

@odoo-service
Copy link
Copy Markdown

@odoo-service odoo-service commented May 13, 2026

Bumps the module to 17.0.1.3.0. The produced XML now validates cleanly against the bundled factur-x 4.x schematrons for all five profiles (MINIMUM, BASICWL, BASIC, EN16931, EXTENDED), and two new extension hooks on account.move let downstream modules plug in their own delivery date and line period source without patching the generator.

Fully includes #1320 and resolves four additional schematron items surfaced by the new meta-test suite.

Details:

tendil and others added 5 commits April 8, 2026 08:23
Prevent Factur-X invoices from failing schematron validation with
factur-x 4.x.

The generated XML must stay compliant with newer validation rules,
otherwise valid Odoo invoices cannot be exported as Factur-X documents.
This change removes the invalid email URI attribute, exports a
proprietary account identifier when IBAN is not available, and updates
tests to cover the new compliant output and the missing-account error.

Task: 5347
Some invoices already have a delivery/service date on the Odoo side, but
this date was not exported to the generated Factur-X XML.

As a result, the XML could fail validation because neither a header
delivery date nor a billing period was present.

This patch adds the delivery date to the header trade delivery block by
exporting `ActualDeliverySupplyChainEvent/OccurrenceDateTime`, using the
invoice date as the default delivery/service date.

The XML node order is also kept XSD-compliant.

Task: 5347
…iles + BG-26 line period

This change brings the Factur-X / ZUGFeRD generator to a state where
the produced XML validates cleanly against the bundled factur-x
schematrons for all five profiles (MINIMUM, BASICWL, BASIC, EN16931,
EXTENDED), and adds a generic extension hook for the EN 16931 invoice
line period (BG-26) so subscription / recurring billing modules can
plug in their own date fields without patching the line generator.
Schematron fixes
----------------
* MINIMUM: do not emit BuyerTradeParty/PostalTradeAddress and
  BuyerTradeParty/SpecifiedTaxRegistration. Both are marked as
  "not used" by the MINIMUM schematron. The corresponding
  SellerTradeParty children are kept because MINIMUM does require
  BT-31 (Seller VAT identifier).
* BASIC: emit a non-empty ApplicableHeaderTradeDelivery block with
  ActualDeliverySupplyChainEvent/OccurrenceDateTime (BT-72), fixing
  both PEPPOL-EN16931-R008 ("document MUST not contain empty
  elements") and BR-FX-EN-04 ("Each invoice must contain a delivery
  date or invoicing period"). The delivery date is read through the
  existing _cii_get_delivery_date() hook so the source field is not
  re-decided here.
* EN16931: do not emit CalculationPercent and BasisAmount inside
  GrossPriceProductTradePrice/AppliedTradeAllowanceCharge. Both are
  marked as "not used" in EN16931. The EXTENDED profile keeps them.
BG-26 line period
-----------------
* Add _cii_get_line_period(iline) hook on account.move that returns
  a (start_date, end_date) tuple. Default implementation reads, in
  order of precedence:
  - account.move.line.deferred_start_date / deferred_end_date
    (Odoo Enterprise account_accountant module, detected at runtime
    via _fields so this OCA module gains no hard dependency on
    Enterprise code), or
  - account.move.line.start_date / end_date (OCA module
    account_invoice_start_end_dates).
* The line generator emits BillingSpecifiedPeriod with StartDateTime
  and EndDateTime children whenever the hook returns at least one
  date, gated to the EN16931 and EXTENDED profiles per the BG-26
  schematron.
Tests
-----
* Add a schematron-based test suite that exercises all five Factur-X
  profiles for two scenarios (default invoice, invoice with
  line-level discount), plus a third subscription scenario covering
  BG-26. The subscription test guards itself with
  account.move.line._fields so it skips gracefully on Community
  installations without account_accountant.
Documentation
-------------
* Add readme/DEVELOP.md describing both extension hooks
  (_cii_get_delivery_date and _cii_get_line_period) with EN 16931
  business term mapping (BT-72, BG-26), the corresponding XPath
  locations and a worked example for downstream subscription
  modules.
* Add readme/HISTORY.md with an "Unreleased" section listing the
  schematron fixes and the BG-26 hook.
Co-authored-by: tendil <tendil@users.noreply.github.com>

Signed-off-by: Pawel Kazakow <github@kazacom.net>
_cii_get_delivery_date() returned self.invoice_date
unconditionally, so a value set in the standard Odoo
account.move.delivery_date field (manually, or via upstream
modules like subscription engines or
account_invoice_delivery_date_from_period) was silently dropped
from ActualDeliverySupplyChainEvent/OccurrenceDateTime (BT-72)
across all five Factur-X profiles. The hook now reads
self.delivery_date and falls back to invoice_date only when it
is unset.

Add an integration test that posts a draft with two subscription
lines and asserts that the auto-derived delivery_date lands in BT-72.
Bumps version to 17.0.1.3.0.
@OCA-git-bot
Copy link
Copy Markdown
Contributor

Hi @alexis-via,
some modules you are maintaining are being modified, check this out!

@OCA-git-bot OCA-git-bot added series:17.0 mod:account_invoice_facturx Module account_invoice_facturx labels May 13, 2026
@odoo-service odoo-service marked this pull request as ready for review May 13, 2026 13:28
@Gladys-Haelters
Copy link
Copy Markdown

What is the status of this fix? Currently this is breaking production for a client

RPC_ERROR Odoo Server Error Traceback (most recent call last): File "/opt/odoo/odoo/odoo/http.py", line 1985, in _serve_db return service_model.retrying(self._serve_ir_http, self.env) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/odoo/service/model.py", line 153, in retrying result = func() ^^^^^^ File "/opt/odoo/odoo/odoo/http.py", line 2013, in _serve_ir_http response = self.dispatcher.dispatch(rule.endpoint, args) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/odoo/http.py", line 2217, in dispatch result = self.request.registry['ir.http']._dispatch(endpoint) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/odoo/addons/base/models/ir_http.py", line 221, in _dispatch result = endpoint(**request.params) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/odoo/http.py", line 799, in route_wrapper result = endpoint(self, *args, **params_ok) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/addons/web/controllers/dataset.py", line 29, in call_button action = self._call_kw(model, method, args, kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/addons/web/controllers/dataset.py", line 21, in _call_kw return call_kw(Model, method, args, kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/odoo/api.py", line 484, in call_kw result = _call_kw_multi(method, model, args, kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/odoo/api.py", line 469, in _call_kw_multi result = method(recs, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/addons/account_peppol/wizard/account_move_send.py", line 142, in action_send_and_print return super().action_send_and_print(force_synchronous=force_synchronous, allow_fallback_pdf=allow_fallback_pdf, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/addons/account/wizard/account_move_send.py", line 833, in action_send_and_print return self._process_send_and_print( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/addons/account/wizard/account_move_send.py", line 767, in _process_send_and_print self._generate_invoice_documents(moves_data, allow_fallback_pdf=allow_fallback_pdf) File "/opt/odoo/odoo/addons/account/wizard/account_move_send.py", line 693, in _generate_invoice_documents self._prepare_invoice_pdf_report(invoice, invoice_data) File "/opt/odoo/odoo/addons/account/wizard/account_move_send.py", line 420, in _prepare_invoice_pdf_report ._render('account.account_invoices', invoice.ids) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/odoo/addons/base/models/ir_actions_report.py", line 1035, in _render return render_func(report_ref, res_ids, data=data) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/addons/account/models/ir_actions_report.py", line 58, in _render_qweb_pdf return super()._render_qweb_pdf(report_ref, res_ids=res_ids, data=data) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo-enterprise/iot/models/ir_actions_report.py", line 120, in _render_qweb_pdf return super()._render_qweb_pdf(report_ref, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/odoo/addons/base/models/ir_actions_report.py", line 928, in _render_qweb_pdf collected_streams = self._render_qweb_pdf_prepare_streams(report_ref, data, res_ids=res_ids) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/odoo/addons/sale_pdf_quote_builder/models/ir_actions_report.py", line 15, in _render_qweb_pdf_prepare_streams result = super()._render_qweb_pdf_prepare_streams(report_ref, data, res_ids=res_ids) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/odoo/addons-extra/account_invoice_facturx/models/ir_actions_report.py", line 31, in _render_qweb_pdf_prepare_streams move.regular_pdf_invoice_to_facturx_invoice(pdf_bytesio) File "/opt/odoo/addons-extra/account_invoice_facturx/models/account_move.py", line 993, in regular_pdf_invoice_to_facturx_invoice generate_from_file( File "/usr/local/lib/python3.11/dist-packages/facturx/facturx.py", line 1334, in generate_from_file xml_check_schematron( File "/usr/local/lib/python3.11/dist-packages/facturx/facturx.py", line 346, in xml_check_schematron raise Exception(full_error) Exception: The Factur-X XML file is not valid against the official schematron. 3 errors found: 1. [BR-S-05]-In an Invoice line (BG-25) where the Invoiced item VAT category code (BT-151) is "Standard rated" the Invoiced item VAT rate (BT-152) shall be greater than zero. Error location: /*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:IncludedSupplyChainTradeLineItem[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:SpecifiedLineTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:ApplicableTradeTax[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1] 2. Attribute @schemeID' marked as not used in the given context. Error location: /*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'] [1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/* :ApplicableHeaderTradeAgreement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/* :SellerTradeParty[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/* :DefinedTradeContact[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/* :EmailURIUniversalCommunication[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/* :URIID[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1] 3. Attribute @schemeID' marked as not used in the given context. Error location: /*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/* :SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/* :ApplicableHeaderTradeAgreement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/* :BuyerTradeParty[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/* :DefinedTradeContact[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/* :EmailURIUniversalCommunication[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/* :URIID[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod:account_invoice_facturx Module account_invoice_facturx series:17.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants