Merge main in automate/issue-103 (Changes.md-Konflikt aufgelöst)
This commit is contained in:
commit
fa4bfcf445
5 changed files with 179 additions and 0 deletions
|
|
@ -7,6 +7,11 @@
|
|||
- **`LlmService`:** Neue Methode `getAvailableModels()` fragt `/api/tags` ab und liefert die Namen aller lokal verfügbaren Ollama-Modelle; neue Getter `getMode()`, `getModel()`, `getVisionModel()`
|
||||
- **Einstellungsseite:** Im KI-Abschnitt wird bei erreichbarem Ollama-Provider die Liste der verfügbaren Modelle angezeigt, das konfigurierte Standard- und Vision-Modell sind markiert
|
||||
|
||||
## 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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__') {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
];
|
||||
?>
|
||||
|
||||
<?php if (empty($connections)): ?>
|
||||
|
|
@ -119,6 +129,7 @@ $fetchRangeOptions = [
|
|||
</option>
|
||||
<?php endforeach; ?>
|
||||
<option value="__new__" data-typ="FinTS">+ Neue Bank…</option>
|
||||
<option value="__csv__" data-typ="CSV">+ CSV-Importformat…</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -200,6 +211,54 @@ $fetchRangeOptions = [
|
|||
<div class="text-muted text-sm">Die Konto-Auswahl wird beim ersten Abruf abgefragt.</div>
|
||||
</div>
|
||||
|
||||
<div id="csv-fields" style="display:none">
|
||||
<div class="text-muted text-sm" style="margin-bottom:.75rem">
|
||||
Grundeinstellungen für ein neues CSV-Importformat. Die Spaltenzuordnung kann danach unter
|
||||
<a href="/settings/bankimport">Einstellungen → CSV Bankimport Formate</a> verfeinert werden.
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label for="fc-csv-delimiter">Trennzeichen</label>
|
||||
<input type="text" id="fc-csv-delimiter" name="csv_delimiter" value=";" maxlength="1">
|
||||
</div>
|
||||
<div class="form-group" style="flex:1">
|
||||
<label for="fc-csv-enclosure">Anführungszeichen</label>
|
||||
<input type="text" id="fc-csv-enclosure" name="csv_enclosure" value='"' maxlength="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fc-csv-date-format">Datumsformat</label>
|
||||
<select id="fc-csv-date-format" name="csv_date_format">
|
||||
<?php foreach ($csvDateFormatOptions as $val => $label): ?>
|
||||
<option value="<?= $h($val) ?>"><?= $h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label for="fc-csv-decimal">Dezimaltrennzeichen</label>
|
||||
<input type="text" id="fc-csv-decimal" name="csv_decimal_separator" value="," maxlength="1">
|
||||
</div>
|
||||
<div class="form-group" style="flex:1">
|
||||
<label for="fc-csv-thousands">Tausendertrennzeichen</label>
|
||||
<input type="text" id="fc-csv-thousands" name="csv_thousands_separator" value="." maxlength="1">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label for="fc-csv-header">Headerzeile vorhanden</label>
|
||||
<select id="fc-csv-header" name="csv_has_header_row">
|
||||
<option value="1" selected>Ja</option>
|
||||
<option value="0">Nein</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1">
|
||||
<label for="fc-csv-skip">Übersprungene Zeilen</label>
|
||||
<input type="number" id="fc-csv-skip" name="csv_skip_rows" value="0" min="0" max="99">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;gap:.5rem;margin-top:1rem">
|
||||
<button type="button" id="btn-modal-cancel" class="btn btn-secondary">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
68
tests/44_CsvDirektverbindungTest.php
Normal file
68
tests/44_CsvDirektverbindungTest.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Steuerkram\Models\BankSource;
|
||||
|
||||
/**
|
||||
* Test 44: Neues CSV-Importformat über "Neue Direktverbindung"-Dialog (BankController::direktStore).
|
||||
* Prüft die Default-Spaltenzuordnung und dass das Format provider_id=NULL hat
|
||||
* und nicht in der Direktverbindungs-Liste (api/fints) auftaucht.
|
||||
*/
|
||||
class CsvDirektverbindungTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 44: CSV-Importformat aus Direktverbindungs-Dialog anlegen';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// Entspricht BankController::direktStore() für provider_id === '__csv__'
|
||||
// mit unveränderten Vorgabewerten aus dem Formular.
|
||||
$id = BankSource::create([
|
||||
'type' => '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'
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue