Banking: Umsätze sammelweise löschen
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>
This commit is contained in:
parent
ca8496f770
commit
9839bd5b16
8 changed files with 397 additions and 5 deletions
19
Changes.md
19
Changes.md
|
|
@ -1,5 +1,24 @@
|
||||||
# Änderungen
|
# Änderungen
|
||||||
|
|
||||||
|
## Umsätze im Banking sammelweise löschen (17.08.2026)
|
||||||
|
|
||||||
|
Die Umsatzlisten des Bankings haben jetzt dieselbe Löschfunktion wie ein Kontoblatt: ein
|
||||||
|
Ankreuzfeld vor jedem Umsatz, eines in der Titelleiste für alle angezeigten und darüber den
|
||||||
|
Knopf **Auswahl löschen**. Bisher ließ sich ein abgerufener Umsatz nur übernehmen oder
|
||||||
|
verwerfen – wegräumen ging gar nicht.
|
||||||
|
|
||||||
|
- Der Knopf steht in jedem Reiter – *offen*, *übernommen* und *verworfen* –, nennt die Anzahl
|
||||||
|
der Auswahl und bleibt ohne Auswahl gesperrt. Die Rückfrage vor dem Löschen nennt sie
|
||||||
|
ebenfalls.
|
||||||
|
- Gelöscht wird allein die abgerufene Zeile. Eine daraus entstandene Buchung bleibt im
|
||||||
|
Kontoblatt stehen; mit dem Umsatz geht aber sein Fingerabdruck, sodass er beim nächsten
|
||||||
|
Abruf wieder hereinkommen kann.
|
||||||
|
- Neue Route `POST /banking/umsaetze/loeschen` auf `Bankumsatz::mehrereLoeschen()`; sie kehrt
|
||||||
|
in den Reiter zurück, aus dem gelöscht wurde.
|
||||||
|
- 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 ist
|
||||||
|
dieselbe wie im Kontoblatt (`.alle-waehlen` mit `data-waehlt`).
|
||||||
|
|
||||||
## Ruhige Buchungszeilen im Kontoblatt (17.08.2026)
|
## Ruhige Buchungszeilen im Kontoblatt (17.08.2026)
|
||||||
|
|
||||||
Die Zeilen des Kontoblatts sind jetzt alle gleich hoch – zwei Textzeilen – und ihr Inhalt
|
Die Zeilen des Kontoblatts sind jetzt alle gleich hoch – zwei Textzeilen – und ihr Inhalt
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,20 @@ Nichts verschwindet von allein: Ein Umsatz, der weder übernommen noch verworfen
|
||||||
offen und steht beim nächsten Öffnen wieder da. Das Cockpit zählt die offenen unter *zum
|
offen und steht beim nächsten Öffnen wieder da. Das Cockpit zählt die offenen unter *zum
|
||||||
Abgleich*.
|
Abgleich*.
|
||||||
|
|
||||||
|
### Umsätze löschen
|
||||||
|
|
||||||
|
Jeder Reiter der Liste – *offen*, *übernommen*, *verworfen* – hat 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** (`POST /banking/umsaetze/loeschen`,
|
||||||
|
`Bankumsatz::mehrereLoeschen()`). Der Knopf nennt die Anzahl und bleibt ohne Auswahl
|
||||||
|
gesperrt; ohne JavaScript weist der Controller eine leere Auswahl ab.
|
||||||
|
|
||||||
|
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 innerhalb des nächsten Abrufzeitraums liegt, kommt danach wieder herein –
|
||||||
|
bei einem verworfenen Umsatz also erneut als offen.
|
||||||
|
|
||||||
## Regeln
|
## Regeln
|
||||||
|
|
||||||
Eine Regel prüft ein Muster gegen Empfänger und/oder Verwendungszweck:
|
Eine Regel prüft ein Muster gegen Empfänger und/oder Verwendungszweck:
|
||||||
|
|
@ -312,6 +326,12 @@ angehängt, und das Speichern des Zugangsformulars lässt die Wahl stehen. Die A
|
||||||
Bank steckt hinter derselben Grenze wie der Dialog – geprüft wird, was mit ihrem Ergebnis
|
Bank steckt hinter derselben Grenze wie der Dialog – geprüft wird, was mit ihrem Ergebnis
|
||||||
geschieht.
|
geschieht.
|
||||||
|
|
||||||
|
`tests/28_UmsaetzeLoeschenTest.php` prüft das Sammellöschen: Es trifft genau die
|
||||||
|
ausgewählten Zeilen, eine schon erzeugte Buchung überlebt es, jeder Status lässt sich
|
||||||
|
löschen, leere und unbekannte Auswahl bleiben folgenlos, eine doppelt gesendete Nummer zählt
|
||||||
|
einmal – und die gerenderte Ansicht trägt in jedem Reiter Ankreuzfelder, Titelleiste und
|
||||||
|
Knopf, während die leere Liste keinen Knopf zeigt.
|
||||||
|
|
||||||
`tests/19_KontenzuordnungTest.php` nimmt sich den Weg vor, den ein Umsatz nimmt, wenn die
|
`tests/19_KontenzuordnungTest.php` nimmt sich den Weg vor, den ein Umsatz nimmt, wenn die
|
||||||
Zuordnung erst nach dem Abruf getroffen wird: Offene Umsätze ziehen mit, übernommene nicht,
|
Zuordnung erst nach dem Abruf getroffen wird: Offene Umsätze ziehen mit, übernommene nicht,
|
||||||
und nur das Bankkonto, um das es geht. Dazu das automatische Buchen – mit Regel, ohne Regel,
|
und nur das Bankkonto, um das es geht. Dazu das automatische Buchen – mit Regel, ohne Regel,
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,7 @@ $router->post('/banking/neu', fn() => (new BankingController())->anlegen());
|
||||||
// „/banking/umsaetze“ und „/banking/regeln“.
|
// „/banking/umsaetze“ und „/banking/regeln“.
|
||||||
$router->get('/banking/umsaetze', fn() => (new BankingController())->umsaetze());
|
$router->get('/banking/umsaetze', fn() => (new BankingController())->umsaetze());
|
||||||
$router->post('/banking/umsaetze/alle', fn() => (new BankingController())->alleUebernehmen());
|
$router->post('/banking/umsaetze/alle', fn() => (new BankingController())->alleUebernehmen());
|
||||||
|
$router->post('/banking/umsaetze/loeschen', fn() => (new BankingController())->umsaetzeLoeschen());
|
||||||
$router->post('/banking/umsaetze/{id}/uebernehmen',
|
$router->post('/banking/umsaetze/{id}/uebernehmen',
|
||||||
fn(array $p) => (new BankingController())->uebernehmen($p));
|
fn(array $p) => (new BankingController())->uebernehmen($p));
|
||||||
$router->post('/banking/umsaetze/{id}/verwerfen',
|
$router->post('/banking/umsaetze/{id}/verwerfen',
|
||||||
|
|
|
||||||
|
|
@ -1637,6 +1637,34 @@ table.bericht .wertlink:hover {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Das Ankreuzfeld der Mehrfachauswahl steht vor dem Text und richtet sich an
|
||||||
|
der ersten Zeile aus, nicht an der Mitte der Karte. */
|
||||||
|
.umsatzwahl {
|
||||||
|
flex: none;
|
||||||
|
margin-top: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Titelleiste einer Kartenliste: Sie vertritt die Kopfzeile, die eine Tabelle
|
||||||
|
von selbst hätte – hier steht das Ankreuzfeld „alle wählen“. */
|
||||||
|
.listenkopf {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: .35rem .9rem;
|
||||||
|
margin-bottom: .4rem;
|
||||||
|
border-bottom: 1px solid var(--rand);
|
||||||
|
font-size: .85rem;
|
||||||
|
color: var(--text-gedaempft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listenkopf .ankreuzfeld {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.umsatztext { min-width: 0; }
|
.umsatztext { min-width: 0; }
|
||||||
|
|
||||||
.umsatztext .nebentext { display: block; font-size: .8rem; }
|
.umsatztext .nebentext { display: block; font-size: .8rem; }
|
||||||
|
|
|
||||||
|
|
@ -557,6 +557,36 @@ class BankingController extends BaseController
|
||||||
View::redirect('/banking/umsaetze?status=offen', 'Umsatz steht wieder zum Abgleich bereit.');
|
View::redirect('/banking/umsaetze?status=offen', 'Umsatz steht wieder zum Abgleich bereit.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mehrere in der Abgleichliste angekreuzte Umsätze auf einmal löschen.
|
||||||
|
*
|
||||||
|
* Gelöscht wird nur die abgerufene Zeile. Eine daraus entstandene Buchung
|
||||||
|
* bleibt im Kontoblatt stehen – wer sie loswerden will, löscht sie dort.
|
||||||
|
*/
|
||||||
|
public function umsaetzeLoeschen(): void
|
||||||
|
{
|
||||||
|
$this->verlangeAnmeldung();
|
||||||
|
|
||||||
|
// Zurück in denselben Reiter, aus dem gelöscht wurde.
|
||||||
|
$status = $this->query('status', 'offen');
|
||||||
|
if (!array_key_exists($status, Bankumsatz::STATUS)) {
|
||||||
|
$status = 'offen';
|
||||||
|
}
|
||||||
|
$ziel = '/banking/umsaetze?status=' . $status;
|
||||||
|
|
||||||
|
$ids = $this->postIds('umsaetze');
|
||||||
|
if ($ids === []) {
|
||||||
|
View::redirect($ziel, 'Kein Umsatz ausgewählt.', 'warnung');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anzahl = Bankumsatz::mehrereLoeschen($ids);
|
||||||
|
|
||||||
|
View::redirect($ziel, $anzahl === 1
|
||||||
|
? 'Umsatz gelöscht.'
|
||||||
|
: $anzahl . ' Umsätze gelöscht.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alles übernehmen, was einen eindeutigen Vorschlag hat.
|
* Alles übernehmen, was einen eindeutigen Vorschlag hat.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -363,4 +363,30 @@ class Bankumsatz
|
||||||
{
|
{
|
||||||
Database::execute('DELETE FROM bank_umsaetze WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]);
|
Database::execute('DELETE FROM bank_umsaetze WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mehrere Umsätze auf einmal löschen – die Mehrfachauswahl der Abgleichliste.
|
||||||
|
*
|
||||||
|
* Eine schon erzeugte Buchung bleibt stehen: Sie gehört ins Kontoblatt und
|
||||||
|
* wird auch nur dort gelöscht. Weg ist allein die abgerufene Zeile – und mit
|
||||||
|
* ihr ihr Fingerabdruck, weshalb ein gelöschter Umsatz beim nächsten Abruf
|
||||||
|
* wieder hereinkommen kann.
|
||||||
|
*
|
||||||
|
* @param list<int> $ids
|
||||||
|
* @return int Wie viele Umsätze tatsächlich gelöscht wurden.
|
||||||
|
*/
|
||||||
|
public static function mehrereLoeschen(array $ids): int
|
||||||
|
{
|
||||||
|
$ids = array_values(array_unique(array_filter($ids, static fn (int $id): bool => $id > 0)));
|
||||||
|
if ($ids === []) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$platzhalter = implode(', ', array_fill(0, count($ids), '?'));
|
||||||
|
|
||||||
|
return Database::execute(
|
||||||
|
'DELETE FROM bank_umsaetze WHERE benutzer_id = ? AND id IN (' . $platzhalter . ')',
|
||||||
|
array_merge([Mandant::id()], $ids)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,28 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<a href="/banking/regeln" class="tab rechtsbuendig">Regeln</a>
|
<a href="/banking/regeln" class="tab rechtsbuendig">Regeln</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($status === 'offen' && $umsaetze !== []): ?>
|
<?php if ($umsaetze !== []): ?>
|
||||||
<div class="seitenaktionen">
|
<div class="seitenaktionen">
|
||||||
|
<?php if ($status === 'offen'): ?>
|
||||||
<form method="post" action="/banking/umsaetze/alle" class="inline"
|
<form method="post" action="/banking/umsaetze/alle" class="inline"
|
||||||
data-bestaetigen="Alle eindeutigen Vorschläge übernehmen? Umsätze ohne Regel bleiben offen.">
|
data-bestaetigen="Alle eindeutigen Vorschläge übernehmen? Umsätze ohne Regel bleiben offen.">
|
||||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
<button type="submit" class="knopf primaer">Alle eindeutigen übernehmen</button>
|
<button type="submit" class="knopf primaer">Alle eindeutigen übernehmen</button>
|
||||||
</form>
|
</form>
|
||||||
<a href="/banking/regeln" class="knopf">Regeln pflegen</a>
|
<a href="/banking/regeln" class="knopf">Regeln pflegen</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php /* Wie im Kontoblatt: Das Formular steht in der Aktionsleiste, seine
|
||||||
|
Ankreuzfelder stehen in den Umsatzkarten – verbunden über das
|
||||||
|
form-Attribut. Ein Formular um die ganze Liste verschachtelte
|
||||||
|
sich mit den Formularen der einzelnen Umsätze. */ ?>
|
||||||
|
<form method="post" action="/banking/umsaetze/loeschen?status=<?= View::h($status) ?>"
|
||||||
|
id="umsatzauswahl" class="inline"
|
||||||
|
data-bestaetigen="Die ausgewählten Umsätze wirklich löschen?"
|
||||||
|
data-frage-eine="Den ausgewählten Umsatz wirklich löschen? Eine daraus erzeugte Buchung bleibt im Kontoblatt stehen."
|
||||||
|
data-frage-mehrere="{anzahl} ausgewählte Umsätze wirklich löschen? Daraus erzeugte Buchungen bleiben im Kontoblatt stehen.">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
|
<button type="submit" class="knopf gefahr">Auswahl löschen</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
|
@ -56,6 +70,19 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
: 'Keine Umsätze mit diesem Status.' ?>
|
: 'Keine Umsätze mit diesem Status.' ?>
|
||||||
</p>
|
</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
<?php /* Die Titelleiste der Liste. Eine Tabelle mit Kopfzeile gibt es hier
|
||||||
|
nicht, das Ankreuzfeld „alle wählen“ braucht aber seinen Platz. */ ?>
|
||||||
|
<div class="listenkopf">
|
||||||
|
<label class="ankreuzfeld">
|
||||||
|
<input type="checkbox" class="alle-waehlen" data-waehlt="umsatzauswahl"
|
||||||
|
aria-label="Alle angezeigten Umsätze auswählen">
|
||||||
|
Alle auswählen
|
||||||
|
</label>
|
||||||
|
<span class="nebentext">
|
||||||
|
<?= count($umsaetze) ?> <?= count($umsaetze) === 1 ? 'Umsatz' : 'Umsätze' ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php foreach ($umsaetze as $umsatz): ?>
|
<?php foreach ($umsaetze as $umsatz): ?>
|
||||||
<?php
|
<?php
|
||||||
$id = (int)$umsatz['id'];
|
$id = (int)$umsatz['id'];
|
||||||
|
|
@ -65,6 +92,9 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
?>
|
?>
|
||||||
<section class="umsatz">
|
<section class="umsatz">
|
||||||
<div class="umsatzkopf">
|
<div class="umsatzkopf">
|
||||||
|
<input type="checkbox" name="umsaetze[]" value="<?= $id ?>" form="umsatzauswahl"
|
||||||
|
class="umsatzwahl"
|
||||||
|
aria-label="Umsatz vom <?= View::h(View::datum($umsatz['buchungstag'])) ?> über <?= View::h(View::betrag($umsatz['betrag'])) ?> auswählen">
|
||||||
<div class="umsatztext">
|
<div class="umsatztext">
|
||||||
<strong><?= View::h($umsatz['empfaenger'] ?: 'Ohne Empfänger') ?></strong>
|
<strong><?= View::h($umsatz['empfaenger'] ?: 'Ohne Empfänger') ?></strong>
|
||||||
<span class="nebentext"><?= View::h(View::datum($umsatz['buchungstag'])) ?>
|
<span class="nebentext"><?= View::h(View::datum($umsatz['buchungstag'])) ?>
|
||||||
|
|
|
||||||
238
tests/28_UmsaetzeLoeschenTest.php
Normal file
238
tests/28_UmsaetzeLoeschenTest.php
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue