diff --git a/Changes.md b/Changes.md index fefc1d6..fe6877f 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,11 @@ --- +## 2026-06-11 – Feature: CSV-Importformat aus „Neue Direktverbindung"-Dialog anlegen (Issue #102) + +- **`bank/fints.php`:** Neue Auswahl „+ CSV-Importformat…" im Anbieter-Dropdown des „Neue Direktverbindung"-Dialogs; zeigt Felder für Trennzeichen, Anführungszeichen, Datumsformat, Dezimal-/Tausendertrennzeichen, Headerzeile und übersprungene Zeilen +- **`BankController::direktStore()`:** Behandelt `provider_id === '__csv__'` gesondert – legt einen `bank_sources`-Eintrag mit `type='csv'` und `provider_id=NULL` an (Standard-Spaltenzuordnung: Buchungstag=0, Wertstellung=1, Vorgang=2, Betrag=4) und leitet zur Feinabstimmung nach `/settings/bankimport` weiter + ## 2026-06-11 – Fix: Direktabruf springt nach Abschluss zurück zu „Bank buchen" (Issue #101) - **`src/Views/bank/fints.php`:** Nach erfolgreichem Direktabruf (FinTS) führt der „Schließen"-Button im Abruf-Log-Modal jetzt zu einem Redirect auf `/bank` ("Bank buchen"); bei Abbruch oder Fehler bleibt die Anzeige wie bisher auf `/bank/direkt`, damit der Abruf wiederholt werden kann diff --git a/docs/funktionen.md b/docs/funktionen.md index f8d79e3..e436719 100644 --- a/docs/funktionen.md +++ b/docs/funktionen.md @@ -66,6 +66,7 @@ Anlagenspiegel für abnutzbare Wirtschaftsgüter – siehe [anlagevermögen.md]( - Import von Kontoauszügen über zwei Wege: **CSV-Upload** (TXT/XLSX ebenfalls) und **Direktverbindung** (API/FinTS) - **CSV-Upload**: konfigurierbare Spalten- und Datumsformate je Bank, automatische Format-Erkennung beim Datei-Upload, Vorschau-Panel mit Headerzeile und Spalten-Zuordnung; bei unbekannten Formaten wird ein Vorschlag generiert und auf der Settings-Seite zum expliziten Speichern angeboten +- **Neues CSV-Format manuell anlegen**: im „Neue Direktverbindung"-Dialog (Importieren) kann per „+ CSV-Importformat…" ein neues CSV-Format mit Trennzeichen, Datums-/Dezimalformat etc. angelegt werden; Spaltenzuordnung wird anschließend unter Einstellungen → CSV Bankimport Formate verfeinert - **Direktverbindung „PayPal"** (NVP/SOAP-API): API-Benutzer, API-Kennwort, Signature und Sandbox-Flag werden verschlüsselt abgelegt. Live-Log via Server-Sent Events - **Direktverbindung „FinTS"** (Comdirect, DKB und weitere): BLZ, Benutzerkennung und PIN, optional FinTS-Server-URL (sonst Default aus dem Provider) und IBAN-Filter. PIN wird verschlüsselt abgelegt (AES-256-GCM). Leseabruf mit PIN; TAN-pflichtige Aktionen werden mit Fehlermeldung abgebrochen - **Letzter Abrufzeitpunkt** wird pro Verbindung gespeichert; nächster Abruf startet ab 00:00 dieses Tages (max. ein Tag Überlappung für Dedup) diff --git a/src/Controllers/BankController.php b/src/Controllers/BankController.php index d04235d..37b65a5 100644 --- a/src/Controllers/BankController.php +++ b/src/Controllers/BankController.php @@ -405,6 +405,47 @@ class BankController extends BaseController return; } + // Neues CSV-Importformat anlegen, wenn Dropdown auf "__csv__" steht. + // Provider entfällt hier (provider_id = NULL); Spaltenzuordnung erfolgt + // anschließend unter /settings/bankimport. + if ($providerRaw === '__csv__') { + $kuerzel = trim($_POST['kuerzel'] ?? ''); + if ($kuerzel === '' || mb_strlen($kuerzel) > 10) { + View::redirect('/bank/direkt', 'Bitte ein Kürzel angeben (max. 10 Zeichen).', 'error'); + return; + } + + $dateFormats = ['d.m.Y', 'd.m.y', 'Y-m-d', 'd/m/Y', 'm/d/Y', 'Y.m.d', 'd-m-Y']; + $delimiter = (string)($_POST['csv_delimiter'] ?? ';'); + $enclosure = (string)($_POST['csv_enclosure'] ?? '"'); + $dateFmt = (string)($_POST['csv_date_format'] ?? 'd.m.Y'); + $decSep = (string)($_POST['csv_decimal_separator'] ?? ','); + $thouSep = (string)($_POST['csv_thousands_separator'] ?? '.'); + $farbe = (string)($_POST['farbe'] ?? ''); + + $id = BankSource::create([ + 'type' => 'csv', + 'bezeichnung' => $bezeichnung, + 'kuerzel' => $kuerzel, + 'farbe' => preg_match('/^#[0-9a-fA-F]{6}$/', $farbe) ? $farbe : '#6366f1', + 'skip_rows' => max(0, min(99, (int)($_POST['csv_skip_rows'] ?? 0))), + 'has_header_row' => ($_POST['csv_has_header_row'] ?? '1') === '0' ? 0 : 1, + 'delimiter' => mb_strlen($delimiter) === 1 ? $delimiter : ';', + 'enclosure' => mb_strlen($enclosure) === 1 ? $enclosure : '"', + 'date_format' => in_array($dateFmt, $dateFormats, true) ? $dateFmt : 'd.m.Y', + 'decimal_separator' => mb_strlen($decSep) === 1 ? $decSep : ',', + 'thousands_separator' => mb_strlen($thouSep) <= 1 ? $thouSep : '.', + 'col_buchungstag' => 0, + 'col_wertstellung' => 1, + 'col_vorgang' => 2, + 'col_buchungstext' => 0, + 'col_wert' => 4, + ]); + + View::redirect('/settings/bankimport#format-' . $id, 'CSV-Format gespeichert. Spaltenzuordnung bei Bedarf anpassen.'); + return; + } + // Neue Bank anlegen, wenn Dropdown auf "__new__" steht. // Verbindungstyp wählt der Nutzer im Formular (API oder FinTS). if ($providerRaw === '__new__') { diff --git a/src/Views/bank/fints.php b/src/Views/bank/fints.php index 9358972..b6f7433 100644 --- a/src/Views/bank/fints.php +++ b/src/Views/bank/fints.php @@ -31,6 +31,16 @@ $fetchRangeOptions = [ 'last_30_days' => 'Letzte 30 Tage', 'full' => 'Ohne Datum (letzte 365 Tage)', ]; + +$csvDateFormatOptions = [ + 'd.m.Y' => '15.05.2026', + 'd.m.y' => '15.05.26', + 'Y-m-d' => '2026-05-15', + 'd/m/Y' => '15/05/2026', + 'm/d/Y' => '05/15/2026', + 'Y.m.d' => '2026.05.15', + 'd-m-Y' => '15-05-2026', +]; ?> @@ -119,6 +129,7 @@ $fetchRangeOptions = [ + @@ -200,6 +211,54 @@ $fetchRangeOptions = [
Die Konto-Auswahl wird beim ersten Abruf abgefragt.
+ +
@@ -411,6 +470,7 @@ $fetchRangeOptions = [ const selectNew = document.getElementById('fc-provider'); const apiNew = document.getElementById('api-fields'); const fintsNew = document.getElementById('fints-fields'); + const csvNew = document.getElementById('csv-fields'); const newBankWrap = document.getElementById('fc-new-bank-wrap'); const newBankInput = document.getElementById('fc-new-bank-name'); @@ -430,11 +490,15 @@ $fetchRangeOptions = [ selectNew.addEventListener('change', () => { const opt = selectNew.options[selectNew.selectedIndex]; const isNew = opt.value === '__new__'; + const isCsv = opt.value === '__csv__'; newBankWrap.style.display = isNew ? 'block' : 'none'; newBankInput.required = isNew; + csvNew.style.display = isCsv ? 'block' : 'none'; if (isNew) { const selected = modalNew.querySelector('input[name="new_bank_typ"]:checked'); syncTypFields(selected ? selected.value : 'FinTS'); + } else if (isCsv) { + syncTypFields(''); } else { syncTypFields(opt.dataset.typ); } diff --git a/src/version.txt b/src/version.txt index aef261c..161bbd1 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -1-260608 +1-260611 diff --git a/tests/44_CsvDirektverbindungTest.php b/tests/44_CsvDirektverbindungTest.php new file mode 100644 index 0000000..fcb0ff7 --- /dev/null +++ b/tests/44_CsvDirektverbindungTest.php @@ -0,0 +1,68 @@ + 'csv', + 'bezeichnung' => 'Test-CSV-Bank', + 'kuerzel' => 'TCB', + 'farbe' => '#6366f1', + 'skip_rows' => 0, + 'has_header_row' => 1, + 'delimiter' => ';', + 'enclosure' => '"', + 'date_format' => 'd.m.Y', + 'decimal_separator' => ',', + 'thousands_separator' => '.', + 'col_buchungstag' => 0, + 'col_wertstellung' => 1, + 'col_vorgang' => 2, + 'col_buchungstext' => 0, + 'col_wert' => 4, + ]); + $this->assertGreaterThan(0, $id, 'CSV-Format wurde angelegt'); + + $format = BankSource::find($id); + $this->assertNotEmpty($format, 'CSV-Format abrufbar'); + $this->assertEquals('csv', $format['type'], 'Typ ist csv'); + $this->assertEquals('TCB', $format['kuerzel'], 'Kürzel korrekt gespeichert'); + $this->assert($format['provider_id'] === null, 'provider_id ist NULL (kein Bank-Anbieter)'); + $this->assertEquals(0, (int)$format['col_buchungstag'], 'Spalte Buchungstag = 0'); + $this->assertEquals(1, (int)$format['col_wertstellung'], 'Spalte Wertstellung = 1'); + $this->assertEquals(2, (int)$format['col_vorgang'], 'Spalte Vorgang = 2'); + $this->assertEquals(4, (int)$format['col_wert'], 'Spalte Betrag = 4'); + $this->assertEquals('d.m.Y', $format['date_format'], 'Datumsformat-Default'); + + // Format erscheint in der CSV-Formatliste … + $csvFormats = BankSource::all('csv'); + $this->assert( + in_array($id, array_map(fn($f) => (int)$f['id'], $csvFormats), true), + 'CSV-Format erscheint in BankSource::all(csv)' + ); + + // … aber nicht in der Direktverbindungs-Liste (BankController::direkt()). + $connections = array_values(array_filter( + BankSource::all(), + fn($s) => in_array($s['type'], ['api', 'fints'], true) + )); + $this->assert( + !in_array($id, array_map(fn($c) => (int)$c['id'], $connections), true), + 'CSV-Format erscheint nicht in der Direktverbindungs-Liste' + ); + } +}