This commit is contained in:
553
src/lib/credit-note-pdf.tsx
Normal file
553
src/lib/credit-note-pdf.tsx
Normal file
@@ -0,0 +1,553 @@
|
||||
/**
|
||||
* Credit-note PDF rendering via @react-pdf/renderer.
|
||||
*
|
||||
* Phase 7. Mirrors billing-pdf.tsx in layout but with:
|
||||
* - Title "Gutschrift" / "Credit note" / "Note de crédit" / "Nota di credito"
|
||||
* - Red accent (vs invoice's emerald) so the document is visually
|
||||
* unmistakable from an invoice
|
||||
* - References the original invoice number prominently
|
||||
* - One amount line ("Refund for invoice 2026-00042" or
|
||||
* "Voided invoice 2026-00042") with VAT broken out
|
||||
* - Optional reason text below the amount
|
||||
* - No bank-transfer instructions (refunds flow the other way:
|
||||
* either Stripe → customer's card, or PieCed → customer's bank
|
||||
* for invoice-paid cases — neither requires the customer to
|
||||
* do anything)
|
||||
*
|
||||
* Issuer block and brand constants are intentionally duplicated
|
||||
* from billing-pdf.tsx for now. A future refactor can hoist them
|
||||
* into lib/pdf-brand.ts; doing so today is out of scope.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
Document,
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
StyleSheet,
|
||||
Svg,
|
||||
Polygon,
|
||||
Polyline,
|
||||
renderToBuffer,
|
||||
} from "@react-pdf/renderer";
|
||||
import type { CreditNote, Invoice } from "@/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Brand constants — keep in sync with billing-pdf.tsx until the
|
||||
// shared brand module lands.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BRAND = {
|
||||
name: "PieCed IT",
|
||||
// Red accent for credit notes — visually distinct from the invoice
|
||||
// emerald so customers can't confuse the two at a glance.
|
||||
primary: "#DC2626",
|
||||
primaryDark: "#991B1B",
|
||||
textColor: "#1a1a1a",
|
||||
mutedColor: "#666",
|
||||
borderColor: "#d4d4d4",
|
||||
issuer: {
|
||||
legalName: "PieCed IT",
|
||||
addressLine1: "Cedric Mosimann",
|
||||
addressLine2: "[Strasse Nr.]",
|
||||
postalCity: "[PLZ] Basel",
|
||||
country: "Switzerland",
|
||||
email: "billing@pieced.ch",
|
||||
web: "pieced.ch",
|
||||
vatNumber: null as string | null,
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Localized strings (mirrors PdfStrings in billing-pdf.tsx, adapted
|
||||
// for the credit-note context)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface CreditNoteStrings {
|
||||
/** Document title at the top — "Gutschrift" etc. */
|
||||
creditNote: string;
|
||||
/** "Credit note no." label */
|
||||
creditNoteNumber: string;
|
||||
/** "Issue date" label */
|
||||
issueDate: string;
|
||||
/** "Bill to" label, same as invoice */
|
||||
billTo: string;
|
||||
/** "Attn:" / "z.Hd." prefix */
|
||||
attentionPrefix: string;
|
||||
/** "Reference invoice" — links the credit note back to the original */
|
||||
referenceInvoice: string;
|
||||
/** "Reason" label for the free-text reason block */
|
||||
reason: string;
|
||||
/** Body text describing what this credit note is for. Takes the
|
||||
* invoice number as a `{number}` placeholder. */
|
||||
voidLineLabel: string;
|
||||
refundLineLabel: string;
|
||||
/** Totals labels — same words as invoice but separated for clarity */
|
||||
subtotal: string;
|
||||
vatLabel: string;
|
||||
totalCredited: string;
|
||||
/** Footer note explaining the document */
|
||||
footerVoidNote: string;
|
||||
footerRefundNote: string;
|
||||
/** VAT note (reverse-charge or normal) — same logic as invoice */
|
||||
vatNoteSwiss: string;
|
||||
vatNoteReverseCharge: string;
|
||||
vatNoteOutOfScope: string;
|
||||
}
|
||||
|
||||
const MESSAGES: Record<string, CreditNoteStrings> = {
|
||||
de: {
|
||||
creditNote: "Gutschrift",
|
||||
creditNoteNumber: "Gutschrift-Nr.",
|
||||
issueDate: "Ausstellungsdatum",
|
||||
billTo: "Empfänger",
|
||||
attentionPrefix: "z.Hd.",
|
||||
referenceInvoice: "Bezug Rechnung",
|
||||
reason: "Begründung",
|
||||
voidLineLabel: "Stornierung Rechnung {number}",
|
||||
refundLineLabel: "Rückerstattung Rechnung {number}",
|
||||
subtotal: "Zwischensumme",
|
||||
vatLabel: "MWST",
|
||||
totalCredited: "Gesamtbetrag Gutschrift",
|
||||
footerVoidNote:
|
||||
"Diese Gutschrift storniert die oben referenzierte Rechnung. Ein Zahlungsausgleich ist nicht erforderlich.",
|
||||
footerRefundNote:
|
||||
"Diese Gutschrift dokumentiert die Rückerstattung des oben genannten Betrags. Die Auszahlung erfolgt über den ursprünglichen Zahlungsweg.",
|
||||
vatNoteSwiss:
|
||||
"MWST gemäss schweizerischem Mehrwertsteuergesetz (MWSTG).",
|
||||
vatNoteReverseCharge:
|
||||
"Reverse Charge: Steuerschuldnerschaft des Leistungsempfängers nach Art. 196 EU-MwStSyst-RL bzw. nationaler Umsetzung.",
|
||||
vatNoteOutOfScope:
|
||||
"Leistung ausserhalb des Geltungsbereichs der schweizerischen MWST.",
|
||||
},
|
||||
en: {
|
||||
creditNote: "Credit note",
|
||||
creditNoteNumber: "Credit note no.",
|
||||
issueDate: "Issue date",
|
||||
billTo: "Bill to",
|
||||
attentionPrefix: "Attn:",
|
||||
referenceInvoice: "Reference invoice",
|
||||
reason: "Reason",
|
||||
voidLineLabel: "Void of invoice {number}",
|
||||
refundLineLabel: "Refund for invoice {number}",
|
||||
subtotal: "Subtotal",
|
||||
vatLabel: "VAT",
|
||||
totalCredited: "Total credited",
|
||||
footerVoidNote:
|
||||
"This credit note voids the referenced invoice. No payment is required.",
|
||||
footerRefundNote:
|
||||
"This credit note documents the refund of the amount above. Settlement occurs via the original payment method.",
|
||||
vatNoteSwiss:
|
||||
"VAT charged in accordance with Swiss VAT law (MWSTG).",
|
||||
vatNoteReverseCharge:
|
||||
"Reverse charge: VAT to be accounted for by the recipient per Art. 196 EU VAT Directive or national implementation.",
|
||||
vatNoteOutOfScope:
|
||||
"Service supplied outside the scope of Swiss VAT.",
|
||||
},
|
||||
fr: {
|
||||
creditNote: "Note de crédit",
|
||||
creditNoteNumber: "N° de note de crédit",
|
||||
issueDate: "Date d'émission",
|
||||
billTo: "Destinataire",
|
||||
attentionPrefix: "À l'attention de",
|
||||
referenceInvoice: "Facture de référence",
|
||||
reason: "Motif",
|
||||
voidLineLabel: "Annulation de la facture {number}",
|
||||
refundLineLabel: "Remboursement de la facture {number}",
|
||||
subtotal: "Sous-total",
|
||||
vatLabel: "TVA",
|
||||
totalCredited: "Total du crédit",
|
||||
footerVoidNote:
|
||||
"Cette note de crédit annule la facture référencée ci-dessus. Aucun paiement n'est requis.",
|
||||
footerRefundNote:
|
||||
"Cette note de crédit documente le remboursement du montant ci-dessus. Le règlement s'effectue via le moyen de paiement initial.",
|
||||
vatNoteSwiss:
|
||||
"TVA facturée conformément à la loi suisse sur la TVA (LTVA).",
|
||||
vatNoteReverseCharge:
|
||||
"Autoliquidation : TVA à acquitter par le destinataire selon l'art. 196 de la directive TVA UE ou sa mise en œuvre nationale.",
|
||||
vatNoteOutOfScope:
|
||||
"Prestation hors du champ d'application de la TVA suisse.",
|
||||
},
|
||||
it: {
|
||||
creditNote: "Nota di credito",
|
||||
creditNoteNumber: "N. nota di credito",
|
||||
issueDate: "Data di emissione",
|
||||
billTo: "Destinatario",
|
||||
attentionPrefix: "c.a.",
|
||||
referenceInvoice: "Fattura di riferimento",
|
||||
reason: "Motivo",
|
||||
voidLineLabel: "Annullamento della fattura {number}",
|
||||
refundLineLabel: "Rimborso della fattura {number}",
|
||||
subtotal: "Subtotale",
|
||||
vatLabel: "IVA",
|
||||
totalCredited: "Totale accreditato",
|
||||
footerVoidNote:
|
||||
"Questa nota di credito annulla la fattura sopra indicata. Non è richiesto alcun pagamento.",
|
||||
footerRefundNote:
|
||||
"Questa nota di credito documenta il rimborso dell'importo sopra indicato. Il regolamento avviene tramite il metodo di pagamento originale.",
|
||||
vatNoteSwiss:
|
||||
"IVA addebitata in conformità alla legge svizzera sull'IVA (LIVA).",
|
||||
vatNoteReverseCharge:
|
||||
"Inversione contabile: IVA dovuta dal destinatario ai sensi dell'art. 196 della direttiva IVA UE o della sua attuazione nazionale.",
|
||||
vatNoteOutOfScope:
|
||||
"Prestazione fuori dal campo di applicazione dell'IVA svizzera.",
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function pickStrings(locale: string): CreditNoteStrings {
|
||||
return MESSAGES[locale] ?? MESSAGES.de;
|
||||
}
|
||||
|
||||
function fmtChf(n: number): string {
|
||||
return n.toLocaleString("de-CH", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
function fmtDate(iso: string, locale: string): string {
|
||||
const d = new Date(iso);
|
||||
const localeMap: Record<string, string> = {
|
||||
de: "de-CH",
|
||||
en: "en-GB",
|
||||
fr: "fr-CH",
|
||||
it: "it-CH",
|
||||
};
|
||||
return d.toLocaleDateString(localeMap[locale] ?? "de-CH", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function pickVatNote(
|
||||
invoice: Invoice,
|
||||
strings: CreditNoteStrings
|
||||
): string | null {
|
||||
// Mirror the invoice's VAT note logic — the credit note's VAT
|
||||
// treatment must match the original invoice's, otherwise the
|
||||
// accounting wouldn't reconcile.
|
||||
const country = invoice.billingSnapshot.country?.toUpperCase();
|
||||
const hasVat = invoice.billingSnapshot.vatNumber?.trim();
|
||||
if (country === "CH" || country === "LI") {
|
||||
return strings.vatNoteSwiss;
|
||||
}
|
||||
if (hasVat) {
|
||||
return strings.vatNoteReverseCharge;
|
||||
}
|
||||
return strings.vatNoteOutOfScope;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
paddingTop: 36,
|
||||
paddingBottom: 50,
|
||||
paddingHorizontal: 50,
|
||||
fontSize: 10,
|
||||
fontFamily: "Helvetica",
|
||||
color: BRAND.textColor,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 32,
|
||||
},
|
||||
logoBlock: { flexDirection: "row", alignItems: "center" },
|
||||
brandName: {
|
||||
fontSize: 16,
|
||||
color: BRAND.primaryDark,
|
||||
marginLeft: 8,
|
||||
fontFamily: "Helvetica-Bold",
|
||||
},
|
||||
issuerBlock: { textAlign: "right", fontSize: 8.5, color: BRAND.mutedColor },
|
||||
issuerName: { fontSize: 11, color: BRAND.primaryDark, marginBottom: 2 },
|
||||
docTitle: {
|
||||
fontSize: 22,
|
||||
color: BRAND.primaryDark,
|
||||
marginBottom: 8,
|
||||
fontFamily: "Helvetica-Bold",
|
||||
},
|
||||
metaTable: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 20,
|
||||
},
|
||||
metaCol: { flexDirection: "column", minWidth: 140 },
|
||||
metaLabel: { fontSize: 8, color: BRAND.mutedColor, marginBottom: 2 },
|
||||
metaValue: { fontSize: 10 },
|
||||
billTo: {
|
||||
marginBottom: 24,
|
||||
padding: 8,
|
||||
backgroundColor: "#fdf2f2",
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: BRAND.primary,
|
||||
},
|
||||
billToLabel: { fontSize: 8, color: BRAND.mutedColor, marginBottom: 4 },
|
||||
billToName: { fontSize: 11, marginBottom: 2 },
|
||||
amountTable: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: BRAND.borderColor,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: BRAND.borderColor,
|
||||
marginBottom: 16,
|
||||
},
|
||||
amountHeader: {
|
||||
flexDirection: "row",
|
||||
backgroundColor: BRAND.primaryDark,
|
||||
color: "#ffffff",
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 6,
|
||||
fontSize: 9,
|
||||
fontFamily: "Helvetica-Bold",
|
||||
},
|
||||
amountRow: {
|
||||
flexDirection: "row",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 6,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
},
|
||||
amountDesc: { flex: 1 },
|
||||
amountValue: { width: 90, textAlign: "right" },
|
||||
totals: {
|
||||
marginLeft: "auto",
|
||||
width: 220,
|
||||
marginBottom: 20,
|
||||
},
|
||||
totalsRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 3,
|
||||
},
|
||||
totalsLabel: { color: BRAND.mutedColor, fontSize: 10 },
|
||||
totalsValue: { fontSize: 10 },
|
||||
totalsGrand: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: BRAND.primaryDark,
|
||||
paddingTop: 6,
|
||||
marginTop: 4,
|
||||
},
|
||||
totalsGrandLabel: {
|
||||
color: BRAND.primaryDark,
|
||||
fontSize: 11,
|
||||
fontFamily: "Helvetica-Bold",
|
||||
},
|
||||
totalsGrandValue: {
|
||||
color: BRAND.primaryDark,
|
||||
fontSize: 11,
|
||||
textAlign: "right",
|
||||
fontFamily: "Helvetica-Bold",
|
||||
},
|
||||
reasonBox: {
|
||||
marginTop: 4,
|
||||
marginBottom: 18,
|
||||
padding: 8,
|
||||
backgroundColor: "#fafafa",
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: BRAND.borderColor,
|
||||
},
|
||||
reasonLabel: {
|
||||
fontSize: 8,
|
||||
color: BRAND.mutedColor,
|
||||
marginBottom: 2,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
reasonText: { fontSize: 9.5, color: BRAND.textColor },
|
||||
noteBox: {
|
||||
marginTop: 12,
|
||||
padding: 8,
|
||||
fontSize: 8.5,
|
||||
color: BRAND.mutedColor,
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
footer: {
|
||||
position: "absolute",
|
||||
bottom: 24,
|
||||
left: 50,
|
||||
right: 50,
|
||||
fontSize: 7.5,
|
||||
color: BRAND.mutedColor,
|
||||
textAlign: "center",
|
||||
borderTopWidth: 0.5,
|
||||
borderTopColor: BRAND.borderColor,
|
||||
paddingTop: 6,
|
||||
},
|
||||
});
|
||||
|
||||
// Mini SVG logo — identical shape to billing-pdf, just recolored
|
||||
// to the credit-note red so it matches the document accent.
|
||||
function Logo() {
|
||||
return (
|
||||
<Svg width={26} height={26} viewBox="0 0 100 100">
|
||||
<Polygon points="50,15 80,40 65,80 35,80 20,40" fill={BRAND.primary} />
|
||||
<Polyline
|
||||
points="35,80 50,60 65,80"
|
||||
stroke="#ffffff"
|
||||
strokeWidth={3}
|
||||
fill="none"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main document
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface CreditNotePdfProps {
|
||||
creditNote: CreditNote;
|
||||
invoice: Invoice;
|
||||
}
|
||||
|
||||
function CreditNotePdfDocument({ creditNote, invoice }: CreditNotePdfProps) {
|
||||
const strings = pickStrings(creditNote.locale);
|
||||
const snap = creditNote.billingSnapshot;
|
||||
const vatNote = pickVatNote(invoice, strings);
|
||||
const amountLabelTemplate =
|
||||
creditNote.kind === "void" ? strings.voidLineLabel : strings.refundLineLabel;
|
||||
const amountLabel = amountLabelTemplate.replace("{number}", invoice.invoiceNumber);
|
||||
const footerNote =
|
||||
creditNote.kind === "void" ? strings.footerVoidNote : strings.footerRefundNote;
|
||||
// Subtotal + VAT breakdown. We carry the VAT proportion from the
|
||||
// credit note row itself (set by billing.ts based on the invoice's
|
||||
// VAT rate × the refund/void amount).
|
||||
const subtotal = creditNote.amountChf - creditNote.vatAmountChf;
|
||||
return (
|
||||
<Document>
|
||||
<Page size="A4" style={styles.page}>
|
||||
{/* Header: logo+brand left, issuer block right */}
|
||||
<View style={styles.headerRow}>
|
||||
<View style={styles.logoBlock}>
|
||||
<Logo />
|
||||
<Text style={styles.brandName}>{BRAND.name}</Text>
|
||||
</View>
|
||||
<View style={styles.issuerBlock}>
|
||||
<Text style={styles.issuerName}>{BRAND.issuer.legalName}</Text>
|
||||
<Text>{BRAND.issuer.addressLine1}</Text>
|
||||
<Text>{BRAND.issuer.addressLine2}</Text>
|
||||
<Text>{BRAND.issuer.postalCity}</Text>
|
||||
<Text>{BRAND.issuer.country}</Text>
|
||||
<Text>{BRAND.issuer.email}</Text>
|
||||
<Text>{BRAND.issuer.web}</Text>
|
||||
{BRAND.issuer.vatNumber && (
|
||||
<Text>MWST-Nr. {BRAND.issuer.vatNumber}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.docTitle}>{strings.creditNote}</Text>
|
||||
|
||||
{/* Meta row: number, issue date, reference invoice */}
|
||||
<View style={styles.metaTable}>
|
||||
<View style={styles.metaCol}>
|
||||
<Text style={styles.metaLabel}>{strings.creditNoteNumber}</Text>
|
||||
<Text style={styles.metaValue}>{creditNote.creditNoteNumber}</Text>
|
||||
</View>
|
||||
<View style={styles.metaCol}>
|
||||
<Text style={styles.metaLabel}>{strings.issueDate}</Text>
|
||||
<Text style={styles.metaValue}>
|
||||
{fmtDate(creditNote.issuedAt, creditNote.locale)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.metaCol}>
|
||||
<Text style={styles.metaLabel}>{strings.referenceInvoice}</Text>
|
||||
<Text style={styles.metaValue}>{invoice.invoiceNumber}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Bill-to (mirrors invoice block) */}
|
||||
<View style={styles.billTo}>
|
||||
<Text style={styles.billToLabel}>{strings.billTo}</Text>
|
||||
<Text style={styles.billToName}>{snap.companyName}</Text>
|
||||
{snap.contactName && snap.contactName.trim().length > 0 && (
|
||||
<Text>
|
||||
{strings.attentionPrefix} {snap.contactName}
|
||||
</Text>
|
||||
)}
|
||||
<Text>{snap.streetAddress}</Text>
|
||||
<Text>
|
||||
{snap.postalCode} {snap.city}
|
||||
</Text>
|
||||
<Text>{snap.country}</Text>
|
||||
{snap.vatNumber && <Text>MWST/VAT: {snap.vatNumber}</Text>}
|
||||
</View>
|
||||
|
||||
{/* Amount line — single row, the credit note isn't itemized */}
|
||||
<View style={styles.amountTable}>
|
||||
<View style={styles.amountHeader}>
|
||||
<Text style={styles.amountDesc}> </Text>
|
||||
<Text style={styles.amountValue}>CHF</Text>
|
||||
</View>
|
||||
<View style={styles.amountRow}>
|
||||
<Text style={styles.amountDesc}>{amountLabel}</Text>
|
||||
<Text style={styles.amountValue}>{fmtChf(subtotal)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Totals */}
|
||||
<View style={styles.totals}>
|
||||
<View style={styles.totalsRow}>
|
||||
<Text style={styles.totalsLabel}>{strings.subtotal}</Text>
|
||||
<Text style={styles.totalsValue}>CHF {fmtChf(subtotal)}</Text>
|
||||
</View>
|
||||
{creditNote.vatAmountChf > 0 && (
|
||||
<View style={styles.totalsRow}>
|
||||
<Text style={styles.totalsLabel}>
|
||||
{strings.vatLabel} ({Number(invoice.vatRate).toFixed(1)}%)
|
||||
</Text>
|
||||
<Text style={styles.totalsValue}>
|
||||
CHF {fmtChf(creditNote.vatAmountChf)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.totalsGrand}>
|
||||
<Text style={styles.totalsGrandLabel}>{strings.totalCredited}</Text>
|
||||
<Text style={styles.totalsGrandValue}>
|
||||
CHF {fmtChf(creditNote.amountChf)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Reason block — only if the admin provided one */}
|
||||
{creditNote.reason && creditNote.reason.trim().length > 0 && (
|
||||
<View style={styles.reasonBox}>
|
||||
<Text style={styles.reasonLabel}>{strings.reason}</Text>
|
||||
<Text style={styles.reasonText}>{creditNote.reason}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Footer note explaining what this document is */}
|
||||
<View style={styles.noteBox}>
|
||||
<Text>{footerNote}</Text>
|
||||
{vatNote && <Text style={{ marginTop: 6 }}>{vatNote}</Text>}
|
||||
</View>
|
||||
|
||||
{/* Tiny footer with credit-note number on every page */}
|
||||
<Text style={styles.footer} fixed>
|
||||
{BRAND.issuer.legalName} · {creditNote.creditNoteNumber}
|
||||
</Text>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
export async function renderCreditNotePdf(
|
||||
creditNote: CreditNote,
|
||||
invoice: Invoice
|
||||
): Promise<Buffer> {
|
||||
const doc = <CreditNotePdfDocument creditNote={creditNote} invoice={invoice} />;
|
||||
// @react-pdf/renderer's renderToBuffer returns a Node Buffer.
|
||||
return renderToBuffer(doc) as unknown as Buffer;
|
||||
}
|
||||
Reference in New Issue
Block a user