Version 2.0 - ZUGFeRD PDF-Einbettung
- 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 <noreply@anthropic.com>
This commit is contained in:
commit
ee37b3dd69
16 changed files with 3169 additions and 0 deletions
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Temp files
|
||||
*.tmp
|
||||
*~
|
||||
34
CHANGELOG.md
Normal file
34
CHANGELOG.md
Normal file
|
|
@ -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
|
||||
106
README.md
Normal file
106
README.md
Normal file
|
|
@ -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
|
||||
<rsm:CrossIndustryInvoice>
|
||||
<rsm:ExchangedDocumentContext>...</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>...</rsm:ExchangedDocument>
|
||||
<rsm:SupplyChainTradeTransaction>...</rsm:SupplyChainTradeTransaction>
|
||||
</rsm:CrossIndustryInvoice>
|
||||
```
|
||||
|
||||
### 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)
|
||||
105
admin/about.php
Executable file
105
admin/about.php
Executable file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \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 = '<a href="' . DOL_URL_ROOT . '/admin/modules.php?restore_lastsearch_values=1">' . $langs->trans('BackToModuleList') . '</a>';
|
||||
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 '<div class="fichecenter">';
|
||||
|
||||
print '<h2>' . $langs->trans('AboutModule') . '</h2>';
|
||||
|
||||
print '<p>' . $langs->trans('ExportZugferdAboutDesc') . '</p>';
|
||||
|
||||
print '<h3>' . $langs->trans('Features') . '</h3>';
|
||||
print '<ul>';
|
||||
print '<li>' . $langs->trans('FeatureGenerateZugferd') . '</li>';
|
||||
print '<li>' . $langs->trans('FeatureXRechnung') . '</li>';
|
||||
print '<li>' . $langs->trans('FeatureEN16931') . '</li>';
|
||||
print '<li>' . $langs->trans('FeatureAutoGenerate') . '</li>';
|
||||
print '</ul>';
|
||||
|
||||
print '<h3>' . $langs->trans('Version') . '</h3>';
|
||||
print '<p>1.0</p>';
|
||||
|
||||
print '<h3>' . $langs->trans('Author') . '</h3>';
|
||||
print '<p>Eduard Wisch - Data IT Solution</p>';
|
||||
|
||||
print '</div>';
|
||||
|
||||
print dol_get_fiche_end();
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
239
admin/setup.php
Executable file
239
admin/setup.php
Executable file
|
|
@ -0,0 +1,239 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \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 = '<a href="' . DOL_URL_ROOT . '/admin/modules.php?restore_lastsearch_values=1">' . $langs->trans('BackToModuleList') . '</a>';
|
||||
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 '<form method="POST" action="' . $_SERVER['PHP_SELF'] . '">';
|
||||
print '<input type="hidden" name="token" value="' . newToken() . '">';
|
||||
print '<input type="hidden" name="action" value="update">';
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
|
||||
// Profile selection
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">' . $langs->trans('GeneralSettings') . '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">' . $langs->trans('ZugferdProfile') . '</td>';
|
||||
print '<td>';
|
||||
print '<select name="EXPORTZUGFERD_PROFILE" class="flat minwidth200">';
|
||||
foreach ($profiles as $code => $label) {
|
||||
$selected = (getDolGlobalString('EXPORTZUGFERD_PROFILE', 'EN16931') == $code) ? ' selected' : '';
|
||||
print '<option value="' . $code . '"' . $selected . '>' . $label . '</option>';
|
||||
}
|
||||
print '</select>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Auto-generate
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('AutoGenerateZugferd') . '</td>';
|
||||
print '<td>';
|
||||
print $form->selectyesno('EXPORTZUGFERD_AUTO_GENERATE', getDolGlobalInt('EXPORTZUGFERD_AUTO_GENERATE', 0), 1);
|
||||
print '<span class="opacitymedium"> - ' . $langs->trans('AutoGenerateZugferdDesc') . '</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Embed in PDF
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('EmbedXMLInPDF') . '</td>';
|
||||
print '<td>';
|
||||
print $form->selectyesno('EXPORTZUGFERD_EMBED_IN_PDF', getDolGlobalInt('EXPORTZUGFERD_EMBED_IN_PDF', 0), 1);
|
||||
print '<span class="opacitymedium"> - ' . $langs->trans('EmbedXMLInPDFDesc') . '</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Delete XML after embedding
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('DeleteXMLAfterEmbed') . '</td>';
|
||||
print '<td>';
|
||||
print $form->selectyesno('EXPORTZUGFERD_DELETE_XML_AFTER_EMBED', getDolGlobalInt('EXPORTZUGFERD_DELETE_XML_AFTER_EMBED', 0), 1);
|
||||
print '<span class="opacitymedium"> - ' . $langs->trans('DeleteXMLAfterEmbedDesc') . '</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Attach to Email
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('AttachZugferdToEmail') . '</td>';
|
||||
print '<td>';
|
||||
print $form->selectyesno('EXPORTZUGFERD_ATTACH_TO_EMAIL', getDolGlobalInt('EXPORTZUGFERD_ATTACH_TO_EMAIL', 0), 1);
|
||||
print '<span class="opacitymedium"> - ' . $langs->trans('AttachZugferdToEmailDesc') . '</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print '<br>';
|
||||
print '<div class="center">';
|
||||
print '<input type="submit" class="button" value="' . $langs->trans('Save') . '">';
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
// Info section
|
||||
print '<br>';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">' . $langs->trans('Information') . '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">' . $langs->trans('SupportedProfiles') . '</td>';
|
||||
print '<td>';
|
||||
print '<ul>';
|
||||
print '<li><strong>ZUGFeRD Minimum:</strong> ' . $langs->trans('ProfileMinimumDesc') . '</li>';
|
||||
print '<li><strong>ZUGFeRD Basic:</strong> ' . $langs->trans('ProfileBasicDesc') . '</li>';
|
||||
print '<li><strong>ZUGFeRD EN16931:</strong> ' . $langs->trans('ProfileEN16931Desc') . '</li>';
|
||||
print '<li><strong>XRechnung:</strong> ' . $langs->trans('ProfileXRechnungDesc') . '</li>';
|
||||
print '</ul>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print dol_get_fiche_end();
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
461
class/actions_exportzugferd.class.php
Executable file
461
class/actions_exportzugferd.class.php
Executable file
|
|
@ -0,0 +1,461 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \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 '<tr class="oddeven">';
|
||||
print '<td class="titlefield">ZUGFeRD/XRechnung</td>';
|
||||
print '<td>';
|
||||
|
||||
if ($xmlExists) {
|
||||
// XML exists - show file info and action buttons
|
||||
print '<span class="fa fa-file-code paddingright" style="color: #28a745;"></span>';
|
||||
print '<a href="' . dol_buildpath('/exportzugferd/download.php', 1) . '?id=' . $object->id . '&action=download_xml" title="' . $langs->trans('DownloadZugferdXML') . '">';
|
||||
print basename($xmlFile);
|
||||
print '</a>';
|
||||
print ' <span class="opacitymedium">(' . dol_print_size(filesize($xmlFile)) . ')</span>';
|
||||
|
||||
// Mini buttons
|
||||
print ' ';
|
||||
print '<a class="paddingleft paddingright" href="' . dol_buildpath('/exportzugferd/preview.php', 1) . '?id=' . $object->id . '" title="' . $langs->trans('PreviewZugferdXML') . '">';
|
||||
print '<span class="fas fa-eye" style="color: #007bff;"></span>';
|
||||
print '</a>';
|
||||
|
||||
print '<a class="paddingleft paddingright" href="' . dol_buildpath('/exportzugferd/download.php', 1) . '?id=' . $object->id . '&action=download_xml" title="' . $langs->trans('DownloadZugferdXML') . '">';
|
||||
print '<span class="fas fa-download" style="color: #17a2b8;"></span>';
|
||||
print '</a>';
|
||||
|
||||
print '<a class="paddingleft paddingright" href="' . dol_buildpath('/exportzugferd/download.php', 1) . '?id=' . $object->id . '&action=generate_xml" title="' . $langs->trans('RegenerateZugferdXML') . '">';
|
||||
print '<span class="fas fa-sync-alt" style="color: #ffc107;"></span>';
|
||||
print '</a>';
|
||||
} else {
|
||||
// XML does not exist - show generate button
|
||||
print '<span class="fa fa-file-code paddingright opacitymedium"></span>';
|
||||
print '<span class="opacitymedium">' . $langs->trans('NoZugferdXML') . '</span>';
|
||||
|
||||
// Generate button
|
||||
print ' ';
|
||||
print '<a class="paddingleft paddingright" href="' . dol_buildpath('/exportzugferd/download.php', 1) . '?id=' . $object->id . '&action=generate_xml" title="' . $langs->trans('GenerateZugferdXML') . '">';
|
||||
print '<span class="fas fa-plus-circle" style="color: #28a745;"></span> ';
|
||||
print '<span style="font-size: 0.9em;">' . $langs->trans('GenerateZugferdXML') . '</span>';
|
||||
print '</a>';
|
||||
}
|
||||
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
327
class/pdfembedder.class.php
Normal file
327
class/pdfembedder.class.php
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \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);
|
||||
}
|
||||
}
|
||||
745
class/zugferdgenerator.class.php
Executable file
745
class/zugferdgenerator.class.php
Executable file
|
|
@ -0,0 +1,745 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \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 = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
||||
$xml .= '<rsm:CrossIndustryInvoice';
|
||||
foreach ($this->namespaces as $prefix => $uri) {
|
||||
$xml .= ' xmlns:' . $prefix . '="' . $uri . '"';
|
||||
}
|
||||
$xml .= '>' . "\n";
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create XML footer
|
||||
*
|
||||
* @return string XML footer
|
||||
*/
|
||||
private function createXMLFooter()
|
||||
{
|
||||
return '</rsm:CrossIndustryInvoice>' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ExchangedDocumentContext element
|
||||
*
|
||||
* @return string XML content
|
||||
*/
|
||||
private function createExchangedDocumentContext()
|
||||
{
|
||||
$profileId = $this->getProfileURN();
|
||||
|
||||
$xml = ' <rsm:ExchangedDocumentContext>' . "\n";
|
||||
$xml .= ' <ram:GuidelineSpecifiedDocumentContextParameter>' . "\n";
|
||||
$xml .= ' <ram:ID>' . $this->xmlEncode($profileId) . '</ram:ID>' . "\n";
|
||||
$xml .= ' </ram:GuidelineSpecifiedDocumentContextParameter>' . "\n";
|
||||
$xml .= ' </rsm:ExchangedDocumentContext>' . "\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 = ' <rsm:ExchangedDocument>' . "\n";
|
||||
$xml .= ' <ram:ID>' . $this->xmlEncode($invoice->ref) . '</ram:ID>' . "\n";
|
||||
$xml .= ' <ram:TypeCode>' . $this->getInvoiceTypeCode($invoice) . '</ram:TypeCode>' . "\n";
|
||||
$xml .= ' <ram:IssueDateTime>' . "\n";
|
||||
$xml .= ' <udt:DateTimeString format="102">' . date('Ymd', $invoice->date) . '</udt:DateTimeString>' . "\n";
|
||||
$xml .= ' </ram:IssueDateTime>' . "\n";
|
||||
|
||||
// Add notes if present
|
||||
if (!empty($invoice->note_public)) {
|
||||
$xml .= ' <ram:IncludedNote>' . "\n";
|
||||
$xml .= ' <ram:Content>' . $this->xmlEncode($invoice->note_public) . '</ram:Content>' . "\n";
|
||||
$xml .= ' </ram:IncludedNote>' . "\n";
|
||||
}
|
||||
|
||||
$xml .= ' </rsm:ExchangedDocument>' . "\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 = ' <rsm:SupplyChainTradeTransaction>' . "\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 .= ' </rsm:SupplyChainTradeTransaction>' . "\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 = ' <ram:IncludedSupplyChainTradeLineItem>' . "\n";
|
||||
|
||||
// Line document
|
||||
$xml .= ' <ram:AssociatedDocumentLineDocument>' . "\n";
|
||||
$xml .= ' <ram:LineID>' . $lineNumber . '</ram:LineID>' . "\n";
|
||||
$xml .= ' </ram:AssociatedDocumentLineDocument>' . "\n";
|
||||
|
||||
// Product info
|
||||
$xml .= ' <ram:SpecifiedTradeProduct>' . "\n";
|
||||
if (!empty($line->product_ref)) {
|
||||
$xml .= ' <ram:SellerAssignedID>' . $this->xmlEncode($line->product_ref) . '</ram:SellerAssignedID>' . "\n";
|
||||
}
|
||||
$xml .= ' <ram:Name>' . $this->xmlEncode($line->product_label ?: $line->desc) . '</ram:Name>' . "\n";
|
||||
if (!empty($line->desc) && $line->desc != $line->product_label) {
|
||||
$xml .= ' <ram:Description>' . $this->xmlEncode(strip_tags($line->desc)) . '</ram:Description>' . "\n";
|
||||
}
|
||||
$xml .= ' </ram:SpecifiedTradeProduct>' . "\n";
|
||||
|
||||
// Line agreement (price)
|
||||
$xml .= ' <ram:SpecifiedLineTradeAgreement>' . "\n";
|
||||
$xml .= ' <ram:NetPriceProductTradePrice>' . "\n";
|
||||
$xml .= ' <ram:ChargeAmount>' . $this->formatAmount($line->subprice) . '</ram:ChargeAmount>' . "\n";
|
||||
$xml .= ' </ram:NetPriceProductTradePrice>' . "\n";
|
||||
$xml .= ' </ram:SpecifiedLineTradeAgreement>' . "\n";
|
||||
|
||||
// Line delivery (quantity)
|
||||
$xml .= ' <ram:SpecifiedLineTradeDelivery>' . "\n";
|
||||
$xml .= ' <ram:BilledQuantity unitCode="' . $this->getUnitCode($line) . '">' . $this->formatQuantity($line->qty) . '</ram:BilledQuantity>' . "\n";
|
||||
$xml .= ' </ram:SpecifiedLineTradeDelivery>' . "\n";
|
||||
|
||||
// Line settlement (tax, total)
|
||||
$xml .= ' <ram:SpecifiedLineTradeSettlement>' . "\n";
|
||||
$xml .= ' <ram:ApplicableTradeTax>' . "\n";
|
||||
$xml .= ' <ram:TypeCode>VAT</ram:TypeCode>' . "\n";
|
||||
$xml .= ' <ram:CategoryCode>' . $this->getVATCategoryCode($line->tva_tx) . '</ram:CategoryCode>' . "\n";
|
||||
$xml .= ' <ram:RateApplicablePercent>' . $this->formatAmount($line->tva_tx) . '</ram:RateApplicablePercent>' . "\n";
|
||||
$xml .= ' </ram:ApplicableTradeTax>' . "\n";
|
||||
$xml .= ' <ram:SpecifiedTradeSettlementLineMonetarySummation>' . "\n";
|
||||
$xml .= ' <ram:LineTotalAmount>' . $this->formatAmount($line->total_ht) . '</ram:LineTotalAmount>' . "\n";
|
||||
$xml .= ' </ram:SpecifiedTradeSettlementLineMonetarySummation>' . "\n";
|
||||
$xml .= ' </ram:SpecifiedLineTradeSettlement>' . "\n";
|
||||
|
||||
$xml .= ' </ram:IncludedSupplyChainTradeLineItem>' . "\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 = ' <ram:ApplicableHeaderTradeAgreement>' . "\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 .= ' <ram:BuyerReference>' . $this->xmlEncode($leitwegId) . '</ram:BuyerReference>' . "\n";
|
||||
}
|
||||
|
||||
// Seller
|
||||
$xml .= ' <ram:SellerTradeParty>' . "\n";
|
||||
$xml .= ' <ram:Name>' . $this->xmlEncode($mysoc->name) . '</ram:Name>' . "\n";
|
||||
|
||||
// Seller address
|
||||
$xml .= ' <ram:PostalTradeAddress>' . "\n";
|
||||
$xml .= ' <ram:PostcodeCode>' . $this->xmlEncode($mysoc->zip) . '</ram:PostcodeCode>' . "\n";
|
||||
$xml .= ' <ram:LineOne>' . $this->xmlEncode($mysoc->address) . '</ram:LineOne>' . "\n";
|
||||
$xml .= ' <ram:CityName>' . $this->xmlEncode($mysoc->town) . '</ram:CityName>' . "\n";
|
||||
$xml .= ' <ram:CountryID>' . $this->xmlEncode($mysoc->country_code) . '</ram:CountryID>' . "\n";
|
||||
$xml .= ' </ram:PostalTradeAddress>' . "\n";
|
||||
|
||||
// Seller tax registration
|
||||
if (!empty($mysoc->tva_intra)) {
|
||||
$xml .= ' <ram:SpecifiedTaxRegistration>' . "\n";
|
||||
$xml .= ' <ram:ID schemeID="VA">' . $this->xmlEncode($mysoc->tva_intra) . '</ram:ID>' . "\n";
|
||||
$xml .= ' </ram:SpecifiedTaxRegistration>' . "\n";
|
||||
}
|
||||
|
||||
$xml .= ' </ram:SellerTradeParty>' . "\n";
|
||||
|
||||
// Buyer
|
||||
$xml .= ' <ram:BuyerTradeParty>' . "\n";
|
||||
$xml .= ' <ram:Name>' . $this->xmlEncode($buyer->name) . '</ram:Name>' . "\n";
|
||||
|
||||
// Buyer address
|
||||
$xml .= ' <ram:PostalTradeAddress>' . "\n";
|
||||
$xml .= ' <ram:PostcodeCode>' . $this->xmlEncode($buyer->zip) . '</ram:PostcodeCode>' . "\n";
|
||||
$xml .= ' <ram:LineOne>' . $this->xmlEncode($buyer->address) . '</ram:LineOne>' . "\n";
|
||||
$xml .= ' <ram:CityName>' . $this->xmlEncode($buyer->town) . '</ram:CityName>' . "\n";
|
||||
$xml .= ' <ram:CountryID>' . $this->xmlEncode($buyer->country_code) . '</ram:CountryID>' . "\n";
|
||||
$xml .= ' </ram:PostalTradeAddress>' . "\n";
|
||||
|
||||
// Buyer tax registration
|
||||
if (!empty($buyer->tva_intra)) {
|
||||
$xml .= ' <ram:SpecifiedTaxRegistration>' . "\n";
|
||||
$xml .= ' <ram:ID schemeID="VA">' . $this->xmlEncode($buyer->tva_intra) . '</ram:ID>' . "\n";
|
||||
$xml .= ' </ram:SpecifiedTaxRegistration>' . "\n";
|
||||
}
|
||||
|
||||
$xml .= ' </ram:BuyerTradeParty>' . "\n";
|
||||
|
||||
// Buyer order reference
|
||||
if (!empty($invoice->ref_client)) {
|
||||
$xml .= ' <ram:BuyerOrderReferencedDocument>' . "\n";
|
||||
$xml .= ' <ram:IssuerAssignedID>' . $this->xmlEncode($invoice->ref_client) . '</ram:IssuerAssignedID>' . "\n";
|
||||
$xml .= ' </ram:BuyerOrderReferencedDocument>' . "\n";
|
||||
}
|
||||
|
||||
$xml .= ' </ram:ApplicableHeaderTradeAgreement>' . "\n";
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ApplicableHeaderTradeDelivery element
|
||||
*
|
||||
* @param Facture $invoice Invoice object
|
||||
* @return string XML content
|
||||
*/
|
||||
private function createApplicableHeaderTradeDelivery($invoice)
|
||||
{
|
||||
$xml = ' <ram:ApplicableHeaderTradeDelivery>' . "\n";
|
||||
|
||||
// Ship to party (same as buyer for now)
|
||||
$buyer = $invoice->thirdparty;
|
||||
$xml .= ' <ram:ShipToTradeParty>' . "\n";
|
||||
$xml .= ' <ram:Name>' . $this->xmlEncode($buyer->name) . '</ram:Name>' . "\n";
|
||||
$xml .= ' <ram:PostalTradeAddress>' . "\n";
|
||||
$xml .= ' <ram:PostcodeCode>' . $this->xmlEncode($buyer->zip) . '</ram:PostcodeCode>' . "\n";
|
||||
$xml .= ' <ram:LineOne>' . $this->xmlEncode($buyer->address) . '</ram:LineOne>' . "\n";
|
||||
$xml .= ' <ram:CityName>' . $this->xmlEncode($buyer->town) . '</ram:CityName>' . "\n";
|
||||
$xml .= ' <ram:CountryID>' . $this->xmlEncode($buyer->country_code) . '</ram:CountryID>' . "\n";
|
||||
$xml .= ' </ram:PostalTradeAddress>' . "\n";
|
||||
$xml .= ' </ram:ShipToTradeParty>' . "\n";
|
||||
|
||||
$xml .= ' </ram:ApplicableHeaderTradeDelivery>' . "\n";
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ApplicableHeaderTradeSettlement element (payment, totals)
|
||||
*
|
||||
* @param Facture $invoice Invoice object
|
||||
* @return string XML content
|
||||
*/
|
||||
private function createApplicableHeaderTradeSettlement($invoice)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$xml = ' <ram:ApplicableHeaderTradeSettlement>' . "\n";
|
||||
|
||||
// Currency
|
||||
$xml .= ' <ram:InvoiceCurrencyCode>' . ($invoice->multicurrency_code ?: $conf->currency) . '</ram:InvoiceCurrencyCode>' . "\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 .= ' <ram:SpecifiedTradeSettlementPaymentMeans>' . "\n";
|
||||
$xml .= ' <ram:TypeCode>' . $this->getPaymentMeansCode($invoice) . '</ram:TypeCode>' . "\n";
|
||||
|
||||
// Bank account for SEPA transfer
|
||||
if (!empty($iban)) {
|
||||
$xml .= ' <ram:PayeePartyCreditorFinancialAccount>' . "\n";
|
||||
$xml .= ' <ram:IBANID>' . $this->xmlEncode($iban) . '</ram:IBANID>' . "\n";
|
||||
$xml .= ' </ram:PayeePartyCreditorFinancialAccount>' . "\n";
|
||||
|
||||
// BIC if available
|
||||
if (!empty($bic)) {
|
||||
$xml .= ' <ram:PayeeSpecifiedCreditorFinancialInstitution>' . "\n";
|
||||
$xml .= ' <ram:BICID>' . $this->xmlEncode($bic) . '</ram:BICID>' . "\n";
|
||||
if (!empty($bankName)) {
|
||||
$xml .= ' <ram:Name>' . $this->xmlEncode($bankName) . '</ram:Name>' . "\n";
|
||||
}
|
||||
$xml .= ' </ram:PayeeSpecifiedCreditorFinancialInstitution>' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$xml .= ' </ram:SpecifiedTradeSettlementPaymentMeans>' . "\n";
|
||||
|
||||
// Tax summary
|
||||
$xml .= $this->createTaxSummary($invoice);
|
||||
|
||||
// Payment terms
|
||||
if (!empty($invoice->cond_reglement_id)) {
|
||||
$xml .= ' <ram:SpecifiedTradePaymentTerms>' . "\n";
|
||||
if ($invoice->date_lim_reglement) {
|
||||
$xml .= ' <ram:DueDateDateTime>' . "\n";
|
||||
$xml .= ' <udt:DateTimeString format="102">' . date('Ymd', $invoice->date_lim_reglement) . '</udt:DateTimeString>' . "\n";
|
||||
$xml .= ' </ram:DueDateDateTime>' . "\n";
|
||||
}
|
||||
$xml .= ' </ram:SpecifiedTradePaymentTerms>' . "\n";
|
||||
}
|
||||
|
||||
// Monetary summation
|
||||
$xml .= ' <ram:SpecifiedTradeSettlementHeaderMonetarySummation>' . "\n";
|
||||
$xml .= ' <ram:LineTotalAmount>' . $this->formatAmount($invoice->total_ht) . '</ram:LineTotalAmount>' . "\n";
|
||||
$xml .= ' <ram:TaxBasisTotalAmount>' . $this->formatAmount($invoice->total_ht) . '</ram:TaxBasisTotalAmount>' . "\n";
|
||||
$xml .= ' <ram:TaxTotalAmount currencyID="' . ($invoice->multicurrency_code ?: $conf->currency) . '">' . $this->formatAmount($invoice->total_tva) . '</ram:TaxTotalAmount>' . "\n";
|
||||
$xml .= ' <ram:GrandTotalAmount>' . $this->formatAmount($invoice->total_ttc) . '</ram:GrandTotalAmount>' . "\n";
|
||||
$xml .= ' <ram:DuePayableAmount>' . $this->formatAmount($invoice->total_ttc - $invoice->getSommePaiement()) . '</ram:DuePayableAmount>' . "\n";
|
||||
$xml .= ' </ram:SpecifiedTradeSettlementHeaderMonetarySummation>' . "\n";
|
||||
|
||||
$xml .= ' </ram:ApplicableHeaderTradeSettlement>' . "\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 .= ' <ram:ApplicableTradeTax>' . "\n";
|
||||
$xml .= ' <ram:CalculatedAmount>' . $this->formatAmount($data['amount']) . '</ram:CalculatedAmount>' . "\n";
|
||||
$xml .= ' <ram:TypeCode>VAT</ram:TypeCode>' . "\n";
|
||||
$xml .= ' <ram:BasisAmount>' . $this->formatAmount($data['base']) . '</ram:BasisAmount>' . "\n";
|
||||
$xml .= ' <ram:CategoryCode>' . $this->getVATCategoryCode($rate) . '</ram:CategoryCode>' . "\n";
|
||||
$xml .= ' <ram:RateApplicablePercent>' . $this->formatAmount($rate) . '</ram:RateApplicablePercent>' . "\n";
|
||||
$xml .= ' </ram:ApplicableTradeTax>' . "\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;
|
||||
}
|
||||
}
|
||||
226
core/modules/modExportZugferd.class.php
Executable file
226
core/modules/modExportZugferd.class.php
Executable file
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
/* Copyright (C) 2004-2018 Laurent Destailleur <eldy@users.sourceforge.net>
|
||||
* Copyright (C) 2018-2019 Nicolas ZABOURI <info@inovea-conseil.com>
|
||||
* Copyright (C) 2019-2024 Frédéric France <frederic.france@free.fr>
|
||||
* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \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);
|
||||
}
|
||||
}
|
||||
152
download.php
Executable file
152
download.php
Executable file
|
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \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;
|
||||
17
img/object_exportzugferd.svg
Normal file
17
img/object_exportzugferd.svg
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#4CAF50;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#2E7D32;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Document base -->
|
||||
<path d="M6 2C4.9 2 4 2.9 4 4v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6H6z" fill="url(#grad1)"/>
|
||||
<!-- Document fold -->
|
||||
<path d="M14 2v6h6" fill="none" stroke="#1B5E20" stroke-width="1"/>
|
||||
<path d="M14 2l6 6h-6V2z" fill="#81C784"/>
|
||||
<!-- XML brackets -->
|
||||
<text x="7" y="15" font-family="monospace" font-size="6" font-weight="bold" fill="white"></></text>
|
||||
<!-- Export arrow -->
|
||||
<path d="M16 14l3 3m0 0l-3 3m3-3H12" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 894 B |
155
index.php
Executable file
155
index.php
Executable file
|
|
@ -0,0 +1,155 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \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 '<div class="fichecenter">';
|
||||
print '<div class="fichethirdleft">';
|
||||
|
||||
// Info box
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">' . $langs->trans('Information') . '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">' . $langs->trans('ZugferdProfile') . '</td>';
|
||||
print '<td>' . getDolGlobalString('EXPORTZUGFERD_PROFILE', 'EN16931') . '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('AutoGenerateZugferd') . '</td>';
|
||||
print '<td>' . yn(getDolGlobalInt('EXPORTZUGFERD_AUTO_GENERATE')) . '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '</div>';
|
||||
print '<div class="fichetwothirdright">';
|
||||
|
||||
// Recent invoices with ZUGFeRD
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="4">' . $langs->trans('RecentInvoicesWithZugferd') . '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// 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 '<tr class="oddeven">';
|
||||
print '<td>' . $invoice->getNomUrl(1) . '</td>';
|
||||
print '<td>' . htmlspecialchars($obj->socname) . '</td>';
|
||||
print '<td class="right">' . price($obj->total_ttc) . ' ' . $conf->currency . '</td>';
|
||||
print '<td class="right">';
|
||||
if ($hasXML) {
|
||||
print '<span class="badge badge-status4">' . $langs->trans('ZugferdXMLFile') . '</span>';
|
||||
} else {
|
||||
print '<a class="button buttongen" href="' . dol_buildpath('/exportzugferd/download.php', 1) . '?id=' . $obj->rowid . '&action=generate_xml">';
|
||||
print $langs->trans('GenerateZugferdXML');
|
||||
print '</a>';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="4" class="opacitymedium">' . $langs->trans('NoRecordFound') . '</td></tr>';
|
||||
}
|
||||
$db->free($resql);
|
||||
} else {
|
||||
dol_print_error($db);
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
78
langs/de_DE/exportzugferd.lang
Executable file
78
langs/de_DE/exportzugferd.lang
Executable file
|
|
@ -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
|
||||
51
langs/en_US/exportzugferd.lang
Executable file
51
langs/en_US/exportzugferd.lang
Executable file
|
|
@ -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
|
||||
53
lib/exportzugferd.lib.php
Executable file
53
lib/exportzugferd.lib.php
Executable file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \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;
|
||||
}
|
||||
404
preview.php
Normal file
404
preview.php
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \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 '<div class="fichecenter">';
|
||||
print '<div class="underbanner clearboth"></div>';
|
||||
|
||||
print '<table class="border centpercent tableforfield">';
|
||||
print '<tr><td class="titlefield">' . $langs->trans('Ref') . '</td>';
|
||||
print '<td>' . $invoice->getNomUrl(1) . '</td></tr>';
|
||||
print '<tr><td>' . $langs->trans('Customer') . '</td>';
|
||||
print '<td>' . $invoice->thirdparty->getNomUrl(1) . '</td></tr>';
|
||||
print '<tr><td>' . $langs->trans('DateInvoice') . '</td>';
|
||||
print '<td>' . dol_print_date($invoice->date, 'day') . '</td></tr>';
|
||||
print '<tr><td>' . $langs->trans('AmountTTC') . '</td>';
|
||||
print '<td>' . price($invoice->total_ttc) . ' ' . $conf->currency . '</td></tr>';
|
||||
print '</table>';
|
||||
|
||||
print '</div>';
|
||||
|
||||
print dol_get_fiche_end();
|
||||
|
||||
// ZUGFeRD XML Preview
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">' . $langs->trans('ZugferdXMLValues') . '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Document Info
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2"><strong>' . $langs->trans('DocumentInfo') . '</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">' . $langs->trans('InvoiceNumber') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($invoice->ref) . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
$typeCode = '380'; // Standard invoice
|
||||
if ($invoice->type == Facture::TYPE_CREDIT_NOTE) {
|
||||
$typeCode = '381';
|
||||
} elseif ($invoice->type == Facture::TYPE_DEPOSIT) {
|
||||
$typeCode = '386';
|
||||
}
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('TypeCode') . '</td>';
|
||||
print '<td><code>' . $typeCode . '</code> (' . ($typeCode == '380' ? 'Commercial Invoice' : ($typeCode == '381' ? 'Credit Note' : 'Deposit')) . ')</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('IssueDate') . '</td>';
|
||||
print '<td><code>' . dol_print_date($invoice->date, '%Y%m%d') . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
if (!empty($invoice->date_lim_reglement)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('DueDate') . '</td>';
|
||||
print '<td><code>' . dol_print_date($invoice->date_lim_reglement, '%Y%m%d') . '</code></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// Seller Info
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2"><strong>' . $langs->trans('Seller') . ' (SellerTradeParty)</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
global $mysoc;
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">' . $langs->trans('Name') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($mysoc->name) . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('Address') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($mysoc->address) . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('Zip') . ' / ' . $langs->trans('Town') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($mysoc->zip) . '</code> / <code>' . htmlspecialchars($mysoc->town) . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('Country') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($mysoc->country_code) . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
if (!empty($mysoc->tva_intra)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('VATIntra') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($mysoc->tva_intra) . '</code></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// Buyer Info
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2"><strong>' . $langs->trans('Customer') . ' (BuyerTradeParty)</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
$buyer = $invoice->thirdparty;
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">' . $langs->trans('Name') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($buyer->name) . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('Address') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($buyer->address) . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('Zip') . ' / ' . $langs->trans('Town') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($buyer->zip) . '</code> / <code>' . htmlspecialchars($buyer->town) . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('Country') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($buyer->country_code) . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
if (!empty($buyer->tva_intra)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('VATIntra') . '</td>';
|
||||
print '<td><code>' . htmlspecialchars($buyer->tva_intra) . '</code></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// Leitweg-ID (for XRechnung)
|
||||
if (!empty($buyer->array_options['options_leitweg_id'])) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>' . $langs->trans('LeitwegID') . ' (BuyerReference)</td>';
|
||||
print '<td><code>' . htmlspecialchars($buyer->array_options['options_leitweg_id']) . '</code></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// Payment Info
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2"><strong>' . $langs->trans('Payment') . '</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
// 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 '<tr class="oddeven">';
|
||||
print '<td class="titlefield">IBAN</td>';
|
||||
print '<td><code>' . htmlspecialchars($bankAccount->iban) . '</code></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
if (!empty($bankAccount->bic)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>BIC</td>';
|
||||
print '<td><code>' . htmlspecialchars($bankAccount->bic) . '</code></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Line Items
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2"><strong>' . $langs->trans('Lines') . ' (IncludedSupplyChainTradeLineItem)</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
$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 '<tr class="oddeven">';
|
||||
print '<td colspan="2" style="background-color: #f5f5f5;"><strong>' . $langs->trans('Line') . ' ' . $lineNo . '</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield paddingleft"> LineID</td>';
|
||||
print '<td><code>' . $lineNo . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
if (!empty($line->fk_product)) {
|
||||
$product = new Product($db);
|
||||
$product->fetch($line->fk_product);
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="paddingleft"> SellerAssignedID</td>';
|
||||
print '<td><code>' . htmlspecialchars($product->ref) . '</code></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="paddingleft"> Name</td>';
|
||||
print '<td><code>' . htmlspecialchars($line->product_label ?: $line->desc) . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="paddingleft"> BilledQuantity</td>';
|
||||
print '<td><code>' . number_format($line->qty, 4, '.', '') . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="paddingleft"> NetPriceAmount (ChargeAmount)</td>';
|
||||
print '<td><code>' . number_format($line->subprice, 2, '.', '') . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="paddingleft"> LineTotalAmount</td>';
|
||||
print '<td><code>' . number_format($line->total_ht, 2, '.', '') . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
$vatRate = $line->tva_tx;
|
||||
$categoryCode = ($vatRate > 0) ? 'S' : 'Z';
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="paddingleft"> VAT (CategoryCode / Rate)</td>';
|
||||
print '<td><code>' . $categoryCode . '</code> / <code>' . number_format($vatRate, 2, '.', '') . '%</code></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// Totals
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2"><strong>' . $langs->trans('Totals') . ' (SpecifiedTradeSettlementHeaderMonetarySummation)</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">LineTotalAmount</td>';
|
||||
print '<td><code>' . number_format($invoice->total_ht, 2, '.', '') . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>TaxBasisTotalAmount</td>';
|
||||
print '<td><code>' . number_format($invoice->total_ht, 2, '.', '') . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>TaxTotalAmount</td>';
|
||||
print '<td><code>' . number_format($invoice->total_tva, 2, '.', '') . '</code> (currencyID="' . $conf->currency . '")</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>GrandTotalAmount</td>';
|
||||
print '<td><code>' . number_format($invoice->total_ttc, 2, '.', '') . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>DuePayableAmount</td>';
|
||||
print '<td><code>' . number_format($invoice->total_ttc - $invoice->getSommePaiement(), 2, '.', '') . '</code></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
// 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 '<br>';
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td>' . $langs->trans('GeneratedXML') . ' (' . basename($xmlFile) . ')</td>';
|
||||
print '</tr>';
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>';
|
||||
print '<pre style="white-space: pre-wrap; word-wrap: break-word; font-size: 11px; max-height: 500px; overflow-y: auto;">';
|
||||
print htmlspecialchars(file_get_contents($xmlFile));
|
||||
print '</pre>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Actions
|
||||
print '<div class="tabsAction">';
|
||||
print '<a class="butAction" href="' . dol_buildpath('/exportzugferd/download.php', 1) . '?id=' . $invoice->id . '&action=generate_xml">';
|
||||
print '<span class="fa fa-sync paddingright"></span>';
|
||||
print $langs->trans('RegenerateZugferdXML');
|
||||
print '</a>';
|
||||
print '<a class="butAction" href="' . dol_buildpath('/exportzugferd/download.php', 1) . '?id=' . $invoice->id . '&action=download_xml">';
|
||||
print '<span class="fa fa-download paddingright"></span>';
|
||||
print $langs->trans('DownloadZugferdXML');
|
||||
print '</a>';
|
||||
print '<a class="butAction" href="' . DOL_URL_ROOT . '/compta/facture/card.php?id=' . $invoice->id . '">';
|
||||
print '<span class="fa fa-arrow-left paddingright"></span>';
|
||||
print $langs->trans('BackToInvoice');
|
||||
print '</a>';
|
||||
print '</div>';
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
Loading…
Reference in a new issue