From da8e3784fa3259f7c93e687f44e0207623435340 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Mon, 31 Aug 2026 07:58:07 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20#50:=20Zahlungsverkehr=20w=C3=A4hlt=20die?= =?UTF-8?q?=20Bank=20automatisch=20zum=20Auftragskonto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das Überweisungsformular fragt nur noch das Auftragskonto ab; die passende Bankverbindung ermittelt Bankzugang::zugangJeKonto() aus der Kontenzuordnung. Konten ohne konfigurierte Bank stehen in der Auswahl nicht mehr zur Verfügung. Co-Authored-By: Claude Sonnet 5 --- Changes.md | 13 ++ src/Controllers/ZahlungsverkehrController.php | 42 +++++- src/Models/Bankzugang.php | 29 ++++ src/Views/zahlungsverkehr/formular.php | 44 ++---- tests/37_UeberweisungBankZuordnungTest.php | 132 ++++++++++++++++++ 5 files changed, 225 insertions(+), 35 deletions(-) create mode 100644 tests/37_UeberweisungBankZuordnungTest.php diff --git a/Changes.md b/Changes.md index c3da3b1..26ebf1a 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,18 @@ # Änderungen +## Zahlungsverkehr: automatische Bankauswahl (31.08.2026) + +Beim Erfassen einer Überweisung musste bisher neben dem Auftragskonto auch die +Bankverbindung von Hand gewählt werden – obwohl sich diese meist schon aus dem Konto ergibt +(Feature #50). Das Formular fragt jetzt nur noch das Auftragskonto ab; die passende Bank +ermittelt `Bankzugang::zugangJeKonto()` anhand der Kontenzuordnung (`bank_konten.konto_id`) +und trägt sie beim Speichern automatisch in `zahlungsauftraege.zugang_id` ein. + +Konten ohne konfigurierte Bankverbindung stehen als Auftragskonto gar nicht erst zur +Auswahl – ohne Bank ließe sich ohnehin nichts senden. Ein bereits erfasster Auftrag behält +sein Auftragskonto auch dann, wenn dessen Bank nachträglich entfernt wurde: Es bleibt in der +Auswahl, statt beim nächsten Speichern unbemerkt zu wechseln. + ## Einstellungen: Reiter "Profil" zum Ändern des eigenen Passworts (20.08.2026) Bislang ließ sich das eigene Passwort nur direkt über den Datenbestand ändern (Feature #46). diff --git a/src/Controllers/ZahlungsverkehrController.php b/src/Controllers/ZahlungsverkehrController.php index 8bcd1f8..581026e 100644 --- a/src/Controllers/ZahlungsverkehrController.php +++ b/src/Controllers/ZahlungsverkehrController.php @@ -40,8 +40,7 @@ class ZahlungsverkehrController extends BaseController $this->render('zahlungsverkehr/formular.php', [ 'titel' => 'Überweisung erfassen', 'auftrag' => null, - 'konten' => Konto::alle(true), - 'zugaenge' => Bankzugang::alle(true), + 'konten' => $this->kontenMitBank(), 'adressen' => Adresse::alle(), 'kategorien' => Kategorie::baumFlach(null, true), 'klassen' => Klasse::alle(), @@ -87,11 +86,22 @@ class ZahlungsverkehrController extends BaseController return; } + $konten = $this->kontenMitBank(); + + // Ein Auftrag, dessen Bank nachträglich entfernt wurde, soll sein + // Auftragskonto nicht verlieren – sonst würde beim Speichern + // unbemerkt ein anderes Konto gewählt. + if (!in_array((int)$auftrag['konto_id'], array_column($konten, 'id'), true)) { + $bisheriges = Konto::find((int)$auftrag['konto_id']); + if ($bisheriges !== null) { + $konten[] = $bisheriges; + } + } + $this->render('zahlungsverkehr/formular.php', [ 'titel' => 'Überweisung an ' . $auftrag['empfaenger'], 'auftrag' => $auftrag, - 'konten' => Konto::alle(true), - 'zugaenge' => Bankzugang::alle(true), + 'konten' => $konten, 'adressen' => Adresse::alle(), 'kategorien' => Kategorie::baumFlach(null, true), 'klassen' => Klasse::alle(), @@ -202,12 +212,32 @@ class ZahlungsverkehrController extends BaseController ); } + /** + * Konten, denen eine Bankverbindung zugeordnet ist – nur die kommen als + * Auftragskonto infrage, denn ohne Bank lässt sich nichts senden. + * + * @return list> + */ + private function kontenMitBank(): array + { + $zugangJeKonto = Bankzugang::zugangJeKonto(); + + return array_values(array_filter( + Konto::alle(true), + static fn(array $konto): bool => isset($zugangJeKonto[(int)$konto['id']]) + )); + } + /** @return array */ private function formulardaten(): array { + $kontoId = (int)$this->post('konto_id'); + return [ - 'konto_id' => $this->post('konto_id'), - 'zugang_id' => $this->post('zugang_id'), + 'konto_id' => $kontoId, + // Die Bank ergibt sich aus dem Auftragskonto – eine eigene Auswahl + // gibt es im Formular nicht mehr. + 'zugang_id' => Bankzugang::zugangJeKonto()[$kontoId] ?? null, 'empfaenger' => $this->post('empfaenger'), 'iban' => $this->post('iban'), 'bic' => $this->post('bic'), diff --git a/src/Models/Bankzugang.php b/src/Models/Bankzugang.php index a273e16..ee8a430 100644 --- a/src/Models/Bankzugang.php +++ b/src/Models/Bankzugang.php @@ -405,6 +405,35 @@ class Bankzugang // ── Zugeordnete Bankkonten ─────────────────────────────────────────────── + /** + * Konto-ID → Zugang-ID, für jedes eigene Konto mit konfigurierter Bank. + * + * Grundlage für die automatische Bankauswahl im Zahlungsverkehr: Nur + * Konten mit einem Eintrag hier tauchen dort in der Auswahl auf, und die + * Bank steht damit fest, sobald das Konto gewählt ist. Nur aktive + * Zugänge zählen – ein deaktivierter kann ohnehin nicht senden. + * + * @return array + */ + public static function zugangJeKonto(): array + { + $zeilen = Database::fetchAll( + 'SELECT bk.konto_id, MIN(bk.zugang_id) AS zugang_id + FROM bank_konten bk + JOIN bank_zugaenge z ON z.id = bk.zugang_id + WHERE z.benutzer_id = ? AND z.aktiv = 1 AND bk.konto_id IS NOT NULL + GROUP BY bk.konto_id', + [Mandant::id()] + ); + + $zuordnung = []; + foreach ($zeilen as $zeile) { + $zuordnung[(int)$zeile['konto_id']] = (int)$zeile['zugang_id']; + } + + return $zuordnung; + } + /** @return list> */ public static function konten(int $zugangId): array { diff --git a/src/Views/zahlungsverkehr/formular.php b/src/Views/zahlungsverkehr/formular.php index 0287956..a158014 100644 --- a/src/Views/zahlungsverkehr/formular.php +++ b/src/Views/zahlungsverkehr/formular.php @@ -6,7 +6,6 @@ use Finanzmanager\View; /** * @var array|null $auftrag * @var list> $konten - * @var list> $zugaenge * @var list> $adressen * @var list> $kategorien * @var list> $klassen @@ -44,35 +43,22 @@ include __DIR__ . '/../layout/kopf.php'; - -

