en16931-cli 0.6.0

The EN 16931 validator as a command. Validates UBL, CII and ZUGFeRD/Factur-X PDFs against the core model, XRechnung and Peppol BIS Billing 3.0; converts between the two syntaxes; compares two documents as invoices rather than as XML; reports as text, JSON or SVRL.
en16931-cli-0.6.0 is not a library.

πŸ‡ͺπŸ‡Ί en16931-cli

The EN 16931 validator as a command. UBL, CII and ZUGFeRD / Factur-X in, a verdict out β€” text for a person, JSON for a pipeline, SVRL for every other Schematron tool in this field. And, because the model is the thing being checked, two documents can be compared as invoices rather than as XML.

$ en16931 validate rechnung.xml
rechnung.xml β€” UBL 2.1
XRechnung 3.0 validation (EN 16931-1:2017+A1:2019) β€” 282 rule(s) checked, 2 finding(s), INVALID
  [BR-DE-15] BT-10 β€” Buyer reference (BT-10) shall be present
  [BR-CL-23] BG-25[0]/BT-130 β€” Unit code MUST be coded according to the UN/ECE Recommendation 20 with Rec 21 extension. [hint: did you mean "KWH"? Code lists are case-sensitive]
  (implementation of the EN 16931-1 semantic data model; Β© CEN, used under the 2018 CEN–EC licence agreement)
$ echo $?
1

Findings point at a business term, never at an XPath, because the rules run against the semantic model rather than the serialised document. BG-25[0]/BT-130 is a field you can find; /ubl:Invoice/cac:InvoiceLine[1]/cbc:InvoicedQuantity/@unitCode is a thing you have to decode first.

cargo install en16931-cli      # the binary is `en16931`

What it is for

Every other validator in this space is a JVM and a Schematron engine: KoSIT's validator, Mustangproject, phive. They are good and they are 200 MB of runtime in a container that exists to answer one yes/no question in CI. This is a single static binary that answers the same question, from the same artefacts, and tells you which business term is wrong rather than which XPath.

It is also the fastest way to find out what a file someone sent you actually is.

Exit codes, because this belongs in a pipeline

0 every document passed
1 a document was read and is invalid
2 a document could not be read at all, or the command was misused

Telling 1 from 2 is the point. A CI job that treats "this invoice is invalid" and "that path does not exist" the same way will eventually ship an invoice because a volume was not mounted.

en16931 validate out/*.xml --quiet || exit 1

Hostile input lands on 2, never on a crash. A document nested a few hundred elements deep is refused before parsing: the XML parser recurses per level, and a stack overflow is not something Rust can catch, so it would abort the process with no report at all. Entity expansion and XXE need a DTD, and the parser rejects any document carrying one.

The commands

validate

en16931 validate INVOICE...
    [-p|--profile auto|<name>|<BT-24>] # default: whatever the document declares
    [--format text|json|svrl]
    [--strict]                        # warnings and information count as failures
    [--without RULE]                  # skip a rule, loudly. Repeatable.
    [--quiet]

--profile auto is the default and it reads BT-24. Β§7.6 puts the specification identifier in the document precisely so a receiver can apply the rules the sender generated under; validating an XRechnung against the bare core model is the most common way to ship a document a counterparty then rejects.

Name a profile to ask a different question β€” "would this pass in Germany?" β€” about a document that does not claim to. A profile answers to three spellings, and en16931 profiles prints all of them:

xrechnung the slug β€” lower case, no spaces, what you type (-p xrechnung)
"XRechnung 3.0" the display name, as a report prints it
urn:cen.eu:en16931:2017#compliant#… BT-24 verbatim, which a script has and a person does not
$ en16931 profiles
PROFILE              NAME                     CHECKS  CIUS?  BT-24
en16931              EN 16931                    227  n/a    urn:cen.eu:en16931:2017
xrechnung            XRechnung 3.0               282  no     urn:cen.eu:en16931:2017#compliant#…
xrechnung-cvd        XRechnung 3.0 CVD           290  no     …#compliant#…:xrechnung:cvd_0.9
xrechnung-extension  XRechnung 3.0 Extension     296  no     …#conformant#…:extension:xrechnung_3.0
peppol               Peppol BIS Billing 3.0      273  yes    urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:…

--strict is off by default, and that is deliberate. KoSIT reports BR-CL-23 at warning on purpose, because CEN's unit-code table lags UN/ECE's; a build that fails on it fails on invoices Germany accepts. The same document, under two rule sets, is the clearest way to see it:

$ en16931 validate rechnung.xml                        # BT-24 says XRechnung
XRechnung 3.0 validation (…) β€” 282 rule(s) checked, 1 finding(s), valid
  [BR-CL-23] BG-25[0]/BT-130 β€” Unit code MUST be coded according to …
$ echo $?
0

$ en16931 validate rechnung.xml --profile en16931      # the core model
EN 16931 validation (…) β€” 227 rule(s) checked, 1 finding(s), INVALID
  [BR-CL-23] BG-25[0]/BT-130 β€” Unit code MUST be coded according to …
$ echo $?
1

That is not a bug in either answer β€” it is what the two authorities publish, and it is why XRechnung is not a conformant CIUS under Β§4.4.2. The en16931 README has the evidence.

--without is recorded on the report and printed in every output format. A deviation you cannot see in the artefact is worse than one you argued for.

convert

en16931 convert INVOICE --to ubl|cii [-p <profile>] [-o OUT]

The conversion goes through the semantic model, so what comes out is what EN 16931 says the document means β€” not a transliteration of its elements. That is why a UBL invoice whose BT-21 is embedded in the note text as #AAI#… comes out of the CII side with its own element.

With --profile nothing is written unless the model passes, and BT-24 is stamped from the profile that was actually run. Anything the target syntax cannot carry β€” BT-11 on a credit note is the known case β€” goes to stderr, so a redirected stdout is the document and nothing else and the loss is still visible.

diff

en16931 diff LEFT RIGHT [--format text|json]

Compare two documents as invoices, not as XML. Both sides are read into the semantic model first, so a UBL invoice and its CII translation come out identical where a textual diff shares almost nothing β€” different root element, different namespaces, a different name for every field.

$ en16931 convert rechnung.xml --to cii -o rechnung.cii.xml
$ en16931 diff rechnung.xml rechnung.cii.xml
--- rechnung.xml β€” UBL 2.1
+++ rechnung.cii.xml β€” UN/CEFACT CII D16B

identical as invoices
$ echo $?
0

That is the question worth asking of a conversion, a migration, or a counterparty who says they received something else. And when they did:

$ en16931 diff ours.xml theirs.xml
--- ours.xml β€” UBL 2.1
+++ theirs.xml β€” UBL 2.1

2 difference(s)
  ~ seller.address.city    "Musterstadt" β†’ "Nirgendwo"
  - lines[2].note          "Rabatt" β†’ null
$ echo $?
1

0 identical, 1 they differ, 2 one could not be read β€” the same split validate makes.

Two details that are not obvious. The path is a path through the model (lines[1].vat.rate), not a business-term path: a hand-written walk over 164 terms would spell it BG-25[2]/BT-152 and would be a second model to keep in step with the first, silently missing whichever field was added without a matching arm. And a scale difference is not a difference β€” UBL writes BT-152 as 25.0 where CII writes 25, and the model holds those to be one value, so the diff does too.

extract

en16931 extract invoice.pdf [-o payload.xml]

The embedded payload of a ZUGFeRD / Factur-X PDF, verbatim. Whoever diagnoses a rejected invoice needs the bytes the counterparty sent, not a reconstruction of them. Disagreements between the PDF's XMP metadata and the payload are reported on stderr.

inspect

en16931 inspect INVOICE... [--format text|json]

What the file is, without a verdict on it: syntax, declared BT-24, which rule set that resolves to here, the parties, the totals, and anything the reader could not map. The first command to run on a document you were sent.

$ en16931 inspect invoice.pdf
invoice.pdf
  syntax        ZUGFeRD / Factur-X (PDF/A-3 + CII)
  ZUGFeRD       Minimum  (No("MINIMUM carries no invoice lines"))
  kind          Invoice
  BT-24 profile urn:factur-x.eu:1p0:minimum
  rule set      EN 16931 (BT-24 unknown here)
  …

explain, rules and profiles

en16931 explain BR-CO-14        # or br-co-14, or BR-CO-3, or BR-IG-1
en16931 profiles
en16931 rules [-p <profile>] [--term BT-117] [--format text|json]

explain resolves rule ids in every spelling the standard and the artefacts use β€” zero-padding, case, and the BR-IG-* / BR-IP-* families the artefacts call BR-AF-* / BR-AG-*. It answers for profile restrictions too, which are data rather than predicates and still appear in reports under their own ids.

It also says which profiles run the check and at what severity, which is the answer to "why did my validator not object to this?" β€” and where an id names two rules, it says so rather than picking one:

$ en16931 explain PEPPOL-EN16931-R120
PEPPOL-EN16931-R120  [varies by profile]  the artefacts only β€” an authority's addition
  …
  run by: XRechnung 3.0 (warning), … , Peppol BIS Billing 3.0 (fatal)

Same id, same text, different consequence: KoSIT's build rewrites the flag when it merges Peppol's rule into XRechnung's Schematron.

profiles names each profile twice β€” the slug --profile takes and the display name a report prints β€” because the two differ and only one of them is typeable. It also prints the authority releases this build was verified against β€” CEN's, KoSIT's Schematron, KoSIT's validator configuration and OpenPeppol's, each at a release tag. The same list travels in every report, per profile, because BR-DE-15 is KoSIT's rule on KoSIT's cadence and stamping validation-1.3.16 beside it would name the wrong authority.

rules prints the whole catalogue, derived from the registry β€” so it cannot drift from what the validator actually runs, the way a hand-maintained table of 317 rules would on the first release nobody remembered to update. It is the thing to diff across versions:

en16931 rules --format json > new.json && diff old.json new.json

rules --profile <name> lists every check that profile declares, rules and Β§7.3.2 restrictions alike, and its total is the same number profiles prints in its CHECKS column and a report prints as rule(s) checked β€” restrictions included, because the twelve BR-DE-* narrowings are the ones every German counterparty quotes. A restriction is marked profile in the SOURCE column and carries "restriction": "mandatory" | "not-used" | "code-values" in the JSON; its wording is this crate's rendering of a narrowing, not an authority's sentence, and the catalogue does not pretend otherwise.

…and the shortest way to see the severity question the libraries spend a page on:

$ en16931 rules --profile en16931    | grep BR-CL-23
BR-CL-23    fatal      artefact    Unit code MUST be coded according to …
$ en16931 rules --profile xrechnung  | grep BR-CL-23
BR-CL-23    warning    artefact    Unit code MUST be coded according to …

generate β€” completions and a man page

en16931 generate bash > /usr/share/bash-completion/completions/en16931
en16931 generate zsh  > ~/.zfunc/_en16931
en16931 generate man  > /usr/share/man/man1/en16931.1

bash, zsh, fish, powershell, elvish and man, all generated from the same argument definitions the binary parses β€” so a flag cannot exist without its completion.

Nothing is read silently

Both readers report what they could not map and what they could not represent, and those lists are printed under every document. The difference between "this document validated" and "the parts of it I understood validated" is the whole reason they exist.

What it does not do

No network. No Peppol access point, no VIES lookup, no SMP resolution β€” the libraries have no I/O and neither does this. No PDF writing: ZUGFeRD is PDF/A-3 and getting that wrong produces a file that opens fine and fails at the counterparty months later. en16931-formats explains where that stops and why.

Licence

MIT OR Apache-2.0. The rules and code lists are derived from CEN's validation artefacts; see the attribution notice the command prints under every report.