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>
91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
// ==================== LIEFERSCHEIN-NUMMERNKREIS ====================
|
|
// Separater Nummernkreis für Lieferscheine (betragslose Sachwert-Übergaben),
|
|
// getrennt von den Gutschriften. Gleiche transaktionale Vergabe (Row-Lock via
|
|
// SELECT ... FOR UPDATE), damit keine Doppelvergaben/Lücken entstehen.
|
|
|
|
import prisma from '../lib/prisma.js';
|
|
import { Prisma } from '@prisma/client';
|
|
import type { NumberRangeConfig } from './creditNoteNumberRange.service.js';
|
|
|
|
export async function getOrCreateRange() {
|
|
const existing = await prisma.deliveryNoteNumberRange.findFirst();
|
|
if (existing) return existing;
|
|
return prisma.deliveryNoteNumberRange.create({ data: {} });
|
|
}
|
|
|
|
export async function updateRange(input: Partial<NumberRangeConfig>) {
|
|
const range = await getOrCreateRange();
|
|
|
|
const data: Prisma.DeliveryNoteNumberRangeUpdateInput = {};
|
|
if (typeof input.prefix === 'string') data.prefix = input.prefix.slice(0, 40);
|
|
if (typeof input.includeYear === 'boolean') data.includeYear = input.includeYear;
|
|
if (typeof input.separator === 'string') data.separator = input.separator.slice(0, 5);
|
|
if (typeof input.padding === 'number' && Number.isInteger(input.padding)) {
|
|
data.padding = Math.min(Math.max(input.padding, 1), 10);
|
|
}
|
|
if (typeof input.resetYearly === 'boolean') data.resetYearly = input.resetYearly;
|
|
if (typeof input.nextNumber === 'number' && Number.isInteger(input.nextNumber) && input.nextNumber >= 1) {
|
|
data.nextNumber = input.nextNumber;
|
|
}
|
|
|
|
return prisma.deliveryNoteNumberRange.update({ where: { id: range.id }, data });
|
|
}
|
|
|
|
function formatNumber(cfg: {
|
|
prefix: string;
|
|
includeYear: boolean;
|
|
separator: string;
|
|
padding: number;
|
|
}, value: number, year: number): string {
|
|
const padded = String(value).padStart(cfg.padding, '0');
|
|
const yearPart = cfg.includeYear ? `${year}${cfg.separator}` : '';
|
|
return `${cfg.prefix}${yearPart}${padded}`;
|
|
}
|
|
|
|
export async function previewNextNumber(): Promise<string> {
|
|
const range = await getOrCreateRange();
|
|
const year = new Date().getFullYear();
|
|
const value = range.resetYearly && range.currentYear !== year ? 1 : range.nextNumber;
|
|
return formatNumber(range, value, year);
|
|
}
|
|
|
|
export async function assignNextNumber(): Promise<string> {
|
|
await getOrCreateRange();
|
|
|
|
return prisma.$transaction(async (tx) => {
|
|
const rows = await tx.$queryRaw<Array<{
|
|
id: number;
|
|
prefix: string;
|
|
includeYear: boolean | number;
|
|
separator: string;
|
|
padding: number;
|
|
nextNumber: number;
|
|
resetYearly: boolean | number;
|
|
currentYear: number | null;
|
|
}>>(Prisma.sql`SELECT * FROM DeliveryNoteNumberRange ORDER BY id ASC LIMIT 1 FOR UPDATE`);
|
|
|
|
const row = rows[0];
|
|
const year = new Date().getFullYear();
|
|
const resetYearly = !!row.resetYearly;
|
|
const value = resetYearly && row.currentYear !== year ? 1 : row.nextNumber;
|
|
|
|
const number = formatNumber(
|
|
{
|
|
prefix: row.prefix,
|
|
includeYear: !!row.includeYear,
|
|
separator: row.separator,
|
|
padding: row.padding,
|
|
},
|
|
value,
|
|
year,
|
|
);
|
|
|
|
await tx.deliveryNoteNumberRange.update({
|
|
where: { id: row.id },
|
|
data: { nextNumber: value + 1, currentYear: year },
|
|
});
|
|
|
|
return number;
|
|
});
|
|
}
|