Dauerzahlungen zu einem Vertrag gelten als belegt
Bei einer monatlichen Praemie gibt es keinen Beleg - es gibt einen Vertrag, und der wird einmal abgeschlossen, nicht monatlich quittiert. Bisher blieb nur ein Freitextfeld pro Buchung: zwoelfmal im Jahr derselbe Satz, mal Anzahl der Vertraege. Buchung.vertragId mit onDelete: SetNull. Der Belegzustand kennt jetzt drei Faelle statt zwei, getrennt ausgewiesen - ein Vertrag belegt, eine Begruendung erklaert nur; zusammengeworfen verliert die Belegquote ihre Aussage gegenueber dem Rechtspfleger. Dazu: automatische Zuordnung beim Import bei eindeutiger Gegenstelle, Sammelzuordnung ueber die Gruppen aus Issue #3, und die Rechnungslegung nennt den Vertrag samt Nummer statt "ohne Beleg". Die Sicherung dagegen: Weicht der Betrag um mehr als ein Fuenftel vom hinterlegten Beitrag ab, faellt die Anerkennung weg und die Buchung wird als "Betrag pruefen" markiert. Sonst waere die Zuordnung eine Hintertuer. Zur Toleranz - ein Entwurf und seine Korrektur: Zuerst verglich ich den Betrag gegen alle ueblichen Zahlweisen, damit es auch passt, wenn die Bank anders abbucht als der Vertrag es fuehrt. Der eigene Test hat das widerlegt: Bei 42,50 monatlich liegen 420 innerhalb von 20 Prozent des Jahresbetrags von 510 - ausgerechnet der Fall aus der Aufgabenstellung waere durchgerutscht. Jetzt wird stur gegen den hinterlegten Beitrag verglichen. Bucht die Bank anders ab, stimmen die Vertragsdaten nicht, und das soll auffallen. Behoben: Die Buchungsliste hatte eine eigene, fast gleiche Datenabfrage neben der aus berichte/gemeinsam.ts. Nur eine von beiden wurde nachgezogen, in der Liste stand weiter "Beleg fehlt". Beide benutzen jetzt BUCHUNG_EINBEZUG und zuBuchungEingabe - das kann nicht halb vergessen werden. Gefunden hat es der Browsertest. Nebenbei: Die Roadmap trug noch "Aktuelle Fassung 1.1.2" - Nachwirkung der parallelen Bearbeitung von #3 und #10. Nachgezogen wie zugesagt. Geprueft: 23 neue Einheitstests, die Haelfte davon darauf gerichtet, dass die Anerkennung auch wirklich wegfaellt. Fuenf End-to-End-Tests auf beiden Geraeteprofilen. 560 Unit-Tests, 63 e2e, null Typfehler. Refs #11
This commit is contained in:
@@ -11,6 +11,7 @@ import { blindindex } from '@/lib/crypto/hash'
|
||||
import { alsTag } from '@/lib/finanzen/datum'
|
||||
import { csvTextLesen, csvZuBuchungen, dublettenMarkieren } from '@/lib/finanzen/import'
|
||||
import { camtZuBuchungen } from '@/lib/finanzen/camt'
|
||||
import { eindeutigerVertrag } from '@/lib/finanzen/wiederkehrend'
|
||||
import {
|
||||
buchungSchema,
|
||||
fehlerAuflisten,
|
||||
@@ -349,6 +350,10 @@ export async function buchungSpeichern(
|
||||
kategorie: werte.kategorie,
|
||||
gegenstelle: werte.gegenstelle,
|
||||
belegId: werte.belegId,
|
||||
vertragId: werte.vertragId,
|
||||
// Von Hand gesetzt heisst: nicht mehr "automatisch". Wer die
|
||||
// Zuordnung anfasst, uebernimmt sie.
|
||||
vertragAutomatisch: false,
|
||||
belegFehltBegruendung: werte.belegId ? null : werte.belegFehltBegruendung,
|
||||
betrifftBetreute: werte.betrifftBetreute,
|
||||
vermoegenswirksam: werte.vermoegenswirksam,
|
||||
@@ -515,8 +520,18 @@ export async function importAusfuehren(
|
||||
|
||||
const anzulegen = zeilen.filter((z) => werte.dublettenUebernehmen || !z.dublette)
|
||||
|
||||
// Vertraege einmal holen, nicht je Zeile. Zugeordnet wird nur, wo die
|
||||
// Gegenstelle zu genau EINEM Vertrag passt - bei Mehrdeutigkeit
|
||||
// geschieht nichts.
|
||||
const vertraege = await db.vertrag.findMany({
|
||||
where: { betreuteId, ...nichtGeloescht },
|
||||
select: { id: true, bezeichnung: true, gesellschaft: true },
|
||||
})
|
||||
|
||||
let angelegt = 0
|
||||
let zugeordnet = 0
|
||||
for (const zeile of anzulegen) {
|
||||
const passenderVertrag = eindeutigerVertrag(zeile.gegenstelle, vertraege)
|
||||
try {
|
||||
await db.buchung.create({
|
||||
data: {
|
||||
@@ -530,10 +545,17 @@ export async function importAusfuehren(
|
||||
gegenstelle: zeile.gegenstelle,
|
||||
betrifftBetreute: werte.betrifftBetreute,
|
||||
importReferenz: werte.dublettenUebernehmen && zeile.dublette ? null : zeile.importReferenz,
|
||||
// Nur bei Ausgaben: Eine Gutschrift der Versicherung ist keine
|
||||
// Praemienzahlung und darf nicht als "durch Vertrag belegt"
|
||||
// gelten.
|
||||
...(zeile.richtung === 'AUSGABE' && passenderVertrag
|
||||
? { vertragId: passenderVertrag.id, vertragAutomatisch: true }
|
||||
: {}),
|
||||
...sealen('Buchung', { verwendungszweck: zeile.verwendungszweck }),
|
||||
},
|
||||
})
|
||||
angelegt++
|
||||
if (zeile.richtung === 'AUSGABE' && passenderVertrag) zugeordnet++
|
||||
} catch (ausnahme) {
|
||||
// Die Eindeutigkeit auf (kontoId, importReferenz) faengt Reste ab,
|
||||
// die zwischen Lesen und Schreiben dazugekommen sind.
|
||||
@@ -563,6 +585,13 @@ export async function importAusfuehren(
|
||||
? [`${gelesen.fehler.length} Zeile(n) konnten nicht gelesen werden:`]
|
||||
: []),
|
||||
...gelesen.fehler.slice(0, 10).map((f) => `Zeile ${f.zeilennummer}: ${f.meldung}`),
|
||||
...(zugeordnet > 0
|
||||
? [
|
||||
`${zugeordnet} Buchung(en) wurden automatisch einem Vertrag zugeordnet und gelten ` +
|
||||
'damit als belegt. Sie sind in der Liste als automatisch zugeordnet erkennbar – ' +
|
||||
'bitte einmal durchsehen.',
|
||||
]
|
||||
: []),
|
||||
'Alle importierten Buchungen stehen zunächst in „Sonstige Einnahmen“ bzw. „Sonstige Ausgaben“. ' +
|
||||
'Bitte die Kategorien nachtragen – danach stimmen die Berichte.',
|
||||
]
|
||||
@@ -924,3 +953,69 @@ export async function zahlungsgruppeZurueckholen(
|
||||
revalidatePath('/heute')
|
||||
return { erfolg: 'Die Gruppe steht wieder in der Liste.' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordnet mehrere Buchungen auf einmal einem Vertrag zu.
|
||||
*
|
||||
* Der Weg dorthin fuehrt ueber "Wiederkehrende Zahlungen": Dort steht eine
|
||||
* Gruppe gleichartiger Abbuchungen beisammen, und wenn dazu ein Vertrag in
|
||||
* der Akte liegt, sind es genau die Buchungen, die er belegt. Sie einzeln
|
||||
* anzuklicken waere die Arbeit, die dieses Issue abschaffen soll.
|
||||
*
|
||||
* Die IDs kommen aus dem Formular und werden NICHT geglaubt: Geschrieben
|
||||
* wird nur, was zu dieser betreuten Person gehoert.
|
||||
*/
|
||||
export async function zahlungenVertragZuordnen(
|
||||
_zustand: Formularzustand,
|
||||
daten: FormData,
|
||||
): Promise<Formularzustand> {
|
||||
const betreuteId = String(daten.get('betreuteId') ?? '')
|
||||
const vertragId = String(daten.get('vertragId') ?? '')
|
||||
const buchungIds = String(daten.get('buchungIds') ?? '')
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
if (!vertragId || buchungIds.length === 0) {
|
||||
return { fehler: 'Es ist nichts zum Zuordnen ausgewählt.' }
|
||||
}
|
||||
|
||||
try {
|
||||
const { benutzer } = await erfordereZugang(betreuteId, 'FINANZEN', 'schreiben')
|
||||
|
||||
const vertrag = await db.vertrag.findFirst({
|
||||
where: { id: vertragId, betreuteId, ...nichtGeloescht },
|
||||
select: { id: true, bezeichnung: true },
|
||||
})
|
||||
if (!vertrag) return { fehler: 'Diesen Vertrag gibt es nicht (mehr).' }
|
||||
|
||||
// betreuteId im WHERE ist der eigentliche Schutz: Eine fremde
|
||||
// Buchungs-ID trifft dann auf null Zeilen.
|
||||
const { count } = await db.buchung.updateMany({
|
||||
where: { id: { in: buchungIds }, betreuteId, ...nichtGeloescht },
|
||||
data: { vertragId: vertrag.id, vertragAutomatisch: false },
|
||||
})
|
||||
|
||||
await protokollieren({
|
||||
aktion: 'GEAENDERT',
|
||||
entitaet: 'Buchung',
|
||||
bezeichnung: `${count} Buchung(en) als Dauerzahlung zu „${vertrag.bezeichnung}“`,
|
||||
betreuteId,
|
||||
userId: benutzer.id,
|
||||
userEmail: benutzer.email,
|
||||
userName: benutzer.name,
|
||||
})
|
||||
|
||||
revalidatePath('/finanzen')
|
||||
revalidatePath('/finanzen/buchungen')
|
||||
revalidatePath('/finanzen/wiederkehrend')
|
||||
revalidatePath('/heute')
|
||||
|
||||
return { erfolg: `${count} Buchung(en) gelten jetzt als durch den Vertrag belegt.` }
|
||||
} catch (ausnahme) {
|
||||
const verweigert = zugriffsfehler(ausnahme)
|
||||
if (verweigert) return verweigert
|
||||
console.error('[finanzen] Sammelzuordnung fehlgeschlagen:', ausnahme)
|
||||
return { fehler: 'Die Zuordnung konnte nicht gespeichert werden.' }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user