feat: PDF-Kontoauszüge per FinTS (HKEKP) abrufen

- 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 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-03-05 14:26:35 +01:00
parent e3687f0d34
commit fc380892f0
16 changed files with 1018 additions and 7 deletions

View file

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

40
CLAUDE.md Normal file → Executable file
View file

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

View file

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

View file

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

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 = '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(

View file

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

View file

@ -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 '<div class="fichecenter">';
// Left side: Fetch from bank (if FinTS configured)
print '<div class="fichehalfleft">';
if ($fintsConfigured) {
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<td colspan="2">'.img_picto('', 'download', 'class="pictofixedwidth"').$langs->trans("FetchFromBank").'</td>';
print '</tr>';
// Fetch all new statements
print '<tr class="oddeven">';
print '<td colspan="2">';
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?action=fetchpdf&token='.newToken().'">';
print img_picto('', 'refresh', 'class="pictofixedwidth"').$langs->trans("FetchNewStatements");
print '</a>';
print '<br><span class="opacitymedium small">'.$langs->trans("FetchNewStatementsDesc").'</span>';
print '</td>';
print '</tr>';
// Fetch specific statement
print '<tr class="oddeven">';
print '<td colspan="2">';
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline;">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="fetchpdf_single">';
print $langs->trans("FetchSpecificStatement").': ';
print '<input type="text" name="fetch_number" class="flat width50" placeholder="Nr." maxlength="5">';
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 ' <input type="submit" class="button smallpaddingimp" value="'.$langs->trans("Fetch").'">';
print '</form>';
print '</td>';
print '</tr>';
print '</table>';
} else {
print '<div class="opacitymedium">';
print img_warning().' '.$langs->trans("FinTSNotConfiguredForPdf");
print ' <a href="'.dol_buildpath('/bankimport/admin/setup.php', 1).'">'.$langs->trans("GoToSetup").'</a>';
print '</div>';
}
print '</div>'; // fichehalfleft
// Right side: Manual upload
print '<div class="fichehalfright">';
// 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" id="uploadform">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="upload">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<td colspan="2">'.$langs->trans("UploadPDFStatement").'</td>';
print '<td colspan="2">'.img_picto('', 'upload', 'class="pictofixedwidth"').$langs->trans("UploadPDFStatement").'</td>';
print '</tr>';
// Upload mode selection
@ -678,7 +831,7 @@ function toggleUploadMode() {
document.addEventListener("DOMContentLoaded", function() { toggleUploadMode(); });
</script>';
print '</div>'; // fichehalfleft
print '</div>'; // fichehalfright (upload form)
print '</div>'; // fichecenter
print '<div class="clearboth"></div><br>';

View file

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

View file

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

View file

@ -0,0 +1,182 @@
<?php
namespace Fhp\Action;
use Fhp\Model\SEPAAccount;
use Fhp\PaginateableAction;
use Fhp\Protocol\BPD;
use Fhp\Protocol\Message;
use Fhp\Protocol\UnexpectedResponseException;
use Fhp\Protocol\UPD;
use Fhp\Segment\Common\Kti;
use Fhp\Segment\EKP\HIEKP;
use Fhp\Segment\EKP\HIEKPSv2;
use Fhp\Segment\EKP\HKEKPv2;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
use Fhp\UnsupportedException;
/**
* Retrieves PDF bank statements (Elektronischer Kontoauszug) via HKEKP.
*/
class GetStatementPDF extends PaginateableAction
{
/** @var SEPAAccount */
private $account;
/** @var int|null Optional: specific statement number */
private $statementNumber;
/** @var int|null Optional: statement year */
private $statementYear;
/** @var bool Whether PDF is base64 encoded (from BPD) */
private $isBase64 = false;
// Response data
/** @var string Raw PDF data (may be from multiple pages) */
private $pdfData = '';
/** @var array Statement metadata from response */
private $statementInfo = [];
/**
* @param SEPAAccount $account The account to get statements for
* @param int|null $statementNumber Optional: Request specific statement by number
* @param int|null $statementYear Optional: Year of the statement (required if number given)
* @return GetStatementPDF
*/
public static function create(
SEPAAccount $account,
?int $statementNumber = null,
?int $statementYear = null
): GetStatementPDF {
$result = new GetStatementPDF();
$result->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,
];
}
}
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Fhp\Segment\EKP;
/**
* Interface für HIEKP Response-Segmente (alle Versionen)
*/
interface HIEKP
{
public function getPdfData(): string;
public function getStatementNumber(): ?int;
public function getStatementYear(): ?int;
public function getIban(): ?string;
public function getFilename(): ?string;
public function getCreationDate(): ?\DateTime;
public function needsReceipt(): bool;
public function getReceiptCode(): ?string;
}

