Erweiterungen
This commit is contained in:
@@ -63,6 +63,9 @@ public class CustomerInvoice {
|
|||||||
private String jobId; // Referenz auf den Auftrag
|
private String jobId; // Referenz auf den Auftrag
|
||||||
private String userId; // Referenz auf den Benutzer (Rechnungsersteller)
|
private String userId; // Referenz auf den Benutzer (Rechnungsersteller)
|
||||||
|
|
||||||
|
// Gespeicherte PDF-Daten (Base64-kodiert)
|
||||||
|
private byte[] pdfData;
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
public CustomerInvoice() {
|
public CustomerInvoice() {
|
||||||
}
|
}
|
||||||
@@ -361,4 +364,12 @@ public class CustomerInvoice {
|
|||||||
public void setUserId(String userId) {
|
public void setUserId(String userId) {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getPdfData() {
|
||||||
|
return pdfData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPdfData(byte[] pdfData) {
|
||||||
|
this.pdfData = pdfData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
|
||||||
import com.vaadin.flow.component.dialog.Dialog;
|
import com.vaadin.flow.component.dialog.Dialog;
|
||||||
import com.vaadin.flow.component.html.IFrame;
|
import com.vaadin.flow.component.html.IFrame;
|
||||||
import com.vaadin.flow.server.StreamResource;
|
import com.vaadin.flow.server.StreamResource;
|
||||||
@@ -435,7 +436,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get current user
|
|
||||||
Optional<User> currentUserOpt = securityService.getAuthenticatedUser()
|
Optional<User> currentUserOpt = securityService.getAuthenticatedUser()
|
||||||
.flatMap(auth -> userRepository.findByEmail(auth.getUsername()));
|
.flatMap(auth -> userRepository.findByEmail(auth.getUsername()));
|
||||||
|
|
||||||
@@ -447,7 +447,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
|
|
||||||
currentUser = currentUserOpt.get();
|
currentUser = currentUserOpt.get();
|
||||||
|
|
||||||
// Load invoice template from service
|
|
||||||
Optional<InvoiceTemplate> templateOpt = invoiceTemplateService
|
Optional<InvoiceTemplate> templateOpt = invoiceTemplateService
|
||||||
.getTemplateByUserId(currentUser.getId().toString());
|
.getTemplateByUserId(currentUser.getId().toString());
|
||||||
if (templateOpt.isEmpty()) {
|
if (templateOpt.isEmpty()) {
|
||||||
@@ -463,38 +462,51 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rechnungsnummer generieren (atomar, Präfix + Zähler)
|
// Vorschau-PDF mit Platzhalter-Nummer generieren (noch kein Speichern)
|
||||||
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(currentUser.getId());
|
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser,
|
||||||
|
getTranslation("createinvoice.preview.number"));
|
||||||
|
|
||||||
|
showPdfPreviewDialog(pdfBytes, templateData, currentUser);
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Fehler beim Erstellen der Rechnungsvorschau", ex);
|
||||||
|
Notification.show(getTranslation("createinvoice.notification.error", ex.getMessage()), 5000,
|
||||||
|
Notification.Position.BOTTOM_END);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveInvoice(String templateData, User user, Dialog previewDialog) {
|
||||||
|
try {
|
||||||
|
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
|
||||||
|
|
||||||
// Rechnung in MongoDB speichern
|
|
||||||
BigDecimal netAmount = calculateNetAmount();
|
BigDecimal netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = calculateAverageVatRate();
|
BigDecimal vatRate = calculateAverageVatRate();
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
|
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, user, invoiceNumber);
|
||||||
|
|
||||||
CustomerInvoice invoice = new CustomerInvoice();
|
CustomerInvoice invoice = new CustomerInvoice();
|
||||||
invoice.setInvoiceNumber(invoiceNumber);
|
invoice.setInvoiceNumber(invoiceNumber);
|
||||||
invoice.setInvoiceDate(java.time.LocalDate.now());
|
invoice.setInvoiceDate(java.time.LocalDate.now());
|
||||||
invoice.setJobId(currentJob.getId().toHexString());
|
invoice.setJobId(currentJob.getId().toHexString());
|
||||||
invoice.setUserId(currentUser.getId().toHexString());
|
invoice.setUserId(user.getId().toHexString());
|
||||||
invoice.setNetAmount(netAmount);
|
invoice.setNetAmount(netAmount);
|
||||||
invoice.setVatRate(vatRate);
|
invoice.setVatRate(vatRate);
|
||||||
invoice.setVatAmount(vatAmount);
|
invoice.setVatAmount(vatAmount);
|
||||||
invoice.setTotalAmount(totalAmount);
|
invoice.setTotalAmount(totalAmount);
|
||||||
|
invoice.setPdfData(pdfBytes);
|
||||||
CustomerInvoice savedInvoice = customerInvoiceRepository.save(invoice);
|
CustomerInvoice savedInvoice = customerInvoiceRepository.save(invoice);
|
||||||
|
|
||||||
// Job mit Rechnungs-ID verknüpfen und speichern
|
|
||||||
currentJob.setInvoiceId(savedInvoice.getId());
|
currentJob.setInvoiceId(savedInvoice.getId());
|
||||||
jobRepository.save(currentJob);
|
jobRepository.save(currentJob);
|
||||||
|
|
||||||
// PDF mit Rechnungsnummer generieren
|
previewDialog.close();
|
||||||
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser, invoiceNumber);
|
Notification.show(getTranslation("createinvoice.notification.saved", invoiceNumber), 4000,
|
||||||
|
Notification.Position.BOTTOM_END);
|
||||||
// PDF im Dialog anzeigen
|
|
||||||
showPdfInDialog(pdfBytes, "Rechnung " + invoiceNumber);
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("Fehler beim Erstellen der Rechnung", ex);
|
log.error("Fehler beim Speichern der Rechnung", ex);
|
||||||
Notification.show(getTranslation("createinvoice.notification.error", ex.getMessage()), 5000,
|
Notification.show(getTranslation("createinvoice.notification.error", ex.getMessage()), 5000,
|
||||||
Notification.Position.BOTTOM_END);
|
Notification.Position.BOTTOM_END);
|
||||||
}
|
}
|
||||||
@@ -617,45 +629,70 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
return value != null ? value : "";
|
return value != null ? value : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showPdfInDialog(byte[] pdfBytes, String title) {
|
private void showPdfPreviewDialog(byte[] pdfBytes, String templateData, User user) {
|
||||||
// Create a stream resource for the PDF
|
String title = getTranslation("createinvoice.preview.title");
|
||||||
StreamResource resource = new StreamResource(title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf",
|
|
||||||
() -> new java.io.ByteArrayInputStream(pdfBytes));
|
|
||||||
resource.setContentType("application/pdf");
|
|
||||||
resource.setCacheTime(0);
|
|
||||||
|
|
||||||
// Store resource in session for access
|
|
||||||
VaadinSession.getCurrent().setAttribute("currentInvoicePdf", resource);
|
|
||||||
|
|
||||||
// Create dialog
|
|
||||||
Dialog pdfDialog = new Dialog();
|
Dialog pdfDialog = new Dialog();
|
||||||
pdfDialog.setHeaderTitle(title);
|
pdfDialog.setHeaderTitle(title);
|
||||||
pdfDialog.setWidth("90vw");
|
pdfDialog.setWidth("90vw");
|
||||||
pdfDialog.setHeight("90vh");
|
pdfDialog.setHeight("90vh");
|
||||||
|
|
||||||
// Create iframe for PDF
|
|
||||||
IFrame pdfFrame = new IFrame();
|
IFrame pdfFrame = new IFrame();
|
||||||
pdfFrame.setWidth("100%");
|
pdfFrame.setWidth("100%");
|
||||||
pdfFrame.setHeight("100%");
|
pdfFrame.setHeight("100%");
|
||||||
|
|
||||||
// Use data URL for PDF display
|
|
||||||
String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes);
|
String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes);
|
||||||
String dataUrl = "data:application/pdf;base64," + base64Pdf;
|
pdfFrame.getElement().setAttribute("src", "data:application/pdf;base64," + base64Pdf);
|
||||||
pdfFrame.getElement().setAttribute("src", dataUrl);
|
|
||||||
pdfFrame.getStyle().set("border", "none");
|
pdfFrame.getStyle().set("border", "none");
|
||||||
|
|
||||||
// Close button
|
|
||||||
Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close());
|
Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close());
|
||||||
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
|
||||||
// Download button
|
Button saveButton = new Button(getTranslation("createinvoice.button.save"), e -> {
|
||||||
Button downloadButton = new Button(getTranslation("button.download"), e -> {
|
ConfirmDialog confirm = new ConfirmDialog();
|
||||||
getElement().executeJs("const link = document.createElement('a');"
|
confirm.setHeader(getTranslation("createinvoice.confirm.save.title"));
|
||||||
+ "link.href = 'data:application/pdf;base64," + base64Pdf + "';" + "link.download = '"
|
confirm.setText(getTranslation("createinvoice.confirm.save.message"));
|
||||||
+ title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';" + "link.click();");
|
confirm.setConfirmText(getTranslation("createinvoice.confirm.save.confirm"));
|
||||||
|
confirm.setConfirmButtonTheme("primary");
|
||||||
|
confirm.setCancelText(getTranslation("button.cancel"));
|
||||||
|
confirm.setCancelable(true);
|
||||||
|
confirm.addConfirmListener(ev -> saveInvoice(templateData, user, pdfDialog));
|
||||||
|
confirm.open();
|
||||||
|
});
|
||||||
|
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
|
||||||
|
|
||||||
|
pdfDialog.add(pdfFrame);
|
||||||
|
pdfDialog.getFooter().add(closeButton, saveButton);
|
||||||
|
pdfDialog.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showSavedInvoiceDialog(byte[] pdfBytes, String invoiceNumber,
|
||||||
|
com.vaadin.flow.component.Component parent) {
|
||||||
|
String title = "Rechnung " + invoiceNumber;
|
||||||
|
Dialog pdfDialog = new Dialog();
|
||||||
|
pdfDialog.setHeaderTitle(title);
|
||||||
|
pdfDialog.setWidth("90vw");
|
||||||
|
pdfDialog.setHeight("90vh");
|
||||||
|
|
||||||
|
IFrame pdfFrame = new IFrame();
|
||||||
|
pdfFrame.setWidth("100%");
|
||||||
|
pdfFrame.setHeight("100%");
|
||||||
|
|
||||||
|
String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes);
|
||||||
|
pdfFrame.getElement().setAttribute("src", "data:application/pdf;base64," + base64Pdf);
|
||||||
|
pdfFrame.getStyle().set("border", "none");
|
||||||
|
|
||||||
|
Button downloadButton = new Button("Herunterladen", e -> {
|
||||||
|
parent.getElement().executeJs("const link = document.createElement('a');"
|
||||||
|
+ "link.href = 'data:application/pdf;base64," + base64Pdf + "';"
|
||||||
|
+ "link.download = '" + title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';"
|
||||||
|
+ "link.click();");
|
||||||
});
|
});
|
||||||
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
|
||||||
|
Button closeButton = new Button("Schließen", e -> pdfDialog.close());
|
||||||
|
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
pdfDialog.add(pdfFrame);
|
pdfDialog.add(pdfFrame);
|
||||||
pdfDialog.getFooter().add(downloadButton, closeButton);
|
pdfDialog.getFooter().add(downloadButton, closeButton);
|
||||||
pdfDialog.open();
|
pdfDialog.open();
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import de.assecutor.votianlt.model.Job;
|
|||||||
import de.assecutor.votianlt.model.JobStatus;
|
import de.assecutor.votianlt.model.JobStatus;
|
||||||
import de.assecutor.votianlt.messaging.MessagingPublisher;
|
import de.assecutor.votianlt.messaging.MessagingPublisher;
|
||||||
import de.assecutor.votianlt.util.DateTimeFormatUtil;
|
import de.assecutor.votianlt.util.DateTimeFormatUtil;
|
||||||
|
import de.assecutor.votianlt.model.invoices.CustomerInvoice;
|
||||||
|
import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
|
||||||
import de.assecutor.votianlt.repository.JobRepository;
|
import de.assecutor.votianlt.repository.JobRepository;
|
||||||
import de.assecutor.votianlt.security.SecurityService;
|
import de.assecutor.votianlt.security.SecurityService;
|
||||||
import de.assecutor.votianlt.service.ClientConnectionService;
|
import de.assecutor.votianlt.service.ClientConnectionService;
|
||||||
@@ -47,17 +49,19 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
private final SecurityService securityService;
|
private final SecurityService securityService;
|
||||||
private final ClientConnectionService clientConnectionService;
|
private final ClientConnectionService clientConnectionService;
|
||||||
private final MessagingPublisher messagingPublisher;
|
private final MessagingPublisher messagingPublisher;
|
||||||
|
private final CustomerInvoiceRepository customerInvoiceRepository;
|
||||||
private final Grid<Job> grid = new Grid<>(Job.class, false);
|
private final Grid<Job> grid = new Grid<>(Job.class, false);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ShowJobsView(JobRepository jobRepository, JobHistoryService jobHistoryService,
|
public ShowJobsView(JobRepository jobRepository, JobHistoryService jobHistoryService,
|
||||||
SecurityService securityService, ClientConnectionService clientConnectionService,
|
SecurityService securityService, ClientConnectionService clientConnectionService,
|
||||||
MessagingPublisher messagingPublisher) {
|
MessagingPublisher messagingPublisher, CustomerInvoiceRepository customerInvoiceRepository) {
|
||||||
this.jobRepository = jobRepository;
|
this.jobRepository = jobRepository;
|
||||||
this.jobHistoryService = jobHistoryService;
|
this.jobHistoryService = jobHistoryService;
|
||||||
this.securityService = securityService;
|
this.securityService = securityService;
|
||||||
this.clientConnectionService = clientConnectionService;
|
this.clientConnectionService = clientConnectionService;
|
||||||
this.messagingPublisher = messagingPublisher;
|
this.messagingPublisher = messagingPublisher;
|
||||||
|
this.customerInvoiceRepository = customerInvoiceRepository;
|
||||||
setSizeFull();
|
setSizeFull();
|
||||||
setPadding(true);
|
setPadding(true);
|
||||||
setSpacing(true);
|
setSpacing(true);
|
||||||
@@ -139,11 +143,28 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
if (job.getStatus() == JobStatus.COMPLETED) {
|
if (job.getStatus() == JobStatus.COMPLETED) {
|
||||||
Button invoiceBtn = new Button(new Icon(VaadinIcon.DOLLAR));
|
Button invoiceBtn = new Button(new Icon(VaadinIcon.DOLLAR));
|
||||||
invoiceBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SUCCESS);
|
invoiceBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SUCCESS);
|
||||||
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.createinvoice"));
|
if (job.getInvoiceId() != null) {
|
||||||
invoiceBtn.addClickListener(e -> {
|
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.showinvoice"));
|
||||||
e.getSource().getElement().getNode(); // prevent row click
|
invoiceBtn.addClickListener(e -> {
|
||||||
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
|
e.getSource().getElement().getNode();
|
||||||
});
|
customerInvoiceRepository.findById(job.getInvoiceId()).ifPresentOrElse(
|
||||||
|
invoice -> {
|
||||||
|
if (invoice.getPdfData() != null) {
|
||||||
|
CreateInvoiceView.showSavedInvoiceDialog(invoice.getPdfData(),
|
||||||
|
invoice.getInvoiceNumber(), this);
|
||||||
|
} else {
|
||||||
|
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() -> getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString())));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.createinvoice"));
|
||||||
|
invoiceBtn.addClickListener(e -> {
|
||||||
|
e.getSource().getElement().getNode();
|
||||||
|
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
|
||||||
|
});
|
||||||
|
}
|
||||||
return invoiceBtn;
|
return invoiceBtn;
|
||||||
}
|
}
|
||||||
return new com.vaadin.flow.component.html.Span();
|
return new com.vaadin.flow.component.html.Span();
|
||||||
|
|||||||
@@ -613,6 +613,7 @@ jobs.historie.manuell=Manuell
|
|||||||
jobs.button.csvexport=CSV Export
|
jobs.button.csvexport=CSV Export
|
||||||
jobs.tooltip.complete=Auftrag abschließen
|
jobs.tooltip.complete=Auftrag abschließen
|
||||||
jobs.tooltip.createinvoice=Rechnung erstellen
|
jobs.tooltip.createinvoice=Rechnung erstellen
|
||||||
|
jobs.tooltip.showinvoice=Rechnung anzeigen
|
||||||
jobs.tooltip.delete=Auftrag löschen
|
jobs.tooltip.delete=Auftrag löschen
|
||||||
jobs.dialog.complete.title=Auftrag abschließen
|
jobs.dialog.complete.title=Auftrag abschließen
|
||||||
jobs.dialog.complete.text=Möchten Sie den Auftrag {0} manuell abschließen?
|
jobs.dialog.complete.text=Möchten Sie den Auftrag {0} manuell abschließen?
|
||||||
@@ -648,6 +649,13 @@ createinvoice.notification.noservices=Bitte wählen Sie mindestens eine Leistung
|
|||||||
createinvoice.notification.nouser=Benutzer nicht gefunden
|
createinvoice.notification.nouser=Benutzer nicht gefunden
|
||||||
createinvoice.notification.notemplate=Kein Rechnungstemplate gefunden
|
createinvoice.notification.notemplate=Kein Rechnungstemplate gefunden
|
||||||
createinvoice.notification.error=Fehler beim Erstellen der Rechnung: {0}
|
createinvoice.notification.error=Fehler beim Erstellen der Rechnung: {0}
|
||||||
|
createinvoice.notification.saved=Rechnung {0} wurde gespeichert
|
||||||
|
createinvoice.preview.title=Rechnungsvorschau
|
||||||
|
createinvoice.preview.number=VORSCHAU
|
||||||
|
createinvoice.button.save=Speichern
|
||||||
|
createinvoice.confirm.save.title=Rechnung speichern
|
||||||
|
createinvoice.confirm.save.message=Diese Rechnung wird dauerhaft gespeichert und kann anschlie\u00dfend nicht mehr ver\u00e4ndert werden. Fortfahren?
|
||||||
|
createinvoice.confirm.save.confirm=Ja, speichern
|
||||||
|
|
||||||
# Invoices
|
# Invoices
|
||||||
invoices.title=Rechnungen
|
invoices.title=Rechnungen
|
||||||
|
|||||||
@@ -613,6 +613,7 @@ jobs.historie.manuell=Manual
|
|||||||
jobs.button.csvexport=CSV Export
|
jobs.button.csvexport=CSV Export
|
||||||
jobs.tooltip.complete=Complete Job
|
jobs.tooltip.complete=Complete Job
|
||||||
jobs.tooltip.createinvoice=Create Invoice
|
jobs.tooltip.createinvoice=Create Invoice
|
||||||
|
jobs.tooltip.showinvoice=Show Invoice
|
||||||
jobs.tooltip.delete=Delete Job
|
jobs.tooltip.delete=Delete Job
|
||||||
jobs.dialog.complete.title=Complete Job
|
jobs.dialog.complete.title=Complete Job
|
||||||
jobs.dialog.complete.text=Do you want to manually complete job {0}?
|
jobs.dialog.complete.text=Do you want to manually complete job {0}?
|
||||||
@@ -648,6 +649,13 @@ createinvoice.notification.noservices=Please select at least one service
|
|||||||
createinvoice.notification.nouser=User not found
|
createinvoice.notification.nouser=User not found
|
||||||
createinvoice.notification.notemplate=No invoice template found
|
createinvoice.notification.notemplate=No invoice template found
|
||||||
createinvoice.notification.error=Error creating invoice: {0}
|
createinvoice.notification.error=Error creating invoice: {0}
|
||||||
|
createinvoice.notification.saved=Invoice {0} has been saved
|
||||||
|
createinvoice.preview.title=Invoice Preview
|
||||||
|
createinvoice.preview.number=PREVIEW
|
||||||
|
createinvoice.button.save=Save
|
||||||
|
createinvoice.confirm.save.title=Save Invoice
|
||||||
|
createinvoice.confirm.save.message=This invoice will be permanently saved and can no longer be modified. Continue?
|
||||||
|
createinvoice.confirm.save.confirm=Yes, save
|
||||||
|
|
||||||
# Invoices
|
# Invoices
|
||||||
invoices.title=Invoices
|
invoices.title=Invoices
|
||||||
|
|||||||
Reference in New Issue
Block a user