From 6e33cc709674fcd28ea111060c216940480066c1 Mon Sep 17 00:00:00 2001 From: data Date: Thu, 12 Feb 2026 15:36:15 +0100 Subject: [PATCH] Version 2.0 - ZUGFeRD PDF-Einbettung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ZUGFeRD/Factur-X XML-Generierung (EN16931) - XRechnung 3.0 Unterstützung - PDF-Einbettung (echtes ZUGFeRD-PDF) - Option XML nach Einbettung zu löschen - ODT-Template Unterstützung - E-Mail Anhang Funktion - XML-Vorschau Co-Authored-By: Claude Opus 4.5 --- .gitignore | 16 + CHANGELOG.md | 34 ++ README.md | 106 ++++ admin/about.php | 105 ++++ admin/setup.php | 239 ++++++++ class/actions_exportzugferd.class.php | 461 +++++++++++++++ class/pdfembedder.class.php | 327 +++++++++++ class/zugferdgenerator.class.php | 745 ++++++++++++++++++++++++ core/modules/modExportZugferd.class.php | 226 +++++++ download.php | 152 +++++ img/object_exportzugferd.svg | 17 + index.php | 155 +++++ langs/de_DE/exportzugferd.lang | 78 +++ langs/en_US/exportzugferd.lang | 51 ++ lib/exportzugferd.lib.php | 53 ++ preview.php | 404 +++++++++++++ 16 files changed, 3169 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100755 admin/about.php create mode 100755 admin/setup.php create mode 100755 class/actions_exportzugferd.class.php create mode 100644 class/pdfembedder.class.php create mode 100755 class/zugferdgenerator.class.php create mode 100755 core/modules/modExportZugferd.class.php create mode 100755 download.php create mode 100644 img/object_exportzugferd.svg create mode 100755 index.php create mode 100755 langs/de_DE/exportzugferd.lang create mode 100755 langs/en_US/exportzugferd.lang create mode 100755 lib/exportzugferd.lib.php create mode 100644 preview.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed86ca1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Temp files +*.tmp +*~ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..afaa317 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert. + +## [2.0] - 2026-02-12 + +### Neu +- PDF-Einbettung: XML wird direkt ins PDF eingebettet (echtes ZUGFeRD-PDF) +- Option "XML nach Einbettung löschen" für saubere Verzeichnisse +- Unterstützung für ODT-Templates mit automatischer PDF-Konvertierung +- Kompakte Mini-Buttons in der Rechnungsansicht +- XML-Vorschau-Funktion +- Automatisches Anhängen der XML an E-Mails + +### Verbessert +- Korrekte ZUGFeRD-Einbettung (nur EmbeddedFiles, keine FileAttachment-Annotation) +- TCPDI/TCPDF Kompatibilität verbessert +- Bessere Fehlerbehandlung und Logging + +### Behoben +- Doppelte XML-Anzeige bei pdfdetach behoben +- TCPDF/TCPDI Ladereihenfolge korrigiert +- setPDFA() Methodenprüfung für TCPDI + +## [1.0] - 2026-01-15 + +### Neu +- Initiale Version +- ZUGFeRD 2.1 / Factur-X XML-Generierung +- XRechnung 3.0 Unterstützung +- EN16931 Konformität +- Automatische Generierung bei PDF-Erstellung +- Manuelle Generierung über Rechnungsansicht +- Download-Funktion für XML-Dateien diff --git a/README.md b/README.md new file mode 100644 index 0000000..1dc5422 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# ExportZugferd - Dolibarr Modul + +Dolibarr-Modul zum Export von Kundenrechnungen im ZUGFeRD/Factur-X und XRechnung Format. + +## Beschreibung + +Dieses Modul ermöglicht den automatischen Export von Kundenrechnungen im standardisierten elektronischen Rechnungsformat: + +- **ZUGFeRD 2.1 / Factur-X** - Hybrides PDF/XML-Format +- **XRechnung 3.0** - Deutscher Standard für Behördenrechnungen +- **EN16931** - Europäische Norm für elektronische Rechnungen + +## Funktionen + +- Automatische XML-Generierung bei PDF-Erstellung +- XML-Einbettung in PDF (echtes ZUGFeRD-PDF) +- Manuelle XML-Generierung über Rechnungsansicht +- Vorschau der generierten XML +- Automatisches Anhängen an E-Mails +- Unterstützung für ODT-Templates mit PDF-Konvertierung + +## Installation + +1. Modul-Ordner nach `htdocs/custom/exportzugferd` kopieren +2. In Dolibarr: Einstellungen > Module > ExportZugferd aktivieren +3. Modul-Einstellungen konfigurieren + +## Konfiguration + +### Einstellungen (Einstellungen > Module > ExportZugferd > Einstellungen) + +| Option | Beschreibung | +|--------|--------------| +| **Profil** | ZUGFeRD-Profil (EN16931 empfohlen) | +| **Auto-Generierung** | XML automatisch bei PDF-Erstellung generieren | +| **In PDF einbetten** | XML in das PDF einbetten (ZUGFeRD-konform) | +| **XML nach Einbettung löschen** | Separate XML-Datei nach Einbettung entfernen | +| **An E-Mail anhängen** | XML automatisch an Rechnungs-E-Mails anhängen | + +### Unterstützte Profile + +- **MINIMUM** - Minimale Rechnungsdaten (nur Archivierung) +- **BASIC** - Grundlegende strukturierte Rechnungsdaten +- **EN16931** - Vollständige EU-konforme Rechnung (empfohlen) +- **XRECHNUNG** - Deutsche Behördenrechnung + +## Systemvoraussetzungen + +- Dolibarr >= 15.0 +- PHP >= 7.4 +- TCPDF (in Dolibarr enthalten) +- TCPDI (in Dolibarr enthalten) +- LibreOffice (für ODT zu PDF Konvertierung) + +## Verwendung + +### Automatische Generierung + +Wenn "Auto-Generierung" aktiviert ist, wird bei jeder PDF-Erstellung einer Kundenrechnung automatisch die ZUGFeRD-XML generiert und optional ins PDF eingebettet. + +### Manuelle Generierung + +In der Rechnungsansicht wird eine ZUGFeRD-Zeile angezeigt mit folgenden Aktionen: +- XML generieren/neu generieren +- XML herunterladen +- XML-Vorschau anzeigen + +## Technische Details + +### Generierte XML-Struktur + +Das Modul generiert CrossIndustryInvoice-XML nach dem UN/CEFACT CII Standard: + +```xml + + ... + ... + ... + +``` + +### Dateiablage + +- XML-Dateien: `documents/facture/{ref}/factur-x.xml` +- XRechnung: `documents/facture/{ref}/xrechnung-{ref}.xml` + +### Hooks + +Das Modul nutzt folgende Dolibarr-Hooks: +- `afterPDFCreation` - Nach PDF-Generierung +- `afterODTCreation` - Nach ODT-Generierung (mit PDF-Konvertierung) +- `formObjectOptions` - Anzeige in Rechnungsansicht + +## Lizenz + +GPL v3 - siehe [LICENSE](LICENSE) + +## Autor + +**Data IT Solution** +- E-Mail: data@data-it-solution.de +- Web: https://data-it-solution.de + +## Changelog + +Siehe [CHANGELOG.md](CHANGELOG.md) diff --git a/admin/about.php b/admin/about.php new file mode 100755 index 0000000..4d7e048 --- /dev/null +++ b/admin/about.php @@ -0,0 +1,105 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file admin/about.php + * \ingroup exportzugferd + * \brief About page for ExportZugferd module + */ + +// Load Dolibarr environment +$res = 0; +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +if (!$res && file_exists("../main.inc.php")) { + $res = @include "../main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} +require_once DOL_DOCUMENT_ROOT . '/core/lib/admin.lib.php'; +require_once __DIR__ . '/../lib/exportzugferd.lib.php'; + +// Security check +if (!$user->admin) { + accessforbidden(); +} + +$langs->loadLangs(array('admin', 'exportzugferd@exportzugferd')); + +/* + * View + */ + +$page_name = 'ExportZugferdAbout'; +llxHeader('', $langs->trans($page_name), '', '', 0, 0, '', '', '', 'mod-exportzugferd page-admin-about'); + +// Subheader +$linkback = '' . $langs->trans('BackToModuleList') . ''; +print load_fiche_titre($langs->trans('ExportZugferdSetup'), $linkback, 'title_setup'); + +// Configuration header +$head = exportzugferd_admin_prepare_head(); +print dol_get_fiche_head($head, 'about', $langs->trans('ExportZugferd'), -1, 'fa-file-export'); + +print '
'; + +print '

' . $langs->trans('AboutModule') . '

'; + +print '

' . $langs->trans('ExportZugferdAboutDesc') . '

'; + +print '

' . $langs->trans('Features') . '

'; +print '
    '; +print '
  • ' . $langs->trans('FeatureGenerateZugferd') . '
  • '; +print '
  • ' . $langs->trans('FeatureXRechnung') . '
  • '; +print '
  • ' . $langs->trans('FeatureEN16931') . '
  • '; +print '
  • ' . $langs->trans('FeatureAutoGenerate') . '
  • '; +print '
'; + +print '

' . $langs->trans('Version') . '

'; +print '

1.0

'; + +print '

' . $langs->trans('Author') . '

'; +print '

Eduard Wisch - Data IT Solution

