Buchungskalender: Abschluss – S2 Rechnungsnummer unter einer Sperre, unique-Index; S3 Storno mit Ursprungs-Aussteller

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 15:21:19 +00:00
1 parent 435dd0b746
commit 727613d0e2
4 files changed
+87 -19

No files matched your search

+1 -1
View File
@@ -291,7 +291,7 @@ export async function storniere(ctx, anfrageId, { grund = '', erstattungCent = 0
let stornoRechnung = null
if (a.rechnungId) {
const orig = await ladeRechnung(a.rechnungId)
if (orig) stornoRechnung = await rechnungEinmal(ctx, a, 'storno', { bezugNummer: orig.nummer, now })
if (orig) stornoRechnung = await rechnungEinmal(ctx, a, 'storno', { bezugNummer: orig.nummer, bezug: orig, now })
}
const anfrage = await aktualisiereAnfrage(a.$id, { status: 'storniert', stornoGrund: text(grund, 500), storniertAt: iso(ms(now)) })
return { anfrage, stornoRechnung, neu: true }
+36 -17
View File
@@ -9,8 +9,12 @@ export const listeRechnungen = (featureId) => listeRe(featureId)
const AUSSTELLER = ['firma', 'inhaber', 'strasse', 'plz', 'ort', 'email', 'telefon', 'steuernummer', 'ustId', 'iban', 'bic', 'bank']
/** Naechste lueckenlose Nummer '<Praefix>-<Jahr>-<NNNN>'; Jahr = Berliner Kalenderjahr, Jahreswechsel setzt den Zaehler auf 1. */
export function naechsteNummer(featureId, now = new Date()) {
/**
* Naechste lueckenlose Nummer '<Praefix>-<Jahr>-<NNNN>' vergeben und fn(nummer) unter DERSELBEN Sperre ausfuehren (Zaehler-Erhoehung und
* Dokument-Anlage sind eine Einheit). Scheitert fn, wird der Zaehler auf den alten Stand zurueckgesetzt, sofern er noch unveraendert ist.
* Jahr = Berliner Kalenderjahr, Jahreswechsel setzt den Zaehler auf 1.
*/
export function mitNummer(featureId, now, fn) {
return withLocks(['uk-rechnungsnr:' + featureId], async () => {
const einst = await ladeEinstellungen(featureId)
const jahr = Number(heute(now).slice(0, 4))
@@ -18,10 +22,23 @@ export function naechsteNummer(featureId, now = new Date()) {
const n = z && z.jahr === jahr && Number.isInteger(z.n) ? z.n + 1 : 1
await speichereEinstellungen(featureId, { rechnungZaehler: { jahr, n } })
const praefix = (einst?.rechnungsPraefix || '').trim() || 'RE'
return `${praefix}-${jahr}-${String(n).padStart(4, '0')}`
try {
return await fn(`${praefix}-${jahr}-${String(n).padStart(4, '0')}`)
} catch (err) {
try {
const jetzt = (await ladeEinstellungen(featureId))?.rechnungZaehler
if (jetzt && jetzt.jahr === jahr && jetzt.n === n) await speichereEinstellungen(featureId, { rechnungZaehler: z ?? null })
} catch (err2) {
console.warn(`[unterkunft] Rechnungszaehler nicht zurueckgesetzt: ${err2?.name ?? 'Fehler'}`)
}
throw err
}
})
}
/** Naechste lueckenlose Nummer (ohne Dokument; fuer Tests und Vorschau). */
export const naechsteNummer = (featureId, now = new Date()) => mitNummer(featureId, now, (nummer) => nummer)
const negiere = (summen) => ({
...summen,
bruttoCent: -summen.bruttoCent,
@@ -30,20 +47,21 @@ const negiere = (summen) => ({
})
/** Rechnung oder Stornorechnung zu einer Anfrage ausstellen (Nummer, Dokument). Die Sperre der Unterkunft haelt der Aufrufer. */
export async function stelleAus(ctx, anfrage, { art = 'rechnung', bezugNummer, now = new Date() } = {}) {
export async function stelleAus(ctx, anfrage, { art = 'rechnung', bezugNummer, bezug, now = new Date() } = {}) {
if (!Array.isArray(anfrage?.posten) || !anfrage.summen || typeof anfrage.summen.bruttoCent !== 'number') {
throw new UkFehler(409, 'status_ungueltig', { posten: 'Posten oder Summen fehlen.' })
}
const storno = art === 'storno'
const e = ctx.einstellungen || {}
const aussteller = Object.fromEntries(AUSSTELLER.map((k) => [k, e[k] ?? '']))
// Stornorechnung: Aussteller und Empfaenger der Ursprungsrechnung (nicht die heutigen Einstellungen)
const ursprung = storno && bezug?.inhalt && typeof bezug.inhalt === 'object' ? bezug.inhalt : null
const aussteller = ursprung?.aussteller ?? Object.fromEntries(AUSSTELLER.map((k) => [k, e[k] ?? '']))
const unterkunft = await ladeUnterkunft(anfrage.unterkunftId)
const posten = anfrage.posten.map((p) => (storno ? { ...p, einzelCent: -p.einzelCent } : { ...p }))
const summen = storno ? negiere(anfrage.summen) : anfrage.summen
const nummer = await naechsteNummer(ctx.featureId, now)
const inhalt = {
aussteller,
empfaenger: anfrage.rechnungsadresse ?? null,
empfaenger: ursprung ? (ursprung.empfaenger ?? null) : (anfrage.rechnungsadresse ?? null),
unterkunft: { name: unterkunft?.name ?? '' },
von: anfrage.von,
bis: anfrage.bis,
@@ -53,16 +71,17 @@ export async function stelleAus(ctx, anfrage, { art = 'rechnung', bezugNummer, n
summen,
steuerart: anfrage.summen.steuerart ?? ((anfrage.summen.ust || []).length === 0 ? 'kleinunternehmer' : 'regel'),
zahlart: anfrage.zahlart || '',
bezugNummer: bezugNummer ?? '',
bezugNummer: bezugNummer ?? bezug?.nummer ?? '',
anfrageNummer: anfrage.nummer,
}
return legeRechnungAn({
featureId: ctx.featureId,
anfrageId: anfrage.$id,
art,
nummer,
datum: heute(now),
bruttoCent: summen.bruttoCent,
inhalt,
})
return mitNummer(ctx.featureId, now, (nummer) =>
legeRechnungAn({
featureId: ctx.featureId,
anfrageId: anfrage.$id,
art,
nummer,
datum: heute(now),
bruttoCent: summen.bruttoCent,
inhalt,
}))
}
+1 -1
View File
@@ -115,7 +115,7 @@ export const UNTERKUNFT_COLLECTIONS = {
int('bruttoCent'),
str('inhalt', 20000),
],
indexes: [index('idx_feature', 'featureId'), index('idx_anfrage', 'anfrageId'), index('idx_feature_nummer', 'featureId', 'nummer')],
indexes: [index('idx_feature', 'featureId'), index('idx_anfrage', 'anfrageId'), { key: 'uniq_feature_nummer', type: 'unique', attributes: ['featureId', 'nummer'] }],
},
ukEinstellungen: {
attributes: [