Erweiterungen
This commit is contained in:
@@ -9,17 +9,18 @@ import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import de.assecutor.votianlt.model.invoices.SystemInvoice;
|
||||
import de.assecutor.votianlt.model.invoices.CustomerInvoiceData;
|
||||
import de.assecutor.votianlt.model.invoices.CustomerInvoiceItem;
|
||||
import de.assecutor.votianlt.security.SecurityService;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||
import de.assecutor.votianlt.model.invoices.SystemInvoiceData;
|
||||
import de.assecutor.votianlt.model.invoices.SystemInvoiceItem;
|
||||
import de.assecutor.votianlt.service.SystemInvoiceService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import com.vaadin.flow.server.StreamRegistration;
|
||||
@@ -31,12 +32,10 @@ public class InvoicesView extends VerticalLayout {
|
||||
|
||||
private final Grid<SystemInvoice> invoiceGrid;
|
||||
|
||||
private final CustomerInvoiceService customerInvoiceService;
|
||||
private final SecurityService securityService;
|
||||
private final SystemInvoiceService systemInvoiceService;
|
||||
|
||||
public InvoicesView(CustomerInvoiceService customerInvoiceService, SecurityService securityService) {
|
||||
this.customerInvoiceService = customerInvoiceService;
|
||||
this.securityService = securityService;
|
||||
public InvoicesView(SystemInvoiceService systemInvoiceService) {
|
||||
this.systemInvoiceService = systemInvoiceService;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
@@ -77,8 +76,8 @@ public class InvoicesView extends VerticalLayout {
|
||||
|
||||
private void downloadInvoicePdf(SystemInvoice systemInvoice) {
|
||||
try {
|
||||
// PDF generieren mit CustomerInvoice (HTML Template)
|
||||
byte[] pdfBytes = generateCustomerInvoicePdf(systemInvoice);
|
||||
// PDF generieren mit SystemInvoice (HTML Template)
|
||||
byte[] pdfBytes = generateSystemInvoicePdf(systemInvoice);
|
||||
StreamResource resource = new StreamResource(systemInvoice.getId() + ".pdf",
|
||||
() -> new ByteArrayInputStream(pdfBytes));
|
||||
resource.setContentType("application/pdf");
|
||||
@@ -94,72 +93,35 @@ public class InvoicesView extends VerticalLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] generateCustomerInvoicePdf(SystemInvoice systemInvoice) throws Exception {
|
||||
// Aktuellen Benutzer als Rechnungssteller ermitteln
|
||||
de.assecutor.votianlt.model.User user = securityService.getCurrentDatabaseUser();
|
||||
private byte[] generateSystemInvoicePdf(SystemInvoice systemInvoice) throws Exception {
|
||||
DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||
NumberFormat CURRENCY_FMT = NumberFormat.getCurrencyInstance(Locale.GERMANY);
|
||||
|
||||
CustomerInvoiceData data = new CustomerInvoiceData();
|
||||
// Kopf
|
||||
SystemInvoiceData data = new SystemInvoiceData();
|
||||
data.setInvoiceNumber(systemInvoice.getId());
|
||||
data.setInvoiceDate(systemInvoice.getDatum());
|
||||
data.setDeliveryDate(systemInvoice.getDatum());
|
||||
data.setDescription(systemInvoice.getBeschreibung());
|
||||
data.setInvoiceDate(DATE_FMT.format(systemInvoice.getDatum()));
|
||||
data.setInvoiceText(systemInvoice.getBeschreibung());
|
||||
|
||||
// Rechnungssteller = eingeloggter Benutzer
|
||||
String senderName = (nullToEmpty(user.getFirstname()) + " " + nullToEmpty(user.getName())).trim();
|
||||
if (senderName.isBlank() && user.getEmail() != null)
|
||||
senderName = user.getEmail();
|
||||
data.setSenderName(user.getCompany() != null && !user.getCompany().isBlank() ? user.getCompany() : senderName);
|
||||
data.setSenderAddress((nullToEmpty(user.getStreet()) + " " + nullToEmpty(user.getHouseNumber())).trim());
|
||||
data.setSenderPostcode(nullToEmpty(user.getZip()));
|
||||
data.setSenderCity(nullToEmpty(user.getCity()));
|
||||
data.setSenderCountry("Deutschland");
|
||||
data.setSenderTaxNumber("");
|
||||
data.setSenderVatId("");
|
||||
data.setSenderPhone(nullToEmpty(user.getPhone()));
|
||||
data.setSenderEmail(nullToEmpty(user.getEmail()));
|
||||
data.setSenderWebsite("");
|
||||
|
||||
// Empfänger = Kunde aus der Zeile (nur Name vorhanden in Testdaten)
|
||||
data.setRecipientCompany("");
|
||||
// Empfänger aus der Zeile (nur Name in den Testdaten vorhanden)
|
||||
data.setRecipientName(systemInvoice.getKunde());
|
||||
data.setRecipientAddress("");
|
||||
data.setRecipientPostcode("");
|
||||
data.setRecipientDepartment("");
|
||||
data.setRecipientStreet("");
|
||||
data.setRecipientCity("");
|
||||
data.setRecipientCountry("Deutschland");
|
||||
data.setRecipientVatId("");
|
||||
|
||||
// Positionen (eine einfache Position aus Betrag/Beschreibung)
|
||||
List<CustomerInvoiceItem> items = new ArrayList<>();
|
||||
BigDecimal vatRate = new BigDecimal("0.19");
|
||||
BigDecimal unitPrice = BigDecimal.valueOf(systemInvoice.getBetrag());
|
||||
items.add(new CustomerInvoiceItem(BigDecimal.ONE, "Stk.", systemInvoice.getBeschreibung(), unitPrice, vatRate));
|
||||
data.setItems(items);
|
||||
// Eine Position mit dem Betrag/Beschreibung
|
||||
List<SystemInvoiceItem> items = new ArrayList<>();
|
||||
String netStr = CURRENCY_FMT.format(systemInvoice.getBetrag());
|
||||
items.add(new SystemInvoiceItem("1", systemInvoice.getBeschreibung(), netStr, netStr));
|
||||
data.setInvoiceItems(items);
|
||||
|
||||
// Summen berechnen
|
||||
BigDecimal netAmount = items.stream().map(CustomerInvoiceItem::getNetTotal).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||
data.setNetAmount(netAmount);
|
||||
data.setVatRate(vatRate);
|
||||
data.setVatAmount(vatAmount);
|
||||
data.setTotalAmount(totalAmount);
|
||||
// Summen berechnen (Betrag als Nettobetrag interpretieren)
|
||||
double net = systemInvoice.getBetrag();
|
||||
double vat = Math.round(net * 0.19 * 100.0) / 100.0;
|
||||
double total = net + vat;
|
||||
data.setNetAmount(CURRENCY_FMT.format(net));
|
||||
data.setVatAmount(CURRENCY_FMT.format(vat));
|
||||
data.setTotalAmount(CURRENCY_FMT.format(total));
|
||||
|
||||
// Zahlung
|
||||
data.setPaymentTerms("Zahlbar innerhalb von 14 Tagen netto ohne Abzug.");
|
||||
data.setPaymentDueDate(systemInvoice.getDatum().plusDays(14));
|
||||
data.setBankAccount(data.getSenderName());
|
||||
data.setIban("");
|
||||
data.setBic("");
|
||||
|
||||
// Rechtliches
|
||||
data.setLegalNotes("");
|
||||
data.setReverseChargeNote("");
|
||||
|
||||
return customerInvoiceService.generateCustomerInvoicePdf(data);
|
||||
}
|
||||
|
||||
private String nullToEmpty(String s) {
|
||||
return s == null ? "" : s;
|
||||
return systemInvoiceService.generateInvoicePdfFromHtml(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package de.assecutor.votianlt.pages.view;
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.grid.GridVariant;
|
||||
import com.vaadin.flow.component.grid.ColumnTextAlign;
|
||||
import com.vaadin.flow.component.html.*;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
@@ -10,23 +12,33 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.select.Select;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||
import com.vaadin.flow.data.value.ValueChangeMode;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||
import de.assecutor.votianlt.pages.base.ui.view.MainLayout;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import com.vaadin.flow.server.StreamRegistration;
|
||||
import de.assecutor.votianlt.service.SystemInvoiceService;
|
||||
import de.assecutor.votianlt.model.invoices.SystemInvoiceData;
|
||||
import de.assecutor.votianlt.model.invoices.SystemInvoiceItem;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Meine Rechnungen – nutzerzentrierte Übersicht.
|
||||
*
|
||||
* Layout orientiert am bereitgestellten Screenshot: - Zwei Karten oben (Offene
|
||||
* Rechnungen, Bankverbindung) - Darunter ein Bereich „Rechnungen" mit Grid,
|
||||
* Suche und Seitengröße
|
||||
* Modernisierte Optik: Responsive Karten, Lumo-Theme-Varianten, Status-Badges,
|
||||
* Suche und leere Zustandsanzeige.
|
||||
*/
|
||||
@PageTitle("Meine Rechnungen")
|
||||
@Route(value = "my-invoices", layout = MainLayout.class)
|
||||
@@ -35,11 +47,18 @@ public class MyInvoicesView extends Main {
|
||||
|
||||
private final Grid<MyInvoiceRow> grid = new Grid<>(MyInvoiceRow.class, false);
|
||||
private final List<MyInvoiceRow> allRows = new ArrayList<>(); // zunächst leer
|
||||
private final Div emptyState = new Div();
|
||||
private final SystemInvoiceService systemInvoiceService;
|
||||
|
||||
public MyInvoicesView() {
|
||||
getStyle().set("max-width", "90%");
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||
private static final NumberFormat CURRENCY_FMT = NumberFormat.getCurrencyInstance(Locale.GERMANY);
|
||||
|
||||
public MyInvoicesView(SystemInvoiceService systemInvoiceService) {
|
||||
this.systemInvoiceService = systemInvoiceService;
|
||||
getStyle().set("max-width", "1100px");
|
||||
getStyle().set("margin-left", "auto");
|
||||
getStyle().set("margin-right", "auto");
|
||||
getStyle().set("padding", "var(--lumo-space-m)");
|
||||
|
||||
// Toolbar / Titel
|
||||
add(new ViewToolbar("Meine Rechnungen"));
|
||||
@@ -49,15 +68,18 @@ public class MyInvoicesView extends Main {
|
||||
|
||||
// Rechnungsbereich unten
|
||||
add(createInvoicesSection());
|
||||
|
||||
// Testdaten hinzufügen
|
||||
addTestInvoices();
|
||||
}
|
||||
|
||||
private Component createTopCards() {
|
||||
// Container mit zwei Spalten (responsiv)
|
||||
// Container mit responsiven Spalten
|
||||
Div container = new Div();
|
||||
container.getStyle().set("display", "grid").set("grid-template-columns", "48% 2% 48%");
|
||||
// .set("gap", "10px");
|
||||
// Spaltenabstände: 2% zwischen den beiden Spalten
|
||||
container.getStyle().set("column-gap", "0");
|
||||
container.getStyle()
|
||||
.set("display", "grid")
|
||||
.set("grid-template-columns", "repeat(auto-fit, minmax(280px, 1fr))")
|
||||
.set("gap", "var(--lumo-space-m)");
|
||||
|
||||
// Karte: Offene Rechnungen
|
||||
Paragraph hint = new Paragraph("Momentan sind keine neuen Rechnungen für Sie im System gespeichert.");
|
||||
@@ -80,7 +102,7 @@ public class MyInvoicesView extends Main {
|
||||
private Component createInvoicesSection() {
|
||||
Div card = new Div();
|
||||
// Abstand zur oberen Zeile
|
||||
card.getStyle().set("margin-top", "30px");
|
||||
card.getStyle().set("margin-top", "var(--lumo-space-l)");
|
||||
|
||||
styleCard(card);
|
||||
|
||||
@@ -93,12 +115,14 @@ public class MyInvoicesView extends Main {
|
||||
pageSize.setItems(10, 25, 50);
|
||||
pageSize.setLabel("Einträge anzeigen");
|
||||
pageSize.setValue(10);
|
||||
pageSize.setWidth("140px");
|
||||
pageSize.setWidth("160px");
|
||||
|
||||
TextField search = new TextField();
|
||||
search.setLabel("Suchen");
|
||||
search.setPlaceholder("Rechnungsnr., Datum, Betrag...");
|
||||
search.setClearButtonVisible(true);
|
||||
search.setValueChangeMode(ValueChangeMode.EAGER);
|
||||
search.setWidth("300px");
|
||||
|
||||
// Layout Kopf + Controls
|
||||
HorizontalLayout header = new HorizontalLayout();
|
||||
@@ -110,14 +134,36 @@ public class MyInvoicesView extends Main {
|
||||
HorizontalLayout controls = new HorizontalLayout(pageSize, search);
|
||||
controls.setWidthFull();
|
||||
controls.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
|
||||
controls.setAlignItems(FlexComponent.Alignment.END);
|
||||
|
||||
// Grid konfigurieren
|
||||
grid.addColumn(MyInvoiceRow::status).setHeader("Status").setAutoWidth(true);
|
||||
grid.addColumn(MyInvoiceRow::invoiceNumber).setHeader("Rechnungsnummer").setAutoWidth(true);
|
||||
grid.addColumn(MyInvoiceRow::date).setHeader("Datum").setAutoWidth(true);
|
||||
grid.addColumn(MyInvoiceRow::amount).setHeader("Betrag").setAutoWidth(true);
|
||||
grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES, GridVariant.LUMO_COMPACT,
|
||||
GridVariant.LUMO_WRAP_CELL_CONTENT, GridVariant.LUMO_COLUMN_BORDERS);
|
||||
grid.setWidthFull();
|
||||
grid.addColumn(new ComponentRenderer<>(row -> statusBadge(row.status()))).setHeader("Status").setAutoWidth(true)
|
||||
.setFlexGrow(0);
|
||||
grid.addColumn(MyInvoicesView::formatInvoiceNumber).setHeader("Rechnungsnummer").setAutoWidth(true);
|
||||
grid.addColumn(row -> DATE_FMT.format(row.date())).setHeader("Datum").setAutoWidth(true).setFlexGrow(0);
|
||||
grid.addColumn(row -> CURRENCY_FMT.format(row.amount())).setHeader("Betrag").setAutoWidth(true)
|
||||
.setTextAlign(ColumnTextAlign.END).setFlexGrow(0);
|
||||
grid.setAllRowsVisible(true);
|
||||
grid.setItems(allRows); // zunächst leer
|
||||
grid.addItemClickListener(event -> {
|
||||
MyInvoiceRow row = event.getItem();
|
||||
if (row != null) {
|
||||
downloadInvoicePdf(row);
|
||||
}
|
||||
});
|
||||
|
||||
// Leerer Zustand
|
||||
emptyState.removeAll();
|
||||
H4 emptyTitle = new H4("Keine Rechnungen vorhanden");
|
||||
Paragraph emptyDesc = new Paragraph("Sobald Rechnungen vorliegen, erscheinen sie hier.");
|
||||
emptyState.add(emptyTitle, emptyDesc);
|
||||
emptyState.getStyle().set("text-align", "center").set("color", "var(--lumo-secondary-text-color)")
|
||||
.set("padding", "var(--lumo-space-m)").set("border", "1px dashed var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)");
|
||||
updateEmptyStateVisibility(allRows);
|
||||
|
||||
// Suche (einfacher Text-Filter über alle sichtbaren Felder)
|
||||
search.addValueChangeListener(e -> applyFilter(e.getValue()));
|
||||
@@ -132,17 +178,51 @@ public class MyInvoicesView extends Main {
|
||||
pager.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
|
||||
|
||||
// Zusammenbauen
|
||||
card.add(header, controls, grid, pager);
|
||||
card.add(header, controls, grid, emptyState, pager);
|
||||
return card;
|
||||
}
|
||||
|
||||
private void applyFilter(String filter) {
|
||||
String f = filter == null ? "" : filter.toLowerCase();
|
||||
grid.setItems(allRows.stream()
|
||||
.filter(row -> row.status.toLowerCase().contains(f) || row.invoiceNumber.toLowerCase().contains(f)
|
||||
|| row.date.toString().toLowerCase().contains(f)
|
||||
|| String.valueOf(row.amount).toLowerCase().contains(f))
|
||||
.toList());
|
||||
List<MyInvoiceRow> filtered = allRows.stream()
|
||||
.filter(row -> row.status().toLowerCase().contains(f) || row.invoiceNumber().toLowerCase().contains(f)
|
||||
|| row.date().toString().toLowerCase().contains(f)
|
||||
|| String.valueOf(row.amount()).toLowerCase().contains(f))
|
||||
.toList();
|
||||
grid.setItems(filtered);
|
||||
updateEmptyStateVisibility(filtered);
|
||||
}
|
||||
|
||||
private void updateEmptyStateVisibility(List<MyInvoiceRow> current) {
|
||||
emptyState.setVisible(current == null || current.isEmpty());
|
||||
}
|
||||
|
||||
private static String formatInvoiceNumber(MyInvoiceRow row) {
|
||||
return row.invoiceNumber();
|
||||
}
|
||||
|
||||
private Span statusBadge(String status) {
|
||||
Span badge = new Span(status);
|
||||
badge.getElement().getThemeList().add("badge pill contrast");
|
||||
String s = status == null ? "" : status.toLowerCase(Locale.ROOT);
|
||||
if (s.contains("bezahlt") || s.contains("paid")) {
|
||||
badge.getElement().getThemeList().add("success");
|
||||
} else if (s.contains("fällig") || s.contains("überfällig") || s.contains("overdue")) {
|
||||
badge.getElement().getThemeList().add("error");
|
||||
} else if (s.contains("offen") || s.contains("open")) {
|
||||
badge.getElement().getThemeList().add("warning");
|
||||
}
|
||||
return badge;
|
||||
}
|
||||
|
||||
private void addTestInvoices() {
|
||||
// Drei Test-Rechnungen hinzufügen
|
||||
allRows.clear();
|
||||
allRows.add(new MyInvoiceRow("Offen", "MI-2025-001", LocalDate.now().minusDays(10), 199.99));
|
||||
allRows.add(new MyInvoiceRow("Bezahlt", "MI-2025-002", LocalDate.now().minusDays(5), 299.49));
|
||||
allRows.add(new MyInvoiceRow("Überfällig", "MI-2025-003", LocalDate.now().minusDays(20), 149.00));
|
||||
grid.setItems(allRows);
|
||||
updateEmptyStateVisibility(allRows);
|
||||
}
|
||||
|
||||
private Div createCard(String title, Component content) {
|
||||
@@ -176,4 +256,48 @@ public class MyInvoicesView extends Main {
|
||||
// Schlanke lokale Repräsentation für das Grid
|
||||
public record MyInvoiceRow(String status, String invoiceNumber, LocalDate date, double amount) {
|
||||
}
|
||||
|
||||
private void downloadInvoicePdf(MyInvoiceRow row) {
|
||||
try {
|
||||
byte[] pdfBytes = generateSystemInvoicePdf(row);
|
||||
StreamResource resource = new StreamResource(row.invoiceNumber() + ".pdf",
|
||||
() -> new ByteArrayInputStream(pdfBytes));
|
||||
resource.setContentType("application/pdf");
|
||||
resource.setCacheTime(0);
|
||||
|
||||
StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry()
|
||||
.registerResource(resource);
|
||||
UI.getCurrent().getPage().open(registration.getResourceUri().toString());
|
||||
} catch (Exception e) {
|
||||
// Optional: could log or show a simple notification
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] generateSystemInvoicePdf(MyInvoiceRow row) throws Exception {
|
||||
SystemInvoiceData data = new SystemInvoiceData();
|
||||
data.setInvoiceNumber(row.invoiceNumber());
|
||||
data.setInvoiceDate(DATE_FMT.format(row.date()));
|
||||
data.setInvoiceText("Rechnung " + row.invoiceNumber());
|
||||
|
||||
// Minimal recipient information
|
||||
data.setRecipientName("Kunde");
|
||||
data.setRecipientDepartment("");
|
||||
data.setRecipientStreet("");
|
||||
data.setRecipientCity("");
|
||||
|
||||
// One simple item based on row
|
||||
List<SystemInvoiceItem> items = new ArrayList<>();
|
||||
String netStr = CURRENCY_FMT.format(row.amount());
|
||||
items.add(new SystemInvoiceItem("1", "Position: " + row.invoiceNumber(), netStr, netStr));
|
||||
data.setInvoiceItems(items);
|
||||
|
||||
double net = row.amount();
|
||||
double vat = Math.round(net * 0.19 * 100.0) / 100.0;
|
||||
double total = net + vat;
|
||||
data.setNetAmount(CURRENCY_FMT.format(net));
|
||||
data.setVatAmount(CURRENCY_FMT.format(vat));
|
||||
data.setTotalAmount(CURRENCY_FMT.format(total));
|
||||
|
||||
return systemInvoiceService.generateInvoicePdfFromHtml(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,15 @@ public class SystemInvoiceService {
|
||||
return generatePdfFromHtmlString(filledHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a SystemInvoice PDF using provided data and the HTML template.
|
||||
*/
|
||||
public byte[] generateInvoicePdfFromHtml(SystemInvoiceData data) throws Exception {
|
||||
String htmlContent = readHtmlTemplate();
|
||||
String filledHtml = fillHtmlWithInvoiceData(htmlContent, data);
|
||||
return generatePdfFromHtmlString(filledHtml);
|
||||
}
|
||||
|
||||
public SystemInvoiceData createSampleInvoiceData() {
|
||||
SystemInvoiceData data = new SystemInvoiceData();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user