Finanzmanager/tests/65_OhneBargeldAbgleichTest.php
fds2610-bot a5989eb262 Fix #136: Buchungen ohne Bargeld-Tool-Abgleich ermöglichen
Neuer Schalter „Ohne Bargeld-Abgleich“ in der Buchungsmaske für
Sammelbuchungen und Differenzausgleiche. Solche Buchungen werden nicht
gegen das Bargeld-Tool geprüft, ein früherer Status wird gelöscht, und sie
erscheinen nicht mehr unter den offenen Vorgängen im Cockpit. Kontoblatt
und Buchungsbericht zeigen einen leeren grauen Kreis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:58:17 +02:00

194 lines
8.5 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
use Finanzmanager\Database;
use Finanzmanager\Models\Buchung;
use Finanzmanager\Models\Einstellung;
use Finanzmanager\Models\Kategorie;
use Finanzmanager\Models\Konto;
use Finanzmanager\Services\Auswertung;
use Finanzmanager\Services\BargeldAbgleich;
use Finanzmanager\Services\Cockpit;
use Finanzmanager\View;
/**
* Bewusst ohne Abgleich mit dem Bargeld-Tool buchen (#136).
*
* Sammelbuchungen und Differenzausgleiche tragen eine Kategorie, die gegen
* das Bargeld-Tool geprüft wird, stehen dort aber absichtlich nicht. Geprüft
* wird, dass der Schalter gespeichert wird, den Abgleich unterbindet und die
* Buchung damit aus den offenen Vorgängen im Cockpit nimmt – auch dann, wenn
* sie vorher schon als „nicht gefunden“ angemahnt war.
*/
class OhneBargeldAbgleichTest extends TestCase
{
protected string $testName = 'Test 65: Ohne Bargeld-Abgleich';
private int $konto = 0;
private int $kategorie = 0;
public function run(): void
{
$this->mandantAnlegen();
$this->konto = Konto::anlegen([
'name' => 'Haushaltsgiro', 'kontoart' => 'bank', 'anfangssaldo' => 500, 'aktiv' => 1,
]);
$this->kategorie = Kategorie::anlegen(['name' => 'Haushalt bar', 'art' => 'ausgabe']);
Kategorie::bargeldAuswahlSetzen([$this->kategorie]);
// Eine Adresse, unter der niemand antwortet: Jeder echte Abruf endet
// als Fehler – ein Abruf, der gar nicht erst stattfindet, nicht.
Einstellung::set('bargeld_url', 'http://127.0.0.1:9', 'bargeld');
Einstellung::cacheLeeren();
$this->schalterWirdGespeichert();
$this->keinAbgleichUndKeinOffenerVorgang();
$this->nachtraeglichSetzenRaeumtDenStatusWeg();
$this->schnellzuordnungBehaeltDenSchalter();
$this->anzeigeImKontoblattUndBericht();
$this->maskeZeigtDenSchalter();
}
/** @param array<string,mixed> $zusatz */
private function buchung(string $datum, array $zusatz = []): int
{
return Buchung::anlegen($zusatz + [
'konto_id' => $this->konto,
'datum' => $datum,
'betrag' => -45.10,
'empfaenger' => 'Sammelbuchung Bargeld',
'kategorie_id' => $this->kategorie,
]);
}
/** @return array<string,int> */
private function vorgaenge(): array
{
return array_column(Cockpit::offeneVorgaenge('2026-09-30'), 'anzahl', 'schluessel');
}
private function schalterWirdGespeichert(): void
{
$id = $this->buchung('2026-09-01', ['ohne_bargeld_abgleich' => true]);
$this->assertEquals(1, (int)Buchung::find($id)['ohne_bargeld_abgleich'], 'Der Schalter wird gespeichert');
$normal = $this->buchung('2026-09-02');
$this->assertEquals(0, (int)Buchung::find($normal)['ohne_bargeld_abgleich'], 'Ohne Schalter bleibt er aus');
}
private function keinAbgleichUndKeinOffenerVorgang(): void
{
$vorher = $this->vorgaenge()['bargeld_fehler'] ?? 0;
$ohne = $this->buchung('2026-09-05', ['ohne_bargeld_abgleich' => true]);
$this->assertNull(BargeldAbgleich::pruefe($ohne), 'Eine Buchung ohne Bargeld-Abgleich wird nicht geprüft');
$this->assertNull(Buchung::find($ohne)['bargeld_status'], 'Sie trägt keinen Abgleichstatus');
$this->assertEquals($vorher, $this->vorgaenge()['bargeld_fehler'] ?? 0, 'Sie erscheint nicht unter den offenen Vorgängen');
// Gegenprobe: Dieselbe Buchung ohne Schalter wird geprüft und angemahnt.
$mit = $this->buchung('2026-09-06');
$this->assertEquals(BargeldAbgleich::FEHLER, BargeldAbgleich::pruefe($mit), 'Ohne Schalter wird abgeglichen');
$this->assertEquals($vorher + 1, $this->vorgaenge()['bargeld_fehler'] ?? 0, 'Und als offener Vorgang angemahnt');
}
private function nachtraeglichSetzenRaeumtDenStatusWeg(): void
{
$id = $this->buchung('2026-09-10');
Database::execute("UPDATE buchungen SET bargeld_status = 'fehlt', bargeld_hinweis = 'Kein Treffer.' WHERE id = ?", [$id]);
$vorher = $this->vorgaenge()['bargeld_fehlt'] ?? 0;
$this->assertGreaterThan(0, $vorher, 'Die nicht gefundene Buchung wird angemahnt');
$buchung = Buchung::find($id);
Buchung::speichern($id, ['ohne_bargeld_abgleich' => 1] + $buchung);
BargeldAbgleich::pruefe($id);
$frisch = Buchung::find($id);
$this->assertEquals(1, (int)$frisch['ohne_bargeld_abgleich'], 'Der Schalter lässt sich nachträglich setzen');
$this->assertNull($frisch['bargeld_status'], 'Der alte Abgleichstatus verschwindet');
$this->assertEquals($vorher - 1, $this->vorgaenge()['bargeld_fehlt'] ?? 0, 'Die Buchung wird nicht mehr angemahnt');
Buchung::speichern($id, ['ohne_bargeld_abgleich' => 0] + $frisch);
$this->assertEquals(0, (int)Buchung::find($id)['ohne_bargeld_abgleich'], 'Abgeschaltet gilt der Abgleich wieder');
}
private function schnellzuordnungBehaeltDenSchalter(): void
{
$id = $this->buchung('2026-09-12', ['ohne_bargeld_abgleich' => true, 'kategorie_id' => null]);
Buchung::zuordnen($id, $this->kategorie, null);
$this->assertNull(BargeldAbgleich::pruefe($id), 'Auch nach der Schnellzuordnung wird nicht abgeglichen');
$this->assertEquals(
1,
(int)Buchung::find($id)['ohne_bargeld_abgleich'],
'Die Schnellzuordnung lässt den Schalter stehen'
);
}
private function anzeigeImKontoblattUndBericht(): void
{
$marke = View::bargeldMarke(null, null, true);
$this->assert(str_contains($marke, 'Bewusst ohne Abgleich mit dem Bargeld-Tool'), 'Die Marke nennt den Grund');
$this->assert(str_contains($marke, '○'), 'Die Marke ist ein leerer Kreis');
$this->assert(str_contains(View::bargeldMarke(null, null), '●'), 'Ohne Schalter bleibt der volle Kreis');
$zeilen = Buchung::kontoblatt($this->konto, ['von' => '2026-09-01', 'bis' => '2026-09-01']);
$this->assertEquals(1, (int)($zeilen[0]['ohne_bargeld_abgleich'] ?? 0), 'Das Kontoblatt liefert den Schalter mit');
$bericht = Auswertung::bericht(Auswertung::normalisiere([
'typ' => 'buchungsbericht', 'zeitraum' => 'alles',
]), '2026-09-30');
$buchungen = array_merge(...array_column($bericht['gruppen'], 'buchungen'));
$treffer = array_values(array_filter(
$buchungen,
static fn(array $b): bool => $b['datum'] === '2026-09-01' && $b['ohne_bargeld_abgleich']
));
$this->assertEquals(1, count($treffer), 'Der Buchungsbericht liefert den Schalter mit');
}
private function maskeZeigtDenSchalter(): void
{
$mit = $this->maske(true, ['ohne_bargeld_abgleich' => 1]);
$this->assert(
(bool)preg_match('#name="ohne_bargeld_abgleich" value="1"\s*checked#', $mit),
'Mit Bargeld-Tool steht der Schalter gesetzt in der Maske'
);
$this->assert(str_contains($mit, 'Ohne Bargeld-Abgleich'), 'Er ist beschriftet');
$ohne = $this->maske(false, ['ohne_bargeld_abgleich' => 1]);
$this->assert(!str_contains($ohne, 'Ohne Bargeld-Abgleich'), 'Ohne Bargeld-Tool ist der Schalter unsichtbar');
$this->assert(
str_contains($ohne, '<input type="hidden" name="ohne_bargeld_abgleich" value="1">'),
'Ein gesetzter Wert geht trotzdem nicht verloren'
);
$leer = $this->maske(false, []);
$this->assert(!str_contains($leer, 'ohne_bargeld_abgleich'), 'Ohne Bargeld-Tool und ohne Wert steht nichts in der Maske');
}
/** @param array<string,mixed> $buchung */
private function maske(bool $bargeld, array $buchung): string
{
$daten = [
'titel' => 'Buchung bearbeiten',
'konto' => Konto::find($this->konto),
'buchung' => $buchung + [
'id' => 1, 'konto_id' => $this->konto, 'datum' => '2026-09-01', 'betrag' => -10.0,
],
'splits' => [],
'eingaben' => null,
'kategorien' => Kategorie::baumFlach(),
'klassen' => [],
'konten' => Konto::alle(),
'bargeld' => $bargeld,
'flash' => [],
'benutzer' => null,
'csrfToken' => 'x',
'aktuellerPfad' => '/konten',
];
extract($daten, EXTR_SKIP);
ob_start();
include dirname(__DIR__) . '/src/Views/buchungen/formular.php';
return (string)ob_get_clean();
}
}