Finanzmanager/public/static/buchungsmaske.js
fds2610-bot 1dbc848cd1 Phase 2: Buchungen, Kontoblatt und QIF-Buchungsimport
Der Plan ging davon aus, Umbuchungen stünden zweimal im Export und müssten
zusammengeführt werden. Der Vollexport zeigt das Gegenteil: Eine Umbuchung
steht einmal, in dem Konto, in dem sie erfasst wurde; die Gegenseite ist nur
durch L[Zielkonto] benannt und muss beim Import erzeugt werden. Von 965
Umbuchungen hat keine ein zweites Vorkommen. Mit erzeugter Gegenseite stimmen
acht gegen den FinanzManager geprüfte Kontosalden auf den Cent.

Datenmodell: Migration 003 mit buchungen, buchung_splits und buchungsadressen.
Eine Umbuchung ist ein Datensatz mit gegenkonto_id, kein gespiegeltes Paar -
zwei Zeilen laufen früher oder später auseinander. Das Kontoblatt des
Gegenkontos zeigt sie mit umgekehrtem Vorzeichen. Auch einzelne Splitzeilen
dürfen ein Gegenkonto tragen. Konten führen zusätzlich in_summe, die
Sigma-Spalte der Kontenübersicht, und saldo_online für Phase 5.

Oberfläche: Kontoblatt mit laufendem Saldo, Filter auf Zeitraum, Kategorie,
Klasse und Abgleich, Volltextsuche und Seitenblättern. Buchungsmaske für
Einnahme, Ausgabe und Umbuchung in beide Richtungen, mit Splitzeilen und
Restbetragsanzeige.

Import: QifBuchungsImport arbeitet mit Stichtag. Alles davor wird je Konto zum
Anfangssaldo aufsummiert, Gegenbuchungen eingerechnet; alles danach bis zum
Importtag kommt einzeln in die Datenbank. Spätere Buchungen sind
Vorausberechnungen des FinanzManagers und bleiben draußen. Wertpapiergeschäfte
schlagen vorerst nur mit ihrem Geldfluss auf dem Verrechnungskonto zu Buche.
Am Vollexport geprüft: 833 Buchungen ab 01.01.2026, 7946 in die Anfangssalden
von 35 Konten verrechnet.

Behoben: Die confirm()-Rückfragen vor dem Löschen waren wirkungslos.
public/static/app.js brachte den Mechanismus mit, war aber nie im Layout
eingebunden; die ersatzweise gesetzten onsubmit-Attribute verstoßen gegen die
eigene Content-Security-Policy und wurden vom Browser verworfen - gelöscht
wurde ohne Rückfrage. Ein E2E-Test besteht jetzt auf dem Dialog.

Tests: 307 PHP-Prüfungen und 21 Playwright-Tests, alle grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:40:31 +02:00

107 lines
3.5 KiB
JavaScript
Raw 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: Felder je nach Art ein- und ausblenden, Restbetrag der
* Splitzeilen mitrechnen.
*
* Nur Komfort – die Maske kommt fertig vom Server. Ohne JavaScript bleibt alles
* bedienbar: dann sind sämtliche Felder sichtbar, und die Splitsumme prüft
* ohnehin das Model.
*/
(function () {
'use strict';
const art = document.getElementById('art');
const maske = document.querySelector('.buchungsmaske');
if (!art || !maske) {
return;
}
const kategorie = maske.querySelector('.nur-kategorie');
const umbuchung = maske.querySelector('.nur-umbuchung');
const gegenkonto = document.getElementById('gegenkonto_id');
const summenfeld = document.getElementById('splitsumme');
const betragsfeld = document.getElementById('betrag');
function zeigePassendeFelder() {
const istUmbuchung = art.value.indexOf('umbuchung') === 0;
if (kategorie) {
kategorie.hidden = istUmbuchung;
}
if (umbuchung) {
umbuchung.hidden = !istUmbuchung;
}
if (gegenkonto) {
gegenkonto.required = istUmbuchung;
}
}
/* Beträge stehen im deutschen Format im Feld. Dieselbe Regel wie
* KontenController::zahl(): ohne Komma entscheidet die Stellenzahl, ob der
* Punkt Tausender- oder Dezimaltrenner ist. */
function zahl(text) {
const bereinigt = String(text).replace(/[\s€]/g, '');
if (bereinigt === '') {
return 0;
}
if (bereinigt.indexOf(',') !== -1) {
return parseFloat(bereinigt.replace(/\./g, '').replace(',', '.')) || 0;
}
const punkte = (bereinigt.match(/\./g) || []).length;
const letzter = bereinigt.split('.').pop();
if (punkte > 1 || (punkte === 1 && letzter.length === 3)) {
return parseFloat(bereinigt.replace(/\./g, '')) || 0;
}
return parseFloat(bereinigt) || 0;
}
function formatiere(wert) {
return wert.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function rechneSumme() {
if (!summenfeld) {
return;
}
let summe = 0;
let belegt = false;
maske.querySelectorAll('.splitbetrag').forEach(function (feld) {
if (feld.value.trim() === '') {
return;
}
belegt = true;
summe += zahl(feld.value);
});
if (!belegt) {
summenfeld.textContent = '0,00';
summenfeld.classList.remove('negativ');
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 = zahl(betragsfeld ? betragsfeld.value : '0');
if (art.value === 'ausgabe' || art.value === 'umbuchung_ab') {
ziel = -ziel;
}
const rest = Math.round((ziel - summe) * 100) / 100;
summenfeld.textContent = formatiere(summe) + (rest === 0 ? '' : ' (Rest ' + formatiere(rest) + ')');
summenfeld.classList.toggle('negativ', rest !== 0);
}
art.addEventListener('change', function () {
zeigePassendeFelder();
rechneSumme();
});
maske.addEventListener('input', function (ereignis) {
if (ereignis.target.classList.contains('splitbetrag') || ereignis.target.id === 'betrag') {
rechneSumme();
}
});
zeigePassendeFelder();
rechneSumme();
})();