Erweiterungen
This commit is contained in:
@@ -1,77 +1,108 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
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 LocalDate invoiceDate;
|
||||
private LocalDate dueDate;
|
||||
private String invoiceDate;
|
||||
private String invoiceText;
|
||||
|
||||
// Company information (sender)
|
||||
private String companyName;
|
||||
private String companyStreet;
|
||||
private String companyHouseNumber;
|
||||
private String companyZip;
|
||||
private String companyCity;
|
||||
private String companyPhone;
|
||||
private String companyEmail;
|
||||
private String companyWebsite;
|
||||
private String senderLine = "Assecutor Data Service GmbH · Gerhart-Hauptmann-Weg 14 · 21502 Geesthacht";
|
||||
private String recipientName;
|
||||
private String recipientDepartment;
|
||||
private String recipientStreet;
|
||||
private String recipientCity;
|
||||
|
||||
// Tax information
|
||||
private String taxNumber; // Steuernummer
|
||||
private String vatId; // USt-IdNr
|
||||
private String commercialRegister; // Handelsregistereintrag
|
||||
private String managingDirector; // Geschäftsführer
|
||||
private List<InvoiceItem> invoiceItems;
|
||||
private String netAmount;
|
||||
private String vatRate = "19";
|
||||
private String vatAmount;
|
||||
private String totalAmount;
|
||||
|
||||
// Bank details
|
||||
private String bankName;
|
||||
private String iban;
|
||||
private String bic;
|
||||
private String paymentTerms = "Zahlungsbedingungen: Gesamtbetrag bis spätestens zum 10. Werktag nach Rechnungserhalt auf unser u. g. Konto.";
|
||||
|
||||
// Customer information (recipient)
|
||||
private String customerName;
|
||||
private String customerStreet;
|
||||
private String customerHouseNumber;
|
||||
private String customerZip;
|
||||
private String customerCity;
|
||||
private String customerCountry;
|
||||
private String footerText = "Geschäftsführer: Carsten Annacker, Halstenbek · Gunnar Timm, Geesthacht<br>" +
|
||||
"Steuernummer: 22 294 53099 · USt-IdNr.: DE261094748 · Sitz: Geesthacht · Handelsregister: Lübeck HRB 8595<br>" +
|
||||
"Bankverbindung: Hamburger Sparkasse · IBAN DE67200505501217139888 · BIC HASPDEHHXXX";
|
||||
|
||||
// Invoice items
|
||||
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 InvoiceData() {
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
50
src/main/java/de/assecutor/votianlt/model/InvoiceItem.java
Normal file
50
src/main/java/de/assecutor/votianlt/model/InvoiceItem.java
Normal 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;
|
||||
}
|
||||
}
|
||||
66
src/main/java/de/assecutor/votianlt/model/PriceTable.java
Normal file
66
src/main/java/de/assecutor/votianlt/model/PriceTable.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -78,12 +78,14 @@ public final class AdminLayout extends AppLayout {
|
||||
// Only admin-specific menu items
|
||||
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 priceTable = new SideNavItem("Preis-Tabelle", "admin-price-table", 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 systemLogs = new SideNavItem("System-Logs", "admin-logs", new Icon(VaadinIcon.FILE_TEXT));
|
||||
|
||||
nav.addItem(dashboard);
|
||||
nav.addItem(pdfTest);
|
||||
nav.addItem(priceTable);
|
||||
//nav.addItem(systemSettings);
|
||||
//nav.addItem(userManagement);
|
||||
//nav.addItem(systemLogs);
|
||||
@@ -131,4 +133,4 @@ public final class AdminLayout extends AppLayout {
|
||||
|
||||
return userMenu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,211 +1,84 @@
|
||||
package de.assecutor.votianlt.pages.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
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.html.H2;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.Menu;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Route(value = "pdf-test", layout = de.assecutor.votianlt.pages.base.ui.view.AdminLayout.class)
|
||||
@Route(value = "pdf-test", layout = AdminLayout.class)
|
||||
@PageTitle("PDF Test")
|
||||
@RolesAllowed("ADMIN")
|
||||
@Menu(order = 2, icon = "lumo:edit")
|
||||
@Slf4j
|
||||
public class PdfTestView extends Main {
|
||||
public class PdfTestView extends VerticalLayout {
|
||||
|
||||
private final InvoicePdfGenerator invoicePdfGenerator;
|
||||
private final VerticalLayout contentLayout;
|
||||
private final PdfService pdfService;
|
||||
|
||||
@Autowired
|
||||
public PdfTestView(InvoicePdfGenerator invoicePdfGenerator) {
|
||||
this.invoicePdfGenerator = invoicePdfGenerator;
|
||||
public PdfTestView(PdfService pdfService) {
|
||||
this.pdfService = pdfService;
|
||||
|
||||
setSizeFull();
|
||||
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX,
|
||||
LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM);
|
||||
setSpacing(false);
|
||||
setPadding(false);
|
||||
getStyle().set("margin", "14px");
|
||||
setWidth("90%");
|
||||
|
||||
// Header
|
||||
H1 title = new H1("PDF Test");
|
||||
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, LumoUtility.Margin.Top.NONE);
|
||||
H2 title = new H2("PDF Test");
|
||||
add(title);
|
||||
|
||||
Paragraph description = new Paragraph(
|
||||
"Hier können Sie den InvoicePdfGenerator mit Testdaten testen. " +
|
||||
"Klicken Sie auf 'PDF generieren', um eine Test-Rechnung zu erstellen."
|
||||
);
|
||||
Button generatePdfButton = new Button("Test-Rechnung PDF generieren");
|
||||
generatePdfButton.addClickListener(e -> generateTestPdf());
|
||||
|
||||
// Generate PDF button
|
||||
Button generateButton = new Button("PDF generieren", VaadinIcon.FILE_TEXT_O.create());
|
||||
generateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
generateButton.addClickListener(e -> generateTestPdf());
|
||||
Button generateHtmlButton = new Button("HTML-Vorschau generieren");
|
||||
generateHtmlButton.addClickListener(e -> generateTestHtml());
|
||||
|
||||
// Content layout for results
|
||||
contentLayout = new VerticalLayout();
|
||||
contentLayout.setPadding(false);
|
||||
contentLayout.setSpacing(true);
|
||||
|
||||
add(title, description, generateButton, contentLayout);
|
||||
add(generatePdfButton, generateHtmlButton);
|
||||
}
|
||||
|
||||
private void generateTestPdf() {
|
||||
try {
|
||||
log.info("Generating test PDF with InvoicePdfGenerator");
|
||||
InvoiceData sampleData = pdfService.createSampleInvoiceData();
|
||||
byte[] pdfBytes = pdfService.generateInvoicePdf(sampleData);
|
||||
|
||||
// Create test invoice data
|
||||
InvoiceData testData = createTestInvoiceData();
|
||||
|
||||
// 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));
|
||||
StreamResource resource = new StreamResource("test-rechnung.pdf",
|
||||
() -> new ByteArrayInputStream(pdfBytes));
|
||||
resource.setContentType("application/pdf");
|
||||
|
||||
Anchor downloadLink = new Anchor(resource, "");
|
||||
downloadLink.getElement().setAttribute("download", true);
|
||||
downloadLink.add(new Button("PDF herunterladen", VaadinIcon.DOWNLOAD.create()));
|
||||
|
||||
// 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);
|
||||
getUI().ifPresent(ui -> {
|
||||
var registration = ui.getSession().getResourceRegistry().registerResource(resource);
|
||||
ui.getPage().open(registration.getResourceUri().toString(), "_blank");
|
||||
});
|
||||
|
||||
Notification.show("PDF erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error generating test PDF", e);
|
||||
|
||||
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);
|
||||
} catch (Exception ex) {
|
||||
Notification.show("Fehler beim Generieren des PDFs: " + ex.getMessage(),
|
||||
5000, Notification.Position.BOTTOM_CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
private InvoiceData createTestInvoiceData() {
|
||||
InvoiceData data = new InvoiceData();
|
||||
private void generateTestHtml() {
|
||||
try {
|
||||
InvoiceData sampleData = pdfService.createSampleInvoiceData();
|
||||
byte[] htmlBytes = pdfService.generateInvoiceHtml(sampleData);
|
||||
|
||||
// Invoice details
|
||||
data.setInvoiceNumber("TEST-2024-001");
|
||||
data.setInvoiceDate(LocalDate.now());
|
||||
data.setDueDate(LocalDate.now().plusDays(14));
|
||||
// Create anchor for download
|
||||
String htmlContent = new String(htmlBytes, StandardCharsets.UTF_8);
|
||||
String dataUrl = "data:text/html;charset=utf-8," +
|
||||
java.net.URLEncoder.encode(htmlContent, StandardCharsets.UTF_8);
|
||||
|
||||
// Company information
|
||||
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");
|
||||
getUI().ifPresent(ui -> ui.getPage().open(dataUrl, "_blank"));
|
||||
|
||||
// Tax information
|
||||
data.setTaxNumber("12/345/67890");
|
||||
data.setVatId("DE123456789");
|
||||
data.setCommercialRegister("HRB 12345 B");
|
||||
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;
|
||||
Notification.show("HTML-Vorschau erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
|
||||
} catch (Exception ex) {
|
||||
Notification.show("Fehler beim Generieren der Vorschau: " + ex.getMessage(),
|
||||
5000, Notification.Position.BOTTOM_CENTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
399
src/main/java/de/assecutor/votianlt/service/PdfBoxService.java
Normal file
399
src/main/java/de/assecutor/votianlt/service/PdfBoxService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
146
src/main/java/de/assecutor/votianlt/service/PdfService.java
Normal file
146
src/main/java/de/assecutor/votianlt/service/PdfService.java
Normal 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;\"> </td>")
|
||||
.append("<td class=\"table-cell\" style=\"width:9.999cm;\"> </td>")
|
||||
.append("<td class=\"table-cell\" style=\"width:3cm;\"> </td>")
|
||||
.append("<td class=\"table-cell\" style=\"width:3cm;\"> </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("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
public InvoiceData createSampleInvoiceData() {
|
||||
return pdfBoxService.createSampleInvoiceData();
|
||||
}
|
||||
}
|
||||
259
src/main/resources/templates/invoice-template.html
Normal file
259
src/main/resources/templates/invoice-template.html
Normal 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>
|
||||
Reference in New Issue
Block a user