Wertpapiere: Depotbuchungen sammelweise löschen
Nach einem Import, der schiefging, war das Löschen Zeile für Zeile mühsam. Die Liste der Depotbuchungen räumt jetzt so auf wie das Kontoblatt: Ankreuzfeld je Zeile, das Feld in der Titelzeile wählt alle angezeigten, „Auswahl löschen“ in der Aktionsleiste schickt sie an POST /wertpapiere/buchungen/loeschen. Der Knopf nennt die Anzahl und bleibt ohne Auswahl gesperrt; die Rückfrage nennt sie ebenfalls. Ohne JavaScript funktioniert alles weiter – dann ist der Knopf offen und der Controller weist eine leere Auswahl ab. Mit jeder Depotbuchung geht ihre Geldbuchung, wie beim Löschen einer einzelnen. Am Bestand ist nichts nachzuziehen: Er wird bei jedem Aufruf aus den Depotbuchungen gerechnet und gibt die Löschung von selbst wieder her, ebenso realisierter Gewinn, Veräußerungen und Erträge. Der eingestellte Filter (Depot, Wertpapier, Vorgang) hängt am Ziel des Formulars, deshalb steht danach wieder dieselbe Ansicht da. Zusammengesetzt wird er im Controller neu, damit kein fremdes Ziel eingeschleust werden kann. Das gilt jetzt auch für das „Löschen“ je Zeile, das bisher zur ungefilterten Liste zurücksprang. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4484ddd90d
commit
4d62529e2c
7 changed files with 402 additions and 2 deletions
23
Changes.md
23
Changes.md
|
|
@ -1,5 +1,28 @@
|
||||||
# Änderungen
|
# Änderungen
|
||||||
|
|
||||||
|
## Wertpapiere: Depotbuchungen sammelweise löschen (12.08.2026)
|
||||||
|
|
||||||
|
Die Liste der Depotbuchungen räumt jetzt so auf wie das Kontoblatt – nach einem Import,
|
||||||
|
der schiefging, war das Löschen Zeile für Zeile mühsam.
|
||||||
|
|
||||||
|
**Auswahl löschen** (`Views/wertpapiere/buchungen.php`, `WertpapiereController`,
|
||||||
|
`public/index.php`)
|
||||||
|
- Ankreuzfeld je Zeile, das Feld in der Titelzeile wählt alle angezeigten Buchungen. Der
|
||||||
|
Knopf steht oben in der Aktionsleiste, nennt die Anzahl und bleibt ohne Auswahl
|
||||||
|
gesperrt; die Rückfrage nennt sie ebenfalls. Ohne JavaScript funktioniert alles weiter:
|
||||||
|
Dann ist der Knopf offen und `POST /wertpapiere/buchungen/loeschen` weist eine leere
|
||||||
|
Auswahl ab.
|
||||||
|
- Mit jeder Depotbuchung geht ihre Geldbuchung – wie beim Löschen einer einzelnen.
|
||||||
|
- **Am Bestand ist nichts nachzuziehen.** Er wird bei jedem Aufruf aus den Depotbuchungen
|
||||||
|
gerechnet und gibt die Löschung von selbst wieder her, ebenso realisierter Gewinn,
|
||||||
|
Veräußerungen und Erträge.
|
||||||
|
- Der eingestellte Filter (Depot, Wertpapier, Vorgang) hängt am Ziel des Formulars,
|
||||||
|
deshalb steht danach wieder dieselbe Ansicht da. Das gilt jetzt auch für das *Löschen*
|
||||||
|
je Zeile, das bisher zur ungefilterten Liste zurücksprang.
|
||||||
|
|
||||||
|
`tests/22_DepotbuchungenLoeschenTest.php` prüft Geldseite, Bestand und Auswertungen nach
|
||||||
|
dem Löschen; `tests/e2e/specs/module.spec.js` den Ablauf im Browser.
|
||||||
|
|
||||||
## Wertpapiere: Depotbestand aus einer CSV-Datei übernehmen (12.08.2026)
|
## Wertpapiere: Depotbestand aus einer CSV-Datei übernehmen (12.08.2026)
|
||||||
|
|
||||||
Ein Depot, das nie im FinanzManager geführt wurde, lässt sich jetzt in einem Zug
|
Ein Depot, das nie im FinanzManager geführt wurde, lässt sich jetzt in einem Zug
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,24 @@ Der Ablauf ist zweistufig: Der Upload zeigt erst eine Vorschau mit allen Positio
|
||||||
Einstand, Wert und dem Status je Zeile; erst *Jetzt übernehmen* schreibt. Die Datei bleibt
|
Einstand, Wert und dem Status je Zeile; erst *Jetzt übernehmen* schreibt. Die Datei bleibt
|
||||||
im Importverzeichnis liegen.
|
im Importverzeichnis liegen.
|
||||||
|
|
||||||
|
## Depotbuchungen löschen
|
||||||
|
|
||||||
|
Die Liste der Depotbuchungen löscht einzeln – *Löschen* je Zeile – oder mehrere auf
|
||||||
|
einmal, genau wie das Kontoblatt: Ankreuzfeld je Zeile, das Feld in der Titelzeile wählt
|
||||||
|
alle angezeigten, *Auswahl löschen* in der Aktionsleiste schickt sie an
|
||||||
|
`POST /wertpapiere/buchungen/loeschen`. Der Knopf nennt die Anzahl und bleibt ohne
|
||||||
|
Auswahl gesperrt; die Rückfrage kommt vor dem Absenden. Ohne JavaScript funktioniert
|
||||||
|
alles weiter – dann ist der Knopf offen und der Controller weist eine leere Auswahl ab.
|
||||||
|
|
||||||
|
Der eingestellte Filter hängt am Ziel beider Formulare, deshalb steht danach wieder
|
||||||
|
dieselbe Ansicht da. Zusammengesetzt wird er im Controller neu, damit kein fremdes Ziel
|
||||||
|
eingeschleust werden kann.
|
||||||
|
|
||||||
|
Mit jeder Depotbuchung geht ihre Geldbuchung (siehe *Zwei Seiten je Geschäft*). Am
|
||||||
|
Bestand ist nichts nachzuziehen: Er wird bei jedem Aufruf aus den Depotbuchungen
|
||||||
|
gerechnet und gibt die Löschung von selbst wieder her – ebenso realisierter Gewinn,
|
||||||
|
Veräußerungen und Erträge. Das Wertpapier selbst bleibt in den Stammdaten stehen.
|
||||||
|
|
||||||
## Kontenübersicht
|
## Kontenübersicht
|
||||||
|
|
||||||
Depots erscheinen dort mit ihrem **bewerteten Bestand**, nicht mit einem Buchungssaldo. Der
|
Depots erscheinen dort mit ihrem **bewerteten Bestand**, nicht mit einem Buchungssaldo. Der
|
||||||
|
|
@ -166,6 +184,11 @@ Bestandsänderung; fehlende Kurse; Veräußerungsrechnung über die Historie hin
|
||||||
QIF-Import eines echten Ausschnitts (Kauf, Dividende, Verkauf) inklusive Idempotenz;
|
QIF-Import eines echten Ausschnitts (Kauf, Dividende, Verkauf) inklusive Idempotenz;
|
||||||
Löschen samt Geldbuchung.
|
Löschen samt Geldbuchung.
|
||||||
|
|
||||||
|
`tests/22_DepotbuchungenLoeschenTest.php` – das Sammellöschen: die Geldbuchungen gehen
|
||||||
|
mit, das Verrechnungskonto stimmt danach wieder, Bestand, Einstand, realisierter Gewinn,
|
||||||
|
Erträge und die Veräußerungsauswertung geben die Löschung her, unbekannte Nummern in der
|
||||||
|
Auswahl bleiben folgenlos. Den Ablauf im Browser prüft `tests/e2e/specs/module.spec.js`.
|
||||||
|
|
||||||
`tests/21_DepotbestandImportTest.php` – der CSV-Bestandsimport an einem Ausschnitt des
|
`tests/21_DepotbestandImportTest.php` – der CSV-Bestandsimport an einem Ausschnitt des
|
||||||
echten DKB-Exports: Vorschau ohne Nebenwirkung, Einstand und Wert aus beiden Kursen,
|
echten DKB-Exports: Vorschau ohne Nebenwirkung, Einstand und Wert aus beiden Kursen,
|
||||||
Bruchstückzahlen, Assetklasse → Art, hinterlegte Bewertungskurse, Idempotenz beim zweiten
|
Bruchstückzahlen, Assetklasse → Art, hinterlegte Bewertungskurse, Idempotenz beim zweiten
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,8 @@ $router->get('/wertpapiere/auswertung', fn() => (new WertpapiereController())->a
|
||||||
$router->get('/wertpapiere/buchungen', fn() => (new WertpapiereController())->buchungen());
|
$router->get('/wertpapiere/buchungen', fn() => (new WertpapiereController())->buchungen());
|
||||||
$router->get('/wertpapiere/buchungen/neu', fn() => (new WertpapiereController())->neu());
|
$router->get('/wertpapiere/buchungen/neu', fn() => (new WertpapiereController())->neu());
|
||||||
$router->post('/wertpapiere/buchungen/neu', fn() => (new WertpapiereController())->anlegen());
|
$router->post('/wertpapiere/buchungen/neu', fn() => (new WertpapiereController())->anlegen());
|
||||||
|
$router->post('/wertpapiere/buchungen/loeschen',
|
||||||
|
fn() => (new WertpapiereController())->auswahlLoeschen());
|
||||||
$router->post('/wertpapiere/buchungen/{id}/loeschen',
|
$router->post('/wertpapiere/buchungen/{id}/loeschen',
|
||||||
fn(array $p) => (new WertpapiereController())->loeschen($p));
|
fn(array $p) => (new WertpapiereController())->loeschen($p));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,65 @@ class WertpapiereController extends BaseController
|
||||||
|
|
||||||
Depotbuchung::loeschen((int)$params['id']);
|
Depotbuchung::loeschen((int)$params['id']);
|
||||||
|
|
||||||
View::redirect('/wertpapiere/buchungen', 'Depotbuchung gelöscht – samt ihrer Geldbuchung.');
|
View::redirect($this->buchungenPfad(), 'Depotbuchung gelöscht – samt ihrer Geldbuchung.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sammellöschen aus der Liste der Depotbuchungen – wie im Kontoblatt.
|
||||||
|
*
|
||||||
|
* Der Bestand wird bei jedem Aufruf aus den Depotbuchungen gerechnet; er
|
||||||
|
* gibt die Löschung deshalb von selbst wieder her, ebenso Veräußerungen und
|
||||||
|
* Erträge. Fortgeschrieben wird nichts.
|
||||||
|
*/
|
||||||
|
public function auswahlLoeschen(): void
|
||||||
|
{
|
||||||
|
$this->verlangeAnmeldung();
|
||||||
|
|
||||||
|
$ziel = $this->buchungenPfad();
|
||||||
|
$ids = $this->postIds('buchungen');
|
||||||
|
if ($ids === []) {
|
||||||
|
View::redirect($ziel, 'Keine Depotbuchung ausgewählt.', 'warnung');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anzahl = 0;
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
// Fremde oder längst gelöschte Nummern still übergehen – der Rest
|
||||||
|
// der Auswahl soll trotzdem verschwinden.
|
||||||
|
if (Depotbuchung::find($id) === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Depotbuchung::loeschen($id);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
View::redirect($ziel, $e->getMessage(), 'fehler');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$anzahl++;
|
||||||
|
}
|
||||||
|
|
||||||
|
View::redirect($ziel, $anzahl === 1
|
||||||
|
? 'Depotbuchung gelöscht – samt ihrer Geldbuchung.'
|
||||||
|
: $anzahl . ' Depotbuchungen gelöscht – samt ihrer Geldbuchungen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zurück in die Liste, mit demselben Filter.
|
||||||
|
*
|
||||||
|
* Die Werte kommen aus der Abfragezeichenfolge des Formularziels und werden
|
||||||
|
* hier neu zusammengesetzt statt übernommen: So kann kein fremdes Ziel
|
||||||
|
* eingeschleust werden.
|
||||||
|
*/
|
||||||
|
private function buchungenPfad(): string
|
||||||
|
{
|
||||||
|
$query = array_filter([
|
||||||
|
'depot' => $this->query('depot'),
|
||||||
|
'wertpapier' => $this->query('wertpapier'),
|
||||||
|
'vorgang' => $this->query('vorgang'),
|
||||||
|
], static fn(string $wert): bool => $wert !== '');
|
||||||
|
|
||||||
|
return '/wertpapiere/buchungen' . ($query === [] ? '' : '?' . http_build_query($query));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bestandsimport aus einer CSV-Datei ───────────────────────────────────
|
// ── Bestandsimport aus einer CSV-Datei ───────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,18 @@ use Finanzmanager\View;
|
||||||
* @var string $csrfToken
|
* @var string $csrfToken
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Nach dem Löschen soll dieselbe Ansicht wieder erscheinen – der Filter hängt
|
||||||
|
// deshalb am Ziel der Löschformulare.
|
||||||
|
$filterPfad = static function () use ($filter): string {
|
||||||
|
$query = array_filter([
|
||||||
|
'depot' => (string)($filter['konto_id'] ?? ''),
|
||||||
|
'wertpapier' => (string)($filter['wertpapier_id'] ?? ''),
|
||||||
|
'vorgang' => (string)($filter['vorgang'] ?? ''),
|
||||||
|
], static fn(string $wert): bool => $wert !== '');
|
||||||
|
|
||||||
|
return $query === [] ? '' : '?' . http_build_query($query);
|
||||||
|
};
|
||||||
|
|
||||||
include __DIR__ . '/../layout/kopf.php';
|
include __DIR__ . '/../layout/kopf.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|
@ -53,6 +65,20 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="knopf klein">Filtern</button>
|
<button type="submit" class="knopf klein">Filtern</button>
|
||||||
</form>
|
</form>
|
||||||
|
<?php if ($buchungen !== []): ?>
|
||||||
|
<!-- Wie im Kontoblatt: Das Formular steht hier, seine Ankreuzfelder
|
||||||
|
stehen in der Tabelle; verbunden sind sie über das form-Attribut.
|
||||||
|
Ein Formular um die ganze Tabelle würde das Filterformular
|
||||||
|
verschachteln. -->
|
||||||
|
<form method="post" action="/wertpapiere/buchungen/loeschen<?= View::h($filterPfad()) ?>"
|
||||||
|
id="depotbuchungsauswahl" class="inline"
|
||||||
|
data-bestaetigen="Die ausgewählten Depotbuchungen wirklich löschen? Die zugehörigen Geldbuchungen werden mitgelöscht."
|
||||||
|
data-frage-eine="Die ausgewählte Depotbuchung wirklich löschen? Die zugehörige Geldbuchung wird mitgelöscht."
|
||||||
|
data-frage-mehrere="{anzahl} ausgewählte Depotbuchungen wirklich löschen? Die zugehörigen Geldbuchungen werden mitgelöscht.">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
|
<button type="submit" class="knopf gefahr">Auswahl löschen</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
<a href="/wertpapiere/buchungen/neu" class="knopf primaer">Depotbuchung erfassen</a>
|
<a href="/wertpapiere/buchungen/neu" class="knopf primaer">Depotbuchung erfassen</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -62,6 +88,10 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<table class="liste zahlen">
|
<table class="liste zahlen">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th class="wahlspalte">
|
||||||
|
<input type="checkbox" class="alle-waehlen" data-waehlt="depotbuchungsauswahl"
|
||||||
|
aria-label="Alle angezeigten Depotbuchungen auswählen">
|
||||||
|
</th>
|
||||||
<th>Datum</th><th>Vorgang</th><th>Wertpapier</th><th>Depot</th>
|
<th>Datum</th><th>Vorgang</th><th>Wertpapier</th><th>Depot</th>
|
||||||
<th class="rechts">Stück</th><th class="rechts">Kurs</th>
|
<th class="rechts">Stück</th><th class="rechts">Kurs</th>
|
||||||
<th class="rechts">Betrag</th><th class="rechts">Kosten</th>
|
<th class="rechts">Betrag</th><th class="rechts">Kosten</th>
|
||||||
|
|
@ -71,6 +101,11 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($buchungen as $buchung): ?>
|
<?php foreach ($buchungen as $buchung): ?>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="wahlspalte">
|
||||||
|
<input type="checkbox" name="buchungen[]" value="<?= (int)$buchung['id'] ?>"
|
||||||
|
form="depotbuchungsauswahl"
|
||||||
|
aria-label="Depotbuchung vom <?= View::h(View::datum($buchung['datum'])) ?> auswählen">
|
||||||
|
</td>
|
||||||
<td><?= View::h(View::datum($buchung['datum'])) ?></td>
|
<td><?= View::h(View::datum($buchung['datum'])) ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?= View::h(Depotbuchung::VORGAENGE[$buchung['vorgang']] ?? $buchung['vorgang']) ?>
|
<?= View::h(Depotbuchung::VORGAENGE[$buchung['vorgang']] ?? $buchung['vorgang']) ?>
|
||||||
|
|
@ -108,7 +143,8 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="rechts">
|
<td class="rechts">
|
||||||
<form method="post" action="/wertpapiere/buchungen/<?= (int)$buchung['id'] ?>/loeschen"
|
<form method="post"
|
||||||
|
action="/wertpapiere/buchungen/<?= (int)$buchung['id'] ?>/loeschen<?= View::h($filterPfad()) ?>"
|
||||||
class="inline"
|
class="inline"
|
||||||
data-bestaetigen="Depotbuchung löschen? Die zugehörige Geldbuchung wird mitgelöscht.">
|
data-bestaetigen="Depotbuchung löschen? Die zugehörige Geldbuchung wird mitgelöscht.">
|
||||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
|
|
|
||||||
177
tests/22_DepotbuchungenLoeschenTest.php
Normal file
177
tests/22_DepotbuchungenLoeschenTest.php
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Finanzmanager\Models\Buchung;
|
||||||
|
use Finanzmanager\Models\Depotbuchung;
|
||||||
|
use Finanzmanager\Models\Konto;
|
||||||
|
use Finanzmanager\Models\Wertpapier;
|
||||||
|
use Finanzmanager\Services\Depot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sammellöschen in der Liste der Depotbuchungen.
|
||||||
|
*
|
||||||
|
* Geprüft wird, was hinter dem Knopf *Auswahl löschen* passiert: Jede gewählte
|
||||||
|
* Depotbuchung geht samt ihrer Geldbuchung, und Bestand, realisierter Gewinn
|
||||||
|
* und Erträge stehen danach wieder auf dem Stand, den sie ohne die Buchungen
|
||||||
|
* hätten – der Bestand wird gerechnet, nicht fortgeschrieben.
|
||||||
|
*/
|
||||||
|
class DepotbuchungenLoeschenTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 22: Depotbuchungen sammelweise löschen';
|
||||||
|
|
||||||
|
private int $depot = 0;
|
||||||
|
private int $verrechnung = 0;
|
||||||
|
private int $papier = 0;
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->mandantAnlegen();
|
||||||
|
$this->stammdaten();
|
||||||
|
|
||||||
|
$this->auswahlNimmtDieGeldbuchungenMit();
|
||||||
|
$this->bestandGibtDieLoeschungWiederHer();
|
||||||
|
$this->unbekannteNummernBleibenFolgenlos();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stammdaten(): void
|
||||||
|
{
|
||||||
|
$this->depot = Konto::anlegen([
|
||||||
|
'name' => 'Sammeldepot', 'kontoart' => 'depot', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||||||
|
]);
|
||||||
|
$this->verrechnung = Konto::anlegen([
|
||||||
|
'name' => 'Sammelverrechnungskonto', 'kontoart' => 'bank',
|
||||||
|
'anfangssaldo' => 10000, 'aktiv' => 1,
|
||||||
|
]);
|
||||||
|
$this->papier = Wertpapier::findenOderAnlegen([
|
||||||
|
'name' => 'Sammelaktie AG', 'isin' => 'DE0007654321', 'art' => 'aktie',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwei Käufe, ein Verkauf, eine Dividende – wie eine Auswahl in der Liste.
|
||||||
|
*
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
private function vierBuchungen(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Depotbuchung::anlegen([
|
||||||
|
'konto_id' => $this->depot, 'wertpapier_id' => $this->papier,
|
||||||
|
'datum' => '2026-01-15', 'vorgang' => 'kauf',
|
||||||
|
'stueck' => 10, 'kurs' => 100, 'betrag' => 1000, 'gebuehren' => 10,
|
||||||
|
'verrechnungskonto_id' => $this->verrechnung,
|
||||||
|
]),
|
||||||
|
Depotbuchung::anlegen([
|
||||||
|
'konto_id' => $this->depot, 'wertpapier_id' => $this->papier,
|
||||||
|
'datum' => '2026-02-15', 'vorgang' => 'kauf',
|
||||||
|
'stueck' => 10, 'kurs' => 120, 'betrag' => 1200, 'gebuehren' => 10,
|
||||||
|
'verrechnungskonto_id' => $this->verrechnung,
|
||||||
|
]),
|
||||||
|
Depotbuchung::anlegen([
|
||||||
|
'konto_id' => $this->depot, 'wertpapier_id' => $this->papier,
|
||||||
|
'datum' => '2026-03-15', 'vorgang' => 'verkauf',
|
||||||
|
'stueck' => 5, 'kurs' => 150, 'betrag' => 750,
|
||||||
|
'gebuehren' => 15, 'steuern' => 20,
|
||||||
|
'verrechnungskonto_id' => $this->verrechnung,
|
||||||
|
]),
|
||||||
|
Depotbuchung::anlegen([
|
||||||
|
'konto_id' => $this->depot, 'wertpapier_id' => $this->papier,
|
||||||
|
'datum' => '2026-04-01', 'vorgang' => 'dividende',
|
||||||
|
'betrag' => 50, 'steuern' => 13.19,
|
||||||
|
'verrechnungskonto_id' => $this->verrechnung,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** So arbeitet der Controller die Auswahl ab: Nummer für Nummer. */
|
||||||
|
private function auswahlLoeschen(array $ids): int
|
||||||
|
{
|
||||||
|
$anzahl = 0;
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
if (Depotbuchung::find((int)$id) === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Depotbuchung::loeschen((int)$id);
|
||||||
|
$anzahl++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $anzahl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function auswahlNimmtDieGeldbuchungenMit(): void
|
||||||
|
{
|
||||||
|
$ids = $this->vierBuchungen();
|
||||||
|
|
||||||
|
$geldIds = [];
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$geldIds[] = (int)Depotbuchung::find($id)['buchung_id'];
|
||||||
|
}
|
||||||
|
$this->assertEquals(4, count(array_filter($geldIds)), 'Jede Depotbuchung hat ihre Geldbuchung');
|
||||||
|
|
||||||
|
// Die beiden Käufe auswählen – der Rest bleibt stehen.
|
||||||
|
$this->assertEquals(2, $this->auswahlLoeschen([$ids[0], $ids[1]]), 'Zwei Buchungen gelöscht');
|
||||||
|
|
||||||
|
$this->assertNull(Depotbuchung::find($ids[0]), 'Der erste Kauf ist weg');
|
||||||
|
$this->assertNull(Depotbuchung::find($ids[1]), 'Der zweite Kauf auch');
|
||||||
|
$this->assertNotEmpty(Depotbuchung::find($ids[2]), 'Der Verkauf steht noch');
|
||||||
|
$this->assertNotEmpty(Depotbuchung::find($ids[3]), 'Die Dividende ebenso');
|
||||||
|
|
||||||
|
$this->assertNull(Buchung::find($geldIds[0]), 'Die Geldbuchung des ersten Kaufs ging mit');
|
||||||
|
$this->assertNull(Buchung::find($geldIds[1]), 'Die des zweiten Kaufs ebenso');
|
||||||
|
$this->assertNotEmpty(Buchung::find($geldIds[2]), 'Die des Verkaufs blieb unberührt');
|
||||||
|
|
||||||
|
// 10000 − 1010 − 1210 + 715 + 36,81; nach dem Löschen der Käufe fehlen
|
||||||
|
// deren Belastungen wieder.
|
||||||
|
$this->assertEquals(
|
||||||
|
10000.0 + 715.0 + 36.81,
|
||||||
|
Konto::saldo($this->verrechnung, '2026-12-31'),
|
||||||
|
'Das Verrechnungskonto trägt nur noch Verkauf und Dividende'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Aufräumen für den nächsten Abschnitt.
|
||||||
|
$this->auswahlLoeschen([$ids[2], $ids[3]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bestandGibtDieLoeschungWiederHer(): void
|
||||||
|
{
|
||||||
|
Wertpapier::kursSetzen($this->papier, '2026-06-30', 130.0, 'test');
|
||||||
|
|
||||||
|
$leer = Depot::bestand($this->depot, '2026-12-31');
|
||||||
|
$this->assertEquals(0, count($leer['positionen']), 'Vorher ist das Depot leer');
|
||||||
|
|
||||||
|
$ids = $this->vierBuchungen();
|
||||||
|
|
||||||
|
$voll = Depot::bestand($this->depot, '2026-12-31');
|
||||||
|
$this->assertEquals(15.0, $voll['positionen'][0]['stueck'], 'Fünfzehn Stück nach zwei Käufen und einem Verkauf');
|
||||||
|
$this->assertGreaterThan(0.0, $voll['realisiert'], 'Der Verkauf hat etwas realisiert');
|
||||||
|
$this->assertEquals(36.81, $voll['ertraege'], 'Die Dividende zählt als Ertrag');
|
||||||
|
|
||||||
|
// Verkauf und Dividende zurücknehmen: Der Bestand steht wieder auf den
|
||||||
|
// beiden Käufen, realisierter Gewinn und Erträge fallen auf null.
|
||||||
|
$this->assertEquals(2, $this->auswahlLoeschen([$ids[2], $ids[3]]), 'Zwei Buchungen gelöscht');
|
||||||
|
|
||||||
|
$nachher = Depot::bestand($this->depot, '2026-12-31');
|
||||||
|
$this->assertEquals(20.0, $nachher['positionen'][0]['stueck'], 'Die verkauften Stücke sind zurück');
|
||||||
|
$this->assertEquals(2220.0, $nachher['positionen'][0]['einstand'], 'Und der volle Einstand beider Käufe');
|
||||||
|
$this->assertEquals(2600.0, $nachher['wert'], 'Zwanzig Stück zu 130 €');
|
||||||
|
$this->assertEquals(0.0, $nachher['realisiert'], 'Ohne Verkauf ist nichts realisiert');
|
||||||
|
$this->assertEquals(0.0, $nachher['ertraege'], 'Ohne Dividende gibt es keinen Ertrag');
|
||||||
|
|
||||||
|
$veraeusserungen = Depot::veraeusserungen('2026-01-01', '2026-12-31', $this->depot);
|
||||||
|
$this->assertEquals(0, count($veraeusserungen['geschaefte']), 'Kein Veräußerungsgeschäft mehr');
|
||||||
|
$this->assertEquals(0.0, $veraeusserungen['gewinn'], 'Und kein Gewinn in der Auswertung');
|
||||||
|
|
||||||
|
$this->auswahlLoeschen([$ids[0], $ids[1]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function unbekannteNummernBleibenFolgenlos(): void
|
||||||
|
{
|
||||||
|
$id = $this->vierBuchungen()[0];
|
||||||
|
|
||||||
|
// Eine Nummer, die es nicht (mehr) gibt, wird übergangen – der Rest der
|
||||||
|
// Auswahl verschwindet trotzdem.
|
||||||
|
$this->assertEquals(1, $this->auswahlLoeschen([999999, $id]), 'Nur die vorhandene Buchung zählt');
|
||||||
|
$this->assertNull(Depotbuchung::find($id), 'Und sie ist gelöscht');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -170,6 +170,87 @@ test.describe('Module der Phasen 4 bis 7', () => {
|
||||||
await expect(page.locator('.meldung')).toContainText('gelöscht')
|
await expect(page.locator('.meldung')).toContainText('gelöscht')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Depotbuchungen lassen sich über die Auswahl sammelweise löschen', async ({ page }) => {
|
||||||
|
const marke = Date.now().toString().slice(-6)
|
||||||
|
const depot = `E2E Depot ${marke}`
|
||||||
|
const papier = `E2E Papier ${marke}`
|
||||||
|
|
||||||
|
await page.goto('/konten/neu')
|
||||||
|
await page.fill('#name', depot)
|
||||||
|
await page.selectOption('#kontoart', 'depot')
|
||||||
|
await page.click('.formular button[type="submit"]')
|
||||||
|
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||||||
|
|
||||||
|
// Zwei Käufe desselben Papiers – der zweite über die Auswahlliste, damit
|
||||||
|
// kein zweites Wertpapier entsteht.
|
||||||
|
for (const [nr, stueck] of [[1, '10'], [2, '5']]) {
|
||||||
|
await page.goto('/wertpapiere/buchungen/neu')
|
||||||
|
await page.selectOption('#konto_id', { label: depot })
|
||||||
|
await page.fill('#datum', '2026-05-0' + nr)
|
||||||
|
if (nr === 1) {
|
||||||
|
await page.fill('#wertpapier_name', papier)
|
||||||
|
} else {
|
||||||
|
await page.selectOption('#wertpapier_id', { label: papier })
|
||||||
|
}
|
||||||
|
await page.fill('#stueck', stueck)
|
||||||
|
await page.fill('#kurs', '100')
|
||||||
|
await page.click('.formular button[type="submit"]')
|
||||||
|
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Der Filter bleibt über das Löschen hinweg stehen – das Formularziel
|
||||||
|
// trägt ihn mit.
|
||||||
|
await page.goto('/wertpapiere/buchungen')
|
||||||
|
await page.selectOption('select[name="depot"]', { label: depot })
|
||||||
|
await page.click('button:has-text("Filtern")')
|
||||||
|
await expect(page.locator('tbody tr')).toHaveCount(2)
|
||||||
|
|
||||||
|
// Der Bestand kennt beide Käufe.
|
||||||
|
await page.goto('/wertpapiere')
|
||||||
|
await page.selectOption('select[name="depot"]', { label: depot })
|
||||||
|
await page.click('button:has-text("Anzeigen")')
|
||||||
|
await expect(page.locator('tbody tr', { hasText: papier })).toContainText('15')
|
||||||
|
|
||||||
|
await page.goBack()
|
||||||
|
await page.goBack()
|
||||||
|
|
||||||
|
// Ohne Auswahl bleibt der Knopf gesperrt.
|
||||||
|
const knopf = page.locator('#depotbuchungsauswahl button')
|
||||||
|
await expect(knopf).toBeDisabled()
|
||||||
|
|
||||||
|
await page.check('.alle-waehlen')
|
||||||
|
await expect(knopf).toContainText('(2)')
|
||||||
|
|
||||||
|
const gefragt = []
|
||||||
|
page.on('dialog', async (dialog) => {
|
||||||
|
gefragt.push(dialog.message())
|
||||||
|
await dialog.accept()
|
||||||
|
})
|
||||||
|
|
||||||
|
await knopf.click()
|
||||||
|
|
||||||
|
expect(gefragt.join(' ')).toContain('löschen')
|
||||||
|
await expect(page.locator('.meldung-erfolg')).toContainText('gelöscht')
|
||||||
|
await expect(page).toHaveURL(/depot=/)
|
||||||
|
// Der Name steht weiter im Filterfeld – gemeint ist die Liste.
|
||||||
|
await expect(page.locator('table.liste')).toHaveCount(0)
|
||||||
|
await expect(page.locator('.platzhalter')).toContainText('Keine Depotbuchungen')
|
||||||
|
|
||||||
|
// Und der Bestand ist die Buchungen los – er wird gerechnet.
|
||||||
|
await page.goto('/wertpapiere')
|
||||||
|
await page.selectOption('select[name="depot"]', { label: depot })
|
||||||
|
await page.click('button:has-text("Anzeigen")')
|
||||||
|
await expect(page.locator('.platzhalter')).toContainText('Keine Bestände')
|
||||||
|
|
||||||
|
// Aufräumen: Das Testdepot wieder entfernen. Die Rückfrage übernimmt der
|
||||||
|
// Dialoghandler von oben.
|
||||||
|
await page.goto('/konten')
|
||||||
|
const pfad = await page.locator('a', { hasText: depot }).first().getAttribute('href')
|
||||||
|
await page.goto(pfad + '/bearbeiten')
|
||||||
|
await page.locator('button:has-text("Konto löschen")').click()
|
||||||
|
await expect(page.locator('.meldung')).toContainText('gelöscht')
|
||||||
|
})
|
||||||
|
|
||||||
test('Der Finanzkalender ist erreichbar und lässt sich einstellen', async ({ page }) => {
|
test('Der Finanzkalender ist erreichbar und lässt sich einstellen', async ({ page }) => {
|
||||||
await page.goto('/termine/kalender')
|
await page.goto('/termine/kalender')
|
||||||
await expect(page.locator('h1')).toContainText('Finanzkalender')
|
await expect(page.locator('h1')).toContainText('Finanzkalender')
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue