Merge pull request 'Fix: Σ-Kennzeichen ließ sich beim Konto nicht abschalten' (#4) from fds2610-bot/Finanzmanager:main into main

Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/4
This commit is contained in:
fds2610 2026-08-10 17:14:03 +02:00
commit bfef887793
5 changed files with 137 additions and 18 deletions

View file

@ -1,5 +1,21 @@
# Änderungen
## Korrektur – Σ-Kennzeichen ließ sich nicht abschalten (10.08.2026)
Das Kennzeichen *Zählt zur Summe der Kontostände* blieb beim Speichern eines Kontos
unverändert stehen, obwohl die Anwendung „Konto gespeichert“ meldete.
- Ursache: `KontenController::formulardaten()` reichte `in_summe` gar nicht durch. Ein
nicht angehaktes Ankreuzfeld steht nicht im POST, also griff im Model der Standardwert
„zählt mit“ – gespeichert wurde ein Ja, das niemand angegeben hatte.
- `formulardaten()` nimmt das POST-Array jetzt als Parameter und ist damit ohne HTTP
prüfbar; die Ankreuzfelder werden ausdrücklich auf `true`/`false` abgebildet.
- Der Standard „zählt zur Summe“ bleibt für Aufrufe ohne den Schlüssel bestehen – der
QIF-Import gibt das Feld nicht an und soll es auch nicht.
- `07_KontenTest` prüft die Formularabbildung und den Weg durch das Model; E2E
`stammdaten.spec.js` hakt das Kennzeichen in der Oberfläche ab, setzt es wieder und
sieht in beiden Fällen nach, statt der Erfolgsmeldung zu glauben.
## Phase 2 – Buchungen, Kontoblatt und Buchungsimport (10.08.2026)
**Erkenntnis aus dem Vollexport**

View file

@ -44,7 +44,7 @@ class KontenController extends BaseController
$this->verlangeAnmeldung();
try {
$id = Konto::anlegen($this->formulardaten());
$id = Konto::anlegen(self::formulardaten($_POST));
} catch (\Throwable $e) {
View::redirect('/konten/neu', $e->getMessage(), 'fehler');
return;
@ -83,7 +83,7 @@ class KontenController extends BaseController
}
try {
Konto::speichern($id, $this->formulardaten());
Konto::speichern($id, self::formulardaten($_POST));
} catch (\Throwable $e) {
View::redirect('/konten/' . $id . '/bearbeiten', $e->getMessage(), 'fehler');
return;
@ -107,23 +107,40 @@ class KontenController extends BaseController
View::redirect('/konten', 'Konto „' . $konto['name'] . '“ gelöscht.');
}
/** @return array<string,mixed> */
private function formulardaten(): array
/**
* Bildet das Kontoformular auf die Modellfelder ab.
*
* Öffentlich und statisch, damit die Abbildung ohne HTTP prüfbar ist: Ein
* nicht angehaktes Ankreuzfeld steht überhaupt nicht im POST, es muss hier
* ausdrücklich zu `false` werden. Fehlt ein solches Feld in dieser Liste,
* lässt es sich in der Oberfläche nie wieder abschalten – die Meldung sagt
* „gespeichert“, der alte Wert bleibt stehen.
*
* @param array<string,mixed> $post
* @return array<string,mixed>
*/
public static function formulardaten(array $post): array
{
$feld = static function (string $name, string $standard = '') use ($post): string {
$wert = $post[$name] ?? $standard;
return is_string($wert) ? trim($wert) : $standard;
};
return [
'name' => $this->post('name'),
'kontoart' => $this->post('kontoart', 'bank'),
'kontengruppe_id' => $this->post('kontengruppe_id'),
'bankname' => $this->post('bankname'),
'kontonummer' => $this->post('kontonummer'),
'iban' => $this->post('iban'),
'bic' => $this->post('bic'),
'beschreibung' => $this->post('beschreibung'),
'anfangssaldo' => self::zahl($this->post('anfangssaldo', '0')),
'kreditlimit' => $this->post('kreditlimit') === '' ? null : self::zahl($this->post('kreditlimit')),
'waehrung' => $this->post('waehrung', 'EUR'),
'aktiv' => isset($_POST['aktiv']),
'sortierung' => (int)$this->post('sortierung', '0'),
'name' => $feld('name'),
'kontoart' => $feld('kontoart', 'bank'),
'kontengruppe_id' => $feld('kontengruppe_id'),
'bankname' => $feld('bankname'),
'kontonummer' => $feld('kontonummer'),
'iban' => $feld('iban'),
'bic' => $feld('bic'),
'beschreibung' => $feld('beschreibung'),
'anfangssaldo' => self::zahl($feld('anfangssaldo', '0')),
'kreditlimit' => $feld('kreditlimit') === '' ? null : self::zahl($feld('kreditlimit')),
'waehrung' => $feld('waehrung', 'EUR'),
'aktiv' => isset($post['aktiv']),
'in_summe' => isset($post['in_summe']),
'sortierung' => (int)$feld('sortierung', '0'),
];
}

View file

@ -277,7 +277,10 @@ class Konto
'kreditlimit' => $limit,
'waehrung' => strtoupper(trim((string)($daten['waehrung'] ?? 'EUR'))) ?: 'EUR',
'aktiv' => !empty($daten['aktiv']) ? 1 : 0,
// Standard ist "zählt zur Summe"; nur ein ausdrückliches Nein schaltet es ab.
// Standard ist "zählt zur Summe"; nur ein ausdrückliches Nein schaltet
// es ab. Der QIF-Import lässt das Feld deshalb weg. Wer aus einem
// Formular kommt, muss den Schlüssel dagegen immer mitgeben – siehe
// KontenController::formulardaten().
'in_summe' => array_key_exists('in_summe', $daten) && empty($daten['in_summe']) ? 0 : 1,
'sortierung' => (int)($daten['sortierung'] ?? 0),
];

View file

@ -18,6 +18,7 @@ class KontenTest extends TestCase
$this->kontoarten();
$this->uebersichtGruppiertUndSummiert();
$this->betragsEingabe();
$this->ankreuzfelderAusDemFormular();
}
private function anlegenUndFinden(): void
@ -155,4 +156,52 @@ class KontenTest extends TestCase
$this->assertEquals(0.0, KontenController::zahl(''), 'Leere Eingabe ergibt null');
$this->assertEquals(99.9, KontenController::zahl(' 99,90 € '), 'Währungszeichen und Leerzeichen stören nicht');
}
/**
* Ein nicht angehaktes Ankreuzfeld schickt der Browser gar nicht mit. Die
* Formularabbildung muss daraus ein ausdrückliches Nein machen, sonst lässt
* sich das Kennzeichen nie wieder abschalten – „Konto gespeichert“ steht
* dann da, geändert hat sich nichts.
*/
private function ankreuzfelderAusDemFormular(): void
{
$angehakt = KontenController::formulardaten([
'name' => 'Girokonto', 'kontoart' => 'bank', 'aktiv' => '1', 'in_summe' => '1',
]);
$this->assert($angehakt['aktiv'] === true, 'Angehaktes „aktiv“ kommt als Ja an');
$this->assert($angehakt['in_summe'] === true, 'Angehaktes Σ-Kennzeichen kommt als Ja an');
$leer = KontenController::formulardaten(['name' => 'Girokonto', 'kontoart' => 'bank']);
$this->assert($leer['aktiv'] === false, 'Fehlendes „aktiv“ heißt inaktiv');
$this->assert($leer['in_summe'] === false, 'Fehlendes Σ-Kennzeichen heißt: zählt nicht zur Summe');
// Und derselbe Weg durch das Model hindurch: Abhaken muss ankommen.
$id = Konto::anlegen(KontenController::formulardaten([
'name' => 'Auslagen Eltern', 'kontoart' => 'sonstiges',
'anfangssaldo' => '1.500,00', 'aktiv' => '1', 'in_summe' => '1',
]));
$this->assertEquals(1, (int)Konto::find($id)['in_summe'], 'Neues Konto zählt zunächst mit');
$this->assertEquals(1500.0, (float)Konto::find($id)['anfangssaldo'], 'Anfangssaldo aus dem Formular');
Konto::speichern($id, KontenController::formulardaten([
'name' => 'Auslagen Eltern', 'kontoart' => 'sonstiges',
'anfangssaldo' => '1.500,00', 'aktiv' => '1',
]));
$this->assertEquals(0, (int)Konto::find($id)['in_summe'], 'Σ-Kennzeichen lässt sich entfernen');
$uebersicht = Konto::uebersicht();
$gefunden = false;
foreach ($uebersicht['gruppen'] as $gruppe) {
foreach ($gruppe['konten'] as $konto) {
if ((int)$konto['id'] === $id) {
$gefunden = true;
}
}
}
$this->assert($gefunden, 'Das Konto bleibt in der Übersicht sichtbar');
// Der QIF-Import gibt das Feld nicht an – dort gilt weiter der Standard.
$importiert = Konto::anlegen(['name' => 'Aus dem Export', 'kontoart' => 'bank', 'aktiv' => 1]);
$this->assertEquals(1, (int)Konto::find($importiert)['in_summe'], 'Ohne Angabe zählt ein Konto mit');
}
}

View file

@ -55,6 +55,40 @@ test.describe('Stammdaten', () => {
await expect(page.locator('.summenzeile')).toContainText('Summe Kontostände')
})
// Ein nicht angehaktes Ankreuzfeld schickt der Browser gar nicht mit. Bleibt
// es serverseitig unbeachtet, meldet die Anwendung „Konto gespeichert“ und
// das Kennzeichen steht unverändert da.
test('Σ-Kennzeichen lässt sich abschalten und wieder setzen', async ({ page }) => {
const name = `E2E Auslagen ${MARKE}`
await page.goto('/konten/neu')
await page.fill('#name', name)
await page.selectOption('#kontoart', 'sonstiges')
await page.fill('#anfangssaldo', '500,00')
await page.click('.formular button[type="submit"]')
await expect(page.locator('.meldung-erfolg')).toBeVisible()
const kennzeichen = page.locator('input[name="in_summe"]')
await expect(kennzeichen).toBeChecked()
await kennzeichen.uncheck()
await page.locator('.formular button[type="submit"]').first().click()
await expect(page.locator('.meldung-erfolg')).toBeVisible()
// In der Übersicht bleibt das Konto sichtbar, aber als „ohne Σ“ markiert.
const zeile = page.locator('tr', { hasText: name })
await expect(zeile).toContainText('ohne Σ')
// Und im Formular ist das Häkchen tatsächlich weg – nicht nur die Meldung da.
await zeile.locator('a').first().click()
await page.click('a.tab[href$="/bearbeiten"]')
await expect(page.locator('input[name="in_summe"]')).not.toBeChecked()
await page.locator('input[name="in_summe"]').check()
await page.locator('.formular button[type="submit"]').first().click()
await expect(page.locator('tr', { hasText: name })).not.toContainText('ohne Σ')
})
test('Kategorie mit Unterkategorie anlegen', async ({ page }) => {
const oben = `E2E Wohnen ${MARKE}`
const unten = `Strom ${MARKE}`