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