DEV Community

kobima6813
kobima6813

Posted on Originally published at kobima6813.github.io

15,000 e-invoices, 12 generators: 4 setups pass every validator and still bill the wrong amount

Finding first: in our October run, 4 generator configurations pass KoSIT (XRechnung 3.0.2), the CEN EN 16931 rules and Peppol BIS 3.0.20, yet on 472 of 600 invoices each the amount due (BT-115) differs from what the shop order actually charged.

No validator catches this, because no validator knows the original order total. It only checks that the invoice is consistent with itself.

What we ran

  • 15,000 synthetic invoices from 12 open-source generators (25 configurations × 600 orders: 300 B2B + 300 B2C).
  • Validators are pinned by SHA-256: KoSIT 1.6.3, CEN EN 16931 and Peppol BIS 3.0.20.
  • The orders are deterministic, tax-inclusive shop carts with % discounts and shipping, i.e. what a WooCommerce or Odoo shop actually produces.

Full report: https://kobima6813.github.io/einvoice-conformance-matrix/reports/2026-10/

The "passes but wrong" cases

Configuration Validators Amount due ≠ order total
Mustang 2.26.0 (library-computed totals) all pass 472 / 600
speedata/einvoice 0.0.23, CII all pass 472 / 600
speedata/einvoice 0.0.23, UBL all pass 472 / 600
josemmo/einvoicing 0.3.1, 2-decimal rounding all pass 472 / 600

The cause is the same in all four. A gross (tax-inclusive) cart is converted into EN 16931 net arithmetic, and rounding lands in a different place than the shop rounded. The amounts are usually cents, but the invoice no longer matches what the customer paid, and that breaks automatic payment matching. Mustang already merged a fix (PR #1318). It isn't released yet.

Validator failures we reported upstream

  • Odoo 18/19 Factur-X: PEPPOL-EN16931-R120 on 242/300 B2B invoices and BR-CO-10 / BR-S-08 with round_globally. We tested two fix PRs (#297750, #297752) against the corpus, and the errors go to 0. The maintainer confirmed the results.
  • WooCommerce PDF Invoices & Packing Slips 5.16.4: ram:RoundingAmount is out of XSD order, so 238/600 Factur-X invoices are XSD-invalid (#1662). Another user reproduced it independently.
  • horstoeko/zugferd: the VAT breakdown for exempt categories was wrong. It's fixed in v1.0.133.
  • speedata/einvoice: UBL element order and a BT-27/BT-28 swap. Both are fixed in v0.0.23.
  • josemmo/einvoicing 0.3.1: the default config gives 590/600 CEN failures (#93). It passes once a rounding preset is set.
  • ZUGFeRD-csharp 18.0.1: CII without a delivery date is XSD-invalid. This was already known upstream (PR #981), so it's not our report.

France: 30 of 30 pass

France's receiving obligation started on 1 Sep 2026. We added 5 French scenarios: standard 20 % + 5.5 %, the VAT franchise (art. 293 B), domestic reverse charge, an intra-EU supply and all four French rates. Each one ran in 2 Factur-X profiles (EN16931, EXTENDED) on 3 generator versions (drafthorse + factur-x, horstoeko 1.0.132 and 1.0.133). All 30 pass.

Honest caveats:

  • In our first run, horstoeko failed BR-IC-12. That was our driver's mistake: the ship-to address is dropped unless setDocumentShipTo() is called before setDocumentShipToAddress(). It was not a library bug.
  • We have not tested the French platform-specific (PA/PPF) rule sets, because they aren't available as free, pinned validators.

Free self-check

You can check one of your own invoices here: https://kobima6813.github.io/einvoice-conformance-matrix/check.html

It runs in your browser, and the file is not uploaded anywhere. It covers a subset of EN 16931 and tells you how many problems it found. It is not a replacement for KoSIT or a Peppol validator.

Limits

  • The orders are synthetic. Real catalogs have edge cases we don't generate yet.
  • Each configuration is one library version with one setting. Your integration code can add or hide bugs.
  • Corrections are welcome. Every number links to a file in the repo, and you can reproduce it with scripts/run_matrix.sh.

Repo and home page: https://kobima6813.github.io/einvoice-conformance-matrix/

If you maintain one of these generators and think a result is wrong, open an issue and I'll re-run it.

Top comments (0)