This commit is contained in:
seb
2026-10-14 22:48:03 +02:00
parent d880273629
commit 0ea7da4f3a

View File

@@ -52,11 +52,11 @@ export function formatQuantity(val) {
const TYPE_CONFIG = {
quote: {
docTitle: 'ANGEBOT',
numberPrefix: 'Angebots-Nr.',
numberPrefix: 'Angebot-Nr.',
showPrices: true,
filenamePrefix: 'angebot',
introText: 'Wir freuen uns über Ihr Interesse und bieten Ihnen freibleibend wie folgt an:',
closingText: 'Dieses Angebot ist freibleibend und 30 Tage ab Ausstellungsdatum gültig. Wir freuen uns auf Ihren Auftrag!',
closingText: '',
defaultSubject: 'Angebot',
},
order: {
@@ -114,7 +114,12 @@ export function generateManifestPdf({
try {
const doc = new PDFDocument({
size: 'A4',
margin: 45,
margins: {
top: 45,
bottom: 25,
left: 45,
right: 45,
},
bufferPages: true,
info: {
Title: `${config.docTitle} ${manifest.number || manifest.id}`,
@@ -195,10 +200,16 @@ export function generateManifestPdf({
}
// Document Number under Document Title
doc.font('Helvetica-Bold')
.fontSize(11)
const headerNumStr = `${config.numberPrefix} ${manifest.number || manifest.id}`;
let headerNumFontSize = 11;
const headerNumWidth = 250;
doc.font('Helvetica-Bold');
while (headerNumFontSize > 8 && doc.fontSize(headerNumFontSize).widthOfString(headerNumStr) > headerNumWidth) {
headerNumFontSize -= 0.5;
}
doc.fontSize(headerNumFontSize)
.fillColor(primaryColor)
.text(`${config.numberPrefix} ${manifest.number || manifest.id}`, 350, y, { align: 'right', width: 200 });
.text(headerNumStr, 300, y, { align: 'right', width: headerNumWidth, lineBreak: false });
y += 18;
@@ -261,12 +272,6 @@ export function generateManifestPdf({
doc.text(chosenAddress.county, 45, y);
y += 13;
}
} else {
doc.font('Helvetica')
.fontSize(9)
.fillColor(mutedColor)
.text(`Keine ${partnerLabel} hinterlegt`, 45, y);
y += 14;
}
// Right side Meta Box
@@ -287,16 +292,24 @@ export function generateManifestPdf({
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(mutedColor)
.text(label, metaBoxX + 10, metaY, { width: 95 });
doc.font('Helvetica-Bold')
.fontSize(8.5)
.text(label, metaBoxX + 10, metaY, { width: 72, lineBreak: false });
const valText = val || '–';
doc.font('Helvetica-Bold');
let valFontSize = 8.5;
const maxValWidth = 128;
while (valFontSize > 6.5 && doc.fontSize(valFontSize).widthOfString(valText) > maxValWidth) {
valFontSize -= 0.5;
}
doc.fontSize(valFontSize)
.fillColor(textColor)
.text(val || '–', metaBoxX + 105, metaY, { width: 105, align: 'right' });
.text(valText, metaBoxX + 82, metaY, { width: maxValWidth, align: 'right', lineBreak: false });
metaY += 15;
};
drawMetaRow(`${config.docTitle.slice(0, 1) + config.docTitle.slice(1).toLowerCase()}-Nr.:`, String(manifest.number || manifest.id));
const dateLabel = type === 'shipment' ? 'Versanddatum:' : (type === 'delivery' ? 'Eingangsdatum:' : 'Belegdatum:');
const numLabel = config.numberPrefix ? (config.numberPrefix.endsWith(':') ? config.numberPrefix : `${config.numberPrefix}:`) : 'Beleg-Nr.:';
drawMetaRow(numLabel, String(manifest.number || manifest.id));
const dateLabel = type === 'shipment' ? 'Versanddatum:' : (type === 'delivery' ? 'Eingangsdatum:' : 'Datum:');
drawMetaRow(dateLabel, formatDate(manifest.date || manifest.createdAt));
if (manifest.externalNumber) {
@@ -344,11 +357,11 @@ export function generateManifestPdf({
const showPrices = config.showPrices;
// Column setups
const colXPrices = { pos: 45, sku: 75, name: 165, amount: 345, net: 405, tax: 465, total: 495 };
const colWidthPrices = { pos: 25, sku: 85, name: 175, amount: 55, net: 55, tax: 25, total: 55 };
const colXPrices = { pos: 45, sku: 70, name: 150, amount: 335, gross: 385, tax: 450, total: 482 };
const colWidthPrices = { pos: 22, sku: 75, name: 180, amount: 45, gross: 60, tax: 30, total: 65 };
const colXNoPrices = { pos: 45, sku: 80, name: 185, amount: 455 };
const colWidthNoPrices = { pos: 30, sku: 100, name: 265, amount: 95 };
const colWidthNoPrices = { pos: 30, sku: 100, name: 265, amount: 92 };
const drawTableHeader = (atY) => {
doc.rect(45, atY, 505, 20)
@@ -364,9 +377,9 @@ export function generateManifestPdf({
doc.text('Art.-Nr.', colXPrices.sku, atY + 6, { width: colWidthPrices.sku });
doc.text('Bezeichnung', colXPrices.name, atY + 6, { width: colWidthPrices.name });
doc.text('Menge', colXPrices.amount, atY + 6, { width: colWidthPrices.amount, align: 'right' });
doc.text('E-Preis Netto', colXPrices.net, atY + 6, { width: colWidthPrices.net, align: 'right' });
doc.text('E-Preis Brutto', colXPrices.gross, atY + 6, { width: colWidthPrices.gross, align: 'right' });
doc.text('MwSt', colXPrices.tax, atY + 6, { width: colWidthPrices.tax, align: 'right' });
doc.text('Gesamt Netto', colXPrices.total, atY + 6, { width: colWidthPrices.total, align: 'right' });
doc.text('Gesamt Brutto', colXPrices.total, atY + 6, { width: colWidthPrices.total, align: 'right' });
} else {
doc.text('Pos', colXNoPrices.pos + 3, atY + 6, { width: colWidthNoPrices.pos });
doc.text('Art.-Nr.', colXNoPrices.sku, atY + 6, { width: colWidthNoPrices.sku });
@@ -389,15 +402,21 @@ export function generateManifestPdf({
const net = Number(line.net || 0);
const rebate = Number(line.rebate || 0);
const taxrate = Number(line.taxrate || 0);
const taxFactor = 1 + taxrate / 100;
totalQty += amount;
let lineNet = 0;
let lineGross = 0;
let unitGross = net * taxFactor;
let unitNetAfterRebate = net;
let unitGrossAfterRebate = unitGross;
if (showPrices) {
unitNetAfterRebate = net * (1 - rebate / 100);
unitGrossAfterRebate = unitNetAfterRebate * taxFactor;
lineNet = amount * unitNetAfterRebate;
const lineTax = lineNet * (taxrate / 100);
lineGross = lineNet + lineTax;
totalNet += lineNet;
@@ -407,13 +426,31 @@ export function generateManifestPdf({
taxBreakdown.set(taxrate, currTax);
}
// Row height estimation
const hasComment = Boolean(line.comment);
const nameWidth = showPrices ? colWidthPrices.name : colWidthNoPrices.name;
const puSuffix = line.pu && line.pu > 1 ? ` (VPE ${line.pu})` : '';
const nameText = (line.name || 'Position') + puSuffix;
// Measure actual rendered text heights to prevent overlaps
doc.font('Helvetica').fontSize(8.5);
const nameHeight = doc.heightOfString(nameText, { width: nameWidth });
const hasRebate = showPrices && rebate > 0;
let extraLines = 0;
if (hasComment) extraLines += 1;
if (hasRebate) extraLines += 1;
const rowHeight = 18 + extraLines * 11;
const rebateText = hasRebate ? `Rabatt: ${rebate}% (effektiver E-Preis: ${formatCurrency(unitGrossAfterRebate)})` : '';
let rebateHeight = 0;
if (hasRebate) {
doc.font('Helvetica').fontSize(7.5);
rebateHeight = doc.heightOfString(rebateText, { width: nameWidth }) + 2;
}
const hasComment = Boolean(line.comment);
let commentHeight = 0;
if (hasComment) {
doc.font('Helvetica-Oblique').fontSize(7.5);
commentHeight = doc.heightOfString(line.comment, { width: nameWidth }) + 2;
}
const contentHeight = nameHeight + rebateHeight + commentHeight;
const rowHeight = Math.max(18, Math.ceil(contentHeight + 6));
// Check page overflow
if (y + rowHeight > 730) {
@@ -428,72 +465,70 @@ export function generateManifestPdf({
.fill();
}
const textY = y + 1;
if (showPrices) {
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(textColor);
const puSuffix = line.pu && line.pu > 1 ? ` (VPE ${line.pu})` : '';
if (showPrices) {
// Pos
doc.text(String(index + 1), colXPrices.pos + 3, y, { width: colWidthPrices.pos });
doc.text(String(index + 1), colXPrices.pos + 3, textY, { width: colWidthPrices.pos });
// SKU
doc.text(line.sku || '–', colXPrices.sku, y, { width: colWidthPrices.sku, ellipsis: true });
// Name
doc.text((line.name || 'Position') + puSuffix, colXPrices.name, y, {
width: colWidthPrices.name,
ellipsis: true,
});
doc.text(line.sku || '–', colXPrices.sku, textY, { width: colWidthPrices.sku, ellipsis: true });
// Name (wraps naturally without clipping)
doc.text(nameText, colXPrices.name, textY, { width: nameWidth });
// Amount
doc.text(formatQuantity(amount), colXPrices.amount, y, {
doc.text(formatQuantity(amount), colXPrices.amount, textY, {
width: colWidthPrices.amount,
align: 'right',
});
// Unit Price Net
doc.text(formatCurrency(net), colXPrices.net, y, {
width: colWidthPrices.net,
// Unit Price Gross
doc.text(formatCurrency(unitGross), colXPrices.gross, textY, {
width: colWidthPrices.gross,
align: 'right',
});
// Tax Rate
doc.text(`${taxrate}%`, colXPrices.tax, y, {
doc.text(`${taxrate}%`, colXPrices.tax, textY, {
width: colWidthPrices.tax,
align: 'right',
});
// Line Total Net
// Line Total Gross
doc.font('Helvetica-Bold')
.text(formatCurrency(lineNet), colXPrices.total, y, {
.text(formatCurrency(lineGross), colXPrices.total, textY, {
width: colWidthPrices.total,
align: 'right',
});
let subY = y + 12;
let subY = textY + nameHeight + 2;
if (hasRebate) {
doc.font('Helvetica')
.fontSize(7.5)
.fillColor(secondaryColor)
.text(`Rabatt: ${rebate}% (effektiver E-Preis: ${formatCurrency(unitNetAfterRebate)})`, colXPrices.name, subY, {
width: colWidthPrices.name,
.text(rebateText, colXPrices.name, subY, {
width: nameWidth,
});
subY += 10;
subY += rebateHeight;
}
if (hasComment) {
doc.font('Helvetica-Oblique')
.fontSize(7.5)
.fillColor(mutedColor)
.text(line.comment, colXPrices.name, subY, {
width: colWidthPrices.name,
width: nameWidth,
});
}
} else {
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(textColor);
// Without Prices
doc.text(String(index + 1), colXNoPrices.pos + 3, y, { width: colWidthNoPrices.pos });
doc.text(line.sku || '–', colXNoPrices.sku, y, { width: colWidthNoPrices.sku, ellipsis: true });
doc.text((line.name || 'Position') + puSuffix, colXNoPrices.name, y, {
width: colWidthNoPrices.name,
ellipsis: true,
});
doc.text(String(index + 1), colXNoPrices.pos + 3, textY, { width: colWidthNoPrices.pos });
doc.text(line.sku || '–', colXNoPrices.sku, textY, { width: colWidthNoPrices.sku, ellipsis: true });
doc.text(nameText, colXNoPrices.name, textY, { width: nameWidth });
doc.font('Helvetica-Bold')
.text(formatQuantity(amount) + ' Stk', colXNoPrices.amount, y, {
.text(formatQuantity(amount) + ' Stk', colXNoPrices.amount, textY, {
width: colWidthNoPrices.amount,
align: 'right',
});
@@ -502,8 +537,8 @@ export function generateManifestPdf({
doc.font('Helvetica-Oblique')
.fontSize(7.5)
.fillColor(mutedColor)
.text(line.comment, colXNoPrices.name, y + 12, {
width: colWidthNoPrices.name,
.text(line.comment, colXNoPrices.name, textY + nameHeight + 2, {
width: nameWidth,
});
}
}
@@ -657,6 +692,7 @@ export function generateManifestPdf({
const range = doc.bufferedPageRange();
for (let i = 0; i < range.count; i++) {
doc.switchToPage(i);
doc.page.margins.bottom = 0;
// Footer rule
doc.moveTo(45, 785)