View file

@ -0,0 +1,11 @@
<?php
namespace Fhp\Segment\EKP;
/**
* Interface für HIEKPS Parameter-Segmente (alle Versionen)
*/
interface HIEKPS
{
public function getParameter(): ParameterKontoauszugPdf;
}

View file

@ -0,0 +1,22 @@
<?php
/** @noinspection PhpUnused */
namespace Fhp\Segment\EKP;
use Fhp\Segment\BaseGeschaeftsvorfallparameter;
/**
* Segment: Elektronischer Kontoauszug PDF Parameter (Version 2)
*
* Bankparameterdaten für den Geschäftsvorfall HKEKP.
*/
class HIEKPSv2 extends BaseGeschaeftsvorfallparameter implements HIEKPS
{
/** Parameter für Kontoauszug PDF */
public ParameterKontoauszugPdf $parameter;
public function getParameter(): ParameterKontoauszugPdf
{
return $this->parameter;
}
}

View file

@ -0,0 +1,95 @@
<?php
/** @noinspection PhpUnused */
namespace Fhp\Segment\EKP;
use Fhp\Segment\BaseSegment;
use Fhp\Segment\Common\Btg;
use Fhp\Syntax\Bin;
/**
* Segment: Elektronischer Kontoauszug PDF Rückmeldung (Version 2)
*
* Enthält die PDF-Daten des Kontoauszugs sowie Metadaten.
*/
class HIEKPv2 extends BaseSegment implements HIEKP
{
/** Gebuchte Umsätze als PDF (binär oder base64-kodiert je nach BPD) */
public Bin $gebuchteUmsaetze;
/** Berichtszeitraum */
public \Fhp\Segment\Common\Tsp $berichtszeitraum;
/** Erstellungsdatum des Kontoauszugs (JJJJMMTT) */
public ?string $erstellungsdatumKontoauszug = null;
/** Jahr des Kontoauszugs */
public ?int $kontoauszugsjahr = null;
/** Nummer des Kontoauszugs */
public ?int $kontoauszugsnummer = null;
/** IBAN des Kontos */
public ?string $ibanKonto = null;
/** BIC des Kontos */
public ?string $bicKonto = null;
/** Auszugsname Zeile 1 (max 35 Zeichen) */
public ?string $auszugsname1 = null;
/** Auszugsname Zeile 2 (max 35 Zeichen) */
public ?string $auszugsname2 = null;
/** Namenszusatz (max 35 Zeichen) */
public ?string $namenszusatz = null;
/** Dateiname des PDF (max 256 Zeichen) */
public ?string $dateiname = null;
/** Quittungscode (binär) - für Quittierungspflicht */
public ?Bin $quittungscode = null;
public function getPdfData(): string
{
return $this->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();
}
}

View file

