@casoon/invoice-generator
Version:
Generate PDF invoices, XRechnung XML, and ZUGFeRD PDF/A-3 from JSON data with TypeScript support
252 lines (193 loc) • 8.81 kB
text/typescript
import PDFKit from 'pdfkit';
import { Invoice } from '../../types/invoice.types';
export class EInvoiceSection {
private doc: PDFKit.PDFDocument;
private currentY: number;
private pageHeight: number = 842; // A4 Höhe in Punkten
private footerHeight: number = 120; // Platz für Footer
constructor(doc: PDFKit.PDFDocument, currentY: number) {
this.doc = doc;
this.currentY = currentY;
}
/**
* E-Rechnung Sektion
* Strukturierte elektronische Rechnung nach XRechnung-Standard
*/
public draw(invoice: Invoice): number {
// E-Rechnung Header
this.drawEInvoiceHeader();
// Rechnungssteller (Seller)
this.drawSellerSection(invoice);
// Rechnungsempfänger (Buyer)
this.drawBuyerSection(invoice);
// Rechnungsdetails
this.drawInvoiceDetails(invoice);
// Positionen in strukturierter Form
this.drawStructuredItems(invoice);
// Summen und Steuern
this.drawStructuredTotals(invoice);
// Zahlungsinformationen
this.drawPaymentInformation(invoice);
return this.currentY;
}
private drawEInvoiceHeader(): void {
// E-Rechnung Titel
this.doc.font('Helvetica-Bold')
.fontSize(18)
.fillColor('#008080')
.text('E-RECHNUNG', 50, this.currentY, { continued: false });
this.currentY += 25;
// XRechnung-konforme Metadaten
this.doc.font('Helvetica-Bold')
.fontSize(12)
.fillColor('black')
.text('Elektronische Rechnung nach XRechnung-Standard', 50, this.currentY, { continued: false });
this.currentY += 20;
// Trennlinie
this.doc.moveTo(50, this.currentY)
.lineTo(550, this.currentY)
.lineWidth(2)
.strokeColor('#008080')
.stroke();
this.currentY += 20;
}
private drawSellerSection(invoice: Invoice): void {
this.doc.font('Helvetica-Bold')
.fontSize(14)
.text('Rechnungssteller (Seller)', 50, this.currentY, { continued: false });
this.currentY += 20;
// Strukturierte Seller-Informationen
this.doc.font('Helvetica')
.fontSize(10);
this.drawInfoRow('Name:', invoice.sender.name, 50);
this.drawInfoRow('Firma:', invoice.sender.company || '', 50);
this.drawInfoRow('Straße:', invoice.sender.address.street, 50);
this.drawInfoRow('PLZ/Ort:', `${invoice.sender.address.postalCode} ${invoice.sender.address.city}`, 50);
this.drawInfoRow('Telefon:', invoice.sender.contactInfo.phone, 50);
this.drawInfoRow('E-Mail:', invoice.sender.contactInfo.email, 50);
this.drawInfoRow('Website:', invoice.sender.contactInfo.website || '', 50);
this.drawInfoRow('Geschäftsinhaber:', invoice.sender.businessOwner || '', 50);
this.drawInfoRow('USt-IdNr.:', invoice.sender.vatId || '', 50);
this.currentY += 20;
}
private drawBuyerSection(invoice: Invoice): void {
this.doc.font('Helvetica-Bold')
.fontSize(14)
.text('Rechnungsempfänger (Buyer)', 50, this.currentY, { continued: false });
this.currentY += 20;
// Strukturierte Buyer-Informationen
this.doc.font('Helvetica')
.fontSize(10);
this.drawInfoRow('Name:', invoice.recipient.name, 50);
this.drawInfoRow('Straße:', invoice.recipient.address.street, 50);
this.drawInfoRow('PLZ/Ort:', `${invoice.recipient.address.postalCode} ${invoice.recipient.address.city}`, 50);
this.currentY += 20;
}
private drawInvoiceDetails(invoice: Invoice): void {
this.doc.font('Helvetica-Bold')
.fontSize(14)
.text('Rechnungsdetails', 50, this.currentY, { continued: false });
this.currentY += 20;
// Strukturierte Rechnungsdetails
this.doc.font('Helvetica')
.fontSize(10);
this.drawInfoRow('Rechnungsnummer:', invoice.details.invoiceNumber, 50);
this.drawInfoRow('Kundennummer:', invoice.details.customerNumber, 50);
this.drawInfoRow('Rechnungsdatum:', invoice.details.invoiceDate, 50);
this.drawInfoRow('Lieferdatum:', invoice.details.deliveryDate, 50);
this.drawInfoRow('Leistungszeitraum:', invoice.details.servicePeriod, 50);
this.currentY += 20;
}
private drawStructuredItems(invoice: Invoice): void {
this.doc.font('Helvetica-Bold')
.fontSize(14)
.text('Rechnungspositionen', 50, this.currentY, { continued: false });
this.currentY += 20;
// Tabellenkopf
const tableWidth = 500;
const colWidths = [250, 50, 50, 50, 100]; // Description, Qty, Unit, VAT, Total
this.doc.rect(50, this.currentY, tableWidth, 25)
.fill('#f0f0f0')
.stroke();
this.doc.font('Helvetica-Bold')
.fontSize(9)
.fillColor('black')
.text('Beschreibung', 55, this.currentY + 8, { width: colWidths[0], continued: false })
.text('Menge', 305, this.currentY + 8, { width: colWidths[1], align: 'center', continued: false })
.text('Einh.', 355, this.currentY + 8, { width: colWidths[2], align: 'center', continued: false })
.text('MwSt.', 405, this.currentY + 8, { width: colWidths[3], align: 'center', continued: false })
.text('Gesamt', 455, this.currentY + 8, { width: colWidths[4], align: 'right', continued: false });
this.currentY += 25;
// Positionen
invoice.items.forEach((item, index) => {
// Prüfe ob genug Platz auf der aktuellen Seite ist
if (this.currentY + 20 > this.pageHeight - this.footerHeight) {
this.addNewPage();
}
const rowColor = index % 2 === 0 ? '#ffffff' : '#f8f8f8';
const rowHeight = 20; // Feste Höhe für E-Rechnung-Tabelle
// Zeilen-Hintergrund - exakt mit Zeilenhöhe
this.doc.rect(50, this.currentY, tableWidth, rowHeight)
.fill(rowColor)
.stroke();
this.doc.font('Helvetica')
.fontSize(8)
.fillColor('black')
.text(item.description, 55, this.currentY + 6, { width: colWidths[0], continued: false })
.text(item.quantity.toString(), 305, this.currentY + 6, { width: colWidths[1], align: 'center', continued: false })
.text(item.unit, 355, this.currentY + 6, { width: colWidths[2], align: 'center', continued: false })
.text(`${item.vatRate}%`, 405, this.currentY + 6, { width: colWidths[3], align: 'center', continued: false })
.font('Helvetica-Bold')
.text(`${item.total.toFixed(2).replace('.', ',')} ${item.currency}`, 455, this.currentY + 6, { width: colWidths[4], align: 'right', continued: false });
this.currentY += rowHeight;
});
this.currentY += 20;
}
private drawStructuredTotals(invoice: Invoice): void {
this.doc.font('Helvetica-Bold')
.fontSize(14)
.text('Summen und Steuern', 50, this.currentY, { continued: false });
this.currentY += 20;
// Strukturierte Summen
this.doc.font('Helvetica')
.fontSize(10);
this.drawInfoRow('Nettobetrag:', `${invoice.totals.netAmount.toFixed(2).replace('.', ',')} ${invoice.totals.currency}`, 50);
this.drawInfoRow('MwSt.-Satz:', `${invoice.totals.vatRate.toFixed(2).replace('.', ',')}%`, 50);
this.drawInfoRow('MwSt.-Betrag:', `${invoice.totals.vatAmount.toFixed(2).replace('.', ',')} ${invoice.totals.currency}`, 50);
this.currentY += 15;
// Gesamtbetrag hervorgehoben
this.doc.font('Helvetica-Bold')
.fontSize(16)
.text(`Gesamtbetrag: ${invoice.totals.grossAmount.toFixed(2).replace('.', ',')} ${invoice.totals.currency}`, 50, this.currentY, { continued: false });
this.currentY += 30;
}
private drawPaymentInformation(invoice: Invoice): void {
this.doc.font('Helvetica-Bold')
.fontSize(14)
.text('Zahlungsinformationen', 50, this.currentY, { continued: false });
this.currentY += 20;
// Strukturierte Zahlungsinformationen
this.doc.font('Helvetica')
.fontSize(10);
const paymentText = `Zahlbar ohne Abzug bis zum ${invoice.paymentInfo.dueDate}.`;
this.drawInfoRow('Zahlungsziel:', paymentText, 50);
this.drawInfoRow('Bank:', invoice.paymentInfo.bank, 50);
this.drawInfoRow('Kontoinhaber:', invoice.paymentInfo.accountHolder, 50);
this.drawInfoRow('IBAN:', invoice.paymentInfo.iban, 50);
this.drawInfoRow('BIC/SWIFT:', invoice.paymentInfo.bic, 50);
this.currentY += 20;
}
private drawInfoRow(label: string, value: string, x: number): void {
if (!value) return; // Überspringe leere Werte
this.doc.font('Helvetica-Bold')
.fontSize(10)
.text(label, x, this.currentY, { width: 150, continued: false })
.font('Helvetica')
.text(value, x + 160, this.currentY, { continued: false });
this.currentY += 15;
}
private addNewPage(): void {
this.doc.addPage();
this.currentY = 50;
}
}