Gutschrift: Sachwert ohne Betrag = keine Rechnung (kein ZUGFeRD)
Eine Sachwert-Gutschrift darf betragslos sein (Betrag leer/0): dann findet keine Rechnungsstellung statt - der Kunde hat den Gegenstand einfach als Subvention erhalten. - Backend: leeres amount -> 0; Sachwert erlaubt 0, Geld verlangt > 0 (400 sonst). vatRelevant bei 0 erzwungen-false. - PDF: betragsloser Sachwert -> Titel 'Sachwert-Uebergabe', kein Betrags-/USt-Block (Hinweis keine Rechnungsstellung), KEIN ZUGFeRD- Embedding. Mit Betrag -> unveraendert ZUGFeRD. - Frontend: Wert-Feld bei Sachwert optional; USt-Block ausgeblendet ohne Betrag; Liste zeigt 'Sachwert ohne Betrag (keine Rechnung)'. Verifiziert: Sachwert 0 -> kein factur-x.xml; Geld 0 -> 400; Sachwert 200 -> ZUGFeRD. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -83,12 +83,21 @@ function validateAndNormalize(input: CreateCreditNoteInput) {
|
||||
? input.amountBasis
|
||||
: 'BRUTTO') as CreditNoteAmountBasis;
|
||||
|
||||
const amount = Number(input.amount);
|
||||
// Leeres/fehlendes Betragsfeld = 0. Sachwerte dürfen betragslos sein
|
||||
// (reine Übergabe des Gegenstands als Subvention → keine Rechnung, s.u.);
|
||||
// Geld-Gutschriften brauchen dagegen einen echten Betrag.
|
||||
const amount = input.amount === undefined || input.amount === null || (input.amount as unknown) === ''
|
||||
? 0
|
||||
: Number(input.amount);
|
||||
if (!Number.isFinite(amount) || amount < 0) {
|
||||
throw new ApiError(400, 'Ungültiger Betrag');
|
||||
}
|
||||
if (type === 'GELD' && amount <= 0) {
|
||||
throw new ApiError(400, 'Bei Geld-Gutschriften bitte einen Betrag größer 0 angeben.');
|
||||
}
|
||||
|
||||
const vatRelevant = !!input.vatRelevant;
|
||||
// Ohne Betrag gibt es keine USt (nicht-monetärer Sachwert).
|
||||
const vatRelevant = amount > 0 && !!input.vatRelevant;
|
||||
const vatRate = Number.isFinite(Number(input.vatRate)) ? Number(input.vatRate) : 19;
|
||||
if (vatRate < 0 || vatRate > 100) {
|
||||
throw new ApiError(400, 'Ungültiger USt-Satz');
|
||||
|
||||
@@ -87,10 +87,14 @@ export async function generateCreditNotePdf(creditNoteId: number): Promise<{ buf
|
||||
|
||||
// ---- Titel + Meta ----
|
||||
doc.moveDown(2);
|
||||
doc.fontSize(18).text('Gutschrift', { align: 'left' });
|
||||
// Sachwert ohne Betrag = reine Übergabe des Gegenstands als Subvention,
|
||||
// KEINE Rechnung (kein Betrag, keine USt, kein ZUGFeRD).
|
||||
const nonMonetary = cn.type === 'SACHWERT' && cn.amountGross === 0;
|
||||
|
||||
doc.fontSize(18).text(nonMonetary ? 'Sachwert-Übergabe' : 'Gutschrift', { align: 'left' });
|
||||
doc.moveDown(0.3);
|
||||
doc.fontSize(10);
|
||||
doc.text(`Gutschrift-Nr.: ${cn.number}`);
|
||||
doc.text(`Nr.: ${cn.number}`);
|
||||
doc.text(`Datum: ${ddmmyyyy(new Date(cn.creditDate))}`);
|
||||
|
||||
// ---- Beschreibung / Betrag ----
|
||||
@@ -113,7 +117,11 @@ export async function generateCreditNotePdf(creditNoteId: number): Promise<{ buf
|
||||
doc.font('Body');
|
||||
};
|
||||
|
||||
if (cn.vatRelevant) {
|
||||
if (nonMonetary) {
|
||||
doc.fontSize(10).fillColor('#333')
|
||||
.text('Der Sachwert wurde als Subvention übergeben. Es erfolgt keine Rechnungsstellung / kein Rechnungsbetrag.', startX, doc.y, { width: 495 });
|
||||
doc.fillColor('#000');
|
||||
} else if (cn.vatRelevant) {
|
||||
line('Nettobetrag', euro(cn.amountNet, cn.currency));
|
||||
line(`Umsatzsteuer ${cn.vatRate}%`, euro(cn.amountVat, cn.currency));
|
||||
doc.moveDown(0.2);
|
||||
@@ -186,44 +194,50 @@ export async function generateCreditNotePdf(creditNoteId: number): Promise<{ buf
|
||||
doc.end();
|
||||
const baseBuffer = await done;
|
||||
|
||||
// ---- ZUGFeRD-XML erzeugen + als hybrides PDF/A-3 einbetten ----
|
||||
const xml = buildZugferdXml({
|
||||
number: cn.number,
|
||||
issueDate: new Date(cn.creditDate),
|
||||
currency: cn.currency,
|
||||
seller: {
|
||||
name: company.name,
|
||||
street: company.street,
|
||||
houseNumber: company.houseNumber,
|
||||
postalCode: company.postalCode,
|
||||
city: company.city,
|
||||
country: company.country || 'DE',
|
||||
vatId: company.vatId || undefined,
|
||||
taxNumber: company.taxNumber || undefined,
|
||||
},
|
||||
buyer: {
|
||||
name: recipientName,
|
||||
street: addr?.street || '',
|
||||
houseNumber: addr?.houseNumber || '',
|
||||
postalCode: addr?.postalCode || '',
|
||||
city: addr?.city || '',
|
||||
country: 'DE',
|
||||
},
|
||||
lineName:
|
||||
cn.type === 'SACHWERT'
|
||||
? `Sachwert / Subvention: ${cn.sachwertDescription || ''}`.trim()
|
||||
: 'Subvention (Auszahlung per Überweisung)',
|
||||
vatRelevant: cn.vatRelevant,
|
||||
vatRatePercent: cn.vatRate,
|
||||
amountNet: cn.amountNet,
|
||||
amountVat: cn.amountVat,
|
||||
amountGross: cn.amountGross,
|
||||
});
|
||||
// Nicht-monetärer Sachwert = keine Rechnung → KEIN ZUGFeRD-Embedding,
|
||||
// nur das reine PDF (Empfangs-/Übergabebeleg).
|
||||
let xml = '';
|
||||
let buffer = baseBuffer;
|
||||
if (!nonMonetary) {
|
||||
// ---- ZUGFeRD-XML erzeugen + als hybrides PDF/A-3 einbetten ----
|
||||
xml = buildZugferdXml({
|
||||
number: cn.number,
|
||||
issueDate: new Date(cn.creditDate),
|
||||
currency: cn.currency,
|
||||
seller: {
|
||||
name: company.name,
|
||||
street: company.street,
|
||||
houseNumber: company.houseNumber,
|
||||
postalCode: company.postalCode,
|
||||
city: company.city,
|
||||
country: company.country || 'DE',
|
||||
vatId: company.vatId || undefined,
|
||||
taxNumber: company.taxNumber || undefined,
|
||||
},
|
||||
buyer: {
|
||||
name: recipientName,
|
||||
street: addr?.street || '',
|
||||
houseNumber: addr?.houseNumber || '',
|
||||
postalCode: addr?.postalCode || '',
|
||||
city: addr?.city || '',
|
||||
country: 'DE',
|
||||
},
|
||||
lineName:
|
||||
cn.type === 'SACHWERT'
|
||||
? `Sachwert / Subvention: ${cn.sachwertDescription || ''}`.trim()
|
||||
: 'Subvention (Auszahlung per Überweisung)',
|
||||
vatRelevant: cn.vatRelevant,
|
||||
vatRatePercent: cn.vatRate,
|
||||
amountNet: cn.amountNet,
|
||||
amountVat: cn.amountVat,
|
||||
amountGross: cn.amountGross,
|
||||
});
|
||||
|
||||
const buffer = await embedZugferd(baseBuffer, xml, {
|
||||
title: `Gutschrift ${cn.number}`,
|
||||
date: new Date(cn.creditDate),
|
||||
});
|
||||
buffer = await embedZugferd(baseBuffer, xml, {
|
||||
title: `Gutschrift ${cn.number}`,
|
||||
date: new Date(cn.creditDate),
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Speichern ----
|
||||
const dir = path.join(process.cwd(), 'uploads', 'credit-notes');
|
||||
|
||||
Reference in New Issue
Block a user