From 0d7223b6b66765643c2a75cc9ef1116252c42edd Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Tue, 24 Feb 2026 14:29:14 +0100 Subject: [PATCH] Erweiterungen --- .../java/de/assecutor/votianlt/model/Job.java | 4 + .../votianlt/model/UserInvoiceData.java | 2 + .../model/invoices/CustomerInvoice.java | 20 ++++ .../pages/service/UserInvoiceDataService.java | 31 ++++- .../pages/view/CreateInvoiceView.java | 106 +++++++++++------- .../repository/CustomerInvoiceRepository.java | 16 +++ .../service/CustomerInvoiceService.java | 8 ++ 7 files changed, 145 insertions(+), 42 deletions(-) create mode 100644 src/main/java/de/assecutor/votianlt/repository/CustomerInvoiceRepository.java diff --git a/src/main/java/de/assecutor/votianlt/model/Job.java b/src/main/java/de/assecutor/votianlt/model/Job.java index 1f2866a..5165547 100644 --- a/src/main/java/de/assecutor/votianlt/model/Job.java +++ b/src/main/java/de/assecutor/votianlt/model/Job.java @@ -154,6 +154,10 @@ public class Job { @Field("route_duration_seconds") private Integer routeDurationSeconds; + // Referenz auf die erstellte Rechnung + @Field("invoice_id") + private String invoiceId; + /** * Returns the ObjectId as string for JSON serialization. This ensures that the * job id is returned as a string when jobs are retrieved via API. diff --git a/src/main/java/de/assecutor/votianlt/model/UserInvoiceData.java b/src/main/java/de/assecutor/votianlt/model/UserInvoiceData.java index 398e7a3..8bc4730 100644 --- a/src/main/java/de/assecutor/votianlt/model/UserInvoiceData.java +++ b/src/main/java/de/assecutor/votianlt/model/UserInvoiceData.java @@ -28,6 +28,8 @@ public class UserInvoiceData { private String introText; private String paymentTerms; + private long nextInvoiceNumber = 0; + private LocalDateTime createdAt; private LocalDateTime updatedAt; diff --git a/src/main/java/de/assecutor/votianlt/model/invoices/CustomerInvoice.java b/src/main/java/de/assecutor/votianlt/model/invoices/CustomerInvoice.java index 1ff902e..8595809 100644 --- a/src/main/java/de/assecutor/votianlt/model/invoices/CustomerInvoice.java +++ b/src/main/java/de/assecutor/votianlt/model/invoices/CustomerInvoice.java @@ -59,6 +59,10 @@ public class CustomerInvoice { private String legalNotes; // Rechtliche Hinweise private String reverseChargeNote; // Hinweis auf Reverse Charge (falls zutreffend) + // Verknüpfung mit Auftrag und Benutzer + private String jobId; // Referenz auf den Auftrag + private String userId; // Referenz auf den Benutzer (Rechnungsersteller) + // Constructors public CustomerInvoice() { } @@ -341,4 +345,20 @@ public class CustomerInvoice { public void setReverseChargeNote(String reverseChargeNote) { this.reverseChargeNote = reverseChargeNote; } + + public String getJobId() { + return jobId; + } + + public void setJobId(String jobId) { + this.jobId = jobId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/service/UserInvoiceDataService.java b/src/main/java/de/assecutor/votianlt/pages/service/UserInvoiceDataService.java index a979e3e..f271fca 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/UserInvoiceDataService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/UserInvoiceDataService.java @@ -3,6 +3,11 @@ package de.assecutor.votianlt.pages.service; import de.assecutor.votianlt.model.UserInvoiceData; import de.assecutor.votianlt.repository.UserInvoiceDataRepository; import org.bson.types.ObjectId; +import org.springframework.data.mongodb.core.FindAndModifyOptions; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Service; import java.util.Optional; @@ -11,9 +16,11 @@ import java.util.Optional; public class UserInvoiceDataService { private final UserInvoiceDataRepository userInvoiceDataRepository; + private final MongoTemplate mongoTemplate; - public UserInvoiceDataService(UserInvoiceDataRepository userInvoiceDataRepository) { + public UserInvoiceDataService(UserInvoiceDataRepository userInvoiceDataRepository, MongoTemplate mongoTemplate) { this.userInvoiceDataRepository = userInvoiceDataRepository; + this.mongoTemplate = mongoTemplate; } public Optional findByUserId(ObjectId userId) { @@ -53,4 +60,26 @@ public class UserInvoiceDataService { public void deleteByUserId(ObjectId userId) { userInvoiceDataRepository.deleteByUserId(userId); } + + /** + * Generiert atomar die nächste Rechnungsnummer für den Benutzer und erhöht den + * Zähler um 1. Gibt die vollständige Rechnungsnummer zurück (Präfix + Nummer). + */ + public String generateNextInvoiceNumber(ObjectId userId) { + Query query = Query.query(Criteria.where("userId").is(userId)); + Update update = new Update().inc("nextInvoiceNumber", 1); + FindAndModifyOptions options = FindAndModifyOptions.options().returnNew(false).upsert(false); + + UserInvoiceData before = mongoTemplate.findAndModify(query, update, options, UserInvoiceData.class); + if (before == null) { + // Kein Eintrag vorhanden - Fallback auf aktuelle Daten + return findByUserId(userId).map(d -> { + String prefix = d.getPrefix() != null ? d.getPrefix() : ""; + return prefix + String.format("%06d", d.getNextInvoiceNumber()); + }).orElse("000000"); + } + + String prefix = before.getPrefix() != null ? before.getPrefix() : ""; + return prefix + String.format("%06d", before.getNextInvoiceNumber()); + } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java index 0f27812..0087c41 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -16,10 +16,15 @@ import com.vaadin.flow.router.HasDynamicTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.BeforeEvent; import com.vaadin.flow.router.HasUrlParameter; +import de.assecutor.votianlt.model.Customer; import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.Service; import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.model.InvoiceTemplate; +import de.assecutor.votianlt.model.invoices.CustomerInvoice; +import de.assecutor.votianlt.pages.service.CustomerService; +import de.assecutor.votianlt.pages.service.UserInvoiceDataService; +import de.assecutor.votianlt.repository.CustomerInvoiceRepository; import de.assecutor.votianlt.repository.JobRepository; import de.assecutor.votianlt.repository.ServiceRepository; import de.assecutor.votianlt.repository.UserRepository; @@ -55,6 +60,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter private final CustomerInvoiceService customerInvoiceService; private final InvoiceTemplateService invoiceTemplateService; private final SecurityService securityService; + private final UserInvoiceDataService userInvoiceDataService; + private final CustomerInvoiceRepository customerInvoiceRepository; + private final CustomerService customerService; private User currentUser; private Job currentJob; private List gridRows = new ArrayList<>(); @@ -91,13 +99,18 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter @Autowired public CreateInvoiceView(JobRepository jobRepository, ServiceRepository serviceRepository, UserRepository userRepository, CustomerInvoiceService customerInvoiceService, - InvoiceTemplateService invoiceTemplateService, SecurityService securityService) { + InvoiceTemplateService invoiceTemplateService, SecurityService securityService, + UserInvoiceDataService userInvoiceDataService, CustomerInvoiceRepository customerInvoiceRepository, + CustomerService customerService) { this.jobRepository = jobRepository; this.serviceRepository = serviceRepository; this.userRepository = userRepository; this.customerInvoiceService = customerInvoiceService; this.invoiceTemplateService = invoiceTemplateService; this.securityService = securityService; + this.userInvoiceDataService = userInvoiceDataService; + this.customerInvoiceRepository = customerInvoiceRepository; + this.customerService = customerService; setSizeFull(); setPadding(true); setSpacing(true); @@ -405,9 +418,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter return; } - // Save the updated job with kilometers and time - jobRepository.save(currentJob); - try { // Get current user Optional currentUserOpt = securityService.getAuthenticatedUser() @@ -431,25 +441,41 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter } String templateData = templateOpt.get().getTemplateData(); - System.out.println("DEBUG CreateInvoiceView: Template data length: " - + (templateData != null ? templateData.length() : 0)); - System.out.println("DEBUG CreateInvoiceView: Template data preview: " - + (templateData != null ? templateData.substring(0, Math.min(200, templateData.length())) - : "null")); - if (templateData == null || templateData.isBlank()) { Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000, Notification.Position.BOTTOM_END); return; } - // Generate PDF with template and actual job data - byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser); - System.out.println( - "DEBUG CreateInvoiceView: PDF bytes generated: " + (pdfBytes != null ? pdfBytes.length : 0)); + // Rechnungsnummer generieren (atomar, Präfix + Zähler) + String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(currentUser.getId()); - // Show PDF in dialog - showPdfInDialog(pdfBytes, "Rechnung " + currentJob.getJobNumber()); + // Rechnung in MongoDB speichern + BigDecimal netAmount = calculateNetAmount(); + BigDecimal vatRate = calculateAverageVatRate(); + BigDecimal vatAmount = netAmount.multiply(vatRate); + BigDecimal totalAmount = netAmount.add(vatAmount); + + CustomerInvoice invoice = new CustomerInvoice(); + invoice.setInvoiceNumber(invoiceNumber); + invoice.setInvoiceDate(java.time.LocalDate.now()); + invoice.setJobId(currentJob.getId().toHexString()); + invoice.setUserId(currentUser.getId().toHexString()); + invoice.setNetAmount(netAmount); + invoice.setVatRate(vatRate); + invoice.setVatAmount(vatAmount); + invoice.setTotalAmount(totalAmount); + CustomerInvoice savedInvoice = customerInvoiceRepository.save(invoice); + + // Job mit Rechnungs-ID verknüpfen und speichern + currentJob.setInvoiceId(savedInvoice.getId()); + jobRepository.save(currentJob); + + // PDF mit Rechnungsnummer generieren + byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser, invoiceNumber); + + // PDF im Dialog anzeigen + showPdfInDialog(pdfBytes, "Rechnung " + invoiceNumber); } catch (Exception ex) { log.error("Fehler beim Erstellen der Rechnung", ex); @@ -461,7 +487,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter /** * Generates an invoice PDF from the template with actual job data. */ - private byte[] generateInvoicePdfFromTemplate(String templateData, User currentUser) throws Exception { + private byte[] generateInvoicePdfFromTemplate(String templateData, User currentUser, String invoiceNumber) + throws Exception { // Calculate totals BigDecimal netAmount = calculateNetAmount(); BigDecimal vatRate = calculateAverageVatRate(); @@ -479,34 +506,29 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter variables.put("masterdata.email", safe(currentUser.getEmail())); variables.put("masterdata.phone", safe(currentUser.getPhone())); - // Customer data (from job) + // Customer data (from job - look up Customer entity for full address) String customerSelection = currentJob.getCustomerSelection(); if (customerSelection != null && !customerSelection.isBlank()) { - // Format: "Firmenname | Vorname Nachname, Straße Hausnummer, PLZ Ort" + // Format: "Firmenname | Vorname Nachname" String[] parts = customerSelection.split("\\|"); String companyName = parts.length > 0 ? parts[0].trim() : ""; - variables.put("customer.company_name", companyName); + String contactName = parts.length > 1 ? parts[1].trim() : ""; - // Extract other customer info if available - if (parts.length > 1) { - String remaining = parts[1].trim(); - // Try to extract contact name (before first comma) - int firstComma = remaining.indexOf(","); - if (firstComma > 0) { - variables.put("customer.contact_name", remaining.substring(0, firstComma).trim()); - // Rest is address - String addressPart = remaining.substring(firstComma + 1).trim(); - // Split address into street and city - String[] addressParts = addressPart.split(","); - if (addressParts.length >= 1) { - variables.put("customer.street", addressParts[0].trim()); - } - if (addressParts.length >= 2) { - variables.put("customer.city", addressParts[1].trim()); - } - } else { - variables.put("customer.contact_name", remaining); - } + variables.put("customer.company_name", companyName); + variables.put("customer.contact_name", contactName); + + // Look up full address from Customer entity + Customer matchedCustomer = customerService.findAllForCurrentOwner().stream() + .filter(c -> companyName.equalsIgnoreCase(c.getCompanyName())).findFirst().orElse(null); + + if (matchedCustomer != null) { + String street = safe(matchedCustomer.getStreet()) + " " + safe(matchedCustomer.getHouseNumber()); + String city = safe(matchedCustomer.getZip()) + " " + safe(matchedCustomer.getCity()); + variables.put("customer.street", street.trim()); + variables.put("customer.city", city.trim()); + } else { + variables.put("customer.street", ""); + variables.put("customer.city", ""); } } else { variables.put("customer.company_name", ""); @@ -516,7 +538,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter } // Invoice data - variables.put("invoice.number", currentJob.getJobNumber() + "-" + System.currentTimeMillis()); + variables.put("invoice.number", invoiceNumber); + variables.put("invoice_number", invoiceNumber); + variables.put("masterdata.invoice_number", invoiceNumber); variables.put("invoice.date", java.time.LocalDate.now().toString()); variables.put("invoice.net_total", netAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"); diff --git a/src/main/java/de/assecutor/votianlt/repository/CustomerInvoiceRepository.java b/src/main/java/de/assecutor/votianlt/repository/CustomerInvoiceRepository.java new file mode 100644 index 0000000..7f90e79 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/repository/CustomerInvoiceRepository.java @@ -0,0 +1,16 @@ +package de.assecutor.votianlt.repository; + +import de.assecutor.votianlt.model.invoices.CustomerInvoice; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface CustomerInvoiceRepository extends MongoRepository { + + Optional findByJobId(String jobId); + + List findByUserId(String userId); +} diff --git a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java index 466153b..8387f9a 100644 --- a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java +++ b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java @@ -705,6 +705,14 @@ public class CustomerInvoiceService { text = "[" + variable.replace("masterdata.", "").replace("customer.", "").replace("invoice.", "") .replace("job.", "") + "]"; System.out.println("DEBUG: No value for variable " + variable + ", using placeholder"); + } else if ("customer".equals(type)) { + // Customer-type element without variable: compose address from customer variables + String line1 = variables.getOrDefault("customer.company_name", + variables.getOrDefault("customer.contact_name", "")); + String line2 = variables.getOrDefault("customer.street", ""); + String line3 = variables.getOrDefault("customer.city", ""); + text = line1 + "\n" + line2 + "\n" + line3; + System.out.println("DEBUG: Customer element - composed address: " + text); } else { System.out.println("DEBUG: Using static text: " + text); }