Fix #50: Zahlungsverkehr wählt die Bank automatisch zum Auftragskonto

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 <noreply@anthropic.com>
This commit is contained in:
fds2610-bot 2026-08-31 07:58:07 +02:00
parent 9022144c9e
commit da8e3784fa
5 changed files with 225 additions and 35 deletions

View file

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

View file

@ -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<array<string,mixed>>
*/
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<string,mixed> */
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'),

View file

@ -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<int,int>
*/
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<array<string,mixed>> */
public static function konten(int $zugangId): array
{

View file

@ -6,7 +6,6 @@ use Finanzmanager\View;
/**
* @var array<string,mixed>|null $auftrag
* @var list<array<string,mixed>> $konten
* @var list<array<string,mixed>> $zugaenge
* @var list<array<string,mixed>> $adressen
* @var list<array<string,mixed>> $kategorien
* @var list<array<string,mixed>> $klassen
@ -44,6 +43,7 @@ include __DIR__ . '/../layout/kopf.php';
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<label for="konto_id">Auftragskonto</label>
<?php if ($konten !== []): ?>
<select id="konto_id" name="konto_id" required>
<?php foreach ($konten as $konto): ?>
<option value="<?= (int)$konto['id'] ?>"
@ -53,26 +53,12 @@ include __DIR__ . '/../layout/kopf.php';
<?php endforeach; ?>
</select>
<p class="feldhinweis">
Die IBAN des Kontos muss zu einem Konto bei der Bank passen – daran erkennt der
Auftrag, von wo er abgeht.
Die Bank ergibt sich aus dem Konto und wird beim Senden automatisch gewählt. Nur
Konten mit eingerichteter Bankverbindung stehen zur Auswahl.
</p>
<label for="zugang_id">Bankverbindung</label>
<?php /* Nicht required: Ein Auftrag darf erfasst werden, bevor eine
Bankverbindung eingerichtet ist. Beim Senden wird sie ohnehin
verlangt – dort mit einer Meldung, die erklärt, was fehlt. */ ?>
<select id="zugang_id" name="zugang_id">
<option value="">– beim Senden wählen –</option>
<?php foreach ($zugaenge as $zugang): ?>
<option value="<?= (int)$zugang['id'] ?>"
<?= (int)$wert('zugang_id') === (int)$zugang['id'] ? 'selected' : '' ?>>
<?= View::h($zugang['name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php if ($zugaenge === []): ?>
<p class="feldhinweis">
Noch keine Bankverbindung eingerichtet – <a href="/banking/neu">jetzt anlegen</a>.
<?php else: ?>
<p class="meldung meldung-fehler" role="status">
Keinem Konto ist eine Bankverbindung zugeordnet – <a href="/banking/neu">jetzt einrichten</a>.
</p>
<?php endif; ?>

View file

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
use Finanzmanager\Models\Bankzugang;
use Finanzmanager\Models\Konto;
use Finanzmanager\Models\Zahlungsauftrag;
/**
* Automatische Bankauswahl im Zahlungsverkehr.
*
* Beim Erfassen einer Überweisung wählt der Nutzer nur noch das Auftragskonto;
* die Bankverbindung ergibt sich aus `Bankzugang::zugangJeKonto()`. Konten ohne
* konfigurierte Bank tauchen dort nicht auf und dürfen deshalb auch nicht als
* Auftragskonto zur Wahl stehen.
*/
class UeberweisungBankZuordnungTest extends TestCase
{
protected string $testName = 'Test 37: Überweisung – automatische Bankzuordnung';
private int $giro = 0;
private int $ohneBank = 0;
private int $zugang = 0;
public function run(): void
{
$this->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'
);
}
}