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