Fix: Σ-Kennzeichen ließ sich beim Konto nicht abschalten

Ein nicht angehaktes Ankreuzfeld steht nicht im POST. KontenController::
formulardaten() reichte "in_summe" gar nicht durch, also griff im Model der
Standardwert "zählt zur Summe" – die Anwendung meldete "Konto gespeichert",
geändert hatte sich nichts.

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 bleibt für Aufrufe ohne den Schlüssel bestehen – der QIF-Import
gibt das Feld nicht an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
fds2610-bot 2026-08-10 17:10:59 +02:00
parent 1dbc848cd1
commit 92e15d54c1
5 changed files with 137 additions and 18 deletions

View file

@ -1,5 +1,21 @@
# Änderungen # Ä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) ## Phase 2 – Buchungen, Kontoblatt und Buchungsimport (10.08.2026)
**Erkenntnis aus dem Vollexport** **Erkenntnis aus dem Vollexport**

View file

@ -44,7 +44,7 @@ class KontenController extends BaseController
$this->verlangeAnmeldung(); $this->verlangeAnmeldung();
try { try {
$id = Konto::anlegen($this->formulardaten()); $id = Konto::anlegen(self::formulardaten($_POST));
} catch (\Throwable $e) { } catch (\Throwable $e) {
View::redirect('/konten/neu', $e->getMessage(), 'fehler'); View::redirect('/konten/neu', $e->getMessage(), 'fehler');
return; return;
@ -83,7 +83,7 @@ class KontenController extends BaseController
} }
try { try {
Konto::speichern($id, $this->formulardaten()); Konto::speichern($id, self::formulardaten($_POST));
} catch (\Throwable $e) { } catch (\Throwable $e) {
View::redirect('/konten/' . $id . '/bearbeiten', $e->getMessage(), 'fehler'); View::redirect('/konten/' . $id . '/bearbeiten', $e->getMessage(), 'fehler');
return; return;
@ -107,23 +107,40 @@ class KontenController extends BaseController
View::redirect('/konten', 'Konto „' . $konto['name'] . '“ gelöscht.'); 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 [ return [
'name' => $this->post('name'), 'name' => $feld('name'),
'kontoart' => $this->post('kontoart', 'bank'), 'kontoart' => $feld('kontoart', 'bank'),
'kontengruppe_id' => $this->post('kontengruppe_id'), 'kontengruppe_id' => $feld('kontengruppe_id'),
'bankname' => $this->post('bankname'), 'bankname' => $feld('bankname'),
'kontonummer' => $this->post('kontonummer'), 'kontonummer' => $feld('kontonummer'),
'iban' => $this->post('iban'), 'iban' => $feld('iban'),
'bic' => $this->post('bic'), 'bic' => $feld('bic'),
'beschreibung' => $this->post('beschreibung'), 'beschreibung' => $feld('beschreibung'),
'anfangssaldo' => self::zahl($this->post('anfangssaldo', '0')), 'anfangssaldo' => self::zahl($feld('anfangssaldo', '0')),
'kreditlimit' => $this->post('kreditlimit') === '' ? null : self::zahl($this->post('kreditlimit')), 'kreditlimit' => $feld('kreditlimit') === '' ? null : self::zahl($feld('kreditlimit')),
'waehrung' => $this->post('waehrung', 'EUR'), 'waehrung' => $feld('waehrung', 'EUR'),
'aktiv' => isset($_POST['aktiv']), 'aktiv' => isset($post['aktiv']),
'sortierung' => (int)$this->post('sortierung', '0'), 'in_summe' => isset($post['in_summe']),
'sortierung' => (int)$feld('sortierung', '0'),
]; ];
} }

View file

@ -277,7 +277,10 @@ class Konto
'kreditlimit' => $limit, 'kreditlimit' => $limit,
'waehrung' => strtoupper(trim((string)($daten['waehrung'] ?? 'EUR'))) ?: 'EUR', 'waehrung' => strtoupper(trim((string)($daten['waehrung'] ?? 'EUR'))) ?: 'EUR',
'aktiv' => !empty($daten['aktiv']) ? 1 : 0, '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, 'in_summe' => array_key_exists('in_summe', $daten) && empty($daten['in_summe']) ? 0 : 1,
'sortierung' => (int)($daten['sortierung'] ?? 0), 'sortierung' => (int)($daten['sortierung'] ?? 0),
]; ];

View file

@ -18,6 +18,7 @@ class KontenTest extends TestCase
$this->kontoarten(); $this->kontoarten();
$this->uebersichtGruppiertUndSummiert(); $this->uebersichtGruppiertUndSummiert();
$this->betragsEingabe(); $this->betragsEingabe();
$this->ankreuzfelderAusDemFormular();
} }
private function anlegenUndFinden(): void private function anlegenUndFinden(): void
@ -155,4 +156,52 @@ class KontenTest extends TestCase
$this->assertEquals(0.0, KontenController::zahl(''), 'Leere Eingabe ergibt null'); $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'); $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') 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 }) => { test('Kategorie mit Unterkategorie anlegen', async ({ page }) => {
const oben = `E2E Wohnen ${MARKE}` const oben = `E2E Wohnen ${MARKE}`
const unten = `Strom ${MARKE}` const unten = `Strom ${MARKE}`