Finanzmanager/tests/23_ZuordnungTest.php
fds2610-bot ca8496f770 Kontoblatt: Klasse direkt auswählen, Zeilen einheitlich zweizeilig
Die Klassenspalte ist anklickbar wie die Zuordnung daneben: Ein Klick
öffnet die Klassenliste in der Zelle, die Auswahl wird sofort über
POST /buchungen/{id}/klasse gespeichert. Buchung::klasseSetzen() lässt
Kategorie, Gegenkonto und Splitzeilen unberührt.

Die Buchungszeilen sind jetzt alle zwei Textzeilen hoch, ihr Inhalt steht
senkrecht in der Mitte. Was nicht auf zwei Zeilen passt, wird
abgeschnitten; den vollständigen Text zeigt der Tooltip – bei Empfänger
und Buchungstext nur dort, wo wirklich etwas fehlt, über dem
Kategoriefeld einer Splitbuchung deren Zeilen mit Kategorie und Betrag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:04:48 +02:00

331 lines
14 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\Controllers\BuchungenController;
use Finanzmanager\Models\Buchung;
use Finanzmanager\Models\Kategorie;
use Finanzmanager\Models\Klasse;
use Finanzmanager\Models\Konto;
use Finanzmanager\Models\Monatsabschluss;
/**
* Die Schnellzuordnung aus dem Kontoblatt: Kategorie oder Gegenkonto in einem
* Feld, Splitzeilen aus dem Dialog.
*/
class ZuordnungTest extends TestCase
{
protected string $testName = 'Test 23: Zuordnung im Kontoblatt';
private int $giro = 0;
private int $spar = 0;
private int $kategorie = 0;
private int $klasse = 0;
public function run(): void
{
$this->mandantAnlegen();
$this->stammdaten();
$this->auswahlwerteLesen();
$this->kategorieUndGegenkontoWechseln();
$this->gegensichtNenntIhrKonto();
$this->splitzeilenErsetzen();
$this->splitbuchungLoeschen();
$this->splitzeilenJeBuchung();
$this->klasseWechseln();
$this->pruefungen();
}
private function stammdaten(): void
{
$this->giro = Konto::anlegen([
'name' => 'Zuordnung Giro', 'kontoart' => 'bank', 'anfangssaldo' => 1000, 'aktiv' => 1,
]);
$this->spar = Konto::anlegen([
'name' => 'Zuordnung Spar', 'kontoart' => 'sparkonto', 'anfangssaldo' => 0, 'aktiv' => 1,
]);
$this->kategorie = Kategorie::anlegen(['name' => 'Zuordnung Einkauf', 'art' => 'ausgabe']);
$this->klasse = Klasse::anlegen('Zuordnung Privat');
}
private function buchung(float $betrag = -50.0, string $datum = '2026-03-10'): int
{
return Buchung::anlegen([
'konto_id' => $this->giro,
'datum' => $datum,
'betrag' => $betrag,
'empfaenger' => 'Noch zuzuordnen',
]);
}
/** Kategorie und Gegenkonto stehen in einem Feld – hier wird es gelesen. */
private function auswahlwerteLesen(): void
{
$this->assertEquals(
['kategorie_id' => 12, 'gegenkonto_id' => null],
BuchungenController::zuordnungAus('kategorie:12'),
'Eine Kategorie wird erkannt'
);
$this->assertEquals(
['kategorie_id' => null, 'gegenkonto_id' => 7],
BuchungenController::zuordnungAus('konto:7'),
'Ein Gegenkonto wird erkannt'
);
$leer = ['kategorie_id' => null, 'gegenkonto_id' => null];
$this->assertEquals($leer, BuchungenController::zuordnungAus(''), 'Leer heißt keine Zuordnung');
$this->assertEquals($leer, BuchungenController::zuordnungAus('split'), 'Splitbuchung ist keine Zuordnung');
$this->assertEquals($leer, BuchungenController::zuordnungAus('konto:0'), 'Kennung 0 zählt nicht');
$this->assertEquals($leer, BuchungenController::zuordnungAus('unfug:3'), 'Unbekanntes Präfix zählt nicht');
}
private function kategorieUndGegenkontoWechseln(): void
{
$id = $this->buchung();
Buchung::zuordnen($id, $this->kategorie, null);
$buchung = Buchung::find($id);
$this->assertEquals($this->kategorie, (int)$buchung['kategorie_id'], 'Die Kategorie steht in der Buchung');
$this->assertNull($buchung['gegenkonto_id'], 'Ohne Gegenkonto');
// Aus der Kategorie wird eine Umbuchung: beides zugleich gibt es nicht.
Buchung::zuordnen($id, $this->kategorie, $this->spar);
$buchung = Buchung::find($id);
$this->assertEquals($this->spar, (int)$buchung['gegenkonto_id'], 'Das Gegenkonto steht in der Buchung');
$this->assertNull($buchung['kategorie_id'], 'Die Kategorie weicht dem Gegenkonto');
// Die Umbuchung wirkt sofort auf beiden Seiten.
$this->assertEquals(50.0, Konto::saldo($this->spar), 'Das Gegenkonto bekommt die Gegenseite');
Buchung::zuordnen($id, null, null);
$buchung = Buchung::find($id);
$this->assertNull($buchung['kategorie_id'], 'Die Zuordnung lässt sich wieder leeren');
$this->assertNull($buchung['gegenkonto_id'], 'Auch das Gegenkonto');
$this->assertEquals(0.0, Konto::saldo($this->spar), 'Und das Gegenkonto ist den Betrag wieder los');
}
/**
* Der Anzeigefehler, um den es ging: Auf dem Gegenkonto stand eine Umbuchung
* ohne erkennbares Gegenüber. Die Zeile muss das Ursprungskonto nennen.
*/
private function gegensichtNenntIhrKonto(): void
{
$id = $this->buchung(-120.0, '2026-03-11');
Buchung::zuordnen($id, null, $this->spar);
$zeilen = array_values(array_filter(
Buchung::kontoblatt($this->spar),
static fn(array $zeile): bool => (int)$zeile['id'] === $id
));
$this->assertEquals(1, count($zeilen), 'Die Umbuchung erscheint auf dem Gegenkonto');
$this->assert($zeilen[0]['gegensicht'], 'Und zwar als Gegensicht');
$this->assertEquals(
$this->giro,
(int)$zeilen[0]['gegenkonto_id'],
'Die Gegensicht nennt das Konto, von dem die Umbuchung kommt'
);
$this->assertEquals(120.0, (float)$zeilen[0]['betrag'], 'Mit umgekehrtem Vorzeichen');
Buchung::loeschen($id);
}
private function splitzeilenErsetzen(): void
{
$id = $this->buchung(-100.0, '2026-03-12');
Buchung::zuordnen($id, $this->kategorie, null);
Buchung::splitsErsetzen($id, [
['kategorie_id' => $this->kategorie, 'klasse_id' => $this->klasse, 'betrag' => -60.0, 'notiz' => 'Einkauf'],
['gegenkonto_id' => $this->spar, 'betrag' => -40.0, 'notiz' => 'Rücklage'],
]);
$buchung = Buchung::find($id);
$this->assertEquals(1, (int)$buchung['gesplittet'], 'Die Buchung ist jetzt gesplittet');
$this->assertNull($buchung['kategorie_id'], 'Die Kategorie von oben weicht den Zeilen');
$splits = Buchung::splits($id);
$this->assertEquals(2, count($splits), 'Zwei Zeilen sind gespeichert');
$this->assertEquals($this->klasse, (int)$splits[0]['klasse_id'], 'Die Klasse der ersten Zeile');
$this->assertEquals($this->spar, (int)$splits[1]['gegenkonto_id'], 'Das Gegenkonto der zweiten Zeile');
$this->assertEquals(40.0, Konto::saldo($this->spar), 'Nur der Betrag dieser Zeile geht aufs Gegenkonto');
// Eine einzelne Zuordnung hebt die Aufteilung auf.
Buchung::zuordnen($id, $this->kategorie, null);
$this->assertEquals(0, (int)Buchung::find($id)['gesplittet'], 'Die Splitbuchung ist aufgehoben');
$this->assertEquals([], Buchung::splits($id), 'Und die Zeilen sind weg');
$this->assertEquals(0.0, Konto::saldo($this->spar), 'Das Gegenkonto ist den Anteil wieder los');
// Ohne Zeilen bleibt eine Buchung ohne Zuordnung stehen.
Buchung::splitsErsetzen($id, [
['kategorie_id' => $this->kategorie, 'betrag' => -100.0, 'notiz' => ''],
]);
Buchung::splitsErsetzen($id, []);
$buchung = Buchung::find($id);
$this->assertEquals(0, (int)$buchung['gesplittet'], 'Leere Zeilen heben die Splitbuchung auf');
$this->assertNull($buchung['kategorie_id'], 'Die Buchung steht danach ohne Zuordnung da');
$this->assertEquals(-100.0, (float)$buchung['betrag'], 'Der Buchungsbetrag bleibt unberührt');
Buchung::loeschen($id);
}
/**
* „Splitbuchung löschen“ aus dem Splitdialog: Die Aufteilung fällt weg und
* mit ihr jede Zuordnung – auch die einer Zeile mit Gegenkonto, deren
* Gegenseite auf dem anderen Konto zu verschwinden hat.
*/
private function splitbuchungLoeschen(): void
{
$id = $this->buchung(-90.0, '2026-03-16');
Buchung::splitsErsetzen($id, [
['kategorie_id' => $this->kategorie, 'klasse_id' => $this->klasse, 'betrag' => -55.0, 'notiz' => 'Teil A'],
['gegenkonto_id' => $this->spar, 'betrag' => -35.0, 'notiz' => 'Teil B'],
]);
$this->assertEquals(35.0, Konto::saldo($this->spar), 'Die Zeile mit Gegenkonto wirkt zunächst');
Buchung::splitsErsetzen($id, []);
$buchung = Buchung::find($id);
$this->assertEquals([], Buchung::splits($id), 'Alle Splitzeilen sind gelöscht');
$this->assertEquals(0, (int)$buchung['gesplittet'], 'Die Buchung gilt nicht mehr als gesplittet');
$this->assertNull($buchung['kategorie_id'], 'Sie hat keine Kategorie');
$this->assertNull($buchung['gegenkonto_id'], 'Und kein Gegenkonto');
$this->assertEquals(-90.0, (float)$buchung['betrag'], 'Die Buchung selbst bleibt stehen');
$this->assertEquals(0.0, Konto::saldo($this->spar), 'Das Gegenkonto ist seinen Anteil wieder los');
Buchung::loeschen($id);
}
private function splitzeilenJeBuchung(): void
{
$eine = $this->buchung(-30.0, '2026-03-13');
$keine = $this->buchung(-20.0, '2026-03-14');
Buchung::splitsErsetzen($eine, [
['kategorie_id' => $this->kategorie, 'betrag' => -10.0, 'notiz' => 'A'],
['kategorie_id' => $this->kategorie, 'betrag' => -20.0, 'notiz' => 'B'],
]);
$jeBuchung = Buchung::splitsJeBuchung([$eine, $keine]);
$this->assertEquals(2, count($jeBuchung[$eine]), 'Die Zeilen kommen der Buchung zugeordnet zurück');
$this->assert(!isset($jeBuchung[$keine]), 'Eine Buchung ohne Zeilen fehlt in der Liste');
$this->assertEquals([], Buchung::splitsJeBuchung([]), 'Ohne Kennungen wird nicht abgefragt');
Buchung::loeschen($eine);
Buchung::loeschen($keine);
}
/**
* Die Schnellauswahl der Klassenspalte: setzen, wechseln, löschen – und die
* Zuordnung bleibt dabei, wie sie ist.
*/
private function klasseWechseln(): void
{
$zweite = Klasse::anlegen('Zuordnung Beruflich');
$id = $this->buchung(-70.0, '2026-03-17');
Buchung::zuordnen($id, $this->kategorie, null);
Buchung::klasseSetzen($id, $this->klasse);
$buchung = Buchung::find($id);
$this->assertEquals($this->klasse, (int)$buchung['klasse_id'], 'Die Klasse steht in der Buchung');
$this->assertEquals(
$this->kategorie,
(int)$buchung['kategorie_id'],
'Die Zuordnung bleibt von der Klasse unberührt'
);
Buchung::klasseSetzen($id, $zweite);
$this->assertEquals($zweite, (int)Buchung::find($id)['klasse_id'], 'Die Klasse lässt sich wechseln');
// Der erste Eintrag der Liste – „keine“ – löscht die vorhandene Klasse.
Buchung::klasseSetzen($id, null);
$this->assertNull(Buchung::find($id)['klasse_id'], 'Und wieder löschen');
// Eine Splitbuchung behält ihre Zeilen: Die Klasse steht daneben.
Buchung::splitsErsetzen($id, [
['kategorie_id' => $this->kategorie, 'betrag' => -40.0, 'notiz' => 'A'],
['kategorie_id' => $this->kategorie, 'betrag' => -30.0, 'notiz' => 'B'],
]);
Buchung::klasseSetzen($id, $this->klasse);
$this->assertEquals(2, count(Buchung::splits($id)), 'Die Splitzeilen bleiben erhalten');
$this->assertEquals(1, (int)Buchung::find($id)['gesplittet'], 'Und die Buchung bleibt gesplittet');
$fremdeKlasse = false;
try {
Buchung::klasseSetzen($id, 999999);
} catch (\Throwable) {
$fremdeKlasse = true;
}
$this->assert($fremdeKlasse, 'Eine Klasse, die es nicht gibt, wird abgewiesen');
Buchung::loeschen($id);
}
private function pruefungen(): void
{
$id = $this->buchung(-80.0, '2026-03-15');
$aufSichSelbst = false;
try {
Buchung::zuordnen($id, null, $this->giro);
} catch (\Throwable) {
$aufSichSelbst = true;
}
$this->assert($aufSichSelbst, 'Ein Konto kann nicht sein eigenes Gegenkonto sein');
$splitAufSichSelbst = false;
try {
Buchung::splitsErsetzen($id, [['gegenkonto_id' => $this->giro, 'betrag' => -80.0]]);
} catch (\Throwable) {
$splitAufSichSelbst = true;
}
$this->assert($splitAufSichSelbst, 'Auch eine Splitzeile nicht');
$krummeSumme = false;
try {
Buchung::splitsErsetzen($id, [['kategorie_id' => $this->kategorie, 'betrag' => -30.0]]);
} catch (\Throwable) {
$krummeSumme = true;
}
$this->assert($krummeSumme, 'Zeilen, die nicht aufgehen, werden abgewiesen');
$this->assertEquals(0, (int)Buchung::find($id)['gesplittet'], 'Und die Buchung bleibt ungesplittet');
$fremdeKategorie = false;
try {
Buchung::zuordnen($id, 999999, null);
} catch (\Throwable) {
$fremdeKategorie = true;
}
$this->assert($fremdeKategorie, 'Eine Kategorie, die es nicht gibt, wird abgewiesen');
// Ein abgeschlossener Monat ist auch für die Schnellzuordnung zu.
Monatsabschluss::abschliessen(2026, 3);
$zu = false;
try {
Buchung::zuordnen($id, $this->kategorie, null);
} catch (\Throwable) {
$zu = true;
}
$this->assert($zu, 'Im abgeschlossenen Monat wird nichts mehr zugeordnet');
$zuFuerSplits = false;
try {
Buchung::splitsErsetzen($id, []);
} catch (\Throwable) {
$zuFuerSplits = true;
}
$this->assert($zuFuerSplits, 'Auch die Splitzeilen bleiben, wie sie sind');
$zuFuerKlasse = false;
try {
Buchung::klasseSetzen($id, $this->klasse);
} catch (\Throwable) {
$zuFuerKlasse = true;
}
$this->assert($zuFuerKlasse, 'Und die Klasse ebenso');
}
}