Fix #166: Buchungszeilen ohne Anlage-Spalte, Typ in der ersten Spalte

This commit is contained in:
fds2610-bot 2026-08-02 20:21:57 +02:00
parent fc65fcb9c5
commit ed58490fc2
5 changed files with 55 additions and 29 deletions

View file

@ -23,8 +23,10 @@
### Buchungszeilen brauchen weniger Breite (#166)
- **`src/Views/belege/detail.php`:** Buchungsnummer und Buchungsdatum teilen sich in der Belegansicht eine Spalte (Nummer oben, Datum darunter) – wie bereits bei Kategorie- und Zahlungskonto. Die Tabelle ist damit eine Spalte schmaler und ohne waagerechtes Scrollen lesbar.
- **`tests/e2e/specs/beleg-buchung.spec.js`:** Neuer Test auf die zusammengelegte Spalte; die Betragsprüfungen der bestehenden Tests greifen jetzt auf die verschobene Zelle zu.
- **`src/Views/belege/detail.php`:** Buchungsnummer, Buchungsdatum und Buchungstyp teilen sich in der Belegansicht eine Spalte (Nummer oben, Datum darunter, Typ-Badge zuunterst) – wie bereits bei Kategorie- und Zahlungskonto. Die Spalte **Anlage** entfällt ersatzlos; das Wirtschaftsgut ist weiterhin über Journal, UStVA- und EÜR-Drill-down sowie die Anlagenliste erreichbar. Von acht Spalten bleiben sechs, die Tabelle ist ohne waagerechtes Scrollen lesbar.
- **`src/View.php`:** `formatMoney()` setzt zwischen Betrag und `€` ein geschütztes Leerzeichen. In schmalen Spalten rutschte das Währungszeichen sonst allein in die nächste Zeile. Wirkt in allen HTML-Ansichten; die PDF-Dienste haben eigene Formatierer und bleiben unberührt.
- **`docs/anlagevermögen.md`:** Der Abschnitt „Anzeige in Buchungstabellen" nennt die Beleg-Detailansicht nicht mehr unter den Übersichten mit Anlage-Spalte.
- **`tests/e2e/specs/beleg-buchung.spec.js`:** Tests auf die zusammengelegte Spalte (inklusive Typ und entfallener Anlage-Spalte) und darauf, dass Betrag und Währungszeichen in einer Zeile bleiben; die Betragsprüfungen der bestehenden Tests greifen jetzt auf die verschobene Zelle zu.
### Nebenbei

View file

@ -52,7 +52,9 @@ In beiden Fällen wird die `asset_id` in der Buchungszeile gespeichert.
## Anzeige in Buchungstabellen
In allen Buchungsübersichten (Beleg-Detail, Journal, UStVA-Drill-down, EÜR-Drill-down) erscheint eine **Anlage**-Spalte. Sie zeigt die Anlagen-Nummer als direkten Link zur Bearbeitungsmaske des Wirtschaftsguts. Falls für ein älteres Wirtschaftsgut keine Anlagen-Nummer hinterlegt ist, wird stattdessen die Bezeichnung angezeigt.
In den Buchungsübersichten Journal, UStVA-Drill-down und EÜR-Drill-down erscheint eine **Anlage**-Spalte. Sie zeigt die Anlagen-Nummer als direkten Link zur Bearbeitungsmaske des Wirtschaftsguts. Falls für ein älteres Wirtschaftsgut keine Anlagen-Nummer hinterlegt ist, wird stattdessen die Bezeichnung angezeigt.
In der **Beleg-Detailansicht** entfällt die Spalte zugunsten einer schmaleren Tabelle; das zugehörige Wirtschaftsgut ist dort über das Journal oder die Anlagenliste erreichbar.
---

View file

@ -76,7 +76,9 @@ class View
public static function formatMoney(mixed $value): string
{
return number_format((float)$value, 2, ',', '.') . ' €';
// Geschütztes Leerzeichen: in schmalen Tabellenspalten rutscht das
// Währungszeichen sonst allein in die nächste Zeile.
return number_format((float)$value, 2, ',', '.') . "\u{00A0}€";
}
public static function formatDate(?string $date): string

View file