@ -0,0 +1,75 @@
<?php
/** @noinspection PhpUnused */
namespace Fhp\Segment\EKP;
use Fhp\Segment\BaseSegment;
use Fhp\Segment\Paginateable;
/**
* Segment: Elektronischer Kontoauszug PDF anfordern (Version 2)
*
* Dieser Geschäftsvorfall dient zum Abruf elektronischer Kontoauszüge im PDF-Format.
* Der Kontoauszug enthält alle Umsätze seit dem letzten Ausdruck.
*
* @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2015-08-07_final_version.pdf
*/
class HKEKPv2 extends BaseSegment implements Paginateable
{
/** Kontoverbindung international (IBAN/BIC) */
public \Fhp\Segment\Common\Kti $kontoverbindungInternational;
/**
* Kontoauszugsnummer - Optional
* Falls das Kreditinstitut den Abruf historischer Kontoauszüge unterstützt,
* kann hier die Nummer eines bereits gedruckten Auszugs angegeben werden.
* Bleibt leer = alle bislang nicht abgerufenen Kontoauszüge werden geliefert.
*/
public ?int $kontoauszugsnummer = null;
/**
* Kontoauszugsjahr - Optional
* Falls die Bank im Jahresturnus die Kontoauszugsnummer neu zu zählen beginnt,
* muss das Jahr angegeben werden um historische Auszüge eindeutig zu kennzeichnen.
*/
public ?int $kontoauszugsjahr = null;
/**
* Maximale Anzahl Einträge - Optional
* Nur erlaubt wenn BPD dies gestattet.
*/
public ?int $maximaleAnzahlEintraege = null;
/**
* Aufsetzpunkt für Pagination - Max 35 Zeichen
*/
public ?string $aufsetzpunkt = null;
/**
* Erstellt ein HKEKP Segment für den Abruf von PDF-Kontoauszügen
*
* @param \Fhp\Segment\Common\Kti $kti Kontoverbindung
* @param int|null $kontoauszugsnummer Optional: Bestimmte Auszugsnummer
* @param int|null $kontoauszugsjahr Optional: Jahr des Auszugs
* @param string|null $aufsetzpunkt Optional: Pagination Token
* @return HKEKPv2
*/
public static function create(
\Fhp\Segment\Common\Kti $kti,
?int $kontoauszugsnummer = null,
?int $kontoauszugsjahr = null,
?string $aufsetzpunkt = null
): HKEKPv2 {
$result = HKEKPv2::createEmpty();
$result->kontoverbindungInternational = $kti;
$result->kontoauszugsnummer = $kontoauszugsnummer;
$result->kontoauszugsjahr = $kontoauszugsjahr;
$result->aufsetzpunkt = $aufsetzpunkt;
return $result;
}
public function setPaginationToken(string $paginationToken): void
{
$this->aufsetzpunkt = $paginationToken;
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace Fhp\Segment\EKP;
/**
* Auftragsspezifische Bankparameterdaten für Kontoauszug PDF (HKEKP)
*/
class ParameterKontoauszugPdf
{
/**
* Kontoauszugsnummer erlaubt
* Gibt an, ob der Kunde anhand einer Auszugsnummer historische Kontoauszüge anfordern kann.
*/
public bool $kontoauszugsnummerErlaubt;
/**
* Quittierung benötigt
* Gibt an, ob der Kunde den Erhalt des Auszugs quittieren muss.
*/
public bool $quittierungBenoetigt;
/**
* Eingabe Anzahl Einträge erlaubt
* Gibt an, ob der Kunde die maximale Anzahl Einträge angeben darf.
*/
public bool $eingabeAnzahlEintraegeErlaubt;
/**
* Base64 kodiert
* Gibt an, ob das PDF base64-kodiert geliefert wird (J) oder als reines Binär (N).
*/
public bool $base64Kodiert;
public function isHistoricalStatementsAllowed(): bool
{
return $this->kontoauszugsnummerErlaubt;
}
public function needsReceipt(): bool
{
return $this->quittierungBenoetigt;
}
public function isBase64Encoded(): bool
{
return $this->base64Kodiert;
}
}