From c82da602a4da2ed6c3abe88714ba2a2aed8ca54c Mon Sep 17 00:00:00 2001
From: Henning Leutz <leutz@pcsg.de>
Date: Fri, 6 Dec 2024 10:43:36 +0100
Subject: [PATCH 1/4] feat: new e-invoice as attachment

---
 composer.json                                 |   3 +-
 locale.xml                                    |  37 +++++
 settings.xml                                  |  85 ++++++++++++
 .../ERP/Accounting/Invoice/EventHandler.php   |  31 ++++-
 .../ERP/Accounting/Invoice/Utils/Invoice.php  | 130 +++++++++++++++++-
 5 files changed, 283 insertions(+), 3 deletions(-)

diff --git a/composer.json b/composer.json
index 116e88b..fe340cb 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 82acacb..c0aeba9 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 53981c2..478a7e3 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 f405b0d..c427995 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 f5bd1f1..9916b9b 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;
+    }
 }
-- 
GitLab


From 2dbb4c1ff9b9605f74cfe6aa9ade6522dcfc2a9c Mon Sep 17 00:00:00 2001
From: Henning <leutz@pcsg.de>
Date: Sat, 14 Dec 2024 15:45:45 +0100
Subject: [PATCH 2/4] feat: invoice download in different formats

---
 bin/backend/controls/panels/Invoice.js |  16 +++
 bin/backend/download.php               |  67 ++++++++++++
 bin/backend/utils/Dialogs.css          |  23 ++++-
 bin/backend/utils/Dialogs.js           | 136 ++++++++++++++++++-------
 locale.xml                             |  13 +++
 5 files changed, 217 insertions(+), 38 deletions(-)
 create mode 100644 bin/backend/download.php