@ -218,13 +218,11 @@ $suppliers = $suppliers ?? [];
<table>
<thead>
<tr>
<th>Buchungsnr.<br>Datum</th>
<th>Typ</th>
<th>Buchungsnr.<br>Datum<br>Typ</th>
<th>Kategorie</th>
<th>Zahlungskonto</th>
<th class="td-right">Wert</th>
<th>Text</th>
<th>Anlage</th>
<th></th>
</tr>
</thead>
@ -236,9 +234,9 @@ $suppliers = $suppliers ?? [];
<tr style="<?= ($isReversed || $isReversal) ? 'opacity:.55;background:#fff5f5' : '' ?>">
<td class="td-mono text-sm">
<?= $h($bk['buchungs_nr']) ?><br>
<span class="td-muted"><?= View::formatDate($bk['buchungs_datum']) ?></span>
<span class="td-muted"><?= View::formatDate($bk['buchungs_datum']) ?></span><br>
<span class="badge <?= $bk['buchungstyp'] === 'Einnahme' ? 'badge-green' : 'badge-red' ?>"><?= $h($bk['buchungstyp']) ?></span>
</td>
<td><span class="badge <?= $bk['buchungstyp'] === 'Einnahme' ? 'badge-green' : 'badge-red' ?>"><?= $h($bk['buchungstyp']) ?></span></td>
<td class="td-mono text-sm"><?= $h($bk['kategorie_nr']) ?><br><span class="td-muted"><?= $h($bk['kategorie_name']) ?></span></td>
<td class="td-mono text-sm"><?= $h($bk['zahlungs_nr']) ?><br><span class="td-muted"><?= $h($bk['zahlungs_name']) ?></span></td>
<td class="td-right td-mono"><?= $fmt($bk['betrag_netto']) ?></td>
@ -250,15 +248,6 @@ $suppliers = $suppliers ?? [];
<span class="badge badge-red">Storniert</span>
<?php endif; ?>
</td>
<td class="td-mono text-sm">
<?php if (!empty($bk['anlage_nr'])): ?>
<a href="/assets/<?= (int)$bk['asset_id'] ?>/edit" title="<?= $h($bk['anlage_bezeichnung']) ?>"><?= $h($bk['anlage_nr']) ?></a>
<?php elseif (!empty($bk['anlage_bezeichnung'])): ?>
<a href="/assets/<?= (int)$bk['asset_id'] ?>/edit"><?= $h($bk['anlage_bezeichnung']) ?></a>
<?php else: ?>
<span class="text-muted">–</span>
<?php endif; ?>
</td>
<td>
<?php if (!$bk['locked'] && !$isReversal && !$isReversed): ?>
<div style="display:flex;gap:.35rem">
@ -300,7 +289,7 @@ $suppliers = $suppliers ?? [];
</tr>
<?php endforeach; ?>
<?php if (empty($bookings)): ?>
<tr><td colspan="8" class="text-center text-muted" style="padding:1rem">Noch keine Buchungen.</td></tr>
<tr><td colspan="6" class="text-center text-muted" style="padding:1rem">Noch keine Buchungen.</td></tr>
<?php endif; ?>
</tbody>
</table>

View file

