fix: Cron-Job Stabilität und Logging (v3.6)
- Fehlendes require_once für admin.lib.php hinzugefügt - Dediziertes Cron-Logging unter /documents/importzugferd/logs/ - Shutdown Handler für fatale PHP-Fehler - Robustere Fehlerbehandlung mit try/catch - CHANGELOG.md erstellt Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1e1d6cab20
commit
a2c492d833
3 changed files with 182 additions and 33 deletions
47
CHANGELOG.md
Normal file
47
CHANGELOG.md
Normal file
|
|
@ -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
|
||||
|
|
@ -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,22 +176,35 @@ 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();
|
||||
$this->cronLog("Starting IMAP import...");
|
||||
$this->fetchFromMailbox();
|
||||
$this->cronLog("IMAP import completed");
|
||||
} else {
|
||||
$this->cronLog("IMAP not configured - skipping");
|
||||
}
|
||||
|
||||
// Update last run time
|
||||
|
|
@ -160,7 +222,23 @@ class CronImportZugferd
|
|||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue