Merge pull request 'Fix n.n.: Regelmäßige Buchungen – Buchen zum gewählten Datum' (#109) from fds2610-bot/Finanzmanager:main into main

Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/109
This commit is contained in:
fds2610 2026-10-01 10:59:27 +02:00
commit 18107959b9
6 changed files with 78 additions and 11 deletions

View file

@ -11,6 +11,10 @@
- Neue Buchungen ohne Nummer erhalten beim Anlegen die nächste laufende Nummer ihres Kontos (höchste rein numerische Nummer + 1, `Buchung::naechsteNummer()`). Vorgegebene Nummern (QIF, CSV, Formular) bleiben, bestehende Buchungen werden nicht verändert; die interne ID bleibt verborgen.
- Test: `53_BuchungsnummerTest.php`.
## Regelmäßige Buchungen: Buchen zum gewählten Datum (01.10.2026)
- „Buchen“ in „Jetzt fällig“ schlug mit einer Fehlermeldung fehl, weil das Formular ein leeres Datum schickte. Jetzt ist „Fällig am“ per Klick editierbar; „Buchen“ bucht zu diesem Datum. Die Änderung gilt nur für diese Buchung und verändert die Reihe nicht. Ein leeres Datum fällt auf den Termin zurück, ein ungültiges wird abgewiesen.
## „+ Neu“ öffnet das Formular als Dialog (01.10.2026)
- Das Formular zur Neuanlage stand in elf Ansichten unter der Liste. Jetzt öffnet der Knopf „+ Neu“ es als modalen Dialog (`app.js`, `.neudialog`); nach dem Speichern erscheint die Liste wieder, mit dem neuen Eintrag. Betroffen: Klassen, Buchungsvorlagen, Regelmäßige Buchungen, Erinnerungen, Verträge, Adressen, Sparziele, Kredite, Abgleichregeln, Budgets, Berichtsgruppen.

View file

@ -5,8 +5,8 @@
(function () {
'use strict';
const tabelle = document.querySelector('table.reihen');
if (!tabelle) {
const tabellen = document.querySelectorAll('table.reihen, table.faellig');
if (tabellen.length === 0) {
return;
}
@ -59,6 +59,7 @@
anzeige.hidden = false;
}
tabellen.forEach(function (tabelle) {
tabelle.querySelectorAll('.feld').forEach(function (feld) {
const anzeige = feld.querySelector('.anzeige');
const steuer = feld.querySelector('input, select');
@ -99,6 +100,7 @@
}
});
});
});
function geaendert(e) {
const zeile = e.target.closest('tr');
@ -107,6 +109,8 @@
knopf.hidden = false;
}
}
tabelle.addEventListener('input', geaendert);
tabelle.addEventListener('change', geaendert);
tabellen.forEach(function (tabelle) {
tabelle.addEventListener('input', geaendert);
tabelle.addEventListener('change', geaendert);
});
})();

View file

@ -169,7 +169,14 @@ class RegelmaessigeBuchung
throw new \RuntimeException('Die regelmäßige Buchung gibt es nicht.');
}
$datum = (string)($abweichungen['datum'] ?? $reihe['naechste_faelligkeit'] ?? date('Y-m-d'));
$datum = trim((string)($abweichungen['datum'] ?? ''));
if ($datum === '') {
$datum = (string)($reihe['naechste_faelligkeit'] ?? date('Y-m-d'));
}
$pruef = \DateTimeImmutable::createFromFormat('!Y-m-d', $datum);
if ($pruef === false || $pruef->format('Y-m-d') !== $datum) {
throw new \InvalidArgumentException('Ungültiges Datum.');
}
$betrag = array_key_exists('betrag', $abweichungen) && $abweichungen['betrag'] !== ''
? round((float)$abweichungen['betrag'], 2)
: (float)$reihe['betrag'];

View file

