diff --git a/backend/src/main/bundles/dev.bundle b/backend/src/main/bundles/dev.bundle index 62da9a8..76304aa 100644 Binary files a/backend/src/main/bundles/dev.bundle and b/backend/src/main/bundles/dev.bundle differ diff --git a/backend/src/main/frontend/invoice-generator/profile-invoice-generator.js b/backend/src/main/frontend/invoice-generator/profile-invoice-generator.js index 6d6a64a..e0de776 100644 --- a/backend/src/main/frontend/invoice-generator/profile-invoice-generator.js +++ b/backend/src/main/frontend/invoice-generator/profile-invoice-generator.js @@ -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; diff --git a/backend/src/main/frontend/themes/votian-modern/styles.css b/backend/src/main/frontend/themes/votian-modern/styles.css index 09ba84c..cb52ebb 100644 --- a/backend/src/main/frontend/themes/votian-modern/styles.css +++ b/backend/src/main/frontend/themes/votian-modern/styles.css @@ -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, diff --git a/backend/src/main/java/de/assecutor/votianlt/model/User.java b/backend/src/main/java/de/assecutor/votianlt/model/User.java index 11d3e05..3f5a5f0 100644 --- a/backend/src/main/java/de/assecutor/votianlt/model/User.java +++ b/backend/src/main/java/de/assecutor/votianlt/model/User.java @@ -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"); } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java index ba29a0d..1b349d2 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -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 gridRows = new ArrayList<>(); private Grid 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); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java index ac43ea4..bd08a74 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -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'); " + " } " diff --git a/backend/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java b/backend/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java index acca170..aadb731 100644 --- a/backend/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java +++ b/backend/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java @@ -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("
")) { // Multi-line text: render without nowrap so
tags work htmlBuilder.append("").append(text).append(""); @@ -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("
"); @@ -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> servicesData = new java.util.ArrayList<>(); @@ -822,7 +838,9 @@ public class CustomerInvoiceService { // Header row html.append(""); html.append( - "Name"); + "Name"); + html.append( + "Steuersatz"); html.append( "Nettobetrag"); html.append(""); @@ -832,7 +850,7 @@ public class CustomerInvoiceService { // Fallback: show a single row with no data html.append(""); html.append( - "Keine Leistungen vorhanden"); + "Keine Leistungen vorhanden"); html.append(""); } 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(""); html.append( - "") + "") .append(escapeHtml(name)).append(""); + html.append("") + .append(escapeHtml(vatRateLabel)).append(""); html.append("") .append(netAmount).append(" €"); html.append(""); @@ -865,6 +885,15 @@ public class CustomerInvoiceService { .append(netTotal).append(""); html.append(""); + // Umsatzsteuer + html.append(""); + html.append(""); + html.append("zzgl. ") + .append(escapeHtml(vatRateLabel)).append(" USt:"); + html.append("") + .append(vatTotal).append(""); + html.append(""); + // Gesamtsumme html.append(""); html.append(""); diff --git a/backend/src/main/resources/messages_de.properties b/backend/src/main/resources/messages_de.properties index 49d2a1c..6a81022 100644 --- a/backend/src/main/resources/messages_de.properties +++ b/backend/src/main/resources/messages_de.properties @@ -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 diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties index 8b3b096..98ddafa 100644 --- a/backend/src/main/resources/messages_en.properties +++ b/backend/src/main/resources/messages_en.properties @@ -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 diff --git a/backend/src/main/resources/messages_lt.properties b/backend/src/main/resources/messages_lt.properties index 3be4689..8485195 100644 --- a/backend/src/main/resources/messages_lt.properties +++ b/backend/src/main/resources/messages_lt.properties @@ -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