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:
parent
9022144c9e
commit
da8e3784fa
5 changed files with 225 additions and 35 deletions
13
Changes.md
13
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).
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,35 +43,22 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||
|
||||
<label for="konto_id">Auftragskonto</label>
|
||||
<select id="konto_id" name="konto_id" required>
|
||||
<?php foreach ($konten as $konto): ?>
|
||||
<option value="<?= (int)$konto['id'] ?>"
|
||||
<?= (int)$wert('konto_id') === (int)$konto['id'] ? 'selected' : '' ?>>
|
||||
<?= View::h($konto['name']) ?><?= $konto['iban'] !== '' ? ' (' . View::h($konto['iban']) . ')' : '' ?>
|
||||
</option>
|
||||
<?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.
|
||||
</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 === []): ?>
|
||||
<?php if ($konten !== []): ?>
|
||||
<select id="konto_id" name="konto_id" required>
|
||||
<?php foreach ($konten as $konto): ?>
|
||||
<option value="<?= (int)$konto['id'] ?>"
|
||||
<?= (int)$wert('konto_id') === (int)$konto['id'] ? 'selected' : '' ?>>
|
||||
<?= View::h($konto['name']) ?><?= $konto['iban'] !== '' ? ' (' . View::h($konto['iban']) . ')' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="feldhinweis">
|
||||
Noch keine Bankverbindung eingerichtet – <a href="/banking/neu">jetzt anlegen</a>.
|
||||
Die Bank ergibt sich aus dem Konto und wird beim Senden automatisch gewählt. Nur
|
||||
Konten mit eingerichteter Bankverbindung stehen zur Auswahl.
|
||||
</p>
|
||||
<?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; ?>
|
||||
|
||||
|
|
|
|||
132
tests/37_UeberweisungBankZuordnungTest.php
Normal file
132
tests/37_UeberweisungBankZuordnungTest.php
Normal 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'
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue