From fc380892f035d3a48038c3c0cedef76fd0fec404 Mon Sep 17 00:00:00 2001 From: data Date: Thu, 5 Mar 2026 14:26:35 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20PDF-Kontoausz=C3=BCge=20per=20FinTS=20(?= =?UTF-8?q?HKEKP)=20abrufen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue php-fints Segmente: HKEKPv2, HIEKPv2, HIEKPSv2 - Action-Klasse GetStatementPDF mit Pagination-Support - Integration in pdfstatements.php (2-Spalten-Layout) - Cronjob doAutoFetchPdf für automatischen Abruf - Bank-Support-Prüfung via BPD (HIEKPS Parameter) Hinweis: Nicht alle Banken unterstützen HKEKP Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 21 ++ CLAUDE.md | 40 +++- class/bankimportcron.class.php | 181 +++++++++++++++++ class/fints.class.php | 122 ++++++++++++ core/modules/modBankImport.class.php | 16 +- langs/de_DE/bankimport.lang | 25 +++ pdfstatements.php | 163 +++++++++++++++- vendor/composer/autoload_classmap.php | 3 + vendor/composer/autoload_static.php | 3 + .../lib/Fhp/Action/GetStatementPDF.php | 182 ++++++++++++++++++ .../php-fints/lib/Fhp/Segment/EKP/HIEKP.php | 18 ++ .../php-fints/lib/Fhp/Segment/EKP/HIEKPS.php | 11 ++ .../lib/Fhp/Segment/EKP/HIEKPSv2.php | 22 +++ .../php-fints/lib/Fhp/Segment/EKP/HIEKPv2.php | 95 +++++++++ .../php-fints/lib/Fhp/Segment/EKP/HKEKPv2.php | 75 ++++++++ .../Segment/EKP/ParameterKontoauszugPdf.php | 48 +++++ 16 files changed, 1018 insertions(+), 7 deletions(-) mode change 100644 => 100755 CLAUDE.md create mode 100644 vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementPDF.php create mode 100644 vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HIEKP.php create mode 100644 vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HIEKPS.php create mode 100644 vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HIEKPSv2.php create mode 100644 vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HIEKPv2.php create mode 100644 vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HKEKPv2.php create mode 100644 vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/ParameterKontoauszugPdf.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6a613..0dd45a0 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert. +## [3.5] - 2026-03-05 + +### Hinzugefügt +- **PDF-Kontoauszüge per FinTS (HKEKP)**: Elektronische Kontoauszüge direkt von der Bank abrufen + - Neue Segmente für php-fints: HKEKPv2, HIEKPv2, HIEKPSv2, ParameterKontoauszugPdf + - Neue Action-Klasse: GetStatementPDF für PDF-Abruf + - Integration in bestehende PDF-Kontoauszüge-Seite + - Support für Base64-kodierte PDFs (automatische Erkennung aus BPD) + - **Hinweis**: Nicht alle Banken unterstützen HKEKP - prüfbar via BPD-Parameter HIEKPS +- **Cronjob für automatischen PDF-Abruf**: Neue geplante Aufgabe `doAutoFetchPdf` + - Aktivierbar über Konstante `BANKIMPORT_PDF_AUTO_ENABLED` + - Ruft automatisch neue PDF-Kontoauszüge ab und speichert sie + +### Geändert +- PDF-Kontoauszüge-Seite: Neues Layout mit zwei Spalten (FinTS-Abruf links, Upload rechts) +- fints.class.php: Neue Methoden `getStatementPDF()` und `supportsPdfStatements()` + +### Technisch +- Erweiterung der php-fints Bibliothek um HKEKP-Unterstützung (Segment/EKP/*) +- Neue Action-Klasse mit Pagination-Support für große PDF-Auszüge + ## [3.1] - 2026-03-05 ### Hinzugefügt diff --git a/CLAUDE.md b/CLAUDE.md old mode 100644 new mode 100755 index bcdfd70..aa43855 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Modul-Übersicht - **Name**: BankImport -- **Version**: 3.1 +- **Version**: 3.5 - **Pfad**: `/srv/http/dolibarr/custom/bankimport/` - **Funktion**: FinTS/HBCI Kontoauszüge importieren und mit Dolibarr-Rechnungen abgleichen @@ -16,6 +16,44 @@ | `repair.php` | Admin-Seite für verwaiste Transaktionen | | `cron/bankimport.cron.php` | Cronjob für automatischen Import | | `admin/cronmonitor.php` | Cron-Monitoring und Pause/Resume | +| `pdfstatements.php` | PDF-Kontoauszüge hochladen und per FinTS abrufen | +| `vendor/.../Segment/EKP/*` | HKEKP-Segmente für PDF-Abruf | +| `vendor/.../Action/GetStatementPDF.php` | Action-Klasse für PDF-Abruf | + +## PDF-Kontoauszüge per FinTS (HKEKP) + +### Übersicht +Seit Version 3.5 können PDF-Kontoauszüge direkt von der Bank abgerufen werden (HKEKP = Elektronischer Kontoauszug PDF). + +### Neue Dateien in php-fints +``` +vendor/nemiah/php-fints/lib/Fhp/ +├── Action/GetStatementPDF.php # Haupt-Action-Klasse +└── Segment/EKP/ + ├── HKEKPv2.php # Request-Segment + ├── HIEKPv2.php # Response-Segment + ├── HIEKP.php # Response-Interface + ├── HIEKPSv2.php # Parameter-Segment + ├── HIEKPS.php # Parameter-Interface + └── ParameterKontoauszugPdf.php # Parameter-Model +``` + +### Verwendung +```php +$fints = new BankImportFinTS(); +if ($fints->supportsPdfStatements()) { + $result = $fints->getStatementPDF(0); // Account-Index, optional Nr+Jahr + if ($result['success']) { + $pdfData = $result['data']['pdf']; + $info = $result['data']['info']; // statementNumber, statementYear, etc. + } +} +``` + +### Cronjob +- Aktivieren: `BANKIMPORT_PDF_AUTO_ENABLED = 1` +- Klasse: `BankImportCron::doAutoFetchPdf()` +- Frequenz: Wie Transaktions-Import konfigurierbar ## Multi-Invoice Matching (Sammelzahlungen) diff --git a/class/bankimportcron.class.php b/class/bankimportcron.class.php index 9c1dd98..fcecc35 100755 --- a/class/bankimportcron.class.php +++ b/class/bankimportcron.class.php @@ -16,6 +16,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; dol_include_once('/bankimport/class/fints.class.php'); dol_include_once('/bankimport/class/banktransaction.class.php'); +dol_include_once('/bankimport/class/bankstatement.class.php'); /** * Class BankImportCron @@ -732,4 +733,184 @@ class BankImportCron return $this->doAutoImport(); } + + /** + * Execute automatic PDF statement fetch via HKEKP + * Called by Dolibarr's scheduled task system + * + * @return int 0 if OK, < 0 if error + */ + public function doAutoFetchPdf() + { + global $conf, $langs, $user; + + // Initialize timing + $this->startTime = microtime(true); + + $langs->load('bankimport@bankimport'); + + $this->cronLog("========== PDF FETCH CRON START =========="); + $this->recordCronStatus('started', 'PDF fetch cron started'); + + // Check if PDF fetch is enabled + if (!getDolGlobalInt('BANKIMPORT_PDF_AUTO_ENABLED')) { + $this->output = $langs->trans('AutoPdfFetchDisabled'); + $this->cronLog("Auto PDF fetch is disabled - exiting"); + $this->recordCronStatus('completed', 'Auto PDF fetch disabled'); + return 0; + } + + // Check cron pause status (shared with main cron) + $pausedUntil = getDolGlobalInt('BANKIMPORT_CRON_PAUSED_UNTIL'); + if ($pausedUntil > 0 && $pausedUntil > time()) { + $pauseReason = getDolGlobalString('BANKIMPORT_CRON_PAUSE_REASON'); + $this->output = "Cron pausiert: {$pauseReason}"; + $this->cronLog("Cron is PAUSED - skipping PDF fetch", 'WARNING'); + return 0; + } + + $this->cronLog("Initializing FinTS for PDF fetch"); + + // Initialize FinTS + $fints = new BankImportFinTS($this->db); + + if (!$fints->isConfigured()) { + $this->error = $langs->trans('AutoImportNotConfigured'); + $this->cronLog("FinTS not configured", 'WARNING'); + return -1; + } + + try { + // Login to bank + $this->cronLog("Logging in to bank"); + $loginResult = $fints->login(); + + if ($loginResult < 0) { + $this->error = $fints->error; + $this->cronLog("Login failed: ".$fints->error, 'ERROR'); + return -1; + } + + if ($loginResult == 0) { + // TAN required + $this->output = $langs->trans('TANRequired'); + $this->cronLog("TAN required for PDF fetch", 'WARNING'); + $this->recordCronStatus('completed', 'TAN required'); + return 0; + } + + // Check if bank supports PDF statements + if (!$fints->supportsPdfStatements()) { + $this->output = $langs->trans('ErrorBankDoesNotSupportPdfStatements'); + $this->cronLog("Bank does not support PDF statements (HKEKP)", 'WARNING'); + $fints->close(); + return 0; + } + + // Fetch PDF statement + $this->cronLog("Fetching PDF statement via HKEKP"); + $pdfResult = $fints->getStatementPDF(0); + + if ($pdfResult === 0) { + // TAN required + $this->output = $langs->trans('TANRequired'); + $this->cronLog("TAN required for PDF statement", 'WARNING'); + $fints->close(); + return 0; + } + + if ($pdfResult === -1) { + $this->error = $fints->error; + $this->cronLog("PDF fetch failed: ".$fints->error, 'ERROR'); + $fints->close(); + return -1; + } + + // Check if we got any data + if (empty($pdfResult['pdfData'])) { + $this->output = $langs->trans('NoPdfStatementsAvailable'); + $this->cronLog("No new PDF statements available"); + $fints->close(); + return 0; + } + + // Save the PDF + $info = $pdfResult['info']; + $pdfData = $pdfResult['pdfData']; + + $this->cronLog("Received PDF statement #".$info['statementNumber'].'/'.$info['statementYear']); + + // Check if statement already exists + $stmt = new BankImportStatement($this->db); + $stmt->statement_number = $info['statementNumber']; + $stmt->statement_year = $info['statementYear']; + $stmt->iban = $info['iban'] ?: getDolGlobalString('BANKIMPORT_IBAN'); + + if ($stmt->exists()) { + $this->output = $langs->trans("StatementAlreadyExists").': '.$stmt->statement_number.'/'.$stmt->statement_year; + $this->cronLog("Statement already exists - skipping"); + $fints->close(); + return 0; + } + + // Save PDF to file + $dir = BankImportStatement::getStorageDir(); + $ibanPart = preg_replace('/[^A-Z0-9]/', '', strtoupper($stmt->iban ?: 'KONTO')); + $filename = sprintf('Kontoauszug_%s_%d_%s.pdf', + $ibanPart, + $stmt->statement_year, + str_pad($stmt->statement_number, 3, '0', STR_PAD_LEFT) + ); + + $filepath = $dir.'/'.$filename; + if (file_put_contents($filepath, $pdfData) === false) { + $this->error = $langs->trans("ErrorSavingPdfFile"); + $this->cronLog("Failed to save PDF file", 'ERROR'); + $fints->close(); + return -1; + } + + // Create database record + $stmt->filename = $filename; + $stmt->filepath = $filepath; + $stmt->filesize = strlen($pdfData); + $stmt->statement_date = $info['creationDate'] ? $info['creationDate']->getTimestamp() : dol_now(); + $stmt->import_key = 'fints_cron_'.date('YmdHis'); + + // Get system user + $importUser = new User($this->db); + $importUser->fetch(1); + + $result = $stmt->create($importUser); + + if ($result > 0) { + $this->output = $langs->trans("PdfStatementFetched", $stmt->statement_number.'/'.$stmt->statement_year); + $this->cronLog("PDF statement saved successfully: ".$stmt->statement_number.'/'.$stmt->statement_year); + + // Update last fetch timestamp + dolibarr_set_const($this->db, 'BANKIMPORT_PDF_LAST_FETCH', time(), 'chaine', 0, '', $conf->entity); + } else { + $this->error = $stmt->error; + $this->cronLog("Failed to create database record: ".$stmt->error, 'ERROR'); + @unlink($filepath); + $fints->close(); + return -1; + } + + $fints->close(); + + $duration = round(microtime(true) - $this->startTime, 2); + $this->cronLog("PDF fetch completed successfully in {$duration}s"); + $this->recordCronStatus('completed', "PDF fetched: {$stmt->statement_number}/{$stmt->statement_year}"); + $this->cronLog("========== PDF FETCH CRON END =========="); + + return 0; + + } catch (Exception $e) { + $this->error = 'Exception: '.$e->getMessage(); + $this->cronLog("EXCEPTION: ".$e->getMessage(), 'ERROR'); + $this->recordCronStatus('error', 'Exception: '.$e->getMessage()); + return -1; + } + } } diff --git a/class/fints.class.php b/class/fints.class.php index 95911e4..6d3822b 100755 --- a/class/fints.class.php +++ b/class/fints.class.php @@ -38,6 +38,7 @@ use Fhp\Options\Credentials; use Fhp\Action\GetSEPAAccounts; use Fhp\Action\GetStatementOfAccount; use Fhp\Action\GetStatementOfAccountXML; +use Fhp\Action\GetStatementPDF; use Fhp\Model\StatementOfAccount\Statement; use Fhp\Model\StatementOfAccount\Transaction; @@ -1017,4 +1018,125 @@ class BankImportFinTS { return $this->iban; } + + /** + * Get PDF bank statement via HKEKP + * + * @param int $accountIndex Index of account to use (default 0) + * @param int|null $statementNumber Optional: specific statement number + * @param int|null $statementYear Optional: statement year + * @return array|int Array with 'pdfData' and 'info', or 0 if TAN required, or -1 on error + */ + public function getStatementPDF($accountIndex = 0, $statementNumber = null, $statementYear = null) + { + global $conf; + + $this->error = ''; + + if (!$this->fints) { + $this->error = 'Not connected'; + return -1; + } + + try { + // Get accounts if not cached + if (empty($this->accounts)) { + $getAccounts = GetSEPAAccounts::create(); + $this->fints->execute($getAccounts); + + if ($getAccounts->needsTan()) { + $this->pendingAction = $getAccounts; + $tanRequest = $getAccounts->getTanRequest(); + $this->tanChallenge = $tanRequest->getChallenge(); + return 0; + } + + $this->accounts = $getAccounts->getAccounts(); + } + + if (empty($this->accounts) || !isset($this->accounts[$accountIndex])) { + $this->error = 'No accounts available or invalid account index'; + return -1; + } + + $selectedAccount = $this->accounts[$accountIndex]; + + dol_syslog("BankImport: Fetching PDF statement via HKEKP", LOG_DEBUG); + + $getPdf = GetStatementPDF::create( + $selectedAccount, + $statementNumber, + $statementYear + ); + + $this->fints->execute($getPdf); + + if ($getPdf->needsTan()) { + $this->pendingAction = $getPdf; + $tanRequest = $getPdf->getTanRequest(); + $this->tanChallenge = $tanRequest->getChallenge(); + return 0; + } + + $pdfData = $getPdf->getPdfData(); + $info = $getPdf->getStatementInfo(); + + if (empty($pdfData)) { + dol_syslog("BankImport: No PDF data received (no new statements available)", LOG_DEBUG); + return array( + 'pdfData' => '', + 'info' => array(), + 'message' => 'No new statements available' + ); + } + + dol_syslog("BankImport: Received PDF statement #".$info['statementNumber'].'/'.$info['statementYear'], LOG_DEBUG); + + return array( + 'pdfData' => $pdfData, + 'info' => $info + ); + + } catch (Exception $e) { + $this->error = $e->getMessage(); + dol_syslog("BankImport: HKEKP failed: ".$this->error, LOG_ERR); + + // Check if bank doesn't support HKEKP + if (stripos($this->error, 'HKEKP') !== false || stripos($this->error, 'not support') !== false) { + $this->error = 'Bank does not support PDF statements (HKEKP)'; + } + + return -1; + } + } + + /** + * Check if bank supports PDF statements (HKEKP) + * + * @return bool + */ + public function supportsPdfStatements() + { + if (!$this->fints) { + return false; + } + + try { + // Try to get BPD and check for HIEKPS + $reflection = new ReflectionClass($this->fints); + $bpdProperty = $reflection->getProperty('bpd'); + $bpdProperty->setAccessible(true); + $bpd = $bpdProperty->getValue($this->fints); + + if ($bpd === null) { + return false; + } + + $hiekps = $bpd->getLatestSupportedParameters('HIEKPS'); + return $hiekps !== null; + + } catch (Exception $e) { + return false; + } + } } diff --git a/core/modules/modBankImport.class.php b/core/modules/modBankImport.class.php index 8c4aa20..981fd06 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 = '3.4'; + $this->version = '3.5'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; @@ -281,6 +281,20 @@ class modBankImport extends DolibarrModules 'test' => 'isModEnabled("bankimport") && getDolGlobalInt("BANKIMPORT_AUTO_ENABLED")', 'priority' => 50, ), + 1 => array( + 'label' => 'BankImportPdfFetch', + 'jobtype' => 'method', + 'class' => '/bankimport/class/bankimportcron.class.php', + 'objectname' => 'BankImportCron', + 'method' => 'doAutoFetchPdf', + 'parameters' => '', + 'comment' => 'Automatic PDF statement fetch via HKEKP', + 'frequency' => 1, + 'unitfrequency' => 86400, // Daily + 'status' => 0, // Disabled by default + 'test' => 'isModEnabled("bankimport") && getDolGlobalInt("BANKIMPORT_PDF_AUTO_ENABLED")', + 'priority' => 51, + ), ); /* END MODULEBUILDER CRON */ // Example: $this->cronjobs=array( diff --git a/langs/de_DE/bankimport.lang b/langs/de_DE/bankimport.lang index abe2fbf..7e1d68d 100755 --- a/langs/de_DE/bankimport.lang +++ b/langs/de_DE/bankimport.lang @@ -375,3 +375,28 @@ Open = Öffnen # Cash Discount / Skonto # CashDiscount = Skonto + +# +# PDF Statement Fetch (HKEKP) +# +FetchFromBank = Von Bank abrufen +FetchNewStatements = Neue Auszüge abrufen +FetchNewStatementsDesc = Ruft alle noch nicht abgerufenen PDF-Kontoauszüge von der Bank ab +FetchSpecificStatement = Bestimmten Auszug abrufen +Fetch = Abrufen +ErrorFinTSNotConfigured = FinTS-Verbindung nicht konfiguriert +ErrorFinTSConnection = FinTS-Verbindungsfehler +ErrorBankDoesNotSupportPdfStatements = Bank unterstützt keine PDF-Kontoauszüge (HKEKP) +ErrorFetchingPdfStatement = Fehler beim Abrufen des PDF-Kontoauszugs +NoPdfStatementsAvailable = Keine neuen PDF-Kontoauszüge verfügbar +PdfStatementFetched = PDF-Kontoauszug %s erfolgreich abgerufen +ErrorSavingPdfFile = Fehler beim Speichern der PDF-Datei +FinTSNotConfiguredForPdf = FinTS nicht konfiguriert - PDF-Abruf nicht möglich +AutoPdfFetchDisabled = Automatischer PDF-Abruf ist deaktiviert +BankImportPdfFetch = PDF-Kontoauszüge abrufen +PdfFetchCronDescription = Automatischer Abruf von PDF-Kontoauszügen via FinTS (HKEKP) +PdfStatementsFetched = %s PDF-Kontoauszüge abgerufen +ManualUpload = Manuell hochladen +Source = Quelle +SourceUpload = Hochgeladen +SourceFinTS = FinTS-Abruf diff --git a/pdfstatements.php b/pdfstatements.php index e20933a..4a5084c 100755 --- a/pdfstatements.php +++ b/pdfstatements.php @@ -46,6 +46,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php'; dol_include_once('/bankimport/class/bankstatement.class.php'); +dol_include_once('/bankimport/class/fints.class.php'); dol_include_once('/bankimport/lib/bankimport.lib.php'); /** @@ -72,6 +73,102 @@ if (!$user->hasRight('bankimport', 'read')) { $statement = new BankImportStatement($db); +// Fetch PDF statement from bank via HKEKP +if ($action == 'fetchpdf' || $action == 'fetchpdf_single') { + if (!$user->hasRight('bankimport', 'write')) { + accessforbidden(); + } + + $fetchNumber = ($action == 'fetchpdf_single') ? GETPOSTINT('fetch_number') : null; + $fetchYear = ($action == 'fetchpdf_single') ? GETPOSTINT('fetch_year') : null; + + // Check FinTS configuration + $fintsUrl = getDolGlobalString('BANKIMPORT_FINTS_URL'); + $fintsBlz = getDolGlobalString('BANKIMPORT_FINTS_BLZ'); + $fintsUser = getDolGlobalString('BANKIMPORT_FINTS_USERNAME'); + $fintsPin = getDolGlobalString('BANKIMPORT_FINTS_PIN'); + + if (empty($fintsUrl) || empty($fintsBlz) || empty($fintsUser) || empty($fintsPin)) { + setEventMessages($langs->trans("ErrorFinTSNotConfigured"), null, 'errors'); + } else { + $fints = new BankImportFinTS($db); + $loginResult = $fints->login(); + + if ($loginResult < 0) { + setEventMessages($langs->trans("ErrorFinTSConnection").': '.$fints->error, null, 'errors'); + } elseif ($loginResult === 0) { + // TAN required for login + setEventMessages($langs->trans("TANRequired").($fints->tanChallenge ? ': '.$fints->tanChallenge : ''), null, 'warnings'); + $fints->close(); + } else { + // Check if bank supports HKEKP + if (!$fints->supportsPdfStatements()) { + setEventMessages($langs->trans("ErrorBankDoesNotSupportPdfStatements"), null, 'errors'); + $fints->close(); + } else { + // Fetch PDF + $pdfResult = $fints->getStatementPDF(0, $fetchNumber, $fetchYear); + + if ($pdfResult === 0) { + // TAN required - save to session and show TAN form + $_SESSION['bankimport_pending_action'] = serialize($fints); + setEventMessages($langs->trans("TANRequired").': '.$fints->tanChallenge, null, 'warnings'); + } elseif ($pdfResult === -1) { + setEventMessages($langs->trans("ErrorFetchingPdfStatement").': '.$fints->error, null, 'errors'); + } elseif (empty($pdfResult['pdfData'])) { + setEventMessages($langs->trans("NoPdfStatementsAvailable"), null, 'warnings'); + } else { + // Save PDF + $info = $pdfResult['info']; + $pdfData = $pdfResult['pdfData']; + + // Check if statement already exists + $stmt = new BankImportStatement($db); + $stmt->statement_number = $info['statementNumber']; + $stmt->statement_year = $info['statementYear']; + $stmt->iban = $info['iban'] ?: getDolGlobalString('BANKIMPORT_IBAN'); + + if ($stmt->exists()) { + setEventMessages($langs->trans("StatementAlreadyExists").': '.$stmt->statement_number.'/'.$stmt->statement_year, null, 'warnings'); + } else { + // Save PDF to file + $dir = BankImportStatement::getStorageDir(); + $ibanPart = preg_replace('/[^A-Z0-9]/', '', strtoupper($stmt->iban ?: 'KONTO')); + $filename = sprintf('Kontoauszug_%s_%d_%s.pdf', + $ibanPart, + $stmt->statement_year, + str_pad($stmt->statement_number, 3, '0', STR_PAD_LEFT) + ); + + $filepath = $dir.'/'.$filename; + if (file_put_contents($filepath, $pdfData) !== false) { + $stmt->filename = $filename; + $stmt->filepath = $filepath; + $stmt->filesize = strlen($pdfData); + $stmt->statement_date = $info['creationDate'] ? $info['creationDate']->getTimestamp() : dol_now(); + $stmt->import_key = 'fints_'.date('YmdHis'); + + $result = $stmt->create($user); + if ($result > 0) { + setEventMessages($langs->trans("PdfStatementFetched", $stmt->statement_number.'/'.$stmt->statement_year), null, 'mesgs'); + $year = $stmt->statement_year; + } else { + setEventMessages($stmt->error, null, 'errors'); + @unlink($filepath); + } + } else { + setEventMessages($langs->trans("ErrorSavingPdfFile"), null, 'errors'); + } + } + } + } + + $fints->close(); + } + } + $action = ''; +} + // Upload PDF (supports multiple files) if ($action == 'upload' && !empty($_FILES['pdffile'])) { $uploadMode = GETPOST('upload_mode', 'alpha'); @@ -540,20 +637,76 @@ if ($action == 'delete') { print $formconfirm; } +// Check if FinTS is configured for PDF fetch +$fintsConfigured = !empty(getDolGlobalString('BANKIMPORT_FINTS_URL')) + && !empty(getDolGlobalString('BANKIMPORT_FINTS_BLZ')) + && !empty(getDolGlobalString('BANKIMPORT_FINTS_USERNAME')) + && !empty(getDolGlobalString('BANKIMPORT_FINTS_PIN')); + +print '
'; + +// Left side: Fetch from bank (if FinTS configured) +print '
'; + +if ($fintsConfigured) { + print ''; + print ''; + print ''; + print ''; + + // Fetch all new statements + print ''; + print ''; + print ''; + + // Fetch specific statement + print ''; + print ''; + print ''; + + print '
'.img_picto('', 'download', 'class="pictofixedwidth"').$langs->trans("FetchFromBank").'
'; + print ''; + print img_picto('', 'refresh', 'class="pictofixedwidth"').$langs->trans("FetchNewStatements"); + print ''; + print '
'.$langs->trans("FetchNewStatementsDesc").''; + print '
'; + print '
'; + print ''; + print ''; + print $langs->trans("FetchSpecificStatement").': '; + print ''; + print ' / '; + $years = array(); + for ($y = (int) date('Y'); $y >= ((int) date('Y') - 5); $y--) { + $years[$y] = $y; + } + print $form->selectarray('fetch_year', $years, (int) date('Y'), 0, 0, 0, '', 0, 0, 0, '', 'minwidth75'); + print ' '; + print '
'; + print '
'; +} else { + print '
'; + print img_warning().' '.$langs->trans("FinTSNotConfiguredForPdf"); + print ' '.$langs->trans("GoToSetup").''; + print '
'; +} + +print '
'; // fichehalfleft + +// Right side: Manual upload +print '
'; + // Upload form $defaultMode = getDolGlobalString('BANKIMPORT_UPLOAD_MODE') ?: 'auto'; $uploadMode = GETPOST('upload_mode', 'alpha') ?: $defaultMode; -print '
'; -print '
'; - print '
'; print ''; print ''; print ''; print ''; -print ''; +print ''; print ''; // Upload mode selection @@ -678,7 +831,7 @@ function toggleUploadMode() { document.addEventListener("DOMContentLoaded", function() { toggleUploadMode(); }); '; -print ''; // fichehalfleft +print ''; // fichehalfright (upload form) print ''; // fichecenter print '

'; diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index a46b42a..c3d39e6 100755 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -6,6 +6,9 @@ $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( + 'BankImportCron' => $baseDir . '/class/bankimportcron.class.php', 'BankImportFinTS' => $baseDir . '/class/fints.class.php', + 'BankImportStatement' => $baseDir . '/class/bankstatement.class.php', + 'BankImportTransaction' => $baseDir . '/class/banktransaction.class.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 5ec4db2..80787f4 100755 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -24,7 +24,10 @@ class ComposerStaticInitcfc07b7e6c4a3dcfdcd6e754983b1a9b ); public static $classMap = array ( + 'BankImportCron' => __DIR__ . '/../..' . '/class/bankimportcron.class.php', 'BankImportFinTS' => __DIR__ . '/../..' . '/class/fints.class.php', + 'BankImportStatement' => __DIR__ . '/../..' . '/class/bankstatement.class.php', + 'BankImportTransaction' => __DIR__ . '/../..' . '/class/banktransaction.class.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', ); diff --git a/vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementPDF.php b/vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementPDF.php new file mode 100644 index 0000000..81427a2 --- /dev/null +++ b/vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementPDF.php @@ -0,0 +1,182 @@ +account = $account; + $result->statementNumber = $statementNumber; + $result->statementYear = $statementYear; + return $result; + } + + public function __serialize(): array + { + return [ + parent::__serialize(), + $this->account, + $this->statementNumber, + $this->statementYear, + $this->isBase64, + ]; + } + + public function __unserialize(array $serialized): void + { + list( + $parentSerialized, + $this->account, + $this->statementNumber, + $this->statementYear, + $this->isBase64 + ) = $serialized; + + is_array($parentSerialized) + ? parent::__unserialize($parentSerialized) + : parent::unserialize($parentSerialized); + } + + /** + * @return string The raw PDF data + */ + public function getPdfData(): string + { + $this->ensureDone(); + + // Decode base64 if needed + if ($this->isBase64 && !empty($this->pdfData)) { + $decoded = base64_decode($this->pdfData, true); + if ($decoded !== false) { + return $decoded; + } + } + + return $this->pdfData; + } + + /** + * @return array Statement metadata (number, year, iban, date, filename) + */ + public function getStatementInfo(): array + { + $this->ensureDone(); + return $this->statementInfo; + } + + /** + * @return bool Whether receipt confirmation is needed + */ + public function needsReceipt(): bool + { + $this->ensureDone(); + return !empty($this->statementInfo['receiptCode']); + } + + /** {@inheritdoc} */ + protected function createRequest(BPD $bpd, ?UPD $upd) + { + /** @var HIEKPSv2|null $hiekps */ + $hiekps = $bpd->getLatestSupportedParameters('HIEKPS'); + + if ($hiekps === null) { + throw new UnsupportedException('The bank does not support PDF statements (HKEKP).'); + } + + $param = $hiekps->getParameter(); + $this->isBase64 = $param->isBase64Encoded(); + + // Check if historical statements are allowed + if ($this->statementNumber !== null && !$param->isHistoricalStatementsAllowed()) { + throw new UnsupportedException('The bank does not allow requesting historical statements by number.'); + } + + return HKEKPv2::create( + Kti::fromAccount($this->account), + $this->statementNumber, + $this->statementYear + ); + } + + /** {@inheritdoc} */ + public function processResponse(Message $response) + { + parent::processResponse($response); + + // Check if no statements available + if ($response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null) { + return; + } + + /** @var HIEKP[] $responseSegments */ + $responseSegments = $response->findSegments(HIEKP::class); + + if (empty($responseSegments)) { + // No segments but also no error = empty response + return; + } + + foreach ($responseSegments as $hiekp) { + // Append PDF data (for pagination) + $this->pdfData .= $hiekp->getPdfData(); + + // Store metadata from first segment + if (empty($this->statementInfo)) { + $this->statementInfo = [ + 'statementNumber' => $hiekp->getStatementNumber(), + 'statementYear' => $hiekp->getStatementYear(), + 'iban' => $hiekp->getIban(), + 'creationDate' => $hiekp->getCreationDate(), + 'filename' => $hiekp->getFilename(), + 'receiptCode' => $hiekp->needsReceipt() ? $hiekp->getReceiptCode() : null, + ]; + } + } + } +} diff --git a/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HIEKP.php b/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HIEKP.php new file mode 100644 index 0000000..81a70e4 --- /dev/null +++ b/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HIEKP.php @@ -0,0 +1,18 @@ +parameter; + } +} diff --git a/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HIEKPv2.php b/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HIEKPv2.php new file mode 100644 index 0000000..8d76d5d --- /dev/null +++ b/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HIEKPv2.php @@ -0,0 +1,95 @@ +gebuchteUmsaetze->getData(); + } + + public function getStatementNumber(): ?int + { + return $this->kontoauszugsnummer; + } + + public function getStatementYear(): ?int + { + return $this->kontoauszugsjahr; + } + + public function getIban(): ?string + { + return $this->ibanKonto; + } + + public function getFilename(): ?string + { + return $this->dateiname; + } + + public function getCreationDate(): ?\DateTime + { + if ($this->erstellungsdatumKontoauszug === null) { + return null; + } + return \DateTime::createFromFormat('Ymd', $this->erstellungsdatumKontoauszug) ?: null; + } + + public function needsReceipt(): bool + { + return $this->quittungscode !== null; + } + + public function getReceiptCode(): ?string + { + return $this->quittungscode?->getData(); + } +} diff --git a/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HKEKPv2.php b/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HKEKPv2.php new file mode 100644 index 0000000..9db96e6 --- /dev/null +++ b/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/HKEKPv2.php @@ -0,0 +1,75 @@ +kontoverbindungInternational = $kti; + $result->kontoauszugsnummer = $kontoauszugsnummer; + $result->kontoauszugsjahr = $kontoauszugsjahr; + $result->aufsetzpunkt = $aufsetzpunkt; + return $result; + } + + public function setPaginationToken(string $paginationToken): void + { + $this->aufsetzpunkt = $paginationToken; + } +} diff --git a/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/ParameterKontoauszugPdf.php b/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/ParameterKontoauszugPdf.php new file mode 100644 index 0000000..8cdeae0 --- /dev/null +++ b/vendor/nemiah/php-fints/lib/Fhp/Segment/EKP/ParameterKontoauszugPdf.php @@ -0,0 +1,48 @@ +kontoauszugsnummerErlaubt; + } + + public function needsReceipt(): bool + { + return $this->quittierungBenoetigt; + } + + public function isBase64Encoded(): bool + { + return $this->base64Kodiert; + } +}
'.$langs->trans("UploadPDFStatement").''.img_picto('', 'upload', 'class="pictofixedwidth"').$langs->trans("UploadPDFStatement").'