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
@@ -35,6 +35,13 @@ function calcAmounts(amount: number, vatRelevant: boolean, basis: CreditNoteAmou
const todayIso = () => new Date().toISOString().split('T')[0];
// Betragsloser Sachwert = Lieferschein → zeigt die Lieferscheinnummer,
// sonst die Gutschriftsnummer.
function displayNumber(cn: CreditNote): string {
const nonMonetary = cn.type === 'SACHWERT' && cn.amountGross === 0;
return (nonMonetary ? cn.deliveryNoteNumber : cn.number) ?? `#${cn.id}`;
}
interface FormDefaults {
customerType: CreditNoteCustomerType;
vatRelevant: boolean;
@@ -158,7 +165,7 @@ function CreditNoteFormModal({
: amountNum > 0; // Geld: Betrag > 0 Pflicht
return (
<Modal isOpen onClose={onClose} title={editing ? `Gutschrift ${editing.number ?? 'Sachwert-Übergabe'} bearbeiten` : 'Gutschrift anlegen'} size="lg">
<Modal isOpen onClose={onClose} title={editing ? `Beleg ${displayNumber(editing)} bearbeiten` : 'Gutschrift anlegen'} size="lg">
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Select
@@ -448,7 +455,7 @@ export default function CreditNotesSection({ contractId, canEdit }: { contractId
</div>
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono text-sm font-medium">{cn.number ?? 'Sachwert-Übergabe'}</span>
<span className="font-mono text-sm font-medium">{displayNumber(cn)}</span>
<Badge variant={cn.type === 'GELD' ? 'info' : 'default'}>
{cn.type === 'GELD' ? 'Geld' : 'Sachwert'}
</Badge>
@@ -511,7 +518,7 @@ export default function CreditNotesSection({ contractId, canEdit }: { contractId
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => { if (confirm(`Gutschrift ${cn.number ?? 'Sachwert-Übergabe'} wirklich löschen?`)) deleteMutation.mutate(cn.id); }}
onClick={() => { if (confirm(`Beleg ${displayNumber(cn)} wirklich löschen?`)) deleteMutation.mutate(cn.id); }}
className="text-gray-400 hover:text-red-600 p-1"
title="Löschen"
>