'; + +print '
'; + +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/admin/setup.php b/admin/setup.php new file mode 100755 index 0000000..a0b34a7 --- /dev/null +++ b/admin/setup.php @@ -0,0 +1,239 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file admin/setup.php + * \ingroup exportzugferd + * \brief ExportZugferd setup page + */ + +// Load Dolibarr environment +$res = 0; +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +if (!$res && file_exists("../main.inc.php")) { + $res = @include "../main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} +require_once DOL_DOCUMENT_ROOT . '/core/lib/admin.lib.php'; +require_once __DIR__ . '/../lib/exportzugferd.lib.php'; + +// Security check +if (!$user->admin) { + accessforbidden(); +} + +$langs->loadLangs(array('admin', 'exportzugferd@exportzugferd')); + +$action = GETPOST('action', 'aZ09'); + +// Available profiles +$profiles = array( + 'MINIMUM' => 'ZUGFeRD Minimum', + 'BASIC WL' => 'ZUGFeRD Basic WL', + 'BASIC' => 'ZUGFeRD Basic', + 'EN16931' => 'ZUGFeRD EN16931 (Comfort)', + 'EXTENDED' => 'ZUGFeRD Extended', + 'XRECHNUNG' => 'XRechnung 3.0', +); + +/* + * Actions + */ + +if ($action == 'update') { + $error = 0; + + $profile = GETPOST('EXPORTZUGFERD_PROFILE', 'alpha'); + $autoGenerate = GETPOSTINT('EXPORTZUGFERD_AUTO_GENERATE'); + $embedPdf = GETPOSTINT('EXPORTZUGFERD_EMBED_IN_PDF'); + $deleteXmlAfterEmbed = GETPOSTINT('EXPORTZUGFERD_DELETE_XML_AFTER_EMBED'); + $attachToEmail = GETPOSTINT('EXPORTZUGFERD_ATTACH_TO_EMAIL'); + + if (!$error) { + $result = dolibarr_set_const($db, 'EXPORTZUGFERD_PROFILE', $profile, 'chaine', 0, '', $conf->entity); + if ($result < 0) { + $error++; + } + } + + if (!$error) { + $result = dolibarr_set_const($db, 'EXPORTZUGFERD_AUTO_GENERATE', $autoGenerate, 'chaine', 0, '', $conf->entity); + if ($result < 0) { + $error++; + } + } + + if (!$error) { + $result = dolibarr_set_const($db, 'EXPORTZUGFERD_EMBED_IN_PDF', $embedPdf, 'chaine', 0, '', $conf->entity); + if ($result < 0) { + $error++; + } + } + + if (!$error) { + $result = dolibarr_set_const($db, 'EXPORTZUGFERD_DELETE_XML_AFTER_EMBED', $deleteXmlAfterEmbed, 'chaine', 0, '', $conf->entity); + if ($result < 0) { + $error++; + } + } + + if (!$error) { + $result = dolibarr_set_const($db, 'EXPORTZUGFERD_ATTACH_TO_EMAIL', $attachToEmail, 'chaine', 0, '', $conf->entity); + if ($result < 0) { + $error++; + } + } + + if (!$error) { + setEventMessages($langs->trans('SetupSaved'), null, 'mesgs'); + } else { + setEventMessages($langs->trans('Error'), null, 'errors'); + } +} + +/* + * View + */ + +$page_name = 'ExportZugferdSetup'; +llxHeader('', $langs->trans($page_name), '', '', 0, 0, '', '', '', 'mod-exportzugferd page-admin-setup'); + +// Subheader +$linkback = '' . $langs->trans('BackToModuleList') . ''; +print load_fiche_titre($langs->trans($page_name), $linkback, 'title_setup'); + +// Configuration header +$head = exportzugferd_admin_prepare_head(); +print dol_get_fiche_head($head, 'settings', $langs->trans('ExportZugferd'), -1, 'fa-file-export'); + +print '
'; +print ''; +print ''; + +print ''; + +// Profile selection +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +// Auto-generate +print ''; +print ''; +print ''; +print ''; + +// Embed in PDF +print ''; +print ''; +print ''; +print ''; + +// Delete XML after embedding +print ''; +print ''; +print ''; +print ''; + +// Attach to Email +print ''; +print ''; +print ''; +print ''; + +print '
' . $langs->trans('GeneralSettings') . '
' . $langs->trans('ZugferdProfile') . ''; +print ''; +print '
' . $langs->trans('AutoGenerateZugferd') . ''; +print $form->selectyesno('EXPORTZUGFERD_AUTO_GENERATE', getDolGlobalInt('EXPORTZUGFERD_AUTO_GENERATE', 0), 1); +print ' - ' . $langs->trans('AutoGenerateZugferdDesc') . ''; +print '
' . $langs->trans('EmbedXMLInPDF') . ''; +print $form->selectyesno('EXPORTZUGFERD_EMBED_IN_PDF', getDolGlobalInt('EXPORTZUGFERD_EMBED_IN_PDF', 0), 1); +print ' - ' . $langs->trans('EmbedXMLInPDFDesc') . ''; +print '
' . $langs->trans('DeleteXMLAfterEmbed') . ''; +print $form->selectyesno('EXPORTZUGFERD_DELETE_XML_AFTER_EMBED', getDolGlobalInt('EXPORTZUGFERD_DELETE_XML_AFTER_EMBED', 0), 1); +print ' - ' . $langs->trans('DeleteXMLAfterEmbedDesc') . ''; +print '
' . $langs->trans('AttachZugferdToEmail') . ''; +print $form->selectyesno('EXPORTZUGFERD_ATTACH_TO_EMAIL', getDolGlobalInt('EXPORTZUGFERD_ATTACH_TO_EMAIL', 0), 1); +print ' - ' . $langs->trans('AttachZugferdToEmailDesc') . ''; +print '
'; + +print '
'; +print '
'; +print ''; +print '
'; + +print '
'; + +// Info section +print '
'; +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print '
' . $langs->trans('Information') . '
' . $langs->trans('SupportedProfiles') . ''; +print '
    '; +print '
  • ZUGFeRD Minimum: ' . $langs->trans('ProfileMinimumDesc') . '
  • '; +print '
  • ZUGFeRD Basic: ' . $langs->trans('ProfileBasicDesc') . '
  • '; +print '
  • ZUGFeRD EN16931: ' . $langs->trans('ProfileEN16931Desc') . '
  • '; +print '
  • XRechnung: ' . $langs->trans('ProfileXRechnungDesc') . '
  • '; +print '
'; +print '
'; + +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/class/actions_exportzugferd.class.php b/class/actions_exportzugferd.class.php new file mode 100755 index 0000000..b55cbb4 --- /dev/null +++ b/class/actions_exportzugferd.class.php @@ -0,0 +1,461 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file class/actions_exportzugferd.class.php + * \ingroup exportzugferd + * \brief Hook class for ZUGFeRD/XRechnung export + */ + +/** + * Class ActionsExportzugferd + * Handles hooks for ZUGFeRD export functionality + */ +class ActionsExportzugferd +{ + /** + * @var DoliDB Database handler + */ + private $db; + + /** + * @var string[] Errors + */ + public $errors = array(); + + /** + * @var string Error message + */ + public $error = ''; + + /** + * @var string Result for hooks + */ + public $resprints = ''; + + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + $this->db = $db; + } + + /** + * Hook executed after PDF/ODT creation + * Creates ZUGFeRD XML file alongside the document and optionally embeds it in the PDF + * + * @param array $parameters Hook parameters + * @param object $object Object (not the invoice!) + * @param string $action Action + * @return int 0=OK, <0=Error + */ + public function afterPDFCreation($parameters, &$object, &$action) + { + global $conf, $langs; + + dol_syslog("ExportZugferd Hook: afterPDFCreation called", LOG_INFO); + + // Get the PDF file path from parameters + $pdfFile = isset($parameters['file']) ? $parameters['file'] : ''; + + if (empty($pdfFile)) { + dol_syslog("ExportZugferd Hook: No file in parameters", LOG_INFO); + return 0; + } + + dol_syslog("ExportZugferd Hook: PDF file = " . $pdfFile, LOG_INFO); + + // Get the invoice from parameters + $invoice = isset($parameters['object']) ? $parameters['object'] : null; + + // Load Facture class if needed + if (!class_exists('Facture')) { + require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php'; + } + + // Check if it's a customer invoice + if (!is_object($invoice)) { + dol_syslog("ExportZugferd Hook: parameters[object] is not an object", LOG_INFO); + return 0; + } + + dol_syslog("ExportZugferd Hook: Object class = " . get_class($invoice), LOG_INFO); + + if (!($invoice instanceof Facture)) { + dol_syslog("ExportZugferd Hook: Not a customer invoice (Facture), skipping. Class: " . get_class($invoice), LOG_INFO); + return 0; + } + + // Check if auto-generation is enabled + $autoGenerate = getDolGlobalInt('EXPORTZUGFERD_AUTO_GENERATE'); + dol_syslog("ExportZugferd Hook: EXPORTZUGFERD_AUTO_GENERATE = " . $autoGenerate, LOG_INFO); + + if (!$autoGenerate) { + dol_syslog("ExportZugferd Hook: Auto-generation disabled", LOG_INFO); + return 0; + } + + dol_syslog("ExportZugferd Hook: Generating ZUGFeRD XML for invoice " . $invoice->ref, LOG_INFO); + + // Generate ZUGFeRD XML and optionally embed into PDF + return $this->generateZugferdXML($invoice, $pdfFile); + } + + /** + * Hook executed after ODT creation + * + * @param array $parameters Hook parameters + * @param object $object Object + * @param string $action Action + * @return int 0=OK, <0=Error + */ + public function afterODTCreation($parameters, &$object, &$action) + { + global $conf; + + dol_syslog("ExportZugferd Hook: afterODTCreation called", LOG_INFO); + + // Get the invoice from parameters + $invoice = isset($parameters['object']) ? $parameters['object'] : null; + + // Load Facture class if needed + if (!class_exists('Facture')) { + require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php'; + } + + // Check if it's a customer invoice + if (!is_object($invoice) || !($invoice instanceof Facture)) { + dol_syslog("ExportZugferd Hook: afterODTCreation - Not a Facture object", LOG_DEBUG); + return 0; + } + + // Check if auto-generation is enabled + if (!getDolGlobalInt('EXPORTZUGFERD_AUTO_GENERATE')) { + dol_syslog("ExportZugferd Hook: afterODTCreation - Auto-generation disabled", LOG_DEBUG); + return 0; + } + + // Get the file path from parameters (ODT or PDF depending on MAIN_ODT_AS_PDF setting) + $file = isset($parameters['file']) ? $parameters['file'] : ''; + dol_syslog("ExportZugferd Hook: afterODTCreation - file parameter = " . $file, LOG_INFO); + + // Find PDF file - either the passed file is already PDF, or we need to find it in the directory + $pdfFile = ''; + if (!empty($file) && pathinfo($file, PATHINFO_EXTENSION) === 'pdf') { + $pdfFile = $file; + } else { + // Search for PDF in invoice directory + $dir = $conf->facture->dir_output . '/' . dol_sanitizeFileName($invoice->ref); + if (is_dir($dir)) { + $files = glob($dir . '/*.pdf'); + if (!empty($files)) { + // Get the most recently modified PDF + usort($files, function($a, $b) { + return filemtime($b) - filemtime($a); + }); + $pdfFile = $files[0]; + } + } + } + + dol_syslog("ExportZugferd Hook: afterODTCreation - PDF file = " . $pdfFile, LOG_INFO); + + // Generate ZUGFeRD XML and embed into PDF + return $this->generateZugferdXML($invoice, $pdfFile); + } + + /** + * Generate ZUGFeRD XML for an invoice and optionally embed it into PDF + * + * @param Facture $invoice Invoice object + * @param string $pdfFile Optional path to PDF file for embedding + * @return int 0=OK, <0=Error + */ + private function generateZugferdXML($invoice, $pdfFile = '') + { + global $conf; + + require_once __DIR__ . '/zugferdgenerator.class.php'; + + $generator = new ZugferdGenerator($this->db); + + // Generate XML + $xml = $generator->generateFromInvoice($invoice); + + if ($xml === false) { + $this->error = $generator->error; + $this->errors = $generator->errors; + dol_syslog("ExportZugferd: Error generating XML: " . $this->error, LOG_ERR); + return -1; + } + + // Save XML file + $filepath = $generator->saveXML($invoice, $xml); + + if ($filepath === false) { + $this->error = $generator->error; + dol_syslog("ExportZugferd: Error saving XML: " . $this->error, LOG_ERR); + return -1; + } + + dol_syslog("ExportZugferd: XML generated successfully: " . $filepath, LOG_INFO); + + // Embed XML into PDF if enabled and PDF file exists + $embedEnabled = getDolGlobalInt('EXPORTZUGFERD_EMBED_IN_PDF'); + dol_syslog("ExportZugferd: EXPORTZUGFERD_EMBED_IN_PDF = " . $embedEnabled . ", pdfFile = " . $pdfFile, LOG_INFO); + + if ($embedEnabled && !empty($pdfFile) && file_exists($pdfFile)) { + dol_syslog("ExportZugferd: Starting PDF embedding...", LOG_INFO); + $result = $this->embedXmlInPdf($pdfFile, $filepath); + if ($result < 0) { + dol_syslog("ExportZugferd: Warning - Could not embed XML in PDF: " . $this->error, LOG_WARNING); + // Don't return error, XML file was created successfully + } else { + dol_syslog("ExportZugferd: XML successfully embedded in PDF", LOG_INFO); + // Delete XML file after embedding if option is enabled + if (getDolGlobalInt('EXPORTZUGFERD_DELETE_XML_AFTER_EMBED')) { + @unlink($filepath); + dol_syslog("ExportZugferd: XML file deleted after embedding: " . $filepath, LOG_INFO); + } + } + } elseif ($embedEnabled && empty($pdfFile)) { + dol_syslog("ExportZugferd: Embedding enabled but no PDF file path provided", LOG_WARNING); + } elseif ($embedEnabled && !file_exists($pdfFile)) { + dol_syslog("ExportZugferd: Embedding enabled but PDF file does not exist: " . $pdfFile, LOG_WARNING); + } + + return 0; + } + + /** + * Embed XML file into PDF + * + * @param string $pdfFile Path to PDF file + * @param string $xmlFile Path to XML file + * @return int 0=OK, <0=Error + */ + private function embedXmlInPdf($pdfFile, $xmlFile) + { + require_once __DIR__ . '/pdfembedder.class.php'; + + $embedder = new ZugferdPdfEmbedder(); + + $profile = getDolGlobalString('EXPORTZUGFERD_PROFILE', 'EN16931'); + + if (!$embedder->embedXmlInPdf($pdfFile, $xmlFile, $profile)) { + $this->error = $embedder->error; + $this->errors = $embedder->errors; + return -1; + } + + dol_syslog("ExportZugferd: XML embedded into PDF successfully: " . $pdfFile, LOG_INFO); + return 0; + } + + /** + * Hook to add buttons/actions on invoice card + * Now disabled - buttons are shown in document area via formObjectOptions + * + * @param array $parameters Hook parameters + * @param object $object Invoice object + * @param string $action Action + * @return int 0=OK + */ + public function addMoreActionsButtons($parameters, &$object, &$action) + { + // Buttons are now shown in the document area (formObjectOptions) to reduce clutter + return 0; + } + + /** + * Hook to add ZUGFeRD info and mini buttons in invoice info area + * + * @param array $parameters Hook parameters + * @param object $object Object + * @param string $action Action + * @return int 0=OK + */ + public function formObjectOptions($parameters, &$object, &$action) + { + global $conf, $langs, $user; + + // Only for invoice card context + $contexts = explode(':', $parameters['context']); + if (!in_array('invoicecard', $contexts)) { + return 0; + } + + // Check if it's a Facture + if (!($object instanceof Facture) || empty($object->ref)) { + return 0; + } + + // Check permissions + $hasRight = $user->hasRight('exportzugferd', 'read') || $user->hasRight('exportzugferd', 'export') || $user->hasRight('facture', 'lire'); + if (!$hasRight) { + return 0; + } + + $langs->load('exportzugferd@exportzugferd'); + + $dir = $conf->facture->dir_output . '/' . dol_sanitizeFileName($object->ref); + $zugferdFile = $dir . '/factur-x.xml'; + $xrechnungFile = $dir . '/xrechnung-' . $object->ref . '.xml'; + + $xmlFile = null; + $xmlExists = false; + if (file_exists($zugferdFile)) { + $xmlFile = $zugferdFile; + $xmlExists = true; + } elseif (file_exists($xrechnungFile)) { + $xmlFile = $xrechnungFile; + $xmlExists = true; + } + + // Build the ZUGFeRD row with mini action buttons + print ''; + print 'ZUGFeRD/XRechnung'; + print ''; + + if ($xmlExists) { + // XML exists - show file info and action buttons + print ''; + print ''; + print basename($xmlFile); + print ''; + print ' (' . dol_print_size(filesize($xmlFile)) . ')'; + + // Mini buttons + print '   '; + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + } else { + // XML does not exist - show generate button + print ''; + print '' . $langs->trans('NoZugferdXML') . ''; + + // Generate button + print '   '; + print ''; + print ' '; + print '' . $langs->trans('GenerateZugferdXML') . ''; + print ''; + } + + print ''; + print ''; + + return 0; + } + + /** + * Hook to add ZUGFeRD XML to email attachments (getFormMail hook) + * + * @param array $parameters Hook parameters + * @param object $object FormMail object + * @param string $action Action + * @return int 0=OK + */ + public function getFormMail($parameters, &$object, &$action) + { + global $conf; + + // Check if auto-attach is enabled + if (!getDolGlobalInt('EXPORTZUGFERD_ATTACH_TO_EMAIL')) { + return 0; + } + + // Check if this is for an invoice (facture trackid) + $trackid = isset($parameters['trackid']) ? $parameters['trackid'] : ''; + if (strpos($trackid, 'inv') !== 0) { + return 0; + } + + // Get invoice ID from parameters + $invoiceId = isset($object->param['id']) ? $object->param['id'] : 0; + if (empty($invoiceId)) { + return 0; + } + + // Load invoice to get ref + require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php'; + $invoice = new Facture($this->db); + if ($invoice->fetch($invoiceId) <= 0) { + return 0; + } + + // Find ZUGFeRD XML file + $xmlFile = $this->getZugferdXmlPath($invoice); + + if ($xmlFile && file_exists($xmlFile)) { + // Add XML file to fileinit array + if (!isset($object->param['fileinit'])) { + $object->param['fileinit'] = array(); + } + if (!is_array($object->param['fileinit'])) { + $object->param['fileinit'] = array($object->param['fileinit']); + } + + // Only add if not already in the list + if (!in_array($xmlFile, $object->param['fileinit'])) { + $object->param['fileinit'][] = $xmlFile; + dol_syslog("ExportZugferd: Added ZUGFeRD XML to email attachments: " . $xmlFile, LOG_DEBUG); + } + } + + return 0; + } + + /** + * Get the path to the ZUGFeRD XML file for an invoice + * + * @param Facture $invoice Invoice object + * @return string|null Path to XML file or null if not found + */ + private function getZugferdXmlPath($invoice) + { + global $conf; + + if (empty($invoice->ref)) { + return null; + } + + $dir = $conf->facture->dir_output . '/' . dol_sanitizeFileName($invoice->ref); + $zugferdFile = $dir . '/factur-x.xml'; + $xrechnungFile = $dir . '/xrechnung-' . $invoice->ref . '.xml'; + + if (file_exists($zugferdFile)) { + return $zugferdFile; + } elseif (file_exists($xrechnungFile)) { + return $xrechnungFile; + } + + return null; + } +} diff --git a/class/pdfembedder.class.php b/class/pdfembedder.class.php new file mode 100644 index 0000000..26e8355 --- /dev/null +++ b/class/pdfembedder.class.php @@ -0,0 +1,327 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file class/pdfembedder.class.php + * \ingroup exportzugferd + * \brief Embeds ZUGFeRD/Factur-X XML into PDF + */ + +/** + * Class ZugferdPdfEmbedder + * Embeds ZUGFeRD XML into existing PDF files to create PDF/A-3 compliant ZUGFeRD PDFs + */ +class ZugferdPdfEmbedder +{ + /** + * @var string Last error message + */ + public $error = ''; + + /** + * @var string[] Error messages + */ + public $errors = array(); + + /** + * Embed XML file into existing PDF + * + * @param string $pdfFile Path to the PDF file + * @param string $xmlFile Path to the XML file to embed + * @param string $profile ZUGFeRD profile (MINIMUM, BASIC, EN16931, XRECHNUNG, EXTENDED) + * @return bool True on success, false on error + */ + public function embedXmlInPdf($pdfFile, $xmlFile, $profile = 'EN16931') + { + global $conf; + + dol_syslog("ZugferdPdfEmbedder::embedXmlInPdf called - PDF: $pdfFile, XML: $xmlFile", LOG_INFO); + + if (!file_exists($pdfFile)) { + $this->error = 'PDF file not found: ' . $pdfFile; + dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR); + return false; + } + + if (!file_exists($xmlFile)) { + $this->error = 'XML file not found: ' . $xmlFile; + dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR); + return false; + } + + // Load TCPDF first (required by TCPDI) + if (!class_exists('TCPDF')) { + $tcpdfPath = DOL_DOCUMENT_ROOT . '/includes/tecnickcom/tcpdf/tcpdf.php'; + if (file_exists($tcpdfPath)) { + require_once $tcpdfPath; + } else { + $this->error = 'TCPDF library not found'; + dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR); + return false; + } + } + + // Check if TCPDI is available + if (!class_exists('TCPDI')) { + $tcpdiPath = DOL_DOCUMENT_ROOT . '/includes/tcpdi/tcpdi.php'; + if (file_exists($tcpdiPath)) { + require_once $tcpdiPath; + } else { + $this->error = 'TCPDI library not found'; + dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR); + return false; + } + } + + try { + // Read XML content + $xmlContent = file_get_contents($xmlFile); + if ($xmlContent === false) { + $this->error = 'Could not read XML file'; + dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR); + return false; + } + + dol_syslog("ZugferdPdfEmbedder: Creating TCPDI object", LOG_DEBUG); + + // Create new PDF with embedded XML + $pdf = new TCPDI('P', 'mm', 'A4', true, 'UTF-8'); + dol_syslog("ZugferdPdfEmbedder: TCPDI object created", LOG_DEBUG); + + // Disable header and footer + $pdf->setPrintHeader(false); + $pdf->setPrintFooter(false); + + // Set PDF/A-3 mode for ZUGFeRD compliance (only if method exists - TCPDI may not have it) + if (method_exists($pdf, 'setPDFA')) { + $pdf->setPDFA(true); + } + + // Set document information + $pdf->SetCreator('Dolibarr ExportZugferd'); + $pdf->SetAuthor('Dolibarr ERP/CRM'); + $pdf->SetTitle('ZUGFeRD Invoice'); + $pdf->SetSubject('Electronic Invoice with embedded ZUGFeRD XML'); + + dol_syslog("ZugferdPdfEmbedder: Setting source file: $pdfFile", LOG_DEBUG); + + // Import existing PDF pages + $pageCount = $pdf->setSourceFile($pdfFile); + dol_syslog("ZugferdPdfEmbedder: Importing $pageCount pages", LOG_DEBUG); + + for ($pageNo = 1; $pageNo <= $pageCount; $pageNo++) { + dol_syslog("ZugferdPdfEmbedder: Importing page $pageNo", LOG_DEBUG); + $templateId = $pdf->importPage($pageNo); + $size = $pdf->getTemplateSize($templateId); + + // TCPDI returns 'w' and 'h' keys, not 'width' and 'height' + $width = isset($size['w']) ? $size['w'] : (isset($size['width']) ? $size['width'] : 210); + $height = isset($size['h']) ? $size['h'] : (isset($size['height']) ? $size['height'] : 297); + $orientation = isset($size['orientation']) ? $size['orientation'] : ($width > $height ? 'L' : 'P'); + + dol_syslog("ZugferdPdfEmbedder: Page $pageNo size: {$width}x{$height}, orientation: $orientation", LOG_DEBUG); + + // Add page with same size as original + $pdf->AddPage($orientation, array($width, $height)); + $pdf->useTemplate($templateId); + } + + dol_syslog("ZugferdPdfEmbedder: All pages imported", LOG_DEBUG); + + // Determine the attachment filename based on profile + $attachmentName = $this->getAttachmentFilename($profile); + + dol_syslog("ZugferdPdfEmbedder: Adding embedded file for $attachmentName", LOG_DEBUG); + + // Embed XML using the correct ZUGFeRD method: only in EmbeddedFiles name tree + // NOT as a FileAttachment annotation (which causes double listing) + $this->addEmbeddedFile($pdf, $xmlFile, $attachmentName); + + dol_syslog("ZugferdPdfEmbedder: Embedded file added", LOG_DEBUG); + + // Create temporary file for output + $tempFile = $pdfFile . '.tmp'; + + // Output the modified PDF + $pdf->Output($tempFile, 'F'); + + dol_syslog("ZugferdPdfEmbedder: Temp file created: $tempFile", LOG_DEBUG); + + // Replace original with modified version + if (file_exists($tempFile)) { + if (filesize($tempFile) > 0) { + // Replace original + if (!rename($tempFile, $pdfFile)) { + $this->error = 'Could not replace original PDF file'; + @unlink($tempFile); + dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR); + return false; + } + + dol_syslog("ZugferdPdfEmbedder: Successfully embedded XML into PDF: " . $pdfFile, LOG_INFO); + return true; + } else { + $this->error = 'Generated PDF is empty'; + @unlink($tempFile); + dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR); + return false; + } + } else { + $this->error = 'Failed to create temporary PDF file'; + dol_syslog("ZugferdPdfEmbedder: " . $this->error, LOG_ERR); + return false; + } + + } catch (Exception $e) { + $this->error = 'PDF embedding failed: ' . $e->getMessage(); + dol_syslog("ZugferdPdfEmbedder error: " . $e->getMessage(), LOG_ERR); + return false; + } + } + + /** + * Get the standard attachment filename for the profile + * + * @param string $profile ZUGFeRD profile + * @return string Filename + */ + private function getAttachmentFilename($profile) + { + switch (strtoupper($profile)) { + case 'XRECHNUNG': + return 'xrechnung.xml'; + case 'MINIMUM': + case 'BASIC': + case 'BASIC WL': + case 'EN16931': + case 'EXTENDED': + default: + return 'factur-x.xml'; + } + } + + /** + * Embed XML content directly into PDF (alternative method) + * + * @param string $pdfFile Path to the PDF file + * @param string $xmlContent XML content to embed + * @param string $xmlFilename Filename for the embedded XML (e.g., factur-x.xml) + * @param string $profile ZUGFeRD profile + * @return bool True on success, false on error + */ + public function embedXmlContentInPdf($pdfFile, $xmlContent, $xmlFilename = 'factur-x.xml', $profile = 'EN16931') + { + // Create temporary XML file + $tempXmlFile = sys_get_temp_dir() . '/' . $xmlFilename; + + if (file_put_contents($tempXmlFile, $xmlContent) === false) { + $this->error = 'Could not create temporary XML file'; + return false; + } + + $result = $this->embedXmlInPdf($pdfFile, $tempXmlFile, $profile); + + // Clean up temporary file + @unlink($tempXmlFile); + + return $result; + } + + /** + * Check if embedding is possible + * + * @return bool True if libraries are available + */ + public static function isEmbeddingAvailable() + { + // Check for TCPDF + $tcpdfAvailable = class_exists('TCPDF') || file_exists(DOL_DOCUMENT_ROOT . '/includes/tecnickcom/tcpdf/tcpdf.php'); + + // Check for TCPDI + $tcpdiAvailable = class_exists('TCPDI') || file_exists(DOL_DOCUMENT_ROOT . '/includes/tcpdi/tcpdi.php'); + + return $tcpdfAvailable && $tcpdiAvailable; + } + + /** + * Get ZUGFeRD conformance level string for XMP metadata + * + * @param string $profile Profile name + * @return string Conformance level + */ + private function getConformanceLevel($profile) + { + switch (strtoupper($profile)) { + case 'MINIMUM': + return 'MINIMUM'; + case 'BASIC WL': + case 'BASICWL': + return 'BASIC WL'; + case 'BASIC': + return 'BASIC'; + case 'EN16931': + return 'EN 16931'; + case 'EXTENDED': + return 'EXTENDED'; + case 'XRECHNUNG': + return 'EN 16931'; + default: + return 'EN 16931'; + } + } + + /** + * Add embedded file to PDF without creating a FileAttachment annotation + * This ensures the file is only listed once in the EmbeddedFiles name tree + * + * @param TCPDI $pdf PDF object + * @param string $filepath Path to the file to embed + * @param string $filename Name for the embedded file + * @return void + */ + private function addEmbeddedFile($pdf, $filepath, $filename) + { + // Access TCPDF's internal embeddedfiles array directly + // This avoids creating a FileAttachment annotation which causes double listing + $reflection = new ReflectionClass($pdf); + + // Get the 'n' property (object counter) + $nProp = $reflection->getProperty('n'); + $nProp->setAccessible(true); + $n = $nProp->getValue($pdf); + + // Get the embeddedfiles array + $efProp = $reflection->getProperty('embeddedfiles'); + $efProp->setAccessible(true); + $embeddedfiles = $efProp->getValue($pdf); + + // Add the file to the embeddedfiles array + $embeddedfiles[$filename] = array( + 'f' => ++$n, + 'n' => ++$n, + 'file' => $filepath + ); + + // Update the object counter + $nProp->setValue($pdf, $n); + + // Update the embeddedfiles array + $efProp->setValue($pdf, $embeddedfiles); + + dol_syslog("ZugferdPdfEmbedder: Added $filename to embeddedfiles array", LOG_DEBUG); + } +} diff --git a/class/zugferdgenerator.class.php b/class/zugferdgenerator.class.php new file mode 100755 index 0000000..f89cf4a --- /dev/null +++ b/class/zugferdgenerator.class.php @@ -0,0 +1,745 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file class/zugferdgenerator.class.php + * \ingroup exportzugferd + * \brief ZUGFeRD/Factur-X/XRechnung XML Generator + */ + +/** + * Class ZugferdGenerator + * Generates EN16931-compliant XML for ZUGFeRD/Factur-X/XRechnung + */ +class ZugferdGenerator +{ + /** + * @var DoliDB Database handler + */ + private $db; + + /** + * @var string[] Error messages + */ + public $errors = array(); + + /** + * @var string Last error message + */ + public $error = ''; + + /** + * Profile constants + */ + const PROFILE_MINIMUM = 'MINIMUM'; + const PROFILE_BASICWL = 'BASIC WL'; + const PROFILE_BASIC = 'BASIC'; + const PROFILE_EN16931 = 'EN16931'; + const PROFILE_EXTENDED = 'EXTENDED'; + const PROFILE_XRECHNUNG = 'XRECHNUNG'; + + /** + * @var string Current profile + */ + private $profile; + + /** + * XML Namespaces + */ + private $namespaces = array( + 'rsm' => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100', + 'ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', + 'qdt' => 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100', + 'udt' => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100', + ); + + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + $this->db = $db; + $this->profile = getDolGlobalString('EXPORTZUGFERD_PROFILE', self::PROFILE_EN16931); + } + + /** + * Generate ZUGFeRD XML from a Dolibarr invoice + * + * @param Facture $invoice Invoice object + * @return string|false XML content or false on error + */ + public function generateFromInvoice($invoice) + { + global $conf, $mysoc; + + if (empty($invoice->id)) { + $this->error = 'Invoice ID is empty'; + return false; + } + + // Fetch invoice data if not already loaded + if (empty($invoice->lines)) { + $invoice->fetch_lines(); + } + + // Fetch thirdparty + if (empty($invoice->thirdparty) || empty($invoice->thirdparty->id)) { + $invoice->fetch_thirdparty(); + } + + // Start building XML + $xml = $this->createXMLHeader(); + $xml .= $this->createExchangedDocumentContext(); + $xml .= $this->createExchangedDocument($invoice); + $xml .= $this->createSupplyChainTradeTransaction($invoice, $mysoc); + $xml .= $this->createXMLFooter(); + + return $xml; + } + + /** + * Create XML header with namespaces + * + * @return string XML header + */ + private function createXMLHeader() + { + $xml = '' . "\n"; + $xml .= 'namespaces as $prefix => $uri) { + $xml .= ' xmlns:' . $prefix . '="' . $uri . '"'; + } + $xml .= '>' . "\n"; + return $xml; + } + + /** + * Create XML footer + * + * @return string XML footer + */ + private function createXMLFooter() + { + return '' . "\n"; + } + + /** + * Create ExchangedDocumentContext element + * + * @return string XML content + */ + private function createExchangedDocumentContext() + { + $profileId = $this->getProfileURN(); + + $xml = ' ' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($profileId) . '' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' ' . "\n"; + + return $xml; + } + + /** + * Get profile URN based on selected profile + * + * @return string Profile URN + */ + private function getProfileURN() + { + switch ($this->profile) { + case self::PROFILE_MINIMUM: + return 'urn:factur-x.eu:1p0:minimum'; + case self::PROFILE_BASICWL: + return 'urn:factur-x.eu:1p0:basicwl'; + case self::PROFILE_BASIC: + return 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic'; + case self::PROFILE_EXTENDED: + return 'urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended'; + case self::PROFILE_XRECHNUNG: + return 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0'; + case self::PROFILE_EN16931: + default: + return 'urn:cen.eu:en16931:2017'; + } + } + + /** + * Create ExchangedDocument element + * + * @param Facture $invoice Invoice object + * @return string XML content + */ + private function createExchangedDocument($invoice) + { + $xml = ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($invoice->ref) . '' . "\n"; + $xml .= ' ' . $this->getInvoiceTypeCode($invoice) . '' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' ' . date('Ymd', $invoice->date) . '' . "\n"; + $xml .= ' ' . "\n"; + + // Add notes if present + if (!empty($invoice->note_public)) { + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($invoice->note_public) . '' . "\n"; + $xml .= ' ' . "\n"; + } + + $xml .= ' ' . "\n"; + + return $xml; + } + + /** + * Get invoice type code (UNCL 1001) + * + * @param Facture $invoice Invoice object + * @return string Type code + */ + private function getInvoiceTypeCode($invoice) + { + // 380 = Commercial invoice + // 381 = Credit note + // 384 = Corrected invoice + // 389 = Self-billed invoice + if ($invoice->type == Facture::TYPE_CREDIT_NOTE) { + return '381'; + } + return '380'; + } + + /** + * Create SupplyChainTradeTransaction element + * + * @param Facture $invoice Invoice object + * @param Societe $mysoc Seller company + * @return string XML content + */ + private function createSupplyChainTradeTransaction($invoice, $mysoc) + { + $xml = ' ' . "\n"; + + // Invoice lines - skip title/subtotal lines with no amount + $lineNumber = 0; + foreach ($invoice->lines as $line) { + // Skip lines that are titles, subtotals, or have zero total and zero price + // These are typically from subtotaltitle module + if ($this->isSkippableLine($line)) { + continue; + } + $lineNumber++; + $xml .= $this->createLineItem($line, $lineNumber); + } + + // Trade agreement (seller/buyer) + $xml .= $this->createApplicableHeaderTradeAgreement($invoice, $mysoc); + + // Delivery information + $xml .= $this->createApplicableHeaderTradeDelivery($invoice); + + // Payment information and totals + $xml .= $this->createApplicableHeaderTradeSettlement($invoice); + + $xml .= ' ' . "\n"; + + return $xml; + } + + /** + * Create line item element + * + * @param FactureLigne $line Invoice line + * @param int $lineNumber Line number + * @return string XML content + */ + private function createLineItem($line, $lineNumber) + { + $xml = ' ' . "\n"; + + // Line document + $xml .= ' ' . "\n"; + $xml .= ' ' . $lineNumber . '' . "\n"; + $xml .= ' ' . "\n"; + + // Product info + $xml .= ' ' . "\n"; + if (!empty($line->product_ref)) { + $xml .= ' ' . $this->xmlEncode($line->product_ref) . '' . "\n"; + } + $xml .= ' ' . $this->xmlEncode($line->product_label ?: $line->desc) . '' . "\n"; + if (!empty($line->desc) && $line->desc != $line->product_label) { + $xml .= ' ' . $this->xmlEncode(strip_tags($line->desc)) . '' . "\n"; + } + $xml .= ' ' . "\n"; + + // Line agreement (price) + $xml .= ' ' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->formatAmount($line->subprice) . '' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' ' . "\n"; + + // Line delivery (quantity) + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->formatQuantity($line->qty) . '' . "\n"; + $xml .= ' ' . "\n"; + + // Line settlement (tax, total) + $xml .= ' ' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' VAT' . "\n"; + $xml .= ' ' . $this->getVATCategoryCode($line->tva_tx) . '' . "\n"; + $xml .= ' ' . $this->formatAmount($line->tva_tx) . '' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->formatAmount($line->total_ht) . '' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' ' . "\n"; + + $xml .= ' ' . "\n"; + + return $xml; + } + + /** + * Create ApplicableHeaderTradeAgreement element (seller/buyer) + * + * @param Facture $invoice Invoice object + * @param Societe $mysoc Seller company + * @return string XML content + */ + private function createApplicableHeaderTradeAgreement($invoice, $mysoc) + { + $buyer = $invoice->thirdparty; + + // Load extrafields for buyer if not loaded + if (empty($buyer->array_options)) { + $buyer->fetch_optionals(); + } + + $xml = ' ' . "\n"; + + // Buyer Reference (Leitweg-ID for XRechnung) - must come before SellerTradeParty + $leitwegId = ''; + if (!empty($buyer->array_options['options_leitweg_id'])) { + $leitwegId = $buyer->array_options['options_leitweg_id']; + } + if (!empty($leitwegId)) { + $xml .= ' ' . $this->xmlEncode($leitwegId) . '' . "\n"; + } + + // Seller + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($mysoc->name) . '' . "\n"; + + // Seller address + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($mysoc->zip) . '' . "\n"; + $xml .= ' ' . $this->xmlEncode($mysoc->address) . '' . "\n"; + $xml .= ' ' . $this->xmlEncode($mysoc->town) . '' . "\n"; + $xml .= ' ' . $this->xmlEncode($mysoc->country_code) . '' . "\n"; + $xml .= ' ' . "\n"; + + // Seller tax registration + if (!empty($mysoc->tva_intra)) { + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($mysoc->tva_intra) . '' . "\n"; + $xml .= ' ' . "\n"; + } + + $xml .= ' ' . "\n"; + + // Buyer + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->name) . '' . "\n"; + + // Buyer address + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->zip) . '' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->address) . '' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->town) . '' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->country_code) . '' . "\n"; + $xml .= ' ' . "\n"; + + // Buyer tax registration + if (!empty($buyer->tva_intra)) { + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->tva_intra) . '' . "\n"; + $xml .= ' ' . "\n"; + } + + $xml .= ' ' . "\n"; + + // Buyer order reference + if (!empty($invoice->ref_client)) { + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($invoice->ref_client) . '' . "\n"; + $xml .= ' ' . "\n"; + } + + $xml .= ' ' . "\n"; + + return $xml; + } + + /** + * Create ApplicableHeaderTradeDelivery element + * + * @param Facture $invoice Invoice object + * @return string XML content + */ + private function createApplicableHeaderTradeDelivery($invoice) + { + $xml = ' ' . "\n"; + + // Ship to party (same as buyer for now) + $buyer = $invoice->thirdparty; + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->name) . '' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->zip) . '' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->address) . '' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->town) . '' . "\n"; + $xml .= ' ' . $this->xmlEncode($buyer->country_code) . '' . "\n"; + $xml .= ' ' . "\n"; + $xml .= ' ' . "\n"; + + $xml .= ' ' . "\n"; + + return $xml; + } + + /** + * Create ApplicableHeaderTradeSettlement element (payment, totals) + * + * @param Facture $invoice Invoice object + * @return string XML content + */ + private function createApplicableHeaderTradeSettlement($invoice) + { + global $conf; + + $xml = ' ' . "\n"; + + // Currency + $xml .= ' ' . ($invoice->multicurrency_code ?: $conf->currency) . '' . "\n"; + + // Get bank account IBAN from invoice + $iban = ''; + $bic = ''; + $bankName = ''; + if (!empty($invoice->fk_account)) { + require_once DOL_DOCUMENT_ROOT . '/compta/bank/class/account.class.php'; + $bankAccount = new Account($this->db); + if ($bankAccount->fetch($invoice->fk_account) > 0) { + $iban = $bankAccount->iban; + $bic = $bankAccount->bic; + $bankName = $bankAccount->bank; + } + } + + // Payment means + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->getPaymentMeansCode($invoice) . '' . "\n"; + + // Bank account for SEPA transfer + if (!empty($iban)) { + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($iban) . '' . "\n"; + $xml .= ' ' . "\n"; + + // BIC if available + if (!empty($bic)) { + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->xmlEncode($bic) . '' . "\n"; + if (!empty($bankName)) { + $xml .= ' ' . $this->xmlEncode($bankName) . '' . "\n"; + } + $xml .= ' ' . "\n"; + } + } + + $xml .= ' ' . "\n"; + + // Tax summary + $xml .= $this->createTaxSummary($invoice); + + // Payment terms + if (!empty($invoice->cond_reglement_id)) { + $xml .= ' ' . "\n"; + if ($invoice->date_lim_reglement) { + $xml .= ' ' . "\n"; + $xml .= ' ' . date('Ymd', $invoice->date_lim_reglement) . '' . "\n"; + $xml .= ' ' . "\n"; + } + $xml .= ' ' . "\n"; + } + + // Monetary summation + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->formatAmount($invoice->total_ht) . '' . "\n"; + $xml .= ' ' . $this->formatAmount($invoice->total_ht) . '' . "\n"; + $xml .= ' ' . $this->formatAmount($invoice->total_tva) . '' . "\n"; + $xml .= ' ' . $this->formatAmount($invoice->total_ttc) . '' . "\n"; + $xml .= ' ' . $this->formatAmount($invoice->total_ttc - $invoice->getSommePaiement()) . '' . "\n"; + $xml .= ' ' . "\n"; + + $xml .= ' ' . "\n"; + + return $xml; + } + + /** + * Check if a line should be skipped (title, subtotal, etc.) + * + * @param FactureLigne $line Invoice line + * @return bool True if line should be skipped + */ + private function isSkippableLine($line) + { + // Skip free text lines (product_type == 9 in some modules) + if (isset($line->product_type) && $line->product_type == 9) { + return true; + } + + // Skip lines with special_code for titles/subtotals (subtotaltitle module uses special_code) + if (!empty($line->special_code) && in_array($line->special_code, array(104777, 104778, 104779))) { + return true; + } + + // Skip lines where qty is 0 and total is 0 and it looks like a title/subtotal + if ((float) $line->qty == 0 && (float) $line->total_ht == 0 && (float) $line->subprice == 0) { + return true; + } + + // Skip lines where description contains "Zwischensumme" or starts with title markers + $desc = strtolower($line->desc ?? ''); + if (strpos($desc, 'zwischensumme') !== false) { + return true; + } + + return false; + } + + /** + * Create tax summary from invoice lines + * + * @param Facture $invoice Invoice object + * @return string XML content + */ + private function createTaxSummary($invoice) + { + // Group by VAT rate - only include real lines + $vatGroups = array(); + foreach ($invoice->lines as $line) { + // Skip title/subtotal lines + if ($this->isSkippableLine($line)) { + continue; + } + $rate = (float) $line->tva_tx; + if (!isset($vatGroups[$rate])) { + $vatGroups[$rate] = array( + 'base' => 0, + 'amount' => 0, + ); + } + $vatGroups[$rate]['base'] += $line->total_ht; + $vatGroups[$rate]['amount'] += $line->total_tva; + } + + $xml = ''; + foreach ($vatGroups as $rate => $data) { + $xml .= ' ' . "\n"; + $xml .= ' ' . $this->formatAmount($data['amount']) . '' . "\n"; + $xml .= ' VAT' . "\n"; + $xml .= ' ' . $this->formatAmount($data['base']) . '' . "\n"; + $xml .= ' ' . $this->getVATCategoryCode($rate) . '' . "\n"; + $xml .= ' ' . $this->formatAmount($rate) . '' . "\n"; + $xml .= ' ' . "\n"; + } + + return $xml; + } + + /** + * Get payment means code (UNCL 4461) + * + * @param Facture $invoice Invoice object + * @return string Payment means code + */ + private function getPaymentMeansCode($invoice) + { + // 10 = Cash + // 30 = Credit transfer + // 42 = Payment to bank account + // 48 = Bank card + // 49 = Direct debit + // 58 = SEPA credit transfer + // 59 = SEPA direct debit + return '58'; // Default to SEPA credit transfer + } + + /** + * Get VAT category code (UNCL 5305) + * + * @param float $rate VAT rate + * @return string Category code + */ + private function getVATCategoryCode($rate) + { + // S = Standard rate + // Z = Zero rated + // E = Exempt + // AE = Reverse charge + // K = Intra-community supply + // G = Export outside EU + // O = Outside scope of VAT + // L = Canary Islands + // M = Ceuta and Melilla + if ($rate == 0) { + return 'Z'; + } + return 'S'; + } + + /** + * Get unit code (UN/ECE Rec. 20) + * + * @param FactureLigne $line Invoice line + * @return string Unit code + */ + private function getUnitCode($line) + { + // Map Dolibarr units to UN/ECE codes + // Common codes: C62=piece, HUR=hour, DAY=day, MTR=meter, KGM=kilogram, LTR=liter + $unit = strtolower($line->product_type == 1 ? 'service' : ($line->fk_unit ?: 'piece')); + + $unitMap = array( + 'piece' => 'C62', + 'pce' => 'C62', + 'stk' => 'C62', + 'stück' => 'C62', + 'hour' => 'HUR', + 'h' => 'HUR', + 'std' => 'HUR', + 'stunde' => 'HUR', + 'day' => 'DAY', + 'tag' => 'DAY', + 'meter' => 'MTR', + 'm' => 'MTR', + 'kg' => 'KGM', + 'kilogram' => 'KGM', + 'liter' => 'LTR', + 'l' => 'LTR', + 'service' => 'C62', + ); + + return $unitMap[$unit] ?? 'C62'; + } + + /** + * Format amount for XML + * + * @param float $amount Amount + * @return string Formatted amount + */ + private function formatAmount($amount) + { + return number_format((float) $amount, 2, '.', ''); + } + + /** + * Format quantity for XML + * + * @param float $qty Quantity + * @return string Formatted quantity + */ + private function formatQuantity($qty) + { + return number_format((float) $qty, 4, '.', ''); + } + + /** + * Encode string for XML + * + * @param string $string String to encode + * @return string Encoded string + */ + private function xmlEncode($string) + { + return htmlspecialchars((string) $string, ENT_XML1 | ENT_QUOTES, 'UTF-8'); + } + + /** + * Set profile + * + * @param string $profile Profile constant + * @return void + */ + public function setProfile($profile) + { + $this->profile = $profile; + } + + /** + * Get generated XML filename + * + * @param Facture $invoice Invoice object + * @return string Filename + */ + public function getXMLFilename($invoice) + { + if ($this->profile === self::PROFILE_XRECHNUNG) { + return 'xrechnung-' . $invoice->ref . '.xml'; + } + return 'factur-x.xml'; + } + + /** + * Save XML to file + * + * @param Facture $invoice Invoice object + * @param string $xml XML content + * @return string|false File path or false on error + */ + public function saveXML($invoice, $xml) + { + global $conf; + + $dir = $conf->facture->dir_output . '/' . dol_sanitizeFileName($invoice->ref); + + if (!is_dir($dir)) { + dol_mkdir($dir); + } + + $filename = $this->getXMLFilename($invoice); + $filepath = $dir . '/' . $filename; + + $result = file_put_contents($filepath, $xml); + + if ($result === false) { + $this->error = 'Failed to write XML file: ' . $filepath; + return false; + } + + return $filepath; + } +} diff --git a/core/modules/modExportZugferd.class.php b/core/modules/modExportZugferd.class.php new file mode 100755 index 0000000..3ca608c --- /dev/null +++ b/core/modules/modExportZugferd.class.php @@ -0,0 +1,226 @@ + + * Copyright (C) 2018-2019 Nicolas ZABOURI + * Copyright (C) 2019-2024 Frédéric France + * Copyright (C) 2026 Eduard Wisch + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \defgroup exportzugferd Module ExportZugferd + * \brief ExportZugferd module descriptor. + * + * \file htdocs/exportzugferd/core/modules/modExportZugferd.class.php + * \ingroup exportzugferd + * \brief Description and activation file for module ExportZugferd + */ +include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php'; + + +/** + * Description and activation class for module ExportZugferd + */ +class modExportZugferd extends DolibarrModules +{ + /** + * Constructor. Define names, constants, directories, boxes, permissions + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + global $conf, $langs; + + $this->db = $db; + + // Id for module (must be unique). + $this->numero = 500017; + + // Key text used to identify module (for permissions, menus, etc...) + $this->rights_class = 'exportzugferd'; + + // Family can be 'base' (core modules),'crm','financial','hr','projects','products','ecm','technic' (transverse modules),'interface' (link with external tools),'other','...' + $this->family = "financial"; + + // Module position in the family on 2 digits ('01', '10', '20', ...) + $this->module_position = '91'; + + // Module label (no space allowed) + $this->name = preg_replace('/^mod/i', '', get_class($this)); + + // Module description + $this->description = "ExportZugferdDescription"; + $this->descriptionlong = "ExportZugferdDescriptionLong"; + + // Author + $this->editor_name = 'Data IT Solution'; + $this->editor_url = ''; + + // Version + $this->version = '2.0'; + + // Key used in llx_const table to save module status + $this->const_name = 'MAIN_MODULE_'.strtoupper($this->name); + + // Module icon + $this->picto = 'object_exportzugferd@exportzugferd'; + + // Define some features supported by module + $this->module_parts = array( + 'triggers' => 0, + 'login' => 0, + 'substitutions' => 0, + 'menus' => 0, + 'tpl' => 0, + 'barcode' => 0, + 'models' => 0, + 'printing' => 0, + 'theme' => 0, + 'css' => array(), + 'js' => array(), + 'hooks' => array( + 'data' => array( + 'pdfgeneration', + 'odtgeneration', + 'invoicecard', + 'formmail', + ), + 'entity' => '0', + ), + 'moduleforexternal' => 0, + 'websitetemplates' => 0, + 'captcha' => 0 + ); + + // Data directories to create when module is enabled + $this->dirs = array("/exportzugferd/temp"); + + // Config pages + $this->config_page_url = array("setup.php@exportzugferd"); + + // Dependencies + $this->hidden = getDolGlobalInt('MODULE_EXPORTZUGFERD_DISABLED'); + $this->depends = array('modFacture'); + $this->requiredby = array(); + $this->conflictwith = array(); + + // Constants + $this->const = array( + 1 => array('EXPORTZUGFERD_PROFILE', 'chaine', 'EN16931', 'Default ZUGFeRD profile', 0, 'current', 1), + 2 => array('EXPORTZUGFERD_EMBED_PDF', 'chaine', '0', 'Embed XML in PDF (requires FPDI)', 0, 'current', 1), + ); + + // Boxes/Widgets + $this->boxes = array(); + + // Cronjobs + $this->cronjobs = array(); + + // Permissions provided by this module + $r = 0; + $this->rights[$r][0] = $this->numero + $r + 1; + $this->rights[$r][1] = 'Read ZUGFeRD exports'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'read'; + $this->rights[$r][5] = ''; + $r++; + + $this->rights[$r][0] = $this->numero + $r + 1; + $this->rights[$r][1] = 'Create/Download ZUGFeRD exports'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'export'; + $this->rights[$r][5] = ''; + $r++; + + // Main menu entries to add + $this->menu = array(); + $r = 0; + + // Add top menu entry + $this->menu[$r++] = array( + 'fk_menu' => 'fk_mainmenu=billing', + 'type' => 'left', + 'titre' => 'ExportZugferd', + 'prefix' => img_picto('', $this->picto, 'class="paddingright pictofixedwidth valignmiddle"'), + 'mainmenu' => 'billing', + 'leftmenu' => 'exportzugferd', + 'url' => '/exportzugferd/index.php', + 'langs' => 'exportzugferd@exportzugferd', + 'position' => 1000 + $r, + 'enabled' => 'isModEnabled("exportzugferd")', + 'perms' => '$user->hasRight("exportzugferd", "read")', + 'target' => '', + 'user' => 0, + ); + } + + /** + * Function called when module is enabled. + * + * @param string $options Options when enabling module ('', 'newboxdefonly', 'noboxes') + * @return int 1 if OK, 0 if KO + */ + public function init($options = '') + { + $result = $this->_load_tables('/install/mysql/', 'exportzugferd'); + if ($result < 0) { + return -1; + } + + // Create extrafield for Leitweg-ID on thirdparty (customer) + include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php'; + $extrafields = new ExtraFields($this->db); + + // Add Leitweg-ID field for XRechnung (German public sector invoices) + // Only add if it doesn't exist + $result = $extrafields->addExtraField( + 'leitweg_id', // attribute code + 'Leitweg-ID (XRechnung)', // label + 'varchar', // type + 100, // position + '50', // size + 'thirdparty', // element type + 0, // unique + 0, // required + '', // default value + '', // params + 1, // always editable + '', // permission + 1, // list (show in list) + '', // computed value + '', // entity + '', // lang file + 'exportzugferd@exportzugferd', // module + 'isModEnabled("exportzugferd")' // enabled condition + ); + + $sql = array(); + + return $this->_init($sql, $options); + } + + /** + * Function called when module is disabled. + * + * @param string $options Options when disabling module + * @return int 1 if OK, 0 if KO + */ + public function remove($options = '') + { + $sql = array(); + + return $this->_remove($sql, $options); + } +} diff --git a/download.php b/download.php new file mode 100755 index 0000000..23be732 --- /dev/null +++ b/download.php @@ -0,0 +1,152 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file download.php + * \ingroup exportzugferd + * \brief Download ZUGFeRD XML file + */ + +// Load Dolibarr environment +$res = 0; +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +if (!$res && file_exists("../main.inc.php")) { + $res = @include "../main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} +require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php'; +require_once __DIR__ . '/class/zugferdgenerator.class.php'; + +// Security check +$id = GETPOSTINT('id'); +$action = GETPOST('action', 'aZ09'); + +if (!$user->hasRight('exportzugferd', 'export') && !$user->hasRight('facture', 'lire')) { + accessforbidden(); +} + +$langs->loadLangs(array('exportzugferd@exportzugferd', 'bills')); + +// Load invoice +$invoice = new Facture($db); +$result = $invoice->fetch($id); + +if ($result <= 0) { + setEventMessages($langs->trans('ErrorRecordNotFound'), null, 'errors'); + header('Location: ' . DOL_URL_ROOT . '/compta/facture/list.php'); + exit; +} + +// Security check on third party +$socid = $invoice->socid; +if ($user->socid > 0 && $socid != $user->socid) { + accessforbidden(); +} + +// Handle actions +if ($action == 'download_xml') { + $generator = new ZugferdGenerator($db); + + // Check if XML already exists + $dir = $conf->facture->dir_output . '/' . dol_sanitizeFileName($invoice->ref); + $zugferdFile = $dir . '/factur-x.xml'; + $xrechnungFile = $dir . '/xrechnung-' . $invoice->ref . '.xml'; + + $xmlFile = null; + if (file_exists($xrechnungFile)) { + $xmlFile = $xrechnungFile; + } elseif (file_exists($zugferdFile)) { + $xmlFile = $zugferdFile; + } + + // Generate if not exists + if (!$xmlFile) { + $xml = $generator->generateFromInvoice($invoice); + + if ($xml === false) { + setEventMessages($generator->error, $generator->errors, 'errors'); + header('Location: ' . DOL_URL_ROOT . '/compta/facture/card.php?id=' . $id); + exit; + } + + $xmlFile = $generator->saveXML($invoice, $xml); + + if ($xmlFile === false) { + setEventMessages($generator->error, null, 'errors'); + header('Location: ' . DOL_URL_ROOT . '/compta/facture/card.php?id=' . $id); + exit; + } + } + + // Download file + $filename = basename($xmlFile); + + header('Content-Type: application/xml'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Length: ' . filesize($xmlFile)); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + + readfile($xmlFile); + exit; +} + +if ($action == 'generate_xml') { + $generator = new ZugferdGenerator($db); + + $xml = $generator->generateFromInvoice($invoice); + + if ($xml === false) { + setEventMessages($generator->error, $generator->errors, 'errors'); + } else { + $xmlFile = $generator->saveXML($invoice, $xml); + + if ($xmlFile === false) { + setEventMessages($generator->error, null, 'errors'); + } else { + setEventMessages($langs->trans('ZugferdXMLGenerated'), null, 'mesgs'); + } + } + + header('Location: ' . DOL_URL_ROOT . '/compta/facture/card.php?id=' . $id); + exit; +} + +// Default: redirect to invoice card +header('Location: ' . DOL_URL_ROOT . '/compta/facture/card.php?id=' . $id); +exit; diff --git a/img/object_exportzugferd.svg b/img/object_exportzugferd.svg new file mode 100644 index 0000000..82fdc4b --- /dev/null +++ b/img/object_exportzugferd.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + </> + + + diff --git a/index.php b/index.php new file mode 100755 index 0000000..edee2e5 --- /dev/null +++ b/index.php @@ -0,0 +1,155 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file index.php + * \ingroup exportzugferd + * \brief Home page for ExportZugferd module + */ + +// Load Dolibarr environment +$res = 0; +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +if (!$res && file_exists("../main.inc.php")) { + $res = @include "../main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} +require_once DOL_DOCUMENT_ROOT . '/core/class/html.formfile.class.php'; +require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php'; + +// Security check +if (!$user->hasRight('exportzugferd', 'read')) { + accessforbidden(); +} + +$langs->loadLangs(array('exportzugferd@exportzugferd', 'bills')); + +/* + * View + */ + +$form = new Form($db); + +llxHeader('', $langs->trans('ExportZugferd'), '', '', 0, 0, '', '', '', 'mod-exportzugferd page-index'); + +print load_fiche_titre($langs->trans('ExportZugferd'), '', 'fa-file-export'); + +print '
'; +print '
'; + +// Info box +print '
'; +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print '
' . $langs->trans('Information') . '
' . $langs->trans('ZugferdProfile') . '' . getDolGlobalString('EXPORTZUGFERD_PROFILE', 'EN16931') . '
' . $langs->trans('AutoGenerateZugferd') . '' . yn(getDolGlobalInt('EXPORTZUGFERD_AUTO_GENERATE')) . '
'; +print '
'; + +print '
'; +print '
'; + +// Recent invoices with ZUGFeRD +print '
'; +print ''; +print ''; +print ''; +print ''; + +// Get recent invoices +$sql = "SELECT f.rowid, f.ref, f.datef, f.total_ttc, s.nom as socname"; +$sql .= " FROM " . MAIN_DB_PREFIX . "facture as f"; +$sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON s.rowid = f.fk_soc"; +$sql .= " WHERE f.entity IN (" . getEntity('invoice') . ")"; +$sql .= " ORDER BY f.datef DESC"; +$sql .= " LIMIT 10"; + +$resql = $db->query($sql); +if ($resql) { + $num = $db->num_rows($resql); + if ($num > 0) { + while ($obj = $db->fetch_object($resql)) { + $invoice = new Facture($db); + $invoice->fetch($obj->rowid); + + // Check if XML exists + $dir = $conf->facture->dir_output . '/' . dol_sanitizeFileName($obj->ref); + $hasXML = file_exists($dir . '/factur-x.xml') || file_exists($dir . '/xrechnung-' . $obj->ref . '.xml'); + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + } + } else { + print ''; + } + $db->free($resql); +} else { + dol_print_error($db); +} + +print '
' . $langs->trans('RecentInvoicesWithZugferd') . '
' . $invoice->getNomUrl(1) . '' . htmlspecialchars($obj->socname) . '' . price($obj->total_ttc) . ' ' . $conf->currency . ''; + if ($hasXML) { + print '' . $langs->trans('ZugferdXMLFile') . ''; + } else { + print ''; + print $langs->trans('GenerateZugferdXML'); + print ''; + } + print '
' . $langs->trans('NoRecordFound') . '
'; +print '
'; + +print '
'; +print '
'; + +llxFooter(); +$db->close(); diff --git a/langs/de_DE/exportzugferd.lang b/langs/de_DE/exportzugferd.lang new file mode 100755 index 0000000..e763821 --- /dev/null +++ b/langs/de_DE/exportzugferd.lang @@ -0,0 +1,78 @@ +# Copyright (C) 2026 Eduard Wisch +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. + +# Module +Module500017Name = Export ZUGFeRD +Module500017Desc = Exportiert Rechnungen im ZUGFeRD/Factur-X/XRechnung Format + +# Admin +ExportZugferdSetup = Export ZUGFeRD Einrichtung +ExportZugferdAbout = Über Export ZUGFeRD +ExportZugferd = Export ZUGFeRD +ExportZugferdDescription = Erstellt ZUGFeRD/Factur-X/XRechnung XML-Dateien für Kundenrechnungen +ExportZugferdDescriptionLong = Dieses Modul generiert standardkonforme elektronische Rechnungen im ZUGFeRD/Factur-X und XRechnung Format (EN16931). Die XML-Dateien können als Anhang versandt oder in PDF-Dokumente eingebettet werden. + +# Settings +GeneralSettings = Allgemeine Einstellungen +ZugferdProfile = ZUGFeRD Profil +AutoGenerateZugferd = Automatisch generieren +AutoGenerateZugferdDesc = Erstellt automatisch eine XML-Datei bei jeder Rechnungsgenerierung +EmbedXMLInPDF = XML in PDF einbetten +EmbedXMLInPDFDesc = Bettet die XML-Datei direkt in das PDF ein (echtes ZUGFeRD-PDF) + +# Profiles +SupportedProfiles = Unterstützte Profile +ProfileMinimumDesc = Minimale Rechnungsdaten (nur für Archivierung) +ProfileBasicDesc = Grundlegende strukturierte Rechnungsdaten +ProfileEN16931Desc = Vollständige EU-konforme Rechnung (empfohlen) +ProfileXRechnungDesc = Deutsche Behördenrechnung nach XRechnung-Standard + +# Actions +DownloadZugferdXML = XML herunterladen +GenerateZugferdXML = XML generieren +ZugferdXMLGenerated = ZUGFeRD XML erfolgreich erstellt +ZugferdXMLFile = ZUGFeRD/XRechnung Datei +NoZugferdXML = Keine XML vorhanden + +# About +AboutModule = Über dieses Modul +ExportZugferdAboutDesc = Dieses Modul ermöglicht den Export von Kundenrechnungen im standardisierten elektronischen Rechnungsformat ZUGFeRD/Factur-X und XRechnung. Diese Formate entsprechen der europäischen Norm EN16931 für elektronische Rechnungen. +Features = Funktionen +FeatureGenerateZugferd = Generierung von ZUGFeRD/Factur-X XML-Dateien +FeatureXRechnung = Unterstützung für XRechnung 3.0 (deutsche Behördenrechnung) +FeatureEN16931 = Vollständige EN16931-Konformität +FeatureAutoGenerate = Automatische Generierung bei Rechnungserstellung + +# Preview +PreviewZugferdXML = ZUGFeRD Vorschau +ZugferdPreview = ZUGFeRD Vorschau +ZugferdXMLValues = ZUGFeRD XML Werte +DocumentInfo = Dokumentinformationen +InvoiceNumber = Rechnungsnummer +TypeCode = Dokumenttyp-Code +IssueDate = Ausstellungsdatum +DueDate = Fälligkeitsdatum +Seller = Verkäufer +Lines = Positionen +Line = Position +Totals = Summen +GeneratedXML = Generierte XML +RegenerateZugferdXML = XML neu generieren +BackToInvoice = Zurück zur Rechnung +LeitwegID = Leitweg-ID + +# XML Options +DeleteXMLAfterEmbed = XML nach Einbettung löschen +DeleteXMLAfterEmbedDesc = Löscht die separate XML-Datei nachdem sie ins PDF eingebettet wurde + +# Email +AttachZugferdToEmail = ZUGFeRD XML an E-Mail anhängen +AttachZugferdToEmailDesc = Hängt die ZUGFeRD/XRechnung XML automatisch an ausgehende Rechnungs-E-Mails an + +# Errors +ErrorGeneratingXML = Fehler beim Erstellen der XML-Datei +ErrorSavingXML = Fehler beim Speichern der XML-Datei diff --git a/langs/en_US/exportzugferd.lang b/langs/en_US/exportzugferd.lang new file mode 100755 index 0000000..ae16184 --- /dev/null +++ b/langs/en_US/exportzugferd.lang @@ -0,0 +1,51 @@ +# Copyright (C) 2026 Eduard Wisch +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. + +# Module +Module500017Name = Export ZUGFeRD +Module500017Desc = Export invoices in ZUGFeRD/Factur-X/XRechnung format + +# Admin +ExportZugferdSetup = Export ZUGFeRD Setup +ExportZugferdAbout = About Export ZUGFeRD +ExportZugferd = Export ZUGFeRD +ExportZugferdDescription = Generate ZUGFeRD/Factur-X/XRechnung XML files for customer invoices +ExportZugferdDescriptionLong = This module generates standard-compliant electronic invoices in ZUGFeRD/Factur-X and XRechnung format (EN16931). The XML files can be sent as attachments or embedded in PDF documents. + +# Settings +GeneralSettings = General Settings +ZugferdProfile = ZUGFeRD Profile +AutoGenerateZugferd = Auto-generate +AutoGenerateZugferdDesc = Automatically create an XML file with each invoice generation +EmbedXMLInPDF = Embed XML in PDF +EmbedXMLInPDFDesc = Embed the XML file in the PDF (requires additional libraries) + +# Profiles +SupportedProfiles = Supported Profiles +ProfileMinimumDesc = Minimal invoice data (archiving only) +ProfileBasicDesc = Basic structured invoice data +ProfileEN16931Desc = Complete EU-compliant invoice (recommended) +ProfileXRechnungDesc = German public sector invoice according to XRechnung standard + +# Actions +DownloadZugferdXML = Download ZUGFeRD XML +GenerateZugferdXML = Generate ZUGFeRD XML +ZugferdXMLGenerated = ZUGFeRD XML successfully created +ZugferdXMLFile = ZUGFeRD/XRechnung File + +# About +AboutModule = About this module +ExportZugferdAboutDesc = This module enables the export of customer invoices in the standardized electronic invoice format ZUGFeRD/Factur-X and XRechnung. These formats comply with the European standard EN16931 for electronic invoices. +Features = Features +FeatureGenerateZugferd = Generation of ZUGFeRD/Factur-X XML files +FeatureXRechnung = Support for XRechnung 3.0 (German public sector invoices) +FeatureEN16931 = Full EN16931 compliance +FeatureAutoGenerate = Automatic generation when creating invoices + +# Errors +ErrorGeneratingXML = Error generating XML file +ErrorSavingXML = Error saving XML file diff --git a/lib/exportzugferd.lib.php b/lib/exportzugferd.lib.php new file mode 100755 index 0000000..417d6d1 --- /dev/null +++ b/lib/exportzugferd.lib.php @@ -0,0 +1,53 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file lib/exportzugferd.lib.php + * \ingroup exportzugferd + * \brief Library functions for ExportZugferd module + */ + +/** + * Prepare admin pages header + * + * @return array Array of tabs + */ +function exportzugferd_admin_prepare_head() +{ + global $langs, $conf; + + $langs->load('exportzugferd@exportzugferd'); + + $h = 0; + $head = array(); + + $head[$h][0] = dol_buildpath('/exportzugferd/admin/setup.php', 1); + $head[$h][1] = $langs->trans('Settings'); + $head[$h][2] = 'settings'; + $h++; + + $head[$h][0] = dol_buildpath('/exportzugferd/admin/about.php', 1); + $head[$h][1] = $langs->trans('About'); + $head[$h][2] = 'about'; + $h++; + + complete_head_from_modules($conf, $langs, null, $head, $h, 'exportzugferd@exportzugferd'); + + complete_head_from_modules($conf, $langs, null, $head, $h, 'exportzugferd@exportzugferd', 'remove'); + + return $head; +} diff --git a/preview.php b/preview.php new file mode 100644 index 0000000..d985d86 --- /dev/null +++ b/preview.php @@ -0,0 +1,404 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file preview.php + * \ingroup exportzugferd + * \brief Preview ZUGFeRD XML values for an invoice + */ + +// Load Dolibarr environment +$res = 0; +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +if (!$res && file_exists("../main.inc.php")) { + $res = @include "../main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} +require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php'; +require_once __DIR__ . '/class/zugferdgenerator.class.php'; + +// Security check +$id = GETPOSTINT('id'); + +if (!$user->hasRight('exportzugferd', 'export') && !$user->hasRight('facture', 'lire')) { + accessforbidden(); +} + +$langs->loadLangs(array('exportzugferd@exportzugferd', 'bills')); + +// Load invoice +$invoice = new Facture($db); +$result = $invoice->fetch($id); + +if ($result <= 0) { + setEventMessages($langs->trans('ErrorRecordNotFound'), null, 'errors'); + header('Location: ' . DOL_URL_ROOT . '/compta/facture/list.php'); + exit; +} + +// Security check on third party +$socid = $invoice->socid; +if ($user->socid > 0 && $socid != $user->socid) { + accessforbidden(); +} + +// Fetch related data +$invoice->fetch_thirdparty(); +if (empty($invoice->lines)) { + $invoice->fetch_lines(); +} + +/* + * View + */ + +$title = $langs->trans('PreviewZugferdXML') . ' - ' . $invoice->ref; +llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-exportzugferd page-preview'); + +$head = array(); +$head[0][0] = DOL_URL_ROOT . '/compta/facture/card.php?id=' . $invoice->id; +$head[0][1] = $langs->trans('Invoice'); +$head[0][2] = 'card'; +$head[1][0] = dol_buildpath('/exportzugferd/preview.php', 1) . '?id=' . $invoice->id; +$head[1][1] = $langs->trans('ZugferdPreview'); +$head[1][2] = 'zugferd'; + +print dol_get_fiche_head($head, 'zugferd', $langs->trans('Invoice'), -1, 'bill'); + +// Invoice info +print '
'; +print '
'; + +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print '
' . $langs->trans('Ref') . '' . $invoice->getNomUrl(1) . '
' . $langs->trans('Customer') . '' . $invoice->thirdparty->getNomUrl(1) . '
' . $langs->trans('DateInvoice') . '' . dol_print_date($invoice->date, 'day') . '
' . $langs->trans('AmountTTC') . '' . price($invoice->total_ttc) . ' ' . $conf->currency . '
'; + +print '
'; + +print dol_get_fiche_end(); + +// ZUGFeRD XML Preview +print '
'; +print ''; +print ''; +print ''; +print ''; + +// Document Info +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +$typeCode = '380'; // Standard invoice +if ($invoice->type == Facture::TYPE_CREDIT_NOTE) { + $typeCode = '381'; +} elseif ($invoice->type == Facture::TYPE_DEPOSIT) { + $typeCode = '386'; +} +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +if (!empty($invoice->date_lim_reglement)) { + print ''; + print ''; + print ''; + print ''; +} + +// Seller Info +print ''; +print ''; +print ''; + +global $mysoc; +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +if (!empty($mysoc->tva_intra)) { + print ''; + print ''; + print ''; + print ''; +} + +// Buyer Info +print ''; +print ''; +print ''; + +$buyer = $invoice->thirdparty; +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +if (!empty($buyer->tva_intra)) { + print ''; + print ''; + print ''; + print ''; +} + +// Leitweg-ID (for XRechnung) +if (!empty($buyer->array_options['options_leitweg_id'])) { + print ''; + print ''; + print ''; + print ''; +} + +// Payment Info +print ''; +print ''; +print ''; + +// Bank Account Info +if (!empty($invoice->fk_account)) { + require_once DOL_DOCUMENT_ROOT . '/compta/bank/class/account.class.php'; + $bankAccount = new Account($db); + if ($bankAccount->fetch($invoice->fk_account) > 0) { + if (!empty($bankAccount->iban)) { + print ''; + print ''; + print ''; + print ''; + } + if (!empty($bankAccount->bic)) { + print ''; + print ''; + print ''; + print ''; + } + } +} + +// Line Items +print ''; +print ''; +print ''; + +$lineNo = 0; +foreach ($invoice->lines as $line) { + // Skip title/subtotal lines + if (!empty($line->special_code) && in_array($line->special_code, array(104777, 104778, 104779))) { + continue; + } + if ($line->qty == 0 && $line->total_ht == 0 && empty($line->fk_product)) { + continue; + } + + $lineNo++; + + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + print ''; + + if (!empty($line->fk_product)) { + $product = new Product($db); + $product->fetch($line->fk_product); + print ''; + print ''; + print ''; + print ''; + } + + print ''; + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + print ''; + + $vatRate = $line->tva_tx; + $categoryCode = ($vatRate > 0) ? 'S' : 'Z'; + print ''; + print ''; + print ''; + print ''; +} + +// Totals +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print '
' . $langs->trans('ZugferdXMLValues') . '
' . $langs->trans('DocumentInfo') . '
' . $langs->trans('InvoiceNumber') . '' . htmlspecialchars($invoice->ref) . '
' . $langs->trans('TypeCode') . '' . $typeCode . ' (' . ($typeCode == '380' ? 'Commercial Invoice' : ($typeCode == '381' ? 'Credit Note' : 'Deposit')) . ')
' . $langs->trans('IssueDate') . '' . dol_print_date($invoice->date, '%Y%m%d') . '
' . $langs->trans('DueDate') . '' . dol_print_date($invoice->date_lim_reglement, '%Y%m%d') . '
' . $langs->trans('Seller') . ' (SellerTradeParty)
' . $langs->trans('Name') . '' . htmlspecialchars($mysoc->name) . '
' . $langs->trans('Address') . '' . htmlspecialchars($mysoc->address) . '
' . $langs->trans('Zip') . ' / ' . $langs->trans('Town') . '' . htmlspecialchars($mysoc->zip) . ' / ' . htmlspecialchars($mysoc->town) . '
' . $langs->trans('Country') . '' . htmlspecialchars($mysoc->country_code) . '
' . $langs->trans('VATIntra') . '' . htmlspecialchars($mysoc->tva_intra) . '
' . $langs->trans('Customer') . ' (BuyerTradeParty)
' . $langs->trans('Name') . '' . htmlspecialchars($buyer->name) . '
' . $langs->trans('Address') . '' . htmlspecialchars($buyer->address) . '
' . $langs->trans('Zip') . ' / ' . $langs->trans('Town') . '' . htmlspecialchars($buyer->zip) . ' / ' . htmlspecialchars($buyer->town) . '
' . $langs->trans('Country') . '' . htmlspecialchars($buyer->country_code) . '
' . $langs->trans('VATIntra') . '' . htmlspecialchars($buyer->tva_intra) . '
' . $langs->trans('LeitwegID') . ' (BuyerReference)' . htmlspecialchars($buyer->array_options['options_leitweg_id']) . '
' . $langs->trans('Payment') . '
IBAN' . htmlspecialchars($bankAccount->iban) . '
BIC' . htmlspecialchars($bankAccount->bic) . '
' . $langs->trans('Lines') . ' (IncludedSupplyChainTradeLineItem)
' . $langs->trans('Line') . ' ' . $lineNo . '
  LineID' . $lineNo . '
  SellerAssignedID' . htmlspecialchars($product->ref) . '
  Name' . htmlspecialchars($line->product_label ?: $line->desc) . '
  BilledQuantity' . number_format($line->qty, 4, '.', '') . '
  NetPriceAmount (ChargeAmount)' . number_format($line->subprice, 2, '.', '') . '
  LineTotalAmount' . number_format($line->total_ht, 2, '.', '') . '
  VAT (CategoryCode / Rate)' . $categoryCode . ' / ' . number_format($vatRate, 2, '.', '') . '%
