- 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>
379 lines
12 KiB
PHP
Executable file
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();
|
|
}
|
|
}
|