Version 1.1: PDF-Kontoauszüge, Dashboard, Menü-Integration

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-14 19:11:46 +01:00
parent 3de2fb6fa3
commit 1fc10d3781
371 changed files with 1123 additions and 394 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
# Uploaded bank statements
/Kontoauszüge/
# Build artifacts
/bin/

View file

@ -1,5 +1,21 @@
# CHANGELOG MODULE BANKIMPORT FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
## 1.1
- Mehrfach-Upload von PDF-Kontoauszügen
- Automatische Metadaten-Erkennung aus PDF-Kontoauszügen (Auszugsnummer, Zeitraum, Saldo)
- Erinnerungsfunktion bei veralteten Kontoauszügen (konfigurierbar im Admin-Bereich)
- Verknüpfung von Buchungen mit PDF-Kontoauszügen
- Dashboard mit Übersichts-Widgets (letzte Buchungen und Kontoauszüge)
- Menü-Integration unter "Banken und Kasse"
- Jahrfilter zeigt nur Jahre mit vorhandenen Kontoauszügen
- Auszugsnummer wird automatisch aus dem Zeitraum abgeleitet (Monat/Jahr)
## 1.0
Initial version
- FinTS/HBCI-Anbindung mit SecureGo Plus TAN-Verfahren
- Import von Kontobuchungen
- Automatischer Import per Cronjob
- Buchungszuordnung zu Rechnungen
- PDF-Kontoauszüge Upload

133
README.md
View file

