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
@@ -3,7 +3,9 @@ import { ApiResponse, AuthRequest } from '../types/index.js';
import { logChange } from '../services/audit.service.js';
import { ApiError } from '../utils/apiError.js';
import * as creditNoteService from '../services/creditNote.service.js';
import { effectiveNumber } from '../services/creditNote.service.js';
import * as numberRangeService from '../services/creditNoteNumberRange.service.js';
import * as deliveryRangeService from '../services/deliveryNoteNumberRange.service.js';
import { generateCreditNotePdf } from '../services/creditNotePdf.service.js';
// Gutschriften sind ein reiner Mitarbeiter-/Admin-Bereich (interne
@@ -82,7 +84,7 @@ export async function create(req: AuthRequest, res: Response): Promise<void> {
action: 'CREATE',
resourceType: 'CreditNote',
resourceId: created.id.toString(),
label: `Gutschrift ${created.number ?? `Sachwert-Uebergabe #${created.id}`} angelegt (${created.type}, ${created.amountGross} ${created.currency}) an Vertrag #${contractId}`,
label: `Beleg ${effectiveNumber(created) ?? `#${created.id}`} angelegt (${created.type}, ${created.amountGross} ${created.currency}) an Vertrag #${contractId}`,
});
res.status(201).json({ success: true, data: created } as ApiResponse);
} catch (error) {
@@ -117,7 +119,7 @@ export async function update(req: AuthRequest, res: Response): Promise<void> {
action: 'UPDATE',
resourceType: 'CreditNote',
resourceId: updated.id.toString(),
label: `Gutschrift ${updated.number ?? `Sachwert-Uebergabe #${updated.id}`} geändert (${updated.type}, ${updated.amountGross} ${updated.currency})`,
label: `Beleg ${effectiveNumber(updated) ?? `#${updated.id}`} geändert (${updated.type}, ${updated.amountGross} ${updated.currency})`,
});
res.json({ success: true, data: updated } as ApiResponse);
} catch (error) {
@@ -136,7 +138,7 @@ export async function remove(req: AuthRequest, res: Response): Promise<void> {
action: 'DELETE',
resourceType: 'CreditNote',
resourceId: id.toString(),
label: `Gutschrift ${deleted.number ?? `Sachwert-Uebergabe #${deleted.id}`} gelöscht`,
label: `Beleg ${effectiveNumber(deleted) ?? `#${deleted.id}`} gelöscht`,
});
res.json({ success: true } as ApiResponse);
} catch (error) {
@@ -193,3 +195,36 @@ export async function updateNumberRange(req: AuthRequest, res: Response): Promis
handleError(res, error, 'Fehler beim Speichern des Nummernkreises');
}
}
// ---- Lieferschein-Nummernkreis (betragslose Sachwerte) ----
export async function getDeliveryNoteNumberRange(req: AuthRequest, res: Response): Promise<void> {
try {
if (blockPortal(req, res)) return;
const [range, nextNumber] = await Promise.all([
deliveryRangeService.getOrCreateRange(),
deliveryRangeService.previewNextNumber(),
]);
res.json({ success: true, data: { ...range, preview: nextNumber } } as ApiResponse);
} catch (error) {
handleError(res, error, 'Fehler beim Laden des Lieferschein-Nummernkreises');
}
}
export async function updateDeliveryNoteNumberRange(req: AuthRequest, res: Response): Promise<void> {
try {
if (blockPortal(req, res)) return;
const range = await deliveryRangeService.updateRange(req.body ?? {});
const preview = await deliveryRangeService.previewNextNumber();
await logChange({
req,
action: 'UPDATE',
resourceType: 'DeliveryNoteNumberRange',
resourceId: range.id.toString(),
label: `Lieferschein-Nummernkreis geändert (Präfix "${range.prefix}", nächste Nr. ${range.nextNumber})`,
});
res.json({ success: true, data: { ...range, preview } } as ApiResponse);
} catch (error) {
handleError(res, error, 'Fehler beim Speichern des Lieferschein-Nummernkreises');
}
}