* * 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/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); // 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->transnoentitiesaliases("StatementNumber")).': '.$fileNames[$fi], null, 'errors'); $errorCount++; continue; } if (empty($statementYear)) { setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesaliases("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 transactions to this statement $linked = $stmt->linkTransactions(); $totalLinked += max(0, $linked); $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'); } } // 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 '
| '.$langs->trans("StatementNumber").' | '; print ''.$langs->trans("IBAN").' | '; print ''.$langs->trans("StatementDate").' | '; print ''.$langs->trans("Period").' | '; print ''.$langs->trans("OpeningBalance").' | '; print ''.$langs->trans("ClosingBalance").' | '; print ''.$langs->trans("Size").' | '; print ''.$langs->trans("DateCreation").' | '; print ''.$langs->trans("Actions").' | '; print '
|---|---|---|---|---|---|---|---|---|
| '; print ''.dol_escape_htmltag($obj->statement_number).'/'.$obj->statement_year; print ' | '; // IBAN print ''; if ($obj->iban) { print dol_escape_htmltag($obj->iban); } else { print '-'; } print ' | '; // Statement date print ''; if ($obj->statement_date) { print dol_print_date($obj->statement_date, 'day'); } else { print '-'; } print ' | '; // Period 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 ' | '; // Opening balance 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 ' | '; // Closing balance 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 ' | '; // Size print ''; if ($obj->filesize) { print dol_print_size($obj->filesize, 1); } else { print '-'; } print ' | '; // Creation date print ''; print dol_print_date($obj->datec, 'day'); print ' | '; // Actions 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 ''; } // Delete print 'id.'&year='.$year.'&token='.newToken().'" title="'.$langs->trans("Delete").'">'; print img_picto($langs->trans("Delete"), 'delete'); print ''; print ' | '; print '
| '; print $langs->trans("NoPDFStatementsFound"); print ' | ||||||||