feat(mcp): Vorschau von Ausgangsbelegen mit Seitenbildern und Umbruchprüfung

- accounting.outgoing_documents.preview rendert den Beleg wie der Editor und liefert Seiten als PNG-Bilder im MCP-Ergebnis
- Layout je Seite in mm mit Warnungen zu übervollen Seiten, allein stehenden Überschriften, leeren Seiten und vom Überweisungs-QR-Code überdeckten Beträgen samt Vorschlag für den Seitenumbruch
- rows testet geänderte Positionen samt Seitenumbrüchen, ohne zu speichern
- Belegdaten für das PDF als Backend-Port von getDocumentData (Titelsummen, Überträge, Abschläge, Optional-/Alternativpositionen)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 08:29:17 +01:00
parent c10bfac23b
commit 544d607088
8 changed files with 617 additions and 1 deletions

View File

@@ -117,7 +117,17 @@ const getDuration = (time) => {
}
export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoiceData, backgroundPath:string) => {
/** Positions in mm from the top edge, recorded while rendering for layout checks (e.g. page breaks). */
export type InvoicePdfLayout = {
pageHeightMm: number
rows: Array<{ index: number, id?: string, pos?: string, mode: string, text: string, page: number, topMm: number, bottomMm: number }>
totals?: { page: number, topMm: number, bottomMm: number }
endText?: { page: number, topMm: number, bottomMm: number }
paymentQr?: { page: number, topMm: number, leftMm: number }
pageCount: number
}
export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoiceData, backgroundPath:string, layout?: InvoicePdfLayout) => {
const deliveryNoteLikeDocumentTypes = ["deliveryNotes", "packingSlips"]
const isPackingSlip = invoiceData?.type === "packingSlips"
@@ -496,6 +506,7 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
}
invoiceData.rows.forEach((row, index) => {
const layoutTop = rowHeight
if (!["pagebreak", "title", "text"].includes(row.mode)) {
@@ -881,10 +892,16 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
}
}
console.log(rowHeight)
layout?.rows.push({
index, id: row.id, pos: row.pos, mode: row.mode, text: String(row.text || "").slice(0, 80),
// rowHeight is the baseline of the first text line; shift to the visible cap height.
page: pageCounter, topMm: row.mode === "pagebreak" ? 30 : layoutTop - 3, bottomMm: row.mode === "pagebreak" ? 30 : rowHeight - 3,
})
})
let endTextDiff = 35
const layoutContentEnd = rowHeight
if (!deliveryNoteLikeDocumentTypes.includes(invoiceData.type)) {
pages[pageCounter - 1].drawLine({
@@ -1001,6 +1018,18 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
const endTextPosition = getCoordinatesForPDFLib(21, rowHeight + endTextDiff + (invoiceData.totalArray.length - 3) * 8, page1)
if (layout) {
const maxWidth = getTextWidthBeforePaymentQr(pages[pageCounter - 1], endTextPosition.x)
const lines = String(invoiceData.endText || "").split("\n")
.reduce((sum, line) => sum + Math.max(1, Math.ceil(font.widthOfTextAtSize(line, 10) / maxWidth)), 0)
const topMm = rowHeight + endTextDiff + (invoiceData.totalArray.length - 3) * 8
// Totals are baselines 8 mm apart; tax notes (13b/19/12.3) add one more line below.
const totalLines = invoiceData.totalArray.length + (["13b UStG", "19 UStG", "12.3 UStG"].includes(invoiceData.taxType) ? 1 : 0)
layout.totals = { page: pageCounter, topMm: layoutContentEnd, bottomMm: rowHeight + (totalLines - 1) * 8 + 1 }
// drawText places the baseline of the first line; 10 pt line height ≈ 3.53 mm.
layout.endText = { page: pageCounter, topMm: topMm - 3.53, bottomMm: topMm + (lines - 1) * 3.53 + 1 }
}
pages[pageCounter - 1].drawText(invoiceData.endText, {
...endTextPosition,
size: 10,
@@ -1030,6 +1059,12 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
font: fontBold,
})
if (layout) layout.paymentQr = {
page: pages.length,
topMm: (lastPage.getHeight() - (qrY + qrSize + labelGap + 8)) / mm,
leftMm: qrX / mm,
}
lastPage.drawImage(paymentQrCode, {
x: qrX,
y: qrY,
@@ -1038,6 +1073,11 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
})
}
if (layout) {
layout.pageCount = pages.length
layout.pageHeightMm = page1.getHeight() / mm
}
return await pdfDoc.saveAsBase64()
}