Belege zu Buchungen: Durchsuchen, Ziehen-und-Ablegen, Einfügen

Zu jeder Buchung lässt sich ein Beleg hinterlegen, im Formular oder
nachträglich über die Buchungsliste.

Der Medientyp wird aus der Dateisignatur bestimmt, nicht aus Endung oder
Browserangabe: eine als rechnung.pdf benannte Textdatei wird abgewiesen. SVG ist
ausgeschlossen, weil es Skripte enthalten kann, die beim Anzeigen ausgeführt
würden. Ausgeliefert wird mit nosniff und enger Content-Security-Policy.

Zur Aufbewahrungspflicht nach §147 AO werden Belege nie überschrieben und nie
gelöscht. Beim Ersetzen bleibt die alte Datei liegen; das Lösen der Verknüpfung
entfernt nur den Verweis. Der Dateiname trägt den SHA-256-Anfang, die Buchung
den vollständigen Hash.

Ablage unter data/belege/<mandant>/<jahr>/ – je Mandant getrennt und im selben
Bind-Mount wie die Datenbanken, sodass ein Backup des data-Ordners Buchungen und
Belege gemeinsam erfasst.

Bisher fehlte jede Möglichkeit, bestehende Mandantendateien um neue Spalten zu
erweitern: CREATE TABLE IF NOT EXISTS lässt vorhandene Tabellen unverändert. Neu
hinzugekommene Spalten werden jetzt beim Öffnen additiv ergänzt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-08-03 22:28:16 +02:00
co-authored by Claude Opus 5
parent 49ddaeccff
commit b343e89fbf
12 changed files with 765 additions and 4 deletions
+33 -1
View File
@@ -64,6 +64,33 @@ export function ausProjektwurzel(pfad: string): string {
return isAbsolute(pfad) ? pfad : resolve(projektWurzel, pfad);
}
/**
* Nachträglich ergänzte Spalten.
*
* `CREATE TABLE IF NOT EXISTS` lässt bestehende Tabellen unverändert ohne
* diesen Schritt fehlten in bereits angelegten Mandantendateien alle später
* hinzugekommenen Spalten. Die Ergänzung ist bewusst additiv: es wird nie eine
* Spalte entfernt oder umbenannt, damit ältere Stände lesbar bleiben.
*/
const NACHTRAEGLICHE_SPALTEN: { tabelle: string; spalte: string; definition: string }[] = [
{ tabelle: 'buchungen', spalte: 'beleg_name', definition: "TEXT NOT NULL DEFAULT ''" },
{ tabelle: 'buchungen', spalte: 'beleg_typ', definition: "TEXT NOT NULL DEFAULT ''" },
{ tabelle: 'buchungen', spalte: 'beleg_hash', definition: "TEXT NOT NULL DEFAULT ''" },
{ tabelle: 'buchungen', spalte: 'beleg_groesse', definition: 'INTEGER NOT NULL DEFAULT 0' },
{ tabelle: 'konten', spalte: 'ustva_kz', definition: 'TEXT' },
];
function ergaenzeSpalten(verbindung: Database.Database): void {
for (const { tabelle, spalte, definition } of NACHTRAEGLICHE_SPALTEN) {
const vorhanden = (
verbindung.prepare(`PRAGMA table_info(${tabelle})`).all() as { name: string }[]
).some((s) => s.name === spalte);
if (!vorhanden) {
verbindung.exec(`ALTER TABLE ${tabelle} ADD COLUMN ${spalte} ${definition}`);
}
}
}
/** Öffnet eine Mandantendatei und legt das Schema an, falls es noch fehlt. */
export function oeffneDatenbank(datei: string): Database.Database {
const pfad = ausProjektwurzel(datei);
@@ -72,6 +99,7 @@ export function oeffneDatenbank(datei: string): Database.Database {
verbindung.pragma('journal_mode = WAL');
verbindung.pragma('foreign_keys = ON');
verbindung.exec(SCHEMA);
ergaenzeSpalten(verbindung);
verbindung.prepare('INSERT OR IGNORE INTO mandant (id) VALUES (1)').run();
return verbindung;
}
@@ -172,7 +200,11 @@ CREATE TABLE IF NOT EXISTS buchungen (
partner TEXT NOT NULL DEFAULT '',
privatanteil_bp INTEGER NOT NULL DEFAULT 0,
anlagegut_id INTEGER REFERENCES anlagegueter(id),
beleg_datei TEXT,
beleg_datei TEXT, -- Pfad relativ zu data/, siehe belege.ts
beleg_name TEXT NOT NULL DEFAULT '', -- ursprünglicher Dateiname
beleg_typ TEXT NOT NULL DEFAULT '', -- erkannter Medientyp
beleg_hash TEXT NOT NULL DEFAULT '', -- SHA-256, GoBD-Unveränderbarkeit
beleg_groesse INTEGER NOT NULL DEFAULT 0,
-- GoBD: keine Löschung, nur Storno; festgeschriebene Sätze sind unveränderbar
storniert INTEGER NOT NULL DEFAULT 0,
storno_von INTEGER REFERENCES buchungen(id),