Erweiterungen

This commit is contained in:
2025-09-19 21:42:57 +02:00
parent 09e39839cc
commit ac08c2b867
2 changed files with 31 additions and 433 deletions

View File

@@ -116,12 +116,7 @@
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- Apache PDFBox for PDF generation -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.3</version>
</dependency>
<!-- iText8 for PDF generation -->
<dependency>

View File

@@ -2,418 +2,38 @@ package de.assecutor.votianlt.service;
import de.assecutor.votianlt.model.invoices.SystemInvoiceData;
import de.assecutor.votianlt.model.invoices.SystemInvoiceItem;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.springframework.stereotype.Service;
import com.itextpdf.html2pdf.HtmlConverter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Service
public class SystemInvoiceService {
private static final float MARGIN = 50;
private static final float FONT_SIZE = 12;
private static final float TITLE_FONT_SIZE = 24;
private static final float SUBTITLE_FONT_SIZE = 16;
private static final float LINE_HEIGHT = 15;
public byte[] generateInvoicePdf(SystemInvoiceData systemInvoiceData) throws IOException {
try (PDDocument document = new PDDocument();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
float yPosition = page.getMediaBox().getHeight() - MARGIN;
// Draw header
yPosition = drawHeader(contentStream, yPosition, systemInvoiceData);
yPosition -= 30;
// Draw company and recipient info
yPosition = drawAddresses(contentStream, yPosition, systemInvoiceData);
yPosition -= 30;
// Draw invoice details
yPosition = drawInvoiceDetails(contentStream, yPosition, systemInvoiceData);
yPosition -= 30;
// Draw items table
yPosition = drawItemsTable(contentStream, yPosition, systemInvoiceData);
yPosition -= 30;
// Draw totals
yPosition = drawTotals(contentStream, yPosition, systemInvoiceData);
yPosition -= 30;
// Draw payment terms
yPosition = drawPaymentTerms(contentStream, yPosition, systemInvoiceData);
// Draw footer at bottom of page
drawFooter(contentStream, page, systemInvoiceData);
}
document.save(outputStream);
return outputStream.toByteArray();
}
public byte[] generateInvoicePdf(SystemInvoiceData systemInvoiceData) throws Exception {
// Generate PDF using HTML to PDF conversion
return generateInvoicePdfFromHtml();
}
private float drawHeader(PDPageContentStream contentStream, float yPosition, SystemInvoiceData data) throws IOException {
// Company name
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), TITLE_FONT_SIZE);
contentStream.setNonStrokingColor(27/255f, 18/255f, 185/255f); // Blue color (RGB values normalized to 0-1)
contentStream.newLineAtOffset(MARGIN, yPosition);
contentStream.showText(data.getCompanyName() != null ? data.getCompanyName() : "");
contentStream.endText();
public byte[] generateInvoicePdfFromHtml() throws Exception {
// Read the HTML template
String htmlContent = readHtmlTemplate();
yPosition -= 30;
// Fill HTML with sample data
SystemInvoiceData sampleData = createSampleInvoiceData();
String filledHtml = fillHtmlWithInvoiceData(htmlContent, sampleData);
// Company subtitle
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), FONT_SIZE);
contentStream.setNonStrokingColor(27/255f, 18/255f, 185/255f);
contentStream.newLineAtOffset(MARGIN, yPosition);
contentStream.showText(data.getCompanySubtitle() != null ? data.getCompanySubtitle() : "");
contentStream.endText();
yPosition -= 20;
// Draw line
contentStream.setStrokingColor(27/255f, 18/255f, 185/255f);
contentStream.setLineWidth(2);
contentStream.moveTo(MARGIN, yPosition);
contentStream.lineTo(PDRectangle.A4.getWidth() - MARGIN, yPosition);
contentStream.stroke();
return yPosition - 10;
}
private float drawAddresses(PDPageContentStream contentStream, float yPosition, SystemInvoiceData data) throws IOException {
contentStream.setNonStrokingColor(0f, 0f, 0f); // Black
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
float leftColumn = MARGIN;
float rightColumn = 350;
// Sender line
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 8);
contentStream.newLineAtOffset(leftColumn, yPosition);
contentStream.showText(data.getSenderLine() != null ? data.getSenderLine() : "");
contentStream.endText();
yPosition -= 20;
float recipientStartY = yPosition; // Store starting position for date alignment
// Recipient address
String[] recipientLines = {
data.getRecipientName(),
data.getRecipientDepartment(),
data.getRecipientStreet(),
data.getRecipientCity()
};
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
for (String line : recipientLines) {
if (line != null && !line.trim().isEmpty()) {
contentStream.beginText();
contentStream.newLineAtOffset(leftColumn, yPosition);
contentStream.showText(line);
contentStream.endText();
yPosition -= LINE_HEIGHT;
}
}
// Date (right aligned, same vertical level as recipient)
float dateColumn = 450; // Move further to the right
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
contentStream.newLineAtOffset(dateColumn, recipientStartY);
contentStream.showText("Datum");
contentStream.endText();
contentStream.beginText();
contentStream.newLineAtOffset(dateColumn, recipientStartY - LINE_HEIGHT);
contentStream.showText(data.getInvoiceDate() != null ? data.getInvoiceDate() : "");
contentStream.endText();
// Company address (right side)
float rightYPosition = yPosition + (recipientLines.length * LINE_HEIGHT);
return Math.min(yPosition, rightYPosition) - 10;
}
private float drawInvoiceDetails(PDPageContentStream contentStream, float yPosition, SystemInvoiceData data) throws IOException {
// Invoice title
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), SUBTITLE_FONT_SIZE);
contentStream.newLineAtOffset(MARGIN, yPosition);
contentStream.showText("Rechnung " + (data.getInvoiceNumber() != null ? data.getInvoiceNumber() : ""));
contentStream.endText();
yPosition -= 30;
// Invoice text
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
contentStream.newLineAtOffset(MARGIN, yPosition);
// Split long text into multiple lines
String invoiceText = data.getInvoiceText();
if (invoiceText != null && invoiceText.length() > 80) {
String[] words = invoiceText.split(" ");
StringBuilder currentLine = new StringBuilder();
for (String word : words) {
if (currentLine.length() + word.length() + 1 > 80) {
contentStream.showText(currentLine.toString());
contentStream.newLineAtOffset(0, -LINE_HEIGHT);
currentLine = new StringBuilder(word);
} else {
if (currentLine.length() > 0) currentLine.append(" ");
currentLine.append(word);
}
}
if (currentLine.length() > 0) {
contentStream.showText(currentLine.toString());
}
} else {
contentStream.showText(invoiceText != null ? invoiceText : "");
}
contentStream.endText();
return yPosition - 40;
}
private float drawItemsTable(PDPageContentStream contentStream, float yPosition, SystemInvoiceData data) throws IOException {
float[] columnWidths = {60, 300, 100, 100}; // Menge, Bezeichnung, Einzelpreis, Gesamt
float rowHeight = 20;
// Calculate actual table width based on column widths
float actualTableWidth = 0;
for (float width : columnWidths) {
actualTableWidth += width;
}
// Add 2cm margin to the right of the gray header
float rightMargin = 2 * 28.35f; // 2cm in points (1cm = 28.35pt)
float headerWidth = actualTableWidth + rightMargin;
// Table header
contentStream.setNonStrokingColor(230/255f, 230/255f, 230/255f); // Light gray background
contentStream.addRect(MARGIN, yPosition - rowHeight, headerWidth, rowHeight);
contentStream.fill();
contentStream.setNonStrokingColor(0f, 0f, 0f); // Black
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 10);
String[] headers = {"Menge", "Bezeichnung", "Einzelpreis", "Gesamt"};
float xOffset = MARGIN + 5;
contentStream.beginText();
contentStream.newLineAtOffset(xOffset, yPosition - 15);
contentStream.showText(headers[0]);
contentStream.endText();
xOffset += columnWidths[0];
contentStream.beginText();
contentStream.newLineAtOffset(xOffset, yPosition - 15);
contentStream.showText(headers[1]);
contentStream.endText();
xOffset += columnWidths[1];
contentStream.beginText();
contentStream.newLineAtOffset(xOffset, yPosition - 15);
contentStream.showText(headers[2]);
contentStream.endText();
xOffset += columnWidths[2];
contentStream.beginText();
contentStream.newLineAtOffset(xOffset, yPosition - 15);
contentStream.showText(headers[3]);
contentStream.endText();
yPosition -= rowHeight;
// Table rows
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 10);
if (data.getInvoiceItems() != null) {
for (SystemInvoiceItem item : data.getInvoiceItems()) {
xOffset = MARGIN + 5;
contentStream.beginText();
contentStream.newLineAtOffset(xOffset, yPosition - 15);
contentStream.showText(item.getQuantity() != null ? item.getQuantity() : "");
contentStream.endText();
xOffset += columnWidths[0];
contentStream.beginText();
contentStream.newLineAtOffset(xOffset, yPosition - 15);
contentStream.showText(item.getDescription() != null ? item.getDescription() : "");
contentStream.endText();
xOffset += columnWidths[1];
contentStream.beginText();
contentStream.newLineAtOffset(xOffset, yPosition - 15);
contentStream.showText(item.getUnitPrice() != null ? item.getUnitPrice() : "");
contentStream.endText();
xOffset += columnWidths[2];
contentStream.beginText();
contentStream.newLineAtOffset(xOffset, yPosition - 15);
contentStream.showText(item.getTotalPrice() != null ? item.getTotalPrice() : "");
contentStream.endText();
yPosition -= rowHeight;
}
}
// Add some empty rows for spacing
for (int i = 0; i < 3; i++) {
yPosition -= rowHeight;
}
return yPosition;
}
private float drawTotals(PDPageContentStream contentStream, float yPosition, SystemInvoiceData data) throws IOException {
float rightColumn = 400;
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
// Net amount
contentStream.beginText();
contentStream.newLineAtOffset(rightColumn, yPosition);
contentStream.showText("Nettobetrag:");
contentStream.endText();
contentStream.beginText();
contentStream.newLineAtOffset(rightColumn + 100, yPosition);
contentStream.showText(data.getNetAmount() != null ? data.getNetAmount() : "");
contentStream.endText();
yPosition -= LINE_HEIGHT;
// VAT
contentStream.beginText();
contentStream.newLineAtOffset(rightColumn, yPosition);
contentStream.showText("+ " + (data.getVatRate() != null ? data.getVatRate() : "") + "% MwSt.:");
contentStream.endText();
contentStream.beginText();
contentStream.newLineAtOffset(rightColumn + 100, yPosition);
contentStream.showText(data.getVatAmount() != null ? data.getVatAmount() : "");
contentStream.endText();
yPosition -= LINE_HEIGHT;
// Total amount
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), FONT_SIZE);
contentStream.beginText();
contentStream.newLineAtOffset(rightColumn, yPosition);
contentStream.showText("Endbetrag:");
contentStream.endText();
contentStream.beginText();
contentStream.newLineAtOffset(rightColumn + 100, yPosition);
contentStream.showText(data.getTotalAmount() != null ? data.getTotalAmount() : "");
contentStream.endText();
return yPosition - 30;
}
private float drawPaymentTerms(PDPageContentStream contentStream, float yPosition, SystemInvoiceData data) throws IOException {
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
// Payment terms with text wrapping
String paymentTerms = data.getPaymentTerms();
if (paymentTerms != null && !paymentTerms.trim().isEmpty()) {
float maxWidth = PDRectangle.A4.getWidth() - 2 * MARGIN;
yPosition = drawWrappedText(contentStream, paymentTerms, MARGIN, yPosition, maxWidth, FONT_SIZE);
}
return yPosition - 20;
}
private float drawWrappedText(PDPageContentStream contentStream, String text, float x, float y, float maxWidth, float fontSize) throws IOException {
String[] words = text.split(" ");
StringBuilder currentLine = new StringBuilder();
float currentY = y;
contentStream.beginText();
contentStream.newLineAtOffset(x, currentY);
for (String word : words) {
String testLine = currentLine.length() > 0 ? currentLine + " " + word : word;
// Estimate text width (rough calculation)
float textWidth = estimateTextWidth(testLine, fontSize);
if (textWidth > maxWidth && currentLine.length() > 0) {
// Print current line and start new line
contentStream.showText(currentLine.toString());
currentLine = new StringBuilder(word);
currentY -= LINE_HEIGHT;
contentStream.newLineAtOffset(0, -LINE_HEIGHT);
} else {
currentLine = new StringBuilder(testLine);
}
}
// Print remaining text
if (currentLine.length() > 0) {
contentStream.showText(currentLine.toString());
currentY -= LINE_HEIGHT;
}
contentStream.endText();
return currentY;
}
private float estimateTextWidth(String text, float fontSize) {
// Rough estimation: average character width is about 0.6 * font size
return text.length() * fontSize * 0.6f;
}
private void drawFooter(PDPageContentStream contentStream, PDPage page, SystemInvoiceData data) throws IOException {
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 8);
// Footer text (company details) - positioned at absolute bottom
String footerText = data.getFooterText();
if (footerText != null) {
String[] footerLines = footerText.split("<br>");
// Calculate footer position from bottom of page
float lineSpacing = 10; // 10 points between lines
float footerBottomMargin = 30; // 30 points from absolute bottom
// Start from the bottom and work upwards
float currentY = footerBottomMargin + (footerLines.length - 1) * lineSpacing;
for (String line : footerLines) {
contentStream.beginText();
contentStream.newLineAtOffset(MARGIN, currentY);
contentStream.showText(line.trim());
contentStream.endText();
currentY -= lineSpacing;
}
}
// Generate PDF from HTML
return generatePdfFromHtmlString(filledHtml);
}
public SystemInvoiceData createSampleInvoiceData() {
SystemInvoiceData data = new SystemInvoiceData();
// Set sample data
data.setInvoiceNumber("HHA-2021-007");
data.setInvoiceDate("19.07.2021");
data.setInvoiceText("Gemäß unserem Nutzungsvertrag zu der Bestellnummer 45519389 berechnen wir Ihnen für den Monat Juli 2021 wie folgt:");
@@ -434,20 +54,6 @@ public class SystemInvoiceService {
return data;
}
public byte[] generateInvoicePdfFromHtml() throws Exception {
// Read the HTML template
String htmlContent = readHtmlTemplate();
// For simplicity, we'll use the existing sample data to fill the PDF
SystemInvoiceData sampleData = createSampleInvoiceData();
// Replace placeholders in HTML with actual data
String filledHtml = fillHtmlWithInvoiceData(htmlContent, sampleData);
// Create PDF from HTML using iText
return generatePdfFromHtmlString(filledHtml);
}
private String readHtmlTemplate() throws Exception {
// Read the HTML template file
java.nio.file.Path path = java.nio.file.Paths.get("src/main/resources/templates/vltInvoice.html");
@@ -458,25 +64,22 @@ public class SystemInvoiceService {
// Replace placeholders in HTML with actual invoice data
String filledHtml = html;
filledHtml = filledHtml.replace("HHA-2021-07", data.getInvoiceNumber() != null ? data.getInvoiceNumber() : "");
// Replace invoice data placeholders
filledHtml = filledHtml.replace("HHA-2021-007", data.getInvoiceNumber() != null ? data.getInvoiceNumber() : "");
filledHtml = filledHtml.replace("19.07.2021", data.getInvoiceDate() != null ? data.getInvoiceDate() : "");
filledHtml = filledHtml.replace("Gemäß unserem Nutzungsvertrag zu der Bestellnummer 45519389 berechnen wir Ihnen für den Monat Juli 2021 wie folgt:",
data.getInvoiceText() != null ? data.getInvoiceText() : "");
// Replace recipient address
filledHtml = filledHtml.replace("Hamburger Hochbahn AG<br>\n Kreditorenbuchhaltung<br>\n Steinstraße 20<br>\n 20095 Hamburg",
data.getRecipientName() != null ? data.getRecipientName() : "" + "<br>\n " +
(data.getRecipientDepartment() != null ? data.getRecipientDepartment() : "") + "<br>\n " +
(data.getRecipientStreet() != null ? data.getRecipientStreet() : "") + "<br>\n " +
(data.getRecipientCity() != null ? data.getRecipientCity() : ""));
filledHtml = filledHtml.replace("Hamburger Hochbahn AG", data.getRecipientName() != null ? data.getRecipientName() : "");
filledHtml = filledHtml.replace("Kreditorenbuchhaltung", data.getRecipientDepartment() != null ? data.getRecipientDepartment() : "");
filledHtml = filledHtml.replace("Steinstraße 20", data.getRecipientStreet() != null ? data.getRecipientStreet() : "");
filledHtml = filledHtml.replace("20095 Hamburg", data.getRecipientCity() != null ? data.getRecipientCity() : "");
// Replace invoice items
if (data.getInvoiceItems() != null && !data.getInvoiceItems().isEmpty()) {
SystemInvoiceItem item = data.getInvoiceItems().get(0);
filledHtml = filledHtml.replace("1", item.getQuantity() != null ? item.getQuantity() : "");
SystemInvoiceItem item = data.getInvoiceItems().getFirst();
filledHtml = filledHtml.replace("Mtl. Lizenzgebühr »ILLT«", item.getDescription() != null ? item.getDescription() : "");
filledHtml = filledHtml.replace("5.639,00 €", item.getUnitPrice() != null ? item.getUnitPrice() : "");
filledHtml = filledHtml.replace("5.639,00 €", item.getTotalPrice() != null ? item.getTotalPrice() : "");
}
// Replace amounts