Merge pull request 'Fix #76: Klassen der Splitzeilen in der Klassenspalte des Kontoblatts' (#88) from fds2610-bot/Finanzmanager:main into main
Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/88
This commit is contained in:
commit
6f29e4644f
7 changed files with 337 additions and 15 deletions
13
Changes.md
13
Changes.md
|
|
@ -1,5 +1,18 @@
|
||||||
# Änderungen
|
# Änderungen
|
||||||
|
|
||||||
|
## Kontoblatt: Klassen der Splitzeilen in der Klassenspalte (28.09.2026)
|
||||||
|
|
||||||
|
Die Klassenspalte des Kontoblatts zeigte bei einer Splitbuchung nur die Klasse der Buchung
|
||||||
|
selbst, nicht die ihrer Splitzeilen (#76).
|
||||||
|
|
||||||
|
- Neu `Buchung::splitKlasse()`: Tragen alle Splitzeilen dieselbe Klasse, steht sie in der
|
||||||
|
Spalte. Unterscheiden sie sich, steht dort die Marke *Splitbuchung*, der Tooltip nennt je
|
||||||
|
Splitzeile Kategorie und Klasse. Ohne Klasse in den Zeilen bleibt es bei der Klasse der
|
||||||
|
Buchung.
|
||||||
|
- Ein Klick auf die Klassenzelle einer Splitbuchung öffnet den Splitdialog, in dem die
|
||||||
|
Klassen der Zeilen geändert werden.
|
||||||
|
- Neuer Test `tests/47_SplitKlasseTest.php`, E2E-Prüfung in `buchungen.spec.js`.
|
||||||
|
|
||||||
## Online-Banking: Alle Umsätze abrufen (28.09.2026)
|
## Online-Banking: Alle Umsätze abrufen (28.09.2026)
|
||||||
|
|
||||||
Über der Tabelle der Bankverbindungen steht ein Knopf *Alle Umsätze abrufen*, der jede
|
Über der Tabelle der Bankverbindungen steht ein Knopf *Alle Umsätze abrufen*, der jede
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,14 @@ Auswertungsdimension. Anders als `Buchung::zuordnen()` rührt `klasseSetzen()` d
|
||||||
Kategorie noch Gegenkonto noch Splitzeilen an – auch eine gesplittete Buchung bleibt beim
|
Kategorie noch Gegenkonto noch Splitzeilen an – auch eine gesplittete Buchung bleibt beim
|
||||||
Wechsel der Klasse gesplittet. Den Monatsabschluss achtet die Methode wie alle anderen.
|
Wechsel der Klasse gesplittet. Den Monatsabschluss achtet die Methode wie alle anderen.
|
||||||
|
|
||||||
|
Bei einer **Splitbuchung** zählen die Klassen ihrer Zeilen (`Buchung::splitKlasse()`):
|
||||||
|
Tragen alle Zeilen dieselbe Klasse, steht sie in der Spalte; unterscheiden sie sich – eine
|
||||||
|
Zeile ohne Klasse zählt dabei als eigene Angabe –, steht dort die Marke *Splitbuchung*, und
|
||||||
|
der Tooltip nennt je Zeile Kategorie (oder Gegenkonto) und Klasse. Hat keine Zeile eine
|
||||||
|
Klasse, bleibt es bei der Klasse der Buchung selbst. Ein Klick auf die Klassenzelle einer
|
||||||
|
Splitbuchung öffnet den Splitdialog statt der Klassenliste (`data-split`): Dort stehen ihre
|
||||||
|
Klassen.
|
||||||
|
|
||||||
## Zeilenhöhe und Tooltips im Kontoblatt
|
## Zeilenhöhe und Tooltips im Kontoblatt
|
||||||
|
|
||||||
Jede Buchungszeile ist **zwei Textzeilen hoch**, gleich wie viel in ihr steht, und ihr
|
Jede Buchungszeile ist **zwei Textzeilen hoch**, gleich wie viel in ihr steht, und ihr
|
||||||
|
|
@ -161,8 +169,8 @@ Den vollständigen Text zeigt der Tooltip:
|
||||||
Fensterbreite. Ein Tooltip, der bloß wiederholt, was ohnehin dasteht, wäre lästig.
|
Fensterbreite. Ein Tooltip, der bloß wiederholt, was ohnehin dasteht, wäre lästig.
|
||||||
* **Kategorie und Klasse** tragen ihren Tooltip vom Server. Bei einer **Splitbuchung** nennt
|
* **Kategorie und Klasse** tragen ihren Tooltip vom Server. Bei einer **Splitbuchung** nennt
|
||||||
er die Zeilen der Aufteilung: je Zeile Kategorie (oder Gegenkonto) und Betrag, sonst
|
er die Zeilen der Aufteilung: je Zeile Kategorie (oder Gegenkonto) und Betrag, sonst
|
||||||
nichts. Notiz und Klasse stünden nur im Weg; wer sie ändern will, öffnet ohnehin den
|
nichts. Notiz und Klasse stünden nur im Weg; die Klassen nennt bei verschiedenen Klassen
|
||||||
Dialog.
|
der Tooltip der Klassenspalte (siehe oben).
|
||||||
|
|
||||||
## Splitbuchungen
|
## Splitbuchungen
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,15 @@
|
||||||
const klassenfeld = klassenauswahl && ereignis.target.closest('a.klassenfeld');
|
const klassenfeld = klassenauswahl && ereignis.target.closest('a.klassenfeld');
|
||||||
if (klassenfeld) {
|
if (klassenfeld) {
|
||||||
ereignis.preventDefault();
|
ereignis.preventDefault();
|
||||||
|
// Die Klassen einer Splitbuchung stehen an ihren Zeilen – der Klick
|
||||||
|
// führt deshalb in den Splitdialog, nicht in die Klassenliste.
|
||||||
|
const splitfeld = klassenfeld.dataset.split
|
||||||
|
? klassenfeld.closest('tr').querySelector('a.zuordnungsfeld[data-splits]')
|
||||||
|
: null;
|
||||||
|
if (splitfeld) {
|
||||||
|
splitOeffnen(splitfeld);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const klasse = klassenfeld.dataset.klasse || '';
|
const klasse = klassenfeld.dataset.klasse || '';
|
||||||
oeffnen(klassenfeld, klassenauswahl, 'klasse', klasse === '0' ? '' : klasse);
|
oeffnen(klassenfeld, klassenauswahl, 'klasse', klasse === '0' ? '' : klasse);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,32 @@ class Buchung
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Klasse, die das Kontoblatt für eine Splitbuchung zeigt. Tragen alle
|
||||||
|
* Splitzeilen dieselbe Klasse, gilt sie für die ganze Buchung; unterscheiden
|
||||||
|
* sie sich, ist die Buchung gemischt und die Spalte zeigt „Splitbuchung“.
|
||||||
|
* Eine Zeile ohne Klasse zählt dabei als eigene Angabe. Hat keine Zeile eine
|
||||||
|
* Klasse, bleibt es bei der Klasse der Buchung selbst – so zeigen ältere
|
||||||
|
* Splitbuchungen weiter, was an ihnen steht.
|
||||||
|
*
|
||||||
|
* @param list<array<string,mixed>> $splitzeilen
|
||||||
|
* @return array{gemischt: bool, klasse_id: ?int}
|
||||||
|
*/
|
||||||
|
public static function splitKlasse(array $splitzeilen, ?int $eigeneKlasse): array
|
||||||
|
{
|
||||||
|
$klassen = [];
|
||||||
|
foreach ($splitzeilen as $zeile) {
|
||||||
|
$klassen[(int)($zeile['klasse_id'] ?? 0)] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($klassen) > 1) {
|
||||||
|
return ['gemischt' => true, 'klasse_id' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
$klasse = (int)array_key_first($klassen);
|
||||||
|
return ['gemischt' => false, 'klasse_id' => $klasse > 0 ? $klasse : $eigeneKlasse];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die Splitzeilen einer Buchung ersetzen – der Splitdialog des Kontoblatts.
|
* Die Splitzeilen einer Buchung ersetzen – der Splitdialog des Kontoblatts.
|
||||||
* Eine leere Liste hebt die Splitbuchung auf; die Buchung steht danach ohne
|
* Eine leere Liste hebt die Splitbuchung auf; die Buchung steht danach ohne
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Finanzmanager\Models\Adresse;
|
use Finanzmanager\Models\Adresse;
|
||||||
|
use Finanzmanager\Models\Buchung;
|
||||||
use Finanzmanager\Models\Wertpapier;
|
use Finanzmanager\Models\Wertpapier;
|
||||||
use Finanzmanager\View;
|
use Finanzmanager\View;
|
||||||
|
|
||||||
|
|
@ -89,19 +90,36 @@ $splitdaten = static function (array $splitzeilen) use ($zuordnungswert): string
|
||||||
/**
|
/**
|
||||||
* Der Tooltip einer Splitbuchung: je Zeile Kategorie und Betrag. In der Spalte
|
* Der Tooltip einer Splitbuchung: je Zeile Kategorie und Betrag. In der Spalte
|
||||||
* steht nur „Splitbuchung“ – wohin das Geld geht, wäre sonst erst im Dialog zu
|
* steht nur „Splitbuchung“ – wohin das Geld geht, wäre sonst erst im Dialog zu
|
||||||
* sehen. Notiz und Klasse bleiben draußen; sie stünden im Weg.
|
* sehen. Notiz und Klasse bleiben draußen; die Klassen nennt die Klassenspalte.
|
||||||
*
|
*
|
||||||
* @param list<array<string,mixed>> $splitzeilen
|
* @param list<array<string,mixed>> $splitzeilen
|
||||||
*/
|
*/
|
||||||
$splituebersicht = static function (array $splitzeilen) use ($kategorieNamen, $kontenNamen): string {
|
$splitname = static fn(array $zeile): string => match (true) {
|
||||||
|
!empty($zeile['gegenkonto_id']) => '[' . ($kontenNamen[(int)$zeile['gegenkonto_id']] ?? '?') . ']',
|
||||||
|
!empty($zeile['kategorie_id']) => (string)($kategorieNamen[(int)$zeile['kategorie_id']] ?? '?'),
|
||||||
|
default => '(ohne Kategorie)',
|
||||||
|
};
|
||||||
|
|
||||||
|
$splituebersicht = static function (array $splitzeilen) use ($splitname): string {
|
||||||
$texte = [];
|
$texte = [];
|
||||||
foreach ($splitzeilen as $zeile) {
|
foreach ($splitzeilen as $zeile) {
|
||||||
$name = match (true) {
|
$texte[] = $splitname($zeile) . ': ' . View::betrag($zeile['betrag']);
|
||||||
!empty($zeile['gegenkonto_id']) => '[' . ($kontenNamen[(int)$zeile['gegenkonto_id']] ?? '?') . ']',
|
}
|
||||||
!empty($zeile['kategorie_id']) => (string)($kategorieNamen[(int)$zeile['kategorie_id']] ?? '?'),
|
|
||||||
default => '(ohne Kategorie)',
|
return implode("\n", $texte);
|
||||||
};
|
};
|
||||||
$texte[] = $name . ': ' . View::betrag($zeile['betrag']);
|
|
||||||
|
/**
|
||||||
|
* Der Tooltip der Klassenspalte, wenn die Splitzeilen verschiedene Klassen
|
||||||
|
* tragen: je Zeile Kategorie und Klasse.
|
||||||
|
*
|
||||||
|
* @param list<array<string,mixed>> $splitzeilen
|
||||||
|
*/
|
||||||
|
$splitklassen = static function (array $splitzeilen) use ($splitname, $klassenNamen): string {
|
||||||
|
$texte = [];
|
||||||
|
foreach ($splitzeilen as $zeile) {
|
||||||
|
$texte[] = $splitname($zeile) . ': '
|
||||||
|
. (string)($klassenNamen[(int)($zeile['klasse_id'] ?? 0)] ?? '(ohne Klasse)');
|
||||||
}
|
}
|
||||||
|
|
||||||
return implode("\n", $texte);
|
return implode("\n", $texte);
|
||||||
|
|
@ -358,19 +376,39 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<?php $klassenname = (string)($klassenNamen[(int)$zeile['klasse_id']] ?? ''); ?>
|
<?php
|
||||||
|
// Bei einer Splitbuchung zählen die Klassen ihrer Zeilen – wie
|
||||||
|
// bei der Kategorie: eine gemeinsame Klasse steht in der
|
||||||
|
// Spalte, verschiedene nennt der Tooltip der Marke.
|
||||||
|
$splitzeilen = !$zeile['gegensicht'] ? ($splits[(int)$zeile['id']] ?? null) : null;
|
||||||
|
$klasse = $splitzeilen !== null
|
||||||
|
? Buchung::splitKlasse($splitzeilen, $zeile['klasse_id'] !== null ? (int)$zeile['klasse_id'] : null)
|
||||||
|
: ['gemischt' => false, 'klasse_id' => (int)$zeile['klasse_id']];
|
||||||
|
$klassenname = (string)($klassenNamen[(int)$klasse['klasse_id']] ?? '');
|
||||||
|
$klassentitel = match (true) {
|
||||||
|
$klasse['gemischt'] => $splitklassen($splitzeilen),
|
||||||
|
$klassenname !== '' => $klassenname,
|
||||||
|
default => 'Klasse zuordnen',
|
||||||
|
};
|
||||||
|
?>
|
||||||
<td class="klassenzelle nebentext">
|
<td class="klassenzelle nebentext">
|
||||||
<?php if ($zeile['gegensicht']): ?>
|
<?php if ($zeile['gegensicht']): ?>
|
||||||
<?= View::h($klassenname) ?>
|
<?= View::h($klassenname) ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php /* Wie bei der Zuordnung: Ohne JavaScript führt der Klick
|
<?php /* Wie bei der Zuordnung: Ohne JavaScript führt der Klick
|
||||||
in die Buchungsmaske, mit ihm öffnet sich die Auswahl
|
in die Buchungsmaske, mit ihm öffnet sich die Auswahl
|
||||||
an Ort und Stelle. */ ?>
|
an Ort und Stelle – bei einer Splitbuchung der
|
||||||
|
Splitdialog, denn dort stehen ihre Klassen. */ ?>
|
||||||
<a href="/buchungen/<?= (int)$zeile['id'] ?>" class="klassenfeld"
|
<a href="/buchungen/<?= (int)$zeile['id'] ?>" class="klassenfeld"
|
||||||
data-buchung="<?= (int)$zeile['id'] ?>"
|
data-buchung="<?= (int)$zeile['id'] ?>"
|
||||||
data-klasse="<?= (int)$zeile['klasse_id'] ?>"
|
data-klasse="<?= (int)$klasse['klasse_id'] ?>"
|
||||||
title="<?= View::h($klassenname !== '' ? $klassenname : 'Klasse zuordnen') ?>">
|
<?php if ($splitzeilen !== null): ?>data-split="1"<?php endif; ?>
|
||||||
<?= View::h($klassenname) ?>
|
title="<?= View::h($klassentitel) ?>">
|
||||||
|
<?php if ($klasse['gemischt']): ?>
|
||||||
|
<span class="marke">Splitbuchung</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= View::h($klassenname) ?>
|
||||||
|
<?php endif; ?>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
192
tests/47_SplitKlasseTest.php
Normal file
192
tests/47_SplitKlasseTest.php
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Finanzmanager\Models\Buchung;
|
||||||
|
use Finanzmanager\Models\Kategorie;
|
||||||
|
use Finanzmanager\Models\Klasse;
|
||||||
|
use Finanzmanager\Models\Konto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix #76: Die Klassenspalte des Kontoblatts berücksichtigt bei einer
|
||||||
|
* Splitbuchung die Klassen ihrer Zeilen. Tragen alle Zeilen dieselbe Klasse,
|
||||||
|
* steht sie in der Spalte; unterscheiden sie sich, steht dort die Marke
|
||||||
|
* „Splitbuchung“ mit den Klassen je Zeile im Tooltip.
|
||||||
|
*/
|
||||||
|
class SplitKlasseTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 47: Klassen der Splitzeilen im Kontoblatt';
|
||||||
|
|
||||||
|
private int $konto = 0;
|
||||||
|
private int $lebensmittel = 0;
|
||||||
|
private int $drogerie = 0;
|
||||||
|
private int $privat = 0;
|
||||||
|
private int $beruf = 0;
|
||||||
|
private int $alt = 0;
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->mandantAnlegen();
|
||||||
|
|
||||||
|
$this->konto = Konto::anlegen([
|
||||||
|
'name' => 'Splitklasse Giro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||||||
|
]);
|
||||||
|
$this->lebensmittel = Kategorie::anlegen(['name' => 'Splitklasse Lebensmittel', 'art' => 'ausgabe']);
|
||||||
|
$this->drogerie = Kategorie::anlegen(['name' => 'Splitklasse Drogerie', 'art' => 'ausgabe']);
|
||||||
|
$this->privat = Klasse::anlegen('Splitklasse Privat');
|
||||||
|
$this->beruf = Klasse::anlegen('Splitklasse Beruf');
|
||||||
|
$this->alt = Klasse::anlegen('Splitklasse Buchungsklasse');
|
||||||
|
|
||||||
|
$this->splitKlasseErmitteln();
|
||||||
|
$this->kontoblattZeigtGemeinsameKlasse();
|
||||||
|
$this->kontoblattZeigtMarkeBeiVerschiedenenKlassen();
|
||||||
|
$this->ohneSplitBleibtEsBeiDerBuchungsklasse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function splitKlasseErmitteln(): void
|
||||||
|
{
|
||||||
|
$gleich = [['klasse_id' => $this->privat], ['klasse_id' => (string)$this->privat]];
|
||||||
|
$this->assertEquals(
|
||||||
|
['gemischt' => false, 'klasse_id' => $this->privat],
|
||||||
|
Buchung::splitKlasse($gleich, $this->alt),
|
||||||
|
'Gleiche Klassen in allen Zeilen gelten für die ganze Buchung'
|
||||||
|
);
|
||||||
|
|
||||||
|
$verschieden = [['klasse_id' => $this->privat], ['klasse_id' => $this->beruf]];
|
||||||
|
$this->assertEquals(
|
||||||
|
['gemischt' => true, 'klasse_id' => null],
|
||||||
|
Buchung::splitKlasse($verschieden, $this->alt),
|
||||||
|
'Verschiedene Klassen machen die Buchung gemischt'
|
||||||
|
);
|
||||||
|
|
||||||
|
$teilweise = [['klasse_id' => $this->privat], ['klasse_id' => null]];
|
||||||
|
$this->assertEquals(
|
||||||
|
true,
|
||||||
|
Buchung::splitKlasse($teilweise, null)['gemischt'],
|
||||||
|
'Eine Zeile ohne Klasse unterscheidet sich von einer mit Klasse'
|
||||||
|
);
|
||||||
|
|
||||||
|
$ohne = [['klasse_id' => null], ['klasse_id' => null]];
|
||||||
|
$this->assertEquals(
|
||||||
|
['gemischt' => false, 'klasse_id' => $this->alt],
|
||||||
|
Buchung::splitKlasse($ohne, $this->alt),
|
||||||
|
'Ohne Klasse in den Zeilen bleibt die Klasse der Buchung'
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
['gemischt' => false, 'klasse_id' => null],
|
||||||
|
Buchung::splitKlasse($ohne, null),
|
||||||
|
'Ohne jede Klasse bleibt die Spalte leer'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function kontoblattZeigtGemeinsameKlasse(): void
|
||||||
|
{
|
||||||
|
$id = $this->splitbuchung('Gleichmarkt', $this->privat, $this->privat);
|
||||||
|
$zelle = $this->klassenzelle($id);
|
||||||
|
|
||||||
|
$this->assert(str_contains($zelle, 'Splitklasse Privat'), 'Die gemeinsame Klasse steht in der Spalte');
|
||||||
|
$this->assert(!str_contains($zelle, 'class="marke"'), 'Ohne Marke „Splitbuchung“');
|
||||||
|
$this->assert(str_contains($zelle, 'data-split="1"'), 'Der Klick führt in den Splitdialog');
|
||||||
|
$this->assert(
|
||||||
|
str_contains($zelle, 'data-klasse="' . $this->privat . '"'),
|
||||||
|
'Die Zelle trägt die gemeinsame Klasse'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function kontoblattZeigtMarkeBeiVerschiedenenKlassen(): void
|
||||||
|
{
|
||||||
|
$id = $this->splitbuchung('Mischmarkt', $this->privat, $this->beruf);
|
||||||
|
$zelle = $this->klassenzelle($id);
|
||||||
|
|
||||||
|
$this->assert(
|
||||||
|
str_contains($zelle, '<span class="marke">Splitbuchung</span>'),
|
||||||
|
'Verschiedene Klassen zeigen die Marke „Splitbuchung“'
|
||||||
|
);
|
||||||
|
$this->assert(
|
||||||
|
str_contains($zelle, 'title="Splitklasse Lebensmittel: Splitklasse Privat' . "\n"
|
||||||
|
. 'Splitklasse Drogerie: Splitklasse Beruf"'),
|
||||||
|
'Der Tooltip nennt je Splitzeile Kategorie und Klasse'
|
||||||
|
);
|
||||||
|
$this->assert(
|
||||||
|
!str_contains($zelle, 'Splitklasse Buchungsklasse'),
|
||||||
|
'Die Klasse der Buchung selbst spielt keine Rolle'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ohneSplitBleibtEsBeiDerBuchungsklasse(): void
|
||||||
|
{
|
||||||
|
$id = Buchung::anlegen([
|
||||||
|
'konto_id' => $this->konto, 'datum' => '2026-05-03', 'betrag' => -12.00,
|
||||||
|
'empfaenger' => 'Einzelmarkt', 'kategorie_id' => $this->lebensmittel, 'klasse_id' => $this->beruf,
|
||||||
|
]);
|
||||||
|
$zelle = $this->klassenzelle($id);
|
||||||
|
|
||||||
|
$this->assert(str_contains($zelle, 'Splitklasse Beruf'), 'Eine einfache Buchung zeigt ihre Klasse');
|
||||||
|
$this->assert(!str_contains($zelle, 'data-split'), 'Und öffnet weiter die Klassenliste');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function splitbuchung(string $empfaenger, int $klasseA, int $klasseB): int
|
||||||
|
{
|
||||||
|
return Buchung::anlegen(
|
||||||
|
[
|
||||||
|
'konto_id' => $this->konto, 'datum' => '2026-05-02', 'betrag' => -30.00,
|
||||||
|
'empfaenger' => $empfaenger, 'klasse_id' => $this->alt,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
['kategorie_id' => $this->lebensmittel, 'klasse_id' => $klasseA, 'betrag' => -20.00],
|
||||||
|
['kategorie_id' => $this->drogerie, 'klasse_id' => $klasseB, 'betrag' => -10.00],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Klassenzelle einer Buchung aus dem gerenderten Kontoblatt.
|
||||||
|
*/
|
||||||
|
private function klassenzelle(int $buchungId): string
|
||||||
|
{
|
||||||
|
$html = $this->kontoblattHtml();
|
||||||
|
$muster = '#<td class="klassenzelle nebentext">\s*<a href="/buchungen/' . $buchungId . '".*?</td>#s';
|
||||||
|
|
||||||
|
return preg_match($muster, $html, $treffer) === 1 ? $treffer[0] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Kontoblatt rendern wie der Controller es täte – ohne Webserver.
|
||||||
|
*/
|
||||||
|
private function kontoblattHtml(): string
|
||||||
|
{
|
||||||
|
$zeilen = Buchung::kontoblatt($this->konto);
|
||||||
|
$gesplittete = [];
|
||||||
|
foreach ($zeilen as $zeile) {
|
||||||
|
if ((int)$zeile['gesplittet'] === 1 && !$zeile['gegensicht']) {
|
||||||
|
$gesplittete[] = (int)$zeile['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$daten = [
|
||||||
|
'konto' => Konto::mitSalden(Konto::find($this->konto)),
|
||||||
|
'zeilen' => $zeilen,
|
||||||
|
'gesamt' => count($zeilen),
|
||||||
|
'seite' => 1,
|
||||||
|
'seiten' => 1,
|
||||||
|
'filter' => [],
|
||||||
|
'kategorien' => Kategorie::baumFlach(),
|
||||||
|
'klassen' => Klasse::alle(),
|
||||||
|
'konten' => Konto::alle(),
|
||||||
|
'splits' => Buchung::splitsJeBuchung($gesplittete),
|
||||||
|
'wertpapiere' => [],
|
||||||
|
'bargeld' => false,
|
||||||
|
'titel' => 'Test',
|
||||||
|
'flash' => [],
|
||||||
|
'benutzer' => null,
|
||||||
|
'csrfToken' => 'x',
|
||||||
|
'aktuellerPfad' => '/konten/' . $this->konto,
|
||||||
|
];
|
||||||
|
|
||||||
|
extract($daten, EXTR_SKIP);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
include dirname(__DIR__) . '/src/Views/buchungen/kontoblatt.php';
|
||||||
|
return (string)ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -372,6 +372,42 @@ test.describe('Buchungen', () => {
|
||||||
expect(titel).toContain(`${KATEGORIE}: -97,98`)
|
expect(titel).toContain(`${KATEGORIE}: -97,98`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Klassen der Splitzeilen stehen in der Klassenspalte', async ({ page }) => {
|
||||||
|
await page.goto(`/konten/${giroId}/buchung`)
|
||||||
|
await page.fill('#datum', '2026-05-03')
|
||||||
|
await page.fill('#betrag', '45')
|
||||||
|
await page.fill('#empfaenger', `Klassensplit ${MARKE}`)
|
||||||
|
await page.selectOption('select[name="split[0][zuordnung]"]', { label: KATEGORIE })
|
||||||
|
await page.selectOption('select[name="split[0][klasse_id]"]', { label: KLASSE })
|
||||||
|
await page.fill('input[name="split[0][betrag]"]', '-25')
|
||||||
|
await page.selectOption('select[name="split[1][zuordnung]"]', { label: KATEGORIE })
|
||||||
|
await page.fill('input[name="split[1][betrag]"]', '-20')
|
||||||
|
await page.click('.buchungsmaske button[type="submit"]')
|
||||||
|
await expect(page.locator('.meldung-erfolg')).toBeVisible()
|
||||||
|
|
||||||
|
// Verschiedene Klassen: die Marke, im Tooltip die Klasse je Zeile.
|
||||||
|
const zeile = page.locator('tr', { hasText: `Klassensplit ${MARKE}` })
|
||||||
|
const klassenfeld = zeile.locator('a.klassenfeld')
|
||||||
|
await expect(klassenfeld.locator('.marke')).toHaveText('Splitbuchung')
|
||||||
|
const titel = await klassenfeld.getAttribute('title')
|
||||||
|
expect(titel).toContain(`${KATEGORIE}: ${KLASSE}`)
|
||||||
|
expect(titel).toContain(`${KATEGORIE}: (ohne Klasse)`)
|
||||||
|
|
||||||
|
// Der Klick führt in den Splitdialog, wo die Klassen der Zeilen stehen.
|
||||||
|
await klassenfeld.click()
|
||||||
|
const dialog = page.locator('#splitdialog')
|
||||||
|
await expect(dialog).toBeVisible()
|
||||||
|
await expect(page.locator('#klassenauswahl')).toBeHidden()
|
||||||
|
await dialog.locator('select[name="split[1][klasse_id]"]').selectOption({ label: KLASSE })
|
||||||
|
await dialog.locator('button[type="submit"].primaer').click()
|
||||||
|
await expect(page.locator('.meldung-erfolg').last()).toContainText('Splitbuchung gespeichert')
|
||||||
|
|
||||||
|
// Gleiche Klassen: die Klasse selbst, ohne Marke.
|
||||||
|
const danach = page.locator('tr', { hasText: `Klassensplit ${MARKE}` }).locator('a.klassenfeld')
|
||||||
|
await expect(danach).toHaveText(KLASSE)
|
||||||
|
await expect(danach.locator('.marke')).toHaveCount(0)
|
||||||
|
})
|
||||||
|
|
||||||
test('Buchungszeilen sind gleich hoch, abgeschnittene Texte stehen im Tooltip', async ({ page }) => {
|
test('Buchungszeilen sind gleich hoch, abgeschnittene Texte stehen im Tooltip', async ({ page }) => {
|
||||||
const LANG = `Verwendungszweck ${MARKE} mit sehr vielen Wörtern, `.repeat(6).trim()
|
const LANG = `Verwendungszweck ${MARKE} mit sehr vielen Wörtern, `.repeat(6).trim()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue