Einnahmen-Überschuss-Rechnung mit ELSTER, DATEV und Mandantenfähigkeit

Gewinnermittlung nach §4 Abs. 3 EStG für Einzelunternehmen und Freiberufler.
Node/Fastify mit SQLite im Backend, React/Vite im Frontend; die Anwendung
läuft lokal ohne Cloud-Anbindung.

Fachlicher Kern ist eine Zeitachse der Besteuerungsform: das umsatzsteuerliche
Regime wird je Buchung aus dem Leistungsdatum aufgelöst, statt aus einer
globalen Ja/Nein-Flagge. Dadurch ist auch ein unterjähriger Wechsel von der
Kleinunternehmerregelung zur Regelbesteuerung abbildbar, wie ihn §19 Abs. 1
Satz 3 UStG seit 2025 beim Überschreiten der 100.000-EUR-Grenze vorsieht.

Enthalten:
- Buchungserfassung nach dem Zu-/Abflussprinzip mit Storno statt Löschung
- Anlagevermögen mit AfA (linear, GWG, Sammelposten); die Aktivierung
  entscheidet das Konto, die Methode der Betrag
- Anlage EÜR, BWA im Aufbau der Form 01, Umsatzsteuer-Voranmeldung
- ELSTER-Anbindung über ERiC mit validierendem Simulationsmodus
- DATEV-Buchungsstapel (EXTF 700) und GoBD-Datenträgerüberlassung (Z3)
- Kontenrahmen SKR03 und SKR04, je Mandant beim Anlegen wählbar
- Mandantenfähigkeit mit einer SQLite-Datei je Mandant; Testmandanten sind
  von der Übermittlung ans Finanzamt ausgeschlossen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-08-03 21:45:52 +02:00