diff --git a/bin/backend/controls/panels/Invoice.js b/bin/backend/controls/panels/Invoice.js
index 1835322..7a2d894 100644
--- a/bin/backend/controls/panels/Invoice.js
+++ b/bin/backend/controls/panels/Invoice.js
@@ -36,6 +36,7 @@ define('package/quiqqer/invoice/bin/backend/controls/panels/Invoice', [
         Binds: [
             'print',
             'storno',
+            'download',
             'copy',
             'creditNote',
             'openInfo',
@@ -199,6 +200,14 @@ define('package/quiqqer/invoice/bin/backend/controls/panels/Invoice', [
                 }
             });
 
+            Actions.appendChild({
+                icon: 'fa fa-download',
+                text: QUILocale.get(lg, 'erp.panel.invoice.button.download'),
+                events: {
+                    onClick: this.download
+                }
+            });
+
             this.fireEvent('actionButtonCreate', [
                 this,
                 Actions
@@ -554,6 +563,13 @@ define('package/quiqqer/invoice/bin/backend/controls/panels/Invoice', [
             });
         },
 
+        download: function()
+        {
+            require(['package/quiqqer/invoice/bin/backend/utils/Dialogs'], (Dialogs)=> {
+                Dialogs.openDownloadDialog(this.getAttribute('data').hash);
+            });
+        },
+
         /**
          * Opens the storno / cancellation dialog
          *
diff --git a/bin/backend/download.php b/bin/backend/download.php
new file mode 100644
index 0000000..bf7c601
--- /dev/null
+++ b/bin/backend/download.php
@@ -0,0 +1,67 @@
+<?php
+
+use QUI\ERP\Accounting\Invoice\Handler;
+use QUI\ERP\Accounting\Invoice\Utils\Invoice as InvoiceUtils;
+use horstoeko\zugferd\ZugferdProfiles;
+use QUI\ERP\Output\Output;
+use Symfony\Component\HttpFoundation\Response;
+
+define('QUIQQER_SYSTEM', true);
+define('QUIQQER_AJAX', true);
+
+require_once dirname(__FILE__, 5) . '/header.php';
+
+$User = QUI::getUserBySession();
+
+if (!$User->canUseBackend()) {
+    exit;
+}
+
+if (empty($_REQUEST['invoice'])) {
+    exit;
+}
+
+if (empty($_REQUEST['type'])) {
+    exit;
+}
+
+$invoiceHash = $_REQUEST['invoice'];
+$type = mb_strtoupper($_REQUEST['type']);
+
+$profileMap = [
+    'PROFILE_BASIC' => ZugferdProfiles::PROFILE_BASIC,
+    'PROFILE_BASICWL' => ZugferdProfiles::PROFILE_BASICWL,
+    'PROFILE_EN16931' => ZugferdProfiles::PROFILE_EN16931,
+    'PROFILE_EXTENDED' => ZugferdProfiles::PROFILE_EXTENDED,
+    'PROFILE_XRECHNUNG' => ZugferdProfiles::PROFILE_XRECHNUNG,
+    'PROFILE_XRECHNUNG_2' => ZugferdProfiles::PROFILE_XRECHNUNG_2,
+    'PROFILE_XRECHNUNG_2_1' => ZugferdProfiles::PROFILE_XRECHNUNG_2_1,
+    'PROFILE_XRECHNUNG_2_2' => ZugferdProfiles::PROFILE_XRECHNUNG_2_2,
+    'PROFILE_MINIMUM' => ZugferdProfiles::PROFILE_MINIMUM,
+    'PROFILE_XRECHNUNG_2_3' => ZugferdProfiles::PROFILE_XRECHNUNG_2_3,
+    'PROFILE_XRECHNUNG_3' => ZugferdProfiles::PROFILE_XRECHNUNG_3,
+];
+
+try {
+    if ($type === 'PDF') {
+        $OutputDocument = Output::getDocumentPdf(
+            $invoiceHash,
+            QUI\ERP\Accounting\Invoice\Output\OutputProviderInvoice::getEntityType()
+        );
+
+        $OutputDocument->download();
+        exit;
+    }
+
+    $Invoice = Handler::getInstance()->getInvoiceByHash($invoiceHash);
+    $document = InvoiceUtils::getElectronicInvoice($Invoice, $profileMap[$type]);
+    $xmlContent = $document->getContent();
+    $fileName = InvoiceUtils::getInvoiceFilename($Invoice);
+
+    $response = new Response($xmlContent);
+    $response->headers->set('Content-Type', 'application/xml');
+    $response->headers->set('Content-Disposition', 'attachment; filename="'. $fileName . '.xml"');
+    $response->headers->set('Content-Length', strlen($xmlContent));
+    $response->send();
+} catch (Exception $e) {
+}
diff --git a/bin/backend/utils/Dialogs.css b/bin/backend/utils/Dialogs.css
index 638963a..a5c3d6a 100644
--- a/bin/backend/utils/Dialogs.css
+++ b/bin/backend/utils/Dialogs.css
@@ -1,4 +1,25 @@
 .quiqqer-invoice-dialog-refund-label input {
     float: left;
     margin-right: 10px;
-}
\ No newline at end of file
+}
+
+/** invoice download dialog
+ ============================================================================== */
+
+.quiqqer-invoice-download-dialog  {
+    text-align: center;
+}
+
+.quiqqer-invoice-download-dialog-buttons {
+    display: flex;
+    width: 100%;
+    gap: 1rem;
+    flex-wrap: wrap;
+    margin-top: 2rem;
+    justify-content: center;
+}
+
+.quiqqer-invoice-download-dialog button {
+    width: 200px;
+}
+
diff --git a/bin/backend/utils/Dialogs.js b/bin/backend/utils/Dialogs.js
index 040dc38..b6449c6 100644
--- a/bin/backend/utils/Dialogs.js
+++ b/bin/backend/utils/Dialogs.js
@@ -11,10 +11,11 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
     'Locale',
     'package/quiqqer/invoice/bin/Invoices',
     'qui/controls/windows/Confirm',
+    'qui/controls/windows/Popup',
 
     'css!package/quiqqer/invoice/bin/backend/utils/Dialogs.css'
 
-], function(QUI, QUILocale, Invoices, QUIConfirm) {
+], function (QUI, QUILocale, Invoices, QUIConfirm, QUIPopup) {
     'use strict';
 
     const lg = 'quiqqer/invoice';
@@ -28,14 +29,14 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
          * @param {String} [entityType]
          * @return {Promise}
          */
-        openPrintDialog: function(invoiceId, entityType) {
+        openPrintDialog: function (invoiceId, entityType) {
             entityType = entityType || 'Invoice';
 
-            return Invoices.getInvoiceHistory(invoiceId).then(function(comments) {
-                return new Promise(function(resolve) {
+            return Invoices.getInvoiceHistory(invoiceId).then(function (comments) {
+                return new Promise(function (resolve) {
                     require([
                         'package/quiqqer/erp/bin/backend/controls/OutputDialog'
-                    ], function(OutputDialog) {
+                    ], function (OutputDialog) {
                         new OutputDialog({
                             entityId: invoiceId,
                             entityType: entityType,
@@ -55,11 +56,11 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
          * @param {String} invoiceId - Invoice ID or Hash
          * @return {Promise}
          */
-        openStornoDialog: function(invoiceId) {
-            return Invoices.get(invoiceId).then(function(result) {
+        openStornoDialog: function (invoiceId) {
+            return Invoices.get(invoiceId).then(function (result) {
                 const id = result.id_prefix + result.id;
 
-                return new Promise(function(resolve, reject) {
+                return new Promise(function (resolve, reject) {
                     new QUIConfirm({
                         icon: 'fa fa-ban',
                         texticon: 'fa fa-ban',
@@ -80,7 +81,7 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
                         maxHeight: 500,
                         maxWidth: 750,
                         events: {
-                            onOpen: function(Win) {
+                            onOpen: function (Win) {
                                 const Container = Win.getContent().getElement('.textbody');
 
                                 // #locale
@@ -111,14 +112,14 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
                                 Reason.focus();
                             },
 
-                            onSubmit: function(Win) {
+                            onSubmit: function (Win) {
                                 const Reason = Win.getContent().getElement('[name="reason"]');
                                 const value = Reason.value;
 
                                 if (value === '') {
                                     Reason.focus();
                                     Reason.required = true;
-                                    
+
                                     if ('reportValidity' in Reason) {
                                         Reason.reportValidity();
                                     }
@@ -128,10 +129,10 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
 
                                 Win.Loader.show();
 
-                                Invoices.reversalInvoice(result.hash, value).then(function(result) {
+                                Invoices.reversalInvoice(result.hash, value).then(function (result) {
                                     Win.close();
                                     resolve(result);
-                                }).catch(function(Exception) {
+                                }).catch(function (Exception) {
                                     Win.close();
                                     reject(Exception);
                                 });
@@ -150,7 +151,7 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
          * @param {String} invoiceId - Invoice ID or Hash
          * @return {*|Promise}
          */
-        openCancellationDialog: function(invoiceId) {
+        openCancellationDialog: function (invoiceId) {
             return this.openStornoDialog(invoiceId);
         },
 
@@ -160,7 +161,7 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
          * @param {String} invoiceId - Invoice ID or Hash
          * @return {*|Promise}
          */
-        openReversalDialog: function(invoiceId) {
+        openReversalDialog: function (invoiceId) {
             return this.openStornoDialog(invoiceId);
         },
 
@@ -170,11 +171,11 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
          * @param {String} invoiceId - Invoice ID or Hash
          * @return {Promise}
          */
-        openCopyDialog: function(invoiceId) {
-            return Invoices.get(invoiceId).then(function(result) {
+        openCopyDialog: function (invoiceId) {
+            return Invoices.get(invoiceId).then(function (result) {
                 const id = result.id_prefix + result.id;
 
-                return new Promise(function(resolve) {
+                return new Promise(function (resolve) {
                     new QUIConfirm({
                         title: QUILocale.get(lg, 'dialog.invoice.copy.title'),
                         text: QUILocale.get(lg, 'dialog.invoice.copy.text'),
@@ -191,13 +192,13 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
                             textimage: 'fa fa-copy'
                         },
                         events: {
-                            onSubmit: function(Win) {
+                            onSubmit: function (Win) {
                                 Win.Loader.show();
 
-                                Invoices.copyInvoice(result.hash).then(function(newId) {
+                                Invoices.copyInvoice(result.hash).then(function (newId) {
                                     Win.close();
                                     resolve(newId);
-                                }).then(function() {
+                                }).then(function () {
                                     Win.Loader.hide();
                                 });
                             }
@@ -213,14 +214,14 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
          * @param {String} invoiceId - Invoice ID or Hash
          * @return {Promise}
          */
-        openCreateCreditNoteDialog: function(invoiceId) {
+        openCreateCreditNoteDialog: function (invoiceId) {
             const self = this;
 
-            return Invoices.get(invoiceId).then(function(result) {
+            return Invoices.get(invoiceId).then(function (result) {
                 let paymentHasRefund = false;
                 const id = result.id_prefix + result.id;
 
-                return new Promise(function(resolve, reject) {
+                return new Promise(function (resolve, reject) {
                     new QUIConfirm({
                         icon: 'fa fa-clipboard',
                         texticon: 'fa fa-clipboard',
@@ -241,10 +242,10 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
                         maxHeight: 400,
                         maxWidth: 600,
                         events: {
-                            onOpen: function(Win) {
+                            onOpen: function (Win) {
                                 Win.Loader.show();
 
-                                Invoices.hasRefund(id).then(function(hasRefund) {
+                                Invoices.hasRefund(id).then(function (hasRefund) {
                                     paymentHasRefund = hasRefund;
 
                                     QUI.fireEvent('quiqqerInvoiceCreateCreditNoteDialogOpen', [id, Win]);
@@ -274,16 +275,16 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
                                 });
                             },
 
-                            onSubmit: function(Win) {
+                            onSubmit: function (Win) {
                                 Win.Loader.show();
 
                                 const Content = Win.getContent(),
                                     Refund = Content.getElement('[name="refund"]');
 
-                                const createInvoice = function(values) {
+                                const createInvoice = function (values) {
                                     values = values || {};
 
-                                    Invoices.createCreditNote(result.hash, values).then(function(newId) {
+                                    Invoices.createCreditNote(result.hash, values).then(function (newId) {
                                         QUI.fireEvent(
                                             'quiqqerInvoiceCreateCreditNoteDialogSubmit',
                                             [newId, Win]
@@ -291,7 +292,7 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
 
                                         resolve(newId);
                                         Win.close();
-                                    }).catch(function(Err) {
+                                    }).catch(function (Err) {
                                         Win.Loader.hide();
                                         console.error(Err);
 
@@ -300,7 +301,7 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
                                 };
 
                                 if (paymentHasRefund && Refund.checked) {
-                                    self.openRefundWindow(invoiceId).then(function(RefundWindow) {
+                                    self.openRefundWindow(invoiceId).then(function (RefundWindow) {
                                         if (!RefundWindow) {
                                             Win.Loader.hide();
                                             return;
@@ -309,7 +310,7 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
                                         createInvoice({
                                             refund: RefundWindow.getValues()
                                         });
-                                    }).catch(function(Err) {
+                                    }).catch(function (Err) {
                                         Win.Loader.hide();
                                         console.error(Err);
                                     });
@@ -319,7 +320,7 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
                                 createInvoice();
                             },
 
-                            onCancel: function() {
+                            onCancel: function () {
                                 resolve(false);
                             }
                         }
@@ -333,23 +334,84 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
          * @param invoiceId
          * @return {Promise}
          */
-        openRefundWindow: function(invoiceId) {
-            return new Promise(function(resolve) {
+        openRefundWindow: function (invoiceId) {
+            return new Promise(function (resolve) {
                 require([
                     'package/quiqqer/invoice/bin/backend/controls/panels/refund/Window'
-                ], function(RefundWindow) {
+                ], function (RefundWindow) {
                     new RefundWindow({
                         invoiceId: invoiceId,
                         autoRefund: false,
                         events: {
                             onSubmit: resolve,
-                            onCancel: function() {
+                            onCancel: function () {
                                 resolve(false);
                             }
                         }
                     }).open();
                 });
             });
+        },
+
+        openDownloadDialog: function (hash) {
+            new QUIPopup({
+                icon: 'fa fa-download',
+                title: QUILocale.get(lg, 'dialog.invoice.download.title'),
+                autoclose: false,
+                maxHeight: 400,
+                maxWidth: 600,
+                buttons: false,
+                events: {
+                    onOpen: function (Win) {
+                        Win.Loader.show();
+
+                        const Content = Win.getContent();
+                        Content.classList.add('quiqqer-invoice-download-dialog');
+
+                        Content.set(
+                            'html',
+
+                            '<h3>' + QUILocale.get(lg, 'dialog.invoice.download.header') + '</h3>' +
+                            QUILocale.get(lg, 'dialog.invoice.download.text') +
+                            '<div class="quiqqer-invoice-download-dialog-buttons">' +
+                            '   <button value="PDF" class="qui-button">PDF</button>' +
+                            '   <button value="PROFILE_BASIC" class="qui-button">ZUGFeRD Basic</button>' +
+                            '   <button value="PROFILE_EN16931" class="qui-button">ZUGFeRD EN16931</button>' +
+                            '   <button value="PROFILE_EXTENDED" class="qui-button">ZUGFeRD Extended</button>' +
+                            '   <button value="PROFILE_XRECHNUNG_2_3" class="qui-button">XRechnung 2.3</button>' +
+                            '   <button value="PROFILE_XRECHNUNG_3" class="qui-button">XRechnung 3</button>' +
+                            '</div>'
+                        );
+
+                        Content.querySelectorAll('button').forEach(function (Button) {
+                            Button.addEventListener('click', function () {
+                                const id = 'download-invoice-' + hash + '-' + Button.value;
+
+                                new Element('iframe', {
+                                    src: URL_OPT_DIR + 'quiqqer/invoice/bin/backend/download.php?' + Object.toQueryString({
+                                        invoice: hash,
+                                        type: Button.value
+                                    }),
+                                    id: id,
+                                    styles: {
+                                        position: 'absolute',
+                                        top: -200,
+                                        left: -200,
+                                        width: 50,
+                                        height: 50
+                                    }
+                                }).inject(document.body);
+
+                                (function () {
+                                    document.getElements('#' + id).destroy();
+                                }).delay(1000, this);
+                            });
+                        });
+
+                        Win.Loader.hide();
+                    }
+                }
+            }).open();
         }
     };
 });
diff --git a/locale.xml b/locale.xml
index c0aeba9..483a0f6 100644
--- a/locale.xml
+++ b/locale.xml
@@ -1282,6 +1282,19 @@
             <en><![CDATA[Please enter a reason for cancellation.]]></en>
         </locale>
 
+        <locale name="dialog.invoice.download.title">
+            <de><![CDATA[Lade die Rechnung in verschiedenen Formaten herunter]]></de>
+            <en><![CDATA[Download the invoice in various formats]]></en>
+        </locale>
+        <locale name="dialog.invoice.download.header">
+            <de><![CDATA[Rechnung herunterladen]]></de>
+            <en><![CDATA[Download invoice]]></en>
+        </locale>
+        <locale name="dialog.invoice.download.text" html="true">
+            <de><![CDATA[<p>Wähle das gewünschte Format aus, um deine Rechnung herunterzuladen.</p>]]></de>
+            <en><![CDATA[<p>Select the desired format to download your invoice.</p>]]></en>
+        </locale>
+
         <locale name="dialog.create.address.title">
             <de><![CDATA[Rechnungsadresse anlegen]]></de>
             <en><![CDATA[Create invoice address]]></en>
-- 
GitLab


From 0501a860087af46d559f128576360ac9bcbf96b7 Mon Sep 17 00:00:00 2001
From: Henning <leutz@pcsg.de>
Date: Sat, 14 Dec 2024 15:50:23 +0100
Subject: [PATCH 3/4] fix: download button - locale

---
 bin/backend/controls/panels/Invoice.js | 2 +-
 locale.xml                             | 4 ++++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/bin/backend/controls/panels/Invoice.js b/bin/backend/controls/panels/Invoice.js
index 7a2d894..b31874b 100644
--- a/bin/backend/controls/panels/Invoice.js
+++ b/bin/backend/controls/panels/Invoice.js
@@ -202,7 +202,7 @@ define('package/quiqqer/invoice/bin/backend/controls/panels/Invoice', [
 
             Actions.appendChild({
                 icon: 'fa fa-download',
-                text: QUILocale.get(lg, 'erp.panel.invoice.button.download'),
+                text: QUILocale.get(lg, 'dialog.invoice.download.button'),
                 events: {
                     onClick: this.download
                 }
diff --git a/locale.xml b/locale.xml
index 483a0f6..5bf5b5c 100644
--- a/locale.xml
+++ b/locale.xml
@@ -1282,6 +1282,10 @@
             <en><![CDATA[Please enter a reason for cancellation.]]></en>
         </locale>
 
+        <locale name="dialog.invoice.download.button">
+            <de><![CDATA[Rechnung herunterladen]]></de>
+            <en><![CDATA[Download invoice]]></en>
+        </locale>
         <locale name="dialog.invoice.download.title">
             <de><![CDATA[Lade die Rechnung in verschiedenen Formaten herunter]]></de>
             <en><![CDATA[Download the invoice in various formats]]></en>
-- 
GitLab


From 7397935fd0440804711a18e6294fb4f50f2f8cce Mon Sep 17 00:00:00 2001
From: Henning <leutz@pcsg.de>
Date: Sat, 14 Dec 2024 18:33:25 +0100
Subject: [PATCH 4/4] fix: pdf download

---
 bin/backend/download.php     | 52 ++++++++++++++++++++++++------------
 bin/backend/utils/Dialogs.js |  2 +-
 2 files changed, 36 insertions(+), 18 deletions(-)

diff --git a/bin/backend/download.php b/bin/backend/download.php
index bf7c601..408f235 100644
--- a/bin/backend/download.php
+++ b/bin/backend/download.php
@@ -3,7 +3,8 @@
 use QUI\ERP\Accounting\Invoice\Handler;
 use QUI\ERP\Accounting\Invoice\Utils\Invoice as InvoiceUtils;
 use horstoeko\zugferd\ZugferdProfiles;
-use QUI\ERP\Output\Output;
+use QUI\ERP\Defaults;
+use QUI\System\Log;
 use Symfony\Component\HttpFoundation\Response;
 
 define('QUIQQER_SYSTEM', true);
@@ -43,25 +44,42 @@
 ];
 
 try {
+    $Invoice = Handler::getInstance()->getInvoiceByHash($invoiceHash);
+    $fileName = InvoiceUtils::getInvoiceFilename($Invoice);
+
     if ($type === 'PDF') {
-        $OutputDocument = Output::getDocumentPdf(
-            $invoiceHash,
-            QUI\ERP\Accounting\Invoice\Output\OutputProviderInvoice::getEntityType()
-        );
+        $defaultTemplates = Defaults::conf('output', 'default_templates');
+        $defaultTemplates = json_decode($defaultTemplates, true);
 
-        $OutputDocument->download();
-        exit;
-    }
+        $templateProvider = '';
+        $templateId = '';
 
-    $Invoice = Handler::getInstance()->getInvoiceByHash($invoiceHash);
-    $document = InvoiceUtils::getElectronicInvoice($Invoice, $profileMap[$type]);
-    $xmlContent = $document->getContent();
-    $fileName = InvoiceUtils::getInvoiceFilename($Invoice);
+        if (!empty($defaultTemplates['Invoice'])) {
+            $templateProvider = $defaultTemplates['Invoice']['provider'];
+            $templateId = $defaultTemplates['Invoice']['id'];
+        }
+
+        $Request = QUI::getRequest();
+
+        $Request->query->set('id', $invoiceHash);
+        $Request->query->set('t', 'Invoice');
+        $Request->query->set('ep', 'quiqqer/invoice');
+        $Request->query->set('tpl', $templateId);
+        $Request->query->set('tplpr', $templateProvider);
 
-    $response = new Response($xmlContent);
-    $response->headers->set('Content-Type', 'application/xml');
-    $response->headers->set('Content-Disposition', 'attachment; filename="'. $fileName . '.xml"');
-    $response->headers->set('Content-Length', strlen($xmlContent));
-    $response->send();
+        include dirname(__FILE__, 4) . '/erp/bin/output/backend/download.php';
+    } else {
+        $document = InvoiceUtils::getElectronicInvoice($Invoice, $profileMap[$type]);
+        $content = $document->getContent();
+        $contentType = 'application/xml';
+        $fileName .= '.xml';
+
+        $response = new Response($content);
+        $response->headers->set('Content-Type', $contentType);
+        $response->headers->set('Content-Disposition', 'attachment; filename="' . $fileName . '"');
+        $response->headers->set('Content-Length', strlen($content));
+        $response->send();
+    }
 } catch (Exception $e) {
+    Log::writeException($e);
 }
diff --git a/bin/backend/utils/Dialogs.js b/bin/backend/utils/Dialogs.js
index b6449c6..96e06de 100644
--- a/bin/backend/utils/Dialogs.js
+++ b/bin/backend/utils/Dialogs.js
@@ -403,7 +403,7 @@ define('package/quiqqer/invoice/bin/backend/utils/Dialogs', [
                                 }).inject(document.body);
 
                                 (function () {
-                                    document.getElements('#' + id).destroy();
+                                    //document.getElements('#' + id).destroy();
                                 }).delay(1000, this);
                             });
                         });
-- 
GitLab