feat: Konten, Kassenbuch, Vermoegen und die beiden Pflicht-PDFs
Etappe 4. Aus diesen Daten entstehen das Vermoegensverzeichnis nach § 1835 BGB und die Rechnungslegung nach §§ 1859, 1865 BGB - beides geht ans Betreuungsgericht, beides ohne dass eine Zahl abgetippt wird. Betreuungsjahr n laeuft von bestellungAm + (n-1) Jahre bis bestellungAm + n Jahre - 1 Tag. Bei Bestellung am 29. Februar endet Jahr 1 am 27.02. und Jahr 2 beginnt am 28.02.: lueckenlos und ohne Ueberschneidung, per Test abgesichert. Rechenprobe der Rechnungslegung: Der Anfangsbestand ist der juengste Saldostand bis zum VORTAG des Zeitraumbeginns - ein Saldostand gilt als Bestand am Tagesende, sonst zaehlte der erste Tag doppelt. Geht die Probe nicht auf, wird die Differenz im PDF ausgewiesen und jeder fehlende Saldostand namentlich benannt. Nie geglaettet: eine still korrigierte Rechnungslegung ist gegenueber dem Gericht wertlos. Posten ohne Wertstand zum Stichtag erscheinen als Luecke, nicht als 0,00 EUR - und Werte aus der Zukunft werden nie herangezogen. Konten kommen ueber ihren Saldostand ins Verzeichnis; Vermoegensposten der Art KONTO werden dort uebersprungen, sonst stuende das Geld doppelt drin. Der CSV- und CAMT-Import raet KEINE Kategorien, sondern bucht alles auf "Sonstige". Eine falsch geratene Kategorie faellt in der Jahresuebersicht niemandem auf und verfaelscht den Bericht. Fremdwaehrung wird abgelehnt statt umgerechnet. Der XML-Leser weist DOCTYPE ab (XXE). 68 Tests, darunter echte IBAN-Pruefziffern und die Rundung: es darf nirgends ein Cent verschwinden. Etappe: 4 Phase: Geld
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||
import { Buchungsformular } from '@/components/finanzen/buchungs-formular'
|
||||
import { Loeschknopf } from '@/components/finanzen/aktionen'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { oeffnen } from '@/lib/crypto/feld'
|
||||
import { formatDatum, formatDatumZeit } from '@/lib/utils'
|
||||
import { finanzKontext } from '@/lib/finanzen/seite'
|
||||
import { tagAlsText } from '@/lib/finanzen/datum'
|
||||
import { betragFormatieren } from '@/lib/finanzen/betrag'
|
||||
import { buchungLoeschen } from '@/server/actions/finanzen'
|
||||
|
||||
export const metadata = { title: 'Buchung' }
|
||||
|
||||
export default async function BuchungSeite({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const { betreute, darfSchreiben } = await finanzKontext()
|
||||
const schreiben = darfSchreiben('FINANZEN')
|
||||
|
||||
const buchung = await db.buchung.findFirst({
|
||||
where: { id, betreuteId: betreute.id, ...nichtGeloescht },
|
||||
})
|
||||
if (!buchung) notFound()
|
||||
|
||||
const offen = oeffnen('Buchung', buchung)
|
||||
|
||||
const [konten, belege] = await Promise.all([
|
||||
db.konto.findMany({
|
||||
where: { betreuteId: betreute.id, ...nichtGeloescht },
|
||||
select: { id: true, bezeichnung: true, istBetreuerkonto: true },
|
||||
orderBy: [{ istBetreuerkonto: 'asc' }, { bezeichnung: 'asc' }],
|
||||
}),
|
||||
db.dokument.findMany({
|
||||
where: { betreuteId: betreute.id, ...nichtGeloescht },
|
||||
select: { id: true, titel: true },
|
||||
orderBy: { erstelltAm: 'desc' },
|
||||
take: 200,
|
||||
}),
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
<SeitenKopf
|
||||
titel={buchung.gegenstelle ?? 'Buchung'}
|
||||
beschreibung={`Gebucht am ${formatDatum(buchung.datum)}, erfasst am ${formatDatumZeit(buchung.erstelltAm)}.`}
|
||||
pfad={[
|
||||
{ titel: 'Vermögen & Kasse', href: '/finanzen' },
|
||||
{ titel: 'Kassenbuch', href: '/finanzen/buchungen' },
|
||||
{ titel: formatDatum(buchung.datum) },
|
||||
]}
|
||||
/>
|
||||
|
||||
{buchung.importReferenz && (
|
||||
<p className="rounded-md border bg-muted/50 p-3 text-sm text-muted-foreground">
|
||||
Diese Buchung stammt aus einem Dateiimport. Der Herkunftsschlüssel verhindert, dass
|
||||
derselbe Umsatz beim nächsten Import ein zweites Mal angelegt wird.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{schreiben ? (
|
||||
<Buchungsformular
|
||||
betreuteId={betreute.id}
|
||||
konten={konten}
|
||||
belege={belege}
|
||||
buchung={{
|
||||
id: buchung.id,
|
||||
kontoId: buchung.kontoId,
|
||||
datum: tagAlsText(buchung.datum),
|
||||
wertstellung: tagAlsText(buchung.wertstellung) || null,
|
||||
betrag: betragFormatieren(buchung.betragCent),
|
||||
richtung: buchung.richtung,
|
||||
kategorie: buchung.kategorie,
|
||||
gegenstelle: buchung.gegenstelle,
|
||||
verwendungszweck: offen.verwendungszweck,
|
||||
belegId: buchung.belegId,
|
||||
belegFehltBegruendung: buchung.belegFehltBegruendung,
|
||||
betrifftBetreute: buchung.betrifftBetreute,
|
||||
vermoegenswirksam: buchung.vermoegenswirksam,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Nur lesender Zugriff</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Du darfst dieses Modul lesen, aber nicht ändern.
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{schreiben && (
|
||||
<div className="border-t pt-6">
|
||||
<Loeschknopf
|
||||
aktion={buchungLoeschen}
|
||||
felder={{ betreuteId: betreute.id, buchungId: buchung.id }}
|
||||
beschriftung="Buchung in den Papierkorb"
|
||||
variante="outline"
|
||||
frage="Buchung in den Papierkorb legen? Sie bleibt 30 Tage wiederherstellbar. Achtung: Danach geht die Rechenprobe der Rechnungslegung um diesen Betrag nicht mehr auf, solange der Saldostand unverändert bleibt."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Rechtshinweis rechtsgrundlage="§ 1865 BGB" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { SeitenKopf } from '@/components/seiten-kopf'
|
||||
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||
import { Importformular } from '@/components/finanzen/import-formular'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { finanzKontext } from '@/lib/finanzen/seite'
|
||||
|
||||
export const metadata = { title: 'Kontoumsätze einlesen' }
|
||||
|
||||
export default async function ImportSeite() {
|
||||
const { betreute, darfSchreiben } = await finanzKontext()
|
||||
if (!darfSchreiben('FINANZEN')) notFound()
|
||||
|
||||
const konten = await db.konto.findMany({
|
||||
where: { betreuteId: betreute.id, ...nichtGeloescht },
|
||||
select: { id: true, bezeichnung: true, istBetreuerkonto: true },
|
||||
orderBy: [{ istBetreuerkonto: 'asc' }, { bezeichnung: 'asc' }],
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
<SeitenKopf
|
||||
titel="Kontoumsätze einlesen"
|
||||
beschreibung="Abtippen ist die Stelle, an der in der Rechnungslegung die Fehler entstehen. Ein Zahlendreher pro hundert Zeilen reicht, und die Rechenprobe geht nicht mehr auf."
|
||||
pfad={[
|
||||
{ titel: 'Vermögen & Kasse', href: '/finanzen' },
|
||||
{ titel: 'Kassenbuch', href: '/finanzen/buchungen' },
|
||||
{ titel: 'Einlesen' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{konten.length === 0 ? (
|
||||
<Alert variant="warnung">
|
||||
<AlertCircle aria-hidden="true" />
|
||||
<AlertTitle>Es gibt noch kein Konto</AlertTitle>
|
||||
<AlertDescription>
|
||||
Umsätze werden immer einem Konto zugeordnet.{' '}
|
||||
<Link href="/finanzen/konten/neu" className="underline">
|
||||
Erst ein Konto anlegen
|
||||
</Link>
|
||||
.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Importformular betreuteId={betreute.id} konten={konten} />
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Was erkannt wird</CardTitle>
|
||||
<CardDescription>
|
||||
Nichts wird stillschweigend geraten. Was nicht gelesen werden kann, wird dir Zeile für
|
||||
Zeile gezeigt.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="list-disc space-y-1.5 pl-5 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<strong className="text-foreground">Trennzeichen:</strong> Semikolon, Komma,
|
||||
Tabulator oder senkrechter Strich – erkannt an der Spaltenzahl, nicht am Raten.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Zeichensatz:</strong> UTF-8 und
|
||||
Windows-1252/ISO-8859-1. Umlaute bleiben Umlaute.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Beträge:</strong> „1.234,56“, „1234,56“, mit
|
||||
nachgestelltem Minus („1.234,56-“), in Klammern und in englischer Schreibweise.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Datum:</strong> TT.MM.JJJJ, TT.MM.JJ,
|
||||
JJJJ-MM-TT und TT/MM/JJJJ.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Spalten:</strong> Buchungstag, Wertstellung,
|
||||
Betrag oder getrennte Soll-/Haben-Spalten, Soll/Haben-Kennzeichen, Begünstigter,
|
||||
Verwendungszweck, Währung.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Vorspann:</strong> Zeilen vor der eigentlichen
|
||||
Tabelle werden übersprungen.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">CAMT.053:</strong> der elektronische
|
||||
Kontoauszug als XML wird ebenfalls gelesen – dort sind Betrag und Richtung
|
||||
eindeutig ausgezeichnet, das ist der genauere Weg.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Fremdwährung</strong> wird nicht übernommen und
|
||||
nicht umgerechnet. Diese Anwendung rechnet in Euro.
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Rechtshinweis rechtsgrundlage="§ 1865 BGB" kurz />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user