- Die IBAN des Kontos muss zu einem Konto bei der Bank passen – daran erkennt der - Auftrag, von wo er abgeht. -

- - - - - + +

- Noch keine Bankverbindung eingerichtet – jetzt anlegen. + Die Bank ergibt sich aus dem Konto und wird beim Senden automatisch gewählt. Nur + Konten mit eingerichteter Bankverbindung stehen zur Auswahl. +

+ +

+ Keinem Konto ist eine Bankverbindung zugeordnet – jetzt einrichten.

diff --git a/tests/37_UeberweisungBankZuordnungTest.php b/tests/37_UeberweisungBankZuordnungTest.php new file mode 100644 index 0000000..cc4e7eb --- /dev/null +++ b/tests/37_UeberweisungBankZuordnungTest.php @@ -0,0 +1,132 @@ +mandantAnlegen(); + $this->stammdaten(); + + $this->kontoOhneBankStehtNichtInDerZuordnung(); + $this->zugeordnetesKontoStehtInDerZuordnung(); + $this->einDeaktivierterZugangFaelltHeraus(); + $this->derAuftragUebernimmtDieBankAutomatisch(); + } + + private function stammdaten(): void + { + $this->giro = Konto::anlegen([ + 'name' => 'Zahlungsverkehr-Giro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1, + ]); + $this->ohneBank = Konto::anlegen([ + 'name' => 'Zahlungsverkehr-Sparbuch', 'kontoart' => 'sparkonto', 'anfangssaldo' => 0, 'aktiv' => 1, + ]); + + $this->zugang = Bankzugang::anlegen([ + 'name' => 'Zahlungsverkehr-Bank', + 'bankname' => 'Zahlungsverkehr-Bank AG', + 'blz' => '12030000', + 'fints_url' => 'https://banking.example.org/fints', + 'benutzerkennung' => 'testkunde', + 'pin' => 'geheim1234', + 'abrufzeitraum' => 'letzter_abruf', + 'aktiv' => 1, + ]); + } + + private function kontoOhneBankStehtNichtInDerZuordnung(): void + { + $this->assert( + !array_key_exists($this->ohneBank, Bankzugang::zugangJeKonto()), + 'Ein Konto ohne Bankverbindung fehlt in der Zuordnung' + ); + } + + private function zugeordnetesKontoStehtInDerZuordnung(): void + { + $bankkonto = Bankzugang::findeOderLegeKontoAn( + $this->zugang, + 'DE29120300000000202052', + '202052', + 'Girokonto' + ); + Bankzugang::ordneKontoZu((int)$bankkonto['id'], $this->giro); + + $this->assertEquals( + $this->zugang, + Bankzugang::zugangJeKonto()[$this->giro] ?? null, + 'Das zugeordnete Konto verweist auf seine Bank' + ); + } + + private function einDeaktivierterZugangFaelltHeraus(): void + { + Bankzugang::speichern($this->zugang, [ + 'name' => 'Zahlungsverkehr-Bank', + 'bankname' => 'Zahlungsverkehr-Bank AG', + 'blz' => '12030000', + 'fints_url' => 'https://banking.example.org/fints', + 'benutzerkennung' => 'testkunde', + 'abrufzeitraum' => 'letzter_abruf', + 'aktiv' => 0, + ]); + + $this->assert( + !array_key_exists($this->giro, Bankzugang::zugangJeKonto()), + 'Ein deaktivierter Zugang taugt nicht als automatische Auswahl' + ); + + Bankzugang::speichern($this->zugang, [ + 'name' => 'Zahlungsverkehr-Bank', + 'bankname' => 'Zahlungsverkehr-Bank AG', + 'blz' => '12030000', + 'fints_url' => 'https://banking.example.org/fints', + 'benutzerkennung' => 'testkunde', + 'abrufzeitraum' => 'letzter_abruf', + 'aktiv' => 1, + ]); + } + + /** + * Genau das, was der Controller beim Anlegen tut: die Bank kommt aus der + * Zuordnung, nicht aus einer eigenen Formularauswahl. + */ + private function derAuftragUebernimmtDieBankAutomatisch(): void + { + $zugangId = Bankzugang::zugangJeKonto()[$this->giro] ?? null; + + $auftragId = Zahlungsauftrag::anlegen([ + 'konto_id' => $this->giro, + 'zugang_id' => $zugangId, + 'empfaenger' => 'Beispielempfänger', + 'iban' => 'DE02120300000000202051', + 'betrag' => 42.50, + ]); + + $this->assertEquals( + $this->zugang, + (int)Zahlungsauftrag::find($auftragId)['zugang_id'], + 'Der Auftrag trägt die automatisch ermittelte Bank' + ); + } +}