inwendo / zugferd
用于创建和读取欧洲电子发票的库
Requires
- php: ^7.3|^7.4|^8.0|^8.1
- ext-simplexml: *
- goetas-webservices/xsd2php-runtime: ^0.2.13
- horstoeko/mimedb: ^1
- horstoeko/stringmanagement: ^1
- jms/serializer: ^3
- setasign/fpdf: ^1
- setasign/fpdi: ^2
- smalot/pdfparser: ^0|^2
- symfony/validator: ^4.4|^5|^6
- symfony/yaml: ^4.4|^5|^6
Requires (Dev)
- goetas-webservices/xsd2php: ^0
- pdepend/pdepend: ^2
- phploc/phploc: ^7
- phpmd/phpmd: ^2
- phpstan/phpstan: ^1.8
- phpunit/phpunit: ^9
- sebastian/phpcpd: ^6
- squizlabs/php_codesniffer: ^3
This package is not auto-updated.
Last update: 2024-09-28 18:37:12 UTC
README
目录
许可协议
本项目中的代码在MIT许可证下提供。
概述
使用horstoeko/zugferd
,您可以读取和写入包含电子发票数据的xml文件,这些数据符合Minimum-, Basic-, EN16931-, Extended-和XRechnung配置文件。此外,还可以将XML数据附加到由ERP系统创建的现有PDF文件中。如果同时存在XML文件(或XML字符串)和PDF文件(或字符串形式的PDF),则可以使用ZugferdDocumentPdfMerger
类创建一个符合规范的带附件的PDF文件。
该库的优势在于,您无需担心特定XML元素是否存在于期望的配置文件中 - 您可以使用相同的程序代码为所有支持的配置文件编写。
支持的配置文件
- EN16931最小
- EN16931基本
- EN16931基本WL
- EN16931舒适
- EN16931扩展
- EN16931 XRechnung 1.x
- EN16931 XRechnung 2.x
- EN16931 XRechnung 3.x
注意:本软件包仅提供CII支持 - 不支持UBL
更多信息
相关项目
依赖项
本软件包利用以下工具
我们的Wiki
我们提供正在建设的Wiki。这个Wiki依赖于您的问题,也依赖于您的合作。如果在使用本库的过程中发现某些内容不清楚或根本没有描述,请告诉我们。
安装
推荐通过以下方式安装horstoeko/zugferd
- 将依赖关系添加到您的
composer.json
文件
"require": { .. "horstoeko/zugferd":"^1", .. },
使用
有关详细说明,您可以查看本软件包的示例以及每个版本附带的文档。
配置
通过ZugferdSettings
类,可以控制XML和PDF生成的各种选项
public static function getAmountDecimals(): int
返回金额字段当前配置的小数位数(默认:2)。
public static function setAmountDecimals(int $amountDecimals): void
设置金额字段的小数位数。
public static function getQuantityDecimals(): int
返回当前配置的数量字段的小数位数(默认:2)。
public static function setQuantityDecimals(int $quantityDecimals): void
设置数量字段的小数位数。
public static function getPercentDecimals(): int
返回当前配置的百分比字段的小数位数(默认:2)。
public static function setPercentDecimals(int $percentDecimals): void
设置百分比字段的小数位数。
public static function getDecimalSeparator(): string
返回当前配置的十进制分隔符字符(默认:.)。
public static function setDecimalSeparator(string $decimalSeparator): void
设置用作小数分隔符的字符。
public static function getThousandsSeparator(): string
返回当前配置的用作千位分隔符的字符。(默认:空)
public static function setThousandsSeparator(string $thousandsSeparator): void
设置用作千位分隔符的字符。
读取XML文件
读取XML数据的中心入口是类 ZugferdDocumentReader
。它提供了读取标题和行信息的方法,以下是一个示例:
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";
读取包含XML附件的PDF文件
从PDF读取发票数据类似:只需使用 ZugferdDocumentPdfReader
类代替 ZugferdDocumentReader
类。
use horstoeko\zugferd\ZugferdDocumentPdfReader; $document = ZugferdDocumentPdfReader::readAndGuessFromFile(dirname(__FILE__) . "/xml/factur-x.pdf");
接下来读取发票数据的方法与读取XML文件相同。
写入XML文件
ZugferdDocumentBuilder
类再次是生成合规XML数据的中心入口。
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");
写入包含附件XML文件的PDF文件
如果您已经有了发票的现有打印输出(例如来自ERP系统)并希望向现有PDF添加XML数据流,请使用类 ZugferdDocumentPdfBuilder
。
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");
合并现有的PDF和XML
假设我们已经有了一个合规的XML(例如在Comfort配置文件中)和一个已经包含打印布局的PDF。然后可以使用类 ZugferdDocumentPdfMerger
将这两个文件合并为一个合规的PDF(带有XML附件)。
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和/或PDF不必作为文件存在。也可以将包含相应数据的字符串传递给 ZugferdDocumentPdfMerger
类。
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);