Gutschrift: separater Lieferschein-Nummernkreis (GoBD, luecken-frei)

Pentester R142: Uebergang Geld->betragsloser Sachwert setzte die schon
vergebene Gutschriftsnummer auf null -> Luecke in der GS-Serie.

Loesung: betragsloser Sachwert = Lieferschein mit eigener
Lieferscheinnummer aus separatem Nummernkreis.

- Schema: CreditNote.deliveryNoteNumber (nullbar, unique) + neues Model
  DeliveryNoteNumberRange (Default-Praefix 'LS-') + Migration.
- deliveryNoteNumberRange.service (mirror, eigener Zaehler, FOR UPDATE).
- Nummern lazy pro Serie, NIE freigeben: Uebergaenge behalten die
  jeweils vergebene Nummer der anderen Serie reserviert -> kein
  Doppelverbrauch, keine Luecke. effectiveNumber() liefert je nach Typ
  die passende (LS/GS) fuer Anzeige/PDF/Audit.
- Endpunkte GET/PUT /credit-notes/delivery-note-number-range; Settings-
  Seite verwaltet jetzt beide Nummernkreise. PDF-Titel 'Sachwert-
  Uebergabe', Dateiname lieferschein-...
- Frontend: Typ + displayNumber in Liste/Modal.

Verifiziert: Sachwert 0 -> LS-Nr, GS-Zaehler unberuehrt; Geld -> GS-Nr;
Uebergaenge behalten beide Nummern (kein Neuverbrauch, keine Luecke).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 08:47:49 +02:00
co-authored by Claude Opus 4.8
parent 79f6f3e629
commit 5e606df49e
13 changed files with 360 additions and 94 deletions
@@ -0,0 +1,26 @@
-- Lieferschein-Nummerierung fuer betragslose Sachwert-Uebergaben:
-- eigene Nummer am Beleg + eigener Nummernkreis (getrennt von Gutschriften).
ALTER TABLE `CreditNote` ADD COLUMN IF NOT EXISTS `deliveryNoteNumber` VARCHAR(191) NULL;
-- UNIQUE-Index auf deliveryNoteNumber (idempotent).
SET @idx := (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'CreditNote'
AND INDEX_NAME = 'CreditNote_deliveryNoteNumber_key');
SET @sql := IF(@idx = 0,
'CREATE UNIQUE INDEX `CreditNote_deliveryNoteNumber_key` ON `CreditNote`(`deliveryNoteNumber`)',
'SELECT 1');
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
CREATE TABLE IF NOT EXISTS `DeliveryNoteNumberRange` (
`id` INT NOT NULL AUTO_INCREMENT,
`prefix` VARCHAR(191) NOT NULL DEFAULT 'LS-',
`includeYear` BOOLEAN NOT NULL DEFAULT true,
`separator` VARCHAR(191) NOT NULL DEFAULT '-',
`padding` INT NOT NULL DEFAULT 4,
`nextNumber` INT NOT NULL DEFAULT 1,
`resetYearly` BOOLEAN NOT NULL DEFAULT true,
`currentYear` INT NULL,
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+21 -3
View File
@@ -984,10 +984,14 @@ model CreditNote {
contractId Int
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
// Gutschrift-Nummer aus dem Nummernkreis (z.B. GS-2026-0001). NULL bei
// betragslosen Sachwert-Übergaben die sind eher ein Lieferschein und
// verbrauchen bewusst KEINE Gutschriftsnummer (Zähler bleibt unangetastet).
// Gutschrift-Nummer aus dem Gutschrift-Nummernkreis (z.B. GS-2026-0001).
// NULL bei betragslosen Sachwert-Übergaben (= Lieferschein, s.u.).
number String? @unique
// Lieferschein-/Subventionsnummer aus dem separaten Lieferschein-
// Nummernkreis (z.B. LS-2026-0001) nur bei betragslosen Sachwerten.
// Beide Nummern werden lazy pro Serie vergeben und NIE wieder freigegeben
// (keine Lücken); angezeigt wird je nach aktuellem Typ die passende.
deliveryNoteNumber String? @unique
type CreditNoteType
// Pflicht bei SACHWERT: was wird gewährt (z.B. "Smartphone Samsung A55").
@@ -1071,6 +1075,20 @@ model CreditNoteNumberRange {
updatedAt DateTime @updatedAt
}
// Separater Nummernkreis für Lieferscheine (betragslose Sachwert-Übergaben).
// Eigene Serie, damit die Gutschrift-Nummerierung sauber/lückenlos bleibt.
model DeliveryNoteNumberRange {
id Int @id @default(autoincrement())
prefix String @default("LS-")
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 {