@ -42,7 +42,13 @@ include __DIR__ . '/../layout/kopf.php';
</div>
<h3>Jetzt fällig</h3>
<table class="liste">
<?php foreach ($faellig as $reihe): ?>
<form id="buchen-<?= (int)$reihe['id'] ?>" method="post"
action="/termine/<?= (int)$reihe['id'] ?>/buchen">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
</form>
<?php endforeach; ?>
<table class="liste faellig">
<thead>
<tr><th>Fällig am</th><th>Name</th><th>Konto</th><th class="rechts">Betrag</th>
<th>Art</th><th class="rechts">Aktion</th></tr>
@ -50,7 +56,14 @@ include __DIR__ . '/../layout/kopf.php';
<tbody>
<?php foreach ($faellig as $reihe): ?>
<tr<?= (string)$reihe['naechste_faelligkeit'] < date('Y-m-d') ? ' class="ueberfaellig"' : '' ?>>
<td><?= View::h(View::datum($reihe['naechste_faelligkeit'])) ?></td>
<td>
<div class="feld" data-datum>
<span class="anzeige"><?= View::h(View::datum($reihe['naechste_faelligkeit'])) ?></span>
<input type="date" form="buchen-<?= (int)$reihe['id'] ?>" name="datum" hidden
id="buchen-datum-<?= (int)$reihe['id'] ?>"
value="<?= View::h((string)$reihe['naechste_faelligkeit']) ?>">
</div>
</td>
<td><?= View::h($reihe['name']) ?></td>
<td><?= View::h($reihe['kontoname']) ?></td>
<td class="rechts <?= View::betragKlasse($reihe['betrag']) ?>">
@ -62,10 +75,8 @@ include __DIR__ . '/../layout/kopf.php';
: '<span class="marke">auf Bestätigung</span>' ?>
</td>
<td class="rechts nowrap">
<form method="post" action="/termine/<?= (int)$reihe['id'] ?>/buchen" class="inline">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<button type="submit" class="knopf klein primaer">Buchen</button>
</form>
<button type="submit" form="buchen-<?= (int)$reihe['id'] ?>"
id="buchen-knopf-<?= (int)$reihe['id'] ?>" class="knopf klein primaer">Buchen</button>
<form method="post" action="/termine/<?= (int)$reihe['id'] ?>/ueberspringen" class="inline">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<button type="submit" class="knopf klein">Überspringen</button>

View file

@ -30,6 +30,7 @@ class RegelmaessigeLoeschenTest extends TestCase
$this->manuelleBuchungBleibtUnberuehrt();
$this->buchungstextWirdUebernommen();
$this->naechsterTerminLaesstSichSetzen();
$this->buchenMitAbweichendemDatum();
}
private function reihe(string $name, string $beginn, ?string $ende = null): int
@ -125,4 +126,25 @@ class RegelmaessigeLoeschenTest extends TestCase
RegelmaessigeBuchung::speichern($id, ['naechste_faelligkeit' => 'Unsinn'] + $daten);
$this->assertEquals('2026-12-15', $this->faelligkeit($id), 'Ungültige Angaben werden ignoriert');
}
private function buchenMitAbweichendemDatum(): void
{
$id = $this->reihe('Datumsbuchung', '2026-10-01');
$b1 = RegelmaessigeBuchung::buchen($id, ['datum' => '', 'betrag' => '']);
$this->assertEquals('2026-10-01', (string)Buchung::find($b1)['datum'], 'Leeres Datum bucht zum Termin');
$id = $this->reihe('Datumsbuchung 2', '2026-10-01');
$termin = $this->faelligkeit($id);
$b2 = RegelmaessigeBuchung::buchen($id, ['datum' => '2026-10-05']);
$this->assertEquals('2026-10-05', (string)Buchung::find($b2)['datum'], 'Das gewählte Datum ist das Buchungsdatum');
$this->assertEquals($termin, (string)Buchung::find($b2)['faelligkeit'], 'Der Termin der Reihe bleibt der gespeicherte');
$fehler = false;
try {
RegelmaessigeBuchung::buchen($id, ['datum' => '31.02.2026']);
} catch (\InvalidArgumentException) {
$fehler = true;
}
$this->assert($fehler, 'Ein ungültiges Datum wird abgewiesen');
}
}

View file

@ -339,6 +339,25 @@ test.describe('Module der Phasen 4 bis 7', () => {
await expect(page.locator('table.reihen tr', { hasText: 'E2E-Reihe' })).toHaveCount(0)
})
test('Fälliges Datum lässt sich vor dem Buchen per Klick ändern', async ({ page }) => {
await page.goto('/termine')
await page.click('#neu-knopf')
await page.fill('#name', 'E2E-Fällig')
await page.fill('#neu_betrag', '-1,00')
await page.click('form[action="/termine/neu"] button.primaer')
const zeile = page.locator('table.faellig tr', { hasText: 'E2E-Fällig' })
const datum = zeile.locator('input[name="datum"]')
await expect(datum).toBeHidden()
await zeile.locator('.feld[data-datum] .anzeige').click()
await expect(datum).toBeVisible()
await expect(zeile.locator('button:has-text("Buchen")')).toBeVisible()
page.once('dialog', (dialog) => dialog.accept())
await page.locator('table.reihen tr', { hasText: 'E2E-Fällig' }).locator('button:has-text("Löschen")').click()
await expect(page.locator('table.reihen tr', { hasText: 'E2E-Fällig' })).toHaveCount(0)
})
test('Der Finanzkalender ist erreichbar und lässt sich einstellen', async ({ page }) => {
await page.goto('/termine/kalender')
await expect(page.locator('h1')).toContainText('Finanzkalender')