From 95eaabf41dec0c2396ab904ff9072c51fab81ba7 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Thu, 18 Sep 2025 12:10:37 +0200 Subject: [PATCH] Erweiterungen --- pom.xml | 7 + .../assecutor/votianlt/model/InvoiceData.java | 157 ++++--- .../assecutor/votianlt/model/InvoiceItem.java | 50 +++ .../assecutor/votianlt/model/PriceTable.java | 66 +++ .../pages/base/ui/view/AdminLayout.java | 4 +- .../pages/view/AdminPricetableView.java | 100 +++++ .../votianlt/pages/view/PdfTestView.java | 217 ++-------- .../repository/PriceTableRepository.java | 9 + .../votianlt/service/InvoicePdfGenerator.java | 354 ---------------- .../votianlt/service/PdfBoxService.java | 399 ++++++++++++++++++ .../votianlt/service/PdfService.java | 146 +++++++ .../resources/templates/invoice-template.html | 259 ++++++++++++ 12 files changed, 1178 insertions(+), 590 deletions(-) create mode 100644 src/main/java/de/assecutor/votianlt/model/InvoiceItem.java create mode 100644 src/main/java/de/assecutor/votianlt/model/PriceTable.java create mode 100644 src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java create mode 100644 src/main/java/de/assecutor/votianlt/repository/PriceTableRepository.java delete mode 100644 src/main/java/de/assecutor/votianlt/service/InvoicePdfGenerator.java create mode 100644 src/main/java/de/assecutor/votianlt/service/PdfBoxService.java create mode 100644 src/main/java/de/assecutor/votianlt/service/PdfService.java create mode 100644 src/main/resources/templates/invoice-template.html diff --git a/pom.xml b/pom.xml index e89cb17..045894f 100644 --- a/pom.xml +++ b/pom.xml @@ -137,6 +137,13 @@ jackson-datatype-jsr310 + + + org.apache.pdfbox + pdfbox + 3.0.3 + + diff --git a/src/main/java/de/assecutor/votianlt/model/InvoiceData.java b/src/main/java/de/assecutor/votianlt/model/InvoiceData.java index be5ea5f..341e1a9 100644 --- a/src/main/java/de/assecutor/votianlt/model/InvoiceData.java +++ b/src/main/java/de/assecutor/votianlt/model/InvoiceData.java @@ -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 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
" + + "Steuernummer: 22 294 53099 · USt-IdNr.: DE261094748 · Sitz: Geesthacht · Handelsregister: Lübeck HRB 8595
" + + "Bankverbindung: Hamburger Sparkasse · IBAN DE67200505501217139888 · BIC HASPDEHHXXX"; - // Invoice items - private List 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 getInvoiceItems() { return invoiceItems; } + public void setInvoiceItems(List 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; } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/model/InvoiceItem.java b/src/main/java/de/assecutor/votianlt/model/InvoiceItem.java new file mode 100644 index 0000000..85c3d36 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/InvoiceItem.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/model/PriceTable.java b/src/main/java/de/assecutor/votianlt/model/PriceTable.java new file mode 100644 index 0000000..6349ff2 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/PriceTable.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java index 9d4f2ee..709317a 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java @@ -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; } -} \ No newline at end of file +} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java b/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java new file mode 100644 index 0000000..c451571 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java @@ -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); + } + } +} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/PdfTestView.java b/src/main/java/de/assecutor/votianlt/pages/view/PdfTestView.java index b6d8ab7..b4c2eca 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/PdfTestView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/PdfTestView.java @@ -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", - ""); - - 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); + } } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/repository/PriceTableRepository.java b/src/main/java/de/assecutor/votianlt/repository/PriceTableRepository.java new file mode 100644 index 0000000..dc3142d --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/repository/PriceTableRepository.java @@ -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 { +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/service/InvoicePdfGenerator.java b/src/main/java/de/assecutor/votianlt/service/InvoicePdfGenerator.java deleted file mode 100644 index dbff090..0000000 --- a/src/main/java/de/assecutor/votianlt/service/InvoicePdfGenerator.java +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/service/PdfBoxService.java b/src/main/java/de/assecutor/votianlt/service/PdfBoxService.java new file mode 100644 index 0000000..e49393e --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/service/PdfBoxService.java @@ -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("
"); + 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 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; + } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/service/PdfService.java b/src/main/java/de/assecutor/votianlt/service/PdfService.java new file mode 100644 index 0000000..ac176d9 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/service/PdfService.java @@ -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 items) { + StringBuilder itemsHtml = new StringBuilder(); + + if (items != null) { + for (InvoiceItem item : items) { + itemsHtml.append("") + .append("") + .append(escapeHtml(safeString(item.getQuantity()))) + .append("") + .append("") + .append(escapeHtml(safeString(item.getDescription()))) + .append("") + .append("") + .append(escapeHtml(safeString(item.getUnitPrice()))) + .append("") + .append("") + .append(escapeHtml(safeString(item.getTotalPrice()))) + .append("") + .append(""); + } + } + + // 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("") + .append(" ") + .append(" ") + .append(" ") + .append(" ") + .append(""); + } + + // 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(); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/invoice-template.html b/src/main/resources/templates/invoice-template.html new file mode 100644 index 0000000..c6b7549 --- /dev/null +++ b/src/main/resources/templates/invoice-template.html @@ -0,0 +1,259 @@ + + + + + Rechnung {{invoiceNumber}} + + + +
+

+ {{companyName}} +

+
+

+ {{companySubtitle}} +

+
+ +
+

{{senderLine}}

+

{{recipientName}}

+

{{recipientDepartment}}

+

{{recipientStreet}}

+

{{recipientCity}}

+
+ +
+

{{companyStreet}}

+

{{companyCity}}

+

+

Tel.: {{companyPhone}}

+

{{companyWebsite}}

+
+ +
+ +

+ Fax + eMail +
+ {{companyFax}} + {{companyEmail}} +

+ +

+ Datum
+ {{invoiceDate}} +

+ +

Rechnung {{invoiceNumber}}

+ +

{{invoiceText}}

+ + + + + + + + + {{invoiceItems}} + + + + + + + + + + + + + + + + + + +
MengeBezeichnungEinzelpreisGesamt
Nettobetrag{{netAmount}}
+ {{vatRate}}% MwSt.{{vatAmount}}
Endbetrag{{totalAmount}}
+ +
+

{{paymentTerms}}

+


+ + + + \ No newline at end of file