Gutschriften Phase 1: Backend (Modell, Nummernkreis, CRUD, USt-Rechnung)

Neues Feature Gutschriftsverwaltung (Subventionen am Vertrag):

- Modelle CreditNote + CreditNoteNumberRange + Enums (GELD/SACHWERT,
  PRIVAT/FIRMA, NETTO/BRUTTO) + Migration (IF NOT EXISTS, auf Dev
  angewandt).
- USt pro Gutschrift waehlbar (vatRelevant + Basis Netto/Brutto +
  Satz); Netto/USt/Brutto werden berechnet und getrennt gespeichert
  (ZUGFeRD-tauglich). Kundentyp Privat/Firma aus Kunde vorbelegt.
- Nummernkreis in Settings verwaltbar; Nummernvergabe transaktional
  mit SELECT ... FOR UPDATE (keine Doppelvergabe). Bsp GS-2026-0001.
- Service/Controller/Routes: GET/POST /contracts/:id/credit-notes,
  GET .../defaults, GET/PUT/DELETE /credit-notes/:id,
  GET/PUT /credit-notes/number-range. Portal-Token geblockt (interner
  Bereich), CREATE/UPDATE/DELETE auditiert.

Verifiziert: USt-Rechnung (200 netto->238, 200 brutto->168,07+31,93)
und fortlaufende Nummernvergabe.

Phase 2 (Vertrag-UI + Beleg-Upload + Nummernkreis-UI) und Phase 3
(PDF + ZUGFeRD) folgen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 09:03:42 +02:00
co-authored by Claude Opus 4.8
parent 3bbe262039
commit f1c37a7d25
9 changed files with 657 additions and 0 deletions
+84
View File
@@ -811,6 +811,7 @@ model Contract {
historyEntries ContractHistoryEntry[]
documents ContractDocument[]
invoices Invoice[]
creditNotes CreditNote[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -946,6 +947,89 @@ model Invoice {
@@index([contractId])
}
// ==================== GUTSCHRIFTEN (CREDIT NOTES / SUBVENTIONEN) ====================
// Art der Gutschrift: Geldbetrag (per Überweisung, in EUR) oder Sachwert
// (Smartphone, Elektrogerät …, näher im Textfeld beschrieben).
enum CreditNoteType {
GELD
SACHWERT
}
// Kundentyp zum Zeitpunkt der Gutschrift bestimmt die USt-Sicht
// (Privat rechnet brutto, Firma zieht Vorsteuer). Wird aus dem Kunden
// vorbelegt, ist aber pro Gutschrift überschreibbar.
enum CreditNoteCustomerType {
PRIVAT
FIRMA
}
// Basis, auf die sich der eingegebene Betrag bezieht (nur relevant, wenn
// vatRelevant = true). Bei NETTO wird die USt aufgeschlagen, bei BRUTTO
// herausgerechnet.
enum CreditNoteAmountBasis {
NETTO
BRUTTO
}
model CreditNote {
id Int @id @default(autoincrement())
contractId Int
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
number String @unique // aus Nummernkreis, z.B. GS-2026-0001
type CreditNoteType
// Pflicht bei SACHWERT: was wird gewährt (z.B. "Smartphone Samsung A55").
sachwertDescription String?
customerType CreditNoteCustomerType
// USt-relevant ja/nein (Steuerberater-Auslegung: Vermittlungsleistung vs.
// reiner Abschlussbonus/Entgeltminderung). Bei false: net = brutto, USt = 0.
vatRelevant Boolean @default(false)
amountBasis CreditNoteAmountBasis @default(BRUTTO)
vatRate Float @default(19) // Prozent
amountNet Float @default(0)
amountVat Float @default(0)
amountGross Float @default(0)
currency String @default("EUR")
// Datum der Gutschrift (frei änderbar).
creditDate DateTime
// Ort der Unterschrift + Unterschrift-/Bestätigungszeitpunkt.
place String?
signedAt DateTime?
// Sachwert: Bestätigung "Ware erhalten".
goodsReceived Boolean @default(false)
// Überweisungsbeleg (nur GELD) hochgeladene Datei.
receiptPath String?
// Generiertes Gutschrift-Dokument (PDF bzw. ZUGFeRD-Hybrid) Phase 3.
pdfPath String?
notes String?
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([contractId])
}
// Nummernkreis für Gutschriften (Einzel-Zeile, in den Einstellungen
// verwaltbar). Nummer wird transaktional vergeben, damit keine Lücken/
// Doppelvergaben entstehen.
model CreditNoteNumberRange {
id Int @id @default(autoincrement())
prefix String @default("GS-")
includeYear Boolean @default(true)
separator String @default("-")
padding Int @default(4)
nextNumber Int @default(1)
resetYearly Boolean @default(true)
currentYear Int?
updatedAt DateTime @updatedAt
}
// ==================== INTERNET CONTRACT DETAILS ====================
model InternetContractDetails {