UNPKG

@casoon/invoice-generator

Version:

Generate PDF invoices, XRechnung XML, and ZUGFeRD PDF/A-3 from JSON data with TypeScript support

193 lines 9.09 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ItemsTableSection = void 0; class ItemsTableSection { constructor(doc, currentY) { this.pageHeight = 842; // A4 Höhe in Punkten this.footerHeight = 120; // Platz für Footer this.minRowHeight = 20; this.maxRowHeight = 60; this.doc = doc; this.currentY = currentY; } /** * Bereich 3: Tabelle mit Rechnungspositionen * DIN 5008: Klare Tabellenstruktur mit ausreichend Abständen * Flexibel mit automatischem Seitenumbruch */ draw(invoice) { const tableWidth = 480; // Schmalere Tabelle const colWidths = [35, 170, 45, 45, 45, 70, 70]; // Angepasste Spaltenbreiten // Tabellenkopf this.drawTableHeader(tableWidth, colWidths); // Tabellenzeilen mit dynamischer Höhe this.drawTableRows(invoice, tableWidth, colWidths); // Summen-Bereich this.drawTotalsArea(invoice); return this.currentY; } drawTableHeader(tableWidth, colWidths) { // Kein Rahmen, keine Linien this.doc.font('Helvetica-Bold') .fontSize(8) .fillColor('black') .text('', 55, this.currentY + 8, { width: colWidths[0], continued: false }) // Leere erste Spalte .text('Bezeichnung', 55 + colWidths[0], this.currentY + 8, { width: colWidths[1], continued: false }) .text('Menge', 55 + colWidths[0] + colWidths[1], this.currentY + 8, { width: colWidths[2], align: 'center', continued: false }) .text('Einh.', 55 + colWidths[0] + colWidths[1] + colWidths[2], this.currentY + 8, { width: colWidths[3], align: 'center', continued: false }) .text('MwSt.', 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3], this.currentY + 8, { width: colWidths[4], align: 'center', continued: false }) .text('Einzelpreis', 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3] + colWidths[4], this.currentY + 8, { width: colWidths[5], align: 'right', continued: false }) .text('Summe', 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3] + colWidths[4] + colWidths[5], this.currentY + 8, { width: colWidths[6], align: 'right', continued: false }); this.currentY += 25; } drawTableRows(invoice, tableWidth, colWidths) { invoice.items.forEach((item, index) => { const descriptionHeight = this.calculateTextHeight(item.description, colWidths[1], 8); const rowHeight = Math.max(this.minRowHeight, Math.min(this.maxRowHeight, descriptionHeight + 12)); if (this.currentY + rowHeight > this.pageHeight - this.footerHeight) { this.addNewPage(); this.drawTableHeader(tableWidth, colWidths); } // Keine Rahmen, nur horizontale Linie unten // Zellen-Inhalte - alle nach oben orientiert const topY = this.currentY + 5; // Alle Inhalte 5px vom oberen Rand // Position (vertikal oben ausgerichtet, nicht eingerückt) this.doc.text((index + 1).toString(), 50, topY, { width: colWidths[0], align: 'center', continued: false }); // Beschreibung (mehrzeilig) - oben ausgerichtet mit fettem Anfang this.drawFormattedDescription(item.description, 55 + colWidths[0], topY, colWidths[1]); this.doc.text(item.quantity.toString(), 55 + colWidths[0] + colWidths[1], topY, { width: colWidths[2], align: 'center', continued: false }); this.doc.text(item.unit, 55 + colWidths[0] + colWidths[1] + colWidths[2], topY, { width: colWidths[3], align: 'center', continued: false }); this.doc.text(`${item.vatRate}`, 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3], topY, { width: colWidths[4], align: 'center', continued: false }); this.doc.text(`${item.unitPrice.toFixed(2).replace('.', ',')} ${item.currency}`, 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3] + colWidths[4], topY, { width: colWidths[5], align: 'right', continued: false }); this.doc.font('Helvetica-Bold') .text(`${item.total.toFixed(2).replace('.', ',')} ${item.currency}`, 55 + colWidths[0] + colWidths[1] + colWidths[2] + colWidths[3] + colWidths[4] + colWidths[5], topY, { width: colWidths[6], align: 'right', continued: false }); this.currentY += rowHeight; }); this.currentY += 20; } drawFormattedDescription(description, x, y, width) { // Teile die Beschreibung in Bezeichnung und Rest const parts = this.splitDescription(description); if (parts.length === 2) { // Bezeichnung fett this.doc.font('Helvetica-Bold') .fontSize(8) .text(parts[0], x, y, { width, continued: false }); // Rest normal this.doc.font('Helvetica') .fontSize(8) .text(parts[1], x, y + 10, { width, continued: false }); } else { // Fallback: normale Beschreibung this.doc.font('Helvetica') .fontSize(8) .text(description, x, y, { width, continued: false }); } } splitDescription(description) { // Suche nach dem ersten Komma oder Bindestrich const commaIndex = description.indexOf(','); const dashIndex = description.indexOf(' - '); if (dashIndex !== -1 && (commaIndex === -1 || dashIndex < commaIndex)) { // Teile bei " - " return [ description.substring(0, dashIndex), description.substring(dashIndex + 3) ]; } else if (commaIndex !== -1) { // Teile bei erstem Komma return [ description.substring(0, commaIndex), description.substring(commaIndex + 2) ]; } // Keine Aufteilung möglich return [description]; } calculateTextHeight(text, width, fontSize) { // Temporärer Text zur Höhenberechnung const lines = this.wrapText(text, width, fontSize); return lines.length * (fontSize + 2); // fontSize + 2 für Zeilenabstand } wrapText(text, width, fontSize) { // Einfache Text-Umbrüche basierend auf Zeichenbreite const avgCharWidth = fontSize * 0.6; // Durchschnittliche Zeichenbreite const charsPerLine = Math.floor(width / avgCharWidth); if (text.length <= charsPerLine) { return [text]; } const words = text.split(' '); const lines = []; let currentLine = ''; words.forEach(word => { if ((currentLine + word).length <= charsPerLine) { currentLine += (currentLine ? ' ' : '') + word; } else { if (currentLine) { lines.push(currentLine); } currentLine = word; } }); if (currentLine) { lines.push(currentLine); } return lines; } addNewPage() { this.doc.addPage(); this.currentY = 50; // Reset Y-Position für neue Seite } drawTotalsArea(invoice) { // Prüfe ob genug Platz für Summen-Bereich ist const totalsHeight = 100; // Geschätzte Höhe für Summen if (this.currentY + totalsHeight > this.pageHeight - this.footerHeight) { this.addNewPage(); } // Trennlinie vor Summen this.doc.moveTo(50, this.currentY) .lineTo(550, this.currentY) .lineWidth(0.5) // Dünne Linie .stroke(); this.currentY += 20; // Zusammenfassung in einer Zeile mit angepassten Schriftgrößen this.doc.font('Helvetica') .fontSize(9) // Kleinere Schrift für Nettobetrag .text(`Nettobetrag: ${invoice.totals.netAmount.toFixed(2).replace('.', ',')} ${invoice.totals.currency}`, 50, this.currentY, { continued: false }) .text(`MwSt. (${invoice.totals.vatRate.toFixed(2).replace('.', ',')}%): ${invoice.totals.vatAmount.toFixed(2).replace('.', ',')} ${invoice.totals.currency}`, 250, this.currentY, { continued: false }) .font('Helvetica-Bold') .fontSize(12) // Größere Schrift für Gesamtbetrag .text(`Gesamtbetrag: ${invoice.totals.grossAmount.toFixed(2).replace('.', ',')} ${invoice.totals.currency}`, 450, this.currentY, { continued: false }); this.currentY += 40; return this.currentY; } } exports.ItemsTableSection = ItemsTableSection; //# sourceMappingURL=items-table-section.js.map