- 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>
461 lines
14 KiB
PHP
Executable file
461 lines
14 KiB
PHP
Executable file
<?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;
|
|
}
|
|
}
|