Fix n.n.: Fälligkeit folgt der Buchung – Löschen öffnet den Termin wieder

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
fds2610-bot 2026-10-01 09:07:07 +02:00
parent da38b931e5
commit 1a464b5b17
5 changed files with 168 additions and 1 deletions

View file

@ -1,5 +1,9 @@
# Änderungen # Änderungen
## Fälligkeit folgt der Buchung (01.10.2026)
- Eine per Fälligkeitslauf oder „Buchen“ erzeugte Buchung merkt sich jetzt ihre regelmäßige Buchung und den Soll-Termin (`buchungen.regelmaessige_id`, `faelligkeit`, Migration 023). Wird sie gelöscht, rückt die Fälligkeit auf diesen Termin zurück, eine ausgelaufene Reihe wird wieder aktiv, `zuletzt_am` folgt der jüngsten verbleibenden Buchung (`RegelmaessigeBuchung::terminWiederOeffnen()`). Bereits vorhandene Buchungen sind nicht verknüpft; bei ihnen bleibt es beim bisherigen Verhalten.
## Darkmode-Farben nach Mockup (01.10.2026) ## Darkmode-Farben nach Mockup (01.10.2026)
- Dunkle Palette bläulicher (Navy) statt grau-blau, Seitenleiste, Flächen, Ränder und Akzent angepasst. Der aktive Menüeintrag ist wie im hellen Modus blau hinterlegt (`--navi-aktiv`), der Balken links bleibt weiß (neue Variable `--navi-balken`). Nur Farben, nur im Darkmode (`app.css`). - Dunkle Palette bläulicher (Navy) statt grau-blau, Seitenleiste, Flächen, Ränder und Akzent angepasst. Der aktive Menüeintrag ist wie im hellen Modus blau hinterlegt (`--navi-aktiv`), der Balken links bleibt weiß (neue Variable `--navi-balken`). Nur Farben, nur im Darkmode (`app.css`).

View file

@ -0,0 +1,14 @@
-- Verknüpfung einer Buchung mit der regelmäßigen Buchung, aus der sie entstand.
--
-- Bisher war „naechste_faelligkeit“ ein reiner Zähler: Wurde eine per
-- Fälligkeitslauf erzeugte Buchung gelöscht, blieb der Termin als erledigt
-- stehen. „faelligkeit“ hält den Soll-Termin fest (nicht das Buchungsdatum, das
-- abweichen kann), damit Buchung::loeschen() die Fälligkeit zurücksetzen kann.
ALTER TABLE buchungen ADD COLUMN IF NOT EXISTS
regelmaessige_id INT UNSIGNED NULL AFTER quelle;
ALTER TABLE buchungen ADD COLUMN IF NOT EXISTS
faelligkeit DATE NULL AFTER regelmaessige_id;
ALTER TABLE buchungen ADD INDEX IF NOT EXISTS idx_buchung_regelmaessig (regelmaessige_id);
ALTER TABLE buchungen ADD CONSTRAINT fk_buchung_regelmaessig FOREIGN KEY IF NOT EXISTS (regelmaessige_id)
REFERENCES regelmaessige_buchungen (id) ON DELETE SET NULL;

View file

