* * 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/pdfstatements.php * \ingroup bankimport * \brief Page to upload and manage PDF bank statements */ // Load Dolibarr environment $res = 0; if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; } $tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1; while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; } if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; } if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; } if (!$res && file_exists("../main.inc.php")) { $res = @include "../main.inc.php"; } if (!$res && file_exists("../../main.inc.php")) { $res = @include "../../main.inc.php"; } if (!$res) { die("Include of main fails"); } require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php'; dol_include_once('/bankimport/class/bankstatement.class.php'); dol_include_once('/bankimport/class/fints.class.php'); dol_include_once('/bankimport/lib/bankimport.lib.php'); /** * @var Conf $conf * @var DoliDB $db * @var Translate $langs * @var User $user */ $langs->loadLangs(array("bankimport@bankimport", "banks", "other")); $action = GETPOST('action', 'aZ09'); $confirm = GETPOST('confirm', 'alpha'); $year = GETPOSTISSET('year') ? GETPOSTINT('year') : (int) date('Y'); // Security check if (!$user->hasRight('bankimport', 'read')) { accessforbidden(); } /* * Actions */ $statement = new BankImportStatement($db); // FinTS: Elektronische Kontoauszuege automatisch abrufen (HKEKP) if ($action == 'fetchfints') { dol_syslog("BankImport HKEKP: ========== START fetchfints Action ==========", LOG_DEBUG); dol_syslog("BankImport HKEKP: User=".$user->login." (ID ".$user->id."), Zeitpunkt=".date('Y-m-d H:i:s'), LOG_DEBUG); if (!$user->hasRight('bankimport', 'write')) { dol_syslog("BankImport HKEKP: Zugriff verweigert - User hat kein Schreibrecht", LOG_WARNING); accessforbidden(); } $fints = new BankImportFinTS($db); dol_syslog("BankImport HKEKP: FinTS-Objekt erstellt, isConfigured=".($fints->isConfigured() ? 'JA' : 'NEIN') .", isLibraryAvailable=".($fints->isLibraryAvailable() ? 'JA' : 'NEIN'), LOG_DEBUG); dol_syslog("BankImport HKEKP: Konfiguration - URL=".getDolGlobalString('BANKIMPORT_FINTS_URL') .", BLZ=".getDolGlobalString('BANKIMPORT_FINTS_BLZ') .", IBAN=".getDolGlobalString('BANKIMPORT_FINTS_IBAN') .", User=".getDolGlobalString('BANKIMPORT_FINTS_USERNAME'), LOG_DEBUG); if (!$fints->isConfigured()) { dol_syslog("BankImport HKEKP: ABBRUCH - FinTS nicht konfiguriert", LOG_WARNING); setEventMessages($langs->trans("FinTSNotConfigured"), null, 'errors'); } elseif (!$fints->isLibraryAvailable()) { dol_syslog("BankImport HKEKP: ABBRUCH - phpFinTS Library fehlt", LOG_WARNING); setEventMessages($langs->trans("FinTSLibraryMissing"), null, 'errors'); } else { // Login dol_syslog("BankImport HKEKP: Starte login()...", LOG_DEBUG); $loginResult = $fints->login(); dol_syslog("BankImport HKEKP: login() Ergebnis=".$loginResult." (1=OK, 0=TAN, -1=Fehler)", LOG_DEBUG); if ($loginResult == -1) { dol_syslog("BankImport HKEKP: Login FEHLGESCHLAGEN - ".$fints->error, LOG_ERR); setEventMessages('FinTS Login fehlgeschlagen: '.$fints->error, null, 'errors'); } if ($loginResult == 0) { // TAN benoetigt - Decoupled-Polling $tanConfirmed = false; $maxWait = 120; // Max 2 Minuten warten $waited = 0; if ($fints->selectedTanMode && $fints->selectedTanMode->isDecoupled()) { dol_syslog("BankImport HKEKP: TAN-Modus: ".$fints->selectedTanMode->getName() ." (ID ".$fints->selectedTanMode->getId().", Decoupled=JA)", LOG_DEBUG); dol_syslog("BankImport HKEKP: Starte Decoupled-TAN-Polling (max ".$maxWait."s, Intervall 3s)...", LOG_DEBUG); setEventMessages($langs->trans("WaitingForTanConfirmation"), null, 'mesgs'); while ($waited < $maxWait) { sleep(3); $waited += 3; $tanStatus = $fints->checkDecoupledTan(); dol_syslog("BankImport HKEKP: TAN-Poll nach ".$waited."s - Status=".$tanStatus." (1=OK, 0=Wartend, -1=Fehler)", LOG_DEBUG); if ($tanStatus == 1) { $tanConfirmed = true; dol_syslog("BankImport HKEKP: TAN BESTAETIGT nach ".$waited."s", LOG_DEBUG); break; } elseif ($tanStatus < 0) { dol_syslog("BankImport HKEKP: TAN-Pruefung FEHLGESCHLAGEN: ".$fints->error, LOG_ERR); setEventMessages($langs->trans("TanCheckFailed").': '.$fints->error, null, 'errors'); break; } } if (!$tanConfirmed && $waited >= $maxWait) { dol_syslog("BankImport HKEKP: TAN-TIMEOUT nach ".$waited."s", LOG_WARNING); setEventMessages($langs->trans("TanTimeout"), null, 'errors'); } } else { $tanModeName = $fints->selectedTanMode ? $fints->selectedTanMode->getName() : 'UNBEKANNT'; $tanModeDecoupled = $fints->selectedTanMode ? ($fints->selectedTanMode->isDecoupled() ? 'JA' : 'NEIN') : '?'; dol_syslog("BankImport HKEKP: TAN-Modus: ".$tanModeName." (Decoupled=".$tanModeDecoupled.") - Manuell benoetigt!", LOG_DEBUG); dol_syslog("BankImport HKEKP: TAN-Challenge: ".$fints->tanChallenge, LOG_DEBUG); setEventMessages($langs->trans("TanRequired").': '.$fints->tanChallenge, null, 'warnings'); } if (!$tanConfirmed) { dol_syslog("BankImport HKEKP: TAN nicht bestaetigt, schliesse Verbindung", LOG_DEBUG); $fints->close(); $action = ''; } else { $loginResult = 1; // Weiter mit Abruf } } if ($loginResult == 1) { // Kontoauszuege abrufen $fetchYear = GETPOST('fetch_year', 'alpha') ?: null; dol_syslog("BankImport HKEKP: Login erfolgreich, starte fetchBankStatements(year=" .($fetchYear ?: 'ALLE').")", LOG_DEBUG); $result = $fints->fetchBankStatements(null, $fetchYear); dol_syslog("BankImport HKEKP: fetchBankStatements() Ergebnis-Typ=".gettype($result) .(is_array($result) ? " count=".$result['count'] : " val=".$result), LOG_DEBUG); if ($result === 0) { dol_syslog("BankImport HKEKP: fetchBankStatements benoetigt TAN", LOG_WARNING); setEventMessages($langs->trans("TanRequiredForStatements"), null, 'warnings'); } elseif ($result === -1) { dol_syslog("BankImport HKEKP: fetchBankStatements FEHLGESCHLAGEN: ".$fints->error, LOG_ERR); setEventMessages($langs->trans("FetchStatementsFailed").': '.$fints->error, null, 'errors'); } elseif (is_array($result)) { $pdfCount = $result['count']; $savedCount = 0; $errorCountFints = 0; dol_syslog("BankImport HKEKP: ".$pdfCount." PDFs empfangen, IBAN=".$result['iban'], LOG_DEBUG); if ($pdfCount == 0) { dol_syslog("BankImport HKEKP: Keine Auszuege verfuegbar", LOG_DEBUG); setEventMessages($langs->trans("NoStatementsAvailable"), null, 'warnings'); } else { // PDFs speichern ueber die bestehende bankstatement-Logik $dir = BankImportStatement::getStorageDir(); dol_syslog("BankImport HKEKP: Speicher-Verzeichnis: ".$dir, LOG_DEBUG); // Identische PDFs deduplizieren (Bank sendet teilweise Duplikate) $seenHashes = []; $uniquePdfs = []; foreach ($result['pdfs'] as $pdfData) { $hash = md5($pdfData); if (!isset($seenHashes[$hash])) { $seenHashes[$hash] = true; $uniquePdfs[] = $pdfData; } else { dol_syslog("BankImport HKEKP: Duplikat-PDF uebersprungen (Hash=".substr($hash, 0, 8)."...)", LOG_DEBUG); } } $result['pdfs'] = $uniquePdfs; $pdfCount = count($uniquePdfs); $skippedCount = 0; foreach ($result['pdfs'] as $idx => $pdfData) { dol_syslog("BankImport HKEKP: --- PDF ".($idx+1)."/".$pdfCount." ---", LOG_DEBUG); dol_syslog("BankImport HKEKP: PDF-Groesse=".strlen($pdfData)." Bytes, Erste 20 Bytes=".bin2hex(substr($pdfData, 0, 20)), LOG_DEBUG); dol_syslog("BankImport HKEKP: PDF startet mit: ".substr($pdfData, 0, 10), LOG_DEBUG); // PDF in Temp-Datei schreiben fuer Metadaten-Extraktion $tmpFile = tempnam(sys_get_temp_dir(), 'fints_stmt_'); file_put_contents($tmpFile, $pdfData); dol_syslog("BankImport HKEKP: Temp-Datei: ".$tmpFile." (".filesize($tmpFile)." Bytes)", LOG_DEBUG); // Metadaten aus PDF parsen dol_syslog("BankImport HKEKP: Starte parsePdfMetadata()...", LOG_DEBUG); $parsed = BankImportStatement::parsePdfMetadata($tmpFile); if ($parsed) { dol_syslog("BankImport HKEKP: Metadaten erkannt: IBAN=".$parsed['iban'] .", Nr=".$parsed['statement_number'].", Jahr=".$parsed['statement_year'] .", Datum=".$parsed['statement_date'] .", Von=".$parsed['date_from'].", Bis=".$parsed['date_to'] .", Saldo_Start=".$parsed['opening_balance'].", Saldo_Ende=".$parsed['closing_balance'], LOG_DEBUG); } else { dol_syslog("BankImport HKEKP: Metadaten NICHT erkannt - verwende Fallback", LOG_WARNING); } $stmt = new BankImportStatement($db); if ($parsed && !empty($parsed['statement_number'])) { // Vollstaendige Metadaten mit Auszugsnummer $stmt->iban = $parsed['iban'] ?: $result['iban']; $stmt->statement_number = $parsed['statement_number']; $stmt->statement_year = $parsed['statement_year']; $stmt->statement_date = $parsed['statement_date']; $stmt->date_from = $parsed['date_from']; $stmt->date_to = $parsed['date_to']; $stmt->opening_balance = $parsed['opening_balance']; $stmt->closing_balance = $parsed['closing_balance']; } elseif ($parsed && !empty($parsed['iban'])) { // IBAN erkannt aber keine Auszugsnummer (z.B. Saldenmitteilung) // Ueberspringe solche PDFs - ohne Nummer nicht sinnvoll speicherbar dol_syslog("BankImport HKEKP: PDF uebersprungen - IBAN erkannt aber keine Auszugsnummer (wahrscheinlich Saldenmitteilung)", LOG_WARNING); $skippedCount++; @unlink($tmpFile); continue; } else { // Keinerlei Metadaten - Fallback mit Index $stmt->iban = $result['iban']; $stmt->statement_number = (string) ($idx + 1); $stmt->statement_year = (int) date('Y'); } $stmt->import_key = 'fints_'.date('YmdHis').'_'.$user->id; dol_syslog("BankImport HKEKP: import_key=".$stmt->import_key, LOG_DEBUG); // Duplikat-Pruefung if ($stmt->statement_number && $stmt->exists()) { dol_syslog("BankImport HKEKP: DUPLIKAT - Auszug ".$stmt->statement_number."/".$stmt->statement_year." existiert bereits, ueberspringe", LOG_DEBUG); @unlink($tmpFile); continue; } // Dateiname generieren if ($parsed) { $newFilename = BankImportStatement::generateFilename($parsed); } else { $newFilename = sprintf('Kontoauszug_FinTS_%s_%d_%03d.pdf', preg_replace('/[^A-Z0-9]/', '', strtoupper($stmt->iban)), $stmt->statement_year, $idx + 1 ); } dol_syslog("BankImport HKEKP: Dateiname=".$newFilename, LOG_DEBUG); $stmt->filepath = $dir.'/'.$newFilename; // Kollisionsvermeidung if (file_exists($stmt->filepath)) { $newFilename = pathinfo($newFilename, PATHINFO_FILENAME).'_'.date('His').'.pdf'; $stmt->filepath = $dir.'/'.$newFilename; dol_syslog("BankImport HKEKP: Datei existiert bereits, neuer Name: ".$newFilename, LOG_DEBUG); } $stmt->filename = $newFilename; // PDF von Temp nach Ziel verschieben if (!rename($tmpFile, $stmt->filepath)) { dol_syslog("BankImport HKEKP: rename() fehlgeschlagen, verwende copy()", LOG_DEBUG); copy($tmpFile, $stmt->filepath); @unlink($tmpFile); } $stmt->filesize = filesize($stmt->filepath); dol_syslog("BankImport HKEKP: Datei gespeichert: ".$stmt->filepath." (".$stmt->filesize." Bytes)", LOG_DEBUG); // In DB speichern dol_syslog("BankImport HKEKP: Starte DB create()...", LOG_DEBUG); $dbResult = $stmt->create($user); dol_syslog("BankImport HKEKP: DB create() Ergebnis=".$dbResult." (>0=ID, <0=Fehler)", LOG_DEBUG); if ($dbResult > 0) { dol_syslog("BankImport HKEKP: DB-Eintrag erstellt mit ID=".$dbResult, LOG_DEBUG); // FinTS-Transaktionen verknuepfen dol_syslog("BankImport HKEKP: Starte linkTransactions()...", LOG_DEBUG); $linkResult = $stmt->linkTransactions(); dol_syslog("BankImport HKEKP: linkTransactions() Ergebnis=".$linkResult, LOG_DEBUG); // PDF-Einzelbuchungen parsen dol_syslog("BankImport HKEKP: Starte parsePdfTransactions()...", LOG_DEBUG); $pdfLines = $stmt->parsePdfTransactions(); dol_syslog("BankImport HKEKP: parsePdfTransactions() ergab ".(is_array($pdfLines) ? count($pdfLines) : 0)." Buchungszeilen", LOG_DEBUG); if (!empty($pdfLines)) { $stmt->saveStatementLines($pdfLines); dol_syslog("BankImport HKEKP: saveStatementLines() abgeschlossen", LOG_DEBUG); } // PDF in Dolibarr Bank-Verzeichnis kopieren $uploadBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID'); dol_syslog("BankImport HKEKP: BANKIMPORT_BANK_ACCOUNT_ID=".$uploadBankAccountId, LOG_DEBUG); if ($uploadBankAccountId > 0) { dol_syslog("BankImport HKEKP: Starte copyToDolibarrStatementDir()...", LOG_DEBUG); $stmt->copyToDolibarrStatementDir($uploadBankAccountId); dol_syslog("BankImport HKEKP: Starte reconcileBankEntries()...", LOG_DEBUG); $reconcileResult = $stmt->reconcileBankEntries($user, $uploadBankAccountId); dol_syslog("BankImport HKEKP: reconcileBankEntries() Ergebnis=".$reconcileResult, LOG_DEBUG); } $savedCount++; } else { $errorCountFints++; dol_syslog("BankImport HKEKP: DB-FEHLER bei create(): ".$stmt->error, LOG_ERR); if (file_exists($stmt->filepath)) { @unlink($stmt->filepath); } } } dol_syslog("BankImport HKEKP: Zusammenfassung: ".$savedCount." gespeichert, ".$errorCountFints." Fehler, ".$skippedCount." uebersprungen von ".$pdfCount." PDFs", LOG_DEBUG); if ($savedCount > 0) { setEventMessages($langs->trans("StatementsDownloaded", $savedCount, $pdfCount), null, 'mesgs'); } if ($errorCountFints > 0) { setEventMessages($langs->trans("StatementsDownloadErrors", $errorCountFints), null, 'warnings'); } if ($skippedCount > 0) { setEventMessages($langs->trans("StatementsSkippedNoNumber", $skippedCount), null, 'warnings'); } } } $fints->close(); dol_syslog("BankImport HKEKP: FinTS-Verbindung geschlossen", LOG_DEBUG); } dol_syslog("BankImport HKEKP: ========== ENDE fetchfints Action ==========", LOG_DEBUG); header("Location: ".$_SERVER['PHP_SELF']."?year=".date('Y')); exit; } } // Upload PDF (supports multiple files) if ($action == 'upload' && !empty($_FILES['pdffile'])) { $uploadMode = GETPOST('upload_mode', 'alpha'); $isAutoMode = ($uploadMode !== 'manual'); // Normalize $_FILES for multi-upload: always work with arrays $fileNames = is_array($_FILES['pdffile']['name']) ? $_FILES['pdffile']['name'] : array($_FILES['pdffile']['name']); $fileTmps = is_array($_FILES['pdffile']['tmp_name']) ? $_FILES['pdffile']['tmp_name'] : array($_FILES['pdffile']['tmp_name']); $fileSizes = is_array($_FILES['pdffile']['size']) ? $_FILES['pdffile']['size'] : array($_FILES['pdffile']['size']); $fileCount = count($fileNames); $uploadedCount = 0; $errorCount = 0; $totalLinked = 0; $lastYear = (int) date('Y'); for ($fi = 0; $fi < $fileCount; $fi++) { $error = 0; // Skip empty file slots if (empty($fileNames[$fi]) || empty($fileTmps[$fi])) { continue; } // Validate uploaded file if (!is_uploaded_file($fileTmps[$fi])) { setEventMessages($langs->trans("ErrorNoFileUploaded").': '.$fileNames[$fi], null, 'errors'); $errorCount++; continue; } // Check MIME type $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = finfo_file($finfo, $fileTmps[$fi]); finfo_close($finfo); if ($mimeType !== 'application/pdf') { setEventMessages($langs->trans("ErrorOnlyPDFAllowed").': '.$fileNames[$fi], null, 'errors'); $errorCount++; continue; } // Check file size (max 10MB) if ($fileSizes[$fi] > 10 * 1024 * 1024) { setEventMessages($langs->trans("ErrorFileTooLarge").': '.$fileNames[$fi], null, 'errors'); $errorCount++; continue; } // Parse PDF metadata automatically $parsed = BankImportStatement::parsePdfMetadata($fileTmps[$fi]); // Determine values: auto mode uses parsed data, manual mode uses form fields if ($isAutoMode && $parsed) { $statementNumber = $parsed['statement_number']; $statementYear = $parsed['statement_year']; $iban = $parsed['iban']; } else { // Manual mode (only for single file upload) $statementNumber = GETPOST('statement_number', 'alpha'); $statementYear = GETPOSTINT('statement_year'); $iban = GETPOST('iban', 'alpha'); // Auto-fill from parsed data if form fields are empty if ($parsed) { if (empty($statementNumber) && !empty($parsed['statement_number'])) { $statementNumber = $parsed['statement_number']; } if (empty($statementYear) && !empty($parsed['statement_year'])) { $statementYear = $parsed['statement_year']; } if (empty($iban) && !empty($parsed['iban'])) { $iban = $parsed['iban']; } } } // Show auto-detection info if ($parsed) { $autoMsg = $langs->trans("PdfAutoDetected").': '.$fileNames[$fi]; if (!empty($statementNumber)) { $autoMsg .= ' | '.$statementNumber.'/'.$statementYear; } if (!empty($parsed['pdf_number'])) { $autoMsg .= ' (PDF-Nr. '.$parsed['pdf_number'].'/'.$parsed['pdf_year'].')'; } if (!empty($parsed['iban'])) { $autoMsg .= ' | IBAN: '.$parsed['iban']; } if ($parsed['date_from'] && $parsed['date_to']) { $autoMsg .= ' | '.$langs->trans("Period").': '.dol_print_date($parsed['date_from'], 'day').' - '.dol_print_date($parsed['date_to'], 'day'); } setEventMessages($autoMsg, null, 'mesgs'); } // Validate required fields if (empty($statementNumber)) { setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentities("StatementNumber")).': '.$fileNames[$fi], null, 'errors'); $errorCount++; continue; } if (empty($statementYear)) { setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentities("Year")).': '.$fileNames[$fi], null, 'errors'); $errorCount++; continue; } // Create new statement object for each file $stmt = new BankImportStatement($db); $stmt->iban = $iban; $stmt->statement_number = $statementNumber; $stmt->statement_year = $statementYear; // Date fields if ($isAutoMode && $parsed) { $stmt->statement_date = $parsed['statement_date']; $stmt->date_from = $parsed['date_from']; $stmt->date_to = $parsed['date_to']; $stmt->opening_balance = $parsed['opening_balance']; $stmt->closing_balance = $parsed['closing_balance']; } else { $statementDate = dol_mktime(0, 0, 0, GETPOSTINT('statement_datemonth'), GETPOSTINT('statement_dateday'), GETPOSTINT('statement_dateyear')); $dateFrom = dol_mktime(0, 0, 0, GETPOSTINT('date_frommonth'), GETPOSTINT('date_fromday'), GETPOSTINT('date_fromyear')); $dateTo = dol_mktime(0, 0, 0, GETPOSTINT('date_tomonth'), GETPOSTINT('date_today'), GETPOSTINT('date_toyear')); $stmt->statement_date = $statementDate ?: ($parsed ? $parsed['statement_date'] : null); $stmt->date_from = $dateFrom ?: ($parsed ? $parsed['date_from'] : null); $stmt->date_to = $dateTo ?: ($parsed ? $parsed['date_to'] : null); $openBal = GETPOST('opening_balance', 'alpha'); $closeBal = GETPOST('closing_balance', 'alpha'); $stmt->opening_balance = ($openBal !== '' && $openBal !== null) ? (float) price2num($openBal) : ($parsed ? $parsed['opening_balance'] : null); $stmt->closing_balance = ($closeBal !== '' && $closeBal !== null) ? (float) price2num($closeBal) : ($parsed ? $parsed['closing_balance'] : null); } $stmt->import_key = date('YmdHis').'_'.$user->id; // Check duplicate if ($stmt->exists()) { setEventMessages($langs->trans("StatementAlreadyExists").': '.$statementNumber.'/'.$statementYear, null, 'warnings'); $errorCount++; continue; } // Generate filename and save file $dir = BankImportStatement::getStorageDir(); if ($parsed) { $newFilename = BankImportStatement::generateFilename($parsed); } else { $ibanPart = !empty($stmt->iban) ? preg_replace('/[^A-Z0-9]/', '', strtoupper($stmt->iban)) : 'KONTO'; $newFilename = sprintf('Kontoauszug_%s_%d_%s.pdf', $ibanPart, $stmt->statement_year, str_pad($stmt->statement_number, 3, '0', STR_PAD_LEFT) ); } $stmt->filepath = $dir.'/'.$newFilename; // Avoid overwriting existing files if (file_exists($stmt->filepath)) { $newFilename = pathinfo($newFilename, PATHINFO_FILENAME).'_'.date('His').'.pdf'; $stmt->filepath = $dir.'/'.$newFilename; } $stmt->filename = $newFilename; // Move uploaded file if (!move_uploaded_file($fileTmps[$fi], $stmt->filepath)) { setEventMessages($langs->trans("ErrorFailedToSaveFile").': '.$fileNames[$fi], null, 'errors'); $errorCount++; continue; } $stmt->filesize = filesize($stmt->filepath); // Save to database $result = $stmt->create($user); if ($result > 0) { // Link matching FinTS transactions to this statement $linked = $stmt->linkTransactions(); $totalLinked += max(0, $linked); // Parse PDF transaction lines and save to database $pdfLines = $stmt->parsePdfTransactions(); if (!empty($pdfLines)) { $linesSaved = $stmt->saveStatementLines($pdfLines); if ($linesSaved > 0) { setEventMessages($langs->trans("StatementLinesExtracted", $linesSaved, $stmt->statement_number.'/'.$stmt->statement_year), null, 'mesgs'); } } // Copy PDF to Dolibarr's bank statement document directory $uploadBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID'); if ($uploadBankAccountId > 0) { $stmt->copyToDolibarrStatementDir($uploadBankAccountId); } // Reconcile bank entries if bank account is configured if ($uploadBankAccountId > 0) { $reconciledCount = $stmt->reconcileBankEntries($user, $uploadBankAccountId); if ($reconciledCount > 0) { setEventMessages($langs->trans("BankEntriesReconciled", $reconciledCount, $stmt->statement_number.'/'.$stmt->statement_year), null, 'mesgs'); } } $uploadedCount++; $lastYear = $stmt->statement_year; } else { setEventMessages($stmt->error, null, 'errors'); $errorCount++; // Clean up file on DB error if (file_exists($stmt->filepath)) { @unlink($stmt->filepath); } } } // Summary message if ($uploadedCount > 0) { if ($uploadedCount == 1) { $msg = $langs->trans("StatementUploaded"); } else { $msg = $langs->trans("StatementsUploaded", $uploadedCount); } if ($totalLinked > 0) { $msg .= ' | '.$langs->trans("TransactionsLinked", $totalLinked); } setEventMessages($msg, null, 'mesgs'); // Redirect: for single upload use the year, for multi-upload show all if ($uploadedCount == 1) { header("Location: ".$_SERVER['PHP_SELF']."?year=".$lastYear); } else { header("Location: ".$_SERVER['PHP_SELF']."?year=0"); } exit; } } // Download PDF if ($action == 'download') { $id = GETPOSTINT('id'); if ($statement->fetch($id) > 0) { $filepath = $statement->getFilePath(); if ($filepath && file_exists($filepath)) { header('Content-Type: application/pdf'); header('Content-Disposition: attachment; filename="'.basename($statement->filename).'"'); header('Content-Length: '.filesize($filepath)); header('Cache-Control: private'); readfile($filepath); exit; } else { setEventMessages($langs->trans("FileNotFound"), null, 'errors'); } } else { setEventMessages($langs->trans("RecordNotFound"), null, 'errors'); } } // View PDF (inline) if ($action == 'view') { $id = GETPOSTINT('id'); if ($statement->fetch($id) > 0) { $filepath = $statement->getFilePath(); if ($filepath && file_exists($filepath)) { header('Content-Type: application/pdf'); header('Content-Disposition: inline; filename="'.basename($statement->filename).'"'); header('Content-Length: '.filesize($filepath)); header('Cache-Control: private'); readfile($filepath); exit; } else { setEventMessages($langs->trans("FileNotFound"), null, 'errors'); } } else { setEventMessages($langs->trans("RecordNotFound"), null, 'errors'); } } // Reconcile single statement if ($action == 'reconcile') { if (!$user->hasRight('bankimport', 'write')) { accessforbidden(); } $id = GETPOSTINT('id'); $reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID'); if (empty($reconcileBankAccountId)) { setEventMessages($langs->trans("ErrorNoBankAccountConfigured"), null, 'errors'); } elseif ($statement->fetch($id) > 0) { // Parse statement lines if not yet done $existingLines = $statement->getStatementLines(); if (is_array($existingLines) && empty($existingLines)) { $pdfLines = $statement->parsePdfTransactions(); if (!empty($pdfLines)) { $statement->saveStatementLines($pdfLines); } } $reconciledCount = $statement->reconcileBankEntries($user, $reconcileBankAccountId); if ($reconciledCount > 0) { setEventMessages($langs->trans("BankEntriesReconciled", $reconciledCount, $statement->statement_number.'/'.$statement->statement_year), null, 'mesgs'); } else { setEventMessages($langs->trans("NoBankEntriesToReconcile"), null, 'warnings'); } } $action = ''; } // Reconcile all statements if ($action == 'reconcileall') { if (!$user->hasRight('bankimport', 'write')) { accessforbidden(); } $reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID'); if (empty($reconcileBankAccountId)) { setEventMessages($langs->trans("ErrorNoBankAccountConfigured"), null, 'errors'); } else { $allStatements = $statement->fetchAll('statement_year,statement_number', 'ASC', 0, 0, array()); $totalReconciled = 0; $stmtCount = 0; if (is_array($allStatements)) { foreach ($allStatements as $stmt) { // Parse statement lines if not yet done $existingLines = $stmt->getStatementLines(); if (is_array($existingLines) && empty($existingLines)) { $pdfLines = $stmt->parsePdfTransactions(); if (!empty($pdfLines)) { $stmt->saveStatementLines($pdfLines); } } $count = $stmt->reconcileBankEntries($user, $reconcileBankAccountId); if ($count > 0) { $totalReconciled += $count; $stmtCount++; } } } if ($totalReconciled > 0) { setEventMessages($langs->trans("BankEntriesReconciledTotal", $totalReconciled, $stmtCount), null, 'mesgs'); } else { setEventMessages($langs->trans("NoBankEntriesToReconcile"), null, 'warnings'); } } $action = ''; } // Confirm a pending reconciliation match if ($action == 'confirmreconcile') { if (!$user->hasRight('bankimport', 'write')) { accessforbidden(); } $lineId = GETPOSTINT('lineid'); $bankId = GETPOSTINT('bankid'); $reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID'); if ($lineId > 0 && $bankId > 0 && $reconcileBankAccountId > 0) { require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php'; // Get statement info from line $sqlLine = "SELECT sl.fk_statement, s.statement_number, s.statement_year"; $sqlLine .= " FROM ".MAIN_DB_PREFIX."bankimport_statement_line sl"; $sqlLine .= " JOIN ".MAIN_DB_PREFIX."bankimport_statement s ON s.rowid = sl.fk_statement"; $sqlLine .= " WHERE sl.rowid = ".((int) $lineId); $resLine = $db->query($sqlLine); if ($resLine && $db->num_rows($resLine) > 0) { $lineObj = $db->fetch_object($resLine); $numReleve = $lineObj->statement_number.'/'.$lineObj->statement_year; // Reconcile the bank entry $bankLine = new AccountLine($db); $bankLine->fetch($bankId); $bankLine->num_releve = $numReleve; $result = $bankLine->update_conciliation($user, 0, 1); if ($result >= 0) { // Update statement line status $sqlUpd = "UPDATE ".MAIN_DB_PREFIX."bankimport_statement_line SET"; $sqlUpd .= " match_status = 'reconciled'"; $sqlUpd .= " WHERE rowid = ".((int) $lineId); $db->query($sqlUpd); setEventMessages($langs->trans("ReconciliationConfirmed"), null, 'mesgs'); } else { setEventMessages($langs->trans("Error"), null, 'errors'); } } } $action = ''; } // Delete confirmation if ($action == 'delete' && $confirm == 'yes') { $id = GETPOSTINT('id'); if ($statement->fetch($id) > 0) { $result = $statement->delete($user); if ($result > 0) { setEventMessages($langs->trans("RecordDeleted"), null, 'mesgs'); } else { setEventMessages($statement->error, null, 'errors'); } } $action = ''; } /* * View */ $form = new Form($db); $title = $langs->trans("PDFStatements"); llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-bankimport page-pdfstatements'); print load_fiche_titre($title, '', 'bank'); // Reminder: check if statements are outdated $reminderEnabled = getDolGlobalString('BANKIMPORT_REMINDER_ENABLED', '1'); if ($reminderEnabled) { $reminderMonths = getDolGlobalInt('BANKIMPORT_REMINDER_MONTHS') ?: 3; $lastEndDate = $statement->getLatestStatementEndDate(); $thresholdDate = dol_time_plus_duree(dol_now(), -$reminderMonths, 'm'); if ($lastEndDate === null) { print '
'; print img_warning().' '.$langs->trans("ReminderNoStatements"); print '

'; } elseif ($lastEndDate < $thresholdDate) { $monthsAgo = (int) round((dol_now() - $lastEndDate) / (30 * 24 * 3600)); print '
'; print img_warning().' '.$langs->trans("ReminderOutdatedStatements", dol_print_date($lastEndDate, 'day'), $monthsAgo); print '

'; } } // Info box print '
'; print ''.$langs->trans("PDFStatementsInfo").'
'; print $langs->trans("PDFStatementsInfoDesc"); print '
'; // FinTS-Abruf Button (wenn konfiguriert) $fintsCheck = new BankImportFinTS($db); if ($fintsCheck->isConfigured() && $fintsCheck->isLibraryAvailable() && $user->hasRight('bankimport', 'write')) { print '
'; print ''.img_picto('', 'bank', 'class="pictofixedwidth"').$langs->trans("AutoFetchStatements").'
'; print ''.$langs->trans("AutoFetchStatementsDesc").'

'; print ''; print img_picto('', 'download', 'class="pictofixedwidth"').$langs->trans("FetchFromBank"); print ''; print '
'; } // Delete confirmation dialog if ($action == 'delete') { $id = GETPOSTINT('id'); $stmt = new BankImportStatement($db); $stmt->fetch($id); $formconfirm = $form->formconfirm( $_SERVER["PHP_SELF"].'?id='.$id.'&year='.$year, $langs->trans('DeleteStatement'), $langs->trans('ConfirmDeleteStatement', $stmt->statement_number.'/'.$stmt->statement_year), 'delete', '', 0, 1 ); print $formconfirm; } // Upload form $defaultMode = getDolGlobalString('BANKIMPORT_UPLOAD_MODE') ?: 'auto'; $uploadMode = GETPOST('upload_mode', 'alpha') ?: $defaultMode; print '
'; print '
'; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; // Upload mode selection print ''; print ''; print ''; print ''; // PDF file (always visible, multiple in auto mode) print ''; print ''; print ''; print ''; // --- Manual fields (hidden when auto mode) --- // IBAN print ''; print ''; print ''; print ''; // Year print ''; print ''; print ''; print ''; // Statement number print ''; print ''; print ''; print ''; // Statement date print ''; print ''; print ''; print ''; // Period from print ''; print ''; print ''; print ''; // Period to print ''; print ''; print ''; print ''; // Opening balance print ''; print ''; print ''; print ''; // Closing balance print ''; print ''; print ''; print ''; print '
'.$langs->trans("UploadPDFStatement").'
'.$langs->trans("UploadMode").''; print ''; print ''; print '
'.$langs->trans("File").''; print ''; print '
'.$langs->trans("MultipleFilesHint").''; print '
'.$langs->trans("IBAN").''; print ''; print '
'.$langs->trans("Year").''; $years = array(); for ($y = (int) date('Y'); $y >= ((int) date('Y') - 10); $y--) { $years[$y] = $y; } print $form->selectarray('statement_year', $years, GETPOSTISSET('statement_year') ? GETPOSTINT('statement_year') : $year, 0, 0, 0, '', 0, 0, 0, '', 'minwidth100'); print '
'.$langs->trans("StatementNumber").''; $nextNum = $statement->getNextStatementNumber($year); print ''; print '
'.$langs->trans("StatementDate").''; print $form->selectDate(GETPOSTISSET('statement_dateday') ? dol_mktime(0, 0, 0, GETPOSTINT('statement_datemonth'), GETPOSTINT('statement_dateday'), GETPOSTINT('statement_dateyear')) : -1, 'statement_date', 0, 0, 1, '', 1, 0); print '
'.$langs->trans("DateFrom").''; print $form->selectDate(GETPOSTISSET('date_fromday') ? dol_mktime(0, 0, 0, GETPOSTINT('date_frommonth'), GETPOSTINT('date_fromday'), GETPOSTINT('date_fromyear')) : -1, 'date_from', 0, 0, 1, '', 1, 0); print '
'.$langs->trans("DateTo").''; print $form->selectDate(GETPOSTISSET('date_today') ? dol_mktime(0, 0, 0, GETPOSTINT('date_tomonth'), GETPOSTINT('date_today'), GETPOSTINT('date_toyear')) : -1, 'date_to', 0, 0, 1, '', 1, 0); print '
'.$langs->trans("OpeningBalance").''; print ''; print ' EUR'; print '
'.$langs->trans("ClosingBalance").''; print ''; print ' EUR'; print '
'; print '
'; print ''; print '
'; print '
'; // JavaScript for toggling upload modes print ''; print '
'; // fichehalfleft print '
'; // fichecenter print '

