Erweiterungen

This commit is contained in:
2025-09-18 12:10:37 +02:00
parent a13aa09a63
commit 95eaabf41d
12 changed files with 1178 additions and 590 deletions

View File

@@ -137,6 +137,13 @@
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>jackson-datatype-jsr310</artifactId>
</dependency> </dependency>
<!-- Apache PDFBox for PDF generation -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -1,77 +1,108 @@
package de.assecutor.votianlt.model; package de.assecutor.votianlt.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List; import java.util.List;
@Data
@NoArgsConstructor
public class InvoiceData { public class InvoiceData {
private String companyName = "Assecutor";
private String companySubtitle = "Data Service GmbH";
private String companyStreet = "Gerhart-Hauptmann-Weg 14";
private String companyCity = "21502 Geesthacht";
private String companyPhone = "040-181237710";
private String companyFax = "040-181237719";
private String companyEmail = "info@assecutor.de";
private String companyWebsite = "www.assecutor.de";
// Invoice details
private String invoiceNumber; private String invoiceNumber;
private LocalDate invoiceDate; private String invoiceDate;
private LocalDate dueDate; private String invoiceText;
// Company information (sender) private String senderLine = "Assecutor Data Service GmbH · Gerhart-Hauptmann-Weg 14 · 21502 Geesthacht";
private String companyName; private String recipientName;
private String companyStreet; private String recipientDepartment;
private String companyHouseNumber; private String recipientStreet;
private String companyZip; private String recipientCity;
private String companyCity;
private String companyPhone;
private String companyEmail;
private String companyWebsite;
// Tax information private List<InvoiceItem> invoiceItems;
private String taxNumber; // Steuernummer private String netAmount;
private String vatId; // USt-IdNr private String vatRate = "19";
private String commercialRegister; // Handelsregistereintrag private String vatAmount;
private String managingDirector; // Geschäftsführer private String totalAmount;
// Bank details private String paymentTerms = "Zahlungsbedingungen: Gesamtbetrag bis spätestens zum 10. Werktag nach Rechnungserhalt auf unser u. g. Konto.";
private String bankName;
private String iban;
private String bic;
// Customer information (recipient) private String footerText = "Geschäftsführer: Carsten Annacker, Halstenbek · Gunnar Timm, Geesthacht<br>" +
private String customerName; "Steuernummer: 22 294 53099 · USt-IdNr.: DE261094748 · Sitz: Geesthacht · Handelsregister: Lübeck HRB 8595<br>" +
private String customerStreet; "Bankverbindung: Hamburger Sparkasse · IBAN DE67200505501217139888 · BIC HASPDEHHXXX";
private String customerHouseNumber;
private String customerZip;
private String customerCity;
private String customerCountry;
// Invoice items public InvoiceData() {
private List<InvoiceItem> items;
// Totals
private BigDecimal subtotal;
private BigDecimal vatAmount;
private BigDecimal totalAmount;
private BigDecimal vatRate = BigDecimal.valueOf(19.0); // 19% standard VAT rate
@Data
@NoArgsConstructor
public static class InvoiceItem {
private String description;
private BigDecimal quantity;
private String unit; // e.g., "Stück", "km", "h"
private BigDecimal unitPrice;
private BigDecimal totalPrice;
private BigDecimal vatRate;
public InvoiceItem(String description, BigDecimal quantity, String unit,
BigDecimal unitPrice, BigDecimal vatRate) {
this.description = description;
this.quantity = quantity;
this.unit = unit;
this.unitPrice = unitPrice;
this.vatRate = vatRate;
this.totalPrice = quantity.multiply(unitPrice);
}
} }
public String getCompanyName() { return companyName; }
public void setCompanyName(String companyName) { this.companyName = companyName; }
public String getCompanySubtitle() { return companySubtitle; }
public void setCompanySubtitle(String companySubtitle) { this.companySubtitle = companySubtitle; }
public String getCompanyStreet() { return companyStreet; }
public void setCompanyStreet(String companyStreet) { this.companyStreet = companyStreet; }
public String getCompanyCity() { return companyCity; }
public void setCompanyCity(String companyCity) { this.companyCity = companyCity; }
public String getCompanyPhone() { return companyPhone; }
public void setCompanyPhone(String companyPhone) { this.companyPhone = companyPhone; }
public String getCompanyFax() { return companyFax; }
public void setCompanyFax(String companyFax) { this.companyFax = companyFax; }
public String getCompanyEmail() { return companyEmail; }
public void setCompanyEmail(String companyEmail) { this.companyEmail = companyEmail; }
public String getCompanyWebsite() { return companyWebsite; }
public void setCompanyWebsite(String companyWebsite) { this.companyWebsite = companyWebsite; }
public String getInvoiceNumber() { return invoiceNumber; }
public void setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; }
public String getInvoiceDate() { return invoiceDate; }
public void setInvoiceDate(String invoiceDate) { this.invoiceDate = invoiceDate; }
public String getInvoiceText() { return invoiceText; }
public void setInvoiceText(String invoiceText) { this.invoiceText = invoiceText; }
public String getSenderLine() { return senderLine; }
public void setSenderLine(String senderLine) { this.senderLine = senderLine; }
public String getRecipientName() { return recipientName; }
public void setRecipientName(String recipientName) { this.recipientName = recipientName; }
public String getRecipientDepartment() { return recipientDepartment; }
public void setRecipientDepartment(String recipientDepartment) { this.recipientDepartment = recipientDepartment; }
public String getRecipientStreet() { return recipientStreet; }
public void setRecipientStreet(String recipientStreet) { this.recipientStreet = recipientStreet; }
public String getRecipientCity() { return recipientCity; }
public void setRecipientCity(String recipientCity) { this.recipientCity = recipientCity; }
public List<InvoiceItem> getInvoiceItems() { return invoiceItems; }
public void setInvoiceItems(List<InvoiceItem> invoiceItems) { this.invoiceItems = invoiceItems; }
public String getNetAmount() { return netAmount; }
public void setNetAmount(String netAmount) { this.netAmount = netAmount; }
public String getVatRate() { return vatRate; }
public void setVatRate(String vatRate) { this.vatRate = vatRate; }
public String getVatAmount() { return vatAmount; }
public void setVatAmount(String vatAmount) { this.vatAmount = vatAmount; }
public String getTotalAmount() { return totalAmount; }
public void setTotalAmount(String totalAmount) { this.totalAmount = totalAmount; }
public String getPaymentTerms() { return paymentTerms; }
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
public String getFooterText() { return footerText; }
public void setFooterText(String footerText) { this.footerText = footerText; }
} }

