diff --git a/composer.json b/composer.json index 116e88b11a104130c5e7c64ecaa811659b669ea7..fe340cb917def36d1d296c4d2bb691371e225c33 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "quiqqer/htmltopdf": "^3", "quiqqer/utils": "^2", "ramsey/uuid": "^3|^4", - "chillerlan/php-qrcode": "^4.3" + "chillerlan/php-qrcode": "^4.3", + "horstoeko/zugferd": "^1" }, "autoload": { "psr-4": { diff --git a/locale.xml b/locale.xml index 82acacb3afb984ca391d6ca668171daa40c7bb67..c0aeba9df98d18f8cf1156566ee9662dc2b16078 100644 --- a/locale.xml +++ b/locale.xml @@ -415,6 +415,43 @@ <en><![CDATA[Edit Invoice status:]]></en> </locale> + <locale name="invoice.settings.e-invoice.xInvoiceAttachment.text"> + <de><![CDATA[X-Rechnung anhängen]]></de> + <en><![CDATA[Attach X-bill]]></en> + </locale> + <locale name="invoice.settings.e-invoice.xInvoiceAttachment.description"> + <de><![CDATA[ + Bei Rechnungsmails werden Rechnungen automatisch auch als X-Rechnung XML angehängt, + um den Standard für elektronische Rechnungen zu erfüllen. + ]]></de> + <en><![CDATA[ + Invoices are automatically attached to invoice e-mails as X-invoices xml format, + to comply with the standard for electronic invoices. + ]]></en> + </locale> + <locale name="invoice.settings.e-invoice.xInvoiceAttachmentType.text"> + <de><![CDATA[X-Rechnung Profil]]></de> + <en><![CDATA[X-bill profile]]></en> + </locale> + + <locale name="invoice.settings.e-invoice.zugferdInvoiceAttachment.text"> + <de><![CDATA[Zugferd-Rechnung anhängen]]></de> + <en><![CDATA[Attach Zugferd-bill]]></en> + </locale> + <locale name="invoice.settings.e-invoice.zugferdInvoiceAttachment.description"> + <de><![CDATA[ + Bei Rechnungsmails werden Rechnungen automatisch im Zugferd PDF Format angehängt. + ]]></de> + <en><![CDATA[ + Invoices are automatically attached to invoice emails in Zugferd PDF format. + ]]></en> + </locale> + <locale name="invoice.settings.e-invoice.zugferdInvoiceAttachmentType.text"> + <de><![CDATA[Zugferd-Rechnung Profil]]></de> + <en><![CDATA[Zugferd-bill profile]]></en> + </locale> + + <locale name="invoice.type.creditNote"> <de><![CDATA[Gutschrift]]></de> <en><![CDATA[Credit Note]]></en> diff --git a/settings.xml b/settings.xml index 53981c2a77d9a6896df93ab1df5177cb722c6113..478a7e3c993a1db6046a5492c5541b6d15eb5f24 100644 --- a/settings.xml +++ b/settings.xml @@ -41,6 +41,23 @@ <conf name="invoiceAddressRequirementThreshold"> <type><![CDATA[float]]></type> </conf> + + <conf name="xInvoiceAttachment"> + <type><![CDATA[bool]]></type> + <defaultvalue><![CDATA[1]]></defaultvalue> + </conf> + <conf name="xInvoiceAttachmentType"> + <type><![CDATA[int]]></type> + <defaultvalue><![CDATA[2]]></defaultvalue> + </conf> + <conf name="zugferdInvoiceAttachment"> + <type><![CDATA[bool]]></type> + <defaultvalue><![CDATA[1]]></defaultvalue> + </conf> + <conf name="zugferdInvoiceAttachmentType"> + <type><![CDATA[int]]></type> + <defaultvalue><![CDATA[2]]></defaultvalue> + </conf> </section> <section name="temporaryInvoice"> @@ -214,6 +231,74 @@ </input> </settings> + + <settings title="eInvoice" name="eInvoice"> + <title> + <locale group="quiqqer/invoice" var="invoice.settings.e-invoice.title"/> + </title> + + <input conf="invoice.xInvoiceAttachment" type="checkbox"> + <text> + <locale group="quiqqer/invoice" + var="invoice.settings.e-invoice.xInvoiceAttachment.text"/> + </text> + <description> + <locale group="quiqqer/invoice" + var="invoice.settings.e-invoice.xInvoiceAttachment.description"/> + </description> + </input> + + <select conf="invoice.xInvoiceAttachmentType"> + <text> + <locale group="quiqqer/invoice" + var="invoice.settings.e-invoice.xInvoiceAttachmentType.text"/> + </text> + + <option value="0">Basic</option> + <option value="1">Basic WL</option> + <option value="2">EN16931</option> + <option value="3">Extended</option> + <option value="4">XRechnung (Germany only)</option> + <option value="5">XRechnung 2.0 (Germany only)</option> + <option value="6">XRechnung 2.1 (Germany only)</option> + <option value="7">XRechnung 2.2 (Germany only)</option> + <option value="8">Minimum</option> + <option value="9">XRechnung 2.3 (Germany only)</option> + <option value="10">XRechnung 3.0 (Germany only)</option> + </select> + + <input conf="invoice.zugferdInvoiceAttachment" type="checkbox"> + <text> + <locale group="quiqqer/invoice" + var="invoice.settings.e-invoice.zugferdInvoiceAttachment.text"/> + </text> + <description> + <locale group="quiqqer/invoice" + var="invoice.settings.e-invoice.zugferdInvoiceAttachment.description"/> + </description> + </input> + + <select conf="invoice.zugferdInvoiceAttachmentType"> + <text> + <locale group="quiqqer/invoice" + var="invoice.settings.e-invoice.zugferdInvoiceAttachmentType.text"/> + </text> + + <option value="0">Basic</option> + <option value="1">Basic WL</option> + <option value="2">EN16931</option> + <option value="3">Extended</option> + <option value="4">XRechnung (Germany only)</option> + <option value="5">XRechnung 2.0 (Germany only)</option> + <option value="6">XRechnung 2.1 (Germany only)</option> + <option value="7">XRechnung 2.2 (Germany only)</option> + <option value="8">Minimum</option> + <option value="9">XRechnung 2.3 (Germany only)</option> + <option value="10">XRechnung 3.0 (Germany only)</option> + </select> + + </settings> + <settings title="invoiceDownload" name="invoiceDownload"> <title> <locale group="quiqqer/invoice" var="invoice.settings.download.title"/> diff --git a/src/QUI/ERP/Accounting/Invoice/EventHandler.php b/src/QUI/ERP/Accounting/Invoice/EventHandler.php index f405b0dfacd2c4e0314d9a6b695dd7bb1702a04a..c427995c0f8145e828016e7658e8e6f208d7efc3 100644 --- a/src/QUI/ERP/Accounting/Invoice/EventHandler.php +++ b/src/QUI/ERP/Accounting/Invoice/EventHandler.php @@ -6,6 +6,9 @@ namespace QUI\ERP\Accounting\Invoice; +use horstoeko\zugferd\ZugferdDocumentBuilder; +use horstoeko\zugferd\ZugferdDocumentPdfBuilder; +use horstoeko\zugferd\ZugferdDocumentPdfMerger; use QUI; use QUI\ERP\Accounting\Invoice\Output\OutputProviderCancelled; use QUI\ERP\Accounting\Invoice\Output\OutputProviderCreditNote; @@ -23,6 +26,7 @@ use function file_get_contents; use function in_array; use function is_numeric; +use function str_replace; use function strtolower; use function strtotime; @@ -287,6 +291,7 @@ public static function onQuiqqerErpGetHistoryByUser( * @param string $entityType * @param string $recipient * @param Mailer $Mailer + * @param string $mailFile * @return void * * @throws Exception @@ -297,7 +302,8 @@ public static function onQuiqqerErpOutputSendMailBefore( $entityId, string $entityType, string $recipient, - QUI\Mail\Mailer $Mailer + QUI\Mail\Mailer $Mailer, + string $mailFile = '' ): void { $allowedEntityTypes = [ OutputProviderInvoice::getEntityType(), @@ -316,6 +322,29 @@ public static function onQuiqqerErpOutputSendMailBefore( return; } + // extend pdf with e-invoice + $Config = QUI::getPackage('quiqqer/invoice')->getConfig(); + + if ($Config->getValue('invoice', 'xInvoiceAttachment')) { + $xmlFile = str_replace('.pdf', '.xml', $mailFile); + $document = QUI\ERP\Accounting\Invoice\Utils\Invoice::getElectronicInvoice( + $Invoice, + $Config->getValue('invoice', 'xInvoiceAttachmentType') + ); + + $document->writeFile($xmlFile); + $Mailer->addAttachment($xmlFile); + } + + if (file_exists($mailFile) && $Config->getValue('invoice', 'zugferdInvoiceAttachment')) { + $document = QUI\ERP\Accounting\Invoice\Utils\Invoice::getElectronicInvoice( + $Invoice, + $Config->getValue('invoice', 'xInvoiceAttachmentType') + ); + $pdfBuilder = new ZugferdDocumentPdfBuilder($document, $mailFile); + $pdfBuilder->generateDocument()->saveDocument($mailFile); + } + // @todo $customerFiles = $Invoice->getCustomerFiles(); diff --git a/src/QUI/ERP/Accounting/Invoice/Utils/Invoice.php b/src/QUI/ERP/Accounting/Invoice/Utils/Invoice.php index f5bd1f114241aa9b71b667ba5847e93b155a278a..9916b9ba349debf17a28b5b0a7a6d4daafbc6cf3 100644 --- a/src/QUI/ERP/Accounting/Invoice/Utils/Invoice.php +++ b/src/QUI/ERP/Accounting/Invoice/Utils/Invoice.php @@ -6,13 +6,17 @@ namespace QUI\ERP\Accounting\Invoice\Utils; -use IntlDateFormatter; +use DateTime; use QUI; use QUI\ERP\Accounting\Invoice\Exception; use QUI\ERP\Accounting\Invoice\InvoiceTemporary; use QUI\ERP\Accounting\Invoice\ProcessingStatus\Handler as ProcessingStatuses; use QUI\ERP\Currency\Currency; use QUI\ExceptionStack; +use QUI\ERP\Defaults; +use IntlDateFormatter; +use horstoeko\zugferd\ZugferdDocumentBuilder; +use horstoeko\zugferd\ZugferdProfiles; use function array_map; use function array_merge; @@ -657,4 +661,128 @@ public static function addressRequirementThreshold(): float return floatval($threshold); } + + public static function getElectronicInvoice( + InvoiceTemporary|QUI\ERP\Accounting\Invoice\Invoice $Invoice, + $type = ZugferdProfiles::PROFILE_EN16931 + ): ZugferdDocumentBuilder { + $document = ZugferdDocumentBuilder::CreateNew($type); + + $date = $Invoice->getAttribute('date'); + $date = strtotime($date); + $date = (new DateTime())->setTimestamp($date); + + $document->setDocumentInformation( + $Invoice->getPrefixedNumber(), + "380", + $date, + $Invoice->getCurrency()->getCode() + ); + + // seller / owner + $document + ->setDocumentSeller(Defaults::conf('company', 'name')) + ->addDocumentSellerGlobalId("4000001123452", "0088") + ->addDocumentSellerTaxRegistration("FC", "201/113/40209") + ->addDocumentSellerTaxRegistration("VA", "DE123456789") + ->setDocumentSellerAddress( + Defaults::conf('company', 'street'), + "", + "", + Defaults::conf('company', 'zip'), + Defaults::conf('company', 'city'), + Defaults::conf('company', 'country') // @todo country ->code<- + ) + ->setDocumentSellerCommunication( + 'EM', + Defaults::conf('company', 'email') + ) + ->setDocumentSellerContact( + Defaults::conf('company', 'owner'), // @todo contact person + '', // @todo contact department + Defaults::conf('company', 'phone'), // @todo contact phone + Defaults::conf('company', 'fax'), // @todo contact fax + Defaults::conf('company', 'email') // @todo contact email + ); + + // bank stuff + $bankAccount = QUI\ERP\BankAccounts\Handler::getCompanyBankAccount(); + + if (!empty($bankAccount)) { + $document->addDocumentPaymentMeanToDirectDebit( + $bankAccount['iban'], + $Invoice->getPrefixedNumber() + ); + } + + // customer + $Customer = $Invoice->getCustomer(); + + $document + ->setDocumentBuyer( + $Customer->getName(), + $Customer->getCustomerNo() + ) + ->setDocumentBuyerAddress( + $Customer->getAddress()->getAttribute('street_no'), + "", + "", + $Customer->getAddress()->getAttribute('zip'), + $Customer->getAddress()->getAttribute('city'), + $Customer->getAddress()->getCountry()->getCode() + ) + ->setDocumentBuyerCommunication('EM', $Customer->getAddress()->getAttribute('email')) + ->setDocumentBuyerReference($Customer->getUUID()); + + // total + $priceCalculation = $Invoice->getPriceCalculation(); + $vatTotal = 0; + + foreach ($priceCalculation->getVat() as $vat) { + $document->addDocumentTax( + "S", + "VAT", + $priceCalculation->getSum()->value(), + $vat->value(), + $vat->getVat() + ); + + $vatTotal = $vatTotal + $vat->value(); + } + + $document->setDocumentSummation( + $priceCalculation->getSum()->value(), + $priceCalculation->getSum()->value(), + $priceCalculation->getSum()->value(), + 0.0, + 0.0, + $priceCalculation->getSum()->value(), + $vatTotal, + null, + 0.0 + ); + + // products + foreach ($Invoice->getArticles() as $Article) { + /* @var $Article QUI\ERP\Accounting\Article */ + $article = $Article->toArray(); + + $document + ->addNewPosition($article['position']) + ->setDocumentPositionProductDetails( + $article['title'], + $article['description'], + null, + null, + null, + null + ) + ->setDocumentPositionNetPrice($article['calculated']['nettoPrice']) + ->setDocumentPositionQuantity($article['quantity'], "H87") + ->addDocumentPositionTax('S', 'VAT', $article['vat']) + ->setDocumentPositionLineSummation($article['sum']); + } + + return $document; + } }