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.
|
||||
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.
|
||||
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)
|
||||
- **Leistungsdaten**: Geben Sie Kilometer und Zeitaufwand ein
|
||||
- **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"**.
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
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 "";
|
||||
}).setHeader("MwSt").setSortable(true);
|
||||
}).setHeader("USt").setSortable(true);
|
||||
servicesGrid.addComponentColumn(service -> {
|
||||
// Verbindliche Leistungen können nicht gelöscht werden
|
||||
if (service.isMandatory()) {
|
||||
@@ -913,6 +913,10 @@ public class AddJobView extends Main {
|
||||
* route distance (for distance-based services).
|
||||
*/
|
||||
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) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
@@ -928,9 +932,13 @@ public class AddJobView extends Main {
|
||||
return BigDecimal.ZERO;
|
||||
|
||||
case TIME:
|
||||
// For time-based services, we would need time units
|
||||
// For now, return the price per 15 minutes as base value
|
||||
return service.getPricePer15Minutes() != null ? service.getPricePer15Minutes() : BigDecimal.ZERO;
|
||||
if (service.getPricePer15Minutes() != null && durationSeconds != null && durationSeconds > 0) {
|
||||
// Dauer in 15-Minuten-Einheiten umrechnen (aufrunden)
|
||||
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;
|
||||
@@ -1185,15 +1193,7 @@ public class AddJobView extends Main {
|
||||
|
||||
binder.forField(deliveryCity).asRequired("").bind(Job::getDeliveryCity, Job::setDeliveryCity);
|
||||
|
||||
// Price is now calculated from selected services - bind to job price for
|
||||
// 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);
|
||||
// Price wird manuell in submit() berechnet und gesetzt - kein Binder notwendig
|
||||
|
||||
// Bind date picker fields with validation
|
||||
binder.forField(pickupDate).asRequired("")
|
||||
@@ -1501,15 +1501,19 @@ public class AddJobView extends Main {
|
||||
if (remarkArea != null)
|
||||
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
|
||||
job.setServiceIds(selectedServices.stream().map(Service::getId).toList());
|
||||
|
||||
// Validate all required fields using the binder
|
||||
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
|
||||
@@ -1522,9 +1526,6 @@ public class AddJobView extends Main {
|
||||
job.setRouteDurationSeconds(manualDurationInput.getValue() * 60); // Minuten in Sekunden umrechnen
|
||||
}
|
||||
}
|
||||
|
||||
// Validate all required fields using the binder
|
||||
if (binder.writeBeanIfValid(job)) {
|
||||
// Additional validation: If digital processing is enabled, app user must be
|
||||
// selected
|
||||
if (digitalProcessing.getValue() && appUser.getValue() == null) {
|
||||
@@ -3263,6 +3264,21 @@ public class AddJobView extends Main {
|
||||
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
|
||||
* Streckeninformationen zurückzusetzen.
|
||||
|
||||
@@ -519,6 +519,40 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
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
|
||||
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.PhotoRepository;
|
||||
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.Barcode;
|
||||
import de.assecutor.votianlt.model.Photo;
|
||||
@@ -55,6 +57,7 @@ import org.bson.types.ObjectId;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -76,6 +79,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
private final AppUserService appUserService;
|
||||
private final JobHistoryService jobHistoryService;
|
||||
private final LocationService locationService;
|
||||
private final ServiceRepository serviceRepository;
|
||||
|
||||
@Value("${app.google.maps.api-key}")
|
||||
private String googleMapsApiKey;
|
||||
@@ -86,7 +90,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository,
|
||||
TaskRepository taskRepository, SignatureRepository signatureRepository, BarcodeRepository barcodeRepository,
|
||||
PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService,
|
||||
MessageService messageService, JobHistoryService jobHistoryService, LocationService locationService) {
|
||||
MessageService messageService, JobHistoryService jobHistoryService, LocationService locationService,
|
||||
ServiceRepository serviceRepository) {
|
||||
this.jobRepository = jobRepository;
|
||||
this.cargoItemRepository = cargoItemRepository;
|
||||
this.taskRepository = taskRepository;
|
||||
@@ -97,6 +102,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
this.appUserService = appUserService;
|
||||
this.jobHistoryService = jobHistoryService;
|
||||
this.locationService = locationService;
|
||||
this.serviceRepository = serviceRepository;
|
||||
|
||||
setSizeFull();
|
||||
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();
|
||||
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()) {
|
||||
infoBox.add(new Span("Bemerkung: " + job.getRemark()));
|
||||
}
|
||||
@@ -1176,4 +1188,75 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
private String getGoogleMapsApiKey() {
|
||||
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
|
||||
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",
|
||||
new BigDecimal("85.00"), vatRate);
|
||||
@@ -739,13 +739,19 @@ public class CustomerInvoiceService {
|
||||
String grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €");
|
||||
String vatRate = variables.getOrDefault("invoice.vat_rate", "19%");
|
||||
|
||||
// Sample data for now - in the future this would come from actual job services
|
||||
// Parse the net total to get individual service amounts (split evenly for demo)
|
||||
String[][] serviceData = {
|
||||
{"Leistung 1", vatRate, "450,00 €"},
|
||||
{"Leistung 2", vatRate, "85,00 €"},
|
||||
{"Leistung 3", vatRate, "120,00 €"}
|
||||
};
|
||||
// Parse services JSON from variables
|
||||
java.util.List<java.util.Map<String, String>> servicesData = new java.util.ArrayList<>();
|
||||
String servicesJson = variables.get("services.json");
|
||||
if (servicesJson != null && !servicesJson.isEmpty() && !servicesJson.equals("[]")) {
|
||||
try {
|
||||
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
|
||||
html.append("<div style='width:100%;box-sizing:border-box;'>");
|
||||
@@ -760,15 +766,27 @@ 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("</tr>");
|
||||
|
||||
// Data rows - use actual service data
|
||||
for (int i = 0; i < serviceData.length; i++) {
|
||||
// Data rows - use actual service data from the job
|
||||
if (servicesData.isEmpty()) {
|
||||
// Fallback: show a single row with no data
|
||||
html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
|
||||
html.append("<td colspan='3' style='text-align:center;padding:4px 8px;white-space:nowrap;'>Keine Leistungen vorhanden</td>");
|
||||
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(serviceData[i][0]).append("</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("<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>");
|
||||
|
||||
@@ -806,4 +824,18 @@ public class CustomerInvoiceService {
|
||||
|
||||
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>
|
||||
</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>
|
||||
</tr>
|
||||
<tr class="total-row">
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<td></td><td style="text-align:left">Nettobetrag</td><td></td><td>5.639,00 €</td>
|
||||
</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 class="total-row">
|
||||
<td></td><td style="text-align:left">Endbetrag</td><td></td><td>6.710,41 €</td>
|
||||
|
||||
Reference in New Issue
Block a user