Erweiterungen
This commit is contained in:
@@ -161,7 +161,7 @@ Aufgaben definieren, welche Schritte der App-Nutzer bei der Ausführung des Auft
|
|||||||
|
|
||||||
1. **Pflichtleistungen** werden automatisch geladen.
|
1. **Pflichtleistungen** werden automatisch geladen.
|
||||||
2. Über die Dropdown-Liste können Sie weitere **Leistungen hinzufügen**.
|
2. Über die Dropdown-Liste können Sie weitere **Leistungen hinzufügen**.
|
||||||
3. Die Berechnung von **Netto**, **MwSt.** und **Brutto** erfolgt automatisch.
|
3. Die Berechnung von **Netto**, **USt.** und **Brutto** erfolgt automatisch.
|
||||||
4. Die **Routeninformationen** (Entfernung und Fahrtdauer) werden aus der Adressvalidierung angezeigt.
|
4. Die **Routeninformationen** (Entfernung und Fahrtdauer) werden aus der Adressvalidierung angezeigt.
|
||||||
5. Im Feld **Bemerkung** können Sie zusätzliche Anmerkungen zum Auftrag hinterlegen.
|
5. Im Feld **Bemerkung** können Sie zusätzliche Anmerkungen zum Auftrag hinterlegen.
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ Jede Konversation zeigt eine Vorschau, den Zeitpunkt der letzten Nachricht und d
|
|||||||
- **Auftragsdetails** (Abhol- und Zustelladresse, Termine)
|
- **Auftragsdetails** (Abhol- und Zustelladresse, Termine)
|
||||||
- **Leistungsdaten**: Geben Sie Kilometer und Zeitaufwand ein
|
- **Leistungsdaten**: Geben Sie Kilometer und Zeitaufwand ein
|
||||||
- **Leistungen**: Wählen Sie die abzurechnenden Leistungen aus Ihrem Leistungskatalog
|
- **Leistungen**: Wählen Sie die abzurechnenden Leistungen aus Ihrem Leistungskatalog
|
||||||
- **Zusammenfassung**: Automatische Berechnung von Netto, MwSt. und Brutto
|
- **Zusammenfassung**: Automatische Berechnung von Netto, USt. und Brutto
|
||||||
5. Klicken Sie auf **"Rechnung erstellen"**.
|
5. Klicken Sie auf **"Rechnung erstellen"**.
|
||||||
|
|
||||||
### 8.2 Meine Rechnungen
|
### 8.2 Meine Rechnungen
|
||||||
@@ -386,7 +386,7 @@ Verwalten Sie Ihren **Leistungskatalog**, der bei der Auftragserstellung und Rec
|
|||||||
|
|
||||||
1. Klicken Sie auf **"Leistung hinzufügen"**, um eine neue Leistung anzulegen.
|
1. Klicken Sie auf **"Leistung hinzufügen"**, um eine neue Leistung anzulegen.
|
||||||
2. Wählen Sie die **Leistungsbezeichnung** aus oder geben Sie eine neue ein.
|
2. Wählen Sie die **Leistungsbezeichnung** aus oder geben Sie eine neue ein.
|
||||||
3. Geben Sie den **Preis** und den **MwSt.-Satz** ein.
|
3. Geben Sie den **Preis** und den **USt.-Satz** ein.
|
||||||
4. Über das Papierkorb-Symbol können Sie einzelne Leistungen entfernen.
|
4. Über das Papierkorb-Symbol können Sie einzelne Leistungen entfernen.
|
||||||
|
|
||||||
Rechts neben dem Formular wird eine **Live-Vorschau** Ihrer Rechnungsvorlage angezeigt, die sich bei Änderungen automatisch aktualisiert.
|
Rechts neben dem Formular wird eine **Live-Vorschau** Ihrer Rechnungsvorlage angezeigt, die sich bei Änderungen automatisch aktualisiert.
|
||||||
|
|||||||
@@ -749,7 +749,7 @@ public class AddJobView extends Main {
|
|||||||
return service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + " %";
|
return service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + " %";
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}).setHeader("MwSt").setSortable(true);
|
}).setHeader("USt").setSortable(true);
|
||||||
servicesGrid.addComponentColumn(service -> {
|
servicesGrid.addComponentColumn(service -> {
|
||||||
// Verbindliche Leistungen können nicht gelöscht werden
|
// Verbindliche Leistungen können nicht gelöscht werden
|
||||||
if (service.isMandatory()) {
|
if (service.isMandatory()) {
|
||||||
@@ -913,6 +913,10 @@ public class AddJobView extends Main {
|
|||||||
* route distance (for distance-based services).
|
* route distance (for distance-based services).
|
||||||
*/
|
*/
|
||||||
private BigDecimal calculateServicePrice(Service service, Double routeDistance) {
|
private BigDecimal calculateServicePrice(Service service, Double routeDistance) {
|
||||||
|
return calculateServicePrice(service, routeDistance, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal calculateServicePrice(Service service, Double routeDistance, Integer durationSeconds) {
|
||||||
if (service.getCalculationBasis() == null) {
|
if (service.getCalculationBasis() == null) {
|
||||||
return BigDecimal.ZERO;
|
return BigDecimal.ZERO;
|
||||||
}
|
}
|
||||||
@@ -928,9 +932,13 @@ public class AddJobView extends Main {
|
|||||||
return BigDecimal.ZERO;
|
return BigDecimal.ZERO;
|
||||||
|
|
||||||
case TIME:
|
case TIME:
|
||||||
// For time-based services, we would need time units
|
if (service.getPricePer15Minutes() != null && durationSeconds != null && durationSeconds > 0) {
|
||||||
// For now, return the price per 15 minutes as base value
|
// Dauer in 15-Minuten-Einheiten umrechnen (aufrunden)
|
||||||
return service.getPricePer15Minutes() != null ? service.getPricePer15Minutes() : BigDecimal.ZERO;
|
int units = durationSeconds / 900; // 900 Sekunden = 15 Minuten
|
||||||
|
if (durationSeconds % 900 > 0) units++; // Aufrunden
|
||||||
|
return service.getPricePer15Minutes().multiply(BigDecimal.valueOf(units));
|
||||||
|
}
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return BigDecimal.ZERO;
|
return BigDecimal.ZERO;
|
||||||
@@ -1185,15 +1193,7 @@ public class AddJobView extends Main {
|
|||||||
|
|
||||||
binder.forField(deliveryCity).asRequired("").bind(Job::getDeliveryCity, Job::setDeliveryCity);
|
binder.forField(deliveryCity).asRequired("").bind(Job::getDeliveryCity, Job::setDeliveryCity);
|
||||||
|
|
||||||
// Price is now calculated from selected services - bind to job price for
|
// Price wird manuell in submit() berechnet und gesetzt - kein Binder notwendig
|
||||||
// storage
|
|
||||||
binder.forField(new com.vaadin.flow.component.textfield.TextField()).withConverter((String str) -> {
|
|
||||||
// Calculate total from selected services
|
|
||||||
BigDecimal total = selectedServices.stream()
|
|
||||||
.map(svc -> svc.getEffectivePrice() != null ? svc.getEffectivePrice() : BigDecimal.ZERO)
|
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
return total;
|
|
||||||
}, (java.math.BigDecimal bd) -> bd == null ? "" : bd.toString()).bind(Job::getPrice, Job::setPrice);
|
|
||||||
|
|
||||||
// Bind date picker fields with validation
|
// Bind date picker fields with validation
|
||||||
binder.forField(pickupDate).asRequired("")
|
binder.forField(pickupDate).asRequired("")
|
||||||
@@ -1501,30 +1501,31 @@ public class AddJobView extends Main {
|
|||||||
if (remarkArea != null)
|
if (remarkArea != null)
|
||||||
job.setRemark(remarkArea.getValue());
|
job.setRemark(remarkArea.getValue());
|
||||||
|
|
||||||
// Calculate price from selected services
|
|
||||||
BigDecimal totalPrice = selectedServices.stream()
|
|
||||||
.map(s -> s.getEffectivePrice() != null ? s.getEffectivePrice() : BigDecimal.ZERO)
|
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
job.setPrice(totalPrice);
|
|
||||||
|
|
||||||
// Store selected service IDs in job for invoice creation
|
// Store selected service IDs in job for invoice creation
|
||||||
job.setServiceIds(selectedServices.stream().map(Service::getId).toList());
|
job.setServiceIds(selectedServices.stream().map(Service::getId).toList());
|
||||||
|
|
||||||
// Store route distance and duration in job for invoice creation
|
|
||||||
if (routeCalculationResult != null && routeCalculationResult.isValid()) {
|
|
||||||
// Berechnete Route verwenden
|
|
||||||
job.setRouteDistanceKm(routeCalculationResult.getDistanceKm());
|
|
||||||
job.setRouteDurationSeconds(routeCalculationResult.getDurationSeconds());
|
|
||||||
} else if (manualDistanceInput != null && manualDistanceInput.getValue() != null) {
|
|
||||||
// Manuelle Eingabe verwenden
|
|
||||||
job.setRouteDistanceKm(manualDistanceInput.getValue());
|
|
||||||
if (manualDurationInput != null && manualDurationInput.getValue() != null) {
|
|
||||||
job.setRouteDurationSeconds(manualDurationInput.getValue() * 60); // Minuten in Sekunden umrechnen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all required fields using the binder
|
// Validate all required fields using the binder
|
||||||
if (binder.writeBeanIfValid(job)) {
|
if (binder.writeBeanIfValid(job)) {
|
||||||
|
// Preis nach dem Binder-Call berechnen (damit er nicht überschrieben wird)
|
||||||
|
Double routeDistance = getEffectiveRouteDistance();
|
||||||
|
Integer durationSeconds = getEffectiveRouteDuration();
|
||||||
|
BigDecimal netTotal = selectedServices.stream()
|
||||||
|
.map(s -> calculateServicePrice(s, routeDistance, durationSeconds))
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
job.setPrice(netTotal);
|
||||||
|
|
||||||
|
// Store route distance and duration in job for invoice creation
|
||||||
|
if (routeCalculationResult != null && routeCalculationResult.isValid()) {
|
||||||
|
// Berechnete Route verwenden
|
||||||
|
job.setRouteDistanceKm(routeCalculationResult.getDistanceKm());
|
||||||
|
job.setRouteDurationSeconds(routeCalculationResult.getDurationSeconds());
|
||||||
|
} else if (manualDistanceInput != null && manualDistanceInput.getValue() != null) {
|
||||||
|
// Manuelle Eingabe verwenden
|
||||||
|
job.setRouteDistanceKm(manualDistanceInput.getValue());
|
||||||
|
if (manualDurationInput != null && manualDurationInput.getValue() != null) {
|
||||||
|
job.setRouteDurationSeconds(manualDurationInput.getValue() * 60); // Minuten in Sekunden umrechnen
|
||||||
|
}
|
||||||
|
}
|
||||||
// Additional validation: If digital processing is enabled, app user must be
|
// Additional validation: If digital processing is enabled, app user must be
|
||||||
// selected
|
// selected
|
||||||
if (digitalProcessing.getValue() && appUser.getValue() == null) {
|
if (digitalProcessing.getValue() && appUser.getValue() == null) {
|
||||||
@@ -3263,6 +3264,21 @@ public class AddJobView extends Main {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die effektive Fahrtzeit zurück (berechnet oder manuell eingegeben).
|
||||||
|
*
|
||||||
|
* @return Dauer in Sekunden oder null, wenn keine Dauer verfügbar
|
||||||
|
*/
|
||||||
|
private Integer getEffectiveRouteDuration() {
|
||||||
|
if (routeCalculationResult != null && routeCalculationResult.isValid()) {
|
||||||
|
return routeCalculationResult.getDurationSeconds();
|
||||||
|
}
|
||||||
|
if (manualDurationInput != null && manualDurationInput.getValue() != null) {
|
||||||
|
return manualDurationInput.getValue() * 60; // Minuten in Sekunden
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registriert ValueChangeListener für alle Adressfelder, um bei Änderungen die
|
* Registriert ValueChangeListener für alle Adressfelder, um bei Änderungen die
|
||||||
* Streckeninformationen zurückzusetzen.
|
* Streckeninformationen zurückzusetzen.
|
||||||
|
|||||||
@@ -519,6 +519,40 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
variables.put("job.distance_km", "-");
|
variables.put("job.distance_km", "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Services data - add as JSON array for the template
|
||||||
|
List<Map<String, String>> servicesData = new ArrayList<>();
|
||||||
|
for (Service service : getSelectedServices()) {
|
||||||
|
Map<String, String> serviceData = new HashMap<>();
|
||||||
|
serviceData.put("name", service.getName());
|
||||||
|
|
||||||
|
// Calculate price based on calculation basis
|
||||||
|
BigDecimal price = calculateServicePrice(service);
|
||||||
|
if (price != null) {
|
||||||
|
serviceData.put("netAmount", price.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ","));
|
||||||
|
} else {
|
||||||
|
serviceData.put("netAmount", "0,00");
|
||||||
|
}
|
||||||
|
|
||||||
|
// VAT rate
|
||||||
|
if (service.getVatRate() != null) {
|
||||||
|
serviceData.put("vatRate", service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + "%");
|
||||||
|
} else {
|
||||||
|
serviceData.put("vatRate", "19%");
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesData.add(serviceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize services data to JSON and store in variables
|
||||||
|
if (!servicesData.isEmpty()) {
|
||||||
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
|
variables.put("services.json", mapper.writeValueAsString(servicesData));
|
||||||
|
variables.put("services.count", String.valueOf(servicesData.size()));
|
||||||
|
} else {
|
||||||
|
variables.put("services.json", "[]");
|
||||||
|
variables.put("services.count", "0");
|
||||||
|
}
|
||||||
|
|
||||||
// Generate PDF using CustomerInvoiceService
|
// Generate PDF using CustomerInvoiceService
|
||||||
return customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData, variables, currentUser);
|
return customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData, variables, currentUser);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import de.assecutor.votianlt.repository.SignatureRepository;
|
|||||||
import de.assecutor.votianlt.repository.BarcodeRepository;
|
import de.assecutor.votianlt.repository.BarcodeRepository;
|
||||||
import de.assecutor.votianlt.repository.PhotoRepository;
|
import de.assecutor.votianlt.repository.PhotoRepository;
|
||||||
import de.assecutor.votianlt.repository.CommentRepository;
|
import de.assecutor.votianlt.repository.CommentRepository;
|
||||||
|
import de.assecutor.votianlt.repository.ServiceRepository;
|
||||||
|
import de.assecutor.votianlt.model.Service;
|
||||||
import de.assecutor.votianlt.model.Signature;
|
import de.assecutor.votianlt.model.Signature;
|
||||||
import de.assecutor.votianlt.model.Barcode;
|
import de.assecutor.votianlt.model.Barcode;
|
||||||
import de.assecutor.votianlt.model.Photo;
|
import de.assecutor.votianlt.model.Photo;
|
||||||
@@ -55,6 +57,7 @@ import org.bson.types.ObjectId;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -76,6 +79,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
|||||||
private final AppUserService appUserService;
|
private final AppUserService appUserService;
|
||||||
private final JobHistoryService jobHistoryService;
|
private final JobHistoryService jobHistoryService;
|
||||||
private final LocationService locationService;
|
private final LocationService locationService;
|
||||||
|
private final ServiceRepository serviceRepository;
|
||||||
|
|
||||||
@Value("${app.google.maps.api-key}")
|
@Value("${app.google.maps.api-key}")
|
||||||
private String googleMapsApiKey;
|
private String googleMapsApiKey;
|
||||||
@@ -86,7 +90,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
|||||||
public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository,
|
public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository,
|
||||||
TaskRepository taskRepository, SignatureRepository signatureRepository, BarcodeRepository barcodeRepository,
|
TaskRepository taskRepository, SignatureRepository signatureRepository, BarcodeRepository barcodeRepository,
|
||||||
PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService,
|
PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService,
|
||||||
MessageService messageService, JobHistoryService jobHistoryService, LocationService locationService) {
|
MessageService messageService, JobHistoryService jobHistoryService, LocationService locationService,
|
||||||
|
ServiceRepository serviceRepository) {
|
||||||
this.jobRepository = jobRepository;
|
this.jobRepository = jobRepository;
|
||||||
this.cargoItemRepository = cargoItemRepository;
|
this.cargoItemRepository = cargoItemRepository;
|
||||||
this.taskRepository = taskRepository;
|
this.taskRepository = taskRepository;
|
||||||
@@ -97,6 +102,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
|||||||
this.appUserService = appUserService;
|
this.appUserService = appUserService;
|
||||||
this.jobHistoryService = jobHistoryService;
|
this.jobHistoryService = jobHistoryService;
|
||||||
this.locationService = locationService;
|
this.locationService = locationService;
|
||||||
|
this.serviceRepository = serviceRepository;
|
||||||
|
|
||||||
setSizeFull();
|
setSizeFull();
|
||||||
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
|
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
|
||||||
@@ -260,7 +266,13 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
|||||||
|
|
||||||
VerticalLayout infoBox = borderedBox();
|
VerticalLayout infoBox = borderedBox();
|
||||||
infoBox.add(new H3("Weitere Informationen"));
|
infoBox.add(new H3("Weitere Informationen"));
|
||||||
infoBox.add(new Span("Preis: " + (job.getPrice() != null ? formatPrice(job.getPrice()) : "-")));
|
|
||||||
|
// Preis basierend auf den hinterlegten Leistungen berechnen
|
||||||
|
PriceCalculationResult priceResult = calculatePriceFromServices(job);
|
||||||
|
infoBox.add(new Span("Netto: " + formatPrice(priceResult.netAmount())));
|
||||||
|
infoBox.add(new Span("USt: " + formatPrice(priceResult.vatAmount())));
|
||||||
|
infoBox.add(new Span("Gesamt: " + formatPrice(priceResult.totalAmount())));
|
||||||
|
|
||||||
if (job.getRemark() != null && !job.getRemark().isBlank()) {
|
if (job.getRemark() != null && !job.getRemark().isBlank()) {
|
||||||
infoBox.add(new Span("Bemerkung: " + job.getRemark()));
|
infoBox.add(new Span("Bemerkung: " + job.getRemark()));
|
||||||
}
|
}
|
||||||
@@ -1176,4 +1188,75 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
|||||||
private String getGoogleMapsApiKey() {
|
private String getGoogleMapsApiKey() {
|
||||||
return googleMapsApiKey != null ? googleMapsApiKey : "";
|
return googleMapsApiKey != null ? googleMapsApiKey : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet den Preis basierend auf den hinterlegten Leistungen des Jobs.
|
||||||
|
*/
|
||||||
|
private PriceCalculationResult calculatePriceFromServices(Job job) {
|
||||||
|
BigDecimal netTotal = BigDecimal.ZERO;
|
||||||
|
BigDecimal vatTotal = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
List<String> serviceIds = job.getServiceIds();
|
||||||
|
if (serviceIds == null || serviceIds.isEmpty()) {
|
||||||
|
// Fallback auf gespeicherten Preis
|
||||||
|
BigDecimal price = job.getPrice() != null ? job.getPrice() : BigDecimal.ZERO;
|
||||||
|
return new PriceCalculationResult(price, BigDecimal.ZERO, price);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routendaten für die Berechnung
|
||||||
|
Double routeDistance = job.getRouteDistanceKm();
|
||||||
|
Integer durationSeconds = job.getRouteDurationSeconds();
|
||||||
|
|
||||||
|
for (String serviceId : serviceIds) {
|
||||||
|
Service service = serviceRepository.findById(serviceId).orElse(null);
|
||||||
|
if (service == null) continue;
|
||||||
|
|
||||||
|
BigDecimal price = calculateServicePrice(service, routeDistance, durationSeconds);
|
||||||
|
BigDecimal vatRate = service.getVatRate() != null ? service.getVatRate() : new BigDecimal("0.19");
|
||||||
|
|
||||||
|
netTotal = netTotal.add(price);
|
||||||
|
vatTotal = vatTotal.add(price.multiply(vatRate));
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal totalAmount = netTotal.add(vatTotal);
|
||||||
|
return new PriceCalculationResult(netTotal, vatTotal, totalAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet den Preis für eine einzelne Leistung basierend auf ihrer Berechnungsgrundlage.
|
||||||
|
*/
|
||||||
|
private BigDecimal calculateServicePrice(Service service, Double routeDistance, Integer durationSeconds) {
|
||||||
|
if (service.getCalculationBasis() == null) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (service.getCalculationBasis()) {
|
||||||
|
case FLAT_RATE:
|
||||||
|
return service.getPrice() != null ? service.getPrice() : BigDecimal.ZERO;
|
||||||
|
|
||||||
|
case DISTANCE:
|
||||||
|
if (service.getPricePerKilometer() != null && routeDistance != null && routeDistance > 0) {
|
||||||
|
return service.getPricePerKilometer().multiply(BigDecimal.valueOf(routeDistance));
|
||||||
|
}
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
|
||||||
|
case TIME:
|
||||||
|
if (service.getPricePer15Minutes() != null && durationSeconds != null && durationSeconds > 0) {
|
||||||
|
// Dauer in 15-Minuten-Einheiten umrechnen
|
||||||
|
int units = durationSeconds / 900; // 900 Sekunden = 15 Minuten
|
||||||
|
if (durationSeconds % 900 > 0) units++; // Aufrunden
|
||||||
|
return service.getPricePer15Minutes().multiply(BigDecimal.valueOf(units));
|
||||||
|
}
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record für die Preisberechnungsergebnisse.
|
||||||
|
*/
|
||||||
|
private record PriceCalculationResult(BigDecimal netAmount, BigDecimal vatAmount, BigDecimal totalAmount) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public class CustomerInvoiceService {
|
|||||||
|
|
||||||
// Rechnungsposten
|
// Rechnungsposten
|
||||||
List<CustomerInvoiceItem> items = new ArrayList<>();
|
List<CustomerInvoiceItem> items = new ArrayList<>();
|
||||||
BigDecimal vatRate = new BigDecimal("0.19"); // 19% MwSt.
|
BigDecimal vatRate = new BigDecimal("0.19"); // 19% USt.
|
||||||
|
|
||||||
CustomerInvoiceItem item1 = new CustomerInvoiceItem(new BigDecimal("2"), "Std.", "Transportdienstleistung",
|
CustomerInvoiceItem item1 = new CustomerInvoiceItem(new BigDecimal("2"), "Std.", "Transportdienstleistung",
|
||||||
new BigDecimal("85.00"), vatRate);
|
new BigDecimal("85.00"), vatRate);
|
||||||
@@ -739,13 +739,19 @@ public class CustomerInvoiceService {
|
|||||||
String grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €");
|
String grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €");
|
||||||
String vatRate = variables.getOrDefault("invoice.vat_rate", "19%");
|
String vatRate = variables.getOrDefault("invoice.vat_rate", "19%");
|
||||||
|
|
||||||
// Sample data for now - in the future this would come from actual job services
|
// Parse services JSON from variables
|
||||||
// Parse the net total to get individual service amounts (split evenly for demo)
|
java.util.List<java.util.Map<String, String>> servicesData = new java.util.ArrayList<>();
|
||||||
String[][] serviceData = {
|
String servicesJson = variables.get("services.json");
|
||||||
{"Leistung 1", vatRate, "450,00 €"},
|
if (servicesJson != null && !servicesJson.isEmpty() && !servicesJson.equals("[]")) {
|
||||||
{"Leistung 2", vatRate, "85,00 €"},
|
try {
|
||||||
{"Leistung 3", vatRate, "120,00 €"}
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
};
|
com.fasterxml.jackson.core.type.TypeReference<java.util.List<java.util.Map<String, String>>> typeRef =
|
||||||
|
new com.fasterxml.jackson.core.type.TypeReference<>() {};
|
||||||
|
servicesData = mapper.readValue(servicesJson, typeRef);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("DEBUG: Failed to parse services JSON: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wrapper div
|
// Wrapper div
|
||||||
html.append("<div style='width:100%;box-sizing:border-box;'>");
|
html.append("<div style='width:100%;box-sizing:border-box;'>");
|
||||||
@@ -760,14 +766,26 @@ public class CustomerInvoiceService {
|
|||||||
html.append("<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
|
html.append("<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
|
|
||||||
// Data rows - use actual service data
|
// Data rows - use actual service data from the job
|
||||||
for (int i = 0; i < serviceData.length; i++) {
|
if (servicesData.isEmpty()) {
|
||||||
String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : "";
|
// Fallback: show a single row with no data
|
||||||
html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
|
html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
|
||||||
html.append("<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>").append(serviceData[i][0]).append("</td>");
|
html.append("<td colspan='3' style='text-align:center;padding:4px 8px;white-space:nowrap;'>Keine Leistungen vorhanden</td>");
|
||||||
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(serviceData[i][1]).append("</td>");
|
|
||||||
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(serviceData[i][2]).append("</td>");
|
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < servicesData.size(); i++) {
|
||||||
|
java.util.Map<String, String> service = servicesData.get(i);
|
||||||
|
String name = service.getOrDefault("name", "Unbekannte Leistung");
|
||||||
|
String serviceVatRate = service.getOrDefault("vatRate", vatRate);
|
||||||
|
String netAmount = service.getOrDefault("netAmount", "0,00");
|
||||||
|
|
||||||
|
String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : "";
|
||||||
|
html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
|
||||||
|
html.append("<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>").append(escapeHtml(name)).append("</td>");
|
||||||
|
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(serviceVatRate).append("</td>");
|
||||||
|
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(netAmount).append(" €</td>");
|
||||||
|
html.append("</tr>");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html.append("</table>");
|
html.append("</table>");
|
||||||
@@ -806,4 +824,18 @@ public class CustomerInvoiceService {
|
|||||||
|
|
||||||
return html.toString();
|
return html.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML special characters to prevent XSS.
|
||||||
|
*/
|
||||||
|
private String escapeHtml(String input) {
|
||||||
|
if (input == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return input.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("\"", """)
|
||||||
|
.replace("'", "'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,7 +423,7 @@
|
|||||||
<td class="amount-col">${invoiceData.netAmount}</td>
|
<td class="amount-col">${invoiceData.netAmount}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-col">zzgl. ${invoiceData.vatRate} MwSt.:</td>
|
<td class="label-col">zzgl. ${invoiceData.vatRate} USt.:</td>
|
||||||
<td class="amount-col">${invoiceData.vatAmount}</td>
|
<td class="amount-col">${invoiceData.vatAmount}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="total-row">
|
<tr class="total-row">
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
<td></td><td style="text-align:left">Nettobetrag</td><td></td><td>5.639,00 €</td>
|
<td></td><td style="text-align:left">Nettobetrag</td><td></td><td>5.639,00 €</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td><td style="text-align:left">+ 19% MwSt.</td><td></td><td>1.071,41 €</td>
|
<td></td><td style="text-align:left">+ 19% USt.</td><td></td><td>1.071,41 €</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="total-row">
|
<tr class="total-row">
|
||||||
<td></td><td style="text-align:left">Endbetrag</td><td></td><td>6.710,41 €</td>
|
<td></td><td style="text-align:left">Endbetrag</td><td></td><td>6.710,41 €</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user