' . $langs->trans('Totals') . ' (SpecifiedTradeSettlementHeaderMonetarySummation)
LineTotalAmount' . number_format($invoice->total_ht, 2, '.', '') . '
TaxBasisTotalAmount' . number_format($invoice->total_ht, 2, '.', '') . '
TaxTotalAmount' . number_format($invoice->total_tva, 2, '.', '') . ' (currencyID="' . $conf->currency . '")
GrandTotalAmount' . number_format($invoice->total_ttc, 2, '.', '') . '
DuePayableAmount' . number_format($invoice->total_ttc - $invoice->getSommePaiement(), 2, '.', '') . '
'; +print '
'; + +// Check if XML exists and show it +$dir = $conf->facture->dir_output . '/' . dol_sanitizeFileName($invoice->ref); +$zugferdFile = $dir . '/factur-x.xml'; +$xrechnungFile = $dir . '/xrechnung-' . $invoice->ref . '.xml'; + +$xmlFile = null; +if (file_exists($zugferdFile)) { + $xmlFile = $zugferdFile; +} elseif (file_exists($xrechnungFile)) { + $xmlFile = $xrechnungFile; +} + +if ($xmlFile) { + print '
'; + print '
'; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print '
' . $langs->trans('GeneratedXML') . ' (' . basename($xmlFile) . ')
'; + print '
';
+	print htmlspecialchars(file_get_contents($xmlFile));
+	print '
'; + print '
'; + print '
'; +} + +// Actions +print ''; + +llxFooter(); +$db->close();