feat: Full company accounting — SKR04, doppelte Buchführung, GoBD Festschreibung
Haben-cloned features for complete company accounting: SKR04 Kontenrahmen (21 Konten): - Bestandskonten (Aktiv/Passiv): Bank, Kasse, EDV-Anlagen, Verbindlichkeiten - Erlöskonten: Erlöse 19% USt, Fremdleistungen - Aufwandskonten: Bezogene Leistungen, Bürobedarf, EDV-Kosten, Reisekosten, Löhne - Steuerkonten: USt 19%/7%, abziehbare Vorsteuer Doppelte Buchführung (Buchungssatz): - Soll an Haben booking system - Journal with lückenlose Nummerierung (BUC-2026-XXXX) - Saldenliste (account balances per Konto) - GoBD Festschreibung (immutable via SHA-256 hash) - Reverse Charge and §19 UStG (Kleinunternehmer) flags EÜR (Einnahmen-Überschuss-Rechnung): - Anlage EÜR with AVEÜR line numbers - § 4 Abs. 3 EStG computation - Gewinn: €14.506 (2026) Jahresabschluss (GoBD): - Immutable year-end archive with SHA-256 hash chain - Links to previous year (2025 → 2026) - Festschreibung of all bookings in the year - Aufbewahrungsfrist: 8 Jahre (GoBD) / 10 Jahre (§ 147 AO) Jahreserklärungen: - USt-Erklärung (yearly, more lines than monthly UStVA) - Anlage EÜR, Einkommensteuer, Gewerbesteuer - ELSTER submission tracking Seed: 21 Konten, 7 Buchungssätze, 9 EÜR entries, 2 Jahreserklärungen, 1 Jahresarchiv
This commit is contained in:
1 parent
0d0fa6b5e1
commit
6cbf0b2879
6 files changed
+613
No files matched your search
@@ -12,6 +12,7 @@ import {
|
||||
Loader2,
|
||||
Link2,
|
||||
Banknote,
|
||||
Lock,
|
||||
} from 'lucide-react'
|
||||
import { useFetch } from '@/hooks/use-fetch'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -209,6 +210,8 @@ export function AccountingView() {
|
||||
<TabsTrigger value="eur">EÜR</TabsTrigger>
|
||||
<TabsTrigger value="bank">Bankabgleich</TabsTrigger>
|
||||
<TabsTrigger value="dunning">Mahnwesen</TabsTrigger>
|
||||
<TabsTrigger value="buchhaltung">SKR04 / Buchführung</TabsTrigger>
|
||||
<TabsTrigger value="jahresabschluss">Jahresabschluss</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* VAT tab */}
|
||||
@@ -634,6 +637,16 @@ export function AccountingView() {
|
||||
<p className="mt-1">Stufe 1: Zahlungserinnerung (kostenlos) · Stufe 2: Mahnung mit Gebühr (2,50 €) · Stufe 3: Letzte Mahnung + Verzugszinsen (§ 288 BGB, 5 % p.a.). Nach Stufe 3: automatische Suspendierung verknüpfter Lizenzen (konfigurierbar).</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* SKR04 / Buchführung tab */}
|
||||
<TabsContent value="buchhaltung" className="mt-4 space-y-4">
|
||||
<BuchhaltungTab />
|
||||
</TabsContent>
|
||||
|
||||
{/* Jahresabschluss tab */}
|
||||
<TabsContent value="jahresabschluss" className="mt-4 space-y-4">
|
||||
<JahresabschlussTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
@@ -659,3 +672,187 @@ function CalcRow({ label, value, negative }: { label: string; value: number; neg
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// SKR04 / Buchführung tab — Kontenrahmen + Journal + Saldenliste
|
||||
function BuchhaltungTab() {
|
||||
const { data, loading, refetch } = useFetch<{
|
||||
konten: { id: string; nummer: string; name: string; art: string }[]
|
||||
journal: { number: string; belegdatum: string; buchungstext: string; betragCents: number; belegNummer: string | null; sollKonto: string; sollName: string; habenKonto: string; habenName: string; festgeschrieben: boolean; reverseCharge: boolean; par19UStG: boolean }[]
|
||||
salden: { konto: string; name: string; art: string; saldoCents: number; buchungen: number }[]
|
||||
stats: { kontenCount: number; buchungenCount: number; festgeschrieben: number; offene: number }
|
||||
}>('/api/aos/buchhaltung')
|
||||
|
||||
if (loading || !data) return <Skeleton className="h-64" />
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<Card className="p-4"><p className="text-xs text-muted-foreground">Konten (SKR04)</p><p className="mt-1 text-2xl font-semibold tabular-nums">{data.stats.kontenCount}</p></Card>
|
||||
<Card className="p-4"><p className="text-xs text-muted-foreground">Buchungen</p><p className="mt-1 text-2xl font-semibold tabular-nums">{data.stats.buchungenCount}</p></Card>
|
||||
<Card className="p-4"><p className="text-xs text-muted-foreground">Festgeschrieben</p><p className="mt-1 text-2xl font-semibold tabular-nums">{data.stats.festgeschrieben}</p></Card>
|
||||
<Card className="p-4"><p className="text-xs text-muted-foreground">Offen</p><p className="mt-1 text-2xl font-semibold tabular-nums">{data.stats.offene}</p></Card>
|
||||
</div>
|
||||
|
||||
{/* Journal */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2"><CardTitle className="text-sm">Journal ({data.journal.length})</CardTitle></CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto max-h-96 overflow-y-auto [scrollbar-width:thin]">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 border-y border-border bg-muted/40 text-left text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-3 py-2 font-medium">Nr.</th>
|
||||
<th className="px-3 py-2 font-medium">Datum</th>
|
||||
<th className="px-3 py-2 font-medium">Buchungstext</th>
|
||||
<th className="px-3 py-2 font-medium">Soll</th>
|
||||
<th className="px-3 py-2 font-medium">Haben</th>
|
||||
<th className="px-3 py-2 font-medium text-right">Betrag</th>
|
||||
<th className="px-3 py-2 font-medium">GoBD</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{data.journal.map((b) => (
|
||||
<tr key={b.number} className="transition hover:bg-muted/40">
|
||||
<td className="px-3 py-2"><code className="font-mono text-[10px]">{b.number}</code></td>
|
||||
<td className="px-3 py-2 text-[11px]">{formatDate(b.belegdatum)}</td>
|
||||
<td className="px-3 py-2 text-[11px]">{b.buchungstext}</td>
|
||||
<td className="px-3 py-2 text-[10px]"><code className="font-mono">{b.sollKonto}</code></td>
|
||||
<td className="px-3 py-2 text-[10px]"><code className="font-mono">{b.habenKonto}</code></td>
|
||||
<td className="px-3 py-2 text-right text-[11px] font-semibold tabular-nums">{formatEuro(b.betragCents)}</td>
|
||||
<td className="px-3 py-2">
|
||||
{b.festgeschrieben ? <Badge variant="outline" className="text-[9px] gap-1"><Lock className="size-2.5" />festgeschrieben</Badge> : <Badge variant="outline" className="text-[9px]">offen</Badge>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Saldenliste */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2"><CardTitle className="text-sm">Saldenliste</CardTitle></CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-y border-border bg-muted/40 text-left text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||
<tr><th className="px-3 py-2 font-medium">Konto</th><th className="px-3 py-2 font-medium">Name</th><th className="px-3 py-2 font-medium">Art</th><th className="px-3 py-2 font-medium text-right">Saldo</th><th className="px-3 py-2 font-medium text-right">Buchungen</th></tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{data.salden.map((s) => (
|
||||
<tr key={s.konto} className="transition hover:bg-muted/40">
|
||||
<td className="px-3 py-2"><code className="font-mono text-[11px]">{s.konto}</code></td>
|
||||
<td className="px-3 py-2 text-[11px]">{s.name}</td>
|
||||
<td className="px-3 py-2"><Badge variant="outline" className="text-[9px]">{s.art}</Badge></td>
|
||||
<td className="px-3 py-2 text-right text-[11px] font-semibold tabular-nums">{formatEuro(s.saldoCents)}</td>
|
||||
<td className="px-3 py-2 text-right text-[11px] tabular-nums">{s.buchungen}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="rounded-md border border-dashed border-border p-3 text-[11px] text-muted-foreground">
|
||||
<p className="font-medium text-foreground">SKR04 Kontenrahmen · Doppelte Buchführung · GoBD Festschreibung</p>
|
||||
<p className="mt-1">Jeder Buchungssatz wird per „Soll an Haben" gebucht. GoBD-Festschreibung macht Buchungen unveränderlich (SHA-256 Hash). Reverse Charge und § 19 UStG (Kleinunternehmer) werden gekennzeichnet. DATEV-Export als Buchungsstapel.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Jahresabschluss tab — GoBD archive with hash chain
|
||||
function JahresabschlussTab() {
|
||||
const { data, loading, refetch } = useFetch<{
|
||||
archiv: { id: string; jahr: number; archivHash: string; buchungenCount: number; belegeCount: number; rechnungenCount: number; zipFileName: string; zipSizeBytes: number; createdBy: string; notes: string | null; createdDate: string }[]
|
||||
jahreserkl: { id: string; jahr: number; art: string; status: string; elsterRef: string | null; submittedAt: string | null; acceptedAt: string | null }[]
|
||||
stats: { jahreserklCount: number; archivCount: number }
|
||||
}>('/api/aos/buchhaltung')
|
||||
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
const createAbschluss = async () => {
|
||||
setCreating(true)
|
||||
const t = toast.loading('Jahresabschluss wird erstellt (GoBD Festschreibung + Hash-Kette)…')
|
||||
try {
|
||||
const r = await fetch('/api/aos/buchhaltung/jahresabschluss', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jahr: 2026 }) })
|
||||
const res = await r.json()
|
||||
if (!r.ok) throw new Error(res.error)
|
||||
toast.success(`Jahresabschluss ${res.archiv.jahr} erstellt — ${res.festgeschriebenCount} Buchungen festgeschrieben`, { id: t, duration: 8000 })
|
||||
refetch()
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Erstellung fehlgeschlagen', { id: t })
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading || !data) return <Skeleton className="h-64" />
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">GoBD Jahresabschluss</h3>
|
||||
<p className="text-xs text-muted-foreground">Unveränderliches Jahresarchiv mit SHA-256 Hash-Kette. Verknüpft mit Vorjahr.</p>
|
||||
</div>
|
||||
<Button size="sm" onClick={createAbschluss} disabled={creating} className="gap-1.5">
|
||||
{creating ? <Loader2 className="size-4 animate-spin" /> : <Lock className="size-4" />}
|
||||
Jahresabschluss 2026
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Archives */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2"><CardTitle className="text-sm">Archive ({data.archiv.length})</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{data.archiv.length === 0 && <p className="py-4 text-center text-xs text-muted-foreground">Keine Jahresabschlüsse.</p>}
|
||||
{data.archiv.map((a) => (
|
||||
<div key={a.id} className="rounded-md border border-border p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-[10px] font-bold">{a.jahr}</Badge>
|
||||
<span className="text-xs font-medium">{a.buchungenCount} Buchungen · {a.belegeCount} Belege · {a.rechnungenCount} Rechnungen</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground">{(a.zipSizeBytes / 1_000_000).toFixed(1)} MB</span>
|
||||
</div>
|
||||
<code className="mt-1 block text-[10px] font-mono text-muted-foreground truncate">{a.archivHash.slice(0, 48)}…</code>
|
||||
{a.notes && <p className="mt-1 text-[10px] text-muted-foreground">{a.notes}</p>}
|
||||
<p className="mt-1 text-[9px] text-muted-foreground">Erstellt von {a.createdBy} am {formatDate(a.createdDate)}</p>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Jahreserklärungen */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2"><CardTitle className="text-sm">Jahreserklärungen ({data.jahreserkl.length})</CardTitle></CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-y border-border bg-muted/40 text-left text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||
<tr><th className="px-3 py-2 font-medium">Jahr</th><th className="px-3 py-2 font-medium">Art</th><th className="px-3 py-2 font-medium">Status</th><th className="px-3 py-2 font-medium">ELSTER-Ref</th></tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{data.jahreserkl.map((e) => (
|
||||
<tr key={e.id} className="transition hover:bg-muted/40">
|
||||
<td className="px-3 py-2.5 text-xs font-medium">{e.jahr}</td>
|
||||
<td className="px-3 py-2.5 text-xs">{e.art}</td>
|
||||
<td className="px-3 py-2.5"><Badge variant="outline" className="text-[10px]">{e.status}</Badge></td>
|
||||
<td className="px-3 py-2.5"><code className="font-mono text-[10px]">{e.elsterRef ?? '—'}</code></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="rounded-md border border-foreground/30 bg-foreground/5 p-3 text-[11px]">
|
||||
<p className="flex items-center gap-1.5 font-medium text-foreground"><Lock className="size-3.5" />GoBD Festschreibung</p>
|
||||
<p className="mt-1 text-muted-foreground">Der Jahresabschluss macht alle Buchungen des Jahres unveränderlich (festgeschrieben). Jede Buchung erhält einen SHA-256 Hash. Das Jahresarchiv selbst wird mit einer Hash-Kette verknüpft (Vorjahr → aktuelles Jahr). Aufbewahrungsfrist: 8 Jahre (GoBD) / 10 Jahre (§ 147 AO).</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user