@ -322,7 +322,18 @@ class Buchung
// übersprungen. // übersprungen.
Bankumsatz::loeseVonBuchung($id); Bankumsatz::loeseVonBuchung($id);
Database::transaktion(static function () use ($id, $buchung): void {
Database::execute('DELETE FROM buchungen WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]); Database::execute('DELETE FROM buchungen WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]);
// Entstand die Buchung aus einer regelmäßigen Buchung, ist deren
// Termin wieder offen.
if ($buchung !== null && !empty($buchung['regelmaessige_id']) && !empty($buchung['faelligkeit'])) {
RegelmaessigeBuchung::terminWiederOeffnen(
(int)$buchung['regelmaessige_id'],
(string)$buchung['faelligkeit']
);
}
});
} }
/** /**

View file

@ -183,12 +183,51 @@ class RegelmaessigeBuchung
$splits $splits
); );
// Merken, aus welchem Termin die Buchung entstand – beim Löschen
// wird die Fälligkeit darüber wieder geöffnet.
Database::execute(
'UPDATE buchungen SET regelmaessige_id = ?, faelligkeit = ? WHERE id = ? AND benutzer_id = ?',
[$id, $reihe['naechste_faelligkeit'], $buchungId, Mandant::id()]
);
self::rueckeVor($id, (string)$reihe['naechste_faelligkeit'], $datum); self::rueckeVor($id, (string)$reihe['naechste_faelligkeit'], $datum);
return $buchungId; return $buchungId;
}); });
} }
/**
* Öffnet einen Termin wieder, dessen Buchung gelöscht wurde: Die Fälligkeit
* rückt auf diesen Termin zurück (falls er vor der aktuellen liegt), eine
* ausgelaufene Reihe wird wieder aktiv, solange der Termin im Zeitraum liegt.
* `zuletzt_am` folgt der jüngsten verbleibenden Buchung der Reihe.
* Aufzurufen, wenn die Buchung schon gelöscht ist.
*/
public static function terminWiederOeffnen(int $id, string $termin): void
{
$reihe = self::find($id);
if ($reihe === null || $termin === '') {
return;
}
$aktuell = (string)($reihe['naechste_faelligkeit'] ?? '');
$neu = ($aktuell === '' || $termin < $aktuell) ? $termin : $aktuell;
$ende = (string)($reihe['ende'] ?? '');
$aktiv = ($ende === '' || $neu <= $ende) ? 1 : (int)$reihe['aktiv'];
$zuletzt = Database::fetchValue(
'SELECT MAX(datum) FROM buchungen WHERE regelmaessige_id = ? AND benutzer_id = ?',
[$id, Mandant::id()]
);
Database::execute(
'UPDATE regelmaessige_buchungen
SET naechste_faelligkeit = ?, zuletzt_am = ?, aktiv = ?
WHERE id = ? AND benutzer_id = ?',
[$neu, $zuletzt !== null && $zuletzt !== false ? $zuletzt : null, $aktiv, $id, Mandant::id()]
);
}
/** /**
* Überspringt einen Termin, ohne zu buchen – etwa wenn die Miete einmal * Überspringt einen Termin, ohne zu buchen – etwa wenn die Miete einmal
* ausgesetzt wurde. * ausgesetzt wurde.

View file

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
use Finanzmanager\Models\Buchung;
use Finanzmanager\Models\Konto;
use Finanzmanager\Models\RegelmaessigeBuchung;
/**
* Wird eine Buchung gelöscht, die aus einer regelmäßigen Buchung entstand,
* ist deren Termin wieder offen: die Fälligkeit rückt zurück.
*/
class RegelmaessigeLoeschenTest extends TestCase
{
protected string $testName = 'Test 52: Fälligkeit nach Löschen der Buchung';
private int $giro = 0;
public function run(): void
{
$this->mandantAnlegen();
$this->giro = Konto::anlegen([
'name' => 'Fälligkeitsgiro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
]);
$this->loeschenOeffnetTerminWieder();
$this->aeltereVonZweienGeloescht();
$this->beendeteReiheWirdAktiv();
$this->manuelleBuchungBleibtUnberuehrt();
}
private function reihe(string $name, string $beginn, ?string $ende = null): int
{
return RegelmaessigeBuchung::anlegen([
'name' => $name, 'konto_id' => $this->giro, 'betrag' => -10,
'intervall' => 'monatlich', 'beginn' => $beginn, 'ende' => $ende, 'aktiv' => 1,
]);
}
private function faelligkeit(int $id): string
{
return (string)RegelmaessigeBuchung::find($id)['naechste_faelligkeit'];
}
private function loeschenOeffnetTerminWieder(): void
{
$id = $this->reihe('Löschtest', '2026-06-01');
$buchung = RegelmaessigeBuchung::buchen($id);
$this->assertEquals($id, (int)Buchung::find($buchung)['regelmaessige_id'], 'Die Buchung kennt ihre Reihe');
$this->assertEquals('2026-06-01', (string)Buchung::find($buchung)['faelligkeit'], 'Und ihren Termin');
$this->assert($this->faelligkeit($id) > '2026-06-01', 'Nach dem Buchen ist der Termin fortgeschrieben');
Buchung::loeschen($buchung);
$this->assertEquals('2026-06-01', $this->faelligkeit($id), 'Nach dem Löschen steht der Termin wieder an');
$this->assertNull(RegelmaessigeBuchung::find($id)['zuletzt_am'], 'Ohne verbleibende Buchung kein letztes Datum');
}
private function aeltereVonZweienGeloescht(): void
{
$id = $this->reihe('Zwei Termine', '2026-03-01');
$erste = RegelmaessigeBuchung::buchen($id);
$zweite = RegelmaessigeBuchung::buchen($id);
Buchung::loeschen($erste);
$this->assertEquals('2026-03-01', $this->faelligkeit($id), 'Es gilt der älteste offene Termin');
$this->assertEquals(
(string)Buchung::find($zweite)['datum'],
(string)RegelmaessigeBuchung::find($id)['zuletzt_am'],
'zuletzt_am folgt der verbleibenden Buchung'
);
}
private function beendeteReiheWirdAktiv(): void
{
$id = $this->reihe('Befristet', '2026-07-01', '2026-07-31');
$buchung = RegelmaessigeBuchung::buchen($id);
$this->assertEquals(0, (int)RegelmaessigeBuchung::find($id)['aktiv'], 'Die Reihe ist ausgelaufen');
Buchung::loeschen($buchung);
$this->assertEquals(1, (int)RegelmaessigeBuchung::find($id)['aktiv'], 'Das Löschen öffnet sie wieder');
$this->assertEquals('2026-07-01', $this->faelligkeit($id), 'Mit dem gelöschten Termin');
}
private function manuelleBuchungBleibtUnberuehrt(): void
{
$id = $this->reihe('Unbeteiligt', '2026-09-01');
$vorher = $this->faelligkeit($id);
$manuell = Buchung::anlegen(['konto_id' => $this->giro, 'datum' => '2026-08-01', 'betrag' => -5]);
Buchung::loeschen($manuell);
$this->assertEquals($vorher, $this->faelligkeit($id), 'Eine freie Buchung ändert keine Fälligkeit');
}
}