Factur-X Builder
Generates the factur-x.xml attachment of a Factur-X invoice in the
EN 16931 profile, from objects that speak the language of invoicing rather than the language of
the CII schema.
The gem produces the XML only. Embedding it into a PDF/A-3 is the job of a separate tool
(facturx-pdfgen, Ghostscript, veraPDF…). This split is deliberate: your Rails app builds the
invoice data, this gem turns it into a compliant XML attachment, and the PDF toolchain stays
independent.
French e-invoicing identifiers are first class — SIRET, SIREN, VAT number and the routing address
(scheme 0225) used by the PPF — and the rules that the French reform adds on top of EN 16931 are
enforced by default.
Installation
gem "factur-x-builder"
Requires Ruby >= 2.7 and Nokogiri ~> 1.15.
Usage
require "factur_x"
seller = FacturX::Party.new(
name: "OFFICE DE TOURISME DES MONTS FICTIFS",
siret: "39876543800005",
siren: "398765438",
vat_number: "FR79398765438",
contact: FacturX::Contact.new(person_name: "Service comptabilité", email: "compta@example.org"),
address: FacturX::Address.new(
line_one: "4 boulevard des Sources",
postcode: "63240",
city: "LE MONT-FICTIF",
country_code: "FR"
)
)
buyer = FacturX::Party.new(
name: "SARL COTEAUX ET COMPAGNIE",
siret: "81234567600009",
siren: "812345676",
vat_number: "FR19812345676",
address: FacturX::Address.new(
line_one: "123 avenue des Alpages",
postcode: "74000",
city: "Annecy",
country_code: "FR"
)
)
invoice = FacturX::Invoice.new(
number: "5-2026-0004",
issued_on: Date.new(2026, 8, 4),
business_process: "S1",
currency: "EUR",
notes: FacturX::Note.french_legal_mentions,
seller: seller,
buyer: buyer,
delivered_on: Date.new(2026, 8, 4),
payment_reference: "5-2026-0004",
payment_means: FacturX::PaymentMeans.new(
iban: "FR54300020001000012345678",
account_name: "OFFICE DE TOURISME DES MONTS FICTIFS",
bic: "AGRIFRPP"
),
payment_terms: FacturX::PaymentTerms.new(description: "Paiement à 30 jours",
due_on: Date.new(2026, 9, 3)),
lines: [
FacturX::Line.new(name: "Partenariat 2026", unit_price: 194.00, quantity: 1,
net_amount: 194.00, vat_rate: 20.0)
],
tax_breakdowns: [
FacturX::TaxBreakdown.new(category: "S", rate: 20.0,
basis_amount: 194.00, calculated_amount: 38.80)
],
totals: FacturX::Totals.new(
line_total: 194.00,
tax_basis_total: 194.00,
tax_total: 38.80,
grand_total: 232.80,
due_payable: 232.80
)
)
File.write("factur-x.xml", FacturX.build(invoice))
include FacturX drops the prefix if you build a lot of invoices in one place —
Invoice.new, Party.new — while the entry point stays FacturX.build.
Totals are yours, coherence is checked
The gem never computes amounts. Your application supplies every total, and the gem verifies they hang together before writing anything — a mismatch raises rather than producing a document the platform would reject:
FacturX.build(invoice)
# FacturX::ValidationError: invoice is not valid:
# - total with VAT (BT-112) is 9999.99 but total without VAT plus VAT add up to 232.80 (BR-CO-15)
To collect every problem instead of raising on the first, use violations:
FacturX.violations(invoice) # => ["...", "..."]
FacturX.valid?(invoice) # => false
One address per party
EN 16931 gives each party exactly one PostalTradeAddress, and it must be the address of the
establishment identified by the SIRET carried in GlobalID. There is no second slot: the element
that would hold a registered office, SpecifiedLegalOrganization/PostalTradeAddress, exists in the
extended XSD but the French CTC schematron forbids it (CII-SR-225), as it forbids
ShipFromTradeParty (CII-SR-166).
When the registered office differs from the invoicing establishment, it belongs in BT-33:
FacturX::Party.new(
name: "OFFICE DE TOURISME DES MONTS FICTIFS - Établissement de Chambéry",
legal_information: "SARL au capital de 50 000 € - RCS Clermont-Ferrand 398 765 438 - " \
"Siège social : 4 boulevard des Sources, 63240 LE MONT-FICTIF",
siret: "39876543800005",
siren: "398765438",
vat_number: "FR79398765438",
address: FacturX::Address.new(line_one: "8 rue des Entrepôts", postcode: "73000",
city: "Chambéry", country_code: "FR")
)
BT-33 is free text — the counterpart of cbc:CompanyLegalForm in UBL. Receivers display it, they do
not parse it.
Profiles
By default the French e-invoicing rules apply on top of EN 16931: an invoicing mode (BT-23) and the three mandatory legal statements are required. For an invoice issued outside that regime:
FacturX.build(invoice, profile: :en16931)
FacturX::Note.french_legal_mentions returns the three statements French law requires (late payment
penalties, the €40 recovery indemnity, and the absence of an early payment discount). Pass your own
wording to any of FacturX::Note.recovery_indemnity, FacturX::Note.late_payment_penalties or
FacturX::Note.no_early_payment_discount to override the default text.
Validation layers
| Layer | What it checks | Cost | When |
|---|---|---|---|
FacturX::Validation::Rules |
EN 16931 + French business rules, in Ruby | sub-millisecond | every build, unless validate: false |
FacturX::Validation::Schema |
official Factur-X 1.0.9 EN 16931 XSD, via Nokogiri | ~1 ms | every build, unless validate_schema: false |
| Schematron | the official rules, via Saxon | ~1.4 s | the test suite only |
The Ruby rules exist so your application gets a fast, readable error in the invoicing domain's own words. The schematron remains the authority: the test suite runs it against every generated document and asserts the Ruby rules agree with it, so the two cannot silently drift apart.
Development
bin/setup
bundle exec rake
rake runs the tests and RuboCop. Conformance tests shell out to Saxon:
brew install saxon
Without it, those tests skip rather than fail. The stylesheets live in test/schematron/ and are
excluded from the packaged gem.
Scope
Supported: invoices and credit notes (380, 381, 389), multiple VAT rates and categories,
exemptions with their reason, document level allowances and charges, prepayments, preceding invoice
references, delivery and billing periods, purchase order / contract / project references, credit
transfers, direct debits and cards.
Not supported: the extended and extended-ctc-fr profiles (the EXT-FR-FE-* fields), UBL syntax,
Order-X, and embedding the XML into a PDF.
License
Available as open source under the terms of the MIT License.