Die Umsatzlisten haben jetzt dieselbe Mehrfachauswahl wie ein Kontoblatt: ein Ankreuzfeld vor jedem Umsatz, eines in der Titelleiste für alle angezeigten und darüber der Knopf „Auswahl löschen“. Er steht in jedem Reiter – offen, übernommen und verworfen –, nennt die Anzahl der Auswahl und bleibt ohne Auswahl gesperrt. Bisher ließ sich ein abgerufener Umsatz nur übernehmen oder verwerfen; wegräumen ging gar nicht. Gelöscht wird allein die abgerufene Zeile. Eine daraus entstandene Buchung gehört ins Kontoblatt und bleibt dort stehen – wer sie loswerden will, löscht sie dort, und dann kehrt ihr Umsatz ohnehin in die Vorschlagsliste zurück. Mit dem Umsatz geht aber sein Fingerabdruck: Was im nächsten Abrufzeitraum liegt, kommt danach wieder herein, bei einem verworfenen Umsatz also erneut als offen. - Neue Route POST /banking/umsaetze/loeschen auf Bankumsatz::mehrereLoeschen(); der Status hängt am Formularziel, sodass der Weg in denselben Reiter zurückführt. Ohne JavaScript weist der Controller eine leere Auswahl ab. - Die Liste ist keine Tabelle, sondern eine Folge von Karten. Die Kopfzeile, die eine Tabelle von selbst hätte, gibt es deshalb neu als .listenkopf; die Mechanik dahinter (.alle-waehlen mit data-waehlt) ist unverändert die des Kontoblatts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
238 lines
9.8 KiB
PHP
238 lines
9.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Finanzmanager\Database;
|
||
use Finanzmanager\Models\Bankumsatz;
|
||
use Finanzmanager\Models\Bankzugang;
|
||
use Finanzmanager\Models\Buchung;
|
||
use Finanzmanager\Models\Kategorie;
|
||
use Finanzmanager\Models\Klasse;
|
||
use Finanzmanager\Models\Konto;
|
||
|
||
/**
|
||
* Sammellöschen in den Umsatzlisten des Bankings.
|
||
*
|
||
* Geprüft wird, was hinter dem Knopf *Auswahl löschen* passiert: Die gewählten
|
||
* Umsätze verschwinden – und zwar nur sie. Eine schon erzeugte Buchung gehört
|
||
* ins Kontoblatt und bleibt dort stehen; sie verliert lediglich den Umsatz, der
|
||
* sie belegt hat. Der Knopf steht in jedem Reiter, also müssen offene,
|
||
* übernommene und verworfene Umsätze gleichermaßen gehen.
|
||
*/
|
||
class UmsaetzeLoeschenTest extends TestCase
|
||
{
|
||
protected string $testName = 'Test 28: Bankumsätze sammelweise löschen';
|
||
|
||
private int $giro = 0;
|
||
private int $zugang = 0;
|
||
private int $bankkonto = 0;
|
||
private int $laufendeNummer = 0;
|
||
|
||
public function run(): void
|
||
{
|
||
$this->mandantAnlegen();
|
||
$this->stammdaten();
|
||
|
||
$this->auswahlVerschwindetAusDerListe();
|
||
$this->buchungUeberlebtDenGeloeschtenUmsatz();
|
||
$this->jederStatusLaesstSichLoeschen();
|
||
$this->leereUndUnbekannteAuswahlBleibtFolgenlos();
|
||
$this->jedeAnsichtBietetDieAuswahlAn();
|
||
}
|
||
|
||
private function stammdaten(): void
|
||
{
|
||
$this->giro = Konto::anlegen([
|
||
'name' => 'Sammelgiro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||
]);
|
||
|
||
$this->zugang = Bankzugang::anlegen([
|
||
'name' => 'Sammelbank',
|
||
'bankname' => 'Sammelbank AG',
|
||
'blz' => '20041112',
|
||
'fints_url' => 'https://banking.example.org/fints',
|
||
'benutzerkennung' => 'testkunde',
|
||
'pin' => 'geheim1234',
|
||
'abrufzeitraum' => 'letzter_abruf',
|
||
'aktiv' => 1,
|
||
]);
|
||
|
||
$this->bankkonto = (int)Bankzugang::findeOderLegeKontoAn(
|
||
$this->zugang,
|
||
'DE53200411120847626901',
|
||
'847626901',
|
||
'Girokonto'
|
||
)['id'];
|
||
|
||
Bankzugang::ordneKontoZu($this->bankkonto, $this->giro);
|
||
}
|
||
|
||
private function umsatzAnlegen(string $zweck, float $betrag = -19.99): int
|
||
{
|
||
// Jeder Umsatz bekommt einen eigenen Tag: Sonst fiele der Fingerabdruck
|
||
// zweier gleich lautender Zeilen zusammen und der zweite käme nie an.
|
||
$tag = str_pad((string)(++$this->laufendeNummer), 2, '0', STR_PAD_LEFT);
|
||
|
||
$id = Bankumsatz::anlegen([
|
||
'zugang_id' => $this->zugang,
|
||
'konto_id' => $this->giro,
|
||
'bank_konto_id' => $this->bankkonto,
|
||
'extern_id' => Bankumsatz::externId($this->bankkonto, '2026-07-' . $tag, $betrag, $zweck),
|
||
'buchungstag' => '2026-07-' . $tag,
|
||
'wertstellung' => '2026-07-' . $tag,
|
||
'betrag' => $betrag,
|
||
'empfaenger' => 'Sammelempfänger',
|
||
'verwendungszweck' => $zweck,
|
||
'buchungsart' => 'LASTSCHRIFT',
|
||
]);
|
||
|
||
$this->assertNotEmpty($id, 'Umsatz angelegt: ' . $zweck);
|
||
|
||
return (int)$id;
|
||
}
|
||
|
||
private function existiert(int $umsatzId): bool
|
||
{
|
||
return Database::fetchValue('SELECT 1 FROM bank_umsaetze WHERE id = ?', [$umsatzId]) !== null;
|
||
}
|
||
|
||
/** Die Mehrfachauswahl trifft genau die angekreuzten Zeilen. */
|
||
private function auswahlVerschwindetAusDerListe(): void
|
||
{
|
||
$eins = $this->umsatzAnlegen('Sammelstrom');
|
||
$zwei = $this->umsatzAnlegen('Sammelwasser');
|
||
$drei = $this->umsatzAnlegen('Sammelgas');
|
||
|
||
$anzahl = Bankumsatz::mehrereLoeschen([$eins, $drei]);
|
||
|
||
$this->assertEquals(2, $anzahl, 'Zwei ausgewählte Umsätze gelöscht');
|
||
$this->assert(!$this->existiert($eins), 'Der erste ausgewählte Umsatz ist weg');
|
||
$this->assert(!$this->existiert($drei), 'Der zweite ausgewählte Umsatz ist weg');
|
||
$this->assert($this->existiert($zwei), 'Der nicht ausgewählte Umsatz steht noch da');
|
||
|
||
$offene = array_map('intval', array_column(Bankumsatz::liste(['status' => 'offen']), 'id'));
|
||
$this->assert(!in_array($eins, $offene, true), 'Der gelöschte Umsatz steht nicht mehr in der Liste');
|
||
$this->assert(in_array($zwei, $offene, true), 'Der übrige Umsatz steht weiter zum Abgleich bereit');
|
||
}
|
||
|
||
/**
|
||
* Der Unterschied zum Kontoblatt: Hier wird die abgerufene Zeile gelöscht,
|
||
* nicht die Buchung. Was einmal gebucht ist, bleibt gebucht.
|
||
*/
|
||
private function buchungUeberlebtDenGeloeschtenUmsatz(): void
|
||
{
|
||
$umsatz = $this->umsatzAnlegen('Sammelmiete', -750.00);
|
||
$buchungId = Bankumsatz::uebernehmen($umsatz, []);
|
||
|
||
$this->assertEquals(1, Bankumsatz::mehrereLoeschen([$umsatz]), 'Der übernommene Umsatz wurde gelöscht');
|
||
$this->assert(!$this->existiert($umsatz), 'Der übernommene Umsatz ist weg');
|
||
|
||
$buchung = Buchung::find((int)$buchungId);
|
||
$this->assertNotEmpty($buchung, 'Die Buchung steht weiter im Kontoblatt');
|
||
$this->assertEquals(-750.00, (float)$buchung['betrag'], 'Und zwar unverändert');
|
||
}
|
||
|
||
/** Der Knopf steht in jedem Reiter – offen, übernommen und verworfen. */
|
||
private function jederStatusLaesstSichLoeschen(): void
|
||
{
|
||
$offen = $this->umsatzAnlegen('Sammelversicherung', -44.00);
|
||
$uebernommen = $this->umsatzAnlegen('Sammelrundfunk', -55.08);
|
||
$verworfen = $this->umsatzAnlegen('Sammelfehlbuchung', -1.00);
|
||
|
||
Bankumsatz::uebernehmen($uebernommen, []);
|
||
Bankumsatz::verwerfen($verworfen);
|
||
|
||
$anzahl = Bankumsatz::mehrereLoeschen([$offen, $uebernommen, $verworfen]);
|
||
|
||
$this->assertEquals(3, $anzahl, 'Aus jedem Reiter lässt sich löschen');
|
||
$this->assert(!$this->existiert($offen), 'Offener Umsatz gelöscht');
|
||
$this->assert(!$this->existiert($uebernommen), 'Übernommener Umsatz gelöscht');
|
||
$this->assert(!$this->existiert($verworfen), 'Verworfener Umsatz gelöscht');
|
||
}
|
||
|
||
/**
|
||
* Ohne Auswahl darf nichts geschehen – der Controller weist den leeren Fall
|
||
* ab, das Modell darf ihn trotzdem nicht als „alles löschen“ lesen.
|
||
*/
|
||
private function leereUndUnbekannteAuswahlBleibtFolgenlos(): void
|
||
{
|
||
$bleibt = $this->umsatzAnlegen('Sammelabo', -9.99);
|
||
|
||
$this->assertEquals(0, Bankumsatz::mehrereLoeschen([]), 'Leere Auswahl löscht nichts');
|
||
$this->assertEquals(0, Bankumsatz::mehrereLoeschen([0, -3]), 'Unbrauchbare Nummern löschen nichts');
|
||
$this->assertEquals(0, Bankumsatz::mehrereLoeschen([$bleibt + 100000]), 'Unbekannte Nummer löscht nichts');
|
||
$this->assert($this->existiert($bleibt), 'Der vorhandene Umsatz ist unangetastet');
|
||
|
||
// Doppelt angekreuzt gibt es nicht, doppelt gesendet schon – die Anzahl
|
||
// in der Meldung darf dadurch nicht steigen.
|
||
$this->assertEquals(1, Bankumsatz::mehrereLoeschen([$bleibt, $bleibt]), 'Doppelte Nummer zählt einmal');
|
||
}
|
||
|
||
/**
|
||
* Die Oberfläche: In jedem Reiter stehen die Ankreuzfelder vor den Umsätzen,
|
||
* das Feld „alle wählen“ in der Titelleiste und darüber der Knopf.
|
||
*/
|
||
private function jedeAnsichtBietetDieAuswahlAn(): void
|
||
{
|
||
$offen = $this->umsatzAnlegen('Ansichtsstrom', -31.00);
|
||
$uebernommen = $this->umsatzAnlegen('Ansichtsmiete', -420.00);
|
||
$verworfen = $this->umsatzAnlegen('Ansichtsdublette', -2.50);
|
||
|
||
Bankumsatz::uebernehmen($uebernommen, []);
|
||
Bankumsatz::verwerfen($verworfen);
|
||
|
||
$erwartet = ['offen' => $offen, 'uebernommen' => $uebernommen, 'verworfen' => $verworfen];
|
||
|
||
foreach ($erwartet as $status => $umsatzId) {
|
||
$html = $this->ansicht($status);
|
||
|
||
$this->assert(
|
||
str_contains($html, 'action="/banking/umsaetze/loeschen?status=' . $status . '"'),
|
||
'Reiter „' . $status . '“: Das Löschformular kehrt in denselben Reiter zurück'
|
||
);
|
||
$this->assert(
|
||
str_contains($html, '>Auswahl löschen</button>'),
|
||
'Reiter „' . $status . '“: Der Knopf „Auswahl löschen“ steht über der Liste'
|
||
);
|
||
$this->assert(
|
||
str_contains($html, 'class="alle-waehlen" data-waehlt="umsatzauswahl"'),
|
||
'Reiter „' . $status . '“: Die Titelleiste hat ein Feld „alle wählen“'
|
||
);
|
||
$this->assert(
|
||
str_contains($html, 'name="umsaetze[]" value="' . $umsatzId . '" form="umsatzauswahl"'),
|
||
'Reiter „' . $status . '“: Vor dem Umsatz steht sein Ankreuzfeld'
|
||
);
|
||
}
|
||
|
||
// Ohne Umsätze steht auch kein Löschknopf da – sonst zeigte die leere
|
||
// Liste eine Schaltfläche, die nichts treffen kann.
|
||
Bankumsatz::mehrereLoeschen(array_values($erwartet));
|
||
$this->assert(
|
||
!str_contains($this->ansicht('verworfen'), '>Auswahl löschen</button>'),
|
||
'Ohne Umsätze gibt es nichts zu löschen'
|
||
);
|
||
}
|
||
|
||
/** Rendert die Abgleichliste eines Reiters so, wie der Controller es täte. */
|
||
private function ansicht(string $status): string
|
||
{
|
||
$umsaetze = [];
|
||
foreach (Bankumsatz::liste(['status' => $status]) as $umsatz) {
|
||
$umsatz['vorschlag'] = $status === 'offen' ? Bankumsatz::vorschlag($umsatz) : null;
|
||
$umsaetze[] = $umsatz;
|
||
}
|
||
|
||
$titel = 'Umsätze abgleichen';
|
||
$flash = [];
|
||
$benutzer = null;
|
||
$csrfToken = 'x';
|
||
$kategorien = Kategorie::baumFlach(null, true);
|
||
$klassen = Klasse::alle();
|
||
$konten = Konto::alle(true);
|
||
$offen = Bankumsatz::anzahlOffen();
|
||
|
||
ob_start();
|
||
include dirname(__DIR__) . '/src/Views/banking/umsaetze.php';
|
||
return (string)ob_get_clean();
|
||
}
|
||
}
|