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:
parent
1dbc848cd1
commit
92e15d54c1
5 changed files with 137 additions and 18 deletions
16
Changes.md
16
Changes.md
|
|
@ -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**
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue