Erweiterungen
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
if (addressParts.length >= 2) {
|
|
||||||
variables.put("customer.city", addressParts[1].trim());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
variables.put("customer.contact_name", remaining);
|
variables.put("customer.street", "");
|
||||||
}
|
variables.put("customer.city", "");
|
||||||
}
|
}
|
||||||
} 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(".", ",") + " €");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user