inwendo/zugferd

用于创建和读取欧洲电子发票的库

安装数: 1 067

依赖关系: 0

建议者: 0

安全性: 0

星级: 0

关注者: 0

分支: 23

类型:package

dev-master 2023-11-24 15:32 UTC

This package is not auto-updated.

Last update: 2024-09-28 18:37:12 UTC


README

Latest Stable Version Total Downloads Latest Unstable Version License Gitter

CI (Ant, PHP 7.3) CI (Ant, PHP 7.4) CI (PHP 8.0) CI (PHP 8.1)

目录

许可协议

本项目中的代码在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依赖于您的问题,也依赖于您的合作。如果在使用本库的过程中发现某些内容不清楚或根本没有描述,请告诉我们。

您可以在以下位置找到我们的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);