diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d577f2d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changelog + +Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert. + +## [3.6] - 2026-02-23 + +### Behoben +- **Cron-Job Fix**: Fehlendes `require_once` für `admin.lib.php` hinzugefügt - verhinderte das Speichern des letzten Laufzeitpunkts +- Cron-Job lief in Endlosschleife weil `dolibarr_set_const()` nicht gefunden wurde + +### Hinzugefügt +- **Dediziertes Cron-Logging**: Separate Log-Datei unter `/documents/importzugferd/logs/cron_importzugferd.log` +- **Shutdown Handler**: Fängt fatale PHP-Fehler ab und protokolliert sie +- **Detailliertes Logging**: Zeigt jeden Schritt des Import-Prozesses (Ordner-Zugriff, PDF-Scan, IMAP-Status) + +### Verbessert +- Robustere Fehlerbehandlung mit try/catch für Exceptions und Throwables +- IMAP-Import wird nur ausgeführt wenn tatsächlich konfiguriert + +## [3.5] - 2026-02-15 + +### Hinzugefügt +- Automatischer Cron-Import aus Watch-Folder +- IMAP-Mailbox-Unterstützung für E-Mail-Rechnungen +- Konfigurierbare Import-Frequenz (stündlich, täglich, wöchentlich) +- Archiv- und Fehler-Ordner für verarbeitete Dateien + +## [3.0] - 2026-02-01 + +### Hinzugefügt +- ZUGFeRD/Factur-X PDF-Parsing +- Automatische Lieferanten-Erkennung +- Rechnungsvorschau vor Import +- Datanorm-Integration für Artikelpreise + +## [2.0] - 2026-01-15 + +### Hinzugefügt +- Basis-Import von ZUGFeRD-Rechnungen +- Manuelle Datei-Auswahl +- Integration in Lieferantenrechnungen + +## [1.0] - 2026-01-01 + +### Erste Version +- Grundlegende ZUGFeRD-Erkennung +- XML-Extraktion aus PDF diff --git a/class/cron_importzugferd.class.php b/class/cron_importzugferd.class.php index 36073aa..0c9262f 100755 --- a/class/cron_importzugferd.class.php +++ b/class/cron_importzugferd.class.php @@ -14,6 +14,7 @@ */ require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php'; dol_include_once('/importzugferd/class/zugferdparser.class.php'); dol_include_once('/importzugferd/class/zugferdimport.class.php'); @@ -60,6 +61,16 @@ class CronImportZugferd */ public $error_count = 0; + /** + * @var string Path to cron log file + */ + private $cronLogFile = ''; + + /** + * @var float Start time of cron execution + */ + private $startTime = 0; + /** * Constructor * @@ -67,7 +78,45 @@ class CronImportZugferd */ public function __construct($db) { + global $conf; $this->db = $db; + + // Set up dedicated log file for cron jobs + $logDir = $conf->importzugferd->dir_output.'/logs'; + if (!is_dir($logDir)) { + dol_mkdir($logDir); + } + $this->cronLogFile = $logDir.'/cron_importzugferd.log'; + } + + /** + * Write to dedicated cron log file + * + * @param string $message Log message + * @param string $level Log level (INFO, WARNING, ERROR, DEBUG) + * @return void + */ + private function cronLog($message, $level = 'INFO') + { + $timestamp = date('Y-m-d H:i:s'); + $elapsed = $this->startTime > 0 ? round(microtime(true) - $this->startTime, 2).'s' : '0s'; + $logLine = "[{$timestamp}] [{$level}] [{$elapsed}] {$message}\n"; + + @file_put_contents($this->cronLogFile, $logLine, FILE_APPEND | LOCK_EX); + dol_syslog("CronImportZugferd: ".$message, $level === 'ERROR' ? LOG_ERR : ($level === 'WARNING' ? LOG_WARNING : LOG_INFO)); + } + + /** + * Shutdown handler to catch fatal errors + */ + public function handleShutdown() + { + $error = error_get_last(); + if ($error !== null && in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) { + $message = "FATAL SHUTDOWN: {$error['message']} in {$error['file']}:{$error['line']}"; + $this->cronLog($message, 'ERROR'); + $this->cronLog("========== CRON END (fatal shutdown) =========="); + } } /** @@ -127,40 +176,69 @@ class CronImportZugferd */ public function runScheduledImport() { - global $conf, $user, $langs; + global $langs; + + // Initialize timing and shutdown handler + $this->startTime = microtime(true); + register_shutdown_function(array($this, 'handleShutdown')); $langs->load('importzugferd@importzugferd'); + $this->cronLog("========== CRON START =========="); + $this->cronLog("PHP Version: ".PHP_VERSION.", Memory Limit: ".ini_get('memory_limit')); + // Reset counters $this->imported_count = 0; $this->skipped_count = 0; $this->error_count = 0; $this->errors = array(); - $folderResult = $this->importFromFolder(); + try { + $this->cronLog("Starting folder import..."); + $this->importFromFolder(); + $this->cronLog("Folder import completed"); - // IMAP nur wenn konfiguriert - $mailboxResult = 0; - if (!empty(getDolGlobalString('IMPORTZUGFERD_IMAP_HOST'))) { - $mailboxResult = $this->fetchFromMailbox(); + // IMAP nur wenn konfiguriert + if (!empty(getDolGlobalString('IMPORTZUGFERD_IMAP_HOST'))) { + $this->cronLog("Starting IMAP import..."); + $this->fetchFromMailbox(); + $this->cronLog("IMAP import completed"); + } else { + $this->cronLog("IMAP not configured - skipping"); + } + + // Update last run time + $this->updateLastRunTime(); + + // Build combined output + $this->output = sprintf( + "Scheduled import complete. Imported: %d, Skipped: %d, Errors: %d", + $this->imported_count, + $this->skipped_count, + $this->error_count + ); + + if ($this->error_count > 0 && !empty($this->errors)) { + $this->output .= "\nErrors: " . implode(", ", array_slice($this->errors, 0, 5)); + } + + $duration = round(microtime(true) - $this->startTime, 2); + $this->cronLog("Completed: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}, duration={$duration}s"); + $this->cronLog("========== CRON END (success) =========="); + + return ($this->error_count > 0) ? -1 : 0; + + } catch (Exception $e) { + $this->error = 'Exception: '.$e->getMessage(); + $this->cronLog("EXCEPTION: ".$e->getMessage()."\n".$e->getTraceAsString(), 'ERROR'); + $this->cronLog("========== CRON END (exception) =========="); + return -1; + } catch (Throwable $t) { + $this->error = 'Fatal: '.$t->getMessage(); + $this->cronLog("FATAL: ".$t->getMessage()."\n".$t->getTraceAsString(), 'ERROR'); + $this->cronLog("========== CRON END (fatal) =========="); + return -1; } - - // Update last run time - $this->updateLastRunTime(); - - // Build combined output - $this->output = sprintf( - "Scheduled import complete. Imported: %d, Skipped: %d, Errors: %d", - $this->imported_count, - $this->skipped_count, - $this->error_count - ); - - if ($this->error_count > 0 && !empty($this->errors)) { - $this->output .= "\nErrors: " . implode(", ", array_slice($this->errors, 0, 5)); - } - - return ($this->error_count > 0) ? -1 : 0; } /** @@ -170,7 +248,7 @@ class CronImportZugferd */ public function importFromFolder() { - global $conf, $user, $langs; + global $langs; $langs->load('importzugferd@importzugferd'); @@ -179,24 +257,45 @@ class CronImportZugferd $errorFolder = getDolGlobalString('IMPORTZUGFERD_ERROR_FOLDER'); $autoCreate = getDolGlobalString('IMPORTZUGFERD_AUTO_CREATE_INVOICE'); + $this->cronLog("Watch folder: {$watchFolder}"); + // Validate settings - if (empty($watchFolder) || !is_dir($watchFolder)) { - $this->output = 'Watch folder not configured or not accessible'; - return 0; // Not an error, just not configured + if (empty($watchFolder)) { + $this->cronLog("Watch folder not configured - skipping"); + $this->output = 'Watch folder not configured'; + return 0; } + if (!is_dir($watchFolder)) { + $this->cronLog("Watch folder not accessible: {$watchFolder}", 'WARNING'); + $this->output = 'Watch folder not accessible'; + return 0; + } + + $this->cronLog("Watch folder accessible, scanning for PDFs..."); + // Load admin user for import actions $admin_user = new User($this->db); $admin_user->fetch(1); // Find PDF files + $this->cronLog("Running glob for *.pdf..."); $files = glob($watchFolder . '/*.pdf'); - $files = array_merge($files, glob($watchFolder . '/*.PDF')); + $this->cronLog("Found ".count($files)." .pdf files"); + + $this->cronLog("Running glob for *.PDF..."); + $filesUpper = glob($watchFolder . '/*.PDF'); + $this->cronLog("Found ".count($filesUpper)." .PDF files"); + + $files = array_merge($files, $filesUpper); if (empty($files)) { + $this->cronLog("No PDF files found in watch folder"); return 0; } + $this->cronLog("Total ".count($files)." PDF files to process"); + // Ensure archive folder exists if configured if (!empty($archiveFolder) && !is_dir($archiveFolder)) { dol_mkdir($archiveFolder); @@ -208,20 +307,22 @@ class CronImportZugferd } foreach ($files as $file) { + $this->cronLog("Processing: ".basename($file)); + // Use ZugferdImport::importFromFile for consistent handling $import = new ZugferdImport($this->db); - $result = $import->importFromFile($admin_user, $file, $autoCreate); + $result = $import->importFromFile($admin_user, $file, !empty($autoCreate)); if ($result > 0) { $this->imported_count++; - dol_syslog("CronImportZugferd: Imported invoice from folder: " . basename($file), LOG_INFO); + $this->cronLog("Imported: ".basename($file)." -> ID {$result}"); // Archive the file $this->moveFile($file, $archiveFolder, 'imported_'); } elseif ($result == -2) { // Duplicate - already imported $this->skipped_count++; - dol_syslog("CronImportZugferd: Skipped duplicate invoice: " . basename($file), LOG_INFO); + $this->cronLog("Skipped (duplicate): ".basename($file)); // Archive duplicates - delete if no archive folder if (!$this->moveFile($file, $archiveFolder, 'duplicate_')) { @@ -230,7 +331,7 @@ class CronImportZugferd } else { $this->error_count++; $this->errors[] = basename($file) . ': ' . $import->error; - dol_syslog("CronImportZugferd: Error importing: " . basename($file) . " - " . $import->error, LOG_WARNING); + $this->cronLog("Error importing ".basename($file).": ".$import->error, 'ERROR'); // Try error folder first, fall back to archive folder if (!$this->moveFile($file, $errorFolder, 'error_')) { @@ -240,6 +341,7 @@ class CronImportZugferd } } + $this->cronLog("Folder import finished: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}"); return 0; } diff --git a/core/modules/modImportZugferd.class.php b/core/modules/modImportZugferd.class.php index eb56629..2900452 100755 --- a/core/modules/modImportZugferd.class.php +++ b/core/modules/modImportZugferd.class.php @@ -76,7 +76,7 @@ class modImportZugferd extends DolibarrModules $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@importzugferd' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '3.5'; + $this->version = '3.6'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt';