Finanzmanager/public/static/buchungsmaske.js
fds2610-bot 275acd3285 Fix #134: Buchungen bewusst ohne Kategorie ermöglichen
Neuer Schalter „Bewusst ohne Kategorie“ in der Buchungsmaske für
Abschreibungen und rein rechnerische Solarerträge. Solche Buchungen
erscheinen nicht unter den offenen Vorgängen im Cockpit, nicht im
Prüflauf des Monatsabschlusses, setzen das Konto in der Kontenübersicht
nicht fett und werden von der Regelanwendung sowie dem automatischen
Zusammenführen zu Umbuchungen ausgelassen. Kommt eine Kategorie, ein
Gegenkonto oder eine Splitbuchung hinzu, entfällt die Markierung.

Migration 028_bewusst_ohne_kategorie.sql, Test 59 und E2E-Test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:22:22 +02:00

134 lines
5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Buchungsmaske: Restbetrag der Splitzeilen mitrechnen und direkte Zuordnung
* und Splitbuchung gegeneinander sperren.
*
* Solange Splitzeilen befüllt sind, aber noch ein Restbetrag zum
* Buchungswert offen ist, bleibt „Speichern“ gesperrt – das Model wiese die
* Buchung sonst ohnehin ab, hier spart es die Rundreise zum Server.
*
* Nur Komfort – die Maske kommt fertig vom Server. Ohne JavaScript bleibt alles
* bedienbar: Die Splitsumme prüft ohnehin das Model.
*
* Eine Buchung ist entweder direkt zugeordnet (Kategorie / Gegenkonto, Klasse)
* oder eine Splitbuchung. Sobald in einer Splitzeile etwas steht, sind die
* beiden Felder oben gesperrt; steht oben eine Kategorie oder Klasse, sind es
* die Splitzeilen. Gesperrte Felder schickt der Browser nicht mit – was dort
* noch stehen mag, geht also nicht in die Buchung ein. Der Knopf
* „Splitbuchungen löschen“ leert alle Splitzeilen und gibt oben wieder frei.
*
* Der Schalter „Bewusst ohne Kategorie“ (#134) sperrt Kategorie / Gegenkonto
* und die Splitzeilen; umgekehrt lässt er sich nur setzen, solange dort nichts
* steht. Die Klasse bleibt frei – sie ist keine Zuordnung.
*
* Kategorie und Gegenkonto stehen in einem Feld, deshalb gibt es hier nichts
* mehr ein- und auszublenden; die Art bestimmt nur noch die Richtung.
*/
(function () {
'use strict';
const art = document.getElementById('art');
const maske = document.querySelector('.buchungsmaske');
if (!art || !maske || !window.FmBetrag) {
return;
}
const summenfeld = document.getElementById('splitsumme');
const betragsfeld = document.getElementById('betrag');
const speichernknopf = maske.querySelector('button[type="submit"]');
function rechneSumme() {
if (!summenfeld) {
return;
}
// Der Buchungsbetrag wird positiv eingegeben; das Vorzeichen ergibt
// sich aus der Art. Für den Vergleich zählt der Betrag mit Richtung.
let ziel = window.FmBetrag.zahl(betragsfeld ? betragsfeld.value : '0');
if (art.value !== 'einnahme') {
ziel = -ziel;
}
const stand = window.FmBetrag.splitsumme(
Array.from(maske.querySelectorAll('.splitbetrag')),
ziel
);
summenfeld.textContent = stand.text;
summenfeld.classList.toggle('negativ', stand.belegt && stand.rest !== 0);
// Gesperrt nur, solange tatsächlich Splitzeilen mit Beträgen stehen und
// deren Summe den Buchungswert verfehlt. Eine direkte Zuordnung (oder
// gar keine Eingabe) deckt den ganzen Betrag stets ab.
if (speichernknopf) {
speichernknopf.disabled = stand.belegt && stand.rest !== 0;
}
}
const direktfelder = ['zuordnung', 'klasse_id']
.map(function (id) { return document.getElementById(id); })
.filter(Boolean);
const splitkasten = document.getElementById('splitzeilen');
const leerenknopf = document.getElementById('splitsleeren');
const ohneKategorie = document.getElementById('bewusst_ohne_kategorie');
const zuordnungsfeld = document.getElementById('zuordnung');
function splitfelder() {
return splitkasten ? Array.from(splitkasten.querySelectorAll('input, select')) : [];
}
function belegt(felder) {
return felder.some(function (feld) { return feld.value.trim() !== ''; });
}
function sperrenAktualisieren() {
const splits = splitfelder();
const splitBelegt = belegt(splits);
// Stehen beim Öffnen beide Seiten voll (ältere Daten), gilt wie im
// Model die Splitbuchung.
const direktBelegt = !splitBelegt && belegt(direktfelder);
direktfelder.forEach(function (feld) { feld.disabled = splitBelegt; });
splits.forEach(function (feld) { feld.disabled = direktBelegt; });
if (ohneKategorie) {
const ohne = ohneKategorie.checked;
if (ohne) {
splits.forEach(function (feld) { feld.disabled = true; });
if (zuordnungsfeld) {
zuordnungsfeld.disabled = true;
}
}
ohneKategorie.disabled = !ohne
&& (splitBelegt || (zuordnungsfeld !== null && zuordnungsfeld.value !== ''));
}
if (leerenknopf) {
leerenknopf.hidden = !splitBelegt;
}
}
function splitsLeeren() {
splitfelder().forEach(function (feld) {
feld.value = '';
});
sperrenAktualisieren();
rechneSumme();
}
art.addEventListener('change', rechneSumme);
maske.addEventListener('input', function (ereignis) {
if (ereignis.target.classList.contains('splitbetrag') || ereignis.target.id === 'betrag') {
rechneSumme();
}
sperrenAktualisieren();
});
maske.addEventListener('change', sperrenAktualisieren);
if (leerenknopf) {
leerenknopf.addEventListener('click', splitsLeeren);
}
sperrenAktualisieren();
rechneSumme();
})();