@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
JavaScript
"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