// @ts-check const { test, expect } = require('@playwright/test') const { komboboxWaehlen } = require('./hilfen') /* * Der Weg, den die Wertpapierabrechnung nimmt: Sie kommt als Buchung aufs * Girokonto, in der Zuordnungsliste wird das Depot gewählt, und dann folgen die * beiden Dialoge – vorne das Geschäft, dahinter die Kosten. */ const BENUTZER = process.env.E2E_USER || 'e2e' const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort' const MARKE = Date.now().toString().slice(-6) const GIRO = `E2E WP Giro ${MARKE}` const DEPOT = `E2E WP Depot ${MARKE}` const KAT_GEBUEHR = `E2E Bankprovision ${MARKE}` const KAT_STEUER = `E2E Kapitalertragsteuer ${MARKE}` const KLASSE = `E2E WP Klasse ${MARKE}` // ISIN und WKN tragen die Marke des Laufs: Ein Papier wird über seine Kennung // wiedergefunden, nicht über den Namen. Feste Kennungen würden den Test beim // zweiten Lauf an das Papier des ersten hängen. const ISIN = `LU1834${MARKE}` const WKN = MARKE const ZWECK = `Depot 0502022353 Wertp.Abrechn. 03.08.2026 WKN ${WKN} Gesch.Art KV ` + `MUL-AMUN ST600 BANK ETF A ISIN ${ISIN} Ihr Wertpapie` /** Ein Sparplan bringt keine Wertpapierkennung mit – das Papier trägt nur seinen Namen. */ const ZWECK_SPARPLAN = 'Sparplan Rate 03.08.2026 Dauerauftrag Depot 0502022353' async function anmelden(page) { await page.goto('/login') if (page.url().includes('/einrichtung')) { throw new Error( 'Instanz ist noch nicht eingerichtet: ' + 'docker compose exec -u www-data php php scripts/e2e-benutzer.php' ) } await page.fill('#benutzername', BENUTZER) await page.fill('#passwort', PASSWORT) await page.click('button[type="submit"]') await expect(page).toHaveURL(/\/$/) } async function kontoAnlegen(page, name, kontoart, anfangssaldo) { await page.goto('/konten/neu') await page.fill('#name', name) await page.selectOption('#kontoart', kontoart) await page.fill('#anfangssaldo', anfangssaldo) await page.click('.formular button[type="submit"]') await expect(page).toHaveURL(/\/konten\/\d+\/bearbeiten/) const treffer = page.url().match(/\/konten\/(\d+)\//) return treffer ? treffer[1] : null } async function kategorieAnlegen(page, name) { await page.goto('/verwaltung/kategorien/neu') await page.fill('#name', name) await page.selectOption('#art', 'ausgabe') await page.click('.formular button[type="submit"]') } /** Eine unzugeordnete Buchung auf dem Girokonto – wie sie der Abruf hinterlässt. */ async function buchungAnlegen(page, giroId, betrag, empfaenger, zweck = ZWECK) { await page.goto(`/konten/${giroId}/buchung`) await page.selectOption('#art', betrag < 0 ? 'ausgabe' : 'einnahme') await page.fill('#datum', '2026-08-03') await page.fill('#betrag', String(Math.abs(betrag)).replace('.', ',')) await page.fill('#empfaenger', empfaenger) await page.fill('#buchungstext', zweck) await page.click('.buchungsmaske button[type="submit"]') await expect(page.locator('.meldung-erfolg')).toBeVisible() } test.describe('Wertpapiergeschäft aus dem Kontoblatt', () => { let giroId = null test.beforeAll(async ({ browser }) => { const page = await browser.newPage() await anmelden(page) giroId = await kontoAnlegen(page, GIRO, 'bank', '10.000,00') await kontoAnlegen(page, DEPOT, 'depot', '0,00') await kategorieAnlegen(page, KAT_GEBUEHR) await kategorieAnlegen(page, KAT_STEUER) await page.goto('/verwaltung/klassen') await page.click('#neu-knopf') await page.fill('form[action="/verwaltung/klassen/neu"] #name', KLASSE) await page.click('form[action="/verwaltung/klassen/neu"] button.primaer') // Die Kostenarten bekommen ihre feste Kategorie – danach fragt der // Kostendialog nur noch Beträge ab. await page.goto('/einstellungen?bereich=wertpapiere') await page.selectOption('#wertpapiere_kategorie_bankprovision', { label: KAT_GEBUEHR }) await page.selectOption('#wertpapiere_kategorie_kapitalertragsteuer', { label: KAT_STEUER }) await page.click('.formular button[type="submit"]') await expect(page.locator('.meldung-erfolg')).toBeVisible() await page.close() }) test.beforeEach(async ({ page }) => { await anmelden(page) }) test('Kauf mit Kosten wird zur Depotbuchung und zur Splitbuchung', async ({ page }) => { const empfaenger = `Wertpapierkauf ${MARKE}` await buchungAnlegen(page, giroId, -255, empfaenger) const zeile = page.locator('tr', { hasText: empfaenger }) await zeile.locator('a.zuordnungsfeld').click() await komboboxWaehlen(zeile.locator('#zuordnungsauswahl select'), `[${DEPOT}] …`) const dialog = page.locator('#wertpapierdialog') await expect(dialog).toBeVisible() await expect(dialog).toContainText('-255,00') await expect(dialog).toContainText(DEPOT) // Ein Abfluss kann nur ein Kauf sein. await expect(page.locator('#wp_vorgang')).toHaveValue('kauf') // ISIN und WKN stehen im Verwendungszweck und sind schon eingetragen. await expect(page.locator('#wp_isin')).toHaveValue(ISIN) await expect(page.locator('#wp_wkn')).toHaveValue(WKN) await page.fill('#wp_name', `LYXOR ETF ${MARKE}`) await page.selectOption('#wp_art', 'fonds') await page.fill('#wp_stueck', '3,3593') await page.fill('#wp_kurs', '74,42026613') // Ohne Kosten ist der ganze Betrag Einstand. await expect(page.locator('#wp_einstand')).toContainText('255,00') // Der zweite Dialog: die Nebenkosten. await page.click('#wp_kostenknopf') const kosten = page.locator('#kostendialog') await expect(kosten).toBeVisible() await expect(kosten).toContainText(KAT_GEBUEHR) await kosten.locator('#kosten_bankprovision').fill('4,00') await kosten.locator('#kosten_spesen').fill('1,00') await expect(page.locator('#wp_kostensumme')).toContainText('5,00') await page.click('#wp_kostenfertig') await expect(kosten).toBeHidden() // Was nach den Kosten übrig bleibt, ist der Einstandspreis. await expect(page.locator('#wp_einstand')).toContainText('250,00') await dialog.locator('button[type="submit"]').click() await expect(page.locator('.meldung-erfolg')).toContainText('Einstandspreis 250,00') // Im Kontoblatt steht jetzt eine Splitbuchung – die eine Buchung der // Bank, aufgeteilt, nicht eine zweite daneben. const danach = page.locator('tr', { hasText: empfaenger }).first() await expect(danach).toContainText('Splitbuchung') await expect(danach).toContainText('-255,00') // Die Depotbuchung führt Stück, Kurs und Nebenkosten. await page.goto('/wertpapiere/buchungen') const depotzeile = page.locator('tr', { hasText: `LYXOR ETF ${MARKE}` }) await expect(depotzeile).toContainText('250,00') await expect(depotzeile).toContainText('3,3593') }) test('Kauf ohne Kosten bleibt eine Umbuchung ins Depot', async ({ page }) => { const empfaenger = `Sparplan ${MARKE}` await buchungAnlegen(page, giroId, -1000, empfaenger, ZWECK_SPARPLAN) const zeile = page.locator('tr', { hasText: empfaenger }) await zeile.locator('a.zuordnungsfeld').click() await komboboxWaehlen(zeile.locator('#zuordnungsauswahl select'), `[${DEPOT}] …`) await expect(page.locator('#wertpapierdialog')).toBeVisible() await page.fill('#wp_name', `Sparplan-Fonds ${MARKE}`) await page.fill('#wp_stueck', '10') // Die Klasse wird im Dialog gleich mitgewählt (#125). await expect(page.locator('#wp_klasse')).toHaveValue('') await page.selectOption('#wp_klasse', { label: KLASSE }) await page.locator('#wertpapierdialog button[type="submit"]').click() await expect(page.locator('.meldung-erfolg')).toContainText('Einstandspreis 1.000,00') // Keine Splitbuchung mit einer einzigen Zeile, sondern eine Umbuchung. const danach = page.locator('tr', { hasText: empfaenger }).first() await expect(danach).toContainText(`[${DEPOT}]`) await expect(danach.locator('a.klassenfeld')).toHaveText(KLASSE) // Ohne Kennung im Verwendungszweck entsteht das Papier unter dem // Namen, der in der Maske steht. await page.goto('/wertpapiere/buchungen') await expect(page.locator('tr', { hasText: `Sparplan-Fonds ${MARKE}` })) .toContainText('1.000,00') }) })