View File

@@ -0,0 +1,50 @@
package de.assecutor.votianlt.model;
public class InvoiceItem {
private String quantity;
private String description;
private String unitPrice;
private String totalPrice;
public InvoiceItem() {
}
public InvoiceItem(String quantity, String description, String unitPrice, String totalPrice) {
this.quantity = quantity;
this.description = description;
this.unitPrice = unitPrice;
this.totalPrice = totalPrice;
}
public String getQuantity() {
return quantity;
}
public void setQuantity(String quantity) {
this.quantity = quantity;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(String unitPrice) {
this.unitPrice = unitPrice;
}
public String getTotalPrice() {
return totalPrice;
}
public void setTotalPrice(String totalPrice) {
this.totalPrice = totalPrice;
}
}

View File

@@ -0,0 +1,66 @@
package de.assecutor.votianlt.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "price_table")
public class PriceTable {
@Id
private String id;
private String monthlyBasePackage;
private String appUsageLicense;
private String revenueParticipation;
private String statisticalEvaluation;
public PriceTable() {
}
public PriceTable(String monthlyBasePackage, String appUsageLicense, String revenueParticipation, String statisticalEvaluation) {
this.monthlyBasePackage = monthlyBasePackage;
this.appUsageLicense = appUsageLicense;
this.revenueParticipation = revenueParticipation;
this.statisticalEvaluation = statisticalEvaluation;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getMonthlyBasePackage() {
return monthlyBasePackage;
}
public void setMonthlyBasePackage(String monthlyBasePackage) {
this.monthlyBasePackage = monthlyBasePackage;
}
public String getAppUsageLicense() {
return appUsageLicense;
}
public void setAppUsageLicense(String appUsageLicense) {
this.appUsageLicense = appUsageLicense;
}
public String getRevenueParticipation() {
return revenueParticipation;
}
public void setRevenueParticipation(String revenueParticipation) {
this.revenueParticipation = revenueParticipation;
}
public String getStatisticalEvaluation() {
return statisticalEvaluation;
}
public void setStatisticalEvaluation(String statisticalEvaluation) {
this.statisticalEvaluation = statisticalEvaluation;
}
}

View File

@@ -78,12 +78,14 @@ public final class AdminLayout extends AppLayout {
// Only admin-specific menu items // Only admin-specific menu items
SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD)); SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD));
SideNavItem pdfTest = new SideNavItem("PDF Test", "pdf-test", new Icon(VaadinIcon.FILE_TEXT_O)); SideNavItem pdfTest = new SideNavItem("PDF Test", "pdf-test", new Icon(VaadinIcon.FILE_TEXT_O));
SideNavItem priceTable = new SideNavItem("Preis-Tabelle", "admin-price-table", new Icon(VaadinIcon.COG));
//SideNavItem systemSettings = new SideNavItem("Systemeinstellungen", "admin-settings", new Icon(VaadinIcon.COG)); //SideNavItem systemSettings = new SideNavItem("Systemeinstellungen", "admin-settings", new Icon(VaadinIcon.COG));
//SideNavItem userManagement = new SideNavItem("Benutzerverwaltung", "admin-users", new Icon(VaadinIcon.USERS)); //SideNavItem userManagement = new SideNavItem("Benutzerverwaltung", "admin-users", new Icon(VaadinIcon.USERS));
//SideNavItem systemLogs = new SideNavItem("System-Logs", "admin-logs", new Icon(VaadinIcon.FILE_TEXT)); //SideNavItem systemLogs = new SideNavItem("System-Logs", "admin-logs", new Icon(VaadinIcon.FILE_TEXT));
nav.addItem(dashboard); nav.addItem(dashboard);
nav.addItem(pdfTest); nav.addItem(pdfTest);
nav.addItem(priceTable);
//nav.addItem(systemSettings); //nav.addItem(systemSettings);
//nav.addItem(userManagement); //nav.addItem(userManagement);
//nav.addItem(systemLogs); //nav.addItem(systemLogs);
@@ -131,4 +133,4 @@ public final class AdminLayout extends AppLayout {
return userMenu; return userMenu;
} }
} }

View File

@@ -0,0 +1,100 @@
package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.PriceTable;
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.repository.PriceTableRepository;
import de.assecutor.votianlt.security.SecurityService;
import jakarta.annotation.security.RolesAllowed;
@Route(value = "admin-price-table", layout = AdminLayout.class)
@PageTitle("Preis-Tabelle")
@RolesAllowed("ADMIN")
public class AdminPricetableView extends VerticalLayout {
private final PriceTableRepository priceTableRepository;
private final TextField monthlyBasePackage;
private final TextField appUsageLicense;
private final TextField revenueParticipation;
public AdminPricetableView(PriceTableRepository priceTableRepository) {
this.priceTableRepository = priceTableRepository;
setSpacing(false);
setPadding(false);
getStyle().set("margin", "14px");
setWidth("90%");
H2 title = new H2("Preis-Tabelle");
add(title);
VerticalLayout fieldsLayout = new VerticalLayout();
fieldsLayout.setSpacing(true);
fieldsLayout.setPadding(false);
monthlyBasePackage = new TextField("Monatliche Grundpauschale");
monthlyBasePackage.setWidth("40%");
monthlyBasePackage.setMaxWidth("40%");
appUsageLicense = new TextField("App-Nutzungslizenz");
appUsageLicense.setWidth("40%");
appUsageLicense.setMaxWidth("40%");
revenueParticipation = new TextField("Umsatzbeteiligung in Prozent");
revenueParticipation.setWidth("40%");
revenueParticipation.setMaxWidth("40%");
fieldsLayout.add(monthlyBasePackage, appUsageLicense, revenueParticipation);
add(fieldsLayout);
Button saveButton = new Button("Speichern");
saveButton.getStyle().set("margin-top", "20px");
saveButton.addClickListener(e -> savePriceTable());
add(saveButton);
// Load existing data
loadPriceTable();
}
private void savePriceTable() {
try {
// Get first entry or create new one
PriceTable priceTable = priceTableRepository.findAll().stream()
.findFirst()
.orElse(new PriceTable());
priceTable.setMonthlyBasePackage(monthlyBasePackage.getValue());
priceTable.setAppUsageLicense(appUsageLicense.getValue());
priceTable.setRevenueParticipation(revenueParticipation.getValue());
priceTableRepository.save(priceTable);
Notification.show("Preise erfolgreich gespeichert!", 3000, Notification.Position.BOTTOM_CENTER);
} catch (Exception ex) {
Notification.show("Fehler beim Speichern: " + ex.getMessage(), 5000, Notification.Position.BOTTOM_CENTER);
}
}
private void loadPriceTable() {
try {
PriceTable priceTable = priceTableRepository.findAll().stream()
.findFirst()
.orElse(null);
if (priceTable != null) {
monthlyBasePackage.setValue(priceTable.getMonthlyBasePackage() != null ? priceTable.getMonthlyBasePackage() : "");
appUsageLicense.setValue(priceTable.getAppUsageLicense() != null ? priceTable.getAppUsageLicense() : "");
revenueParticipation.setValue(priceTable.getRevenueParticipation() != null ? priceTable.getRevenueParticipation() : "");
}
} catch (Exception ex) {
Notification.show("Fehler beim Laden der Daten: " + ex.getMessage(), 5000, Notification.Position.BOTTOM_CENTER);
}
}
}

