The code in this project is provided under the MIT license.
With horstoeko/zugferd
you can read and write xml files containing electronic invoice data in the Minimum-, Basic-, EN16931-, Extended- and XRechnung Profile. In addition, it is possible to attach the XML data to an existing PDF file, which was created from an ERP system, for example. If both an XML file (or XML string) and a PDF file (or a PDF in the form of a string) exist, then a compliant PDF file with attachment can be created using the ZugferdDocumentPdfMerger
class.
The advantage of this library is that you don't have to worry about whether a particular XML element exists in a desired profile - you can use the same program code for all supported profiles.
- EN16931 Minimum
- EN16931 Basic
- EN16931 Basic WL
- EN16931 Comfort
- EN16931 Extended
- EN16931 XRechnung 1.x
- EN16931 XRechnung 2.x
- EN16931 XRechnung 3.x
Note: This package provides only support for CII - not UBL
This package makes use of
We offer you a wiki that is currently under construction. This wiki lives from your questions, but also from your cooperation. If you find that something is unclear or not described at all during your work with this library, please let us know.
There is one recommended way to install horstoeko/zugferd
via Composer:
- adding the dependency to your
composer.json
file:
"require": {
..
"horstoeko/zugferd":"^1",
..
},
For detailed eplanation you may have a look in the examples of this package and the documentation attached to every release.
By means of the ZugferdSettings
class it is possible to control various options for XML and PDF generation:
public static function getAmountDecimals(): int
Returns the currently configured number of decimal places for amount fields (Default: 2).
public static function setAmountDecimals(int $amountDecimals): void
Set the number of decimal places for amount fields.
public static function getQuantityDecimals(): int
Returns the currently configured number of decimal places for quantity fields (Default: 2).
public static function setQuantityDecimals(int $quantityDecimals): void
Set the number of decimal places for quantity fields.
public static function getPercentDecimals(): int
Returns the currently configured number of decimal places for percentage fields (Default: 2).
public static function setPercentDecimals(int $percentDecimals): void
Set the number of decimal places for percentage fields.
public static function getDecimalSeparator(): string
Returns the currently configured character for the decimal separator. _(Default: .)
public static function setDecimalSeparator(string $decimalSeparator): void
Set the character to use as the decimal separator.
public static function getThousandsSeparator(): string
Returns the currently configured character for the thousands separator. (Default: Empty)
public static function setThousandsSeparator(string $thousandsSeparator): void
Set the character to use as the thousands separator.
The central entry point to read XML data is the class ZugferdDocumentReader
. Among other things, this provides methods for reading header and line information, as can be seen in the following example:
use horstoeko\zugferd\ZugferdDocumentReader;
$document = ZugferdDocumentReader::readAndGuessFromFile(dirname(__FILE__) . "/xml/factur-x.xml");
$document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $invoiceCurrency, $taxCurrency, $documentname, $documentlanguage, $effectiveSpecifiedPeriod);
echo "\r\nGeneral document information\r\n";
echo "----------------------------------------------------------------------\r\n";
echo "Profile: {$document->profileDefinition["name"]}\r\n";
echo "Profile: {$document->profileDefinition["altname"]}\r\n";
echo "Document No: {$documentno}\r\n";
echo "Document Type: {$documenttypecode}\r\n";
echo "Document Date: {$documentdate->format("Y-m-d")}\r\n";
echo "Invoice currency: {$invoiceCurrency}\r\n";
echo "Tax currency: {$taxCurrency}\r\n";
if ($document->firstDocumentPosition()) {
echo "\r\nDocument positions\r\n";
echo "----------------------------------------------------------------------\r\n";
do {
$document->getDocumentPositionGenerals($lineid, $linestatuscode, $linestatusreasoncode);
$document->getDocumentPositionProductDetails($prodname, $proddesc, $prodsellerid, $prodbuyerid, $prodglobalidtype, $prodglobalid);
$document->getDocumentPositionGrossPrice($grosspriceamount, $grosspricebasisquantity, $grosspricebasisquantityunitcode);
$document->getDocumentPositionNetPrice($netpriceamount, $netpricebasisquantity, $netpricebasisquantityunitcode);
$document->getDocumentPositionLineSummation($lineTotalAmount, $totalAllowanceChargeAmount);
$document->getDocumentPositionQuantity($billedquantity, $billedquantityunitcode, $chargeFreeQuantity, $chargeFreeQuantityunitcode, $packageQuantity, $packageQuantityunitcode);
echo " - Line Id: {$lineid}\r\n";
echo " - Product Name: {$prodname}\r\n";
echo " - Product Description: {$proddesc}\r\n";
echo " - Product Buyer ID: {$prodbuyerid}\r\n";
echo " - Product Gross Price: {$grosspriceamount}\r\n";
echo " - Product Gross Price Basis Qty.: {$grosspricebasisquantity} {$grosspricebasisquantityunitcode}\r\n";
echo " - Product Net Price: {$netpriceamount}\r\n";
echo " - Product Net Price Basis Qty.: {$netpricebasisquantity} {$netpricebasisquantityunitcode}\r\n";
echo " - Quantity: {$billedquantity} {$billedquantityunitcode}\r\n";
echo " - Line amount: {$lineTotalAmount}\r\n";
if ($document->firstDocumentPositionTax()) {
echo " - Position Tax(es)\r\n";
do {
$document->getDocumentPositionTax($categoryCode, $typeCode, $rateApplicablePercent, $calculatedAmount, $exemptionReason, $exemptionReasonCode);
echo " - Tax category code: {$categoryCode}\r\n";
echo " - Tax type code: {$typeCode}\r\n";
echo " - Tax percent: {$rateApplicablePercent}\r\n";
echo " - Tax amount: {$calculatedAmount}\r\n";
} while ($document->nextDocumentPositionTax());
}
if ($document->firstDocumentPositionAllowanceCharge()) {
echo " - Position Allowance(s)/Charge(s)\r\n";
do {
$document->getDocumentPositionAllowanceCharge($actualAmount, $isCharge, $calculationPercent, $basisAmount, $reason, $taxTypeCode, $taxCategoryCode, $rateApplicablePercent, $sequence, $basisQuantity, $basisQuantityUnitCode, $reasonCode);
echo " - Information\r\n";
echo " - Actual Amount: {$actualAmount}\r\n";
echo " - Type: " . ($isCharge ? "Charge" : "Allowance") . "\r\n";
echo " - Tax category code: {$taxCategoryCode}\r\n";
echo " - Tax type code: {$taxTypeCode}\r\n";
echo " - Tax percent: {$rateApplicablePercent}\r\n";
echo " - Calculated percent: {$calculationPercent}\r\n";
echo " - Basis amount: {$basisAmount}\r\n";
echo " - Basis qty.: {$basisQuantity} {$basisQuantityUnitCode}\r\n";
} while ($document->nextDocumentPositionAllowanceCharge());
}
echo "\r\n";
} while ($document->nextDocumentPosition());
}
if ($document->firstDocumentAllowanceCharge()) {
echo "\r\nDocument allowance(s)/charge(s)\r\n";
echo "----------------------------------------------------------------------\r\n";
do {
$document->getDocumentAllowanceCharge($actualAmount, $isCharge, $taxCategoryCode, $taxTypeCode, $rateApplicablePercent, $sequence, $calculationPercent, $basisAmount, $basisQuantity, $basisQuantityUnitCode, $reasonCode, $reason);
echo " - Information\r\n";
echo " - Actual Amount: {$actualAmount}\r\n";
echo " - Type: " . ($isCharge ? "Charge" : "Allowance") . "\r\n";
echo " - Tax category code: {$taxCategoryCode}\r\n";
echo " - Tax type code: {$taxTypeCode}\r\n";
echo " - Tax percent: {$rateApplicablePercent}\r\n";
echo " - Calculated percent: {$calculationPercent}\r\n";
echo " - Basis amount: {$basisAmount}\r\n";
echo " - Basis qty.: {$basisQuantity} {$basisQuantityUnitCode}\r\n";
} while ($document->nextDocumentAllowanceCharge());
}
if ($document->firstDocumentTax()) {
echo "\r\nDocument tax\r\n";
echo "----------------------------------------------------------------------\r\n";
do {
$document->getDocumentTax($categoryCode, $typeCode, $basisAmount, $calculatedAmount, $rateApplicablePercent, $exemptionReason, $exemptionReasonCode, $lineTotalBasisAmount, $allowanceChargeBasisAmount, $taxPointDate, $dueDateTypeCode);
echo " - Information\r\n";
echo " - Tax category code: {$categoryCode}\r\n";
echo " - Tax type code: {$typeCode}\r\n";
echo " - Basis amount: {$basisAmount}\r\n";
echo " - Line total Basis amount: {$lineTotalBasisAmount}\r\n";
echo " - Tax percent: {$rateApplicablePercent}\r\n";
echo " - Tax amount: {$calculatedAmount}\r\n";
} while ($document->nextDocumentTax());
}
$document->getDocumentSummation($grandTotalAmount, $duePayableAmount, $lineTotalAmount, $chargeTotalAmount, $allowanceTotalAmount, $taxBasisTotalAmount, $taxTotalAmount, $roundingAmount, $totalPrepaidAmount);
echo "\r\nDocument summation\r\n";
echo "----------------------------------------------------------------------\r\n";
echo " - Line total amount {$lineTotalAmount}\r\n";
echo " - Charge total amount {$chargeTotalAmount}\r\n";
echo " - Allowance total amount {$allowanceTotalAmount}\r\n";
echo " - Tax basis total amount {$taxBasisTotalAmount}\r\n";
echo " - Tax total amount {$taxTotalAmount}\r\n";
echo " - Grant total amount {$grandTotalAmount}\r\n";
echo " - Due payable amount {$duePayableAmount}\r\n";
Reading invoice data from a PDF is similar: you just need to use the ZugferdDocumentPdfReader
class instead of ZugferdDocumentReader
:
use horstoeko\zugferd\ZugferdDocumentPdfReader;
$document = ZugferdDocumentPdfReader::readAndGuessFromFile(dirname(__FILE__) . "/xml/factur-x.pdf");
The further reading of the invoice data is then identical with Reading a xml file
The ZugferdDocumentBuilder
class is again the central entry point to generate compliant XML data:
use horstoeko\zugferd\ZugferdDocumentBuilder;
use horstoeko\zugferd\ZugferdProfiles;
// Create an empty invoice document in the EN16931 profile
$document = ZugferdDocumentBuilder::CreateNew(ZugferdProfiles::PROFILE_EN16931);
// Add invoice and position information
$document
->setDocumentInformation("471102", "380", \DateTime::createFromFormat("Ymd", "20180305"), "EUR")
->addDocumentNote('Rechnung gemäß Bestellung vom 01.03.2018.')
->setDocumentSupplyChainEvent(\DateTime::createFromFormat('Ymd', '20180305'))
->setDocumentSeller("Lieferant GmbH", "549910")
->addDocumentSellerGlobalId("4000001123452", "0088")
->addDocumentSellerTaxRegistration("FC", "201/113/40209")
->addDocumentSellerTaxRegistration("VA", "DE123456789")
->setDocumentSellerAddress("Lieferantenstraße 20", "", "", "80333", "München", "DE")
->setDocumentBuyer("Kunden AG Mitte", "GE2020211")
->setDocumentBuyerAddress("Kundenstraße 15", "", "", "69876", "Frankfurt", "DE")
->addDocumentTax("S", "VAT", 275.0, 19.25, 7.0)
->addDocumentTax("S", "VAT", 198.0, 37.02, 19.0)
->setDocumentSummation(529.87, 529.87, 473.00, 0.0, 0.0, 473.00, 56.87, null, 0.0)
->addDocumentPaymentTerm("Zahlbar innerhalb 30 Tagen netto bis 04.04.2018, 3% Skonto innerhalb 10 Tagen bis 15.03.2018")
->addNewPosition("1")
->setDocumentPositionProductDetails("Trennblätter A4", "", "TB100A4", null, "0160", "4012345001235")
->setDocumentPositionGrossPrice(9.9000)
->setDocumentPositionNetPrice(9.9000)
->setDocumentPositionQuantity(20, "H87")
->addDocumentPositionTax('S', 'VAT', 19)
->setDocumentPositionLineSummation(198.0)
->addNewPosition("2")
->setDocumentPositionProductDetails("Joghurt Banane", "", "ARNR2", null, "0160", "4000050986428")
->SetDocumentPositionGrossPrice(5.5000)
->SetDocumentPositionNetPrice(5.5000)
->SetDocumentPositionQuantity(50, "H87")
->AddDocumentPositionTax('S', 'VAT', 7)
->SetDocumentPositionLineSummation(275.0)
->writeFile("/tmp/factur-x.xml");
Use the class ZugferdDocumentPdfBuilder
if you already have an existing print output of the invoice (for example from an ERP system) and want to add an XML data stream to the existing PDF:
use horstoeko\zugferd\ZugferdDocumentBuilder;
use horstoeko\zugferd\ZugferdDocumentPdfBuilder;
use horstoeko\zugferd\ZugferdProfiles;
// Create an empty invoice document in the EN16931 profile
$document = ZugferdDocumentBuilder::CreateNew(ZugferdProfiles::PROFILE_EN16931);
// Add invoice and position information
$document
->setDocumentInformation("471102", "380", \DateTime::createFromFormat("Ymd", "20180305"), "EUR")
->addDocumentNote('Rechnung gemäß Bestellung vom 01.03.2018.')
->setDocumentSupplyChainEvent(\DateTime::createFromFormat('Ymd', '20180305'))
->setDocumentSeller("Lieferant GmbH", "549910")
->addDocumentSellerGlobalId("4000001123452", "0088")
->addDocumentSellerTaxRegistration("FC", "201/113/40209")
->addDocumentSellerTaxRegistration("VA", "DE123456789")
->setDocumentSellerAddress("Lieferantenstraße 20", "", "", "80333", "München", "DE")
->setDocumentBuyer("Kunden AG Mitte", "GE2020211")
->setDocumentBuyerAddress("Kundenstraße 15", "", "", "69876", "Frankfurt", "DE")
->addDocumentTax("S", "VAT", 275.0, 19.25, 7.0)
->addDocumentTax("S", "VAT", 198.0, 37.02, 19.0)
->setDocumentSummation(529.87, 529.87, 473.00, 0.0, 0.0, 473.00, 56.87, null, 0.0)
->addDocumentPaymentTerm("Zahlbar innerhalb 30 Tagen netto bis 04.04.2018, 3% Skonto innerhalb 10 Tagen bis 15.03.2018")
->addNewPosition("1")
->setDocumentPositionProductDetails("Trennblätter A4", "", "TB100A4", null, "0160", "4012345001235")
->setDocumentPositionGrossPrice(9.9000)
->setDocumentPositionNetPrice(9.9000)
->setDocumentPositionQuantity(20, "H87")
->addDocumentPositionTax('S', 'VAT', 19)
->setDocumentPositionLineSummation(198.0)
->addNewPosition("2")
->setDocumentPositionProductDetails("Joghurt Banane", "", "ARNR2", null, "0160", "4000050986428")
->SetDocumentPositionGrossPrice(5.5000)
->SetDocumentPositionNetPrice(5.5000)
->SetDocumentPositionQuantity(50, "H87")
->AddDocumentPositionTax('S', 'VAT', 7)
->SetDocumentPositionLineSummation(275.0);
// Save merged PDF (existing original and XML) to a file
$pdfBuilder = new ZugferdDocumentPdfBuilder($document, "/tmp/existingprintlayout.pdf");
$pdfBuilder->generateDocument()->saveDocument("/tmp/merged.pdf");
// Alternatively, you can also return the merged output (existing original and XML) as a binary string
$pdfBuilder = new ZugferdDocumentPdfBuilder($document, "/tmp/existingprintlayout.pdf");
$pdfBinaryString = $pdfBuilder->generateDocument()->downloadString("merged.pdf");
Let's assume we already have a compliant XML (for example in the Comfort profile) and a PDF that already contains the print layout. Then it is possible to merge these two files into a compliant PDF (with XML attachment) using the class ZugferdDocumentPdfMerger
.
use horstoeko\zugferd\ZugferdDocumentPdfMerger;
require dirname(__FILE__) . "/../vendor/autoload.php";
$existingXml = dirname(__FILE__) . "/invoice_1.xml";
$existingPdf = dirname(__FILE__) . "/emptypdf.pdf";
$mergeToPdf = dirname(__FILE__) . "/fullpdf.pdf";
if (!file_exists($existingXml) || !file_exists($existingPdf)) {
throw new \Exception("XML and/or PDF does not exist");
}
(new ZugferdDocumentPdfMerger($existingXml, $existingPdf))->generateDocument()->saveDocument($mergeToPdf);
XML and/or PDF do not have to be available as a file. Strings containing the corresponding data can also be passed to the ZugferdDocumentPdfMerger class.
use horstoeko\zugferd\ZugferdDocumentPdfMerger;
require dirname(__FILE__) . "/../vendor/autoload.php";
$existingXml = "<xml>,,,,,</xml>";
$existingPdf = "%PDF-1.5...........";
$mergeToPdf = dirname(__FILE__) . "/fullpdf.pdf";
(new ZugferdDocumentPdfMerger($existingXml, $existingPdf))->generateDocument()->saveDocument($mergeToPdf);