ZUGFeRD/X-Rechnung: create required XML+PDF invoice format for EU compliance for 2025 in Ruby + Ghostscript

26th November 2024 – 1228 words

On 1st Jan 2025 B2B companies will be required to send invoices in a machine-readable format. The name/spec in Germany is called ZUGFeRD (Zentraler User Guide des Forums elektronische Rechnung Deutschland). As not all receiving companies will be able to process a raw XML file, it is best to produce a PDF/A-3 file with embedded XML. This is also known as X-Rechnung in Germany.

To enable the whole process for our B2B company, I did the following steps:

1. Create a XML version of your invoice

The simplest way could be to just make a “invoice.xml.erb”, extract an example xrechnung.xml from ferd-net’s repo and fill in the blanks with your invoice + company’s details.

The way, we did it, by using halfbyte’s secretariat Gem, which provides a cleaner way to pass business objects to the XML generator and also allow validations. Currently, we also forked it and opened an PR to support more options. The Gem is functional, but has no docs, so have a look at the test directory. A fully fledged example from our system:

invoice = Secretariat::Invoice.new(
  id: rechnung.id,
  issue_date: rechnung.billing_date,
  currency_code: 'EUR',
  payment_type: :BANKACCOUNT,
  payment_text: "IBAN: DE11111111111111111111, BIC/SWIFT: DEUTDEDBCHE",
  tax_category: :STANDARDRATE,
  tax_percent: (rechnung.vat_rate * 100).to_s,
  tax_amount: rechnung.vat.to_s, # 19
  basis_amount: rechnung.total.to_s,
  grand_total_amount: rechnung.total_with_vat.to_s,
  paid_amount: '0',
  due_amount: rechnung.total.to_s,

  # extra fields added from our PR: 
  payment_iban: 'DE11111111111111111111',
  payment_terms_text: "Zahlbar innerhalb von 14 Tagen ohne Abzug per SEPA-Überweisung.",
  payment_due_date: 14.days.from_now.to_date,
  service_period_start: rechnung.billing_date,
  service_period_end: rechnung.billing_date + 30,

  seller: Secretariat::TradeParty.new(
    name: 'yourcompany GmbH',
    street1: 'example road. 73b',
    postal_code: '01234',
    city: 'Musterstadt',
    country_id: 'DE',
    vat_id: 'DE111111111',
  ),
  buyer: Secretariat::TradeParty.new(
    name: 'customer GmbH',
    street1: 'example road. 73b',
    postal_code: '01234',
    city: 'Musterstadt',
    country_id: 'DE',
    vat_id: 'DE111111111',
  ),
  line_items: rechnung.line_items.map do |item|
    Secretariat::LineItem.new(
      name: item.position,
      quantity: 1,
      net_amount: item.netto.to_s,
      gross_amount: item.brutto.to_s,
      charge_amount: item.netto.to_s,
      tax_category: :STANDARDRATE,
      tax_percent: (item.vat_rate * 100).to_s,
      tax_amount: item.vat.to_s,
      origin_country_code: 'DE',
      currency_code: 'EUR',
    )
  end
)

xml = invoice.to_xml(version: 2)

Depending on your use-case, you might need to add even more fields, The RAM standard has a lot of fields to describe almost any kind of invoice, such as discounts, payment methods, partial payment etc.

2. Create a PDF/A-3 file with the embedded XML

Ghostscript writes in their blog about the constructing a command. In conclusion, we will need the following extra files accessible from our app:

  • zugferd.ps
  • default_rgb.icc

E.g. just put in in your vendor folder:

mkdir -p vendor/zugferd
wget -O vendor/zugferd/zugferd.ps https://ghostscript.com/~rparada/zugferd.ps
wget -O vendor/zugferd/default_rgb.icc https://github.com/ArtifexSoftware/ghostpdl/raw/refs/heads/master/iccprofiles/default_rgb.icc

Then, wrap Ghostscript in a ruby block:


class ZugferdConversionError < StandardError; end

def convert_to_zugferd(pdf, invoice)
  ps = Rails.root.join("vendor/zugferd/zugferd.ps")
  icc = Rails.root.join("vendor/zugferd/default_rgb.icc")
  
  zugferd_xml = Tempfile.new(["invoice", ".xml"])
  zugferd_xml.write(invoice.to_xml(version: 2))
  zugferd_xml.rewind
  
  output = Tempfile.new(["invoice", ".pdf"])
  output.binmode
  
  cmd = <<~CMD.squish
  gs
    --permit-file-read=#{Rails.root.join('vendor/data/')}
    --permit-file-read=#{File.dirname(pdf.path)}
    -sDEVICE=pdfwrite
    -dPDFA=3
    -sColorConversionStrategy=RGB
    -sZUGFeRDXMLFile=#{zugferd_xml.path}
    -sZUGFeRDProfile=#{icc}
    -sZUGFeRDVersion=2p1
    -sZUGFeRDConformanceLevel=BASIC
    -o #{output.path}
    #{ps}
    #{pdf.path}
  CMD
  stdout, stderr, status = Open3.capture3(cmd)
  if !status.success?
    raise ZugferdConversionError, "error while running #{cmd}\n #{stderr} #{stdout}"
  end
  output
end

If you receive Error: /invalidfileaccess in --file-- .. Last OS error: Permission denied, you can also add the option -dNOSAFER to the command. This will allow Ghostscript to execute ps files. Make sure, that you trust the PS + PDF files. In our case, it was needed on systems were the GS version was stuck at 9.5.

EDIT: if you read errors about producing the PDF/A or get user reports that some software cannot open it, it might be helpful, to convert the PDF to PDF/A first. For this to work, you need to download the PDFA_def.ps file from the Ghostscript repo and add it to the vendor folder - Inside, you need to CUSTOMIZE the path to ICC profile to be relative to your workdir + you can override title, producer etc. there.

wget -O vendor/data/PDFA_def.ps "https://git.ghostscript.com/?p=ghostpdl.git;a=blob_plain;f=lib/PDFA_def.ps"
vim vendor/data/PDFA_def.ps

Adjust the lines like you want:

...
 % Define entries in the document Info dictionary :
  [ /Title (Invoice) /Creator (example GmbH) /Producer (example GmbH) /Author (example GmbH) /Subject (Invoice) /Keywords ()
    /DOCINFO pdfmark

  % Define an ICC profile :
  /ICCProfile (vendor/data/default_rgb.icc) % Customise

Then you can create a PDF/A with this command:

gs \
  --permit-file-read=vendor/data/ \
  --permit-file-read=/tmp \
  -dNOSAFER \
  -sDEVICE=pdfwrite \
  -dPDFA=3 \
  -sColorConversionStrategy=RGB \
  -o output.pdf \
  vendor/data/PDFA_def.ps \
  input.pdf

Then pass the input.pdf to the previous command and generate the ZUGFeRD file.


Alternatively, you can have a look at this Python library.

3. Validating

This website has a list of validators / testing tools:

easyfirm.net/e-rechnungzugferd/validatoren

Some of those require registration or a paid subscription, but the first tool worked good for a brief test of a PDF that was generated with the above code:

erechnungs-validator.winball.de

References: