Writing Zugferd/Factur-X invoices with Latex

I was using Pages to create invoices for the last decade, but with the necessity to embed XML in the invoice I was looking for another solution.
You could keep writing your invoices with a text editor and then combine the pdf with the Zugferd XML in a separate step, but as a developer I saw the high probability to mess something up having two sources of truth. Especially because the embedded XML counts, not what you are actually seeing in the PDF!
My Latex crash course
I had used some Latex templates on Overleaf in the past but otherwise I am a noobie. So just as a disclaimer - there are probably better ways to do it but this is what worked for me and I mostly write it to remember it later 😎
First the required zugferd package sadly is not on Overleaf (yet), but it is probably better to do it locally anyways. So what I did was install Basic Tex for macOS, which is a small distribution of Latex that does not include all the packages (100MB vs 3GB). It turned out I only had to install some packages to get the invoice to build in the end. The package manager is called tlmgr
, for some reason you need to run it as sudo. So after installation I ran
sudo tlmgr install tabto-ltx
sudo tlmgr install ltablex
sudo tlmgr install xltabular
sudo tlmgr install tagpdf
sudo tlmgr install zugferd
sudo tlmgr install babel
I am using TextMate2 (yes) for text editing and with the LaTex bundle you can easily compile your .tex
files and set the compiler to LuaTex for easier font handling. I just had to add my homebrew path to the TextMate PATH like PATH = $PATH:/opt/homebrew/bin
.
In the mentioned zugferd package there is an example invoice which is a big help to get started.
What I tried to achieve is put all the recurring data in variables, and there is some weirdness going on with white space in variables, it is somehow always stripped. So that is why I put \space
everywhere, because when using backslash for a space there was a backslash ending up in the XML.
The file structure is as follows:
|
|- images/
|
|- logo-bw.eps
|
|- invoice.tex
|- kris-invoice.sty
Note that your logo needs to be an EPS, SVG is not supported. But that should be no problem to convert using Inkscape, Affinity Designer etc.
The .sty
file is a package and is used for all the “logic” of your document. It is adapted/copied from the sample as the author of the package suggests.
The package file kris-invoice.sty
looks scary, but I put everything that eventually needs editing in the START VARIABLES
block. That is basically your (seller) information. The rest you only need to touch if you want to change something with the layout or the logic. I only added the variables and configured the styling of the letter document class in use scrlttr2
, the rest is from the example implementation.
kris-invoice.sty:
\NeedsTeXFormat{LaTeX2e}
\ProvidesExplPackage{kris-invoice}{2025-01-02}{0.9d}{Invoices for krisdigital}
\keys_define:nn {zugferd/invoice}{
default-vat .tl_set:N = \defaultVAT,
default-vat .initial:n = 19,
format .code:n = \PassOptionsToPackage{format=#1}{zugferd},
format .initial:n = xrechnung3.0,
}
\ProcessKeyOptions[zugferd/invoice]
\RequirePackage{ragged2e}
\RequirePackage{zugferd}
\RequirePackage[ngerman]{babel}
\usepackage{graphicx}
\graphicspath{{images/}}
\usepackage[margin=2cm]{geometry}
\usepackage{tabto}
\NumTabs{8}
% e.g. use comma as output decimal marker if german
\addto\extrasgerman{\sisetup{locale=DE}}
\RequirePackage{xltabular}
\RequirePackage{booktabs}
% START VARIABLES
\newcommand{\fromfirstname}{Firstname}
\newcommand{\fromlastname}{Lastname}
\newcommand{\fromfullname}{\fromfirstname\space\fromlastname}
\newcommand{\frombank}{Yourbank}
\newcommand{\fromvatid}{DE111111111}
\newcommand{\fromtaxid}{42/000/000000}
\newcommand{\frombic}{XXXXXXXXXXX}
\newcommand{\fromiban}{DE00~0000~0000~0000~0000~00}
\newcommand{\fromstreet}{Yourstreet}
\newcommand{\fromstreetno}{42}
\newcommand{\fromstreetandno}{\fromstreet\space\fromstreetno}
\newcommand{\frompostalcode}{20000}
\newcommand{\fromcity}{Hamburg}
\newcommand{\fromaddress}{\fromstreet\space\fromstreetno,\space\frompostalcode\space\fromcity}
\newcommand{\fromphone}{+49~000~000~00~00}
\newcommand{\fromurl}{www.myspace.com}
\newcommand{\fromemail}{info@myspace.com}
\newcommand{\fromname}{yourcompany}
\newcommand{\paymentdays}{14}
% END VARIABLES
\KOMAoptions{fromlogo=true,fromrule=afteraddress,fromphone,fromemail,fromlogo,fromalign=right,enlargefirstpage=false,parskip=full,numericaldate=true, addrfield=topaligned}
\setkomavar{fromlogo}{\includegraphics[width=1cm]{logo-bw}}
\setkomavar{fromname}{\fromname}
\setkomavar{fromemail}{\fromemail}
\setkomavar{fromurl}{\fromurl}
\setkomavar{fromphone}{\fromphone}
\setkomavar{signature}{\fromfullname}
\setkomavar{fromaddress}{\fromstreet\space\fromstreetno \\ \frompostalcode\space\fromcity}
\newkomavar[IBAN:]{fromiban}
\setkomavar{fromiban}{\fromiban}
\newkomavar[BIC:]{frombic}
\setkomavar{frombic}{\frombic}
\newkomavar[Steuernummer:]{fromtaxid}
\setkomavar{fromtaxid}{\fromtaxid}
\newkomavar[Umsatzsteuer-Identifikationsnummer:]{fromvatid}
\setkomavar{fromvatid}{\fromvatid}
\newkomavar[Name:]{fromaccount}
\setkomavar{fromaccount}{\fromfullname}
\setkomavar{frombank}{\frombank}
\setkomavar{title}{Rechnung}
\setkomafont{fromaddress}{\small}
\setkomafont{addressee}{\small}
\setkomafont{title}{\normalfont\large}
\setkomavar{specialmail}{\rule{0pt}{2mm}}
\@addtoplength{refvpos}{-15mm}
\setkomavar{firsthead}{%
\usekomafont{fromaddress}
\begin{tabular}[c]{@{}l}
\usekomavar{fromlogo}
\end{tabular}%
\hfill
\begin{tabular}[c]{r@{}}
\usekomavar{fromname}\\
\usekomavar{fromaddress}\\
\usekomavar{fromemail}\\
\usekomavar{fromphone}
\end{tabular}%
}
\newcounter{invoiceitem}
\seq_new:N \g__ptxcd_VAT_rates_seq
% InitVAT accepts 2 Arguments
% Percentage + Tax Type Code the latter one is set to S as a default
\NewDocumentCommand{\InitVAT}{mO{S}}{
\seq_gput_right:Nn \g__ptxcd_VAT_rates_seq {#1}
\fp_new:c {g__ptxcd_invoice_sum_vat#1_fp}
\fp_new:c {g__ptxcd_invoice_base_vat#1_fp}
\cs_new:cn {__ptxcd_invoice_type_code#1:} {#2}
}
%Initialize VAT rates for (5),7,(16) and 19 % VAT
%\InitVAT{16}
\InitVAT{19}
%\InitVAT{5}
%\InitVAT{7}
% Tax initialisation with a different Code than S in this example Syntax would be
% \InitVAT{0}[AE]
\newcommand*{\SetDefaultVAT}[1]{\def\defaultVAT{#1}}
\seq_new:N \l__ptxcd_invoice_items_seq
% Auxiliary macro to allow setting the Invoice items at a different position as they are printed later
\NewDocumentCommand{\AddInvoiceItem}{D<>{}O{\defaultVAT}mmm}{
\seq_put_right:Nn \l__ptxcd_invoice_items_seq {
{#2}{#3}{#4}{#5}{#1}
}
}
\newcolumntype{P}{r<{\PrintTableCurrency}}
\fp_new:N \g__ptxcd_invoice_sum_fp
\fp_new:N \g__ptxcd_invoice_total_fp
\fp_new:N \g__ptxcd_tax_total_fp
\fp_new:N \g__ptxcd_invoice_item_fp
\fp_new:N \g__ptxcd_invoice_item_vat_fp
\fp_new:N \g__ptxcd_invoice_sum_vat_fp
\newcommand*{\PrintInvoiceTabular}{
\bool_gset_true:N \g_ptxcd_first_run_bool
\begin{ZUGFeRD}
\sisetup{round-precision=2,round-mode=places,round-pad=false,table-number-alignment=right,minimum-decimal-digits=2,mode=text}
\begin{xltabular}{\linewidth}{@{}rS[round-precision=1,table-format=2.1]>{\RaggedRight}XPP@{}}
\toprule[\lightrulewidth]
\noalign{\global\let\PrintTableCurrency\relax}%
\small{Pos.}&\small{Std.}&\small{Beschreibung}&\small{Einzelpreis}&\small{Gesamtpreis}\\\midrule[\heavyrulewidth]
\noalign{\global\let\PrintTableCurrency\TableCurrency}%
\endhead
\bottomrule[\lightrulewidth]\multicolumn{5}{@{}p{\textwidth}@{}}{\strut\hspace*{\fill}\footnotesize Fortsetzung auf der nächsten Seite}\endfoot
\bottomrule\endlastfoot
% Only write xml for the first run of the tabular.
\fp_compare:nNnF {\g__ptxcd_invoice_sum_fp} = {\c_zero_dim} {
\fp_gzero:N \g__ptxcd_invoice_sum_fp
\zugferd_disable_XML_interfaces:
}
\seq_map_inline:Nn \g__ptxcd_VAT_rates_seq {
\fp_gzero:c {g__ptxcd_invoice_sum_vat##1_fp}
\fp_gzero:c {g__ptxcd_invoice_base_vat##1_fp}
}
\fp_gzero:N \g__ptxcd_invoice_sum_fp
\seq_map_inline:Nn \l__ptxcd_invoice_items_seq {
\PrintInvoiceItem##1
}
\tabularnewline
\noalign{\skip_vertical:n {-\ht\strutbox-\dp\strutbox}}%offset for extra empty row of mapping
\midrule[\heavyrulewidth]
\PrintInvoiceTotal
\end{xltabular}
\end{ZUGFeRD}
}
\newcommand*{\PrintInvoiceTotal}{
\zugferd_startInvoiceSums:
\fp_gset:Nn \g__ptxcd_invoice_total_fp { \g__ptxcd_invoice_sum_fp}
\fp_gzero:N \g__ptxcd_tax_total_fp
\PrintInvoiceSum{netto}{\fp_use:N \g__ptxcd_invoice_sum_fp}
\seq_map_inline:Nn \g__ptxcd_VAT_rates_seq {
\fp_compare:nNnF {\fp_use:c {g__ptxcd_invoice_sum_vat##1_fp}} = {0} {
\zugferd_write_TaxEntry:ennn {\use:c {__ptxcd_invoice_type_code##1:}} {##1} {\fp_use:c {g__ptxcd_invoice_base_vat##1_fp}} {\fp_use:c {g__ptxcd_invoice_sum_vat##1_fp}}
\fp_gadd:Nn \g__ptxcd_tax_total_fp {\fp_use:c {g__ptxcd_invoice_sum_vat##1_fp}}
\PrintVatSum[{\fp_use:c {g__ptxcd_invoice_base_vat##1_fp}}]{##1 }{\fp_use:c {g__ptxcd_invoice_sum_vat##1_fp}}
}
}
\PrintInvoiceSum{brutto}{\fp_eval:n {\g__ptxcd_tax_total_fp + \g__ptxcd_invoice_total_fp }}
% TODO add support for allowance, chargeTotal, and prepaid
\zugferd_write_Summation:nnnnnnnn
{\fp_use:N \g__ptxcd_invoice_sum_fp}% LineTotalAmount
{0} %ChargeTotalAmount
{0} %AllowanceTotalAmount
{\fp_use:N \g__ptxcd_invoice_sum_fp} %TaxBasisTotalAmount
{\fp_use:N \g__ptxcd_tax_total_fp} %TaxTotalAmount
{\fp_eval:n {\g__ptxcd_tax_total_fp + \g__ptxcd_invoice_total_fp }} %GrandTotalAmount
{0} % TotalPrepaidAmount
{\fp_eval:n {\g__ptxcd_tax_total_fp + \g__ptxcd_invoice_total_fp }} %DuePayableAmount = GrandTotalAmount - TotalPrepaidAmount
\zugferd_stopInvoiceSums:
}
%Ausgabe der einzelnen Rechnungspositionen
\newcommand*{\PrintInvoiceItem}[5]{%
\stepcounter{invoiceitem}%
\theinvoiceitem%Positionsnummer
\zugferd_fp_gset_rounded:Nn \g__ptxcd_invoice_item_vat_fp {#2 * (#1/100) * #4}
\zugferd_fp_gset_rounded:Nn \g__ptxcd_invoice_item_fp {#2 * #4}
\fp_gadd:cn {g__ptxcd_invoice_base_vat#1_fp} {\g__ptxcd_invoice_item_fp}
\fp_gadd:cn {g__ptxcd_invoice_sum_vat#1_fp} {\g__ptxcd_invoice_item_vat_fp}
\fp_gadd:Nn \g__ptxcd_invoice_sum_fp {\g__ptxcd_invoice_item_fp}
% optionen position nummer name einzel-preis anzahl gesamtpreis
\zugferd_write_Item:ennnnnn {tax/rate=#1, tax/category=\use:c {__ptxcd_invoice_type_code#1:},#5} {\theinvoiceitem} {} {#3} {#4} {#2} {\fp_use:N \g__ptxcd_invoice_item_fp}
% Anzahl
\space(\printVAT{#1}~MwSt.)% Beschreibung mit Angabe der MwSt, in Klammern
&\num{#4}%\num[round-mode=places,output-decimal-marker={,},round-pad = false]{#4}\tl_show:n {#4}%Einzelpreis
&\exp_args:Nx \num{\fp_use:N \g__ptxcd_invoice_item_fp}
\tabularnewline
}
\newcommand*{\PrintInvoiceSum}[2]{
\PrintSum{\csname invoicesum#1name\endcsname}{#2}
}
\newcommand*{\PrintVatSum}[3][]{
\PrintSum{\invoicesumvatname[#1]{#2}}{#3}
}
\newcommand*{\invoicesumvatname}[2][]{MwSt.~\printVAT{#2}\tl_if_empty:nF {#1} {\space(\num[round-precision=2]{#1}\TableCurrency)}}
\renewcommand*{\theinvoiceitem}{\int_compare:nNnT {\value{invoiceitem}}<{10}{0}\arabic{invoiceitem}}
\newcommand*{\PrintSum}[2]{
&&\multicolumn{1}{r}{#1\invoicesumseparator}&\multicolumn{1}{l}{}&\exp_args:Nx \num {#2}\tabularnewline
}
\ExplSyntaxOff
\newcommand*{\invoicesumnettoname}{Summe (Netto)}
\newcommand*{\invoicesumbruttoname}{Summe (Brutto)}
\newcommand*{\invoicesumseparator}{:\space}
\newcommand*{\TableCurrency}{\,€}
\newcommand*{\printVAT}[1]{\num[round-mode=none]{#1}\,\%}
\newcommand*{\PrintPositionenVAT}[5]{%
\stepcounter{invoiceitem}%
\theinvoiceitem\space(\printVAT{#3})\tabularnewline
}
\endinput
The .tex
file invoice.tex
is where you adapt the invoice for each client.
invoice.tex:
\DocumentMetadata{
pdfstandard=a-3b,
lang=de,
}
\documentclass[DIN,version=last]{scrlttr2}
\usepackage[
format=xrechnung3.0
]{kris-invoice}
\setkomavar{invoice}{20250804}
% \setkomavar{date}{05.05.2025}
\newkomafont{invoicetotal}{\bfseries}
\SetZUGFeRDData*{
% document-type = commercial-invoice, % commented as this setting matches the initial value
id=komavar,
% date=auto, % commented as this setting matches the initial value
% delivery-date = auto, % commented as this setting matches the initial value
subject=komavar,
fromaddress=komavar,
% tax/category=S, % commented as this setting matches the initial value
% tax/rate=19, % commented as this setting matches the initial value
unit=hour,
seller/name = {\fromname\space(\fromfullname)},
seller/postcode = {\frompostalcode},
seller/city ={\fromcity},
seller/country = DE,
seller/address = {\fromstreet\space\fromstreetno},
seller/vatid = {\fromvatid},
seller/contact = {\fromfullname\\\fromphone\\\fromemail},
seller/email = {\fromemail},
buyer/reference = {buyer-reference}, %oder Leitweg-ID
buyer/name = {Käufer Name},
buyer/postcode = {20253},
buyer/city ={Hamburg},
buyer/country = DE,
buyer/address = {Address 1\\Address 2},
buyer/vatid = {DE280195195},
buyer/email = {invoice@example.org},
currency=€,
payment-terms={Zahlbar innerhalb von\space\paymentdays\space Tagen},% either this one or the due-date below is required
% due-date={20240118},
payment-means / type = 58, % SEPA Übereisung,
payment-means / iban = {\fromiban},
payment-means / account-holder = {\fromfullname},
payment-means / bic = {\frombic}
}
\usepackage{fontspec}
\setmainfont{Helvetica}
\begin{document}
\begin{letter}{Kunde\\Twietestr. 32\\20000 Hamburg}
\opening{Guten Tag,}
hiermit stelle ich Ihnen meine Arbeit im Rahmen des Projektes XX in Rechnung. Die Leistung wurde im Januar 2024 erbracht.
\AddInvoiceItem{3}{Weiterentwicklung/funktionale Erweiterung}{90}
\vspace{\baselineskip}
\PrintInvoiceTabular
Ich bitte Sie den oben genannten Betrag binnen \paymentdays{} Tagen auf unten genanntes Konto zu überweisen.
\usekomavar*{fromaccount} \tab\usekomavar{fromaccount}\\
\usekomavar*{fromiban} \tab\usekomavar{fromiban}\\
\usekomavar*{frombic} \tab\usekomavar{frombic}\\
Bank: \tab\usekomavar{frombank}\\
\usekomavar*{fromtaxid} \usekomavar{fromtaxid}\\
\usekomavar*{fromvatid} \usekomavar{fromvatid}
\end{letter}
\end{document}
You basically just need to adapt the client data and with \AddInvoiceItem
you add the amounts which are then automatically summed.
So with everything in place you can compile the .tex
file from your editor or by running
lualatex -pdf invoice.tex
in your terminal. You can/should then double check if the XML embed is correct in a Zugferd viewer tool like the one from Elster 🪹.
And there it is your valid Zugferd/Factur-X invoice 🥳 You can find an example generated with this template here!