u
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user