@@ -20,6 +20,7 @@ use Finanzmanager\View;
| Buchungstext |
Kategorie |
Klasse |
+ ✓ |
Bar |
@@ -52,6 +53,7 @@ use Finanzmanager\View;
= View::h($buchung['klasse']) ?> |
+ = $buchung['abgeglichen'] ? '✓' : '' ?> |
= View::bargeldMarke($buchung['bargeld_status'], $buchung['bargeld_hinweis'], !empty($buchung['ohne_bargeld_abgleich'])) ?> |
@@ -89,6 +91,9 @@ use Finanzmanager\View;
dem Summenbericht überein.
Der Empfänger führt in die Buchungsmaske.
+
+ Den Haken „abgeglichen“ setzt die Buchungsmaske oder der Abgleich im Kontoblatt.
+
Zu diesen Buchungen wurde im Bargeld-Tool keine Ausgabe mit demselben Betrag gefunden.
diff --git a/tests/14_AuswertungTest.php b/tests/14_AuswertungTest.php
index 0f6aec9..6ca6bfe 100644
--- a/tests/14_AuswertungTest.php
+++ b/tests/14_AuswertungTest.php
@@ -53,6 +53,7 @@ class AuswertungTest extends TestCase
$this->buchungsbericht();
$this->buchungsberichtBargeld();
+ $this->buchungsberichtAbgeglichen();
$this->vergleichsbericht();
$this->kontostandsbericht();
$this->budgetbericht();
@@ -505,6 +506,42 @@ class AuswertungTest extends TestCase
Einstellung::set('bargeld_url', (string)$vorher);
}
+ /** Spalte und Filter „abgeglichen“ im Buchungsbericht (#137). */
+ private function buchungsberichtAbgeglichen(): void
+ {
+ Database::execute(
+ "UPDATE buchungen SET abgeglichen = CASE WHEN empfaenger IN ('Edeka', 'Rewe') THEN 1 ELSE 0 END
+ WHERE konto_id = ?",
+ [$this->giro]
+ );
+
+ $alle = $this->bericht(['typ' => 'buchungsbericht', 'zeilensumme' => 'konto']);
+ $markiert = [];
+ foreach ($alle['gruppen'][0]['buchungen'] as $buchung) {
+ $markiert[$buchung['empfaenger']] = $buchung['abgeglichen'];
+ }
+ $this->assert($markiert['Edeka'] && $markiert['Rewe'], 'Abgeglichene Buchungen tragen den Haken');
+ $this->assert(!$markiert['Kino'], 'Nicht abgeglichene Buchungen tragen keinen');
+
+ $ja = $this->bericht(['typ' => 'buchungsbericht', 'zeilensumme' => 'konto', 'abgeglichen' => 'ja']);
+ $this->assertEquals(2, $ja['anzahl'], 'Der Filter „abgeglichen“ lässt Edeka und die Splitbuchung übrig');
+
+ $nein = $this->bericht(['typ' => 'buchungsbericht', 'zeilensumme' => 'konto', 'abgeglichen' => 'nein']);
+ $this->assertEquals($alle['anzahl'] - 2, $nein['anzahl'], 'Der Filter „nicht abgeglichen“ lässt beide weg');
+
+ // Splitzeilen erben den Haken ihrer Buchung.
+ $nachKategorie = $this->bericht(['typ' => 'buchungsbericht', 'abgeglichen' => 'ja']);
+ $this->assertEquals(3, $nachKategorie['anzahl'], 'Nach Kategorie: Edeka und beide Splitzeilen');
+
+ $unbekannt = $this->bericht(['typ' => 'buchungsbericht', 'abgeglichen' => 'vielleicht']);
+ $this->assertEquals('', $unbekannt['filter']['abgeglichen'], 'Ein unbekannter Wert filtert nicht');
+
+ $summe = $this->bericht(['typ' => 'summenbericht', 'abgeglichen' => 'nein']);
+ $this->assertEquals('', $summe['filter']['abgeglichen'], 'Außerhalb des Buchungsberichts wirkt der Filter nicht');
+
+ Database::execute('UPDATE buchungen SET abgeglichen = 0 WHERE konto_id = ?', [$this->giro]);
+ }
+
private function buchungsbericht(): void
{
$bericht = $this->bericht(['typ' => 'buchungsbericht', 'zeilensumme' => 'konto']);
diff --git a/tests/15_CockpitTest.php b/tests/15_CockpitTest.php
index 06d76f2..eb3ae6b 100644
--- a/tests/15_CockpitTest.php
+++ b/tests/15_CockpitTest.php
@@ -37,6 +37,7 @@ class CockpitTest extends TestCase
$this->verteilungKuerztAufDieGrenze();
$this->offeneVorgaengeZaehlenWasWartet();
$this->bargeldFehlerErscheintAlsOffenerVorgang();
+ $this->nichtAbgeglichenFuehrtInDenBuchungsbericht();
$this->legendenFuehrenZuDenBuchungen();
$this->vorgangspruefungenKennenJedenVorgang();
}
@@ -325,6 +326,56 @@ class CockpitTest extends TestCase
Database::execute("UPDATE buchungen SET bargeld_status = 'fehler' WHERE id = ?", [$id]);
}
+ /**
+ * „Buchungen nicht abgeglichen“ öffnet den Buchungsbericht, gefiltert auf
+ * nicht abgeglichene Buchungen – und der zeigt genau die gezählten (#137).
+ */
+ private function nichtAbgeglichenFuehrtInDenBuchungsbericht(): void
+ {
+ $pruefen = function (string $fall): void {
+ $vorgang = null;
+ foreach (Cockpit::offeneVorgaenge(self::HEUTE) as $eintrag) {
+ if ($eintrag['schluessel'] === 'nicht_abgeglichen') {
+ $vorgang = $eintrag;
+ }
+ }
+ $this->assertNotEmpty($vorgang, $fall . ': Der Vorgang erscheint');
+
+ $this->assert(str_starts_with($vorgang['pfad'], '/auswertungen/bericht?'), $fall . ': Der Weg führt in den Buchungsbericht');
+ parse_str((string)parse_url($vorgang['pfad'], PHP_URL_QUERY), $filter);
+ $this->assertEquals('buchungsbericht', $filter['typ'] ?? '', $fall . ': Typ Buchungsbericht');
+ $this->assertEquals('nein', $filter['abgeglichen'] ?? '', $fall . ': Gefiltert auf nicht abgeglichen');
+
+ $bericht = Auswertung::bericht($filter, self::HEUTE);
+ $ids = [];
+ foreach ($bericht['gruppen'] as $gruppe) {
+ foreach ($gruppe['buchungen'] as $buchung) {
+ $this->assert(!$buchung['abgeglichen'], $fall . ': Nur nicht abgeglichene Buchungen');
+ $ids[$buchung['buchung_id']] = true;
+ }
+ }
+ $this->assertEquals($vorgang['anzahl'], count($ids), $fall . ': Der Bericht zeigt genau die gezählten Buchungen');
+ };
+
+ // Eine künftige Buchung zählt der Vorgang nicht – der Bericht auch nicht.
+ Buchung::anlegen([
+ 'konto_id' => $this->giro, 'datum' => '2026-12-24', 'betrag' => -40,
+ 'empfaenger' => 'Zukunft', 'kategorie_id' => $this->freizeit,
+ ]);
+ $pruefen('Aktive Konten');
+
+ // Ein aufgelöstes Konto zählt nicht mit – der Bericht beschränkt sich
+ // dann auf die aktiven Konten.
+ $alt = Konto::anlegen([
+ 'name' => 'Cockpitaltkonto', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 0,
+ ]);
+ Buchung::anlegen([
+ 'konto_id' => $alt, 'datum' => '2026-08-01', 'betrag' => -5,
+ 'empfaenger' => 'Altlast', 'kategorie_id' => $this->freizeit,
+ ]);
+ $pruefen('Mit aufgelöstem Konto');
+ }
+
/**
* Jeder Legendeneintrag führt zu einer Buchungsliste, deren Summe genau
* seinen Wert trifft – auch „Nicht kategorisiert“ und das Sammelsegment.
diff --git a/tests/e2e/specs/auswertungen.spec.js b/tests/e2e/specs/auswertungen.spec.js
index 769993f..7e4d1c2 100644
--- a/tests/e2e/specs/auswertungen.spec.js
+++ b/tests/e2e/specs/auswertungen.spec.js
@@ -67,6 +67,27 @@ test.describe('Auswertungen', () => {
await expect(page.locator('thead th', { hasText: 'Saldo' })).toHaveCount(1)
})
+ test('Buchungsbericht führt die Spalte „abgeglichen“ und filtert danach (#137)', async ({ page }) => {
+ await vorlage(page, 'buchungsbericht')
+ await expect(page.locator('table.bericht thead th.abgleichspalte')).toHaveCount(1)
+
+ await page.selectOption('select[name="abgeglichen"]', 'nein')
+ await page.click('button:has-text("Anzeigen")')
+ await expect(page).toHaveURL(/abgeglichen=nein/)
+ await expect(page.locator('select[name="abgeglichen"]')).toHaveValue('nein')
+ await expect(page.locator('table.bericht tbody td.abgleichspalte', { hasText: '✓' })).toHaveCount(0)
+ })
+
+ test('Cockpit: „nicht abgeglichen“ öffnet den gefilterten Buchungsbericht (#137)', async ({ page }) => {
+ await page.goto('/')
+ const eintrag = page.locator('.vorgangsliste a', { hasText: 'nicht abgeglichen' })
+ test.skip(await eintrag.count() === 0, 'Keine nicht abgeglichenen Buchungen auf dieser Instanz')
+
+ await eintrag.first().click()
+ await expect(page).toHaveURL(/typ=buchungsbericht/)
+ await expect(page.locator('select[name="abgeglichen"]')).toHaveValue('nein')
+ })
+
test('Spalten-Aufteilung macht aus einer Spalte zwölf Monate', async ({ page }) => {
await vorlage(page, 'summenbericht')
await expect(page.locator('table.bericht thead th')).toHaveCount(2)