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>
134 lines
5 KiB
JavaScript
134 lines
5 KiB
JavaScript
/* 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();
|
||
})();
|