Erweiterungen
This commit is contained in:
7
pom.xml
7
pom.xml
@@ -137,6 +137,13 @@
|
|||||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Apache PDFBox for PDF generation -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.pdfbox</groupId>
|
||||||
|
<artifactId>pdfbox</artifactId>
|
||||||
|
<version>3.0.3</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -1,77 +1,108 @@
|
|||||||
package de.assecutor.votianlt.model;
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class InvoiceData {
|
public class InvoiceData {
|
||||||
|
private String companyName = "Assecutor";
|
||||||
|
private String companySubtitle = "Data Service GmbH";
|
||||||
|
private String companyStreet = "Gerhart-Hauptmann-Weg 14";
|
||||||
|
private String companyCity = "21502 Geesthacht";
|
||||||
|
private String companyPhone = "040-181237710";
|
||||||
|
private String companyFax = "040-181237719";
|
||||||
|
private String companyEmail = "info@assecutor.de";
|
||||||
|
private String companyWebsite = "www.assecutor.de";
|
||||||
|
|
||||||
// Invoice details
|
|
||||||
private String invoiceNumber;
|
private String invoiceNumber;
|
||||||
private LocalDate invoiceDate;
|
private String invoiceDate;
|
||||||
private LocalDate dueDate;
|
private String invoiceText;
|
||||||
|
|
||||||
// Company information (sender)
|
private String senderLine = "Assecutor Data Service GmbH · Gerhart-Hauptmann-Weg 14 · 21502 Geesthacht";
|
||||||
private String companyName;
|
private String recipientName;
|
||||||
private String companyStreet;
|
private String recipientDepartment;
|
||||||
private String companyHouseNumber;
|
private String recipientStreet;
|
||||||
private String companyZip;
|
private String recipientCity;
|
||||||
private String companyCity;
|
|
||||||
private String companyPhone;
|
|
||||||
private String companyEmail;
|
|
||||||
private String companyWebsite;
|
|
||||||
|
|
||||||
// Tax information
|
private List<InvoiceItem> invoiceItems;
|
||||||
private String taxNumber; // Steuernummer
|
private String netAmount;
|
||||||
private String vatId; // USt-IdNr
|
private String vatRate = "19";
|
||||||
private String commercialRegister; // Handelsregistereintrag
|
private String vatAmount;
|
||||||
private String managingDirector; // Geschäftsführer
|
private String totalAmount;
|
||||||
|
|
||||||
// Bank details
|
private String paymentTerms = "Zahlungsbedingungen: Gesamtbetrag bis spätestens zum 10. Werktag nach Rechnungserhalt auf unser u. g. Konto.";
|
||||||
private String bankName;
|
|
||||||
private String iban;
|
|
||||||
private String bic;
|
|
||||||
|
|
||||||
// Customer information (recipient)
|
private String footerText = "Geschäftsführer: Carsten Annacker, Halstenbek · Gunnar Timm, Geesthacht<br>" +
|
||||||
private String customerName;
|
"Steuernummer: 22 294 53099 · USt-IdNr.: DE261094748 · Sitz: Geesthacht · Handelsregister: Lübeck HRB 8595<br>" +
|
||||||
private String customerStreet;
|
"Bankverbindung: Hamburger Sparkasse · IBAN DE67200505501217139888 · BIC HASPDEHHXXX";
|
||||||
private String customerHouseNumber;
|
|
||||||
private String customerZip;
|
|
||||||
private String customerCity;
|
|
||||||
private String customerCountry;
|
|
||||||
|
|
||||||
// Invoice items
|
public InvoiceData() {
|
||||||
private List<InvoiceItem> items;
|
|
||||||
|
|
||||||
// Totals
|
|
||||||
private BigDecimal subtotal;
|
|
||||||
private BigDecimal vatAmount;
|
|
||||||
private BigDecimal totalAmount;
|
|
||||||
private BigDecimal vatRate = BigDecimal.valueOf(19.0); // 19% standard VAT rate
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
public static class InvoiceItem {
|
|
||||||
private String description;
|
|
||||||
private BigDecimal quantity;
|
|
||||||
private String unit; // e.g., "Stück", "km", "h"
|
|
||||||
private BigDecimal unitPrice;
|
|
||||||
private BigDecimal totalPrice;
|
|
||||||
private BigDecimal vatRate;
|
|
||||||
|
|
||||||
public InvoiceItem(String description, BigDecimal quantity, String unit,
|
|
||||||
BigDecimal unitPrice, BigDecimal vatRate) {
|
|
||||||
this.description = description;
|
|
||||||
this.quantity = quantity;
|
|
||||||
this.unit = unit;
|
|
||||||
this.unitPrice = unitPrice;
|
|
||||||
this.vatRate = vatRate;
|
|
||||||
this.totalPrice = quantity.multiply(unitPrice);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCompanyName() { return companyName; }
|
||||||
|
public void setCompanyName(String companyName) { this.companyName = companyName; }
|
||||||
|
|
||||||
|
public String getCompanySubtitle() { return companySubtitle; }
|
||||||
|
public void setCompanySubtitle(String companySubtitle) { this.companySubtitle = companySubtitle; }
|
||||||
|
|
||||||
|
public String getCompanyStreet() { return companyStreet; }
|
||||||
|
public void setCompanyStreet(String companyStreet) { this.companyStreet = companyStreet; }
|
||||||
|
|
||||||
|
public String getCompanyCity() { return companyCity; }
|
||||||
|
public void setCompanyCity(String companyCity) { this.companyCity = companyCity; }
|
||||||
|
|
||||||
|
public String getCompanyPhone() { return companyPhone; }
|
||||||
|
public void setCompanyPhone(String companyPhone) { this.companyPhone = companyPhone; }
|
||||||
|
|
||||||
|
public String getCompanyFax() { return companyFax; }
|
||||||
|
public void setCompanyFax(String companyFax) { this.companyFax = companyFax; }
|
||||||
|
|
||||||
|
public String getCompanyEmail() { return companyEmail; }
|
||||||
|
public void setCompanyEmail(String companyEmail) { this.companyEmail = companyEmail; }
|
||||||
|
|
||||||
|
public String getCompanyWebsite() { return companyWebsite; }
|
||||||
|
public void setCompanyWebsite(String companyWebsite) { this.companyWebsite = companyWebsite; }
|
||||||
|
|
||||||
|
public String getInvoiceNumber() { return invoiceNumber; }
|
||||||
|
public void setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; }
|
||||||
|
|
||||||
|
public String getInvoiceDate() { return invoiceDate; }
|
||||||
|
public void setInvoiceDate(String invoiceDate) { this.invoiceDate = invoiceDate; }
|
||||||
|
|
||||||
|
public String getInvoiceText() { return invoiceText; }
|
||||||
|
public void setInvoiceText(String invoiceText) { this.invoiceText = invoiceText; }
|
||||||
|
|
||||||
|
public String getSenderLine() { return senderLine; }
|
||||||
|
public void setSenderLine(String senderLine) { this.senderLine = senderLine; }
|
||||||
|
|
||||||
|
public String getRecipientName() { return recipientName; }
|
||||||
|
public void setRecipientName(String recipientName) { this.recipientName = recipientName; }
|
||||||
|
|
||||||
|
public String getRecipientDepartment() { return recipientDepartment; }
|
||||||
|
public void setRecipientDepartment(String recipientDepartment) { this.recipientDepartment = recipientDepartment; }
|
||||||
|
|
||||||
|
public String getRecipientStreet() { return recipientStreet; }
|
||||||
|
public void setRecipientStreet(String recipientStreet) { this.recipientStreet = recipientStreet; }
|
||||||
|
|
||||||
|
public String getRecipientCity() { return recipientCity; }
|
||||||
|
public void setRecipientCity(String recipientCity) { this.recipientCity = recipientCity; }
|
||||||
|
|
||||||
|
public List<InvoiceItem> getInvoiceItems() { return invoiceItems; }
|
||||||
|
public void setInvoiceItems(List<InvoiceItem> invoiceItems) { this.invoiceItems = invoiceItems; }
|
||||||
|
|
||||||
|
public String getNetAmount() { return netAmount; }
|
||||||
|
public void setNetAmount(String netAmount) { this.netAmount = netAmount; }
|
||||||
|
|
||||||
|
public String getVatRate() { return vatRate; }
|
||||||
|
public void setVatRate(String vatRate) { this.vatRate = vatRate; }
|
||||||
|
|
||||||
|
public String getVatAmount() { return vatAmount; }
|
||||||
|
public void setVatAmount(String vatAmount) { this.vatAmount = vatAmount; }
|
||||||
|
|
||||||
|
public String getTotalAmount() { return totalAmount; }
|
||||||
|
public void setTotalAmount(String totalAmount) { this.totalAmount = totalAmount; }
|
||||||
|
|
||||||
|
public String getPaymentTerms() { return paymentTerms; }
|
||||||
|
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
|
||||||
|
|
||||||
|
public String getFooterText() { return footerText; }
|
||||||
|
public void setFooterText(String footerText) { this.footerText = footerText; }
|
||||||
}
|
}
|
||||||
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
|
// Only admin-specific menu items
|
||||||
SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD));
|
SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD));
|
||||||
SideNavItem pdfTest = new SideNavItem("PDF Test", "pdf-test", new Icon(VaadinIcon.FILE_TEXT_O));
|
SideNavItem pdfTest = new SideNavItem("PDF Test", "pdf-test", new Icon(VaadinIcon.FILE_TEXT_O));
|
||||||
|
SideNavItem priceTable = new SideNavItem("Preis-Tabelle", "admin-price-table", new Icon(VaadinIcon.COG));
|
||||||
//SideNavItem systemSettings = new SideNavItem("Systemeinstellungen", "admin-settings", new Icon(VaadinIcon.COG));
|
//SideNavItem systemSettings = new SideNavItem("Systemeinstellungen", "admin-settings", new Icon(VaadinIcon.COG));
|
||||||
//SideNavItem userManagement = new SideNavItem("Benutzerverwaltung", "admin-users", new Icon(VaadinIcon.USERS));
|
//SideNavItem userManagement = new SideNavItem("Benutzerverwaltung", "admin-users", new Icon(VaadinIcon.USERS));
|
||||||
//SideNavItem systemLogs = new SideNavItem("System-Logs", "admin-logs", new Icon(VaadinIcon.FILE_TEXT));
|
//SideNavItem systemLogs = new SideNavItem("System-Logs", "admin-logs", new Icon(VaadinIcon.FILE_TEXT));
|
||||||
|
|
||||||
nav.addItem(dashboard);
|
nav.addItem(dashboard);
|
||||||
nav.addItem(pdfTest);
|
nav.addItem(pdfTest);
|
||||||
|
nav.addItem(priceTable);
|
||||||
//nav.addItem(systemSettings);
|
//nav.addItem(systemSettings);
|
||||||
//nav.addItem(userManagement);
|
//nav.addItem(userManagement);
|
||||||
//nav.addItem(systemLogs);
|
//nav.addItem(systemLogs);
|
||||||
|
|||||||
@@ -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;
|
package de.assecutor.votianlt.pages.view;
|
||||||
|
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
import com.vaadin.flow.component.button.ButtonVariant;
|
import com.vaadin.flow.component.html.H2;
|
||||||
import com.vaadin.flow.component.html.Anchor;
|
|
||||||
import com.vaadin.flow.component.html.Div;
|
|
||||||
import com.vaadin.flow.component.html.H1;
|
|
||||||
import com.vaadin.flow.component.html.H3;
|
|
||||||
import com.vaadin.flow.component.html.Main;
|
|
||||||
import com.vaadin.flow.component.html.Paragraph;
|
|
||||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
import com.vaadin.flow.router.Menu;
|
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
import com.vaadin.flow.server.StreamResource;
|
import com.vaadin.flow.server.StreamResource;
|
||||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
|
||||||
import de.assecutor.votianlt.model.InvoiceData;
|
import de.assecutor.votianlt.model.InvoiceData;
|
||||||
import de.assecutor.votianlt.service.InvoicePdfGenerator;
|
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
|
||||||
|
import de.assecutor.votianlt.service.PdfService;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.math.BigDecimal;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
@Route(value = "pdf-test", layout = de.assecutor.votianlt.pages.base.ui.view.AdminLayout.class)
|
@Route(value = "pdf-test", layout = AdminLayout.class)
|
||||||
@PageTitle("PDF Test")
|
@PageTitle("PDF Test")
|
||||||
@RolesAllowed("ADMIN")
|
@RolesAllowed("ADMIN")
|
||||||
@Menu(order = 2, icon = "lumo:edit")
|
public class PdfTestView extends VerticalLayout {
|
||||||
@Slf4j
|
|
||||||
public class PdfTestView extends Main {
|
|
||||||
|
|
||||||
private final InvoicePdfGenerator invoicePdfGenerator;
|
private final PdfService pdfService;
|
||||||
private final VerticalLayout contentLayout;
|
|
||||||
|
|
||||||
@Autowired
|
public PdfTestView(PdfService pdfService) {
|
||||||
public PdfTestView(InvoicePdfGenerator invoicePdfGenerator) {
|
this.pdfService = pdfService;
|
||||||
this.invoicePdfGenerator = invoicePdfGenerator;
|
|
||||||
|
|
||||||
setSizeFull();
|
setSpacing(false);
|
||||||
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX,
|
setPadding(false);
|
||||||
LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM);
|
getStyle().set("margin", "14px");
|
||||||
|
setWidth("90%");
|
||||||
|
|
||||||
// Header
|
H2 title = new H2("PDF Test");
|
||||||
H1 title = new H1("PDF Test");
|
add(title);
|
||||||
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, LumoUtility.Margin.Top.NONE);
|
|
||||||
|
|
||||||
Paragraph description = new Paragraph(
|
Button generatePdfButton = new Button("Test-Rechnung PDF generieren");
|
||||||
"Hier können Sie den InvoicePdfGenerator mit Testdaten testen. " +
|
generatePdfButton.addClickListener(e -> generateTestPdf());
|
||||||
"Klicken Sie auf 'PDF generieren', um eine Test-Rechnung zu erstellen."
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate PDF button
|
Button generateHtmlButton = new Button("HTML-Vorschau generieren");
|
||||||
Button generateButton = new Button("PDF generieren", VaadinIcon.FILE_TEXT_O.create());
|
generateHtmlButton.addClickListener(e -> generateTestHtml());
|
||||||
generateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
|
||||||
generateButton.addClickListener(e -> generateTestPdf());
|
|
||||||
|
|
||||||
// Content layout for results
|
add(generatePdfButton, generateHtmlButton);
|
||||||
contentLayout = new VerticalLayout();
|
|
||||||
contentLayout.setPadding(false);
|
|
||||||
contentLayout.setSpacing(true);
|
|
||||||
|
|
||||||
add(title, description, generateButton, contentLayout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void generateTestPdf() {
|
private void generateTestPdf() {
|
||||||
try {
|
try {
|
||||||
log.info("Generating test PDF with InvoicePdfGenerator");
|
InvoiceData sampleData = pdfService.createSampleInvoiceData();
|
||||||
|
byte[] pdfBytes = pdfService.generateInvoicePdf(sampleData);
|
||||||
|
|
||||||
// Create test invoice data
|
|
||||||
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",
|
StreamResource resource = new StreamResource("test-rechnung.pdf",
|
||||||
() -> new ByteArrayInputStream(pdfBytes));
|
() -> new ByteArrayInputStream(pdfBytes));
|
||||||
resource.setContentType("application/pdf");
|
resource.setContentType("application/pdf");
|
||||||
|
|
||||||
Anchor downloadLink = new Anchor(resource, "");
|
getUI().ifPresent(ui -> {
|
||||||
downloadLink.getElement().setAttribute("download", true);
|
var registration = ui.getSession().getResourceRegistry().registerResource(resource);
|
||||||
downloadLink.add(new Button("PDF herunterladen", VaadinIcon.DOWNLOAD.create()));
|
ui.getPage().open(registration.getResourceUri().toString(), "_blank");
|
||||||
|
});
|
||||||
// Success message
|
|
||||||
H3 successTitle = new H3("PDF erfolgreich generiert!");
|
|
||||||
successTitle.addClassName(LumoUtility.TextColor.SUCCESS);
|
|
||||||
|
|
||||||
Paragraph info = new Paragraph(
|
|
||||||
String.format("PDF-Größe: %d KB", pdfBytes.length / 1024)
|
|
||||||
);
|
|
||||||
|
|
||||||
// PDF preview container
|
|
||||||
Div pdfContainer = new Div();
|
|
||||||
pdfContainer.addClassName(LumoUtility.Background.CONTRAST_5);
|
|
||||||
pdfContainer.getStyle()
|
|
||||||
.set("border-radius", "8px")
|
|
||||||
.set("padding", "1rem")
|
|
||||||
.set("margin-top", "1rem");
|
|
||||||
|
|
||||||
// Embed PDF for preview
|
|
||||||
String pdfDataUrl = "data:application/pdf;base64," +
|
|
||||||
java.util.Base64.getEncoder().encodeToString(pdfBytes);
|
|
||||||
|
|
||||||
pdfContainer.getElement().setProperty("innerHTML",
|
|
||||||
"<embed src=\"" + pdfDataUrl + "\" type=\"application/pdf\" " +
|
|
||||||
"width=\"100%\" height=\"600px\" style=\"border-radius: 4px;\">");
|
|
||||||
|
|
||||||
contentLayout.add(successTitle, info, downloadLink, pdfContainer);
|
|
||||||
|
|
||||||
Notification.show("PDF erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
|
Notification.show("PDF erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
|
} catch (Exception ex) {
|
||||||
} catch (Exception e) {
|
Notification.show("Fehler beim Generieren des PDFs: " + ex.getMessage(),
|
||||||
log.error("Error generating test PDF", e);
|
5000, Notification.Position.BOTTOM_CENTER);
|
||||||
|
|
||||||
contentLayout.removeAll();
|
|
||||||
|
|
||||||
H3 errorTitle = new H3("Fehler beim Generieren des PDFs");
|
|
||||||
errorTitle.addClassName(LumoUtility.TextColor.ERROR);
|
|
||||||
|
|
||||||
Paragraph errorMsg = new Paragraph("Fehler: " + e.getMessage());
|
|
||||||
|
|
||||||
contentLayout.add(errorTitle, errorMsg);
|
|
||||||
|
|
||||||
Notification.show("Fehler beim PDF-Generieren: " + e.getMessage(),
|
|
||||||
5000, Notification.Position.BOTTOM_CENTER);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private InvoiceData createTestInvoiceData() {
|
private void generateTestHtml() {
|
||||||
InvoiceData data = new InvoiceData();
|
try {
|
||||||
|
InvoiceData sampleData = pdfService.createSampleInvoiceData();
|
||||||
|
byte[] htmlBytes = pdfService.generateInvoiceHtml(sampleData);
|
||||||
|
|
||||||
// Invoice details
|
// Create anchor for download
|
||||||
data.setInvoiceNumber("TEST-2024-001");
|
String htmlContent = new String(htmlBytes, StandardCharsets.UTF_8);
|
||||||
data.setInvoiceDate(LocalDate.now());
|
String dataUrl = "data:text/html;charset=utf-8," +
|
||||||
data.setDueDate(LocalDate.now().plusDays(14));
|
java.net.URLEncoder.encode(htmlContent, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
// Company information
|
getUI().ifPresent(ui -> ui.getPage().open(dataUrl, "_blank"));
|
||||||
data.setCompanyName("VotianLT Logistics GmbH");
|
|
||||||
data.setCompanyStreet("Musterstraße");
|
|
||||||
data.setCompanyHouseNumber("123");
|
|
||||||
data.setCompanyZip("12345");
|
|
||||||
data.setCompanyCity("Berlin");
|
|
||||||
data.setCompanyPhone("+49 30 12345678");
|
|
||||||
data.setCompanyEmail("info@votianlt.de");
|
|
||||||
data.setCompanyWebsite("www.votianlt.de");
|
|
||||||
|
|
||||||
// Tax information
|
Notification.show("HTML-Vorschau erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
data.setTaxNumber("12/345/67890");
|
} catch (Exception ex) {
|
||||||
data.setVatId("DE123456789");
|
Notification.show("Fehler beim Generieren der Vorschau: " + ex.getMessage(),
|
||||||
data.setCommercialRegister("HRB 12345 B");
|
5000, Notification.Position.BOTTOM_CENTER);
|
||||||
data.setManagingDirector("Max Mustermann");
|
}
|
||||||
|
|
||||||
// Bank details
|
|
||||||
data.setBankName("Deutsche Bank AG");
|
|
||||||
data.setIban("DE89 3704 0044 0532 0130 00");
|
|
||||||
data.setBic("COBADEFFXXX");
|
|
||||||
|
|
||||||
// Customer information
|
|
||||||
data.setCustomerName("Musterkunde GmbH");
|
|
||||||
data.setCustomerStreet("Kundenstraße");
|
|
||||||
data.setCustomerHouseNumber("456");
|
|
||||||
data.setCustomerZip("54321");
|
|
||||||
data.setCustomerCity("Hamburg");
|
|
||||||
data.setCustomerCountry("Deutschland");
|
|
||||||
|
|
||||||
// Invoice items
|
|
||||||
data.setItems(Arrays.asList(
|
|
||||||
new InvoiceData.InvoiceItem(
|
|
||||||
"Transport Hamburg - Berlin",
|
|
||||||
new BigDecimal("1"),
|
|
||||||
"Stück",
|
|
||||||
new BigDecimal("250.00"),
|
|
||||||
new BigDecimal("19")
|
|
||||||
),
|
|
||||||
new InvoiceData.InvoiceItem(
|
|
||||||
"Zusätzliche Wartezeit",
|
|
||||||
new BigDecimal("2.5"),
|
|
||||||
"Stunden",
|
|
||||||
new BigDecimal("45.00"),
|
|
||||||
new BigDecimal("19")
|
|
||||||
),
|
|
||||||
new InvoiceData.InvoiceItem(
|
|
||||||
"Verpackungsmaterial",
|
|
||||||
new BigDecimal("5"),
|
|
||||||
"Stück",
|
|
||||||
new BigDecimal("12.50"),
|
|
||||||
new BigDecimal("19")
|
|
||||||
),
|
|
||||||
new InvoiceData.InvoiceItem(
|
|
||||||
"Expressaufschlag",
|
|
||||||
new BigDecimal("1"),
|
|
||||||
"Stück",
|
|
||||||
new BigDecimal("75.00"),
|
|
||||||
new BigDecimal("19")
|
|
||||||
)
|
|
||||||
));
|
|
||||||
|
|
||||||
// VAT rate
|
|
||||||
data.setVatRate(new BigDecimal("19.0"));
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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