View File

@@ -1,211 +1,84 @@
package de.assecutor.votianlt.pages.view; package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.StreamResource; import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.InvoiceData; import de.assecutor.votianlt.model.InvoiceData;
import de.assecutor.votianlt.service.InvoicePdfGenerator; import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.service.PdfService;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.math.BigDecimal; import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.Arrays;
@Route(value = "pdf-test", layout = de.assecutor.votianlt.pages.base.ui.view.AdminLayout.class) @Route(value = "pdf-test", layout = AdminLayout.class)
@PageTitle("PDF Test") @PageTitle("PDF Test")
@RolesAllowed("ADMIN") @RolesAllowed("ADMIN")
@Menu(order = 2, icon = "lumo:edit") public class PdfTestView extends VerticalLayout {
@Slf4j
public class PdfTestView extends Main {
private final InvoicePdfGenerator invoicePdfGenerator; private final PdfService pdfService;
private final VerticalLayout contentLayout;
@Autowired public PdfTestView(PdfService pdfService) {
public PdfTestView(InvoicePdfGenerator invoicePdfGenerator) { this.pdfService = pdfService;
this.invoicePdfGenerator = invoicePdfGenerator;
setSizeFull(); setSpacing(false);
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, setPadding(false);
LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM); getStyle().set("margin", "14px");
setWidth("90%");
// Header H2 title = new H2("PDF Test");
H1 title = new H1("PDF Test"); add(title);
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, LumoUtility.Margin.Top.NONE);
Paragraph description = new Paragraph( Button generatePdfButton = new Button("Test-Rechnung PDF generieren");
"Hier können Sie den InvoicePdfGenerator mit Testdaten testen. " + generatePdfButton.addClickListener(e -> generateTestPdf());
"Klicken Sie auf 'PDF generieren', um eine Test-Rechnung zu erstellen."
);
// Generate PDF button Button generateHtmlButton = new Button("HTML-Vorschau generieren");
Button generateButton = new Button("PDF generieren", VaadinIcon.FILE_TEXT_O.create()); generateHtmlButton.addClickListener(e -> generateTestHtml());
generateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
generateButton.addClickListener(e -> generateTestPdf());
// Content layout for results add(generatePdfButton, generateHtmlButton);
contentLayout = new VerticalLayout();
contentLayout.setPadding(false);
contentLayout.setSpacing(true);
add(title, description, generateButton, contentLayout);
} }
private void generateTestPdf() { private void generateTestPdf() {
try { try {
log.info("Generating test PDF with InvoicePdfGenerator"); InvoiceData sampleData = pdfService.createSampleInvoiceData();
byte[] pdfBytes = pdfService.generateInvoicePdf(sampleData);
// Create test invoice data StreamResource resource = new StreamResource("test-rechnung.pdf",
InvoiceData testData = createTestInvoiceData(); () -> new ByteArrayInputStream(pdfBytes));
// Generate PDF
byte[] pdfBytes = invoicePdfGenerator.generateInvoicePdf(testData);
// Clear previous content
contentLayout.removeAll();
// Create download link
StreamResource resource = new StreamResource("test-rechnung.pdf",
() -> new ByteArrayInputStream(pdfBytes));
resource.setContentType("application/pdf"); resource.setContentType("application/pdf");
Anchor downloadLink = new Anchor(resource, ""); getUI().ifPresent(ui -> {
downloadLink.getElement().setAttribute("download", true); var registration = ui.getSession().getResourceRegistry().registerResource(resource);
downloadLink.add(new Button("PDF herunterladen", VaadinIcon.DOWNLOAD.create())); ui.getPage().open(registration.getResourceUri().toString(), "_blank");
});
// Success message
H3 successTitle = new H3("PDF erfolgreich generiert!");
successTitle.addClassName(LumoUtility.TextColor.SUCCESS);
Paragraph info = new Paragraph(
String.format("PDF-Größe: %d KB", pdfBytes.length / 1024)
);
// PDF preview container
Div pdfContainer = new Div();
pdfContainer.addClassName(LumoUtility.Background.CONTRAST_5);
pdfContainer.getStyle()
.set("border-radius", "8px")
.set("padding", "1rem")
.set("margin-top", "1rem");
// Embed PDF for preview
String pdfDataUrl = "data:application/pdf;base64," +
java.util.Base64.getEncoder().encodeToString(pdfBytes);
pdfContainer.getElement().setProperty("innerHTML",
"<embed src=\"" + pdfDataUrl + "\" type=\"application/pdf\" " +
"width=\"100%\" height=\"600px\" style=\"border-radius: 4px;\">");
contentLayout.add(successTitle, info, downloadLink, pdfContainer);
Notification.show("PDF erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER); Notification.show("PDF erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
} catch (Exception ex) {
} catch (Exception e) { Notification.show("Fehler beim Generieren des PDFs: " + ex.getMessage(),
log.error("Error generating test PDF", e); 5000, Notification.Position.BOTTOM_CENTER);
contentLayout.removeAll();
H3 errorTitle = new H3("Fehler beim Generieren des PDFs");
errorTitle.addClassName(LumoUtility.TextColor.ERROR);
Paragraph errorMsg = new Paragraph("Fehler: " + e.getMessage());
contentLayout.add(errorTitle, errorMsg);
Notification.show("Fehler beim PDF-Generieren: " + e.getMessage(),
5000, Notification.Position.BOTTOM_CENTER);
} }
} }
private InvoiceData createTestInvoiceData() { private void generateTestHtml() {
InvoiceData data = new InvoiceData(); try {
InvoiceData sampleData = pdfService.createSampleInvoiceData();
byte[] htmlBytes = pdfService.generateInvoiceHtml(sampleData);
// Invoice details // Create anchor for download
data.setInvoiceNumber("TEST-2024-001"); String htmlContent = new String(htmlBytes, StandardCharsets.UTF_8);
data.setInvoiceDate(LocalDate.now()); String dataUrl = "data:text/html;charset=utf-8," +
data.setDueDate(LocalDate.now().plusDays(14)); java.net.URLEncoder.encode(htmlContent, StandardCharsets.UTF_8);
// Company information getUI().ifPresent(ui -> ui.getPage().open(dataUrl, "_blank"));
data.setCompanyName("VotianLT Logistics GmbH");
data.setCompanyStreet("Musterstraße");
data.setCompanyHouseNumber("123");
data.setCompanyZip("12345");
data.setCompanyCity("Berlin");
data.setCompanyPhone("+49 30 12345678");
data.setCompanyEmail("info@votianlt.de");
data.setCompanyWebsite("www.votianlt.de");
// Tax information Notification.show("HTML-Vorschau erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
data.setTaxNumber("12/345/67890"); } catch (Exception ex) {
data.setVatId("DE123456789"); Notification.show("Fehler beim Generieren der Vorschau: " + ex.getMessage(),
data.setCommercialRegister("HRB 12345 B"); 5000, Notification.Position.BOTTOM_CENTER);
data.setManagingDirector("Max Mustermann"); }
// Bank details
data.setBankName("Deutsche Bank AG");
data.setIban("DE89 3704 0044 0532 0130 00");
data.setBic("COBADEFFXXX");
// Customer information
data.setCustomerName("Musterkunde GmbH");
data.setCustomerStreet("Kundenstraße");
data.setCustomerHouseNumber("456");
data.setCustomerZip("54321");
data.setCustomerCity("Hamburg");
data.setCustomerCountry("Deutschland");
// Invoice items
data.setItems(Arrays.asList(
new InvoiceData.InvoiceItem(
"Transport Hamburg - Berlin",
new BigDecimal("1"),
"Stück",
new BigDecimal("250.00"),
new BigDecimal("19")
),
new InvoiceData.InvoiceItem(
"Zusätzliche Wartezeit",
new BigDecimal("2.5"),
"Stunden",
new BigDecimal("45.00"),
new BigDecimal("19")
),
new InvoiceData.InvoiceItem(
"Verpackungsmaterial",
new BigDecimal("5"),
"Stück",
new BigDecimal("12.50"),
new BigDecimal("19")
),
new InvoiceData.InvoiceItem(
"Expressaufschlag",
new BigDecimal("1"),
"Stück",
new BigDecimal("75.00"),
new BigDecimal("19")
)
));
// VAT rate
data.setVatRate(new BigDecimal("19.0"));
return data;
} }
} }

View File

@@ -0,0 +1,9 @@
package de.assecutor.votianlt.repository;
import de.assecutor.votianlt.model.PriceTable;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PriceTableRepository extends MongoRepository<PriceTable, String> {
}

View File

@@ -1,354 +0,0 @@
package de.assecutor.votianlt.service;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.borders.Border;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.layout.properties.UnitValue;
import de.assecutor.votianlt.model.InvoiceData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
@Service
@Slf4j
public class InvoicePdfGenerator {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY);
private static final int ITEMS_PER_PAGE = 15; // Maximum items per page before page break
public byte[] generateInvoicePdf(InvoiceData invoiceData) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PdfWriter writer = new PdfWriter(baos);
PdfDocument pdfDoc = new PdfDocument(writer);
Document document = new Document(pdfDoc, PageSize.A4)) {
// Set document margins
document.setMargins(50, 50, 80, 50); // top, right, bottom, left
PdfFont font = PdfFontFactory.createFont("Helvetica");
PdfFont boldFont = PdfFontFactory.createFont("Helvetica-Bold");
document.setFont(font);
// Calculate totals
calculateTotals(invoiceData);
// Add header with company and customer info
addHeader(document, invoiceData, boldFont, font);
// Add invoice details
addInvoiceDetails(document, invoiceData, boldFont, font);
// Add invoice items (with page breaks if necessary)
addInvoiceItems(document, invoiceData, boldFont, font);
// Add totals
addTotals(document, invoiceData, boldFont, font);
// Add footer on all pages
addFooter(pdfDoc, invoiceData, font);
document.close();
}
log.info("Generated invoice PDF for invoice number: {}", invoiceData.getInvoiceNumber());
return baos.toByteArray();
}
private void calculateTotals(InvoiceData invoiceData) {
BigDecimal subtotal = BigDecimal.ZERO;
BigDecimal vatAmount = BigDecimal.ZERO;
for (InvoiceData.InvoiceItem item : invoiceData.getItems()) {
BigDecimal itemTotal = item.getQuantity().multiply(item.getUnitPrice());
item.setTotalPrice(itemTotal);
subtotal = subtotal.add(itemTotal);
// Calculate VAT for this item
BigDecimal itemVat = itemTotal.multiply(item.getVatRate())
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
vatAmount = vatAmount.add(itemVat);
}
invoiceData.setSubtotal(subtotal);
invoiceData.setVatAmount(vatAmount);
invoiceData.setTotalAmount(subtotal.add(vatAmount));
}
private void addHeader(Document document, InvoiceData invoiceData, PdfFont boldFont, PdfFont font) {
// Create header table with company info (left) and customer info (right)
Table headerTable = new Table(UnitValue.createPercentArray(new float[]{50, 50}))
.setWidth(UnitValue.createPercentValue(100))
.setMarginBottom(20);
// Company information (left side)
Cell companyCell = new Cell()
.setBorder(Border.NO_BORDER)
.setVerticalAlignment(com.itextpdf.layout.properties.VerticalAlignment.TOP);
companyCell.add(new Paragraph(invoiceData.getCompanyName())
.setFont(boldFont).setFontSize(14));
companyCell.add(new Paragraph(invoiceData.getCompanyStreet() + " " +
invoiceData.getCompanyHouseNumber()).setFont(font).setFontSize(10));
companyCell.add(new Paragraph(invoiceData.getCompanyZip() + " " +
invoiceData.getCompanyCity()).setFont(font).setFontSize(10));
if (invoiceData.getCompanyPhone() != null) {
companyCell.add(new Paragraph("Tel: " + invoiceData.getCompanyPhone())
.setFont(font).setFontSize(10));
}
if (invoiceData.getCompanyEmail() != null) {
companyCell.add(new Paragraph("E-Mail: " + invoiceData.getCompanyEmail())
.setFont(font).setFontSize(10));
}
// Customer information (right side)
Cell customerCell = new Cell()
.setBorder(Border.NO_BORDER)
.setVerticalAlignment(com.itextpdf.layout.properties.VerticalAlignment.TOP);
customerCell.add(new Paragraph("Rechnungsempfänger:")
.setFont(boldFont).setFontSize(10));
customerCell.add(new Paragraph(invoiceData.getCustomerName())
.setFont(boldFont).setFontSize(12));
customerCell.add(new Paragraph(invoiceData.getCustomerStreet() + " " +
invoiceData.getCustomerHouseNumber()).setFont(font).setFontSize(10));
customerCell.add(new Paragraph(invoiceData.getCustomerZip() + " " +
invoiceData.getCustomerCity()).setFont(font).setFontSize(10));
if (invoiceData.getCustomerCountry() != null &&
!invoiceData.getCustomerCountry().equalsIgnoreCase("Deutschland")) {
customerCell.add(new Paragraph(invoiceData.getCustomerCountry())
.setFont(font).setFontSize(10));
}
headerTable.addCell(companyCell);
headerTable.addCell(customerCell);
document.add(headerTable);
}
private void addInvoiceDetails(Document document, InvoiceData invoiceData, PdfFont boldFont, PdfFont font) {
// Invoice title
document.add(new Paragraph("RECHNUNG")
.setFont(boldFont)
.setFontSize(20)
.setTextAlignment(TextAlignment.CENTER)
.setMarginTop(20)
.setMarginBottom(20));
// Invoice details table
Table detailsTable = new Table(UnitValue.createPercentArray(new float[]{30, 20, 30, 20}))
.setWidth(UnitValue.createPercentValue(100))
.setMarginBottom(20);
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("Rechnungsnummer:").setFont(boldFont).setFontSize(10)));
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(invoiceData.getInvoiceNumber()).setFont(font).setFontSize(10)));
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("Rechnungsdatum:").setFont(boldFont).setFontSize(10)));
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(invoiceData.getInvoiceDate().format(DATE_FORMATTER)).setFont(font).setFontSize(10)));
if (invoiceData.getDueDate() != null) {
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("Fälligkeitsdatum:").setFont(boldFont).setFontSize(10)));
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(invoiceData.getDueDate().format(DATE_FORMATTER)).setFont(font).setFontSize(10)));
}
document.add(detailsTable);
}
private void addInvoiceItems(Document document, InvoiceData invoiceData, PdfFont boldFont, PdfFont font) {
// Items table header
Table itemsTable = new Table(UnitValue.createPercentArray(new float[]{40, 10, 10, 15, 10, 15}))
.setWidth(UnitValue.createPercentValue(100))
.setMarginBottom(10);
// Header row
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Beschreibung").setFont(boldFont).setFontSize(10)));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Menge").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Einheit").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.CENTER));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Einzelpreis").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("MwSt%").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Gesamtpreis").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
// Add items
int itemCount = 0;
for (InvoiceData.InvoiceItem item : invoiceData.getItems()) {
if (itemCount > 0 && itemCount % ITEMS_PER_PAGE == 0) {
// Add current table and start new page
document.add(itemsTable);
document.add(new com.itextpdf.layout.element.AreaBreak());
// Create new table with header
itemsTable = new Table(UnitValue.createPercentArray(new float[]{40, 10, 10, 15, 10, 15}))
.setWidth(UnitValue.createPercentValue(100))
.setMarginBottom(10);
// Re-add header
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Beschreibung").setFont(boldFont).setFontSize(10)));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Menge").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Einheit").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.CENTER));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Einzelpreis").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("MwSt%").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Gesamtpreis").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
}
itemsTable.addCell(new Cell().add(new Paragraph(item.getDescription()).setFont(font).setFontSize(9)));
itemsTable.addCell(new Cell().add(new Paragraph(formatDecimal(item.getQuantity())).setFont(font).setFontSize(9))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addCell(new Cell().add(new Paragraph(item.getUnit() != null ? item.getUnit() : "").setFont(font).setFontSize(9))
.setTextAlignment(TextAlignment.CENTER));
itemsTable.addCell(new Cell().add(new Paragraph(formatCurrency(item.getUnitPrice())).setFont(font).setFontSize(9))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addCell(new Cell().add(new Paragraph(formatDecimal(item.getVatRate()) + "%").setFont(font).setFontSize(9))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addCell(new Cell().add(new Paragraph(formatCurrency(item.getTotalPrice())).setFont(font).setFontSize(9))
.setTextAlignment(TextAlignment.RIGHT));
itemCount++;
}
document.add(itemsTable);
}
private void addTotals(Document document, InvoiceData invoiceData, PdfFont boldFont, PdfFont font) {
// Totals table (right-aligned)
Table totalsTable = new Table(UnitValue.createPercentArray(new float[]{70, 30}))
.setWidth(UnitValue.createPercentValue(100))
.setMarginTop(20);
// Empty cell for spacing
totalsTable.addCell(new Cell().setBorder(Border.NO_BORDER));
// Totals cell
Cell totalsCell = new Cell().setBorder(Border.NO_BORDER);
Table innerTotalsTable = new Table(UnitValue.createPercentArray(new float[]{70, 30}))
.setWidth(UnitValue.createPercentValue(100));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("Nettobetrag:").setFont(font).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(formatCurrency(invoiceData.getSubtotal())).setFont(font).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("MwSt (" + formatDecimal(invoiceData.getVatRate()) + "%):").setFont(font).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(formatCurrency(invoiceData.getVatAmount())).setFont(font).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("Rechnungsbetrag:").setFont(boldFont).setFontSize(12))
.setTextAlignment(TextAlignment.RIGHT));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(formatCurrency(invoiceData.getTotalAmount())).setFont(boldFont).setFontSize(12))
.setTextAlignment(TextAlignment.RIGHT));
totalsCell.add(innerTotalsTable);
totalsTable.addCell(totalsCell);
document.add(totalsTable);
}
private void addFooter(PdfDocument pdfDoc, InvoiceData invoiceData, PdfFont font) {
int numberOfPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= numberOfPages; i++) {
com.itextpdf.kernel.pdf.PdfPage page = pdfDoc.getPage(i);
Document footerDoc = new Document(pdfDoc, PageSize.A4);
footerDoc.setFont(font);
// Footer content
Table footerTable = new Table(UnitValue.createPercentArray(new float[]{33, 33, 34}))
.setWidth(UnitValue.createPercentValue(100))
.setFixedPosition(50, 20, 495); // x, y, width
// Bank details
Cell bankCell = new Cell().setBorder(Border.NO_BORDER);
bankCell.add(new Paragraph("Bankverbindung:").setFont(font).setFontSize(8).setBold());
bankCell.add(new Paragraph(invoiceData.getBankName()).setFont(font).setFontSize(7));
bankCell.add(new Paragraph("IBAN: " + invoiceData.getIban()).setFont(font).setFontSize(7));
bankCell.add(new Paragraph("BIC: " + invoiceData.getBic()).setFont(font).setFontSize(7));
// Tax information
Cell taxCell = new Cell().setBorder(Border.NO_BORDER);
taxCell.add(new Paragraph("Steuerliche Angaben:").setFont(font).setFontSize(8).setBold());
if (invoiceData.getTaxNumber() != null) {
taxCell.add(new Paragraph("Steuernr.: " + invoiceData.getTaxNumber()).setFont(font).setFontSize(7));
}
if (invoiceData.getVatId() != null) {
taxCell.add(new Paragraph("USt-IdNr.: " + invoiceData.getVatId()).setFont(font).setFontSize(7));
}
// Company details
Cell companyCell = new Cell().setBorder(Border.NO_BORDER);
if (invoiceData.getCommercialRegister() != null) {
companyCell.add(new Paragraph(invoiceData.getCommercialRegister()).setFont(font).setFontSize(7));
}
if (invoiceData.getManagingDirector() != null) {
companyCell.add(new Paragraph("Geschäftsführer: " + invoiceData.getManagingDirector()).setFont(font).setFontSize(7));
}
footerTable.addCell(bankCell);
footerTable.addCell(taxCell);
footerTable.addCell(companyCell);
footerDoc.add(footerTable);
footerDoc.close();
}
}
private String formatCurrency(BigDecimal amount) {
return String.format(Locale.GERMANY, "%.2f €", amount);
}
private String formatDecimal(BigDecimal amount) {
return String.format(Locale.GERMANY, "%.2f", amount);
}
}

View File

@@ -0,0 +1,399 @@
package de.assecutor.votianlt.service;
import de.assecutor.votianlt.model.InvoiceData;
import de.assecutor.votianlt.model.InvoiceItem;
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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Service
public class PdfBoxService {
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(InvoiceData invoiceData) 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, invoiceData);
yPosition -= 30;
// Draw company and recipient info
yPosition = drawAddresses(contentStream, yPosition, invoiceData);
yPosition -= 30;
// Draw invoice details
yPosition = drawInvoiceDetails(contentStream, yPosition, invoiceData);
yPosition -= 30;
// Draw items table
yPosition = drawItemsTable(contentStream, yPosition, invoiceData);
yPosition -= 30;
// Draw totals
yPosition = drawTotals(contentStream, yPosition, invoiceData);
yPosition -= 30;
// Draw payment terms
yPosition = drawPaymentTerms(contentStream, yPosition, invoiceData);
// Draw footer at bottom of page
drawFooter(contentStream, page, invoiceData);
}
document.save(outputStream);
return outputStream.toByteArray();
}
}
private float drawHeader(PDPageContentStream contentStream, float yPosition, InvoiceData 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());
contentStream.endText();
yPosition -= 30;
// 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());
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, InvoiceData 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());
contentStream.endText();
yPosition -= 20;
// 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;
}
}
// Company address (right side)
float rightYPosition = yPosition + (recipientLines.length * LINE_HEIGHT);
String[] companyLines = {
data.getCompanyStreet(),
data.getCompanyCity(),
"",
"Tel.: " + data.getCompanyPhone(),
data.getCompanyWebsite()
};
for (String line : companyLines) {
contentStream.beginText();
contentStream.newLineAtOffset(rightColumn, rightYPosition);
contentStream.showText(line != null ? line : "");
contentStream.endText();
rightYPosition -= LINE_HEIGHT;
}
return Math.min(yPosition, rightYPosition) - 10;
}
private float drawInvoiceDetails(PDPageContentStream contentStream, float yPosition, InvoiceData data) throws IOException {
// Invoice date (right aligned)
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
contentStream.newLineAtOffset(400, yPosition);
contentStream.showText("Datum");
contentStream.endText();
contentStream.beginText();
contentStream.newLineAtOffset(400, yPosition - LINE_HEIGHT);
contentStream.showText(data.getInvoiceDate());
contentStream.endText();
yPosition -= 40;
// Invoice title
contentStream.beginText();
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), SUBTITLE_FONT_SIZE);
contentStream.newLineAtOffset(MARGIN, yPosition);
contentStream.showText("Rechnung " + 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, InvoiceData data) throws IOException {
float tableWidth = PDRectangle.A4.getWidth() - 2 * MARGIN;
float[] columnWidths = {60, 300, 100, 100}; // Menge, Bezeichnung, Einzelpreis, Gesamt
float rowHeight = 20;
// Table header
contentStream.setNonStrokingColor(230/255f, 230/255f, 230/255f); // Light gray background
contentStream.addRect(MARGIN, yPosition - rowHeight, tableWidth, 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 (InvoiceItem 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, InvoiceData 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() + "% 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, InvoiceData data) throws IOException {
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
// Payment terms
contentStream.beginText();
contentStream.newLineAtOffset(MARGIN, yPosition);
contentStream.showText(data.getPaymentTerms() != null ? data.getPaymentTerms() : "");
contentStream.endText();
return yPosition - 40;
}
private void drawFooter(PDPageContentStream contentStream, PDPage page, InvoiceData data) throws IOException {
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 8);
// Calculate footer position - start from bottom of page
float footerStartY = MARGIN + 60; // 60 points from bottom
// Footer text (company details) - positioned at bottom
String footerText = data.getFooterText();
if (footerText != null) {
String[] footerLines = footerText.split("<br>");
float currentY = footerStartY;
// Calculate the total height needed for footer text
float totalFooterHeight = footerLines.length * 12;
currentY = footerStartY + totalFooterHeight - 12; // Start from top of footer area
for (String line : footerLines) {
contentStream.beginText();
contentStream.newLineAtOffset(MARGIN, currentY);
contentStream.showText(line.trim());
contentStream.endText();
currentY -= 12;
}
}
}
public InvoiceData createSampleInvoiceData() {
InvoiceData data = new InvoiceData();
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:");
data.setRecipientName("Hamburger Hochbahn AG");
data.setRecipientDepartment("Kreditorenbuchhaltung");
data.setRecipientStreet("Steinstraße 20");
data.setRecipientCity("20095 Hamburg");
List<InvoiceItem> items = new ArrayList<>();
items.add(new InvoiceItem("1", "Mtl. Lizenzgebühr »ILLT«", "5.639,00 €", "5.639,00 €"));
data.setInvoiceItems(items);
data.setNetAmount("5.639,00 €");
data.setVatAmount("1.071,41 €");
data.setTotalAmount("6.710,41 €");
return data;
}
}

