import { summe } from '@/lib/finanzen/betrag' import { alsTag, imZeitraum, nachTagSortieren, tagePlus, vorOderGleich } from '@/lib/finanzen/datum' import type { Betreuungsjahr } from '@/lib/finanzen/betreuungsjahr' import { ausgabenGruppiert, belegDurchVertrag, belegFehlt, betragPruefen, einnahmenGruppiert, istVermoegensbuchung, belegquote, type Belegquote, type BuchungEingabe, type KategorieGruppe, } from '@/lib/finanzen/kassenbuch' import { KONTO_ART_BESCHRIFTUNG } from '@/lib/finanzen/kategorien' import type { KontoArt } from '@prisma/client' /** * Rechnungslegung nach §§ 1859, 1865 BGB. * * Aufbau, den das Gericht erwartet: * * Anfangsbestand * + Einnahmen (nach Kategorien geordnet) * - Ausgaben (nach Kategorien geordnet) * = Endbestand * * Die letzte Zeile ist eine Behauptung, und sie wird geprueft. Der * Endbestand aus den Saldostaenden muss dem herausgerechneten Wert * entsprechen. Tut er das nicht, fehlen Buchungen oder Saldostaende. * * Diese Abweichung wird ausgewiesen, nicht versteckt. Ein Bericht, der * eine Differenz stillschweigend glattzieht, ist schlimmer als einer, * der sie benennt: die Zahl waere dann falsch, ohne dass es jemand * merkt - und unterschrieben wird sie trotzdem. * * Zwei Varianten: * * - § 1859 BGB - befreiter Betreuer (Angehoerige). Es genuegt eine * Vermoegensuebersicht: Bestaende und Summen je Kategorie, keine * Einzelbelegliste. * - § 1865 BGB - vollstaendige Rechnungslegung. Jede Buchung einzeln, * mit laufender Belegnummer. */ export type Variante = 'UEBERSICHT_1859' | 'VOLL_1865' export const VARIANTE_BESCHRIFTUNG: Record = { UEBERSICHT_1859: 'Vermögensübersicht (§ 1859 BGB, befreiter Betreuer)', VOLL_1865: 'Vollständige Rechnungslegung (§ 1865 BGB)', } export const VARIANTE_RECHTSGRUNDLAGE: Record = { UEBERSICHT_1859: '§ 1859 BGB', VOLL_1865: '§ 1865 BGB', } export function standardVariante(befreiterBetreuer: boolean): Variante { return befreiterBetreuer ? 'UEBERSICHT_1859' : 'VOLL_1865' } // ----------------------------------------------------------------------------- // Bestaende // ----------------------------------------------------------------------------- export type SaldoEingabe = { stichtag: Date; betragCent: bigint; quelle: string | null } export type KontoEingabe = { id: string bezeichnung: string bank: string | null art: KontoArt istBetreuerkonto: boolean ibanMaskiert?: string | null saldostaende: SaldoEingabe[] } export type Bestandszeile = { kontoId: string bezeichnung: string bank: string | null art: KontoArt artBeschriftung: string ibanMaskiert: string | null anfangCent: bigint /** Stichtag des tatsaechlich benutzten Saldostands - null, wenn keiner da war. */ anfangStichtag: Date | null endeCent: bigint endeStichtag: Date | null } /** Der juengste Saldostand, der nicht nach dem Stichtag liegt. */ export function saldoZumStichtag( saldostaende: SaldoEingabe[], stichtag: Date, ): SaldoEingabe | null { let treffer: SaldoEingabe | null = null for (const saldo of saldostaende) { if (!vorOderGleich(saldo.stichtag, stichtag)) continue if (treffer === null || alsTag(saldo.stichtag).getTime() > alsTag(treffer.stichtag).getTime()) { treffer = saldo } } return treffer } /** * Bestaende zu Beginn und zum Ende des Zeitraums. * * Ein Saldostand gilt als Bestand am Ende seines Stichtages. Der * Anfangsbestand eines Zeitraums ist deshalb der Bestand am Tag VOR dem * ersten Tag - sonst waeren die Buchungen des ersten Tages doppelt * enthalten. * * Konten des Betreuers bleiben aussen vor: sie gehoeren nicht zum * Vermoegen der betreuten Person. */ export function bestaendeErmitteln( konten: KontoEingabe[], von: Date, bis: Date, ): Bestandszeile[] { const vortag = tagePlus(von, -1) return konten .filter((konto) => !konto.istBetreuerkonto) .map((konto) => { const anfang = saldoZumStichtag(konto.saldostaende, vortag) const ende = saldoZumStichtag(konto.saldostaende, bis) return { kontoId: konto.id, bezeichnung: konto.bezeichnung, bank: konto.bank, art: konto.art, artBeschriftung: KONTO_ART_BESCHRIFTUNG[konto.art], ibanMaskiert: konto.ibanMaskiert ?? null, anfangCent: anfang?.betragCent ?? 0n, anfangStichtag: anfang ? alsTag(anfang.stichtag) : null, endeCent: ende?.betragCent ?? 0n, endeStichtag: ende ? alsTag(ende.stichtag) : null, } }) } // ----------------------------------------------------------------------------- // Belegliste // ----------------------------------------------------------------------------- export type Belegzeile = { /** Laufende Nummer im Bericht, ab 1. Danach wird im Ordner sortiert. */ nummer: number buchungId: string datum: Date betragCent: bigint richtung: BuchungEingabe['richtung'] beschriftung: string gegenstelle: string | null verwendungszweck: string | null belegId: string | null belegTitel: string | null belegFehltBegruendung: string | null /** * Dauerzahlung zu einem Vertrag. Steht im Bericht anstelle von * "Beleg fehlt" - die Luecke wird damit nicht versteckt, sondern * erklaert: Wer nachpruefen will, weiss, welches Papier er aus dem * Ordner holt. */ vertragBezeichnung: string | null vertragsnummer: string | null /** Zugeordnet, aber der Betrag weicht ab - im Bericht ausgewiesen. */ betragZuPruefen: boolean fehlt: boolean } /** * Laufende Belegnummern. Sortiert nach Datum - so laesst sich die * Belegmappe in derselben Reihenfolge abheften, in der die Zahlen im * Bericht stehen. */ export function beleglisteErstellen(buchungen: BuchungEingabe[]): Belegzeile[] { return nachTagSortieren(buchungen, (b) => b.datum).map((buchung, i) => ({ nummer: i + 1, buchungId: buchung.id, datum: alsTag(buchung.datum), betragCent: buchung.betragCent, richtung: buchung.richtung, beschriftung: buchung.gegenstelle ?? buchung.verwendungszweck ?? 'ohne Bezeichnung', gegenstelle: buchung.gegenstelle, verwendungszweck: buchung.verwendungszweck, belegId: buchung.belegId, belegTitel: buchung.belegTitel ?? null, belegFehltBegruendung: buchung.belegFehltBegruendung, vertragBezeichnung: belegDurchVertrag(buchung) ? (buchung.vertragBezeichnung ?? null) : null, vertragsnummer: belegDurchVertrag(buchung) ? (buchung.vertragsnummer ?? null) : null, betragZuPruefen: betragPruefen(buchung), fehlt: belegFehlt(buchung), })) } // ----------------------------------------------------------------------------- // Die Rechnung selbst // ----------------------------------------------------------------------------- export type Rechenprobe = { anfangsbestandCent: bigint einnahmenCent: bigint ausgabenCent: bigint /** Anfangsbestand + Einnahmen - Ausgaben. */ rechnerischerEndbestandCent: bigint /** Summe der Saldostaende zum Ende des Zeitraums. */ gebuchterEndbestandCent: bigint /** gebucht - rechnerisch. Positiv: es ist mehr da, als die Buchungen erklären. */ differenzCent: bigint stimmt: boolean } export type Rechnungsdaten = { von: Date bis: Date variante: Variante /** Gesetzt, wenn der Zeitraum einem Betreuungsjahr entspricht. */ betreuungsjahr: Betreuungsjahr | null /** Ein noch laufendes Jahr ergibt eine Zwischenaufstellung, keine Schlussrechnung. */ zwischenaufstellung: boolean bestaende: Bestandszeile[] anfangsbestandCent: bigint endbestandCent: bigint einnahmen: KategorieGruppe[] ausgaben: KategorieGruppe[] summeEinnahmenCent: bigint summeAusgabenCent: bigint vermoegenswirksamEinnahmenCent: bigint vermoegenswirksamAusgabenCent: bigint umbuchungen: BuchungEingabe[] eigeneAuslagen: BuchungEingabe[] eigeneAuslagenCent: bigint probe: Rechenprobe belege: Belegzeile[] beleg: Belegquote /** Klartexthinweise fuer den Bericht - jede Luecke wird benannt. */ hinweise: string[] } export function rechnungslegungBerechnen(eingabe: { von: Date bis: Date variante: Variante konten: KontoEingabe[] buchungen: BuchungEingabe[] betreuungsjahr?: Betreuungsjahr | null /** Fuer die Frage, ob das Jahr schon abgeschlossen ist. */ heute?: Date }): Rechnungsdaten { const von = alsTag(eingabe.von) const bis = alsTag(eingabe.bis) const vortag = tagePlus(von, -1) const imZeitraumListe = eingabe.buchungen.filter((b) => imZeitraum(b.datum, von, bis)) const bestaende = bestaendeErmitteln(eingabe.konten, von, bis) const anfangsbestandCent = summe(bestaende.map((b) => b.anfangCent)) const endbestandCent = summe(bestaende.map((b) => b.endeCent)) const einnahmen = einnahmenGruppiert(imZeitraumListe) const ausgaben = ausgabenGruppiert(imZeitraumListe) const summeEinnahmenCent = summe(einnahmen.map((g) => g.summeCent)) const summeAusgabenCent = summe(ausgaben.map((g) => g.summeCent)) const rechnerischerEndbestandCent = anfangsbestandCent + summeEinnahmenCent - summeAusgabenCent const differenzCent = endbestandCent - rechnerischerEndbestandCent const relevante = imZeitraumListe.filter(istVermoegensbuchung) const eigeneAuslagen = imZeitraumListe.filter((b) => !b.betrifftBetreute) const hinweise: string[] = [] for (const zeile of bestaende) { if (zeile.anfangStichtag === null) { hinweise.push( `Für „${zeile.bezeichnung}“ gibt es keinen Saldostand zum ${formatKurz(vortag)} oder davor. ` + `Der Anfangsbestand wurde mit 0,00 € angesetzt.`, ) } else if (zeile.anfangStichtag.getTime() !== vortag.getTime()) { hinweise.push( `Der Anfangsbestand von „${zeile.bezeichnung}“ stammt vom ${formatKurz(zeile.anfangStichtag)} ` + `statt vom ${formatKurz(vortag)}. Buchungen dazwischen sind in der Rechenprobe nicht gedeckt.`, ) } if (zeile.endeStichtag === null) { hinweise.push( `Für „${zeile.bezeichnung}“ gibt es keinen Saldostand zum ${formatKurz(bis)} oder davor. ` + `Der Endbestand wurde mit 0,00 € angesetzt.`, ) } else if (zeile.endeStichtag.getTime() !== bis.getTime()) { hinweise.push( `Der Endbestand von „${zeile.bezeichnung}“ stammt vom ${formatKurz(zeile.endeStichtag)} ` + `statt vom ${formatKurz(bis)}.`, ) } } if (differenzCent !== 0n) { hinweise.push( 'Die Rechenprobe geht nicht auf: Anfangsbestand zuzüglich Einnahmen abzüglich Ausgaben ' + 'ergibt nicht den Endbestand. Entweder fehlen Buchungen, oder ein Saldostand ist nicht ' + 'erfasst. Die Differenz ist im Bericht ausgewiesen und sollte vor der Abgabe geklärt werden.', ) } const fehlendeBelege = relevante.filter(belegFehlt).length if (fehlendeBelege > 0) { hinweise.push( `${fehlendeBelege} Buchung(en) haben weder einen Beleg noch eine Begründung, warum er fehlt.`, ) } const vermischt = imZeitraumListe.filter((b) => b.aufBetreuerkonto && b.betrifftBetreute).length if (vermischt > 0) { hinweise.push( `${vermischt} Buchung(en) laufen über ein Konto des Betreuers und sind trotzdem als ` + 'Vermögen der betreuten Person gekennzeichnet. Das ist zu klären, bevor der Bericht abgeht.', ) } const jahr = eingabe.betreuungsjahr ?? null const stichtagHeute = eingabe.heute ? alsTag(eingabe.heute) : null const zwischenaufstellung = stichtagHeute !== null && stichtagHeute.getTime() < bis.getTime() return { von, bis, variante: eingabe.variante, betreuungsjahr: jahr, zwischenaufstellung, bestaende, anfangsbestandCent, endbestandCent, einnahmen, ausgaben, summeEinnahmenCent, summeAusgabenCent, vermoegenswirksamEinnahmenCent: summe(einnahmen.map((g) => g.davonVermoegenswirksamCent)), vermoegenswirksamAusgabenCent: summe(ausgaben.map((g) => g.davonVermoegenswirksamCent)), umbuchungen: imZeitraumListe.filter((b) => b.richtung === 'UMBUCHUNG' && b.betrifftBetreute), eigeneAuslagen, eigeneAuslagenCent: summe(eigeneAuslagen.map((b) => b.betragCent)), probe: { anfangsbestandCent, einnahmenCent: summeEinnahmenCent, ausgabenCent: summeAusgabenCent, rechnerischerEndbestandCent, gebuchterEndbestandCent: endbestandCent, differenzCent, stimmt: differenzCent === 0n, }, belege: beleglisteErstellen(relevante), beleg: belegquote(relevante), hinweise, } } function formatKurz(datum: Date): string { const tag = String(datum.getUTCDate()).padStart(2, '0') const monat = String(datum.getUTCMonth() + 1).padStart(2, '0') return `${tag}.${monat}.${datum.getUTCFullYear()}` }