@ -73,7 +73,7 @@ test.describe('Beleg-Upload und Buchung', () => {
const zeile = page.locator('table tbody tr', { hasText: tag }).first()
await expect(zeile).toHaveCount(1)
expect(parseMoney(await zeile.locator('td').nth(4).textContent())).toBeCloseTo(100.0, 2)
expect(parseMoney(await zeile.locator('td').nth(3).textContent())).toBeCloseTo(100.0, 2)
})
test('Buchung im BRUTTO-Modus wird vor dem Absenden in Netto umgerechnet', async ({ page }) => {
@ -98,7 +98,7 @@ test.describe('Beleg-Upload und Buchung', () => {
// Kern der Regression: Gespeichert werden muss der Nettobetrag 100,00 —
// nicht die eingegebenen 119,00.
const gespeichert = parseMoney(await zeile.locator('td').nth(4).textContent())
const gespeichert = parseMoney(await zeile.locator('td').nth(3).textContent())
expect(gespeichert, 'Bruttoeingabe wurde nicht in Netto umgerechnet').toBeCloseTo(100.0, 2)
})
@ -131,13 +131,14 @@ test.describe('Beleg-Upload und Buchung', () => {
const zeile = page.locator('table tbody tr', { hasText: tag }).first()
await expect(zeile).toHaveCount(1)
expect(parseMoney(await zeile.locator('td').nth(4).textContent())).toBeCloseTo(100.0, 2)
expect(parseMoney(await zeile.locator('td').nth(3).textContent())).toBeCloseTo(100.0, 2)
})
// Issue #166: Buchungsnummer und Buchungsdatum teilen sich eine Spalte,
// damit die Tabelle ohne waagerechtes Scrollen lesbar bleibt. Prüfbar nur
// am gerenderten DOM — die Zellaufteilung existiert nur in der View.
test('Buchungsnummer und Datum stehen in einer gemeinsamen Spalte', async ({ page }) => {
// Issue #166: Buchungsnummer, Buchungsdatum und Typ teilen sich eine Spalte,
// die Spalte „Anlage" entfällt — damit die Tabelle ohne waagerechtes Scrollen
// lesbar bleibt. Prüfbar nur am gerenderten DOM: die Zellaufteilung existiert
// nur in der View.
test('Buchungsnummer, Datum und Typ stehen in einer gemeinsamen Spalte', async ({ page }) => {
const { id, tag } = await uploadBeleg(page)
belegId = id
@ -149,17 +150,47 @@ test.describe('Beleg-Upload und Buchung', () => {
const zeile = page.locator('table tbody tr', { hasText: tag }).first()
await expect(zeile).toHaveCount(1)
// Erste Zelle trägt beides: Buchungsnummer (BU-JAHR-NNNNNN) und Datum.
// Erste Zelle trägt alle drei Angaben: Buchungsnummer (BU-JAHR-NNNNNN),
// Datum und Buchungstyp.
const ersteZelle = await zeile.locator('td').nth(0).textContent()
expect(ersteZelle).toMatch(/BU-\d{4}-\d+/)
expect(ersteZelle).toMatch(/\d{2}\.\d{2}\.\d{4}/)
expect(ersteZelle).toMatch(/Einnahme|Ausgabe/)
// Die Kopfzeile führt beide Bezeichnungen in einer einzigen Spalte.
// Die Kopfzeile führt alle drei Bezeichnungen in einer einzigen Spalte;
// „Anlage" gibt es nicht mehr.
const kopf = page.locator('table thead tr').first()
const ersteKopfzelle = await kopf.locator('th').nth(0).textContent()
expect(ersteKopfzelle).toContain('Buchungsnr.')
expect(ersteKopfzelle).toContain('Datum')
await expect(kopf.locator('th')).toHaveCount(8)
expect(ersteKopfzelle).toContain('Typ')
await expect(kopf.locator('th')).toHaveCount(6)
await expect(kopf.locator('th', { hasText: 'Anlage' })).toHaveCount(0)
})
// Der Betrag darf nicht zwischen Zahl und Währungszeichen umbrechen.
test('Betrag und Währungszeichen bleiben zusammen', async ({ page }) => {
const { id, tag } = await uploadBeleg(page)
belegId = id
await buchungsmaskeVorbereiten(page, { buchungstext: tag })
await page.fill('#betrag_netto_input', '1234.56')
await page.click('#buchung_form button[type="submit"]')
await page.waitForLoadState('networkidle')
const wert = page.locator('table tbody tr', { hasText: tag }).first().locator('td').nth(3)
const text = await wert.textContent()
// Geschütztes Leerzeichen (U+00A0) statt eines gewöhnlichen Leerzeichens.
expect(text).toBe('1.234,56\u00A0€')
// Und der Betrag steht auch gerendert auf einer einzigen Zeile — ein
// Umbruch zwischen Zahl und Währungszeichen ergäbe zwei Rechtecke.
const fragmente = await wert.evaluate((el) => {
const r = document.createRange()
r.selectNodeContents(el)
return r.getClientRects().length
})
expect(fragmente).toBe(1)
})
// Hinweis: Dass ein Beleg mit Buchungen nicht löschbar ist, ist reine