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
This commit is contained in:
Binary file not shown.
@@ -346,11 +346,14 @@ window.initProfileInvoiceGenerator = function() {
|
|||||||
// Nettobetrag column header (right-aligned)
|
// Nettobetrag column header (right-aligned)
|
||||||
ctx.fillText('Nettobetrag', colNetX + colNetWidth - padding, y + rowHeight / 2);
|
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)
|
// Sample data rows (placeholder)
|
||||||
var sampleData = [
|
var sampleData = [
|
||||||
{ name: 'Umzugsleistung inkl. Verpackung', vat: '19%', net: '450,00 €' },
|
{ name: 'Umzugsleistung inkl. Verpackung', vat: vatPctLabel, net: '450,00 €' },
|
||||||
{ name: 'Entsorgung Möbel', vat: '19%', net: '85,00 €' },
|
{ name: 'Entsorgung Möbel', vat: vatPctLabel, net: '85,00 €' },
|
||||||
{ name: 'Montage/De-Montage', vat: '19%', net: '120,00 €' }
|
{ name: 'Montage/De-Montage', vat: vatPctLabel, net: '120,00 €' }
|
||||||
];
|
];
|
||||||
|
|
||||||
var currentY = y + rowHeight;
|
var currentY = y + rowHeight;
|
||||||
@@ -415,9 +418,8 @@ window.initProfileInvoiceGenerator = function() {
|
|||||||
|
|
||||||
// Calculate totals from sample data
|
// Calculate totals from sample data
|
||||||
var netTotal = 655.00; // 450 + 85 + 120
|
var netTotal = 655.00; // 450 + 85 + 120
|
||||||
var vatRate = 0.19;
|
var vatTotal = netTotal * vatRate;
|
||||||
var vatTotal = 124.45; // 655 * 0.19
|
var grossTotal = netTotal + vatTotal;
|
||||||
var grossTotal = 779.45; // 655 + 124.45
|
|
||||||
|
|
||||||
// Draw summary lines
|
// Draw summary lines
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
@@ -435,7 +437,7 @@ window.initProfileInvoiceGenerator = function() {
|
|||||||
// Umsatzsteuer - label left, value right
|
// Umsatzsteuer - label left, value right
|
||||||
ctx.font = fontSize + 'px Arial';
|
ctx.font = fontSize + 'px Arial';
|
||||||
ctx.textAlign = 'left';
|
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.font = 'bold ' + fontSize + 'px Arial';
|
||||||
ctx.textAlign = 'right';
|
ctx.textAlign = 'right';
|
||||||
ctx.fillText(vatTotal.toFixed(2).replace('.', ',') + ' €', valueX, summaryY + summaryRowHeight / 2);
|
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) {
|
window.updateProfileMasterdataValue = function(key, value) {
|
||||||
if (!window.masterdataValues) window.masterdataValues = {};
|
if (!window.masterdataValues) window.masterdataValues = {};
|
||||||
window.masterdataValues[key] = value;
|
window.masterdataValues[key] = value;
|
||||||
|
|||||||
@@ -1068,6 +1068,7 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background: rgba(255, 255, 255, 0.84);
|
background: rgba(255, 255, 255, 0.84);
|
||||||
box-shadow: var(--app-shadow-sm);
|
box-shadow: var(--app-shadow-sm);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.route-card,
|
.route-card,
|
||||||
@@ -1095,6 +1096,7 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
box-shadow: var(--app-shadow-sm);
|
box-shadow: var(--app-shadow-sm);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-card,
|
.detail-card,
|
||||||
|
|||||||
@@ -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.mapping.Field;
|
||||||
import org.springframework.data.mongodb.core.index.Indexed;
|
import org.springframework.data.mongodb.core.index.Indexed;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -68,4 +69,7 @@ public class User {
|
|||||||
// Spracheinstellung (standardmäßig Deutsch)
|
// Spracheinstellung (standardmäßig Deutsch)
|
||||||
@Field("language")
|
@Field("language")
|
||||||
private Language language = Language.DE;
|
private Language language = Language.DE;
|
||||||
|
|
||||||
|
// Umsatzsteuer-Satz (als Dezimalwert, z.B. 0.19 für 19 %)
|
||||||
|
private BigDecimal vatRate = new BigDecimal("0.19");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.vaadin.flow.component.html.Span;
|
|||||||
import com.vaadin.flow.component.notification.Notification;
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
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.HasDynamicTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
@@ -68,6 +69,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
private List<ServiceRow> gridRows = new ArrayList<>();
|
private List<ServiceRow> gridRows = new ArrayList<>();
|
||||||
private Grid<ServiceRow> servicesGrid;
|
private Grid<ServiceRow> servicesGrid;
|
||||||
private Div servicesSection;
|
private Div servicesSection;
|
||||||
|
private Div summarySection;
|
||||||
|
private NumberField vatField;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class to represent a row in the services grid
|
* Helper class to represent a row in the services grid
|
||||||
@@ -176,6 +179,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentUser = securityService.getAuthenticatedUser()
|
||||||
|
.flatMap(auth -> userRepository.findByEmail(auth.getUsername())).orElse(null);
|
||||||
|
|
||||||
createInvoiceView();
|
createInvoiceView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,8 +209,12 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
Div servicesSection = createServicesSelectionSection();
|
Div servicesSection = createServicesSelectionSection();
|
||||||
add(servicesSection);
|
add(servicesSection);
|
||||||
|
|
||||||
|
// VAT Section (must exist before summary so effectiveVatRate() can read the field)
|
||||||
|
Div vatSection = createVatSection();
|
||||||
|
add(vatSection);
|
||||||
|
|
||||||
// Summary Section
|
// Summary Section
|
||||||
Div summarySection = createSummarySection();
|
summarySection = createSummarySection();
|
||||||
add(summarySection);
|
add(summarySection);
|
||||||
|
|
||||||
// Create Invoice Button
|
// Create Invoice Button
|
||||||
@@ -336,13 +346,16 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
section.setWidthFull();
|
section.setWidthFull();
|
||||||
section.addClassName("invoice-section-card");
|
section.addClassName("invoice-section-card");
|
||||||
section.getStyle().set("margin-bottom", "var(--lumo-space-m)");
|
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"));
|
H3 sectionTitle = new H3(getTranslation("createinvoice.section.summary"));
|
||||||
section.add(sectionTitle);
|
section.add(sectionTitle);
|
||||||
|
|
||||||
// Calculate totals
|
|
||||||
BigDecimal netAmount = calculateNetAmount();
|
BigDecimal netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
BigDecimal vatRate = effectiveVatRate();
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
@@ -355,9 +368,40 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
totalAmount.setScale(2, RoundingMode.HALF_UP) + " €", true));
|
totalAmount.setScale(2, RoundingMode.HALF_UP) + " €", true));
|
||||||
|
|
||||||
section.add(priceTable);
|
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;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refreshSummarySection() {
|
||||||
|
if (summarySection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
summarySection.removeAll();
|
||||||
|
populateSummarySection(summarySection);
|
||||||
|
}
|
||||||
|
|
||||||
private Div createPriceRow(String label, String value, boolean bold) {
|
private Div createPriceRow(String label, String value, boolean bold) {
|
||||||
Div row = new Div();
|
Div row = new Div();
|
||||||
row.addClassName("price-row");
|
row.addClassName("price-row");
|
||||||
@@ -427,6 +471,17 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
return units;
|
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() {
|
private BigDecimal calculateNetAmount() {
|
||||||
BigDecimal total = BigDecimal.ZERO;
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
|
||||||
@@ -514,7 +569,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
|
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
|
||||||
|
|
||||||
BigDecimal netAmount = calculateNetAmount();
|
BigDecimal netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
BigDecimal vatRate = effectiveVatRate();
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
@@ -553,7 +608,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
throws Exception {
|
throws Exception {
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
BigDecimal netAmount = calculateNetAmount();
|
BigDecimal netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
BigDecimal vatRate = effectiveVatRate();
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
private final InvoiceTemplateService invoiceTemplateService;
|
private final InvoiceTemplateService invoiceTemplateService;
|
||||||
private UserInvoiceData currentInvoiceData;
|
private UserInvoiceData currentInvoiceData;
|
||||||
private Checkbox billingEnabled;
|
private Checkbox billingEnabled;
|
||||||
|
private NumberField vatRateField;
|
||||||
private VerticalLayout propertiesPanelProfile;
|
private VerticalLayout propertiesPanelProfile;
|
||||||
|
|
||||||
private final ServiceRepository serviceRepository;
|
private final ServiceRepository serviceRepository;
|
||||||
@@ -330,10 +331,9 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
termsTextArea = new TextArea();
|
termsTextArea = new TextArea();
|
||||||
pdfFrame = new IFrame();
|
pdfFrame = new IFrame();
|
||||||
|
|
||||||
// Nur die Checkbox "Rechnungslegung über votianLT"
|
// Checkbox "Rechnungslegung über votianLT"
|
||||||
billingEnabled = new Checkbox(getTranslation("profile.billing.enabled"));
|
billingEnabled = new Checkbox(getTranslation("profile.billing.enabled"));
|
||||||
billingEnabled.setValue(true); // Standardmäßig aktiviert
|
billingEnabled.setValue(true); // Standardmäßig aktiviert
|
||||||
billingTab.add(billingEnabled);
|
|
||||||
|
|
||||||
prefixField.setLabel(getTranslation("profile.billing.prefix"));
|
prefixField.setLabel(getTranslation("profile.billing.prefix"));
|
||||||
prefixField.setPlaceholder("z.B. RE-2024-");
|
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', '"
|
"if (window.updateProfileMasterdataValue) { window.updateProfileMasterdataValue('masterdata.invoice_number', '"
|
||||||
+ invNr.replace("'", "\\'") + "'); }");
|
+ 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)
|
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
||||||
final HorizontalLayout mainLayout = new HorizontalLayout();
|
final HorizontalLayout mainLayout = new HorizontalLayout();
|
||||||
@@ -451,6 +474,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
billingEnabled.addValueChangeListener(e -> {
|
billingEnabled.addValueChangeListener(e -> {
|
||||||
boolean visible = e.getValue();
|
boolean visible = e.getValue();
|
||||||
prefixField.setVisible(visible);
|
prefixField.setVisible(visible);
|
||||||
|
vatRateField.setVisible(visible);
|
||||||
mainLayout.setVisible(visible);
|
mainLayout.setVisible(visible);
|
||||||
actionLayout.setVisible(visible);
|
actionLayout.setVisible(visible);
|
||||||
});
|
});
|
||||||
@@ -843,6 +867,17 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
&& !houseNumberField.isInvalid() && !zipField.isInvalid() && !cityField.isInvalid();
|
&& !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
|
// Methoden für den Rechnungsgenerator im Profil
|
||||||
private void generatePreviewPdfFromProfile() {
|
private void generatePreviewPdfFromProfile() {
|
||||||
try {
|
try {
|
||||||
@@ -862,8 +897,9 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
} else {
|
} else {
|
||||||
templateData = result.toString();
|
templateData = result.toString();
|
||||||
}
|
}
|
||||||
|
BigDecimal previewVatRate = getPreviewVatRate();
|
||||||
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData,
|
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData,
|
||||||
currentUser, prefixField.getValue());
|
currentUser, prefixField.getValue(), previewVatRate);
|
||||||
showPdfInDialog(pdfBytes);
|
showPdfInDialog(pdfBytes);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Notification.show(getTranslation("profile.invoice.pdf.preview.error", ex.getMessage()),
|
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("'", "\\'")
|
+ city.replace("'", "\\'") + "'," + "'masterdata.email': '" + email.replace("'", "\\'")
|
||||||
+ "'," + "'masterdata.phone': '" + phone.replace("'", "\\'") + "',"
|
+ "'," + "'masterdata.phone': '" + phone.replace("'", "\\'") + "',"
|
||||||
+ "'masterdata.invoice_number': '" + invoiceNumber.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() { "
|
getElement().executeJs("setTimeout(function() { "
|
||||||
+ " if (window.loadProfileTemplate && document.getElementById('invoice-canvas-container-profile')) { "
|
+ " 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 + "'); "
|
+ masterdataJson + "; " + " var templateData = JSON.parse('" + escapedJson + "'); "
|
||||||
+ " window.loadProfileTemplate(templateData); " + " } else { "
|
+ " window.loadProfileTemplate(templateData); " + " } else { "
|
||||||
+ " console.error('loadProfileTemplate or canvas not available'); " + " } "
|
+ " console.error('loadProfileTemplate or canvas not available'); " + " } "
|
||||||
|
|||||||
@@ -258,6 +258,13 @@ public class CustomerInvoiceService {
|
|||||||
|
|
||||||
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
|
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
|
||||||
String invoicePrefix) throws Exception {
|
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
|
// Parse the JSON template data
|
||||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData);
|
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData);
|
||||||
@@ -458,7 +465,7 @@ public class CustomerInvoiceService {
|
|||||||
}
|
}
|
||||||
} else if ("services.list".equals(variable)) {
|
} else if ("services.list".equals(variable)) {
|
||||||
// Render services list as a table
|
// Render services list as a table
|
||||||
htmlBuilder.append(generateServicesTableHtml(mmWidth));
|
htmlBuilder.append(generateServicesTableHtml(mmWidth, effectiveVatRate));
|
||||||
} else if (text.contains("<br>")) {
|
} else if (text.contains("<br>")) {
|
||||||
// Multi-line text: render without nowrap so <br> tags work
|
// Multi-line text: render without nowrap so <br> tags work
|
||||||
htmlBuilder.append("<span>").append(text).append("</span>");
|
htmlBuilder.append("<span>").append(text).append("</span>");
|
||||||
@@ -484,16 +491,23 @@ public class CustomerInvoiceService {
|
|||||||
/**
|
/**
|
||||||
* Generate HTML table for services list with summary section below.
|
* 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();
|
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)
|
// Sample data for preview (will be replaced with actual job data later)
|
||||||
String[][] sampleData = { { "Umzugsleistung inkl. Verpackung", "19%", "450,00 €" },
|
String[][] sampleData = { { "Umzugsleistung inkl. Verpackung", vatLabel, "450,00 €" },
|
||||||
{ "Entsorgung Möbel", "19%", "85,00 €" }, { "Montage/De-Montage", "19%", "120,00 €" } };
|
{ "Entsorgung Möbel", vatLabel, "85,00 €" }, { "Montage/De-Montage", vatLabel, "120,00 €" } };
|
||||||
|
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
double netTotal = 655.00;
|
double netTotal = 655.00;
|
||||||
double grossTotal = 779.45;
|
double grossTotal = netTotal + (netTotal * vatRate.doubleValue());
|
||||||
|
|
||||||
// Wrapper div
|
// Wrapper div
|
||||||
html.append("<div style='width:100%;box-sizing:border-box;'>");
|
html.append("<div style='width:100%;box-sizing:border-box;'>");
|
||||||
@@ -797,7 +811,9 @@ public class CustomerInvoiceService {
|
|||||||
|
|
||||||
// Get invoice data from variables
|
// Get invoice data from variables
|
||||||
String netTotal = variables.getOrDefault("invoice.net_total", "0,00 €");
|
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 grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €");
|
||||||
|
String vatRateLabel = variables.getOrDefault("invoice.vat_rate", "19%");
|
||||||
|
|
||||||
// Parse services JSON from variables
|
// Parse services JSON from variables
|
||||||
java.util.List<java.util.Map<String, String>> servicesData = new java.util.ArrayList<>();
|
java.util.List<java.util.Map<String, String>> servicesData = new java.util.ArrayList<>();
|
||||||
@@ -822,7 +838,9 @@ public class CustomerInvoiceService {
|
|||||||
// Header row
|
// Header row
|
||||||
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
|
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
|
||||||
html.append(
|
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(
|
html.append(
|
||||||
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
|
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
@@ -832,7 +850,7 @@ public class CustomerInvoiceService {
|
|||||||
// Fallback: show a single row with no data
|
// Fallback: show a single row with no data
|
||||||
html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
|
html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
|
||||||
html.append(
|
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>");
|
html.append("</tr>");
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < servicesData.size(); i++) {
|
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);" : "";
|
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("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
|
||||||
html.append(
|
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>");
|
.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%;'>")
|
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;width:25%;'>")
|
||||||
.append(netAmount).append(" €</td>");
|
.append(netAmount).append(" €</td>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
@@ -865,6 +885,15 @@ public class CustomerInvoiceService {
|
|||||||
.append(netTotal).append("</td>");
|
.append(netTotal).append("</td>");
|
||||||
html.append("</tr>");
|
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
|
// Gesamtsumme
|
||||||
html.append("<tr>");
|
html.append("<tr>");
|
||||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ profile.settings.digitalprocess=Digitale Abwicklung
|
|||||||
profile.settings.digitalprocess.info=Aufträge werden digital über die App abgewickelt
|
profile.settings.digitalprocess.info=Aufträge werden digital über die App abgewickelt
|
||||||
profile.settings.locateappuser=App-Nutzer orten
|
profile.settings.locateappuser=App-Nutzer orten
|
||||||
profile.settings.locateappuser.info=Standort der App-Nutzer wird regelmäßig übertragen
|
profile.settings.locateappuser.info=Standort der App-Nutzer wird regelmäßig übertragen
|
||||||
|
profile.settings.vatrate=Umsatzsteuer
|
||||||
profile.account=Konto
|
profile.account=Konto
|
||||||
profile.security=Sicherheit
|
profile.security=Sicherheit
|
||||||
profile.security.twofactor=Zwei-Faktor-Authentifizierung
|
profile.security.twofactor=Zwei-Faktor-Authentifizierung
|
||||||
@@ -662,6 +663,8 @@ createinvoice.section.job=Auftragsdetails
|
|||||||
createinvoice.section.route=Streckeninfo
|
createinvoice.section.route=Streckeninfo
|
||||||
createinvoice.section.services=Leistungen
|
createinvoice.section.services=Leistungen
|
||||||
createinvoice.section.summary=Zusammenfassung
|
createinvoice.section.summary=Zusammenfassung
|
||||||
|
createinvoice.section.vat=Umsatzsteuer
|
||||||
|
createinvoice.field.vatrate=USt-Satz
|
||||||
createinvoice.field.jobnumber=Auftragsnummer
|
createinvoice.field.jobnumber=Auftragsnummer
|
||||||
createinvoice.field.customer=Kunde
|
createinvoice.field.customer=Kunde
|
||||||
createinvoice.field.status=Status
|
createinvoice.field.status=Status
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ profile.settings.digitalprocess=Digital Processing
|
|||||||
profile.settings.digitalprocess.info=Jobs are processed digitally via the app
|
profile.settings.digitalprocess.info=Jobs are processed digitally via the app
|
||||||
profile.settings.locateappuser=Locate App Users
|
profile.settings.locateappuser=Locate App Users
|
||||||
profile.settings.locateappuser.info=App user location is transmitted regularly
|
profile.settings.locateappuser.info=App user location is transmitted regularly
|
||||||
|
profile.settings.vatrate=VAT rate
|
||||||
profile.account=Account
|
profile.account=Account
|
||||||
profile.security=Security
|
profile.security=Security
|
||||||
profile.security.twofactor=Two-Factor Authentication
|
profile.security.twofactor=Two-Factor Authentication
|
||||||
@@ -662,6 +663,8 @@ createinvoice.section.job=Job Details
|
|||||||
createinvoice.section.route=Route Info
|
createinvoice.section.route=Route Info
|
||||||
createinvoice.section.services=Services
|
createinvoice.section.services=Services
|
||||||
createinvoice.section.summary=Summary
|
createinvoice.section.summary=Summary
|
||||||
|
createinvoice.section.vat=VAT
|
||||||
|
createinvoice.field.vatrate=VAT rate
|
||||||
createinvoice.field.jobnumber=Job Number
|
createinvoice.field.jobnumber=Job Number
|
||||||
createinvoice.field.customer=Customer
|
createinvoice.field.customer=Customer
|
||||||
createinvoice.field.status=Status
|
createinvoice.field.status=Status
|
||||||
|
|||||||
@@ -662,6 +662,8 @@ createinvoice.section.job=Užsakymo informacija
|
|||||||
createinvoice.section.route=Maršruto informacija
|
createinvoice.section.route=Maršruto informacija
|
||||||
createinvoice.section.services=Paslaugos
|
createinvoice.section.services=Paslaugos
|
||||||
createinvoice.section.summary=Santrauka
|
createinvoice.section.summary=Santrauka
|
||||||
|
createinvoice.section.vat=PVM
|
||||||
|
createinvoice.field.vatrate=PVM tarifas
|
||||||
createinvoice.field.jobnumber=Užsakymo numeris
|
createinvoice.field.jobnumber=Užsakymo numeris
|
||||||
createinvoice.field.customer=Klientas
|
createinvoice.field.customer=Klientas
|
||||||
createinvoice.field.status=Būsena
|
createinvoice.field.status=Būsena
|
||||||
|
|||||||
Reference in New Issue
Block a user