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:
parent
da38b931e5
commit
1a464b5b17
5 changed files with 168 additions and 1 deletions
|
|
@ -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`).
|
||||||
|
|
|
||||||
14
migrations/023_buchung_regelmaessig.sql
Normal file
14
migrations/023_buchung_regelmaessig.sql
Normal 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;
|
||||||
|
|
@ -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']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
99
tests/52_RegelmaessigeLoeschenTest.php
Normal file
99
tests/52_RegelmaessigeLoeschenTest.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue