*
* 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
@@ -23,16 +18,14 @@
/**
* \file bankimport/bankimportindex.php
* \ingroup bankimport
- * \brief Home page of bankimport top menu
+ * \brief Dashboard page for BankImport module
*/
// Load Dolibarr environment
$res = 0;
-// Try main.inc.php into web root known defined into CONTEXT_DOCUMENT_ROOT (not always defined)
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
}
-// Try main.inc.php into web root detected using web root calculated from SCRIPT_FILENAME
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
$tmp2 = realpath(__FILE__);
$i = strlen($tmp) - 1;
@@ -47,7 +40,6 @@ if (!$res && $i > 0 && file_exists(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";
}
-// Try main.inc.php using relative path
if (!$res && file_exists("../main.inc.php")) {
$res = @include "../main.inc.php";
}
@@ -72,185 +64,286 @@ require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php';
*/
// Load translation files required by the page
-$langs->loadLangs(array("bankimport@bankimport"));
+$langs->loadLangs(array("bankimport@bankimport", "banks"));
$action = GETPOST('action', 'aZ09');
-$now = dol_now();
-$max = getDolGlobalInt('MAIN_SIZE_SHORTLIST_LIMIT', 5);
-
-// Security check - Protection if external user
+// Security check
+if (!$user->hasRight('bankimport', 'read')) {
+ accessforbidden();
+}
$socid = GETPOSTINT('socid');
if (!empty($user->socid) && $user->socid > 0) {
$action = '';
$socid = $user->socid;
}
-// Initialize a technical object to manage hooks. Note that conf->hooks_modules contains array
-//$hookmanager->initHooks(array($object->element.'index'));
-
-// Security check (enable the most restrictive one)
-//if ($user->socid > 0) accessforbidden();
-//if ($user->socid > 0) $socid = $user->socid;
-//if (!isModEnabled('bankimport')) {
-// accessforbidden('Module not enabled');
-//}
-//if (! $user->hasRight('bankimport', 'myobject', 'read')) {
-// accessforbidden();
-//}
-//restrictedArea($user, 'bankimport', 0, 'bankimport_myobject', 'myobject', '', 'rowid');
-//if (empty($user->admin)) {
-// accessforbidden('Must be admin');
-//}
-
-
-/*
- * Actions
- */
-
-// None
-
/*
* View
*/
$form = new Form($db);
-$formfile = new FormFile($db);
+dol_include_once('/bankimport/class/bankstatement.class.php');
llxHeader("", $langs->trans("BankImportArea"), '', '', 0, 0, '', '', '', 'mod-bankimport page-index');
-print load_fiche_titre($langs->trans("BankImportArea"), '', 'bankimport.png@bankimport');
+print load_fiche_titre($langs->trans("BankImportArea"), '', 'bank');
+
+// Reminder: check if statements are outdated
+$reminderEnabled = getDolGlobalString('BANKIMPORT_REMINDER_ENABLED', '1');
+if ($reminderEnabled) {
+ $reminderMonths = getDolGlobalInt('BANKIMPORT_REMINDER_MONTHS') ?: 3;
+ $stmtCheck = new BankImportStatement($db);
+ $lastEndDate = $stmtCheck->getLatestStatementEndDate();
+ $thresholdDate = dol_time_plus_duree(dol_now(), -$reminderMonths, 'm');
+
+ if ($lastEndDate === null) {
+ // No statements at all
+ print ' ';
+ } elseif ($lastEndDate < $thresholdDate) {
+ $monthsAgo = (int) round((dol_now() - $lastEndDate) / (30 * 24 * 3600));
+ print ' ';
+ }
+}
print '';
+// -----------------------------------------------
+// Widget: Letzte 10 importierte Buchungen
+// -----------------------------------------------
+$max = 10;
-/* BEGIN MODULEBUILDER DRAFT MYOBJECT
-// Draft MyObject
-if (isModEnabled('bankimport') && $user->hasRight('bankimport', 'read')) {
- $langs->load("orders");
+$sql = "SELECT t.rowid, t.ref, t.date_trans, t.name, t.description, t.amount, t.currency, t.status";
+$sql .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction as t";
+$sql .= " WHERE t.entity IN (".getEntity('banktransaction').")";
+$sql .= " ORDER BY t.date_trans DESC, t.rowid DESC";
+$sql .= $db->plimit($max, 0);
- $sql = "SELECT c.rowid, c.ref, c.ref_client, c.total_ht, c.tva as total_tva, c.total_ttc, s.rowid as socid, s.nom as name, s.client, s.canvas";
- $sql.= ", s.code_client";
- $sql.= " FROM ".$db->prefix()."commande as c";
- $sql.= ", ".$db->prefix()."societe as s";
- $sql.= " WHERE c.fk_soc = s.rowid";
- $sql.= " AND c.fk_statut = 0";
- $sql.= " AND c.entity IN (".getEntity('commande').")";
- if ($socid) $sql.= " AND c.fk_soc = ".((int) $socid);
+$resql = $db->query($sql);
- $resql = $db->query($sql);
- if ($resql)
- {
- $total = 0;
- $num = $db->num_rows($resql);
+print '
';
+print '';
+print '';
+print $langs->trans("LastImportedTransactions");
- print '';
- print '';
- print ''.$langs->trans("DraftMyObjects").($num?''.$num.' ':'').' ';
-
- $var = true;
- if ($num > 0)
- {
- $i = 0;
- while ($i < $num)
- {
-
- $obj = $db->fetch_object($resql);
- print '';
-
- $myobjectstatic->id=$obj->rowid;
- $myobjectstatic->ref=$obj->ref;
- $myobjectstatic->ref_client=$obj->ref_client;
- $myobjectstatic->total_ht = $obj->total_ht;
- $myobjectstatic->total_tva = $obj->total_tva;
- $myobjectstatic->total_ttc = $obj->total_ttc;
-
- print $myobjectstatic->getNomUrl(1);
- print ' ';
- print '';
- print ' ';
- print ''.price($obj->total_ttc).' ';
- $i++;
- $total += $obj->total_ttc;
- }
- if ($total>0)
- {
-
- print ''.$langs->trans("Total").' '.price($total)." ";
- }
- }
- else
- {
-
- print ''.$langs->trans("NoOrder").' ';
- }
- print "
";
-
- $db->free($resql);
- }
- else
- {
- dol_print_error($db);
+// Count total
+$sqlcount = "SELECT COUNT(*) as total FROM ".MAIN_DB_PREFIX."bankimport_transaction WHERE entity IN (".getEntity('banktransaction').")";
+$rescount = $db->query($sqlcount);
+if ($rescount) {
+ $objcount = $db->fetch_object($rescount);
+ if ($objcount->total > 0) {
+ print '';
+ print ''.$objcount->total.' ';
+ print ' ';
}
}
-END MODULEBUILDER DRAFT MYOBJECT */
+print ' ';
+print ' ';
+
+if ($resql) {
+ $num = $db->num_rows($resql);
+
+ if ($num > 0) {
+ $i = 0;
+ while ($i < $num) {
+ $obj = $db->fetch_object($resql);
+
+ print '';
+
+ // Date
+ print '';
+ print dol_print_date($db->jdate($obj->date_trans), 'day');
+ print ' ';
+
+ // Name + Description
+ print '';
+ print '';
+ print dol_escape_htmltag(dol_trunc($obj->name, 30));
+ print ' ';
+ if ($obj->description) {
+ print ''.dol_escape_htmltag(dol_trunc($obj->description, 40)).' ';
+ }
+ print ' ';
+
+ // Amount
+ print '';
+ if ($obj->amount >= 0) {
+ print '+'.price($obj->amount, 0, $langs, 1, -1, 2, $obj->currency).' ';
+ } else {
+ print ''.price($obj->amount, 0, $langs, 1, -1, 2, $obj->currency).' ';
+ }
+ print ' ';
+
+ // Status
+ print '';
+ switch ($obj->status) {
+ case 0:
+ print ''.$langs->trans("New").' ';
+ break;
+ case 1:
+ print ''.$langs->trans("Matched").' ';
+ break;
+ case 2:
+ print ''.$langs->trans("Reconciled").' ';
+ break;
+ case 9:
+ print ''.$langs->trans("Ignored").' ';
+ break;
+ }
+ print ' ';
+
+ print ' ';
+ $i++;
+ }
+ } else {
+ print ''.$langs->trans("NoTransactionsInDatabase").' ';
+ }
+
+ $db->free($resql);
+} else {
+ dol_print_error($db);
+}
+
+print '
';
+
+// Link "Alle anzeigen"
+if (!empty($objcount) && $objcount->total > 0) {
+ print '
';
+}
print '
';
-/* BEGIN MODULEBUILDER LASTMODIFIED MYOBJECT
-// Last modified myobject
-if (isModEnabled('bankimport') && $user->hasRight('bankimport', 'read')) {
- $sql = "SELECT s.rowid, s.ref, s.label, s.date_creation, s.tms";
- $sql.= " FROM ".$db->prefix()."bankimport_myobject as s";
- $sql.= " WHERE s.entity IN (".getEntity($myobjectstatic->element).")";
- //if ($socid) $sql.= " AND s.rowid = $socid";
- $sql .= " ORDER BY s.tms DESC";
- $sql .= $db->plimit($max, 0);
+// -----------------------------------------------
+// Widget: Letzte 5 PDF-Kontoauszüge
+// -----------------------------------------------
+$maxpdf = 5;
- $resql = $db->query($sql);
- if ($resql)
- {
- $num = $db->num_rows($resql);
- $i = 0;
+$sql2 = "SELECT s.rowid, s.statement_number, s.statement_year, s.iban, s.date_from, s.date_to,";
+$sql2 .= " s.opening_balance, s.closing_balance, s.filename, s.filepath, s.filesize, s.datec";
+$sql2 .= " FROM ".MAIN_DB_PREFIX."bankimport_statement as s";
+$sql2 .= " WHERE s.entity IN (".getEntity('bankstatement').")";
+$sql2 .= " ORDER BY s.datec DESC";
+$sql2 .= $db->plimit($maxpdf, 0);
- print '
';
- print '';
- print '';
- print $langs->trans("BoxTitleLatestModifiedMyObjects", $max);
- print ' ';
- print ''.$langs->trans("DateModificationShort").' ';
- print ' ';
- if ($num)
- {
- while ($i < $num)
- {
- $objp = $db->fetch_object($resql);
+$resql2 = $db->query($sql2);
- $myobjectstatic->id=$objp->rowid;
- $myobjectstatic->ref=$objp->ref;
- $myobjectstatic->label=$objp->label;
- $myobjectstatic->status = $objp->status;
+print '';
+print '';
+print '';
+print $langs->trans("LastPDFStatements");
- print ' ';
- print ''.$myobjectstatic->getNomUrl(1).' ';
- print '';
- print " ";
- print ''.dol_print_date($db->jdate($objp->tms), 'day')." ";
- print ' ';
- $i++;
- }
-
- $db->free($resql);
- } else {
- print ''.$langs->trans("None").' ';
- }
- print "
";
+// Count total
+$sqlcount2 = "SELECT COUNT(*) as total FROM ".MAIN_DB_PREFIX."bankimport_statement WHERE entity IN (".getEntity('bankstatement').")";
+$rescount2 = $db->query($sqlcount2);
+if ($rescount2) {
+ $objcount2 = $db->fetch_object($rescount2);
+ if ($objcount2->total > 0) {
+ print '';
+ print ''.$objcount2->total.' ';
+ print ' ';
}
}
-*/
+print '';
+print '';
+
+if ($resql2) {
+ $num2 = $db->num_rows($resql2);
+
+ if ($num2 > 0) {
+ $i = 0;
+ while ($i < $num2) {
+ $obj2 = $db->fetch_object($resql2);
+
+ print '';
+
+ // Statement number / Year
+ print '';
+ print ''.dol_escape_htmltag($obj2->statement_number).' /'.$obj2->statement_year;
+ print ' ';
+
+ // IBAN (shortened)
+ print '';
+ if ($obj2->iban) {
+ print dol_escape_htmltag(dol_trunc($obj2->iban, 20));
+ } else {
+ print '- ';
+ }
+ print ' ';
+
+ // Period
+ print '';
+ if ($obj2->date_from && $obj2->date_to) {
+ print dol_print_date($db->jdate($obj2->date_from), 'day').' - '.dol_print_date($db->jdate($obj2->date_to), 'day');
+ } elseif ($obj2->date_from) {
+ print dol_print_date($db->jdate($obj2->date_from), 'day').' -';
+ } else {
+ print '- ';
+ }
+ print ' ';
+
+ // Closing balance
+ print '';
+ if ($obj2->closing_balance !== null && $obj2->closing_balance !== '') {
+ $color = (float) $obj2->closing_balance >= 0 ? '' : 'color: red;';
+ print ''.price($obj2->closing_balance, 0, $langs, 1, -1, 2, 'EUR').' ';
+ } else {
+ print '- ';
+ }
+ print ' ';
+
+ // Actions
+ print '';
+ if ($obj2->filepath && file_exists($obj2->filepath)) {
+ print '';
+ print img_picto($langs->trans("View"), 'eye');
+ print ' ';
+
+ print '';
+ print img_picto($langs->trans("Download"), 'download');
+ print ' ';
+ }
+ print ' ';
+
+ print ' ';
+ $i++;
+ }
+ } else {
+ print ''.$langs->trans("NoPDFStatementsFound").' ';
+ }
+
+ $db->free($resql2);
+} else {
+ dol_print_error($db);
+}
+
+print '
';
+
+// Links
+print '
';
+
print '
';
diff --git a/card.php b/card.php
old mode 100644
new mode 100755
index 5818407..a388220
--- a/card.php
+++ b/card.php
@@ -61,6 +61,11 @@ $ref = GETPOST('ref', 'alpha');
$action = GETPOST('action', 'aZ09');
$confirm = GETPOST('confirm', 'alpha');
+// Security check
+if (!$user->hasRight('bankimport', 'read')) {
+ accessforbidden();
+}
+
/*
* Actions
*/
@@ -245,6 +250,25 @@ if ($object->id > 0) {
print '';
}
+ // Linked PDF statement
+ if (!empty($object->fk_statement)) {
+ dol_include_once('/bankimport/class/bankstatement.class.php');
+ $stmt = new BankImportStatement($db);
+ $stmt->fetch($object->fk_statement);
+ print '';
+ print ''.$langs->trans("PDFStatement").' ';
+ print '';
+ print '';
+ print img_picto($langs->trans("ViewPDFStatement"), 'pdf').' ';
+ print $langs->trans("StatementNumber").' '.$stmt->statement_number.'/'.$stmt->statement_year;
+ print ' ';
+ if ($stmt->date_from && $stmt->date_to) {
+ print ' ('.dol_print_date($stmt->date_from, 'day').' - '.dol_print_date($stmt->date_to, 'day').') ';
+ }
+ print ' ';
+ print ' ';
+ }
+
// Linked third party
if ($object->fk_societe > 0) {
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
diff --git a/class/bankimportcron.class.php b/class/bankimportcron.class.php
old mode 100644
new mode 100755
diff --git a/class/bankstatement.class.php b/class/bankstatement.class.php
old mode 100644
new mode 100755
index 3f366b3..e870bde
--- a/class/bankstatement.class.php
+++ b/class/bankstatement.class.php
@@ -480,6 +480,192 @@ class BankImportStatement extends CommonObject
return 1;
}
+ /**
+ * Parse PDF bank statement metadata using pdfinfo and pdftotext
+ *
+ * Extracts: statement number, year, IBAN, date range, opening/closing balance,
+ * account number, bank name, statement date.
+ *
+ * @param string $filepath Path to PDF file
+ * @return array|false Array with extracted data or false on failure
+ */
+ public static function parsePdfMetadata($filepath)
+ {
+ if (!file_exists($filepath)) {
+ return false;
+ }
+
+ $result = array(
+ 'statement_number' => '',
+ 'statement_year' => 0,
+ 'pdf_number' => '', // Original statement number from PDF (e.g. "1" from Nr. 1/2025)
+ 'pdf_year' => 0, // Original year from PDF
+ 'iban' => '',
+ 'date_from' => null,
+ 'date_to' => null,
+ 'opening_balance' => null,
+ 'closing_balance' => null,
+ 'statement_date' => null,
+ 'account_number' => '',
+ 'bank_name' => '',
+ 'author' => '',
+ );
+
+ $escapedPath = escapeshellarg($filepath);
+
+ // 1. Extract metadata via pdfinfo
+ $pdfinfo = array();
+ exec("pdfinfo ".$escapedPath." 2>/dev/null", $pdfinfo);
+
+ foreach ($pdfinfo as $line) {
+ if (preg_match('/^Title:\s+(.+)$/', $line, $m)) {
+ // Title format: "000000000000000000000013438147 001/2025" or "Kontoauszug 13438147"
+ if (preg_match('/(\d+)\s+(\d+)\/(\d{4})/', $m[1], $tm)) {
+ $result['account_number'] = ltrim($tm[1], '0');
+ $result['pdf_number'] = (string) intval($tm[2]);
+ $result['pdf_year'] = (int) $tm[3];
+ }
+ }
+ if (preg_match('/^Author:\s+(.+)$/', $line, $m)) {
+ $result['author'] = trim($m[1]);
+ }
+ }
+
+ // 2. Extract text via pdftotext
+ $text = '';
+ exec("pdftotext -layout ".$escapedPath." - 2>/dev/null", $textlines);
+ $text = implode("\n", $textlines);
+
+ // Statement number from text (fallback if not in metadata)
+ if (empty($result['pdf_number']) && preg_match('/Nr\.\s+(\d+)\/(\d{4})/', $text, $m)) {
+ $result['pdf_number'] = (string) intval($m[1]);
+ $result['pdf_year'] = (int) $m[2];
+ }
+
+ // IBAN
+ if (preg_match('/IBAN:\s*([A-Z]{2}\d{2}\s*[\d\s]+)/', $text, $m)) {
+ $result['iban'] = preg_replace('/\s+/', ' ', trim($m[1]));
+ }
+
+ // Account number (fallback)
+ if (empty($result['account_number']) && preg_match('/Kontonummer\s+(\d+)/', $text, $m)) {
+ $result['account_number'] = $m[1];
+ }
+
+ // Date range from Kontoabschluss
+ if (preg_match('/Kontoabschluss vom (\d{2}\.\d{2}\.\d{4}) bis (\d{2}\.\d{2}\.\d{4})/', $text, $m)) {
+ $dateFrom = DateTime::createFromFormat('d.m.Y', $m[1]);
+ $dateTo = DateTime::createFromFormat('d.m.Y', $m[2]);
+ if ($dateFrom) {
+ $result['date_from'] = $dateFrom->setTime(0, 0, 0)->getTimestamp();
+ }
+ if ($dateTo) {
+ $result['date_to'] = $dateTo->setTime(0, 0, 0)->getTimestamp();
+ }
+ }
+
+ // Statement date (erstellt am)
+ if (preg_match('/erstellt am\s+(\d{2}\.\d{2}\.\d{4})/', $text, $m)) {
+ $stmtDate = DateTime::createFromFormat('d.m.Y', $m[1]);
+ if ($stmtDate) {
+ $result['statement_date'] = $stmtDate->setTime(0, 0, 0)->getTimestamp();
+ }
+ }
+
+ // Opening balance: "alter Kontostand [vom DD.MM.YYYY] X.XXX,XX H/S"
+ if (preg_match('/alter Kontostand(?:\s+vom\s+\d{2}\.\d{2}\.\d{4})?\s+([\d.,]+)\s+(H|S)/', $text, $m)) {
+ $amount = self::parseGermanAmount($m[1]);
+ if ($m[2] === 'S') {
+ $amount = -$amount;
+ }
+ $result['opening_balance'] = $amount;
+ }
+
+ // Closing balance: "neuer Kontostand vom DD.MM.YYYY X.XXX,XX H/S"
+ if (preg_match('/neuer Kontostand(?:\s+vom\s+\d{2}\.\d{2}\.\d{4})?\s+([\d.,]+)\s+(H|S)/', $text, $m)) {
+ $amount = self::parseGermanAmount($m[1]);
+ if ($m[2] === 'S') {
+ $amount = -$amount;
+ }
+ $result['closing_balance'] = $amount;
+ }
+
+ // Bank name (first line that contains "Bank" or known patterns)
+ if (preg_match('/(?:VR\s*B\s*ank|Volksbank|Raiffeisenbank|Sparkasse)[^\n]*/i', $text, $m)) {
+ $bankName = trim($m[0]);
+ // Fix OCR artifacts: single chars separated by spaces ("V R B a n k" → "VRBank")
+ // Strategy: collapse all single-space gaps between word chars that look like OCR splitting
+ $bankName = preg_replace('/\b(\w) (\w) (\w) (\w)\b/', '$1$2$3$4', $bankName);
+ $bankName = preg_replace('/\b(\w) (\w) (\w)\b/', '$1$2$3', $bankName);
+ $bankName = preg_replace('/\b(\w) (\w)\b/', '$1$2', $bankName);
+ // Fix common OCR pattern "VR B ank" → "VR Bank", "S chleswig" → "Schleswig"
+ $bankName = preg_replace('/\bB ank\b/', 'Bank', $bankName);
+ $bankName = preg_replace('/\bS (\w)/', 'S$1', $bankName);
+ $bankName = preg_replace('/\bW (\w)/', 'W$1', $bankName);
+ // Clean up multiple spaces and trim address parts after comma
+ $bankName = preg_replace('/\s{2,}/', ' ', $bankName);
+ $bankName = preg_replace('/,.*$/', '', $bankName);
+ $result['bank_name'] = trim($bankName);
+ }
+
+ // Derive statement_number (=month) and statement_year from end date of period
+ if ($result['date_to']) {
+ $result['statement_number'] = (string) intval(date('m', $result['date_to']));
+ $result['statement_year'] = (int) date('Y', $result['date_to']);
+ } elseif ($result['date_from']) {
+ $result['statement_number'] = (string) intval(date('m', $result['date_from']));
+ $result['statement_year'] = (int) date('Y', $result['date_from']);
+ } elseif (!empty($result['pdf_year'])) {
+ // Fallback to PDF metadata if no date range
+ $result['statement_number'] = $result['pdf_number'];
+ $result['statement_year'] = $result['pdf_year'];
+ }
+
+ // Validate: at least statement number or IBAN must be present
+ if (empty($result['statement_number']) && empty($result['iban'])) {
+ return false;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Parse a German formatted amount (e.g., "3.681,45" → 3681.45)
+ *
+ * @param string $amount German formatted amount string
+ * @return float Parsed amount
+ */
+ private static function parseGermanAmount($amount)
+ {
+ $amount = str_replace('.', '', $amount); // Remove thousands separator
+ $amount = str_replace(',', '.', $amount); // Convert decimal separator
+ return (float) $amount;
+ }
+
+ /**
+ * Generate a clean filename for a PDF statement
+ *
+ * @param array $parsed Parsed metadata from parsePdfMetadata()
+ * @return string Generated filename
+ */
+ public static function generateFilename($parsed)
+ {
+ $bank = 'Bank';
+ if (!empty($parsed['bank_name'])) {
+ // Shorten bank name - take first meaningful words
+ $bank = preg_replace('/\s+(eG|AG|e\.G\.).*$/', '', $parsed['bank_name']);
+ $bank = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß-]/', '_', $bank);
+ $bank = preg_replace('/_+/', '_', $bank);
+ $bank = trim($bank, '_');
+ }
+
+ $account = !empty($parsed['account_number']) ? $parsed['account_number'] : 'Konto';
+ $year = !empty($parsed['statement_year']) ? $parsed['statement_year'] : date('Y');
+ $nr = !empty($parsed['statement_number']) ? str_pad($parsed['statement_number'], 3, '0', STR_PAD_LEFT) : '000';
+
+ return sprintf('%s_%s_%d_%s.pdf', $bank, $account, $year, $nr);
+ }
+
/**
* Get next available statement number for a year
*
@@ -501,4 +687,86 @@ class BankImportStatement extends CommonObject
}
return '1';
}
+
+ /**
+ * Get the end date (date_to) of the most recent statement
+ *
+ * @return int|null Timestamp of latest date_to, or null if none
+ */
+ public function getLatestStatementEndDate()
+ {
+ $sql = "SELECT MAX(date_to) as last_date";
+ $sql .= " FROM ".MAIN_DB_PREFIX."bankimport_statement";
+ $sql .= " WHERE entity = ".((int) $this->entity);
+
+ $resql = $this->db->query($sql);
+ if ($resql) {
+ $obj = $this->db->fetch_object($resql);
+ if ($obj->last_date) {
+ return $this->db->jdate($obj->last_date);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get list of years that have stored statements
+ *
+ * @return array Array of years (descending)
+ */
+ public function getAvailableYears()
+ {
+ $sql = "SELECT DISTINCT statement_year";
+ $sql .= " FROM ".MAIN_DB_PREFIX."bankimport_statement";
+ $sql .= " WHERE entity = ".((int) $this->entity);
+ $sql .= " ORDER BY statement_year DESC";
+
+ $result = array();
+ $resql = $this->db->query($sql);
+ if ($resql) {
+ while ($obj = $this->db->fetch_object($resql)) {
+ $result[(int) $obj->statement_year] = (string) $obj->statement_year;
+ }
+ $this->db->free($resql);
+ }
+ return $result;
+ }
+
+ /**
+ * Link transactions to this statement based on date range and IBAN
+ *
+ * Updates all transactions that fall within the statement's date range
+ * and match the IBAN, setting their fk_statement to this statement's ID.
+ *
+ * @return int Number of linked transactions, or -1 on error
+ */
+ public function linkTransactions()
+ {
+ if (empty($this->id) || empty($this->date_from) || empty($this->date_to)) {
+ return 0;
+ }
+
+ $sql = "UPDATE ".MAIN_DB_PREFIX."bankimport_transaction SET";
+ $sql .= " fk_statement = ".((int) $this->id);
+ $sql .= " WHERE entity = ".((int) $this->entity);
+ $sql .= " AND date_trans >= '".$this->db->idate($this->date_from)."'";
+ $sql .= " AND date_trans <= '".$this->db->idate($this->date_to)."'";
+ $sql .= " AND fk_statement IS NULL"; // Don't overwrite existing links
+
+ // Match by IBAN if available
+ if (!empty($this->iban)) {
+ $ibanClean = preg_replace('/\s+/', '', $this->iban);
+ $sql .= " AND REPLACE(iban, ' ', '') = '".$this->db->escape($ibanClean)."'";
+ }
+
+ dol_syslog(get_class($this)."::linkTransactions", LOG_DEBUG);
+ $resql = $this->db->query($sql);
+
+ if ($resql) {
+ return $this->db->affected_rows($resql);
+ } else {
+ $this->error = $this->db->lasterror();
+ return -1;
+ }
+ }
}
diff --git a/class/banktransaction.class.php b/class/banktransaction.class.php
old mode 100644
new mode 100755
index c5f5197..4c63631
--- a/class/banktransaction.class.php
+++ b/class/banktransaction.class.php
@@ -161,6 +161,11 @@ class BankImportTransaction extends CommonObject
*/
public $fk_societe;
+ /**
+ * @var int Link to llx_bankimport_statement
+ */
+ public $fk_statement;
+
/**
* @var int Status (0=new, 1=matched, 2=reconciled, 9=ignored)
*/
@@ -335,6 +340,7 @@ class BankImportTransaction extends CommonObject
$this->fk_don = $obj->fk_don;
$this->fk_loan = $obj->fk_loan;
$this->fk_societe = $obj->fk_societe;
+ $this->fk_statement = $obj->fk_statement;
$this->status = $obj->status;
$this->import_key = $obj->import_key;
$this->fk_user_creat = $obj->fk_user_creat;
@@ -392,6 +398,7 @@ class BankImportTransaction extends CommonObject
$sql .= " fk_don = ".($this->fk_don > 0 ? ((int) $this->fk_don) : "NULL").",";
$sql .= " fk_loan = ".($this->fk_loan > 0 ? ((int) $this->fk_loan) : "NULL").",";
$sql .= " fk_societe = ".($this->fk_societe > 0 ? ((int) $this->fk_societe) : "NULL").",";
+ $sql .= " fk_statement = ".($this->fk_statement > 0 ? ((int) $this->fk_statement) : "NULL").",";
$sql .= " status = ".((int) $this->status).",";
$sql .= " fk_user_modif = ".((int) $user->id).",";
$sql .= " note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL").",";
diff --git a/class/fints.class.php b/class/fints.class.php
old mode 100644
new mode 100755
diff --git a/composer.json b/composer.json
old mode 100644
new mode 100755
diff --git a/composer.lock b/composer.lock
old mode 100644
new mode 100755
diff --git a/core/modules/modBankImport.class.php b/core/modules/modBankImport.class.php
index e5d97ea..871aa89 100755
--- a/core/modules/modBankImport.class.php
+++ b/core/modules/modBankImport.class.php
@@ -76,7 +76,7 @@ class modBankImport extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@bankimport'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
- $this->version = '1.0';
+ $this->version = '1.1';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
@@ -87,7 +87,7 @@ class modBankImport extends DolibarrModules
// If file is in theme/yourtheme/img directory under name object_pictovalue.png, use this->picto='pictovalue'
// If file is in module/img directory under name object_pictovalue.png, use this->picto='pictovalue@module'
// To use a supported fa-xxx css style of font awesome, use this->picto='xxx'
- $this->picto = 'fa-file';
+ $this->picto = 'fa-money-check-alt';
// Define some features supported by module (triggers, login, substitutions, menus, css, etc...)
$this->module_parts = array(
@@ -292,146 +292,98 @@ class modBankImport extends DolibarrModules
// Permissions provided by this module
$this->rights = array();
$r = 0;
- // Add here entries to declare new permissions
- /* BEGIN MODULEBUILDER PERMISSIONS */
- /*
- $o = 1;
- $this->rights[$r][0] = $this->numero . sprintf("%02d", ($o * 10) + 1); // Permission id (must not be already used)
- $this->rights[$r][1] = 'Read objects of BankImport'; // Permission label
- $this->rights[$r][4] = 'myobject';
- $this->rights[$r][5] = 'read'; // In php code, permission will be checked by test if ($user->hasRight('bankimport', 'myobject', 'read'))
+
+ // $user->hasRight('bankimport', 'read')
+ $this->rights[$r][0] = $this->numero . '01';
+ $this->rights[$r][1] = 'PermBankImportRead';
+ $this->rights[$r][2] = 'r';
+ $this->rights[$r][3] = 1; // Default enabled
+ $this->rights[$r][4] = 'read';
$r++;
- $this->rights[$r][0] = $this->numero . sprintf("%02d", ($o * 10) + 2); // Permission id (must not be already used)
- $this->rights[$r][1] = 'Create/Update objects of BankImport'; // Permission label
- $this->rights[$r][4] = 'myobject';
- $this->rights[$r][5] = 'write'; // In php code, permission will be checked by test if ($user->hasRight('bankimport', 'myobject', 'write'))
+
+ // $user->hasRight('bankimport', 'write')
+ $this->rights[$r][0] = $this->numero . '02';
+ $this->rights[$r][1] = 'PermBankImportWrite';
+ $this->rights[$r][2] = 'w';
+ $this->rights[$r][3] = 0;
+ $this->rights[$r][4] = 'write';
$r++;
- $this->rights[$r][0] = $this->numero . sprintf("%02d", ($o * 10) + 3); // Permission id (must not be already used)
- $this->rights[$r][1] = 'Delete objects of BankImport'; // Permission label
- $this->rights[$r][4] = 'myobject';
- $this->rights[$r][5] = 'delete'; // In php code, permission will be checked by test if ($user->hasRight('bankimport', 'myobject', 'delete'))
+
+ // $user->hasRight('bankimport', 'delete')
+ $this->rights[$r][0] = $this->numero . '03';
+ $this->rights[$r][1] = 'PermBankImportDelete';
+ $this->rights[$r][2] = 'd';
+ $this->rights[$r][3] = 0;
+ $this->rights[$r][4] = 'delete';
$r++;
- */
- /* END MODULEBUILDER PERMISSIONS */
// Main menu entries to add
$this->menu = array();
$r = 0;
- // Add here entries to declare new menus
- /* BEGIN MODULEBUILDER TOPMENU */
- $this->menu[$r++] = array(
- 'fk_menu' => '', // Will be stored into mainmenu + leftmenu. Use '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode
- 'type' => 'top', // This is a Top menu entry
- 'titre' => 'ModuleBankImportName',
- 'prefix' => img_picto('', $this->picto, 'class="pictofixedwidth valignmiddle"'),
- 'mainmenu' => 'bankimport',
- 'leftmenu' => '',
- 'url' => '/bankimport/bankimportindex.php',
- 'langs' => 'bankimport@bankimport', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory.
- 'position' => 1000 + $r,
- 'enabled' => 'isModEnabled("bankimport")', // Define condition to show or hide menu entry. Use 'isModEnabled("bankimport")' if entry must be visible if module is enabled.
- 'perms' => '1', // Use 'perms'=>'$user->hasRight("bankimport", "myobject", "read")' if you want your menu with a permission rules
- 'target' => '',
- 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both
- );
- /* END MODULEBUILDER TOPMENU */
- /* BEGIN MODULEBUILDER LEFTMENU MYOBJECT */
+ // Left menu entries under "Banken und Kasse" (mainmenu=bank)
+ $r = 0;
$this->menu[$r++] = array(
- 'fk_menu' => 'fk_mainmenu=bankimport',
+ 'fk_menu' => 'fk_mainmenu=bank',
+ 'type' => 'left',
+ 'titre' => 'BankImportMenu',
+ 'prefix' => img_picto('', 'download', 'class="pictofixedwidth valignmiddle paddingright"'),
+ 'mainmenu' => 'bank',
+ 'leftmenu' => 'bankimport',
+ 'url' => '/bankimport/bankimportindex.php?mainmenu=bank&leftmenu=bankimport',
+ 'langs' => 'bankimport@bankimport',
+ 'position' => 200,
+ 'enabled' => 'isModEnabled("bankimport")',
+ 'perms' => '$user->hasRight("bankimport", "read")',
+ 'target' => '',
+ 'user' => 2,
+ );
+ $this->menu[$r++] = array(
+ 'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
'type' => 'left',
'titre' => 'BankStatements',
'prefix' => img_picto('', 'bank_account', 'class="pictofixedwidth valignmiddle paddingright"'),
- 'mainmenu' => 'bankimport',
+ 'mainmenu' => 'bank',
'leftmenu' => 'bankimport_statements',
- 'url' => '/bankimport/statements.php',
+ 'url' => '/bankimport/statements.php?mainmenu=bank&leftmenu=bankimport',
'langs' => 'bankimport@bankimport',
- 'position' => 1001,
+ 'position' => 201,
'enabled' => 'isModEnabled("bankimport")',
- 'perms' => '1',
+ 'perms' => '$user->hasRight("bankimport", "write")',
'target' => '',
'user' => 2,
);
$this->menu[$r++] = array(
- 'fk_menu' => 'fk_mainmenu=bankimport',
+ 'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
'type' => 'left',
'titre' => 'TransactionList',
'prefix' => img_picto('', 'list', 'class="pictofixedwidth valignmiddle paddingright"'),
- 'mainmenu' => 'bankimport',
+ 'mainmenu' => 'bank',
'leftmenu' => 'bankimport_transactions',
- 'url' => '/bankimport/list.php',
+ 'url' => '/bankimport/list.php?mainmenu=bank&leftmenu=bankimport',
'langs' => 'bankimport@bankimport',
- 'position' => 1002,
+ 'position' => 202,
'enabled' => 'isModEnabled("bankimport")',
- 'perms' => '1',
+ 'perms' => '$user->hasRight("bankimport", "read")',
'target' => '',
'user' => 2,
);
$this->menu[$r++] = array(
- 'fk_menu' => 'fk_mainmenu=bankimport',
+ 'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
'type' => 'left',
'titre' => 'PDFStatements',
'prefix' => img_picto('', 'pdf', 'class="pictofixedwidth valignmiddle paddingright"'),
- 'mainmenu' => 'bankimport',
+ 'mainmenu' => 'bank',
'leftmenu' => 'bankimport_pdfstatements',
- 'url' => '/bankimport/pdfstatements.php',
+ 'url' => '/bankimport/pdfstatements.php?mainmenu=bank&leftmenu=bankimport',
'langs' => 'bankimport@bankimport',
- 'position' => 1003,
+ 'position' => 203,
'enabled' => 'isModEnabled("bankimport")',
- 'perms' => '1',
+ 'perms' => '$user->hasRight("bankimport", "read")',
'target' => '',
'user' => 2,
);
- /*
- $this->menu[$r++]=array(
- 'fk_menu' => 'fk_mainmenu=bankimport', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode
- 'type' => 'left', // This is a Left menu entry
- 'titre' => 'MyObject',
- 'prefix' => img_picto('', $this->picto, 'class="pictofixedwidth valignmiddle paddingright"'),
- 'mainmenu' => 'bankimport',
- 'leftmenu' => 'myobject',
- 'url' => '/bankimport/bankimportindex.php',
- 'langs' => 'bankimport@bankimport', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory.
- 'position' => 1000 + $r,
- 'enabled' => 'isModEnabled("bankimport")', // Define condition to show or hide menu entry. Use 'isModEnabled("bankimport")' if entry must be visible if module is enabled.
- 'perms' => '$user->hasRight("bankimport", "myobject", "read")',
- 'target' => '',
- 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both
- 'object' => 'MyObject'
- );
- $this->menu[$r++]=array(
- 'fk_menu' => 'fk_mainmenu=bankimport,fk_leftmenu=myobject', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode
- 'type' => 'left', // This is a Left menu entry
- 'titre' => 'New_MyObject',
- 'mainmenu' => 'bankimport',
- 'leftmenu' => 'bankimport_myobject_new',
- 'url' => '/bankimport/myobject_card.php?action=create',
- 'langs' => 'bankimport@bankimport', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory.
- 'position' => 1000 + $r,
- 'enabled' => 'isModEnabled("bankimport")', // Define condition to show or hide menu entry. Use 'isModEnabled("bankimport")' if entry must be visible if module is enabled. Use '$leftmenu==\'system\'' to show if leftmenu system is selected.
- 'perms' => '$user->hasRight("bankimport", "myobject", "write")'
- 'target' => '',
- 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both
- 'object' => 'MyObject'
- );
- $this->menu[$r++]=array(
- 'fk_menu' => 'fk_mainmenu=bankimport,fk_leftmenu=myobject', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode
- 'type' => 'left', // This is a Left menu entry
- 'titre' => 'List_MyObject',
- 'mainmenu' => 'bankimport',
- 'leftmenu' => 'bankimport_myobject_list',
- 'url' => '/bankimport/myobject_list.php',
- 'langs' => 'bankimport@bankimport', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory.
- 'position' => 1000 + $r,
- 'enabled' => 'isModEnabled("bankimport")', // Define condition to show or hide menu entry. Use 'isModEnabled("bankimport")' if entry must be visible if module is enabled.
- 'perms' => '$user->hasRight("bankimport", "myobject", "read")'
- 'target' => '',
- 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both
- 'object' => 'MyObject'
- );
- */
- /* END MODULEBUILDER LEFTMENU MYOBJECT */
// Exports profiles provided by this module
diff --git a/langs/de_DE/bankimport.lang b/langs/de_DE/bankimport.lang
old mode 100644
new mode 100755
index 2fb6afb..553ceb8
--- a/langs/de_DE/bankimport.lang
+++ b/langs/de_DE/bankimport.lang
@@ -174,6 +174,13 @@ DownloadPDF = PDF herunterladen
NoPDFStatementsFound = Keine PDF-Kontoauszüge gefunden
PDFStatementsImported = %s Kontoauszüge importiert
StatementAlreadyExists = Kontoauszug bereits vorhanden
+DeleteStatement = Kontoauszug löschen
+ConfirmDeleteStatement = Möchten Sie den Kontoauszug %s wirklich löschen?
+StatementsInYear = Kontoauszüge im Jahr %s
+AllStatements = Alle Kontoauszüge
+OpeningBalance = Anfangssaldo
+ClosingBalance = Endsaldo
+StatementUploaded = Kontoauszug erfolgreich hochgeladen
#
# Über-Seite
@@ -183,6 +190,59 @@ BankImportAbout = Über Bankimport
BankImportAboutPage = Bankimport Info-Seite
#
-# Startseite
+# Startseite / Dashboard
#
BankImportArea = Bankimport Übersicht
+LastImportedTransactions = Letzte importierte Buchungen
+LastPDFStatements = Letzte PDF-Kontoauszüge
+ShowAll = Alle anzeigen
+UploadNew = Neuen hochladen
+BankImportMenu = Bankimport
+
+#
+# PDF Kontoauszüge Seite
+#
+PDFStatementsInfo = PDF-Kontoauszüge
+PDFStatementsInfoDesc = Hier können Sie Ihre PDF-Kontoauszüge hochladen, verwalten und einsehen. Die Auszüge werden sicher gespeichert und können jederzeit heruntergeladen werden.
+UploadPDFStatement = PDF-Kontoauszüge hochladen
+
+#
+# Upload-Modus
+#
+UploadMode = Upload-Modus
+UploadModeAuto = Automatisch erkennen
+UploadModeManual = Manuelle Eingabe
+PdfAutoDetected = PDF-Metadaten automatisch erkannt
+ErrorNoFileUploaded = Keine Datei hochgeladen
+ErrorOnlyPDFAllowed = Nur PDF-Dateien sind erlaubt
+ErrorFileTooLarge = Datei ist zu groß (max. 10 MB)
+ErrorFailedToSaveFile = Datei konnte nicht gespeichert werden
+TransactionsLinked = %s Buchungen dem Kontoauszug zugeordnet
+StatementsUploaded = %s Kontoauszüge erfolgreich hochgeladen
+MultipleFilesHint = Sie können mehrere PDF-Dateien gleichzeitig auswählen (Strg+Klick oder Shift+Klick)
+
+#
+# Admin - PDF Upload
+#
+PDFUploadSettings = PDF-Upload Einstellungen
+DefaultUploadMode = Standard Upload-Modus
+DefaultUploadModeHelp = Automatisch: Metadaten werden aus dem PDF extrahiert. Manuell: Alle Felder müssen von Hand ausgefüllt werden.
+ReminderEnabled = Erinnerung aktivieren
+ReminderEnabledHelp = Zeigt eine Warnung wenn Kontoauszüge nicht aktuell sind
+ReminderMonths = Erinnerung nach (Monate)
+ReminderMonthsHelp = Warnung anzeigen, wenn der letzte Kontoauszug älter als X Monate ist
+ReminderNoStatements = Es wurden noch keine Kontoauszüge hochgeladen. Bitte laden Sie Ihre Kontoauszüge hoch.
+ReminderOutdatedStatements = Der letzte Kontoauszug endet am %s (vor %s Monaten). Bitte laden Sie aktuelle Kontoauszüge hoch.
+
+#
+# Kontoauszug-Verknüpfung
+#
+PDFStatement = PDF-Kontoauszug
+ViewPDFStatement = PDF-Kontoauszug anzeigen
+
+#
+# Berechtigungen
+#
+PermBankImportRead = Bankimport: Buchungen und Kontoauszüge ansehen
+PermBankImportWrite = Bankimport: Kontoauszüge abrufen und PDF hochladen
+PermBankImportDelete = Bankimport: Buchungen und Kontoauszüge löschen
diff --git a/list.php b/list.php
old mode 100644
new mode 100755
index 3a07868..0ee2faa
--- a/list.php
+++ b/list.php
@@ -62,6 +62,11 @@ $confirm = GETPOST('confirm', 'alpha');
$toselect = GETPOST('toselect', 'array');
$contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'bankimporttransactionlist';
+// Security check
+if (!$user->hasRight('bankimport', 'read')) {
+ accessforbidden();
+}
+
// Search parameters
$search_ref = GETPOST('search_ref', 'alpha');
$search_iban = GETPOST('search_iban', 'alpha');
@@ -206,6 +211,10 @@ print ' - ';
print ' ';
print '';
+// Statement
+print '';
+print ' ';
+
// Status
print '';
$statusArray = array(
@@ -235,6 +244,7 @@ print_liste_field_titre($langs->trans("Date"), $_SERVER["PHP_SELF"], "date_trans
print_liste_field_titre($langs->trans("Counterparty"), $_SERVER["PHP_SELF"], "name", "", $param, "", $sortfield, $sortorder);
print_liste_field_titre($langs->trans("Description"), $_SERVER["PHP_SELF"], "description", "", $param, "", $sortfield, $sortorder);
print_liste_field_titre($langs->trans("Amount"), $_SERVER["PHP_SELF"], "amount", "", $param, 'class="right"', $sortfield, $sortorder);
+print_liste_field_titre($langs->trans("PDFStatement"), $_SERVER["PHP_SELF"], "fk_statement", "", $param, 'class="center"', $sortfield, $sortorder);
print_liste_field_titre($langs->trans("Status"), $_SERVER["PHP_SELF"], "status", "", $param, 'class="center"', $sortfield, $sortorder);
print_liste_field_titre('', $_SERVER["PHP_SELF"], "", "", $param, 'class="center"', $sortfield, $sortorder);
print '';
@@ -278,6 +288,15 @@ if (is_array($records) && count($records) > 0) {
}
print ' ';
+ // Statement link
+ print '';
+ if (!empty($obj->fk_statement)) {
+ print '';
+ print img_picto($langs->trans("ViewPDFStatement"), 'pdf');
+ print ' ';
+ }
+ print ' ';
+
// Status
print '';
print $obj->getLibStatut(5);
@@ -291,7 +310,7 @@ if (is_array($records) && count($records) > 0) {
print '';
}
} else {
- print ' ';
+ print ' ';
print $langs->trans("NoTransactionsInDatabase");
print ' ';
}
diff --git a/pdfstatements.php b/pdfstatements.php
old mode 100644
new mode 100755
index c01c12e..233b08c
--- a/pdfstatements.php
+++ b/pdfstatements.php
@@ -59,10 +59,10 @@ $langs->loadLangs(array("bankimport@bankimport", "banks", "other"));
$action = GETPOST('action', 'aZ09');
$confirm = GETPOST('confirm', 'alpha');
-$year = GETPOSTINT('year') ?: (int) date('Y');
+$year = GETPOSTISSET('year') ? GETPOSTINT('year') : (int) date('Y');
// Security check
-if (empty($user->rights->bankimport->statement->read)) {
+if (!$user->hasRight('bankimport', 'read')) {
accessforbidden();
}
@@ -72,66 +72,217 @@ if (empty($user->rights->bankimport->statement->read)) {
$statement = new BankImportStatement($db);
-// Upload PDF
-if ($action == 'upload' && !empty($_FILES['pdffile']['name'])) {
- $error = 0;
+// Upload PDF (supports multiple files)
+if ($action == 'upload' && !empty($_FILES['pdffile'])) {
+ $uploadMode = GETPOST('upload_mode', 'alpha');
+ $isAutoMode = ($uploadMode !== 'manual');
- // Validate required fields
- $statementNumber = GETPOST('statement_number', 'alpha');
- $statementYear = GETPOSTINT('statement_year');
- $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'));
+ // 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);
- if (empty($statementNumber)) {
- setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesaliases("StatementNumber")), null, 'errors');
- $error++;
- }
- if (empty($statementYear)) {
- setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesaliases("Year")), null, 'errors');
- $error++;
- }
+ $uploadedCount = 0;
+ $errorCount = 0;
+ $totalLinked = 0;
+ $lastYear = (int) date('Y');
- if (!$error) {
- $statement->iban = GETPOST('iban', 'alpha');
- $statement->statement_number = $statementNumber;
- $statement->statement_year = $statementYear;
- $statement->statement_date = $statementDate ?: null;
- $statement->date_from = $dateFrom ?: null;
- $statement->date_to = $dateTo ?: null;
- $statement->opening_balance = GETPOST('opening_balance', 'alpha') !== '' ? (float) price2num(GETPOST('opening_balance', 'alpha')) : null;
- $statement->closing_balance = GETPOST('closing_balance', 'alpha') !== '' ? (float) price2num(GETPOST('closing_balance', 'alpha')) : null;
- $statement->import_key = date('YmdHis').'_'.$user->id;
+ 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 ($statement->exists()) {
- setEventMessages($langs->trans("StatementAlreadyExists"), null, 'errors');
- $error++;
+ if ($stmt->exists()) {
+ setEventMessages($langs->trans("StatementAlreadyExists").': '.$statementNumber.'/'.$statementYear, null, 'warnings');
+ $errorCount++;
+ continue;
}
- }
- if (!$error) {
- // Save uploaded file
- $uploadResult = $statement->saveUploadedPDF($_FILES['pdffile']);
+ // Generate filename and save file
+ $dir = BankImportStatement::getStorageDir();
- if ($uploadResult < 0) {
- setEventMessages($statement->error, null, 'errors');
- $error++;
+ 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)
+ );
}
- }
- if (!$error) {
+ $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 = $statement->create($user);
+ $result = $stmt->create($user);
if ($result > 0) {
- setEventMessages($langs->trans("StatementUploaded"), null, 'mesgs');
- header("Location: ".$_SERVER['PHP_SELF']."?year=".$statementYear);
- exit;
+ // Link matching transactions to this statement
+ $linked = $stmt->linkTransactions();
+ $totalLinked += max(0, $linked);
+ $uploadedCount++;
+ $lastYear = $stmt->statement_year;
} else {
- setEventMessages($statement->error, null, 'errors');
+ 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
@@ -205,6 +356,25 @@ llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-bankimport page-pdfstatemen
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").' ';
@@ -230,10 +400,13 @@ if ($action == 'delete') {
}
// Upload form
+$defaultMode = getDolGlobalString('BANKIMPORT_UPLOAD_MODE') ?: 'auto';
+$uploadMode = GETPOST('upload_mode', 'alpha') ?: $defaultMode;
+
print '