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
+185
View File
@@ -0,0 +1,185 @@
import { useState } from 'react';
import { api, useDaten } from '../lib/api';
import { useJahr } from '../lib/jahr';
import { centAus, datum, eur } from '../lib/format';
interface AnlageMitAfa {
id: number;
bezeichnung: string;
anschaffungsdatum: string;
akosten_netto: number;
akosten_brutto: number;
nutzungsdauer: number;
afa_methode: string;
abgang_datum: string | null;
notiz: string;
afa: {
bemessung: number;
buchwertAnfang: number;
afa: number;
buchwertEnde: number;
restbuchwertAbgang: number;
hinweis: string;
};
}
const METHODEN: Record<string, string> = {
linear: 'linear (§7 Abs. 1 EStG)',
gwg_sofort: 'GWG-Sofortabschreibung',
sammelposten: 'Sammelposten über 5 Jahre',
keine: 'nicht abnutzbar',
};
export default function Anlagen() {
const [jahr, setJahr, jahre] = useJahr();
const { daten, neuLaden } = useDaten<AnlageMitAfa[]>(`/api/anlagen?jahr=${jahr}`);
const [form, setForm] = useState({
bezeichnung: '',
anschaffungsdatum: new Date().toISOString().slice(0, 10),
nettoFeld: '',
bruttoFeld: '',
nutzungsdauer: '3',
afa_methode: '',
});
const [meldung, setMeldung] = useState<string | null>(null);
async function anlegen(e: React.FormEvent) {
e.preventDefault();
try {
const netto = centAus(form.nettoFeld);
await api.post('/api/anlagen', {
bezeichnung: form.bezeichnung,
anschaffungsdatum: form.anschaffungsdatum,
akosten_netto: netto,
akosten_brutto: form.bruttoFeld ? centAus(form.bruttoFeld) : netto,
nutzungsdauer: Number(form.nutzungsdauer) || 3,
afa_methode: form.afa_methode || undefined,
});
setForm({ ...form, bezeichnung: '', nettoFeld: '', bruttoFeld: '' });
setMeldung('Anlagegut angelegt.');
neuLaden();
} catch (err) {
setMeldung((err as Error).message);
}
}
async function abgangEintragen(a: AnlageMitAfa) {
const d = prompt(`Abgangsdatum für "${a.bezeichnung}" (JJJJ-MM-TT):`, `${jahr}-12-31`);
if (!d) return;
await api.put(`/api/anlagen/${a.id}`, { abgang_datum: d });
neuLaden();
}
const summeAfa = (daten ?? []).reduce((s, a) => s + a.afa.afa, 0);
const summeBuchwert = (daten ?? []).reduce((s, a) => s + a.afa.buchwertEnde, 0);
return (
<>
<h1>Anlagevermögen</h1>
<p className="untertitel">
Wirtschaftsgüter mit mehrjähriger Nutzung sind nicht sofort abziehbar, sondern über die
Nutzungsdauer abzuschreiben. Die AfA fließt automatisch in EÜR und BWA ein.
</p>
<div className="filterreihe">
<div className="feld">
<label>Jahr</label>
<select value={jahr} onChange={(e) => setJahr(Number(e.target.value))}>
{jahre.map((j) => <option key={j} value={j}>{j}</option>)}
</select>
</div>
</div>
<div className="kachelreihe">
<div className="kachel">
<div className="beschriftung">Abschreibung {jahr}</div>
<div className="wert zahl">{eur(summeAfa)}</div>
</div>
<div className="kachel">
<div className="beschriftung">Buchwert 31.12.{jahr}</div>
<div className="wert zahl">{eur(summeBuchwert)}</div>
</div>
</div>
<div className="karte">
<h2>Anlagegut erfassen</h2>
<form onSubmit={anlegen}>
<div className="formularraster">
<div className="feld" style={{ gridColumn: 'span 2' }}>
<label>Bezeichnung</label>
<input required value={form.bezeichnung} onChange={(e) => setForm({ ...form, bezeichnung: e.target.value })} />
</div>
<div className="feld">
<label>Anschaffungsdatum</label>
<input type="date" required value={form.anschaffungsdatum} onChange={(e) => setForm({ ...form, anschaffungsdatum: e.target.value })} />
</div>
<div className="feld">
<label>Anschaffungskosten netto</label>
<input required inputMode="decimal" placeholder="0,00" value={form.nettoFeld} onChange={(e) => setForm({ ...form, nettoFeld: e.target.value })} />
</div>
<div className="feld">
<label> brutto</label>
<input inputMode="decimal" placeholder="leer = wie netto" value={form.bruttoFeld} onChange={(e) => setForm({ ...form, bruttoFeld: e.target.value })} />
<span className="hilfe">für Kleinunternehmer maßgeblich</span>
</div>
<div className="feld">
<label>Nutzungsdauer (Jahre)</label>
<input inputMode="numeric" value={form.nutzungsdauer} onChange={(e) => setForm({ ...form, nutzungsdauer: e.target.value })} />
</div>
<div className="feld">
<label>AfA-Methode</label>
<select value={form.afa_methode} onChange={(e) => setForm({ ...form, afa_methode: e.target.value })}>
<option value="">automatisch nach Betrag</option>
{Object.entries(METHODEN).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
</div>
</div>
<div className="knopfreihe">
<button type="submit" className="primaer">Anlegen</button>
</div>
</form>
{meldung && <div className="hinweis" style={{ marginTop: 12 }}>{meldung}</div>}
</div>
<div className="karte">
<div className="tabellenrahmen">
<table>
<thead>
<tr>
<th>Wirtschaftsgut</th><th>Anschaffung</th><th>Methode</th>
<th className="rechts">Bemessung</th><th className="rechts">Buchwert 1.1.</th>
<th className="rechts">AfA {jahr}</th><th className="rechts">Buchwert 31.12.</th><th></th>
</tr>
</thead>
<tbody>
{(daten ?? []).map((a) => (
<tr key={a.id}>
<td>
{a.bezeichnung}
{a.abgang_datum && <div className="abzeichen">Abgang {datum(a.abgang_datum)}</div>}
</td>
<td>{datum(a.anschaffungsdatum)}</td>
<td className="gedimmt" style={{ fontSize: 12 }}>{METHODEN[a.afa_methode] ?? a.afa_methode}</td>
<td className="rechts zahl">{eur(a.afa.bemessung)}</td>
<td className="rechts zahl">{eur(a.afa.buchwertAnfang)}</td>
<td className="rechts zahl">{eur(a.afa.afa)}</td>
<td className="rechts zahl">{eur(a.afa.buchwertEnde)}</td>
<td className="rechts">
{!a.abgang_datum && (
<button type="button" style={{ padding: '3px 8px', fontSize: 12 }} onClick={() => abgangEintragen(a)}>
Abgang
</button>
)}
</td>
</tr>
))}
{daten?.length === 0 && (
<tr><td colSpan={8} className="gedimmt">Noch kein Anlagevermögen erfasst.</td></tr>
)}
</tbody>
</table>
</div>
</div>
</>
);
}