dolibarr.bankimport/class/bankimportcron.class.php
data 1fc10d3781 Version 1.1: PDF-Kontoauszüge, Dashboard, Menü-Integration
- Mehrfach-Upload von PDF-Kontoauszügen mit automatischer Metadaten-Erkennung
- Dashboard mit Übersichts-Widgets (letzte Buchungen und Kontoauszüge)
- Menü-Integration unter "Banken und Kasse" statt eigenem Top-Menü
- Erinnerungsfunktion bei veralteten Kontoauszügen (konfigurierbar)
- Verknüpfung von Buchungen mit PDF-Kontoauszügen
- Auszugsnummer wird automatisch aus dem Zeitraum abgeleitet (Monat/Jahr)
- Jahrfilter zeigt nur Jahre mit vorhandenen Kontoauszügen
- Modul-Icon auf fa-money-check-alt gesetzt
- README und ChangeLog aktualisiert
- .gitignore für Kontoauszüge und Build-Artefakte hinzugefügt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 19:11:46 +01:00

379 lines
12 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.
*/
/**
* \file bankimport/class/bankimportcron.class.php
* \ingroup bankimport
* \brief Cron job class for automatic bank statement import
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
dol_include_once('/bankimport/class/fints.class.php');
dol_include_once('/bankimport/class/banktransaction.class.php');
/**
* Class BankImportCron
* Handles automatic bank statement import via scheduled task
*/
class BankImportCron
{
/**
* @var DoliDB Database handler
*/
public $db;
/**
* @var string Error message
*/
public $error = '';
/**
* @var array Error messages
*/
public $errors = array();
/**
* @var string Output message for cron log
*/
public $output = '';
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Execute the automatic import cron job
* Called by Dolibarr's scheduled task system
*
* @return int 0 if OK, < 0 if error
*/
public function doAutoImport()
{
global $conf, $langs, $user;
$langs->load('bankimport@bankimport');
dol_syslog("BankImportCron::doAutoImport - Starting automatic import", LOG_INFO);
// Check if automatic import is enabled
if (!getDolGlobalInt('BANKIMPORT_AUTO_ENABLED')) {
$this->output = $langs->trans('AutoImportDisabled');
dol_syslog("BankImportCron::doAutoImport - Auto import is disabled", LOG_INFO);
return 0;
}
// Initialize FinTS
$fints = new BankImportFinTS($this->db);
if (!$fints->isConfigured()) {
$this->error = $langs->trans('AutoImportNotConfigured');
dol_syslog("BankImportCron::doAutoImport - FinTS not configured", LOG_WARNING);
$this->setNotification('config_error');
return -1;
}
if (!$fints->isLibraryAvailable()) {
$this->error = $langs->trans('FinTSLibraryNotFound');
dol_syslog("BankImportCron::doAutoImport - Library not found", LOG_ERR);
return -1;
}
// Check for stored session state (from previous successful TAN)
$storedState = getDolGlobalString('BANKIMPORT_CRON_STATE');
$sessionRestored = false;
try {
// Try to restore previous session
if (!empty($storedState)) {
dol_syslog("BankImportCron::doAutoImport - Attempting to restore previous session (state length: ".strlen($storedState).")", LOG_INFO);
$restoreResult = $fints->restore($storedState);
if ($restoreResult < 0) {
// Session expired, need fresh login
dol_syslog("BankImportCron::doAutoImport - Session restore failed: ".$fints->error.", trying fresh login", LOG_WARNING);
$storedState = '';
// Clear stale session
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_STATE', $conf->entity);
} else {
dol_syslog("BankImportCron::doAutoImport - Session restored successfully", LOG_INFO);
$sessionRestored = true;
}
} else {
dol_syslog("BankImportCron::doAutoImport - No stored session found", LOG_INFO);
}
// If no stored session or restore failed, try fresh login
if (empty($storedState)) {
dol_syslog("BankImportCron::doAutoImport - Attempting fresh login", LOG_INFO);
$loginResult = $fints->login();
if ($loginResult < 0) {
$this->error = $langs->trans('LoginFailed').': '.$fints->error;
dol_syslog("BankImportCron::doAutoImport - Login failed: ".$fints->error, LOG_ERR);
$this->setNotification('login_error');
return -1;
}
if ($loginResult == 0) {
// TAN required - can't proceed automatically
$this->output = $langs->trans('TANRequired');
dol_syslog("BankImportCron::doAutoImport - TAN required for login, cannot proceed automatically", LOG_WARNING);
$this->setNotification('tan_required');
// Store the state so user can complete TAN manually
$state = $fints->persist();
if (!empty($state)) {
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $state, 'chaine', 0, '', $conf->entity);
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', serialize($fints->getPendingAction()), 'chaine', 0, '', $conf->entity);
}
return 0; // Not an error, just can't proceed
}
dol_syslog("BankImportCron::doAutoImport - Fresh login successful", LOG_INFO);
}
// Login successful or session restored - fetch statements
$daysToFetch = getDolGlobalInt('BANKIMPORT_AUTO_DAYS') ?: 30;
$dateFrom = strtotime("-{$daysToFetch} days");
$dateTo = time();
dol_syslog("BankImportCron::doAutoImport - Fetching statements from ".date('Y-m-d', $dateFrom)." to ".date('Y-m-d', $dateTo)." ({$daysToFetch} days)", LOG_INFO);
$result = $fints->fetchStatements($dateFrom, $dateTo);
// Log what we got back
if (is_array($result)) {
$txCount = count($result['transactions'] ?? array());
$hasBalance = !empty($result['balance']);
$isPartial = !empty($result['partial']);
dol_syslog("BankImportCron::doAutoImport - fetchStatements returned array: transactions={$txCount}, hasBalance=".($hasBalance?'yes':'no').", partial=".($isPartial?'yes':'no'), LOG_INFO);
} else {
dol_syslog("BankImportCron::doAutoImport - fetchStatements returned: ".var_export($result, true), LOG_INFO);
}
if ($result === 0) {
// TAN required for statements
$this->output = $langs->trans('TANRequired');
dol_syslog("BankImportCron::doAutoImport - TAN required for statements", LOG_WARNING);
$this->setNotification('tan_required');
// Store state for manual completion
$state = $fints->persist();
if (!empty($state)) {
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $state, 'chaine', 0, '', $conf->entity);
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', serialize($fints->getPendingAction()), 'chaine', 0, '', $conf->entity);
}
return 0;
}
if ($result < 0) {
$this->error = $langs->trans('FetchFailed').': '.$fints->error;
dol_syslog("BankImportCron::doAutoImport - Fetch failed: ".$fints->error, LOG_ERR);
$this->setNotification('fetch_error');
return -1;
}
// Success - import transactions
$transactions = $result['transactions'] ?? array();
$fetchedCount = count($transactions);
$importedCount = 0;
$skippedCount = 0;
dol_syslog("BankImportCron::doAutoImport - Bank returned {$fetchedCount} transactions", LOG_INFO);
// If restored session returned 0 transactions, the session might be stale
// Try fresh login as fallback
if ($fetchedCount == 0 && $sessionRestored) {
dol_syslog("BankImportCron::doAutoImport - Restored session returned 0 transactions, clearing stale session", LOG_WARNING);
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_STATE', $conf->entity);
// Note: We don't retry here because a fresh login would require TAN
// Just mark that the session was stale
$this->output = $langs->trans('AutoImportNoTransactions').' (Session abgelaufen - nächster Lauf erfordert TAN)';
$this->setNotification('session_expired');
return 0;
}
if (!empty($transactions)) {
// Get a system user for the import
$importUser = new User($this->db);
$importUser->fetch(1); // Admin user
$iban = $fints->getIban();
// Use the importFromFinTS method for correct field mapping
$transImporter = new BankImportTransaction($this->db);
$importResult = $transImporter->importFromFinTS($transactions, $iban, $importUser);
$importedCount = $importResult['imported'] ?? 0;
$skippedCount = $importResult['skipped'] ?? 0;
dol_syslog("BankImportCron::doAutoImport - Import result: imported={$importedCount}, skipped={$skippedCount}", LOG_INFO);
}
// Update last fetch info
dolibarr_set_const($this->db, 'BANKIMPORT_LAST_FETCH', time(), 'chaine', 0, '', $conf->entity);
dolibarr_set_const($this->db, 'BANKIMPORT_LAST_FETCH_COUNT', $importedCount, 'chaine', 0, '', $conf->entity);
// Store session for next run (might avoid TAN)
$state = $fints->persist();
if (!empty($state)) {
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_STATE', $state, 'chaine', 0, '', $conf->entity);
}
// Clear any pending state
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity);
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity);
// Clear notification flag on success
$this->clearNotification();
$fints->close();
if ($importedCount > 0) {
$this->output = $langs->trans('AutoImportSuccess', $importedCount);
} elseif ($skippedCount > 0) {
$this->output = $langs->trans('AutoImportNoTransactions').' ('.$skippedCount.' bereits vorhanden)';
} else {
$this->output = $langs->trans('AutoImportNoTransactions');
}
dol_syslog("BankImportCron::doAutoImport - Completed: imported={$importedCount}, skipped={$skippedCount}", LOG_INFO);
return 0;
} catch (Exception $e) {
$this->error = 'Exception: '.$e->getMessage();
dol_syslog("BankImportCron::doAutoImport - Exception: ".$e->getMessage(), LOG_ERR);
$this->setNotification('error');
return -1;
}
}
/**
* Set notification flag for admin users
*
* @param string $type Notification type (tan_required, error, etc.)
* @return void
*/
private function setNotification($type)
{
global $conf;
dolibarr_set_const($this->db, 'BANKIMPORT_NOTIFICATION', $type, 'chaine', 0, '', $conf->entity);
dolibarr_set_const($this->db, 'BANKIMPORT_NOTIFICATION_DATE', time(), 'chaine', 0, '', $conf->entity);
}
/**
* Clear notification flag
*
* @return void
*/
private function clearNotification()
{
global $conf;
dolibarr_del_const($this->db, 'BANKIMPORT_NOTIFICATION', $conf->entity);
dolibarr_del_const($this->db, 'BANKIMPORT_NOTIFICATION_DATE', $conf->entity);
}
/**
* Check if there's a pending notification
*
* @return array|null Notification info or null
*/
public static function getNotification()
{
$type = getDolGlobalString('BANKIMPORT_NOTIFICATION');
$date = getDolGlobalInt('BANKIMPORT_NOTIFICATION_DATE');
if (empty($type)) {
return null;
}
return array(
'type' => $type,
'date' => $date
);
}
/**
* Resume a pending TAN action (called from web interface)
*
* @return int 1 if TAN confirmed and import done, 0 if still waiting, -1 if error
*/
public function resumePendingAction()
{
global $conf, $langs, $user;
$pendingState = getDolGlobalString('BANKIMPORT_CRON_PENDING_STATE');
$pendingAction = getDolGlobalString('BANKIMPORT_CRON_PENDING_ACTION');
if (empty($pendingState) || empty($pendingAction)) {
$this->error = 'No pending action';
return -1;
}
$fints = new BankImportFinTS($this->db);
$restoreResult = $fints->restore($pendingState);
if ($restoreResult < 0) {
$this->error = $fints->error;
// Clear expired state
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity);
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity);
return -1;
}
$action = @unserialize($pendingAction);
if ($action === false) {
$this->error = 'Could not restore pending action';
return -1;
}
$fints->setPendingAction($action);
// Check if TAN was confirmed
$checkResult = $fints->checkDecoupledTan();
if ($checkResult == 0) {
// Still waiting
// Update state
$newState = $fints->persist();
if (!empty($newState)) {
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $newState, 'chaine', 0, '', $conf->entity);
}
return 0;
}
if ($checkResult < 0) {
$this->error = $fints->error;
return -1;
}
// TAN confirmed - now run the import
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity);
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity);
// Store the confirmed session and run import
$state = $fints->persist();
if (!empty($state)) {
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_STATE', $state, 'chaine', 0, '', $conf->entity);
}
return $this->doAutoImport();
}
}