'; // Year filter for list - only show years that have statements $yearsFilter = array(0 => $langs->trans("AllStatements")); $availableYears = $statement->getAvailableYears(); foreach ($availableYears as $yKey => $yVal) { $yearsFilter[$yKey] = $yVal; } // If current year not in list, add it if (!isset($yearsFilter[(int) date('Y')])) { $yearsFilter[(int) date('Y')] = (string) date('Y'); krsort($yearsFilter); } print '
'; print '
'; print ''.$langs->trans("Year").': '; print $form->selectarray('year', $yearsFilter, $year, 0, 0, 0, '', 0, 0, 0, '', 'minwidth100'); print ' '; print '
'; print '
'; // Reconcile All button $reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID'); if (!empty($reconcileBankAccountId)) { print '
'; print ''; print img_picto('', 'bank', 'class="pictofixedwidth"').$langs->trans("ReconcileAllStatements"); print ''; print '
'; } else { print '
'; print img_warning().' '.$langs->trans("NoBankAccountConfigured"); print ' '.$langs->trans("GoToSetup").''; print '
'; } // List of existing PDF statements print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; $filter = array(); if ($year > 0) { $filter['year'] = $year; } $records = $statement->fetchAll('statement_year,statement_number', 'DESC', 100, 0, $filter); if (is_array($records) && count($records) > 0) { foreach ($records as $obj) { print ''; // Statement number print ''; // IBAN print ''; // Statement date print ''; // Period print ''; // Opening balance print ''; // Closing balance print ''; // Size print ''; // Creation date print ''; // Actions print ''; print ''; } } else { print ''; } print '
'.$langs->trans("StatementNumber").''.$langs->trans("IBAN").''.$langs->trans("StatementDate").''.$langs->trans("Period").''.$langs->trans("OpeningBalance").''.$langs->trans("ClosingBalance").''.$langs->trans("Size").''.$langs->trans("DateCreation").''.$langs->trans("Actions").'
'; print ''.dol_escape_htmltag($obj->statement_number).'/'.$obj->statement_year; print ''; if ($obj->iban) { print dol_escape_htmltag($obj->iban); } else { print '-'; } print ''; if ($obj->statement_date) { print dol_print_date($obj->statement_date, 'day'); } else { print '-'; } print ''; if ($obj->date_from && $obj->date_to) { print dol_print_date($obj->date_from, 'day').' - '.dol_print_date($obj->date_to, 'day'); } elseif ($obj->date_from) { print $langs->trans("From").' '.dol_print_date($obj->date_from, 'day'); } elseif ($obj->date_to) { print $langs->trans("To").' '.dol_print_date($obj->date_to, 'day'); } else { print '-'; } print ''; if ($obj->opening_balance !== null) { $color = $obj->opening_balance >= 0 ? '' : 'color: red;'; print ''.price($obj->opening_balance, 0, $langs, 1, -1, 2, 'EUR').''; } else { print '-'; } print ''; if ($obj->closing_balance !== null) { $color = $obj->closing_balance >= 0 ? '' : 'color: red;'; print ''.price($obj->closing_balance, 0, $langs, 1, -1, 2, 'EUR').''; } else { print '-'; } print ''; if ($obj->filesize) { print dol_print_size($obj->filesize, 1); } else { print '-'; } print ''; print dol_print_date($obj->datec, 'day'); print ''; if ($obj->filepath && file_exists($obj->filepath)) { // View (inline) print 'id.'&token='.newToken().'" target="_blank" title="'.$langs->trans("View").'">'; print img_picto($langs->trans("View"), 'eye'); print ''; // Download print 'id.'&token='.newToken().'" title="'.$langs->trans("Download").'">'; print img_picto($langs->trans("Download"), 'download'); print ''; } // Reconcile if (!empty($reconcileBankAccountId) && $obj->date_from && $obj->date_to) { print 'id.'&year='.$year.'&token='.newToken().'" title="'.$langs->trans("ReconcileStatement").'">'; print img_picto($langs->trans("ReconcileStatement"), 'bank'); print ''; } // Delete print 'id.'&year='.$year.'&token='.newToken().'" title="'.$langs->trans("Delete").'">'; print img_picto($langs->trans("Delete"), 'delete'); print ''; print '
'; print $langs->trans("NoPDFStatementsFound"); print '
'; print '
'; // Pending review matches $sqlPending = "SELECT sl.rowid as line_id, sl.fk_statement, sl.line_number, sl.date_booking, sl.amount as stmt_amount,"; $sqlPending .= " sl.name as stmt_name, sl.fk_bank,"; $sqlPending .= " b.rowid as bank_id, b.datev, b.amount as bank_amount, b.label as bank_label,"; $sqlPending .= " s.statement_number, s.statement_year"; $sqlPending .= " FROM ".MAIN_DB_PREFIX."bankimport_statement_line sl"; $sqlPending .= " JOIN ".MAIN_DB_PREFIX."bankimport_statement s ON s.rowid = sl.fk_statement"; $sqlPending .= " JOIN ".MAIN_DB_PREFIX."bank b ON b.rowid = sl.fk_bank"; $sqlPending .= " WHERE sl.match_status = 'pending_review'"; $sqlPending .= " AND sl.entity = ".((int) $conf->entity); $sqlPending .= " ORDER BY s.statement_year, s.statement_number, sl.line_number"; $resPending = $db->query($sqlPending); if ($resPending && $db->num_rows($resPending) > 0) { print '
'; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; while ($pendObj = $db->fetch_object($resPending)) { $diff = abs(abs((float) $pendObj->stmt_amount) - abs((float) $pendObj->bank_amount)); $diffColor = ($diff > 10) ? 'color: red; font-weight: bold;' : 'color: #e68a00;'; print ''; // Statement number print ''; // Booking date print ''; // Name print ''; // Amount from PDF statement print ''; // Amount from Dolibarr bank print ''; // Difference print ''; // Bank entry link print ''; // Action: confirm button print ''; print ''; } print '
'; print img_warning().' '.$langs->trans("PendingReconciliationMatches").''; print ' - '.$langs->trans("PendingReconciliationMatchesDesc"); print '
'.$langs->trans("StatementNumber").''.$langs->trans("BookingDate").''.$langs->trans("Name").''.$langs->trans("AmountStatement").''.$langs->trans("AmountDolibarr").''.$langs->trans("Difference").''.$langs->trans("BankEntry").''.$langs->trans("Action").'
'.$pendObj->statement_number.'/'.$pendObj->statement_year.''.dol_print_date($db->jdate($pendObj->date_booking), 'day').''.dol_escape_htmltag($pendObj->stmt_name).''; $stmtColor = $pendObj->stmt_amount >= 0 ? '' : 'color: red;'; print ''.price($pendObj->stmt_amount, 0, $langs, 1, -1, 2, 'EUR').''; print ''; $bankColor = $pendObj->bank_amount >= 0 ? '' : 'color: red;'; print ''.price($pendObj->bank_amount, 0, $langs, 1, -1, 2, 'EUR').''; print ''; print ''.price($diff, 0, $langs, 1, -1, 2, 'EUR').''; print ''; print '#'.$pendObj->bank_id.''; print ''; print 'line_id.'&bankid='.$pendObj->bank_id.'&year='.$year.'&token='.newToken().'">'; print $langs->trans("Confirm"); print ''; print '
'; print '
'; } $db->free($resPending); // Statistics $totalCount = $statement->fetchAll('', '', 0, 0, array(), 'count'); $yearCount = is_array($records) ? count($records) : 0; print '
'; if ($year > 0) { print $langs->trans("Total").': '.$yearCount.' '.$langs->trans("StatementsInYear", $year); print ' | '.$langs->trans("AllStatements").': '.$totalCount.''; } else { print $langs->trans("Total").': '.$totalCount.' '.$langs->trans("AllStatements"); } print '
'; llxFooter(); $db->close();