Merge pull request 'Fix #102: Offener Vorgang für fehlgeschlagene Bargeld-Tool-Prüfungen' (#103) from fds2610-bot/Finanzmanager:main into main

Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/103
This commit is contained in:
fds2610 2026-09-30 17:46:39 +02:00
commit 66042dc99f
10 changed files with 248 additions and 5 deletions

View file

@ -1,5 +1,14 @@
# Änderungen # Änderungen
## Offener Vorgang für fehlgeschlagene Bargeld-Tool-Prüfungen (30.09.2026)
- Die Kachel „Offene Vorgänge“ im Cockpit zählt jetzt auch Buchungen, deren letzter Abgleich
mit dem Bargeld-Tool an einem Abrufproblem gescheitert ist (`bargeld_status = 'fehler'`) –
nicht zu verwechseln mit „im Bargeld-Tool nicht gefunden“. Ein Klick führt auf die neue
Liste unter Verwaltung → Bargeld-Tool → fehlgeschlagene Prüfungen
(`/verwaltung/bargeld/fehler`), die diese Buchungen kontoübergreifend mit Konto, Betrag und
Fehlermeldung auflistet (`Buchung::mitBargeldFehler()`). Fix #102.
## phpFinTS auf v4.1.6 (30.09.2026) ## phpFinTS auf v4.1.6 (30.09.2026)
- Fork neu auf Upstream vom 22.09.2026 aufgebaut (psr/log und Depotaufstellung v6, #598, bleiben - Fork neu auf Upstream vom 22.09.2026 aufgebaut (psr/log und Depotaufstellung v6, #598, bleiben

View file

@ -271,6 +271,12 @@ treffen kann. Der Tooltip nennt den Treffer bzw. die Meldung der Gegenstelle. Is
Adresse hinterlegt, entfällt die Spalte ganz; sie zeigte sonst in jeder Zeile denselben Adresse hinterlegt, entfällt die Spalte ganz; sie zeigte sonst in jeder Zeile denselben
grauen Kreis. grauen Kreis.
Buchungen mit dem Status `fehler` zählen im Cockpit als offener Vorgang „Bargeld-Tool-Prüfung
fehlgeschlagen“ (siehe `docs/cockpit.md`) – ein Klick führt auf
`/verwaltung/bargeld/fehler`, die kontoübergreifende Liste dieser Buchungen
(`Buchung::mitBargeldFehler()`). Der Status `fehlt` erscheint dort bewusst nicht: Er ist ein
Ergebnis, kein offenes Problem – eine erneute Zuordnung fragt einfach wieder ohne Erfolg.
**Ein Fehlschlag ist ein Ergebnis, kein Abbruch.** Weder ein abgeschaltetes Bargeld-Tool **Ein Fehlschlag ist ein Ergebnis, kein Abbruch.** Weder ein abgeschaltetes Bargeld-Tool
noch ein Netzfehler darf verhindern, dass die Kategorie gespeichert wird; der Abruf bricht noch ein Netzfehler darf verhindern, dass die Kategorie gespeichert wird; der Abruf bricht
nach acht Sekunden ab. Null Treffer sind ausdrücklich kein Fehler – ein 404 dagegen schon, nach acht Sekunden ab. Null Treffer sind ausdrücklich kein Fehler – ein 404 dagegen schon,

View file

@ -177,6 +177,11 @@ Was auf Bearbeitung wartet, mit Zahl und Weg dorthin:
* **ohne Kategorie** – Buchungen ohne Kategorie, die auch keine Umbuchung und nicht * **ohne Kategorie** – Buchungen ohne Kategorie, die auch keine Umbuchung und nicht
gesplittet sind. Umbuchungen brauchen keine Kategorie und zählen deshalb nicht mit. gesplittet sind. Umbuchungen brauchen keine Kategorie und zählen deshalb nicht mit.
* **nicht abgeglichen** – Buchungen aktiver Konten bis heute ohne Abgleich-Kennzeichen. * **nicht abgeglichen** – Buchungen aktiver Konten bis heute ohne Abgleich-Kennzeichen.
* **Bargeld-Tool-Prüfung fehlgeschlagen** – Buchungen mit `bargeld_status = 'fehler'`, also
einem gescheiterten Abruf beim Bargeld-Tool (siehe `docs/buchungen.md`), nicht zu
verwechseln mit „im Bargeld-Tool nicht gefunden“. Führt auf
`/verwaltung/bargeld/fehler`, die Liste dieser Buchungen kontoübergreifend
(`Buchung::mitBargeldFehler()`), denn die Prüfung hängt an der Kategorie, nicht am Konto.
* **Monatsabschluss steht aus** – der Vorschlag aus `Monatsabschluss::naechsterOffener()`, * **Monatsabschluss steht aus** – der Vorschlag aus `Monatsabschluss::naechsterOffener()`,
aber nur, wenn dieser Monat wirklich noch offen ist und Buchungen trägt. aber nur, wenn dieser Monat wirklich noch offen ist und Buchungen trägt.
@ -188,8 +193,11 @@ regelmäßigen Buchungen.
* `tests/15_CockpitTest.php` – Kachelauswahl (Reihenfolge, Unbekanntes, leere Auswahl), * `tests/15_CockpitTest.php` – Kachelauswahl (Reihenfolge, Unbekanntes, leere Auswahl),
Monatsreihe mit Trennung von Einnahmen und Ausgaben, Umbuchungen bleiben draußen, Monatsreihe mit Trennung von Einnahmen und Ausgaben, Umbuchungen bleiben draußen,
Verteilung auf die Wurzelkategorie, Kürzung auf die Segmentgrenze, offene Vorgänge, Verteilung auf die Wurzelkategorie, Kürzung auf die Segmentgrenze, offene Vorgänge
Legendeneinträge führen zu Buchungslisten, deren Summe den Legendenwert trifft (auch fehlgeschlagene Bargeld-Tool-Prüfungen), Legendeneinträge führen zu
Buchungslisten, deren Summe den Legendenwert trifft
* `tests/34_BargeldAbgleichTest.php` – die Liste der fehlgeschlagenen Prüfungen
(`Buchung::mitBargeldFehler()`) und ihre Anzeige unter Verwaltung → Bargeld-Tool
* `tests/13_VermoegensentwicklungTest.php` – Zeitraum, Fortschreibung der Stände, * `tests/13_VermoegensentwicklungTest.php` – Zeitraum, Fortschreibung der Stände,
Umbuchungen, Σ-Kennzeichen, Schalter „nur aktive“, Zusammenfassen jenseits der Palette, Umbuchungen, Σ-Kennzeichen, Schalter „nur aktive“, Zusammenfassen jenseits der Palette,
Ebenen (jedes Konto genau einmal, letzte Ebene ohne Sammelstück, zu tiefe Ebene fällt Ebenen (jedes Konto genau einmal, letzte Ebene ohne Sammelstück, zu tiefe Ebene fällt

View file

@ -139,6 +139,7 @@ $router->post('/verwaltung/kontengruppen/{id}/loeschen',
$router->get('/verwaltung/bargeld', fn() => (new VerwaltungController())->bargeld()); $router->get('/verwaltung/bargeld', fn() => (new VerwaltungController())->bargeld());
$router->post('/verwaltung/bargeld', fn() => (new VerwaltungController())->bargeldSpeichern()); $router->post('/verwaltung/bargeld', fn() => (new VerwaltungController())->bargeldSpeichern());
$router->post('/verwaltung/bargeld/test', fn() => (new VerwaltungController())->bargeldTest()); $router->post('/verwaltung/bargeld/test', fn() => (new VerwaltungController())->bargeldTest());
$router->get('/verwaltung/bargeld/fehler', fn() => (new VerwaltungController())->bargeldFehler());
// Das Bankleitzahlen-Verzeichnis: nachschlagen und die Institutsliste einlesen. // Das Bankleitzahlen-Verzeichnis: nachschlagen und die Institutsliste einlesen.
$router->get('/verwaltung/bankinstitute', fn() => (new BankinstituteController())->index()); $router->get('/verwaltung/bankinstitute', fn() => (new BankinstituteController())->index());

View file

@ -6,6 +6,7 @@ namespace Finanzmanager\Controllers;
use Finanzmanager\Models\Adresse; use Finanzmanager\Models\Adresse;
use Finanzmanager\Models\Bankinstitut; use Finanzmanager\Models\Bankinstitut;
use Finanzmanager\Models\Buchung;
use Finanzmanager\Models\Einstellung; use Finanzmanager\Models\Einstellung;
use Finanzmanager\Models\Erinnerung; use Finanzmanager\Models\Erinnerung;
use Finanzmanager\Models\Kategorie; use Finanzmanager\Models\Kategorie;
@ -275,4 +276,19 @@ class VerwaltungController extends BaseController
$ergebnis = $zugang->test(); $ergebnis = $zugang->test();
View::redirect('/verwaltung/bargeld', $ergebnis['meldung'], $ergebnis['erfolg'] ? 'erfolg' : 'fehler'); View::redirect('/verwaltung/bargeld', $ergebnis['meldung'], $ergebnis['erfolg'] ? 'erfolg' : 'fehler');
} }
/**
* Alle Buchungen, an denen die Bargeld-Tool-Prüfung zuletzt einen Fehler
* festgestellt hat – der Weg, den die Kachel „Offene Vorgänge“ des
* Cockpit anbietet, sobald es solche Buchungen gibt.
*/
public function bargeldFehler(): void
{
$this->verlangeAnmeldung();
$this->render('verwaltung/bargeld_fehler.php', [
'titel' => 'Bargeld-Tool: fehlgeschlagene Prüfungen',
'buchungen' => Buchung::mitBargeldFehler(),
]);
}
} }

View file

@ -472,6 +472,28 @@ class Buchung
return $zeilen; return $zeilen;
} }
/**
* Alle Buchungen, an denen die Bargeld-Tool-Prüfung zuletzt einen Fehler
* festgestellt hat – kontoübergreifend, denn die Prüfung hängt an der
* Kategorie der Buchung, nicht an ihrem Konto. Der Status 'fehler' steht
* für einen gescheiterten Abruf (siehe Services\BargeldAbgleich::FEHLER),
* nicht für eine im Bargeld-Tool fehlende Buchung.
*
* @return list<array<string,mixed>>
*/
public static function mitBargeldFehler(): array
{
return Database::fetchAll(
'SELECT b.id, b.datum, b.betrag, b.empfaenger, b.buchungstext,
b.bargeld_hinweis, b.bargeld_geprueft_am, b.konto_id, k.name AS konto_name
FROM buchungen b
JOIN konten k ON k.id = b.konto_id
WHERE b.benutzer_id = ? AND b.bargeld_status = \'fehler\'
ORDER BY b.datum DESC, b.id DESC',
[Mandant::id()]
);
}
/** /**
* Salden aller Konten in einem Rutsch: Anfangssaldo plus Buchungen, plus die * Salden aller Konten in einem Rutsch: Anfangssaldo plus Buchungen, plus die
* Gegenseiten fremder Umbuchungen. Ohne Stichtag zählt alles (erwarteter * Gegenseiten fremder Umbuchungen. Ohne Stichtag zählt alles (erwarteter

View file

@ -271,6 +271,23 @@ class Cockpit
]; ];
} }
// Buchungen, deren letzter Abgleich mit dem Bargeld-Tool gescheitert
// ist – ein Abrufproblem, keine Aussage über die Buchung selbst (siehe
// BargeldAbgleich::FEHLER), aber solange offen, bis er erneut glückt.
$bargeldFehler = (int)Database::fetchValue(
'SELECT COUNT(*) FROM buchungen WHERE benutzer_id = ? AND bargeld_status = ?',
[$benutzer, BargeldAbgleich::FEHLER],
0
);
if ($bargeldFehler > 0) {
$vorgaenge[] = [
'schluessel' => 'bargeld_fehler',
'text' => 'Bargeld-Tool-Prüfung fehlgeschlagen',
'anzahl' => $bargeldFehler,
'pfad' => '/verwaltung/bargeld/fehler',
];
}
// Fällige regelmäßige Buchungen – die automatischen zählen mit, denn // Fällige regelmäßige Buchungen – die automatischen zählen mit, denn
// gebucht sind sie erst nach dem Fälligkeitslauf. // gebucht sind sie erst nach dem Fälligkeitslauf.
$termine = Faelligkeitslauf::vorschau($heute); $termine = Faelligkeitslauf::vorschau($heute);

View file

@ -0,0 +1,62 @@
<?php
use Finanzmanager\View;
/**
* Alle Buchungen, an denen die Bargeld-Tool-Prüfung zuletzt einen Fehler
* festgestellt hat – der Ziel der Kachel „Offene Vorgänge“ im Cockpit.
*
* @var list<array<string,mixed>> $buchungen
*/
include __DIR__ . '/../layout/kopf.php';
?>
<div class="seitenaktionen">
<a href="/verwaltung/bargeld" class="knopf">Bargeld-Tool</a>
<a href="/" class="knopf">Cockpit</a>
</div>
<p class="feldhinweis">
Bei diesen Buchungen ist der letzte Abruf beim Bargeld-Tool gescheitert – ein
Abrufproblem, keine Aussage darüber, ob die Buchung dort steht. Eine erneute
Zuordnung der Kategorie in der Buchungsmaske oder im Kontoblatt stößt die
Prüfung erneut an.
</p>
<?php if ($buchungen === []): ?>
<p class="platzhalter">Keine offenen Fehler.</p>
<?php else: ?>
<table class="liste">
<thead>
<tr>
<th>Datum</th>
<th>Konto</th>
<th>Empfänger / Buchungstext</th>
<th class="rechts">Betrag</th>
<th>Meldung</th>
</tr>
</thead>
<tbody>
<?php foreach ($buchungen as $buchung): ?>
<tr>
<td><?= View::h(View::datum($buchung['datum'])) ?></td>
<td>
<a href="/konten/<?= (int)$buchung['konto_id'] ?>">
<?= View::h($buchung['konto_name']) ?>
</a>
</td>
<td>
<?= View::h($buchung['empfaenger'] !== '' ? $buchung['empfaenger'] : $buchung['buchungstext']) ?>
</td>
<td class="rechts <?= View::betragKlasse($buchung['betrag']) ?>">
<?= View::betrag($buchung['betrag']) ?>
</td>
<td class="klein"><?= View::h($buchung['bargeld_hinweis'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -2,6 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use Finanzmanager\Database;
use Finanzmanager\Models\Buchung; use Finanzmanager\Models\Buchung;
use Finanzmanager\Models\Kategorie; use Finanzmanager\Models\Kategorie;
use Finanzmanager\Models\Konto; use Finanzmanager\Models\Konto;
@ -35,6 +36,7 @@ class CockpitTest extends TestCase
$this->verteilungFasstAufDerWurzelZusammen(); $this->verteilungFasstAufDerWurzelZusammen();
$this->verteilungKuerztAufDieGrenze(); $this->verteilungKuerztAufDieGrenze();
$this->offeneVorgaengeZaehlenWasWartet(); $this->offeneVorgaengeZaehlenWasWartet();
$this->bargeldFehlerErscheintAlsOffenerVorgang();
$this->legendenFuehrenZuDenBuchungen(); $this->legendenFuehrenZuDenBuchungen();
} }
@ -251,6 +253,65 @@ class CockpitTest extends TestCase
} }
} }
/**
* Eine Buchung, deren letzter Abgleich mit dem Bargeld-Tool gescheitert
* ist, erscheint als eigener offener Vorgang – unabhängig vom Konto, denn
* die Prüfung hängt an der Kategorie.
*/
private function bargeldFehlerErscheintAlsOffenerVorgang(): void
{
$schluessel = static fn(array $vorgaenge): array => array_column($vorgaenge, 'anzahl', 'schluessel');
$vorher = $schluessel(Cockpit::offeneVorgaenge(self::HEUTE));
$this->assert(
!isset($vorher['bargeld_fehler']),
'Ohne betroffene Buchung fehlt der Vorgang ganz'
);
$id = Buchung::anlegen([
'konto_id' => $this->giro, 'datum' => '2026-08-09', 'betrag' => -15,
'empfaenger' => 'Bargeldabhebung', 'kategorie_id' => $this->freizeit, 'abgeglichen' => 1,
]);
Database::execute(
"UPDATE buchungen SET bargeld_status = 'fehler', bargeld_hinweis = 'Kasse nicht erreichbar.'
WHERE id = ?",
[$id]
);
$nachher = Cockpit::offeneVorgaenge(self::HEUTE);
$eintrag = null;
foreach ($nachher as $vorgang) {
if ($vorgang['schluessel'] === 'bargeld_fehler') {
$eintrag = $vorgang;
}
}
$this->assertNotEmpty($eintrag, 'Der offene Vorgang erscheint jetzt');
$this->assertEquals(1, $eintrag['anzahl'], 'Genau die eine betroffene Buchung wird gezählt');
$this->assertEquals(
'/verwaltung/bargeld/fehler',
$eintrag['pfad'],
'Der Weg führt zur Liste der fehlgeschlagenen Prüfungen'
);
$liste = Buchung::mitBargeldFehler();
$this->assertEquals(1, count($liste), 'Die Liste enthält genau diese Buchung');
$this->assertEquals($id, $liste[0]['id'], 'Es ist dieselbe Buchung');
$this->assertEquals('Cockpitgiro', $liste[0]['konto_name'], 'Der Kontoname kommt mit');
$this->assertEquals(
'Kasse nicht erreichbar.',
$liste[0]['bargeld_hinweis'],
'Die Meldung der Gegenstelle steht dabei'
);
// Ein Status ungleich 'fehler' (z. B. 'gefunden') zählt nicht mit.
Database::execute("UPDATE buchungen SET bargeld_status = 'gefunden' WHERE id = ?", [$id]);
$ohne = $schluessel(Cockpit::offeneVorgaenge(self::HEUTE));
$this->assert(!isset($ohne['bargeld_fehler']), 'Ein gefundener Treffer ist kein offener Vorgang mehr');
Database::execute("UPDATE buchungen SET bargeld_status = 'fehler' WHERE id = ?", [$id]);
}
/** /**
* Jeder Legendeneintrag führt zu einer Buchungsliste, deren Summe genau * Jeder Legendeneintrag führt zu einer Buchungsliste, deren Summe genau
* seinen Wert trifft – auch „Nicht kategorisiert“ und das Sammelsegment. * seinen Wert trifft – auch „Nicht kategorisiert“ und das Sammelsegment.

View file

@ -2,6 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use Finanzmanager\Database;
use Finanzmanager\Models\Buchung; use Finanzmanager\Models\Buchung;
use Finanzmanager\Models\Einstellung; use Finanzmanager\Models\Einstellung;
use Finanzmanager\Models\Kategorie; use Finanzmanager\Models\Kategorie;
@ -51,6 +52,7 @@ class BargeldAbgleichTest extends TestCase
$this->kontoblattFuehrtDenStatusMit(); $this->kontoblattFuehrtDenStatusMit();
$this->kontoblattZeigtDieViervierSymbole(); $this->kontoblattZeigtDieViervierSymbole();
$this->konfigurationsseiteZeigtDieAuswahl(); $this->konfigurationsseiteZeigtDieAuswahl();
$this->fehlerlisteZeigtDieBuchungen();
} }
protected function tearDown(): void protected function tearDown(): void
@ -509,6 +511,45 @@ class BargeldAbgleichTest extends TestCase
); );
} }
/**
* Die Liste unter „Verwaltung → Bargeld-Tool → fehlgeschlagene Prüfungen“,
* das Ziel der Kachel „Offene Vorgänge“ im Cockpit. Buchung::mitBargeldFehler()
* ist kontoübergreifend – geprüft am Datenmodell, nicht am Kontoblatt.
*/
private function fehlerlisteZeigtDieBuchungen(): void
{
$id = $this->buchung(-27.30, '2026-06-01');
Buchung::zuordnen($id, $this->bargeldKategorie, null);
Database::execute(
"UPDATE buchungen SET bargeld_status = 'fehler', bargeld_hinweis = 'Kasse steht still.' WHERE id = ?",
[$id]
);
$liste = Buchung::mitBargeldFehler();
$treffer = null;
foreach ($liste as $zeile) {
if ((int)$zeile['id'] === $id) {
$treffer = $zeile;
}
}
$this->assertNotEmpty($treffer, 'Die Buchung mit Status „fehler“ steht in der Liste');
$this->assertEquals('Bargeld Giro', $treffer['konto_name'], 'Der Kontoname kommt mit – die Liste ist kontoübergreifend');
$this->assertEquals('Kasse steht still.', $treffer['bargeld_hinweis'], 'Die Meldung steht dabei');
$html = $this->rendere('verwaltung/bargeld_fehler.php', ['buchungen' => $liste]);
$this->assert(str_contains($html, 'Bargeld Giro'), 'Der Kontoname steht in der Liste');
$this->assert(str_contains($html, 'Bargeldabhebung'), 'Der Empfänger steht in der Liste');
$this->assert(str_contains($html, 'Kasse steht still.'), 'Die Meldung der Gegenstelle steht dabei');
$this->assert(
str_contains($html, '/konten/' . $this->konto),
'Der Kontoname führt ins Kontoblatt'
);
$leer = $this->rendere('verwaltung/bargeld_fehler.php', ['buchungen' => []]);
$this->assert(str_contains($leer, 'Keine offenen Fehler.'), 'Eine leere Liste sagt das ausdrücklich');
}
/** @param list<array<string,mixed>> $zeilen */ /** @param list<array<string,mixed>> $zeilen */
private function kontoblattHtml(array $zeilen, bool $bargeld): string private function kontoblattHtml(array $zeilen, bool $bargeld): string
{ {