Project

einvoicing

0.0
The project is in a healthy, maintained state
EN 16931-compliant e-invoicing for Ruby. Generates Factur-X (PDF/A-3 + CII XML), UBL 2.1, and CII D16B. Validates French B2B requirements (SIREN, SIRET, TVA). Rails concern for ActiveRecord models. Targets French September 2026 mandate.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

~> 1.0
~> 1.0
 Project Readme

einvoicing

Gem Version CI

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.

Why

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.

Features

  • 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)
  • Full EN 16931 VAT category set (S, Z, E, AE, K, G, O) with exemption reasons (BT-120 / BT-121)
  • Validate French B2B requirements: SIREN, SIRET (Luhn), EU VAT number formats with the French check key verified, French VAT rates including the DOM and Corsican ones
  • 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.

Installation

# Gemfile
gem "einvoicing"
bundle install

Quick Start

require "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:   "FR27552032534"
)

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)

Validation Errors

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 failure

i18n (French messages)

The 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" }]

Check a generated file online

Drop a Factur-X PDF into our free validator for a report on the PDF/A-3 container, the embedded XML, the official XSD and the EN 16931 business rules. It runs entirely in the browser — the invoice never leaves your machine, and there is no account to create.

Formats

Factur-X (PDF/A-3 + CII)

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.

CII D16B (XML only)

xml = Einvoicing::Formats::CII.generate(invoice)

Produces a rsm:CrossIndustryInvoice document with guideline ID urn:cen.eu:en16931:2017.

UBL 2.1 (Peppol)

ubl = Einvoicing::Formats::UBL.generate(invoice)

Produces a UBL 2.1 Invoice document with Peppol BIS Billing 3.0 customization ID.

Chorus Pro / PPF (France)

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:

  • schemeID for SIRET identifiers must be "SIRET" (not the ISO 6523 code "0002")
  • SpecifiedTradeSettlementPaymentMeans is mandatory — set payment_means_code: on the invoice (30 = credit transfer)
  • Invoice ID (invoice_number) must not exceed 20 characters
  • Use einvoicing-connect for the actual API submission

Rails Integration

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
end

Then 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] }
end

Custom validator

Use 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
end

A custom validator is any module that responds to .validate(invoice) and returns Array<Hash>.

Document-Level Allowances and Charges

A global discount (BG-20) or a global charge such as shipping (BG-21) is not a line item — it applies to the whole document, at a given VAT rate:

invoice = Einvoicing::Invoice.new(
  # ... other fields ...
  lines: lines,
  allowances: [
    Einvoicing::AllowanceCharge.new(
      amount:      BigDecimal("100.00"),   # BT-92 — always positive
      vat_rate:    0.20,                   # BT-96 — the rate it applies to
      reason:      "Remise commerciale",   # BT-97
      reason_code: "95",                   # BT-98 (UNCL5189)
      base_amount: BigDecimal("1000.00"),  # BT-93 (optional)
      percentage:  10                      # BT-94 (optional)
    )
  ],
  charges: [
    Einvoicing::AllowanceCharge.new(amount: BigDecimal("50.00"), vat_rate: 0.20, reason: "Frais de port")
  ]
)

invoice.line_total       # BT-106 — sum of line net amounts       => 1000.00
invoice.allowance_total  # BT-107                                 =>  100.00
invoice.charge_total     # BT-108                                 =>   50.00
invoice.net_total        # BT-109 — lines − allowances + charges  =>  950.00
invoice.tax_total        # BT-110 — VAT on the adjusted base      =>  190.00
invoice.gross_total      # BT-112                                 => 1140.00

Each allowance and charge adjusts the taxable base and the VAT of its own category in the breakdown (EN 16931 BR-CO-10 to BR-CO-13). EN 16931 requires a reason or a reason code on each one (BR-33 / BR-38), and the FR validator enforces it.

VAT Categories and Exemption Reasons

Every EN 16931 VAT category (BT-118) is available on a line, on a document-level allowance or charge, and on a hand-built breakdown entry:

Symbol Code Meaning
:standard S Standard rate
:zero_rated Z Zero rated
:exempt E Exempt from VAT
:reverse_charge AE Reverse charge
:intra_community K Intra-Community supply
:export G Export outside the EU
:not_subject O Outside the scope of VAT

E, AE, K, G and O must state why no VAT is charged (BR-E-10, BR-AE-10, BR-IC-10, BR-G-10, BR-O-10). AE, K, G and O fall back to their VATEX code; E depends on the exemption invoked, so you supply it:

Einvoicing::LineItem.new(
  description: "Prestation de conseil",
  quantity:    1,
  unit_price:  BigDecimal("2500.00"),
  vat_rate:    0,
  category:    :exempt,
  exemption_reason:      "TVA non applicable, art. 293 B du CGI",
  exemption_reason_code: Einvoicing::Tax::VATEX_FR_FRANCHISE
)

A reverse-charge line needs nothing extra — the breakdown carries VATEX-EU-AE / "Reverse charge" on its own:

Einvoicing::LineItem.new(description: "Développement", quantity: 1,
                         unit_price: BigDecimal("10920.00"),
                         vat_rate: 0, category: :reverse_charge)

Category VAT (BT-117) is computed from the category's taxable base (BT-116 × BT-119), not by summing the per-line VAT — the two diverge by a cent every few dozen lines and BR-S-09 is checked to the cent.

VAT Accounting Currency

When you invoice in one currency and account for VAT in another (BT-6 ≠ BT-5), BR-53 requires the VAT total restated in the accounting currency (BT-111). Give the gem the rate and it emits both:

invoice = Einvoicing::Invoice.new(
  # ... other fields ...
  currency:          "USD",
  tax_currency:      "EUR",
  tax_exchange_rate: BigDecimal("0.92")
)

invoice.tax_total                  # => 1840.00 (USD)
invoice.tax_total_in_tax_currency  # => 1692.80 (EUR)

The FR validator flags a declared tax_currency with no tax_exchange_rate, since the document could not satisfy BR-53.

Payment Means

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

Requirements

  • 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

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Write tests first (bundle exec rspec)
  4. Submit a pull request

License

MIT — see LICENSE.