Read and write electronic invoices in Delphi: ZUGFeRD, its successor Factur-X, and the German XRechnung format. The library works on a single invoice object — you fill it in, then decide which version, profile and syntax to write it as.
This is a port of ZUGFeRD-csharp by Stephan Stapel. The port is free to go its own way where that yields the better result; see Relationship to the C# library.
| Format | write | read |
|---|---|---|
| ZUGFeRD 2.3 / Factur-X 1.0 — CII (Cross Industry Invoice) | yes | yes |
| XRechnung 3.x — UBL (Universal Business Language) | yes | yes |
| XRechnung — CII | yes | yes |
| ZUGFeRD 2.0 — CII | yes | yes |
ZUGFeRD 1.0 (rsm:CrossIndustryDocument) |
yes | yes |
UBL is available for version 2.3 only, and there only for the XRechnung profiles — that is what the format is defined for. Everything else is CII.
Minimum, BasicWL, Basic, Comfort (EN 16931), Extended, XRechnung1, XRechnung and
EReporting. The profile decides which elements end up in the document: writing is filtered through
intf.ZUGFeRDProfileAwareXmlTextWriter.pas, so the same
invoice object yields a valid MINIMUM or a full EXTENDED document without any changes on your side.
Leere optionale Container werden unterdrückt. WriteStartRequiredElement erhält dagegen auch
einen leeren, im Profil sichtbaren Pflichtcontainer, ohne Unterelemente oder Werte zu ergänzen.
Die CII-Writer für Version 2.0 und 2.3 verwenden dies für ApplicableHeaderTradeDelivery, auch
wenn nur ein Abrechnungszeitraum (BG-14/BG-26), aber kein Lieferdatum (BT-72) vorliegt.
In ZUGFeRD 1.0 bleibt ApplicableSupplyChainTradeDelivery gemäß XSD optional.
Reading detects version and profile from the document itself. TZUGFeRDInvoiceDescriptor.GetVersion
answers that question before you load anything.
Delphi 10.4 Sydney or newer, Win32 and Win64 — inline variable declarations are used throughout, and
ZUGFeRDNullable<T> relies on the custom managed record operators (class operator Initialize)
introduced with 10.4. XML is
handled through MSXML, so the library is Windows-only; apart from the RTL there are no dependencies.
MSXML is a COM component. A VCL application initialises COM for you; a console application has to do it itself, otherwise the first read or write fails with "Microsoft MSXML is not installed":
uses Winapi.ActiveX;
...
CoInitialize(nil);
try
//...
finally
CoUninitialize;
end;Put the units of the root directory on your search path. That is all the setup there is.
| Unit | Role |
|---|---|
| intf.ZUGFeRDInvoiceDescriptor.pas | The invoice object: TZUGFeRDInvoiceDescriptor, plus Load and Save |
| intf.ZUGFeRDInvoiceDescriptor23CIIWriter.pas / ...23CIIReader.pas | CII for ZUGFeRD 2.2+ and Factur-X |
| intf.ZUGFeRDInvoiceDescriptor22UBLWriter.pas / ...22UblReader.pas | UBL for XRechnung |
| intf.ZUGFeRDInvoiceDescriptor20Writer.pas / ...20Reader.pas | ZUGFeRD 2.0 |
| intf.ZUGFeRDInvoiceDescriptor1Writer.pas / ...1Reader.pas | ZUGFeRD 1.0 |
| intf.ZUGFeRDInvoiceValidator.pas | Recalculates the document totals against the EN 16931 rules |
| intf.ZUGFeRDHelper.pas | Nullable wrapper, enum conversion, PDFtk wrapper |
The version-specific writers are picked for you — you never instantiate them directly.
uses
intf.ZUGFeRDInvoiceDescriptor, intf.ZUGFeRDVersion, intf.ZUGFeRDProfile,
intf.ZUGFeRDFormats, intf.ZUGFeRDCurrencyCodes, intf.ZUGFeRDCountryCodes,
intf.ZUGFeRDQuantityCodes, intf.ZUGFeRDTaxTypes, intf.ZUGFeRDTaxCategoryCodes,
intf.ZUGFeRDHelper;
var
desc : TZUGFeRDInvoiceDescriptor;
begin
desc := TZUGFeRDInvoiceDescriptor.CreateInvoice('R2026-0815', EncodeDate(2026, 9, 3),
TZUGFeRDCurrencyCodes.EUR);
try
desc.SetSeller('Verkaeufer GmbH', '01234', 'Verkaeuferstadt', 'Hauptstrasse 1',
TZUGFeRDNullableParam<TZUGFeRDCountryCodes>.Create(TZUGFeRDCountryCodes.DE));
desc.SetBuyer('Kaeufer AG', '05678', 'Kaeuferstadt', 'Nebenweg 2',
TZUGFeRDNullableParam<TZUGFeRDCountryCodes>.Create(TZUGFeRDCountryCodes.DE));
//BR-DE-2 and BR-DE-5..7: XRechnung requires a seller contact with name, phone and mail
desc.SetSellerContact('Meier', '', 'meier@verkaeufer.de', '030 0815');
desc.AddTradeLineItem(
{name=} 'Beratungsleistung',
{netUnitPrice=} TZUGFeRDNullableParam<Currency>.Create(100),
{description=} '',
{unitCode=} TZUGFeRDNullableParam<TZUGFeRDQuantityCodes>.Create(TZUGFeRDQuantityCodes.HUR),
{unitQuantity=} nil,
{grossUnitPrice=} nil,
{billedQuantity=} 2,
{lineTotalAmount=} 200,
{taxType=} TZUGFeRDNullableParam<TZUGFeRDTaxTypes>.Create(TZUGFeRDTaxTypes.VAT),
{categoryCode=} TZUGFeRDNullableParam<TZUGFeRDTaxCategoryCodes>.Create(TZUGFeRDTaxCategoryCodes.S),
{taxPercent=} 19.0);
desc.AddApplicableTradeTax({calculatedAmount=} 38.00, {basisAmount=} 200.00,
{percent=} 19.0, TZUGFeRDTaxTypes.VAT, TZUGFeRDTaxCategoryCodes.S);
desc.SetTotals(
{lineTotalAmount=} 200.00,
{chargeTotalAmount=} 0,
{allowanceTotalAmount=} 0,
{taxBasisAmount=} 200.00,
{taxTotalAmount=} 38.00,
{grandTotalAmount=} 238.00,
{totalPrepaidAmount=} 0,
{duePayableAmount=} 238.00);
desc.Save('invoice-cii.xml', TZUGFeRDVersion.Version23, TZUGFeRDProfile.Comfort);
finally
desc.Free;
end;
end;The last call decides the output. The same invoice object also writes as an XRechnung in UBL:
desc.Save('invoice-ubl.xml', TZUGFeRDVersion.Version23, TZUGFeRDProfile.XRechnung,
TZUGFeRDFormats.UBL);Save also takes a TStream. Amounts and dates are formatted invariantly, so the output does not
depend on the machine's locale.
Writing to a file checks the rules that a profile makes mandatory and raises
TZUGFeRDMissingDataException when something required is missing — the seller contact above is
there for exactly that reason. Leave it out and the CII document is still written, while the
XRechnung one is refused. The document is built in full before the target file is opened, so a
rejected invoice leaves an existing file untouched (an I/O error while writing, such as a full
disk, can still truncate it).
The TStream overload skips that shared check. Writer-specific checks still apply: UBL rejects any
profile other than XRechnung, and CII validates advance payments in the Extended profile. Call
Save with a file name, or use TZUGFeRDInvoiceDescriptor23Writer.Validate yourself, if you want
the full profile rules enforced before writing to a stream.
Seller and buyer contacts carry a single name in CII (BT-41 / BT-56): ram:PersonName and
ram:DepartmentName must not both be present (CII-SR-465/466, enforced by the Factur-X and
XRechnung validators in every profile). If both Name and OrgUnit are set, the CII writers for
2.0 and 2.3 write only the person name and drop the department; the department is written only
when no person name is given. Contacts of other parties keep both elements.
The project reference (BT-11) is written in CII as ram:SpecifiedProcuringProject, whose schema
also requires a ram:Name. Without a project name the CII writer fills in Project reference, the
value KoSIT uses in its own test documents, so such an invoice reads back with that name instead of
an empty one. A project without an ID is not written at all.
One call handles every supported version and syntax; the reader is selected from the document:
var
desc : TZUGFeRDInvoiceDescriptor;
begin
desc := TZUGFeRDInvoiceDescriptor.Load('invoice.xml');
try
Writeln(desc.InvoiceNo, ' ', desc.GrandTotalAmount.GetValueOrDefault:0:2);
finally
desc.Free;
end;
end;Load is also available for a TStream and an IXMLDocument. Optional fields come back as
ZUGFeRDNullable<T> — check HasValue before reading Value, because a missing amount and an
amount of zero are not the same thing.
Profile reflects what the document declares. For UBL that is cbc:CustomizationID, so a Peppol
BIS document reads back as Unknown rather than XRechnung, and cannot be written out again
unchanged. The raw identifier is always kept in GuideLine.
TZUGFeRDTax.TaxAmount und der Parameter calculatedAmount von AddApplicableTradeTax
haben den Typ ZUGFeRDNullable<Currency>. Ein neues Steuerobjekt hat keinen Betrag;
die Zuweisung TaxAmount := 0 setzt dagegen einen vorhandenen Nullbetrag. Numerische
Zuweisungen bleiben möglich. Bei Berechnungen und Vergleichen zuerst HasValue prüfen
und anschließend Value verwenden. Der Betrag wird nicht automatisch aus Basis und
Steuersatz berechnet. Abhängige Anwendungen müssen neu kompiliert werden.
Der CII-Reader erhält Anzahlungen in SpecifiedAdvancePayment (BG-X-45) auch bei
fehlenden oder unlesbaren Pflichtangaben, statt deshalb den übrigen Beleg abzulehnen.
Fehlende oder unlesbare Pflichtbeträge und unbekannte Steuercodes bleiben
ohne Wert, eine fehlende Steuerliste bleibt leer; eine vorhandene Rechnungsreferenz
bleibt auch ohne ID erhalten. Ein leerer Nullable unterscheidet fehlende und
unlesbare Werte nicht; der ursprüngliche ungültige Text wird nicht gespeichert.
XML-, Datei-, Stream- und Datumsfehler werden nicht zusätzlich abgefangen.
Das tolerante Lesen bestätigt keine Standardkonformität:
Der EXTENDED-Writer lehnt unvollständige Anzahlungen weiterhin ab, insbesondere einen
fehlenden Steuerbetrag BT-X-293 nach FX-SCH-A-000952. Auch die Kopfsteuer-Writer
verlangen vorhandene Beträge; der Summenvalidator meldet fehlendes BT-117.
TZUGFeRDInvoiceValidator recalculates the monetary summation of a loaded or newly built invoice and
compares it against the declared values, following the EN 16931 rules BR-CO-10 through BR-CO-17,
BR-DEC-20 and BR-53:
var
res : TZUGFeRDValidationResult;
begin
res := TZUGFeRDInvoiceValidator.Validate(desc, TZUGFeRDVersion.Version23);
try
if not res.IsValid then
Writeln(res.Messages.Text); //every step of the recalculation is logged
finally
res.Free;
end;
end;This checks arithmetic consistency, not schema conformance. For the full picture validate the generated XML against the official Schematron as well — see Links.
The library reads and writes invoice XML; it does not open PDF containers itself. To get the XML out of a ZUGFeRD or Factur-X PDF you need PDFtk Server, which intf.ZUGFeRDHelper.pas wraps:
var pdfAsStream : TStream;
var cmdOutput : String;
if GetZUGFeRDPdfHelper.SetPdfTkServerPath('C:\Program Files (x86)\PDFtk Server')
.PdfTkServerGetZUGFeRDPdfAttachment('zugferd.pdf',
pdfAsStream,
cmdOutput) then
try
System.Write(cmdOutput);
finally
pdfAsStream.Free;
end;If you would rather not depend on an external tool, the sibling library XRechnung-for-Delphi extracts PDF attachments in pure Pascal.
- Samples/ — a VCL demo that builds, writes and reads invoices.
- Unittest/ — DUnitX suite, console (
ZfDUnitTest.dpr) and GUI runner. Beyond the per-version tests it sweeps every example invoice shipped under documentation/: each one is read, written back and read again, then the core data is compared (invoice number, number of line items, currency and grand total). - documentation/ — the official specifications, schemas and example invoices from ZUGFeRD 1.0 up to 2.5.2 / Factur-X 1.09.2.
Building and running the suite, selecting individual tests, the exit codes and the limits of the memory checks are documented in Unittest/README.md.
Conventions that are not visible from the source - build baseline, comment language, source encoding and the checks to run before a pull request - are collected in CONTRIBUTING.md.
Synchronization point: https://github.com/stephanstapel/ZUGFeRD-csharp/commit/0bfdcb10680579c5a866d7301c848a39a4e67feb
The C# library is a reference, not a boundary. Where a finding turns out to be a genuine defect it is fixed here rather than deferred — several corrections to the totals validation have since been ported back the other way.
XRechnung-for-Delphi covers the same invoice formats with a data model built around the EN 16931 business terms, and can use this library as an optional reading path for profile content beyond that model.
- FeRD — ZUGFeRD 2.2 specification
- FACTUR-X / ZUGFeRD
- GEFEG profile browser — CII / Factur-X
- Validate online: ecosio · invoice-portal · ZUGFeRD Community
Subject to the Apache License 2.0.