// @ts-check // Hotspot: Beleg-Upload → Buchungsmaske → gespeicherte Buchung. // // Der Netto/Brutto-Umschalter (Issue #137) ist reines JavaScript: Im Brutto-Modus // steht im Feld `betrag_netto` ein Bruttowert, den ein submit-Handler kurz vor dem // Absenden in den Nettobetrag umrechnet (src/Views/belege/detail.php). // Ohne Browser ist dieser Pfad nicht prüfbar — ein PHP-Test sieht immer nur den // bereits umgerechneten Wert und würde eine kaputte Umrechnung nie bemerken. const { test, expect } = require('@playwright/test') const { uploadBeleg, cleanupBeleg, gotoOk, parseMoney } = require('../helpers/app') const KATEGORIE_KONTO = '4930' // Bürobedarf (Betriebsausgabe) const ZAHLUNGS_KONTO = '1200' // Bank /** Wählt im Konto-Select die Option mit der gegebenen Kontonummer. */ async function waehleKonto(page, selectId, kontonummer) { const option = page.locator(`#${selectId} option`).filter({ hasText: new RegExp(`^\\s*${kontonummer}\\s+–`) }).first() await expect(option, `Konto ${kontonummer} nicht in #${selectId} gefunden`).toHaveCount(1) const value = await option.getAttribute('value') await page.selectOption(`#${selectId}`, value) return value } /** * Füllt die Buchungsmaske bis auf den Betrag aus. * * Reihenfolge beachten: Die Auswahl des Kategorie-Kontos ruft updateAutoUst() * auf und setzt den USt-Satz passend zum Steuercode des Kontos. Der Satz muss * deshalb nach der Kontoauswahl gesetzt werden — sonst wird er überschrieben. */ async function buchungsmaskeVorbereiten(page, { ustSatz = '19', buchungstext }) { await page.check('input[name="buchungstyp"][value="Ausgabe"]') await waehleKonto(page, 'kategorie_konto_id', KATEGORIE_KONTO) await waehleKonto(page, 'zahlungskonto_id', ZAHLUNGS_KONTO) await page.selectOption('#ust_satz_select', ustSatz) await page.fill('input[name="buchungstext"]', buchungstext) } test.describe('Beleg-Upload und Buchung', () => { let belegId = 0 test.afterEach(async ({ page }) => { await cleanupBeleg(page, belegId) belegId = 0 }) test('Beleg wird hochgeladen und erscheint in der Belegliste', async ({ page }) => { const { id, tag } = await uploadBeleg(page) belegId = id await expect(page.locator('body')).toContainText(tag) await gotoOk(page, '/belege') await expect(page.locator('body')).toContainText(tag) }) test('Buchung im NETTO-Modus speichert den eingegebenen Wert unverändert', async ({ page }) => { const { id, tag } = await uploadBeleg(page) belegId = id await buchungsmaskeVorbereiten(page, { buchungstext: tag }) // Netto ist der Default-Modus. await expect(page.locator('#betrag_modus_netto_btn')).toHaveAttribute('aria-pressed', 'true') await page.fill('#betrag_netto_input', '100.00') // Live-Anzeige rechnet vorwärts: Netto 100 → USt 19 → Brutto 119. await expect(page.locator('#ust_brutto_anzeige')).toContainText('19,00') await expect(page.locator('#ust_brutto_anzeige')).toContainText('119,00') await page.click('#buchung_form button[type="submit"]') await page.waitForLoadState('networkidle') 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) }) test('Buchung im BRUTTO-Modus wird vor dem Absenden in Netto umgerechnet', async ({ page }) => { const { id, tag } = await uploadBeleg(page) belegId = id await buchungsmaskeVorbereiten(page, { buchungstext: tag }) await page.click('#betrag_modus_brutto_btn') await expect(page.locator('#betrag_modus_brutto_btn')).toHaveAttribute('aria-pressed', 'true') await expect(page.locator('#betrag_modus_netto_btn')).toHaveAttribute('aria-pressed', 'false') // 119,00 brutto bei 19 % → 100,00 netto. await page.fill('#betrag_netto_input', '119.00') await expect(page.locator('#ust_brutto_anzeige')).toContainText('100,00') await page.click('#buchung_form button[type="submit"]') await page.waitForLoadState('networkidle') const zeile = page.locator('table tbody tr', { hasText: tag }).first() await expect(zeile, 'Buchung wurde nicht gespeichert').toHaveCount(1) // 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()) expect(gespeichert, 'Bruttoeingabe wurde nicht in Netto umgerechnet').toBeCloseTo(100.0, 2) }) test('Moduswechsel rechnet den bereits eingegebenen Betrag um', async ({ page }) => { const { id } = await uploadBeleg(page) belegId = id await page.selectOption('#ust_satz_select', '19') await page.fill('#betrag_netto_input', '100.00') // Netto → Brutto await page.click('#betrag_modus_brutto_btn') await expect(page.locator('#betrag_netto_input')).toHaveValue('119.00') // und wieder zurück await page.click('#betrag_modus_netto_btn') await expect(page.locator('#betrag_netto_input')).toHaveValue('100.00') }) test('Buchung mit 7 % USt rechnet im Brutto-Modus korrekt', async ({ page }) => { const { id, tag } = await uploadBeleg(page) belegId = id await buchungsmaskeVorbereiten(page, { ustSatz: '7', buchungstext: tag }) await page.click('#betrag_modus_brutto_btn') await page.fill('#betrag_netto_input', '107.00') await page.click('#buchung_form button[type="submit"]') await page.waitForLoadState('networkidle') 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) }) // 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 }) => { const { id, tag } = await uploadBeleg(page) belegId = id await buchungsmaskeVorbereiten(page, { buchungstext: tag }) await page.fill('#betrag_netto_input', '100.00') await page.click('#buchung_form button[type="submit"]') await page.waitForLoadState('networkidle') 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. 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}/) // Die Kopfzeile führt beide Bezeichnungen in einer einzigen Spalte. 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) }) // Hinweis: Dass ein Beleg mit Buchungen nicht löschbar ist, ist reine // Model-Logik (Beleg::delete) und wird in tests/77_BelegLoeschsperreTest.php // geprüft — dort schneller, isoliert und ohne Browser. })