Documentation & overview → sxnlabs.com/gems/einvoicing
EN 16931 electronic invoicing for Ruby. Generate Factur-X (PDF/A-3 + CII XML), UBL 2.1, and CII D16B invoices. Validate French B2B compliance (SIREN, SIRET, TVA). Rails-ready.
France mandates structured e-invoicing for all B2B transactions starting September 2026 (Ordonnance n° 2021-1190). Every invoice between French VAT-registered companies must be issued in a structured format (Factur-X, UBL, or CII) and transmitted via the PPF or a certified PDP.
This gem gives you a clean Ruby API to build compliant invoices, validate them against French rules, and produce all required output formats — without pulling in a heavy XML library.
- Generate Factur-X invoices (PDF/A-3b with embedded CII D16B XML)
- Generate UBL 2.1 XML (Peppol BIS Billing 3.0)
- Generate CII D16B XML (EN 16931 / ZUGFeRD)
- Validate French B2B requirements: SIREN, SIRET (Luhn), TVA format, standard VAT rates
- Structured error reporting:
{ field:, error:, message: }with i18n support (EN + FR) - Payment means: IBAN, BIC/SWIFT, UNCL4461 type codes
- Rails concern (
Einvoicing::Invoiceable) for ActiveRecord models - Only one runtime dependency:
hexapdf(for PDF/A-3 embedding). XML generation uses Ruby stdlib.
# Gemfile
gem "einvoicing"bundle installrequire "einvoicing"
require "date"
seller = Einvoicing::Party.new(
name: "SXN Labs",
street: "5 Lot Coat an Lem",
city: "Plouezoc'h",
postal_code: "29252",
country_code: "FR",
siren: "898208145",
siret: "89820814500018",
vat_number: "FR46898208145",
email: "contact@sxnlabs.com"
)
buyer = Einvoicing::Party.new(
name: "Gecobat",
street: "12 Construction Street",
city: "Paris",
postal_code: "75001",
country_code: "FR",
siren: "552032534",
vat_number: "FR83552032534"
)
lines = [
Einvoicing::LineItem.new(
description: "Backend development — REST API (fixed fee)",
quantity: 1,
unit_price: BigDecimal("2500.00"),
vat_rate: 0.20
),
Einvoicing::LineItem.new(
description: "Factur-X integration",
quantity: 5,
unit_price: BigDecimal("350.00"),
vat_rate: 0.20
)
]
invoice = Einvoicing::Invoice.new(
invoice_number: "FAC-2024-0042",
issue_date: Date.new(2024, 3, 15),
due_date: Date.new(2024, 4, 15),
seller: seller,
buyer: buyer,
lines: lines,
payment_reference: "FAC-2024-0042",
note: "Net 30",
payment_means_code: 30,
iban: "FR7630006000011234567890189",
bic: "BNPAFRPP"
)
# Totals are computed automatically (BigDecimal, no rounding errors)
invoice.net_total # => 0.4000e4 (4000.00)
invoice.tax_total # => 0.800e3 (800.00)
invoice.gross_total # => 0.4800e4 (4800.00)
# Validate for French compliance
errors = Einvoicing::Validators::FR.validate(invoice)
errors.empty? # => true
# Generate CII D16B XML (Factur-X / ZUGFeRD)
xml = Einvoicing::Formats::CII.generate(invoice)
File.write("invoice.xml", xml)
# Generate UBL 2.1 XML (Peppol)
ubl = Einvoicing::Formats::UBL.generate(invoice)
File.write("invoice_ubl.xml", ubl)
# Embed CII XML into an existing PDF → Factur-X PDF/A-3
pdf_data = File.binread("invoice.pdf")
facturx = Einvoicing::Formats::FacturX.embed(pdf_data, xml)
File.binwrite("invoice_facturx.pdf", facturx)Errors are returned as an array of hashes — no exceptions, no monkey-patching:
errors = Einvoicing::Validators::FR.validate(invoice)
# => [
# { field: :seller_siren, error: :siren_invalid, message: "SIREN is invalid" },
# { field: :invoice_number, error: :number_invalid, message: "Invoice number format is invalid" }
# ]
# Raise instead of returning
Einvoicing::Validators::FR.validate!(invoice)
# => raises Einvoicing::Validators::ValidationError on failureThe gem integrates with Rails i18n automatically. For standalone Ruby, set the locale before validating:
require "i18n"
I18n.load_path += Dir[File.join(__dir__, "config/locales/*.yml")]
I18n.locale = :fr
errors = Einvoicing::Validators::FR.validate(invoice)
# => [{ field: :seller_siren, error: :siren_invalid, message: "Le numéro SIREN est invalide" }]The standard French hybrid format: a valid PDF that also carries machine-readable XML inside.
xml = Einvoicing::Formats::CII.generate(invoice)
pdf_data = File.binread("invoice.pdf")
facturx = Einvoicing::Formats::FacturX.embed(pdf_data, xml)
File.binwrite("invoice_facturx.pdf", facturx)The result is PDF/A-3b conformant with an embedded factur-x.xml file tagged as AFRelationship: Data.
xml = Einvoicing::Formats::CII.generate(invoice)Produces a rsm:CrossIndustryInvoice document with guideline ID urn:cen.eu:en16931:2017.
ubl = Einvoicing::Formats::UBL.generate(invoice)Produces a UBL 2.1 Invoice document with Peppol BIS Billing 3.0 customization ID.
When submitting to Chorus Pro (French B2G portal), generate CII XML with the :chorus_pro profile:
xml = Einvoicing.xml(invoice, format: :cii, profile: :chorus_pro)Chorus Pro-specific requirements:
schemeIDfor SIRET identifiers must be"SIRET"(not the ISO 6523 code"0002")SpecifiedTradeSettlementPaymentMeansis mandatory — setpayment_means_code:on the invoice (30 = credit transfer)- Invoice ID (
invoice_number) must not exceed 20 characters - Use
einvoicing-connectfor the actual API submission
Include Einvoicing::Invoiceable in your ActiveRecord model and implement three methods:
class Invoice < ApplicationRecord
include Einvoicing::Invoiceable
def einvoicing_seller
Einvoicing::Party.new(
name: company.name,
siren: company.siren,
vat_number: company.vat_number,
street: company.address_street,
city: company.address_city,
postal_code: company.address_postal_code
)
end
def einvoicing_buyer
Einvoicing::Party.new(name: client.name, siren: client.siren)
end
def einvoicing_lines
line_items.map do |li|
Einvoicing::LineItem.new(
description: li.description,
quantity: li.quantity,
unit_price: li.unit_price_excl_tax,
vat_rate: li.vat_rate
)
end
end
endThen in a controller or service:
invoice = Invoice.find(42)
if invoice.einvoicing_valid?
cii_xml = invoice.to_cii_xml
ubl_xml = invoice.to_ubl_xml
pdf_data = invoice.pdf_attachment.download
facturx_pdf = invoice.to_facturx(pdf_data)
else
puts invoice.einvoicing_errors.map { |e| e[:message] }
endUse a different validator (e.g. for a non-French context):
class Invoice < ApplicationRecord
include Einvoicing::Invoiceable
self.einvoicing_validator = Einvoicing::Validators::FR # default; swap for your own
endA custom validator is any module that responds to .validate(invoice) and returns Array<Hash>.
Add IBAN, BIC, and UNCL4461 payment type code to the invoice. Both CII and UBL generators emit the appropriate elements automatically.
invoice = Einvoicing::Invoice.new(
# ... other fields ...
payment_means_code: 30, # UNCL4461: 30 = credit transfer
iban: "FR7630006000011234567890189",
bic: "BNPAFRPP"
)Common payment_means_code values (UNCL4461):
| Code | Meaning |
|---|---|
| 30 | Credit transfer |
| 42 | Payment to bank account |
| 58 | SEPA credit transfer |
- Ruby >= 3.2 (uses
Data.define) - hexapdf ~> 1.0 (runtime, for Factur-X PDF/A-3 embedding)
- Java (optional) — for local validation with the Mustang CLI validator
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Write tests first (
bundle exec rspec) - Submit a pull request
MIT — see LICENSE.