Erweiterungen

This commit is contained in:
2026-02-24 14:29:14 +01:00
parent c50d1c92da
commit 0d7223b6b6
7 changed files with 145 additions and 42 deletions

View File

@@ -154,6 +154,10 @@ public class Job {
@Field("route_duration_seconds") @Field("route_duration_seconds")
private Integer routeDurationSeconds; 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 * 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. * job id is returned as a string when jobs are retrieved via API.

View File

@@ -28,6 +28,8 @@ public class UserInvoiceData {
private String introText; private String introText;
private String paymentTerms; private String paymentTerms;
private long nextInvoiceNumber = 0;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;

View File

@@ -59,6 +59,10 @@ public class CustomerInvoice {
private String legalNotes; // Rechtliche Hinweise private String legalNotes; // Rechtliche Hinweise
private String reverseChargeNote; // Hinweis auf Reverse Charge (falls zutreffend) 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 // Constructors
public CustomerInvoice() { public CustomerInvoice() {
} }
@@ -341,4 +345,20 @@ public class CustomerInvoice {
public void setReverseChargeNote(String reverseChargeNote) { public void setReverseChargeNote(String reverseChargeNote) {
this.reverseChargeNote = 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;
}
} }

View File

@@ -3,6 +3,11 @@ package de.assecutor.votianlt.pages.service;
import de.assecutor.votianlt.model.UserInvoiceData; import de.assecutor.votianlt.model.UserInvoiceData;
import de.assecutor.votianlt.repository.UserInvoiceDataRepository; import de.assecutor.votianlt.repository.UserInvoiceDataRepository;
import org.bson.types.ObjectId; 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 org.springframework.stereotype.Service;
import java.util.Optional; import java.util.Optional;
@@ -11,9 +16,11 @@ import java.util.Optional;
public class UserInvoiceDataService { public class UserInvoiceDataService {
private final UserInvoiceDataRepository userInvoiceDataRepository; private final UserInvoiceDataRepository userInvoiceDataRepository;
private final MongoTemplate mongoTemplate;
public UserInvoiceDataService(UserInvoiceDataRepository userInvoiceDataRepository) { public UserInvoiceDataService(UserInvoiceDataRepository userInvoiceDataRepository, MongoTemplate mongoTemplate) {
this.userInvoiceDataRepository = userInvoiceDataRepository; this.userInvoiceDataRepository = userInvoiceDataRepository;
this.mongoTemplate = mongoTemplate;
} }
public Optional<UserInvoiceData> findByUserId(ObjectId userId) { public Optional<UserInvoiceData> findByUserId(ObjectId userId) {
@@ -53,4 +60,26 @@ public class UserInvoiceDataService {
public void deleteByUserId(ObjectId userId) { public void deleteByUserId(ObjectId userId) {
userInvoiceDataRepository.deleteByUserId(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());
}
} }

View File

@@ -16,10 +16,15 @@ import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.BeforeEvent; import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasUrlParameter; import com.vaadin.flow.router.HasUrlParameter;
import de.assecutor.votianlt.model.Customer;
import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.Service; import de.assecutor.votianlt.model.Service;
import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.model.InvoiceTemplate; 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.JobRepository;
import de.assecutor.votianlt.repository.ServiceRepository; import de.assecutor.votianlt.repository.ServiceRepository;
import de.assecutor.votianlt.repository.UserRepository; import de.assecutor.votianlt.repository.UserRepository;
@@ -55,6 +60,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private final CustomerInvoiceService customerInvoiceService; private final CustomerInvoiceService customerInvoiceService;
private final InvoiceTemplateService invoiceTemplateService; private final InvoiceTemplateService invoiceTemplateService;
private final SecurityService securityService; private final SecurityService securityService;
private final UserInvoiceDataService userInvoiceDataService;
private final CustomerInvoiceRepository customerInvoiceRepository;
private final CustomerService customerService;
private User currentUser; private User currentUser;
private Job currentJob; private Job currentJob;
private List<ServiceRow> gridRows = new ArrayList<>(); private List<ServiceRow> gridRows = new ArrayList<>();
@@ -91,13 +99,18 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
@Autowired @Autowired
public CreateInvoiceView(JobRepository jobRepository, ServiceRepository serviceRepository, public CreateInvoiceView(JobRepository jobRepository, ServiceRepository serviceRepository,
UserRepository userRepository, CustomerInvoiceService customerInvoiceService, UserRepository userRepository, CustomerInvoiceService customerInvoiceService,
InvoiceTemplateService invoiceTemplateService, SecurityService securityService) { InvoiceTemplateService invoiceTemplateService, SecurityService securityService,
UserInvoiceDataService userInvoiceDataService, CustomerInvoiceRepository customerInvoiceRepository,
CustomerService customerService) {
this.jobRepository = jobRepository; this.jobRepository = jobRepository;
this.serviceRepository = serviceRepository; this.serviceRepository = serviceRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.customerInvoiceService = customerInvoiceService; this.customerInvoiceService = customerInvoiceService;
this.invoiceTemplateService = invoiceTemplateService; this.invoiceTemplateService = invoiceTemplateService;
this.securityService = securityService; this.securityService = securityService;
this.userInvoiceDataService = userInvoiceDataService;
this.customerInvoiceRepository = customerInvoiceRepository;
this.customerService = customerService;
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
setSpacing(true); setSpacing(true);
@@ -405,9 +418,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return; return;
} }
// Save the updated job with kilometers and time
jobRepository.save(currentJob);
try { try {
// Get current user // Get current user
Optional<User> currentUserOpt = securityService.getAuthenticatedUser() Optional<User> currentUserOpt = securityService.getAuthenticatedUser()
@@ -431,25 +441,41 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
} }
String templateData = templateOpt.get().getTemplateData(); 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()) { if (templateData == null || templateData.isBlank()) {
Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000, Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000,
Notification.Position.BOTTOM_END); Notification.Position.BOTTOM_END);
return; return;
} }
// Generate PDF with template and actual job data // Rechnungsnummer generieren (atomar, Präfix + Zähler)
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser); String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(currentUser.getId());
System.out.println(
"DEBUG CreateInvoiceView: PDF bytes generated: " + (pdfBytes != null ? pdfBytes.length : 0));
// Show PDF in dialog // Rechnung in MongoDB speichern
showPdfInDialog(pdfBytes, "Rechnung " + currentJob.getJobNumber()); 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) { } catch (Exception ex) {
log.error("Fehler beim Erstellen der Rechnung", 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. * 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 // Calculate totals
BigDecimal netAmount = calculateNetAmount(); BigDecimal netAmount = calculateNetAmount();
BigDecimal vatRate = calculateAverageVatRate(); BigDecimal vatRate = calculateAverageVatRate();
@@ -479,34 +506,29 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
variables.put("masterdata.email", safe(currentUser.getEmail())); variables.put("masterdata.email", safe(currentUser.getEmail()));
variables.put("masterdata.phone", safe(currentUser.getPhone())); 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(); String customerSelection = currentJob.getCustomerSelection();
if (customerSelection != null && !customerSelection.isBlank()) { if (customerSelection != null && !customerSelection.isBlank()) {
// Format: "Firmenname | Vorname Nachname, Straße Hausnummer, PLZ Ort" // Format: "Firmenname | Vorname Nachname"
String[] parts = customerSelection.split("\\|"); String[] parts = customerSelection.split("\\|");
String companyName = parts.length > 0 ? parts[0].trim() : ""; 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 variables.put("customer.company_name", companyName);
if (parts.length > 1) { variables.put("customer.contact_name", contactName);
String remaining = parts[1].trim();
// Try to extract contact name (before first comma) // Look up full address from Customer entity
int firstComma = remaining.indexOf(","); Customer matchedCustomer = customerService.findAllForCurrentOwner().stream()
if (firstComma > 0) { .filter(c -> companyName.equalsIgnoreCase(c.getCompanyName())).findFirst().orElse(null);
variables.put("customer.contact_name", remaining.substring(0, firstComma).trim());
// Rest is address if (matchedCustomer != null) {
String addressPart = remaining.substring(firstComma + 1).trim(); String street = safe(matchedCustomer.getStreet()) + " " + safe(matchedCustomer.getHouseNumber());
// Split address into street and city String city = safe(matchedCustomer.getZip()) + " " + safe(matchedCustomer.getCity());
String[] addressParts = addressPart.split(","); variables.put("customer.street", street.trim());
if (addressParts.length >= 1) { variables.put("customer.city", city.trim());
variables.put("customer.street", addressParts[0].trim()); } else {
} variables.put("customer.street", "");
if (addressParts.length >= 2) { variables.put("customer.city", "");
variables.put("customer.city", addressParts[1].trim());
}
} else {
variables.put("customer.contact_name", remaining);
}
} }
} else { } else {
variables.put("customer.company_name", ""); variables.put("customer.company_name", "");
@@ -516,7 +538,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
} }
// Invoice data // 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.date", java.time.LocalDate.now().toString());
variables.put("invoice.net_total", variables.put("invoice.net_total",
netAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + ""); netAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + "");

View File

@@ -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<CustomerInvoice, String> {
Optional<CustomerInvoice> findByJobId(String jobId);
List<CustomerInvoice> findByUserId(String userId);
}

View File

@@ -705,6 +705,14 @@ public class CustomerInvoiceService {
text = "[" + variable.replace("masterdata.", "").replace("customer.", "").replace("invoice.", "") text = "[" + variable.replace("masterdata.", "").replace("customer.", "").replace("invoice.", "")
.replace("job.", "") + "]"; .replace("job.", "") + "]";
System.out.println("DEBUG: No value for variable " + variable + ", using placeholder"); 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 { } else {
System.out.println("DEBUG: Using static text: " + text); System.out.println("DEBUG: Using static text: " + text);
} }