diff --git a/Changes.md b/Changes.md index cf4bde8..0fc1276 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,13 @@ --- +## 2026-07-27 – Feature: Netto/Brutto-Umschalter in der Buchungsmaske (Issue #137) + +- Beim Erfassen einer Buchung musste bisher immer der Nettobetrag eingegeben werden – lag nur der Bruttobetrag vor (z. B. von einem Kassenbon), führte die manuelle Rückrechnung regelmäßig zu Rundungsdifferenzen. +- **`src/Views/belege/detail.php`:** Das Label „Nettobetrag (€)" wurde durch zwei Umschalt-Buttons „NETTOBETRAG (€)" / „BRUTTOBETRAG (€)" ersetzt (`setBetragModus()`); der jeweils aktive Button ist optisch eingedrückt hervorgehoben und bestimmt, wie der eingegebene Betrag interpretiert wird. Die Live-Anzeige (`updateNettoAnzeige()`) zeigt je nach Modus die jeweils fehlenden Werte (Netto/USt. bzw. USt./Brutto) an. Beim Absenden des Formulars wird ein im Brutto-Modus eingegebener Betrag serverseitig weiterhin als Nettobetrag benötigt – die Umrechnung erfolgt daher unmittelbar vor dem Submit per JavaScript, sodass Backend (`BookingController`, `Models/Booking.php`) unverändert bleibt. +- **`public/static/css/app.css`:** Neue Klasse `.betrag-modus-btn` (inkl. `.active`-Zustand) für den Umschalter. +- **`tests/61_BetragModusToggleTest.php`:** Neuer Test prüft Vorhandensein der Umschalt-Buttons, der zugehörigen JS-Funktionen sowie der Umrechnung vor dem Absenden. + ## 2026-07-27 – Feature: Lieferanten-Typeahead in der Belegliste (Issue #138) - Beim Klick in die Spalte „Lieferant" der Belegliste (Inline-Bearbeitung) gab es bisher nur ein freies Textfeld – Lieferanten konnten dadurch versehentlich mit unterschiedlicher Schreibweise mehrfach angelegt werden. diff --git a/docs/funktionen.md b/docs/funktionen.md index 98e114e..f13cdce 100644 --- a/docs/funktionen.md +++ b/docs/funktionen.md @@ -24,6 +24,7 @@ Vollständige Kontenliste: [kontenrahmen.md](kontenrahmen.md) EÜR-Buchungsmodell (Einnahme/Ausgabe) mit Kategorie- und Zahlungskonto. +- **Netto-/Brutto-Umschalter**: Der Betrag kann in der Buchungsmaske wahlweise als Netto- oder Bruttobetrag eingegeben werden (Umschalt-Buttons oberhalb des Betragsfelds); der jeweils aktive Button bestimmt die Interpretation. Vermeidet manuelle Rundungsdifferenzen bei bekanntem Bruttobetrag. - **Steuerarten** für Auslandsumsätze: Inland, steuerfreie ig. Lieferung (Kz 41), steuerfreie Ausfuhr (Kz 44), ig. Erwerb 19 % (Kz 89/91), ig. Erwerb 7 % (Kz 93/94) - **Bewirtungsausgaben**: automatischer Split 70 %/30 % bei Konten 4650, 4651, 4652 - **Eigenanteil** (30 %) bei Telefon/Internet (Konten 4920/4925): optional zubuchbar diff --git a/public/static/css/app.css b/public/static/css/app.css index cf3c0f3..ddaa19d 100644 --- a/public/static/css/app.css +++ b/public/static/css/app.css @@ -212,6 +212,27 @@ img { max-width: 100%; } /* ── Forms ───────────────────────────────────────────────────── */ .form-group { margin-bottom: 1rem; } +.betrag-modus-btn { + padding: .35rem .7rem; + font-size: .75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .04em; + border: 1px solid var(--color-border); + background: var(--color-surface); + color: var(--color-muted); + border-radius: var(--radius); + cursor: pointer; + transition: background .15s, color .15s, border-color .15s, box-shadow .15s; +} +.betrag-modus-btn:hover { background: var(--color-bg); } +.betrag-modus-btn.active { + background: var(--color-primary); + color: #fff; + border-color: var(--color-primary); + box-shadow: inset 0 1px 3px rgba(0,0,0,.3); +} + label { display: block; font-size: .8rem; diff --git a/src/Views/belege/detail.php b/src/Views/belege/detail.php index b2c46a0..5ef2d14 100644 --- a/src/Views/belege/detail.php +++ b/src/Views/belege/detail.php @@ -430,7 +430,7 @@ $suppliers = $suppliers ?? []; -
+
- +
+ + +

- +
+ +
v.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' €'; - anzeige.innerHTML = 'USt. ' + fmt(ust) + '  ·  Brutto ' + fmt(brutto); + + if (_betragModus === 'brutto') { + const netto = Math.round(eingabe / (1 + ustSatz / 100) * 100) / 100; + const ust = Math.round((eingabe - netto) * 100) / 100; + anzeige.innerHTML = 'Netto ' + fmt(netto) + '  ·  USt. ' + fmt(ust); + } else { + const ust = Math.round(eingabe * ustSatz / 100 * 100) / 100; + const brutto = Math.round((eingabe + ust) * 100) / 100; + anzeige.innerHTML = 'USt. ' + fmt(ust) + '  ·  Brutto ' + fmt(brutto); + } +} + +// Vor dem Absenden: Im Brutto-Modus enthält das Feld einen Bruttobetrag, +// das Backend erwartet aber immer den Nettobetrag (Feldname betrag_netto). +const _buchungFormEl = document.getElementById('buchung_form'); +if (_buchungFormEl) { + _buchungFormEl.addEventListener('submit', function () { + if (_betragModus !== 'brutto') return; + const input = document.getElementById('betrag_netto_input'); + const ustEl = document.getElementById('ust_satz_select'); + const ustSatz = ustEl ? (parseFloat(ustEl.value) || 0) : 0; + const brutto = parseFloat(input.value); + if (!isNaN(brutto)) { + input.value = (Math.round(brutto / (1 + ustSatz / 100) * 100) / 100).toFixed(2); + } + }); } @@ -1015,8 +1087,7 @@ function _applyKiVorschlag(v) { const zhlEl2 = document.getElementById('zahlungskonto_id'); _selectById(zhlEl2, v.zahlungskonto_id); if (zhlEl2) transferKontoNr(zhlEl2); - const netEl = document.querySelector('input[name="betrag_netto"]'); - if (netEl && v.betrag_netto) { netEl.value = v.betrag_netto; updateNettoAnzeige(); } + if (v.betrag_netto) setBetragNettoWert(v.betrag_netto); const txtEl = document.querySelector('input[name="buchungstext"]'); if (txtEl && v.buchungstext) txtEl.value = v.buchungstext; diff --git a/tests/61_BetragModusToggleTest.php b/tests/61_BetragModusToggleTest.php new file mode 100644 index 0000000..b6ee142 --- /dev/null +++ b/tests/61_BetragModusToggleTest.php @@ -0,0 +1,75 @@ +assert( + $content !== false, + 'View-Datei belege/detail.php ist lesbar' + ); + + $this->assert( + str_contains($content, 'id="betrag_modus_netto_btn"') + && str_contains($content, 'id="betrag_modus_brutto_btn"'), + 'Beide Umschalt-Buttons (Netto/Brutto) sind vorhanden' + ); + + $this->assert( + str_contains($content, "onclick=\"setBetragModus('netto')\"") + && str_contains($content, "onclick=\"setBetragModus('brutto')\""), + 'Buttons rufen setBetragModus() mit dem jeweiligen Modus auf' + ); + + $this->assert( + str_contains($content, 'function setBetragModus(modus)'), + 'JavaScript-Funktion setBetragModus() ist definiert' + ); + + $this->assert( + str_contains($content, 'function getBetragModus()'), + 'JavaScript-Funktion getBetragModus() ist definiert' + ); + + $this->assert( + str_contains($content, "classList.toggle('active'"), + 'Aktiver Button wird optisch (CSS-Klasse) hervorgehoben' + ); + + // Vor dem Absenden muss im Brutto-Modus der Feldwert in den Nettobetrag + // umgerechnet werden, da das Backend ausschließlich betrag_netto erwartet. + $this->assert( + str_contains($content, "if (_betragModus !== 'brutto') return;") + && str_contains($content, "addEventListener('submit'"), + 'Vor dem Absenden wird im Brutto-Modus auf Nettobetrag umgerechnet' + ); + + // CSS-Klasse für den Umschalter muss definiert sein + $cssFile = __DIR__ . '/../public/static/css/app.css'; + $cssContent = file_get_contents($cssFile); + + $this->assert( + $cssContent !== false && str_contains($cssContent, '.betrag-modus-btn'), + 'CSS-Klasse .betrag-modus-btn ist definiert' + ); + + $this->assert( + str_contains($cssContent, '.betrag-modus-btn.active'), + 'CSS für aktiven Zustand des Umschalters ist definiert' + ); + } +}