Compare commits

..

1 Commits

Author SHA1 Message Date
069b829294 feat: konfigurierbarer USt-Satz im Profil, Rechnungserstellung und Vorschau
- Neues Feld vatRate im User-Profil (Default 19 %), bearbeitbar im
  Rechnungs-Tab neben Rechnungslegung-Checkbox und Rechnungsprefix
- Canvas-Vorschau und PDF-Vorschau reagieren live auf den eingegebenen
  Steuersatz (JS-Setter updateProfileVatRate, dynamische Sample-Zeilen
  und Summary)
- Neue USt-Kachel auf create_invoice mit Eingabefeld; Summary-Kachel,
  PDF-Preview und gespeicherte Rechnung übernehmen den Feldwert
- Rechnungsvorschau für reale Aufträge auf dreispaltiges Layout (Name,
  Steuersatz, Nettobetrag) inkl. "zzgl. X% USt"-Zeile vereinheitlicht
- Kachel-Overflow auf create_invoice durch box-sizing: border-box
  korrigiert
2026-04-21 10:01:11 +02:00
10 changed files with 172 additions and 25 deletions

Binary file not shown.

View File

@@ -346,11 +346,14 @@ window.initProfileInvoiceGenerator = function() {
// Nettobetrag column header (right-aligned)
ctx.fillText('Nettobetrag', colNetX + colNetWidth - padding, y + rowHeight / 2);
var vatRate = (window.profileInvoiceVatRate != null) ? window.profileInvoiceVatRate : 0.19;
var vatPctLabel = (Math.round(vatRate * 10000) / 100).toString().replace('.', ',') + '%';
// Sample data rows (placeholder)
var sampleData = [
{ name: 'Umzugsleistung inkl. Verpackung', vat: '19%', net: '450,00 €' },
{ name: 'Entsorgung Möbel', vat: '19%', net: '85,00 €' },
{ name: 'Montage/De-Montage', vat: '19%', net: '120,00 €' }
{ name: 'Umzugsleistung inkl. Verpackung', vat: vatPctLabel, net: '450,00 €' },
{ name: 'Entsorgung Möbel', vat: vatPctLabel, net: '85,00 €' },
{ name: 'Montage/De-Montage', vat: vatPctLabel, net: '120,00 €' }
];
var currentY = y + rowHeight;
@@ -415,9 +418,8 @@ window.initProfileInvoiceGenerator = function() {
// Calculate totals from sample data
var netTotal = 655.00; // 450 + 85 + 120
var vatRate = 0.19;
var vatTotal = 124.45; // 655 * 0.19
var grossTotal = 779.45; // 655 + 124.45
var vatTotal = netTotal * vatRate;
var grossTotal = netTotal + vatTotal;
// Draw summary lines
ctx.textBaseline = 'middle';
@@ -435,7 +437,7 @@ window.initProfileInvoiceGenerator = function() {
// Umsatzsteuer - label left, value right
ctx.font = fontSize + 'px Arial';
ctx.textAlign = 'left';
ctx.fillText('zzgl. 19% USt:', labelX, summaryY + summaryRowHeight / 2);
ctx.fillText('zzgl. ' + vatPctLabel + ' USt:', labelX, summaryY + summaryRowHeight / 2);
ctx.font = 'bold ' + fontSize + 'px Arial';
ctx.textAlign = 'right';
ctx.fillText(vatTotal.toFixed(2).replace('.', ',') + ' €', valueX, summaryY + summaryRowHeight / 2);
@@ -1135,6 +1137,12 @@ window.initProfileInvoiceGenerator = function() {
};
};
window.updateProfileVatRate = function(rate) {
if (rate == null || isNaN(rate)) return;
window.profileInvoiceVatRate = rate;
draw();
};
window.updateProfileMasterdataValue = function(key, value) {
if (!window.masterdataValues) window.masterdataValues = {};
window.masterdataValues[key] = value;

View File

@@ -1068,6 +1068,7 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
border-radius: 24px;
background: rgba(255, 255, 255, 0.84);
box-shadow: var(--app-shadow-sm);
box-sizing: border-box;
}
.route-card,
@@ -1095,6 +1096,7 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
border-radius: 24px;
background: rgba(255, 255, 255, 0.9);
box-shadow: var(--app-shadow-sm);
box-sizing: border-box;
}
.detail-card,

View File

@@ -7,6 +7,7 @@ import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.index.Indexed;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Set;
@@ -68,4 +69,7 @@ public class User {
// Spracheinstellung (standardmäßig Deutsch)
@Field("language")
private Language language = Language.DE;
// Umsatzsteuer-Satz (als Dezimalwert, z.B. 0.19 für 19 %)
private BigDecimal vatRate = new BigDecimal("0.19");
}

View File

@@ -11,6 +11,7 @@ import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
@@ -68,6 +69,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private List<ServiceRow> gridRows = new ArrayList<>();
private Grid<ServiceRow> servicesGrid;
private Div servicesSection;
private Div summarySection;
private NumberField vatField;
/**
* Helper class to represent a row in the services grid
@@ -176,6 +179,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return;
}
currentUser = securityService.getAuthenticatedUser()
.flatMap(auth -> userRepository.findByEmail(auth.getUsername())).orElse(null);
createInvoiceView();
}
@@ -203,8 +209,12 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
Div servicesSection = createServicesSelectionSection();
add(servicesSection);
// VAT Section (must exist before summary so effectiveVatRate() can read the field)
Div vatSection = createVatSection();
add(vatSection);
// Summary Section
Div summarySection = createSummarySection();
summarySection = createSummarySection();
add(summarySection);
// Create Invoice Button
@@ -336,13 +346,16 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
section.setWidthFull();
section.addClassName("invoice-section-card");
section.getStyle().set("margin-bottom", "var(--lumo-space-m)");
populateSummarySection(section);
return section;
}
private void populateSummarySection(Div section) {
H3 sectionTitle = new H3(getTranslation("createinvoice.section.summary"));
section.add(sectionTitle);
// Calculate totals
BigDecimal netAmount = calculateNetAmount();
BigDecimal vatRate = Service.FIXED_VAT_RATE;
BigDecimal vatRate = effectiveVatRate();
BigDecimal vatAmount = netAmount.multiply(vatRate);
BigDecimal totalAmount = netAmount.add(vatAmount);
@@ -355,9 +368,40 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
totalAmount.setScale(2, RoundingMode.HALF_UP) + "", true));
section.add(priceTable);
}
private Div createVatSection() {
Div section = new Div();
section.setWidthFull();
section.addClassName("invoice-section-card");
section.getStyle().set("margin-bottom", "var(--lumo-space-m)");
H3 sectionTitle = new H3(getTranslation("createinvoice.section.vat"));
section.add(sectionTitle);
vatField = new NumberField();
vatField.setLabel(getTranslation("createinvoice.field.vatrate"));
vatField.setSuffixComponent(new Span("%"));
vatField.setStep(0.01);
vatField.setMin(0);
BigDecimal initialRate = currentUser != null && currentUser.getVatRate() != null
? currentUser.getVatRate()
: Service.FIXED_VAT_RATE;
vatField.setValue(initialRate.multiply(new BigDecimal("100")).doubleValue());
vatField.addValueChangeListener(e -> refreshSummarySection());
section.add(vatField);
return section;
}
private void refreshSummarySection() {
if (summarySection == null) {
return;
}
summarySection.removeAll();
populateSummarySection(summarySection);
}
private Div createPriceRow(String label, String value, boolean bold) {
Div row = new Div();
row.addClassName("price-row");
@@ -427,6 +471,17 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return units;
}
private BigDecimal effectiveVatRate() {
if (vatField != null && vatField.getValue() != null) {
return new BigDecimal(Double.toString(vatField.getValue()))
.divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP);
}
if (currentUser != null && currentUser.getVatRate() != null) {
return currentUser.getVatRate();
}
return Service.FIXED_VAT_RATE;
}
private BigDecimal calculateNetAmount() {
BigDecimal total = BigDecimal.ZERO;
@@ -514,7 +569,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
BigDecimal netAmount = calculateNetAmount();
BigDecimal vatRate = Service.FIXED_VAT_RATE;
BigDecimal vatRate = effectiveVatRate();
BigDecimal vatAmount = netAmount.multiply(vatRate);
BigDecimal totalAmount = netAmount.add(vatAmount);
@@ -553,7 +608,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
throws Exception {
// Calculate totals
BigDecimal netAmount = calculateNetAmount();
BigDecimal vatRate = Service.FIXED_VAT_RATE;
BigDecimal vatRate = effectiveVatRate();
BigDecimal vatAmount = netAmount.multiply(vatRate);
BigDecimal totalAmount = netAmount.add(vatAmount);

View File

@@ -76,6 +76,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
private final InvoiceTemplateService invoiceTemplateService;
private UserInvoiceData currentInvoiceData;
private Checkbox billingEnabled;
private NumberField vatRateField;
private VerticalLayout propertiesPanelProfile;
private final ServiceRepository serviceRepository;
@@ -330,10 +331,9 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
termsTextArea = new TextArea();
pdfFrame = new IFrame();
// Nur die Checkbox "Rechnungslegung über votianLT"
// Checkbox "Rechnungslegung über votianLT"
billingEnabled = new Checkbox(getTranslation("profile.billing.enabled"));
billingEnabled.setValue(true); // Standardmäßig aktiviert
billingTab.add(billingEnabled);
prefixField.setLabel(getTranslation("profile.billing.prefix"));
prefixField.setPlaceholder("z.B. RE-2024-");
@@ -347,7 +347,30 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
"if (window.updateProfileMasterdataValue) { window.updateProfileMasterdataValue('masterdata.invoice_number', '"
+ invNr.replace("'", "\\'") + "'); }");
});
billingTab.add(prefixField);
vatRateField = new NumberField();
vatRateField.setLabel(getTranslation("profile.settings.vatrate"));
vatRateField.setSuffixComponent(new Span("%"));
vatRateField.setStep(0.01);
vatRateField.setMin(0);
vatRateField.setMaxWidth("200px");
if (currentUser.getVatRate() != null) {
vatRateField.setValue(currentUser.getVatRate().multiply(new java.math.BigDecimal("100")).doubleValue());
}
vatRateField.addValueChangeListener(e -> {
Double v = e.getValue();
if (v != null) {
currentUser.setVatRate(new java.math.BigDecimal(Double.toString(v))
.divide(new java.math.BigDecimal("100"), 4, java.math.RoundingMode.HALF_UP));
getElement().executeJs("if (window.updateProfileVatRate) { window.updateProfileVatRate($0); }",
v / 100.0);
}
});
HorizontalLayout billingHeaderLayout = new HorizontalLayout(billingEnabled, prefixField, vatRateField);
billingHeaderLayout.setSpacing(true);
billingHeaderLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
billingTab.add(billingHeaderLayout);
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
final HorizontalLayout mainLayout = new HorizontalLayout();
@@ -451,6 +474,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
billingEnabled.addValueChangeListener(e -> {
boolean visible = e.getValue();
prefixField.setVisible(visible);
vatRateField.setVisible(visible);
mainLayout.setVisible(visible);
actionLayout.setVisible(visible);
});
@@ -843,6 +867,17 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
&& !houseNumberField.isInvalid() && !zipField.isInvalid() && !cityField.isInvalid();
}
private BigDecimal getPreviewVatRate() {
if (vatRateField != null && vatRateField.getValue() != null) {
return new BigDecimal(Double.toString(vatRateField.getValue())).divide(new BigDecimal("100"), 4,
java.math.RoundingMode.HALF_UP);
}
if (currentUser != null && currentUser.getVatRate() != null) {
return currentUser.getVatRate();
}
return Service.FIXED_VAT_RATE;
}
// Methoden für den Rechnungsgenerator im Profil
private void generatePreviewPdfFromProfile() {
try {
@@ -862,8 +897,9 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
} else {
templateData = result.toString();
}
BigDecimal previewVatRate = getPreviewVatRate();
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData,
currentUser, prefixField.getValue());
currentUser, prefixField.getValue(), previewVatRate);
showPdfInDialog(pdfBytes);
} catch (Exception ex) {
Notification.show(getTranslation("profile.invoice.pdf.preview.error", ex.getMessage()),
@@ -1424,9 +1460,14 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
+ city.replace("'", "\\'") + "'," + "'masterdata.email': '" + email.replace("'", "\\'")
+ "'," + "'masterdata.phone': '" + phone.replace("'", "\\'") + "',"
+ "'masterdata.invoice_number': '" + invoiceNumber.replace("'", "\\'") + "'" + "}";
BigDecimal rate = currentUser.getVatRate() != null ? currentUser.getVatRate()
: Service.FIXED_VAT_RATE;
double vatRateJs = rate.doubleValue();
getElement().executeJs("setTimeout(function() { "
+ " if (window.loadProfileTemplate && document.getElementById('invoice-canvas-container-profile')) { "
+ " console.log('Loading template into canvas...'); " + " window.masterdataValues = "
+ " console.log('Loading template into canvas...'); "
+ " window.profileInvoiceVatRate = " + vatRateJs + "; "
+ " window.masterdataValues = "
+ masterdataJson + "; " + " var templateData = JSON.parse('" + escapedJson + "'); "
+ " window.loadProfileTemplate(templateData); " + " } else { "
+ " console.error('loadProfileTemplate or canvas not available'); " + " } "

View File

@@ -258,6 +258,13 @@ public class CustomerInvoiceService {
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
String invoicePrefix) throws Exception {
return generatePdfFromCanvasTemplate(jsonTemplateData, user, invoicePrefix, null);
}
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
String invoicePrefix, BigDecimal vatRate) throws Exception {
BigDecimal effectiveVatRate = vatRate != null ? vatRate
: (user != null && user.getVatRate() != null ? user.getVatRate() : new BigDecimal("0.19"));
// Parse the JSON template data
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData);
@@ -458,7 +465,7 @@ public class CustomerInvoiceService {
}
} else if ("services.list".equals(variable)) {
// Render services list as a table
htmlBuilder.append(generateServicesTableHtml(mmWidth));
htmlBuilder.append(generateServicesTableHtml(mmWidth, effectiveVatRate));
} else if (text.contains("<br>")) {
// Multi-line text: render without nowrap so <br> tags work
htmlBuilder.append("<span>").append(text).append("</span>");
@@ -484,16 +491,23 @@ public class CustomerInvoiceService {
/**
* Generate HTML table for services list with summary section below.
*/
private String generateServicesTableHtml(double widthMm) {
private String generateServicesTableHtml(double widthMm, BigDecimal vatRate) {
StringBuilder html = new StringBuilder();
BigDecimal pct = vatRate.multiply(new BigDecimal("100")).setScale(2, java.math.RoundingMode.HALF_UP)
.stripTrailingZeros();
if (pct.scale() < 0) {
pct = pct.setScale(0);
}
String vatLabel = pct.toPlainString().replace('.', ',') + "%";
// Sample data for preview (will be replaced with actual job data later)
String[][] sampleData = { { "Umzugsleistung inkl. Verpackung", "19%", "450,00 €" },
{ "Entsorgung Möbel", "19%", "85,00 €" }, { "Montage/De-Montage", "19%", "120,00 €" } };
String[][] sampleData = { { "Umzugsleistung inkl. Verpackung", vatLabel, "450,00 €" },
{ "Entsorgung Möbel", vatLabel, "85,00 €" }, { "Montage/De-Montage", vatLabel, "120,00 €" } };
// Calculate totals
double netTotal = 655.00;
double grossTotal = 779.45;
double grossTotal = netTotal + (netTotal * vatRate.doubleValue());
// Wrapper div
html.append("<div style='width:100%;box-sizing:border-box;'>");
@@ -797,7 +811,9 @@ public class CustomerInvoiceService {
// Get invoice data from variables
String netTotal = variables.getOrDefault("invoice.net_total", "0,00 €");
String vatTotal = variables.getOrDefault("invoice.vat_total", "0,00 €");
String grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €");
String vatRateLabel = variables.getOrDefault("invoice.vat_rate", "19%");
// Parse services JSON from variables
java.util.List<java.util.Map<String, String>> servicesData = new java.util.ArrayList<>();
@@ -822,7 +838,9 @@ public class CustomerInvoiceService {
// Header row
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
html.append(
"<th style='text-align:left;padding:4px 8px;font-weight:bold;width:75%;white-space:nowrap;'>Name</th>");
"<th style='text-align:left;padding:4px 8px;font-weight:bold;width:55%;white-space:nowrap;'>Name</th>");
html.append(
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:20%;white-space:nowrap;'>Steuersatz</th>");
html.append(
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
html.append("</tr>");
@@ -832,7 +850,7 @@ public class CustomerInvoiceService {
// Fallback: show a single row with no data
html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
html.append(
"<td colspan='2' style='text-align:center;padding:4px 8px;white-space:nowrap;'>Keine Leistungen vorhanden</td>");
"<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++) {
@@ -843,8 +861,10 @@ public class CustomerInvoiceService {
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;width:75%;'>")
"<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:55%;'>")
.append(escapeHtml(name)).append("</td>");
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;width:20%;'>")
.append(escapeHtml(vatRateLabel)).append("</td>");
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;width:25%;'>")
.append(netAmount).append(" €</td>");
html.append("</tr>");
@@ -865,6 +885,15 @@ public class CustomerInvoiceService {
.append(netTotal).append("</td>");
html.append("</tr>");
// Umsatzsteuer
html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. ")
.append(escapeHtml(vatRateLabel)).append(" USt:</td>");
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>")
.append(vatTotal).append("</td>");
html.append("</tr>");
// Gesamtsumme
html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>");

View File

@@ -46,6 +46,7 @@ profile.settings.digitalprocess=Digitale Abwicklung
profile.settings.digitalprocess.info=Aufträge werden digital über die App abgewickelt
profile.settings.locateappuser=App-Nutzer orten
profile.settings.locateappuser.info=Standort der App-Nutzer wird regelmäßig übertragen
profile.settings.vatrate=Umsatzsteuer
profile.account=Konto
profile.security=Sicherheit
profile.security.twofactor=Zwei-Faktor-Authentifizierung
@@ -662,6 +663,8 @@ createinvoice.section.job=Auftragsdetails
createinvoice.section.route=Streckeninfo
createinvoice.section.services=Leistungen
createinvoice.section.summary=Zusammenfassung
createinvoice.section.vat=Umsatzsteuer
createinvoice.field.vatrate=USt-Satz
createinvoice.field.jobnumber=Auftragsnummer
createinvoice.field.customer=Kunde
createinvoice.field.status=Status

View File

@@ -46,6 +46,7 @@ profile.settings.digitalprocess=Digital Processing
profile.settings.digitalprocess.info=Jobs are processed digitally via the app
profile.settings.locateappuser=Locate App Users
profile.settings.locateappuser.info=App user location is transmitted regularly
profile.settings.vatrate=VAT rate
profile.account=Account
profile.security=Security
profile.security.twofactor=Two-Factor Authentication
@@ -662,6 +663,8 @@ createinvoice.section.job=Job Details
createinvoice.section.route=Route Info
createinvoice.section.services=Services
createinvoice.section.summary=Summary
createinvoice.section.vat=VAT
createinvoice.field.vatrate=VAT rate
createinvoice.field.jobnumber=Job Number
createinvoice.field.customer=Customer
createinvoice.field.status=Status

View File

@@ -662,6 +662,8 @@ createinvoice.section.job=Užsakymo informacija
createinvoice.section.route=Maršruto informacija
createinvoice.section.services=Paslaugos
createinvoice.section.summary=Santrauka
createinvoice.section.vat=PVM
createinvoice.field.vatrate=PVM tarifas
createinvoice.field.jobnumber=Užsakymo numeris
createinvoice.field.customer=Klientas
createinvoice.field.status=Būsena