co-authored by Claude Opus 5
commit bf371c8118
59 changed files with 12913 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
import { db } from '../db.js';
import type { Regime, RegimeEintrag, UstBehandlung } from './typen.js';
import { ustAusBrutto, ustAusNetto } from './geld.js';
/**
* Grenzen der Kleinunternehmerregelung (§19 UStG) ab dem Besteuerungszeitraum 2025.
* Bis einschließlich 2024 galten 22.000 EUR (Vorjahr, brutto) / 50.000 EUR (Prognose).
*/
export const KU_GRENZEN = {
bis2024: { vorjahr: 22_000_00, laufend: 50_000_00, laufendIstPrognose: true },
ab2025: { vorjahr: 25_000_00, laufend: 100_000_00, laufendIstPrognose: false },
} as const;
export function grenzenFuer(jahr: number) {
return jahr >= 2025 ? KU_GRENZEN.ab2025 : KU_GRENZEN.bis2024;
}
export function alleRegime(): RegimeEintrag[] {
return db
.prepare('SELECT * FROM steuer_regime ORDER BY gueltig_ab ASC')
.all() as RegimeEintrag[];
}
/**
* Löst das umsatzsteuerliche Regime für ein Datum auf: der jüngste Eintrag,
* dessen `gueltig_ab` nicht in der Zukunft des Datums liegt. Ohne passenden
* Eintrag gilt Kleinunternehmer (konservativer Default für Gründer).
*/
export function regimeAm(datum: string): Regime {
const row = db
.prepare(
'SELECT modus FROM steuer_regime WHERE gueltig_ab <= ? ORDER BY gueltig_ab DESC LIMIT 1',
)
.get(datum) as { modus: Regime } | undefined;
return row?.modus ?? 'kleinunternehmer';
}
/** Gibt es innerhalb des Zeitraums einen Regimewechsel? Wichtig für UStVA-Hinweise. */
export function regimeWechselIm(von: string, bis: string): RegimeEintrag[] {
return db
.prepare('SELECT * FROM steuer_regime WHERE gueltig_ab > ? AND gueltig_ab <= ? ORDER BY gueltig_ab')
.all(von, bis) as RegimeEintrag[];
}
export interface UstErgebnis {
brutto: number;
netto: number;
ust: number;
satzBp: number;
behandlung: UstBehandlung;
vorsteuerAbziehbar: boolean;
hinweise: string[];
}
export interface UstEingabe {
art: 'einnahme' | 'ausgabe';
/** Maßgeblich für das Regime: der Zeitpunkt der Leistungsausführung. */
leistungsdatum: string;
/** Genau eines von beiden angeben. */
brutto?: number;
netto?: number;
satzBp: number;
behandlung: UstBehandlung;
}
/**
* Zentrale USt-Ermittlung. Sie erzwingt die Regeln des zum Leistungsdatum
* geltenden Regimes – dadurch bleibt ein unterjähriger Wechsel automatisch
* konsistent, ohne dass Altbuchungen angefasst werden müssen.
*/
export function berechneUst(e: UstEingabe): UstErgebnis {
const regime = regimeAm(e.leistungsdatum);
const hinweise: string[] = [];
let behandlung = e.behandlung;
let satzBp = e.satzBp;
let vorsteuerAbziehbar = true;
if (regime === 'kleinunternehmer') {
if (e.art === 'einnahme') {
if (behandlung === 'regel' && satzBp > 0) {
hinweise.push(
'Zum Leistungsdatum galt die Kleinunternehmerregelung (§19 UStG): ' +
'der Umsatz ist steuerfrei, es wurde keine Umsatzsteuer angesetzt.',
);
}
behandlung = 'kleinunternehmer';
satzBp = 0;
} else {
// Vorsteuerabzug ist nach §19 Abs. 1 Satz 4 UStG ausgeschlossen ->
// die Ausgabe geht brutto in die EÜR ein.
if (satzBp > 0 && behandlung === 'regel') {
hinweise.push(
'Kein Vorsteuerabzug als Kleinunternehmer – die Betriebsausgabe wird mit dem Bruttobetrag erfasst.',
);
}
vorsteuerAbziehbar = false;
// Reverse-Charge/i.g. Erwerb bleiben auch für Kleinunternehmer steuerpflichtig.
if (behandlung !== 'reverse_charge_13b' && behandlung !== 'ig_erwerb') {
behandlung = 'kleinunternehmer';
satzBp = 0;
} else {
hinweise.push(
'Steuerschuld des Leistungsempfängers besteht auch für Kleinunternehmer – ' +
'eine USt-Voranmeldung ist für diesen Zeitraum abzugeben (§18 Abs. 4a UStG), ' +
'ein Vorsteuerabzug daraus jedoch nicht möglich.',
);
vorsteuerAbziehbar = false;
}
}
} else {
if (behandlung === 'kleinunternehmer') {
hinweise.push(
'Zum Leistungsdatum galt bereits die Regelbesteuerung – Behandlung auf "regel" gesetzt.',
);
behandlung = 'regel';
}
if (
behandlung === 'steuerfrei_ohne_vst' ||
behandlung === 'nicht_steuerbar'
) {
vorsteuerAbziehbar = behandlung === 'nicht_steuerbar';
satzBp = 0;
}
if (behandlung === 'steuerfrei_mit_vst' || behandlung === 'ig_lieferung') {
satzBp = 0;
}
}
let brutto: number;
let netto: number;
let ust: number;
if (behandlung === 'reverse_charge_13b' || behandlung === 'ig_erwerb') {
// Der Rechnungsbetrag ist netto; die Steuer schuldet der Leistungsempfänger.
netto = e.netto ?? e.brutto ?? 0;
ust = ustAusNetto(netto, satzBp);
brutto = netto; // gezahlt wird nur der Nettobetrag
} else if (e.brutto !== undefined) {
brutto = e.brutto;
ust = ustAusBrutto(brutto, satzBp);
netto = brutto - ust;
} else {
netto = e.netto ?? 0;
ust = ustAusNetto(netto, satzBp);
brutto = netto + ust;
}
return { brutto, netto, ust, satzBp, behandlung, vorsteuerAbziehbar, hinweise };
}
export interface GrenzenStatus {
jahr: number;
vorjahresumsatz: number;
laufenderUmsatz: number;
grenzeVorjahr: number;
grenzeLaufend: number;
regimeJetzt: Regime;
vorjahrUeberschritten: boolean;
laufendUeberschritten: boolean;
/** Kleinunternehmerregelung entfällt zum 1. Januar des Folgejahres */
wechselImFolgejahr: boolean;
warnungen: string[];
}
/**
* Überwacht die §19-Grenzen. Maßgeblich ist der Gesamtumsatz nach §19 Abs. 2 UStG
* (vereinnahmte Entgelte); hier näherungsweise die Summe der steuerbaren
* Betriebseinnahmen nach Zuflussprinzip.
*/
export function grenzenStatus(jahr: number): GrenzenStatus {
const umsatz = (j: number): number => {
const r = db
.prepare(
`SELECT COALESCE(SUM(betrag_brutto),0) AS s FROM buchungen
WHERE art='einnahme' AND storniert=0
AND ust_behandlung NOT IN ('nicht_steuerbar')
AND zahlungsdatum BETWEEN ? AND ?`,
)
.get(`${j}-01-01`, `${j}-12-31`) as { s: number };
return r.s;
};
const g = grenzenFuer(jahr);
const vorjahresumsatz = umsatz(jahr - 1);
const laufenderUmsatz = umsatz(jahr);
const regimeJetzt = regimeAm(`${jahr}-12-31`);
const warnungen: string[] = [];
const vorjahrUeberschritten = vorjahresumsatz > g.vorjahr;
const laufendUeberschritten = laufenderUmsatz > g.laufend;
const folgejahr = grenzenFuer(jahr + 1);
const wechselImFolgejahr =
regimeJetzt === 'kleinunternehmer' && laufenderUmsatz > folgejahr.vorjahr;
if (regimeJetzt === 'kleinunternehmer') {
if (wechselImFolgejahr) {
warnungen.push(
`Der Umsatz ${jahr} liegt mit ${(laufenderUmsatz / 100).toLocaleString('de-DE')} EUR über der ` +
`Vorjahresgrenze von ${(folgejahr.vorjahr / 100).toLocaleString('de-DE')} EUR. ` +
`Die Kleinunternehmerregelung entfällt damit ab dem 01.01.${jahr + 1} – bitte zu diesem ` +
'Datum einen Wechsel zur Regelbesteuerung anlegen und ab dann Umsatzsteuer ausweisen.',
);
}
if (vorjahrUeberschritten) {
warnungen.push(
`Der Vorjahresumsatz ${jahr - 1} liegt über ${(g.vorjahr / 100).toLocaleString('de-DE')} EUR. ` +
`Ab dem 01.01.${jahr} greift die Regelbesteuerung – bitte einen Regimewechsel zum 01.01.${jahr} anlegen.`,
);
}
if (laufendUeberschritten && jahr >= 2025) {
warnungen.push(
`Die laufende Grenze von ${(g.laufend / 100).toLocaleString('de-DE')} EUR wurde in ${jahr} überschritten. ` +
'Seit 2025 endet die Kleinunternehmerregelung sofort mit dem Umsatz, der die Grenze übersteigt ' +
'(§19 Abs. 1 Satz 3 UStG) – der Regimewechsel ist unterjährig zu diesem Datum einzutragen.',
);
}
if (!laufendUeberschritten && laufenderUmsatz > (g.laufend * 8) / 10) {
warnungen.push(
`Achtung: ${((laufenderUmsatz / g.laufend) * 100).toFixed(0)} % der laufenden Umsatzgrenze erreicht.`,
);
}
}
return {
jahr,
vorjahresumsatz,
laufenderUmsatz,
grenzeVorjahr: g.vorjahr,
grenzeLaufend: g.laufend,
regimeJetzt,
vorjahrUeberschritten,
laufendUeberschritten,
wechselImFolgejahr,
warnungen,
};
}