@ -1,96 +1,83 @@
# BANKIMPORT FOR [DOLIBARR ERP & CRM](https://www.dolibarr.org)
Dolibarr-Modul zum Import von Kontoauszügen und Buchungen über die FinTS/HBCI-Schnittstelle deutscher Banken.
## Features
Description of the module...
- **FinTS/HBCI-Anbindung**: Automatischer Abruf von Kontobuchungen über die FinTS-Schnittstelle (getestet mit VR-Banken/Atruvia)
- **TAN-Verfahren**: Unterstützung von SecureGo Plus (Decoupled TAN) für die TAN-Bestätigung per App
- **Automatischer Import**: Cronjob-basierter Import von Buchungen (täglich, zweimal wöchentlich oder wöchentlich)
- **Buchungszuordnung**: Automatische Zuordnung importierter Buchungen zu Rechnungen anhand von Referenznummern, Beträgen, Namen und IBAN
- **PDF-Kontoauszüge**: Upload und Verwaltung von PDF-Kontoauszügen mit automatischer Metadaten-Erkennung (Auszugsnummer, Zeitraum, Saldo)
- **Mehrfach-Upload**: Gleichzeitiger Upload mehrerer PDF-Kontoauszüge
- **Dashboard**: Übersichtsseite mit den letzten Buchungen und Kontoauszügen
- **Erinnerungsfunktion**: Konfigurierbare Warnung wenn Kontoauszüge nicht aktuell sind
- **Integration**: Einbindung in das Dolibarr-Menü "Banken und Kasse"
<!--
![Screenshot bankimport](img/screenshot_bankimport.png?raw=true "BankImport"){imgmd}
-->
Other external modules are available on [Dolistore.com](https://www.dolistore.com).
## Translations
Translations can be completed manually by editing files in the module directories under `langs`.
<!--
This module contains also a sample configuration for Transifex, under the hidden directory [.tx](.tx), so it is possible to manage translation using this service.
For more information, see the [translator's documentation](https://wiki.dolibarr.org/index.php/Translator_documentation).
There is a [Transifex project](https://transifex.com/projects/p/dolibarr-module-template) for this module.
-->
## Voraussetzungen
- Dolibarr ERP & CRM >= 16.0
- PHP >= 8.0
- `pdfinfo` und `pdftotext` (Paket `poppler-utils`) für die PDF-Metadaten-Erkennung
- Zugang zu einer Bank mit FinTS/HBCI-Schnittstelle
## Installation
Prerequisites: You must have Dolibarr ERP & CRM software installed. You can download it from [Dolistore.org](https://www.dolibarr.org).
You can also get a ready-to-use instance in the cloud from https://saas.dolibarr.org
### From the ZIP file and GUI interface
If the module is a ready-to-deploy zip file, so with a name `module_xxx-version.zip` (e.g., when downloading it from a marketplace like [Dolistore](https://www.dolistore.com)),
go to menu `Home> Setup> Modules> Deploy external module` and upload the zip file.
<!--
Note: If this screen tells you that there is no "custom" directory, check that your setup is correct:
- In your Dolibarr installation directory, edit the `htdocs/conf/conf.php` file and check that following lines are not commented:
```php
//$dolibarr_main_url_root_alt ...
//$dolibarr_main_document_root_alt ...
```
- Uncomment them if necessary (delete the leading `//`) and assign the proper value according to your Dolibarr installation
For example :
- UNIX:
```php
$dolibarr_main_url_root_alt = '/custom';
$dolibarr_main_document_root_alt = '/var/www/Dolibarr/htdocs/custom';
```
- Windows:
```php
$dolibarr_main_url_root_alt = '/custom';
$dolibarr_main_document_root_alt = 'C:/My Web Sites/Dolibarr/htdocs/custom';
```
-->
<!--
### From a GIT repository
Clone the repository in `$dolibarr_main_document_root_alt/bankimport`
### Aus dem Git-Repository
```shell
cd ....../custom
git clone git@github.com:gitlogin/bankimport.git bankimport
cd /path/to/dolibarr/custom
git clone <repository-url> bankimport
cd bankimport
composer install
```
-->
### Aktivierung
### Final steps
1. In Dolibarr als Administrator anmelden
2. Unter "Einstellungen" > "Module/Applikationen" das Modul "Bankimport" aktivieren
3. Unter "Banken und Kasse" > "Bankimport" die FinTS-Verbindungsdaten konfigurieren
Using your browser:
## Konfiguration
- Log into Dolibarr as a super-administrator
- Go to "Setup"> "Modules"
- You should now be able to find and enable the module
### FinTS-Verbindung
- **FinTS Server URL**: Die FinTS-URL Ihrer Bank (z.B. `https://fints1.atruvia.de/cgi-bin/hbciservlet` für VR-Banken)
- **Bankleitzahl (BLZ)**: 8-stellige Bankleitzahl
- **Benutzerkennung**: Ihre Online-Banking Benutzerkennung
- **PIN**: Wird verschlüsselt in der Datenbank gespeichert
- **IBAN**: Kontonummer/IBAN des abzurufenden Kontos
### Automatischer Import
## Licenses
Der automatische Import kann im Admin-Bereich aktiviert werden. Die Buchungen werden dann per Dolibarr-Cronjob abgerufen. Unterstützte Intervalle: täglich, zweimal wöchentlich, wöchentlich.
### Main code
### PDF-Upload Einstellungen
GPLv3 or (at your option) any later version. See file COPYING for more information.
- **Upload-Modus**: Automatisch (Metadaten aus PDF extrahieren) oder Manuell
- **Erinnerung**: Konfigurierbare Warnung wenn der letzte Kontoauszug älter als X Monate ist
### Documentation
## Berechtigungen
All texts and readme's are licensed under [GFDL](https://www.gnu.org/licenses/fdl-1.3.en.html).
- **Bankimport lesen**: Buchungen und Kontoauszüge ansehen
- **Bankimport schreiben**: Kontoauszüge abrufen und PDF hochladen
- **Bankimport löschen**: Buchungen und Kontoauszüge löschen
## Technische Details
### Verwendete Bibliotheken
- [nemiah/php-fints](https://github.com/nemiah/php-fints) - PHP FinTS/HBCI Bibliothek
### Datenbank-Tabellen
- `llx_bankimport_transaction` - Importierte Buchungen
- `llx_bankimport_statement` - PDF-Kontoauszüge
## Lizenz
GPLv3 oder (nach Wahl) jede spätere Version. Siehe Datei COPYING für weitere Informationen.
## Autor
Eduard Wisch - [data IT solution](https://data-it-solution.de)

View file

@ -193,6 +193,23 @@ if ($action == 'update') {
$error++;
}
// PDF Upload mode default
$res = dolibarr_set_const($db, "BANKIMPORT_UPLOAD_MODE", GETPOST('BANKIMPORT_UPLOAD_MODE', 'alpha'), 'chaine', 0, '', $conf->entity);
if (!($res > 0)) {
$error++;
}
// Reminder setting
$res = dolibarr_set_const($db, "BANKIMPORT_REMINDER_ENABLED", GETPOSTINT('BANKIMPORT_REMINDER_ENABLED'), 'chaine', 0, '', $conf->entity);
if (!($res > 0)) {
$error++;
}
$res = dolibarr_set_const($db, "BANKIMPORT_REMINDER_MONTHS", GETPOSTINT('BANKIMPORT_REMINDER_MONTHS'), 'chaine', 0, '', $conf->entity);
if (!($res > 0)) {
$error++;
}
// Automatic import settings
$res = dolibarr_set_const($db, "BANKIMPORT_AUTO_ENABLED", GETPOSTINT('BANKIMPORT_AUTO_ENABLED'), 'chaine', 0, '', $conf->entity);
if (!($res > 0)) {
@ -371,6 +388,51 @@ print '</tr>';
print '</table>';
// PDF Upload Section
print '<br>';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<td colspan="2">'.$langs->trans("PDFUploadSettings").'</td>';
print '</tr>';
// Default upload mode
$defaultUploadMode = getDolGlobalString('BANKIMPORT_UPLOAD_MODE') ?: 'auto';
print '<tr class="oddeven">';
print '<td class="titlefield">'.$langs->trans("DefaultUploadMode").'</td>';
print '<td>';
$uploadModes = array(
'auto' => $langs->trans("UploadModeAuto"),
'manual' => $langs->trans("UploadModeManual"),
);
print $form->selectarray('BANKIMPORT_UPLOAD_MODE', $uploadModes, $defaultUploadMode, 0, 0, 0, '', 0, 0, 0, '', 'minwidth200');
print ' <span class="opacitymedium small">'.$langs->trans("DefaultUploadModeHelp").'</span>';
print '</td>';
print '</tr>';
// Reminder when no statement uploaded
$reminderEnabled = getDolGlobalString('BANKIMPORT_REMINDER_ENABLED', '1');
print '<tr class="oddeven">';
print '<td class="titlefield">'.$langs->trans("ReminderEnabled").'</td>';
print '<td>';
print '<input type="checkbox" name="BANKIMPORT_REMINDER_ENABLED" value="1"'.($reminderEnabled ? ' checked' : '').'>';
print ' <span class="opacitymedium small">'.$langs->trans("ReminderEnabledHelp").'</span>';
print '</td>';
print '</tr>';
// Reminder months threshold
$reminderMonths = getDolGlobalInt('BANKIMPORT_REMINDER_MONTHS') ?: 3;
print '<tr class="oddeven">';
print '<td class="titlefield">'.$langs->trans("ReminderMonths").'</td>';
print '<td>';
$monthOptions = array(1 => '1', 2 => '2', 3 => '3', 4 => '4', 5 => '5', 6 => '6');
print $form->selectarray('BANKIMPORT_REMINDER_MONTHS', $monthOptions, $reminderMonths, 0, 0, 0, '', 0, 0, 0, '', 'minwidth75');
print ' <span class="opacitymedium small">'.$langs->trans("ReminderMonthsHelp").'</span>';
print '</td>';
print '</tr>';
print '</table>';
// Automatic Import Section
print '<br>';
print '<table class="noborder centpercent">';

0
ajax/checktan.php Normal file → Executable file
View file

View file

@ -1,10 +1,5 @@
<?php
/* Copyright (C) 2001-2005 Rodolphe Quiedeville <rodolphe@quiedeville.org>
* Copyright (C) 2004-2015 Laurent Destailleur <eldy@users.sourceforge.net>
* Copyright (C) 2005-2012 Regis Houssin <regis.houssin@inodbox.com>
* Copyright (C) 2015 Jean-François Ferry <jfefe@aternatik.fr>
* Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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 '<div class="warning">';
print img_warning().' '.$langs->trans("ReminderNoStatements");
print ' <a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'">'.$langs->trans("UploadPDFStatement").'</a>';
print '</div><br>';
} elseif ($lastEndDate < $thresholdDate) {
$monthsAgo = (int) round((dol_now() - $lastEndDate) / (30 * 24 * 3600));
print '<div class="warning">';
print img_warning().' '.$langs->trans("ReminderOutdatedStatements", dol_print_date($lastEndDate, 'day'), $monthsAgo);
print ' <a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'">'.$langs->trans("UploadPDFStatement").'</a>';
print '</div><br>';
}
}
print '<div class="fichecenter"><div class="fichethirdleft">';
// -----------------------------------------------
// 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 '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th colspan="4">';
print $langs->trans("LastImportedTransactions");
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th colspan="3">'.$langs->trans("DraftMyObjects").($num?'<span class="badge marginleftonlyshort">'.$num.'</span>':'').'</th></tr>';
$var = true;
if ($num > 0)
{
$i = 0;
while ($i < $num)
{
$obj = $db->fetch_object($resql);
print '<tr class="oddeven"><td class="nowrap">';
$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 '</td>';
print '<td class="nowrap">';
print '</td>';
print '<td class="right" class="nowrap">'.price($obj->total_ttc).'</td></tr>';
$i++;
$total += $obj->total_ttc;
}
if ($total>0)
{
print '<tr class="liste_total"><td>'.$langs->trans("Total").'</td><td colspan="2" class="right">'.price($total)."</td></tr>";
}
}
else
{
print '<tr class="oddeven"><td colspan="3" class="opacitymedium">'.$langs->trans("NoOrder").'</td></tr>';
}
print "</table><br>";
$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 '<a class="paddingleft" href="'.dol_buildpath('/bankimport/list.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
print '<span class="badge">'.$objcount->total.'</span>';
print '</a>';
}
}
END MODULEBUILDER DRAFT MYOBJECT */
print '</th>';
print '</tr>';
if ($resql) {
$num = $db->num_rows($resql);
if ($num > 0) {
$i = 0;
while ($i < $num) {
$obj = $db->fetch_object($resql);
print '<tr class="oddeven">';
// Date
print '<td class="nowraponall">';
print dol_print_date($db->jdate($obj->date_trans), 'day');
print '</td>';
// Name + Description
print '<td class="tdoverflowmax200">';
print '<a href="'.dol_buildpath('/bankimport/card.php', 1).'?id='.$obj->rowid.'&mainmenu=bank&leftmenu=bankimport">';
print dol_escape_htmltag(dol_trunc($obj->name, 30));
print '</a>';
if ($obj->description) {
print '<br><span class="opacitymedium small">'.dol_escape_htmltag(dol_trunc($obj->description, 40)).'</span>';
}
print '</td>';
// Amount
print '<td class="right nowraponall">';
if ($obj->amount >= 0) {
print '<span class="amount" style="color: green;">+'.price($obj->amount, 0, $langs, 1, -1, 2, $obj->currency).'</span>';
} else {
print '<span class="amount" style="color: red;">'.price($obj->amount, 0, $langs, 1, -1, 2, $obj->currency).'</span>';
}
print '</td>';
// Status
print '<td class="right nowraponall">';
switch ($obj->status) {
case 0:
print '<span class="badge badge-status4 badge-status">'.$langs->trans("New").'</span>';
break;
case 1:
print '<span class="badge badge-status1 badge-status">'.$langs->trans("Matched").'</span>';
break;
case 2:
print '<span class="badge badge-status6 badge-status">'.$langs->trans("Reconciled").'</span>';
break;
case 9:
print '<span class="badge badge-status5 badge-status">'.$langs->trans("Ignored").'</span>';
break;
}
print '</td>';
print '</tr>';
$i++;
}
} else {
print '<tr class="oddeven"><td colspan="4" class="opacitymedium">'.$langs->trans("NoTransactionsInDatabase").'</td></tr>';
}
$db->free($resql);
} else {
dol_print_error($db);
}
print '</table>';
// Link "Alle anzeigen"
if (!empty($objcount) && $objcount->total > 0) {
print '<div class="right" style="margin-top: 5px;">';
print '<a href="'.dol_buildpath('/bankimport/list.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
print $langs->trans("ShowAll").' &raquo;';
print '</a>';
print '</div>';
}
print '</div><div class="fichetwothirdright">';
/* 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 '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th colspan="2">';
print $langs->trans("BoxTitleLatestModifiedMyObjects", $max);
print '</th>';
print '<th class="right">'.$langs->trans("DateModificationShort").'</th>';
print '</tr>';
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 '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th colspan="5">';
print $langs->trans("LastPDFStatements");
print '<tr class="oddeven">';
print '<td class="nowrap">'.$myobjectstatic->getNomUrl(1).'</td>';
print '<td class="right nowrap">';
print "</td>";
print '<td class="right nowrap">'.dol_print_date($db->jdate($objp->tms), 'day')."</td>";
print '</tr>';
$i++;
}
$db->free($resql);
} else {
print '<tr class="oddeven"><td colspan="3" class="opacitymedium">'.$langs->trans("None").'</td></tr>';
}
print "</table><br>";
// 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 '<a class="paddingleft" href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
print '<span class="badge">'.$objcount2->total.'</span>';
print '</a>';
}
}
*/
print '</th>';
print '</tr>';
if ($resql2) {
$num2 = $db->num_rows($resql2);
if ($num2 > 0) {
$i = 0;
while ($i < $num2) {
$obj2 = $db->fetch_object($resql2);
print '<tr class="oddeven">';
// Statement number / Year
print '<td class="nowraponall">';
print '<strong>'.dol_escape_htmltag($obj2->statement_number).'</strong>/'.$obj2->statement_year;
print '</td>';
// IBAN (shortened)
print '<td class="tdoverflowmax150">';
if ($obj2->iban) {
print dol_escape_htmltag(dol_trunc($obj2->iban, 20));
} else {
print '<span class="opacitymedium">-</span>';
}
print '</td>';
// Period
print '<td class="center nowraponall">';
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 '<span class="opacitymedium">-</span>';
}
print '</td>';
// Closing balance
print '<td class="right nowraponall">';
if ($obj2->closing_balance !== null && $obj2->closing_balance !== '') {
$color = (float) $obj2->closing_balance >= 0 ? '' : 'color: red;';
print '<span style="'.$color.'">'.price($obj2->closing_balance, 0, $langs, 1, -1, 2, 'EUR').'</span>';
} else {
print '<span class="opacitymedium">-</span>';
}
print '</td>';
// Actions
print '<td class="center nowraponall">';
if ($obj2->filepath && file_exists($obj2->filepath)) {
print '<a class="paddingright" href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?action=view&id='.$obj2->rowid.'&token='.newToken().'" target="_blank" title="'.$langs->trans("View").'">';
print img_picto($langs->trans("View"), 'eye');
print '</a>';
print '<a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?action=download&id='.$obj2->rowid.'&token='.newToken().'" title="'.$langs->trans("Download").'">';
print img_picto($langs->trans("Download"), 'download');
print '</a>';
}
print '</td>';
print '</tr>';
$i++;
}
} else {
print '<tr class="oddeven"><td colspan="5" class="opacitymedium">'.$langs->trans("NoPDFStatementsFound").'</td></tr>';
}
$db->free($resql2);
} else {
dol_print_error($db);
}
print '</table>';
// Links
print '<div class="right" style="margin-top: 5px;">';
if (!empty($objcount2) && $objcount2->total > 0) {
print '<a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
print $langs->trans("ShowAll");
print '</a>';
print ' | ';
}
print '<a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
print $langs->trans("UploadNew").' &raquo;';
print '</a>';
print '</div>';
print '</div></div>';

24
card.php Normal file → Executable file
View file

@ -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 '</tr>';
}
// 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 '<tr>';
print '<td>'.$langs->trans("PDFStatement").'</td>';
print '<td>';
print '<a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?action=view&id='.$stmt->id.'&token='.newToken().'" target="_blank">';
print img_picto($langs->trans("ViewPDFStatement"), 'pdf').' ';
print $langs->trans("StatementNumber").' '.$stmt->statement_number.'/'.$stmt->statement_year;
print '</a>';
if ($stmt->date_from && $stmt->date_to) {
print ' <span class="opacitymedium">('.dol_print_date($stmt->date_from, 'day').' - '.dol_print_date($stmt->date_to, 'day').')</span>';
}
print '</td>';
print '</tr>';
}
// Linked third party
if ($object->fk_societe > 0) {
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';

0
class/bankimportcron.class.php Normal file → Executable file
View file

268
class/bankstatement.class.php Normal file → Executable file
View file

@ -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;
}
}
}

7
class/banktransaction.class.php Normal file → Executable file
View file

@ -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").",";

0
class/fints.class.php Normal file → Executable file
View file

0
composer.json Normal file → Executable file
View file

0
composer.lock generated Normal file → Executable file
View file

View file

@ -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

62
langs/de_DE/bankimport.lang Normal file → Executable file
View file

@ -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

21
list.php Normal file → Executable file
View file

@ -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 '<input type="text" class="flat maxwidth50" name="search_amount_max" placeholder="Max" value="'.dol_escape_htmltag($search_amount_max).'">';
print '</td>';
// Statement
print '<td class="liste_titre">';
print '</td>';
// Status
print '<td class="liste_titre center">';
$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 '</tr>';
@ -278,6 +288,15 @@ if (is_array($records) && count($records) > 0) {
}
print '</td>';
// Statement link
print '<td class="center nowraponall">';
if (!empty($obj->fk_statement)) {
print '<a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?action=view&id='.$obj->fk_statement.'&token='.newToken().'" target="_blank" title="'.$langs->trans("ViewPDFStatement").'">';
print img_picto($langs->trans("ViewPDFStatement"), 'pdf');
print '</a>';
}
print '</td>';
// Status
print '<td class="center">';
print $obj->getLibStatut(5);
@ -291,7 +310,7 @@ if (is_array($records) && count($records) > 0) {
print '</tr>';
}
} else {
print '<tr class="oddeven"><td colspan="8" class="opacitymedium center">';
print '<tr class="oddeven"><td colspan="9" class="opacitymedium center">';
print $langs->trans("NoTransactionsInDatabase");
print '</td></tr>';
}

355
pdfstatements.php Normal file → Executable file
View file

@ -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 '<div class="warning">';
print img_warning().' '.$langs->trans("ReminderNoStatements");
print '</div><br>';
} elseif ($lastEndDate < $thresholdDate) {
$monthsAgo = (int) round((dol_now() - $lastEndDate) / (30 * 24 * 3600));
print '<div class="warning">';
print img_warning().' '.$langs->trans("ReminderOutdatedStatements", dol_print_date($lastEndDate, 'day'), $monthsAgo);
print '</div><br>';
}
}
// Info box
print '<div class="info" style="margin-bottom: 15px;">';
print '<strong>'.$langs->trans("PDFStatementsInfo").'</strong><br>';
@ -230,10 +400,13 @@ if ($action == 'delete') {
}
// Upload form
$defaultMode = getDolGlobalString('BANKIMPORT_UPLOAD_MODE') ?: 'auto';
$uploadMode = GETPOST('upload_mode', 'alpha') ?: $defaultMode;
print '<div class="fichecenter">';
print '<div class="fichehalfleft">';
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" enctype="multipart/form-data">';
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" enctype="multipart/form-data" id="uploadform">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="upload">';
@ -242,16 +415,28 @@ print '<tr class="liste_titre">';
print '<td colspan="2">'.$langs->trans("UploadPDFStatement").'</td>';
print '</tr>';
// PDF file
// Upload mode selection
print '<tr class="oddeven">';
print '<td class="titlefield fieldrequired">'.$langs->trans("File").'</td>';
print '<td class="titlefield">'.$langs->trans("UploadMode").'</td>';
print '<td>';
print '<input type="file" name="pdffile" accept=".pdf,application/pdf" required>';
print '<label style="margin-right: 15px;"><input type="radio" name="upload_mode" value="auto" id="mode_auto"'.($uploadMode == 'auto' ? ' checked' : '').' onchange="toggleUploadMode()"> '.$langs->trans("UploadModeAuto").'</label>';
print '<label><input type="radio" name="upload_mode" value="manual" id="mode_manual"'.($uploadMode == 'manual' ? ' checked' : '').' onchange="toggleUploadMode()"> '.$langs->trans("UploadModeManual").'</label>';
print '</td>';
print '</tr>';
// IBAN (optional)
// PDF file (always visible, multiple in auto mode)
print '<tr class="oddeven">';
print '<td class="fieldrequired">'.$langs->trans("File").'</td>';
print '<td>';
print '<input type="file" name="pdffile[]" id="pdffile_input" accept=".pdf,application/pdf" multiple required>';
print '<br><span class="opacitymedium small" id="multi_hint">'.$langs->trans("MultipleFilesHint").'</span>';
print '</td>';
print '</tr>';
// --- Manual fields (hidden when auto mode) ---
// IBAN
print '<tr class="oddeven manual-field">';
print '<td>'.$langs->trans("IBAN").'</td>';
print '<td>';
print '<input type="text" class="flat minwidth200" name="iban" value="'.dol_escape_htmltag(GETPOST('iban', 'alpha')).'" placeholder="DE89 3704 0044 0532 0130 00">';
@ -259,7 +444,7 @@ print '</td>';
print '</tr>';
// Year
print '<tr class="oddeven">';
print '<tr class="oddeven manual-field">';
print '<td class="fieldrequired">'.$langs->trans("Year").'</td>';
print '<td>';
$years = array();
@ -271,16 +456,16 @@ print '</td>';
print '</tr>';
// Statement number
print '<tr class="oddeven">';
print '<tr class="oddeven manual-field">';
print '<td class="fieldrequired">'.$langs->trans("StatementNumber").'</td>';
print '<td>';
$nextNum = $statement->getNextStatementNumber($year);
print '<input type="text" class="flat width75" name="statement_number" value="'.dol_escape_htmltag(GETPOSTISSET('statement_number') ? GETPOST('statement_number', 'alpha') : $nextNum).'" required>';
print '<input type="text" class="flat width75" name="statement_number" value="'.dol_escape_htmltag(GETPOSTISSET('statement_number') ? GETPOST('statement_number', 'alpha') : '').'">';
print '</td>';
print '</tr>';
// Statement date
print '<tr class="oddeven">';
print '<tr class="oddeven manual-field">';
print '<td>'.$langs->trans("StatementDate").'</td>';
print '<td>';
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);
@ -288,7 +473,7 @@ print '</td>';
print '</tr>';
// Period from
print '<tr class="oddeven">';
print '<tr class="oddeven manual-field">';
print '<td>'.$langs->trans("DateFrom").'</td>';
print '<td>';
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);
@ -296,7 +481,7 @@ print '</td>';
print '</tr>';
// Period to
print '<tr class="oddeven">';
print '<tr class="oddeven manual-field">';
print '<td>'.$langs->trans("DateTo").'</td>';
print '<td>';
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);
@ -304,7 +489,7 @@ print '</td>';
print '</tr>';
// Opening balance
print '<tr class="oddeven">';
print '<tr class="oddeven manual-field">';
print '<td>'.$langs->trans("OpeningBalance").'</td>';
print '<td>';
print '<input type="text" class="flat width100" name="opening_balance" value="'.dol_escape_htmltag(GETPOST('opening_balance', 'alpha')).'" placeholder="1.234,56">';
@ -313,7 +498,7 @@ print '</td>';
print '</tr>';
// Closing balance
print '<tr class="oddeven">';
print '<tr class="oddeven manual-field">';
print '<td>'.$langs->trans("ClosingBalance").'</td>';
print '<td>';
print '<input type="text" class="flat width100" name="closing_balance" value="'.dol_escape_htmltag(GETPOST('closing_balance', 'alpha')).'" placeholder="1.345,67">';
@ -329,16 +514,49 @@ print '</div>';
print '</form>';
// JavaScript for toggling upload modes
print '<script type="text/javascript">
function toggleUploadMode() {
var isManual = document.getElementById("mode_manual").checked;
var manualFields = document.querySelectorAll(".manual-field");
var fileInput = document.getElementById("pdffile_input");
var multiHint = document.getElementById("multi_hint");
for (var i = 0; i < manualFields.length; i++) {
manualFields[i].style.display = isManual ? "" : "none";
}
// In manual mode: single file only. In auto mode: multiple files allowed
if (isManual) {
fileInput.removeAttribute("multiple");
multiHint.style.display = "none";
} else {
fileInput.setAttribute("multiple", "multiple");
multiHint.style.display = "";
}
}
// Initial state
document.addEventListener("DOMContentLoaded", function() { toggleUploadMode(); });
</script>';
print '</div>'; // fichehalfleft
print '</div>'; // fichecenter
print '<div class="clearboth"></div><br>';
// Year filter for list
// 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 '<form method="GET" action="'.$_SERVER["PHP_SELF"].'">';
print '<div class="center" style="margin-bottom: 15px;">';
print '<strong>'.$langs->trans("Year").':</strong> ';
print $form->selectarray('year', $years, $year, 0, 0, 0, '', 0, 0, 0, '', 'minwidth100');
print $form->selectarray('year', $yearsFilter, $year, 0, 0, 0, '', 0, 0, 0, '', 'minwidth100');
print ' <input type="submit" class="button smallpaddingimp" value="'.$langs->trans("Filter").'">';
print '</div>';
print '</form>';
@ -358,8 +576,11 @@ print '<th class="center">'.$langs->trans("DateCreation").'</th>';
print '<th class="center" width="150">'.$langs->trans("Actions").'</th>';
print '</tr>';
$filter = array('year' => $year);
$records = $statement->fetchAll('statement_number', 'ASC', 100, 0, $filter);
$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) {
@ -472,8 +693,12 @@ $totalCount = $statement->fetchAll('', '', 0, 0, array(), 'count');
$yearCount = is_array($records) ? count($records) : 0;
print '<div class="opacitymedium" style="margin-top: 10px;">';
print $langs->trans("Total").': <strong>'.$yearCount.'</strong> '.$langs->trans("StatementsInYear", $year);
print ' | '.$langs->trans("AllStatements").': <strong>'.$totalCount.'</strong>';
if ($year > 0) {
print $langs->trans("Total").': <strong>'.$yearCount.'</strong> '.$langs->trans("StatementsInYear", $year);
print ' | '.$langs->trans("AllStatements").': <strong>'.$totalCount.'</strong>';
} else {
print $langs->trans("Total").': <strong>'.$totalCount.'</strong> '.$langs->trans("AllStatements");
}
print '</div>';
llxFooter();

View file

@ -1,3 +1,7 @@
--
-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version.
--
-- v1.1: Add fk_statement to transaction table
ALTER TABLE llx_bankimport_transaction ADD COLUMN fk_statement INTEGER AFTER fk_societe;
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_statement (fk_statement);

0
sql/llx_bankimport_statement.key.sql Normal file → Executable file
View file

0
sql/llx_bankimport_statement.sql Normal file → Executable file
View file

1
sql/llx_bankimport_transaction.key.sql Normal file → Executable file
View file

@ -19,6 +19,7 @@ ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_f
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_facture_fourn (fk_facture_fourn);
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_societe (fk_societe);
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_import_key (import_key);
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_statement (fk_statement);
-- Foreign keys (optional, depends on your setup)
-- ALTER TABLE llx_bankimport_transaction ADD CONSTRAINT fk_bankimport_transaction_fk_bank FOREIGN KEY (fk_bank) REFERENCES llx_bank(rowid);

1
sql/llx_bankimport_transaction.sql Normal file → Executable file
View file

@ -45,6 +45,7 @@ CREATE TABLE llx_bankimport_transaction (
fk_don INTEGER, -- Link to llx_don (donation)
fk_loan INTEGER, -- Link to llx_loan (loan payment)
fk_societe INTEGER, -- Link to llx_societe (third party)
fk_statement INTEGER, -- Link to llx_bankimport_statement (PDF statement)
-- Status
status SMALLINT DEFAULT 0, -- 0=new, 1=matched, 2=reconciled, 9=ignored

5
statements.php Normal file → Executable file
View file

@ -61,6 +61,11 @@ $langs->loadLangs(array("bankimport@bankimport", "banks"));
$action = GETPOST('action', 'aZ09');
// Security check
if (!$user->hasRight('bankimport', 'write')) {
accessforbidden();
}
// Date filters
$date_fromday = GETPOSTINT('date_fromday');
$date_frommonth = GETPOSTINT('date_frommonth');

0
vendor/autoload.php vendored Normal file → Executable file
View file

0
vendor/composer/ClassLoader.php vendored Normal file → Executable file
View file

0
vendor/composer/InstalledVersions.php vendored Normal file → Executable file
View file

0
vendor/composer/LICENSE vendored Normal file → Executable file
View file

0
vendor/composer/autoload_classmap.php vendored Normal file → Executable file
View file

0
vendor/composer/autoload_namespaces.php vendored Normal file → Executable file
View file

0
vendor/composer/autoload_psr4.php vendored Normal file → Executable file
View file

0
vendor/composer/autoload_real.php vendored Normal file → Executable file
View file

0
vendor/composer/autoload_static.php vendored Normal file → Executable file
View file

0
vendor/composer/installed.json vendored Normal file → Executable file
View file

0
vendor/composer/installed.php vendored Normal file → Executable file
View file

0
vendor/composer/platform_check.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/.github/workflows/tests.yml vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/.gitignore vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/.php-cs-fixer.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/.travis.yml vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/DEVELOPER-GUIDE.md vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/LICENSE vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/README.md vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/accounts.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/balance.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/bpd.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/browser.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/directDebit_Sephpa.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/directDebit_phpSepaXml.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/init.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/login.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/statementOfAccount.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/statementOfHoldings.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/tanModesAndMedia.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/transfer.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/composer.json vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Action/GetBalance.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Action/GetDepotAufstellung.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Action/GetSEPAAccounts.php vendored Normal file → Executable file
View file

View file

0
vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementOfAccount.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementOfAccountXML.php vendored Normal file → Executable file
View file

View file

0
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPADirectDebit.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPARealtimeTransfer.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPATransfer.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/BaseAction.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Connection.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/CurlException.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/FinTs.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/MT535/MT535.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/PostbankMT940.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/SpardaMT940.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/MT940/MT940.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/MT940/MT940Exception.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/Account.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/DataElement.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/StartCode.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/SvgRenderer.php vendored Normal file → Executable file
View file

View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/NoPsd2TanMode.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/SEPAAccount.php vendored Normal file → Executable file
View file

View file

View file

View file

View file

View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/TanMedium.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/TanMode.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/TanRequest.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/TanRequestChallengeImage.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Options/Credentials.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Options/FinTsOptions.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Options/SanitizingLogger.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/PaginateableAction.php vendored Normal file → Executable file
View file

View file

0
vendor/nemiah/php-fints/lib/Fhp/Protocol/BPD.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Protocol/DialogInitialization.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Protocol/GetTanMedia.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Protocol/Message.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Protocol/MessageBuilder.php vendored Normal file → Executable file
View file

Some files were not shown because too many files have changed in this diff Show more