View File

@@ -0,0 +1,146 @@
package de.assecutor.votianlt.service;
import de.assecutor.votianlt.model.InvoiceData;
import de.assecutor.votianlt.model.InvoiceItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Service
public class PdfService {
@Autowired
private PdfBoxService pdfBoxService;
public byte[] generateInvoicePdf(InvoiceData invoiceData) throws IOException {
// Use PDFBox for actual PDF generation
return pdfBoxService.generateInvoicePdf(invoiceData);
}
public byte[] generateInvoiceHtml(InvoiceData invoiceData) throws IOException {
// Keep HTML generation for preview purposes
String htmlTemplate = loadTemplate();
String processedHtml = processTemplate(htmlTemplate, invoiceData);
return processedHtml.getBytes(StandardCharsets.UTF_8);
}
private String loadTemplate() throws IOException {
ClassPathResource resource = new ClassPathResource("templates/invoice-template.html");
try (InputStream inputStream = resource.getInputStream()) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
}
private String processTemplate(String template, InvoiceData data) {
String processed = template;
// Replace company information
processed = processed.replace("{{companyName}}", escapeHtml(safeString(data.getCompanyName())));
processed = processed.replace("{{companySubtitle}}", escapeHtml(safeString(data.getCompanySubtitle())));
processed = processed.replace("{{companyStreet}}", escapeHtml(safeString(data.getCompanyStreet())));
processed = processed.replace("{{companyCity}}", escapeHtml(safeString(data.getCompanyCity())));
processed = processed.replace("{{companyPhone}}", escapeHtml(safeString(data.getCompanyPhone())));
processed = processed.replace("{{companyFax}}", escapeHtml(safeString(data.getCompanyFax())));
processed = processed.replace("{{companyEmail}}", escapeHtml(safeString(data.getCompanyEmail())));
processed = processed.replace("{{companyWebsite}}", escapeHtml(safeString(data.getCompanyWebsite())));
// Replace invoice details
processed = processed.replace("{{invoiceNumber}}", escapeHtml(safeString(data.getInvoiceNumber())));
processed = processed.replace("{{invoiceDate}}", escapeHtml(safeString(data.getInvoiceDate())));
processed = processed.replace("{{invoiceText}}", escapeHtml(safeString(data.getInvoiceText())));
// Replace recipient information
processed = processed.replace("{{senderLine}}", escapeHtml(safeString(data.getSenderLine())));
processed = processed.replace("{{recipientName}}", escapeHtml(safeString(data.getRecipientName())));
processed = processed.replace("{{recipientDepartment}}", escapeHtml(safeString(data.getRecipientDepartment())));
processed = processed.replace("{{recipientStreet}}", escapeHtml(safeString(data.getRecipientStreet())));
processed = processed.replace("{{recipientCity}}", escapeHtml(safeString(data.getRecipientCity())));
// Replace financial information
processed = processed.replace("{{netAmount}}", escapeHtml(safeString(data.getNetAmount())));
processed = processed.replace("{{vatRate}}", escapeHtml(safeString(data.getVatRate())));
processed = processed.replace("{{vatAmount}}", escapeHtml(safeString(data.getVatAmount())));
processed = processed.replace("{{totalAmount}}", escapeHtml(safeString(data.getTotalAmount())));
// Replace terms and footer (allow HTML for line breaks)
processed = processed.replace("{{paymentTerms}}", safeString(data.getPaymentTerms()));
processed = processed.replace("{{footerText}}", safeString(data.getFooterText()));
// Process invoice items
processed = processInvoiceItems(processed, data.getInvoiceItems());
return processed;
}
private String processInvoiceItems(String template, List<InvoiceItem> items) {
StringBuilder itemsHtml = new StringBuilder();
if (items != null) {
for (InvoiceItem item : items) {
itemsHtml.append("<tr>")
.append("<td class=\"table-cell table-cell-center\" style=\"width:2cm;\">")
.append(escapeHtml(safeString(item.getQuantity())))
.append("</td>")
.append("<td class=\"table-cell\" style=\"width:9.999cm;\">")
.append(escapeHtml(safeString(item.getDescription())))
.append("</td>")
.append("<td class=\"table-cell table-cell-right\" style=\"width:3cm;\">")
.append(escapeHtml(safeString(item.getUnitPrice())))
.append("</td>")
.append("<td class=\"table-cell table-cell-right\" style=\"width:3cm;\">")
.append(escapeHtml(safeString(item.getTotalPrice())))
.append("</td>")
.append("</tr>");
}
}
// Add empty rows to fill the table (up to 12 rows total as in original)
int emptyRowsNeeded = Math.max(0, 12 - (items != null ? items.size() : 0));
StringBuilder emptyRowsHtml = new StringBuilder();
for (int i = 0; i < emptyRowsNeeded; i++) {
emptyRowsHtml.append("<tr>")
.append("<td class=\"table-cell\" style=\"width:2cm;\">&nbsp;</td>")
.append("<td class=\"table-cell\" style=\"width:9.999cm;\">&nbsp;</td>")
.append("<td class=\"table-cell\" style=\"width:3cm;\">&nbsp;</td>")
.append("<td class=\"table-cell\" style=\"width:3cm;\">&nbsp;</td>")
.append("</tr>");
}
// Remove template placeholders
String result = template.replace("{{#invoiceItems}}", "")
.replace("{{/invoiceItems}}", "")
.replace("{{#emptyRows}}", "")
.replace("{{/emptyRows}}", "");
// Replace the placeholder with actual items
String itemsPlaceholder = "{{invoiceItems}}";
if (result.contains(itemsPlaceholder)) {
result = result.replace(itemsPlaceholder, itemsHtml.toString() + emptyRowsHtml.toString());
}
return result;
}
private String safeString(String value) {
return value != null ? value : "";
}
private String escapeHtml(String value) {
if (value == null) return "";
return value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
public InvoiceData createSampleInvoiceData() {
return pdfBoxService.createSampleInvoiceData();
}
}

View File

@@ -0,0 +1,259 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="application/xhtml+xml; charset=utf-8" http-equiv="Content-Type"/>
<title>Rechnung {{invoiceNumber}}</title>
<style type="text/css">
@page {
size: A4;
margin: 1.5cm 1cm 1.5cm 2cm;
}
body {
font-family: Arial, sans-serif;
font-size: 12pt;
margin: 0;
padding: 0;
border: 2px solid #1b12b9;
max-width: 21.001cm;
background-color: transparent;
}
.header {
position: absolute;
top: 1.5cm;
right: 0;
}
.company-name {
font-family: Verdana;
font-size: 24pt;
color: #1b12b9;
text-align: center;
margin-left: 14cm;
font-weight: bold;
}
.company-subtitle {
font-family: Verdana;
font-size: 9pt;
color: #1b12b9;
text-align: center;
margin-left: 14cm;
margin-top: 0.2cm;
font-weight: bold;
}
hr {
border: 0;
border-top: 2px solid #1b12b9;
width: 27cm;
position: absolute;
margin-top: 0cm;
left: 0;
}
.recipient-box {
height: 4.5cm;
width: 8.999cm;
padding: 0;
float: left;
left: 0cm;
margin-top: 5.5cm;
}
.sender-line {
font-size: 8pt;
line-height: 100%;
margin-bottom: 0.106cm;
margin-top: 0cm;
font-family: Arial;
}
.recipient-content {
font-size: 12pt;
line-height: 100%;
margin-bottom: 0cm;
margin-top: 0cm;
font-family: Arial;
}
.company-address {
width: 5.701cm;
padding: 0;
float: right;
margin-left: 14.25cm;
text-align: left;
top: 4cm;
min-height: 3cm;
font-size: 10pt;
font-family: Arial;
line-height: 100%;
}
.contact-info {
position: relative;
left: 110px;
margin-top: -270px;
font-size: 8pt;
line-height: 120%;
font-family: Arial;
}
.invoice-date {
float: right;
margin-right: 150px;
font-size: 8pt;
line-height: 120%;
font-family: Arial;
}
.subject {
font-size: 12pt;
line-height: 120%;
margin-bottom: 0.64cm;
margin-top: 100px;
font-family: Arial;
font-weight: bold;
}
.text-body {
font-size: 12pt;
font-family: Arial;
margin-top: 0cm;
margin-bottom: 0.212cm;
line-height: 120%;
}
.invoice-table {
width: 18cm;
margin-top: 0cm;
margin-bottom: 0.635cm;
border-collapse: collapse;
border-spacing: 0;
}
.table-header {
background-color: #e6e6e6;
padding: 0.097cm;
border-left: 0.018cm solid #808080;
font-size: 8pt;
font-family: Arial;
text-align: left;
}
.table-cell {
padding: 0.097cm;
border-left: 0.018cm solid #808080;
vertical-align: top;
font-size: 12pt;
font-family: Arial;
}
.table-cell-right {
text-align: right;
}
.table-cell-center {
text-align: center;
}
.summary-row {
background-color: #e6e6e6;
}
.footer-text {
text-align: center;
font-size: 8pt;
font-family: Arial;
margin-top: 2cm;
}
.clear {
clear: both;
}
</style>
</head>
<body>
<div class="header">
<p class="company-name">
<b>{{companyName}}</b>
</p>
<hr>
<p class="company-subtitle">
<b>{{companySubtitle}}</b>
</p>
</div>
<div class="recipient-box">
<p class="sender-line">{{senderLine}}</p>
<p class="recipient-content">{{recipientName}}</p>
<p class="recipient-content">{{recipientDepartment}}</p>
<p class="recipient-content">{{recipientStreet}}</p>
<p class="recipient-content">{{recipientCity}}</p>
</div>
<div class="company-address">
<p>{{companyStreet}}</p>
<p>{{companyCity}}</p>
<p> </p>
<p>Tel.: {{companyPhone}}</p>
<p>{{companyWebsite}}</p>
</div>
<div class="clear"></div>
<p class="contact-info">
Fax
<span style="margin-left: 60px">eMail</span>
<br/>
{{companyFax}}
<span>{{companyEmail}}</span>
</p>
<p class="invoice-date">
<span>Datum<br></span>
<span>{{invoiceDate}}</span>
</p>
<p class="subject">Rechnung {{invoiceNumber}}</p>
<p class="text-body">{{invoiceText}}</p>
<table class="invoice-table">
<tr>
<td class="table-header" style="width:2cm;">Menge</td>
<td class="table-header" style="width:9.999cm;">Bezeichnung</td>
<td class="table-header" style="width:3cm;">Einzelpreis</td>
<td class="table-header" style="width:3cm;">Gesamt</td>
</tr>
{{invoiceItems}}
<tr>
<td class="table-cell" style="width:2cm;"> </td>
<td class="table-cell" style="width:9.999cm;"> </td>
<td class="table-header" style="width:3cm;">Nettobetrag</td>
<td class="table-cell summary-row table-cell-right" style="width:3cm;">{{netAmount}}</td>
</tr>
<tr>
<td class="table-cell" style="width:2cm;"> </td>
<td class="table-cell" style="width:9.999cm;"> </td>
<td class="table-cell table-cell-right" style="width:3cm;">+ {{vatRate}}% MwSt.</td>
<td class="table-cell table-cell-right" style="width:3cm;">{{vatAmount}}</td>
</tr>
<tr>
<td class="table-cell" style="width:2cm;"> </td>
<td class="table-cell" style="width:9.999cm;"> </td>
<td class="table-header" style="width:3cm;">Endbetrag</td>
<td class="table-cell summary-row table-cell-right" style="width:3cm;">{{totalAmount}}</td>
</tr>
</table>
<br>
<p class="text-body">{{paymentTerms}}</p>
<br><br><br>
<p class="footer-text">
<span>{{footerText}}</span>
</p>
</body>
</html>