feat: E-Rechnungs-Vorschau mit Versand, USt-IdNr.-Validierung und Icon-Buttons

- Rechnungsvorschau (CreateInvoiceView): ZUGFeRD-ZIP wird schon vor der
  Vorschau über das PDF Tool erzeugt und das signierte PDF daraus angezeigt;
  "Speichern" heißt jetzt "Senden" und versendet die Rechnung nach dem
  Speichern als E-Rechnung per E-Mail (Status "Versendet")
- USt-IdNr.-Formatvalidierung (VatIdUtil, EU-Länderformate) in Profil-
  Rechnungsdaten sowie Kunde anlegen/bearbeiten, mit Unit-Tests
- Rechnungsliste: Aktions-Buttons durch Icon-Buttons mit Tooltip ersetzt
- Jobs-Seite: CSV-Export als Icon-Button, Filter greifen automatisch,
  "Filter anwenden"-Button entfernt
- Neue Übersetzungsschlüssel in allen 10 Sprachdateien

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 11:06:21 +02:00
co-authored by Claude Fable 5
parent 1741ccc67a
commit 5169527f1f
18 changed files with 398 additions and 65 deletions
@@ -63,7 +63,7 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
ustId.setRequiredIndicatorVisible(true); ustId.setRequiredIndicatorVisible(true);
ustId.setPlaceholder("DE123456789"); ustId.setPlaceholder("DE123456789");
ustId.setWidthFull(); ustId.setWidthFull();
ustId.addBlurListener(e -> validateField(ustId)); ustId.addBlurListener(e -> validateUstId());
// Anrede (Dropdown) // Anrede (Dropdown)
title = new ComboBox<>(getTranslation("addjob.address.salutation")); title = new ComboBox<>(getTranslation("addjob.address.salutation"));
@@ -184,8 +184,10 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
binder.forField(taxNumber).asRequired(getTranslation("profile.validation.taxnumber.required")) binder.forField(taxNumber).asRequired(getTranslation("profile.validation.taxnumber.required"))
.bind(Customer::getTaxNumber, Customer::setTaxNumber); .bind(Customer::getTaxNumber, Customer::setTaxNumber);
binder.forField(ustId).asRequired(getTranslation("profile.validation.ustid.required")).bind(Customer::getUstId, binder.forField(ustId).asRequired(getTranslation("profile.validation.ustid.required"))
Customer::setUstId); .withValidator(de.assecutor.votianlt.util.VatIdUtil::isValid,
getTranslation("profile.validation.ustid.invalid"))
.bind(Customer::getUstId, Customer::setUstId);
binder.forField(title).bind(Customer::getTitle, Customer::setTitle); binder.forField(title).bind(Customer::getTitle, Customer::setTitle);
@@ -262,6 +264,24 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
} }
} }
/**
* Prüft die USt-IdNr. auf Pflichtangabe und gültiges Format
* (Länderkennzeichen + länderspezifischer Aufbau, z. B. DE123456789).
*/
private void validateUstId() {
String value = ustId.getValue();
if (value == null || value.trim().isEmpty()) {
ustId.setInvalid(true);
ustId.setErrorMessage(getTranslation("profile.validation.ustid.required"));
} else if (!de.assecutor.votianlt.util.VatIdUtil.isValid(value)) {
ustId.setInvalid(true);
ustId.setErrorMessage(getTranslation("profile.validation.ustid.invalid"));
} else {
ustId.setInvalid(false);
ustId.setErrorMessage("");
}
}
private void validateField(TextField field) { private void validateField(TextField field) {
String value = field.getValue(); String value = field.getValue();
if (value == null || value.trim().isEmpty()) { if (value == null || value.trim().isEmpty()) {
@@ -286,7 +306,7 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
private boolean validateAllFields() { private boolean validateAllFields() {
validateField(taxNumber); validateField(taxNumber);
validateField(ustId); validateUstId();
validateField(firstName); validateField(firstName);
validateField(lastName); validateField(lastName);
validateField(telephone); validateField(telephone);
@@ -33,9 +33,12 @@ import de.assecutor.votianlt.repository.ServiceRepository;
import de.assecutor.votianlt.repository.UserRepository; import de.assecutor.votianlt.repository.UserRepository;
import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.CustomerInvoiceService; import de.assecutor.votianlt.service.CustomerInvoiceService;
import de.assecutor.votianlt.service.EmailService;
import de.assecutor.votianlt.service.InvoiceLifecycleException; import de.assecutor.votianlt.service.InvoiceLifecycleException;
import de.assecutor.votianlt.service.InvoiceLifecycleService; import de.assecutor.votianlt.service.InvoiceLifecycleService;
import de.assecutor.votianlt.service.InvoiceTemplateService; import de.assecutor.votianlt.service.InvoiceTemplateService;
import de.assecutor.votianlt.service.PdfToolService;
import de.assecutor.votianlt.service.UserInvoiceZugferdService;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
@@ -66,6 +69,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private final UserInvoiceDataService userInvoiceDataService; private final UserInvoiceDataService userInvoiceDataService;
private final CustomerService customerService; private final CustomerService customerService;
private final InvoiceLifecycleService invoiceLifecycleService; private final InvoiceLifecycleService invoiceLifecycleService;
private final UserInvoiceZugferdService userInvoiceZugferdService;
private final EmailService emailService;
private User currentUser; private User currentUser;
private Job currentJob; private Job currentJob;
private List<ServiceRow> gridRows = new ArrayList<>(); private List<ServiceRow> gridRows = new ArrayList<>();
@@ -120,7 +125,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
UserRepository userRepository, CustomerInvoiceService customerInvoiceService, UserRepository userRepository, CustomerInvoiceService customerInvoiceService,
InvoiceTemplateService invoiceTemplateService, SecurityService securityService, InvoiceTemplateService invoiceTemplateService, SecurityService securityService,
UserInvoiceDataService userInvoiceDataService, CustomerService customerService, UserInvoiceDataService userInvoiceDataService, CustomerService customerService,
InvoiceLifecycleService invoiceLifecycleService) { InvoiceLifecycleService invoiceLifecycleService, UserInvoiceZugferdService userInvoiceZugferdService,
EmailService emailService) {
this.jobRepository = jobRepository; this.jobRepository = jobRepository;
this.serviceRepository = serviceRepository; this.serviceRepository = serviceRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
@@ -130,6 +136,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
this.userInvoiceDataService = userInvoiceDataService; this.userInvoiceDataService = userInvoiceDataService;
this.customerService = customerService; this.customerService = customerService;
this.invoiceLifecycleService = invoiceLifecycleService; this.invoiceLifecycleService = invoiceLifecycleService;
this.userInvoiceZugferdService = userInvoiceZugferdService;
this.emailService = emailService;
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
setSpacing(true); setSpacing(true);
@@ -557,7 +565,19 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser, byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser,
getTranslation("createinvoice.preview.number")); getTranslation("createinvoice.preview.number"));
showPdfPreviewDialog(pdfBytes, templateData, currentUser); // Schon für die Vorschau das ZUGFeRD-ZIP über das PDF Tool erzeugen
// und das signierte PDF aus dem ZIP anzeigen — so sieht der Nutzer
// exakt das Dokument, das später versendet wird.
CustomerInvoice previewInvoice = buildInvoice(getTranslation("createinvoice.preview.number"), pdfBytes,
currentUser);
PdfToolService.SignedInvoice signed = userInvoiceZugferdService.sign(previewInvoice);
if (!signed.valid()) {
Notification.show(getTranslation("invoices.notification.zugferd.invalid"), 7000,
Notification.Position.MIDDLE);
}
byte[] signedPdf = extractPdfFromZip(signed.zip());
showPdfPreviewDialog(signedPdf != null ? signedPdf : pdfBytes, templateData, currentUser);
} catch (Exception ex) { } catch (Exception ex) {
log.error("Fehler beim Erstellen der Rechnungsvorschau", ex); log.error("Fehler beim Erstellen der Rechnungsvorschau", ex);
@@ -566,17 +586,18 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
} }
} }
private void saveInvoice(String templateData, User user, Dialog previewDialog) { /**
try { * Baut die (noch nicht persistierte) Rechnung mit allen Pflichtangaben nach
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId()); * §14 UStG auf — auch Grundlage der ZUGFeRD-Metadaten für die E-Rechnung.
* Wird sowohl für die Vorschau (Platzhalter-Nummer, transient) als auch für
* das endgültige Speichern verwendet.
*/
private CustomerInvoice buildInvoice(String invoiceNumber, byte[] pdfBytes, User user) {
BigDecimal netAmount = calculateNetAmount(); BigDecimal netAmount = calculateNetAmount();
BigDecimal vatRate = effectiveVatRate(); BigDecimal vatRate = effectiveVatRate();
BigDecimal vatAmount = netAmount.multiply(vatRate); BigDecimal vatAmount = netAmount.multiply(vatRate);
BigDecimal totalAmount = netAmount.add(vatAmount); BigDecimal totalAmount = netAmount.add(vatAmount);
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, user, invoiceNumber);
CustomerInvoice invoice = new CustomerInvoice(); CustomerInvoice invoice = new CustomerInvoice();
invoice.setInvoiceNumber(invoiceNumber); invoice.setInvoiceNumber(invoiceNumber);
invoice.setInvoiceDate(java.time.LocalDate.now()); invoice.setInvoiceDate(java.time.LocalDate.now());
@@ -587,8 +608,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
invoice.setVatAmount(vatAmount); invoice.setVatAmount(vatAmount);
invoice.setTotalAmount(totalAmount); invoice.setTotalAmount(totalAmount);
// Pflichtangaben nach §14 UStG: Leistungsdatum, Absender, Empfänger und
// Positionen — auch Grundlage der ZUGFeRD-Metadaten für die E-Rechnung.
invoice.setDeliveryDate(currentJob.getDeliveryDate() != null ? currentJob.getDeliveryDate() invoice.setDeliveryDate(currentJob.getDeliveryDate() != null ? currentJob.getDeliveryDate()
: java.time.LocalDate.now()); : java.time.LocalDate.now());
applySenderData(invoice, user); applySenderData(invoice, user);
@@ -596,6 +615,18 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
invoice.setItems(buildInvoiceItems(vatRate)); invoice.setItems(buildInvoiceItems(vatRate));
invoice.setPdfData(pdfBytes); invoice.setPdfData(pdfBytes);
return invoice;
}
/**
* Speichert die Rechnung endgültig und versendet sie anschließend als
* signierte ZUGFeRD-E-Rechnung (ZIP) per E-Mail an den Kunden.
*/
private void saveAndSendInvoice(String templateData, User user, Dialog previewDialog) {
try {
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, user, invoiceNumber);
CustomerInvoice invoice = buildInvoice(invoiceNumber, pdfBytes, user);
// Finalisierung mit Audit-Eintrag und Eindeutigkeitsprüfung der Rechnungsnummer (R-07/R-11/R-36). // Finalisierung mit Audit-Eintrag und Eindeutigkeitsprüfung der Rechnungsnummer (R-07/R-11/R-36).
CustomerInvoice savedInvoice = invoiceLifecycleService.createAndIssue(invoice, CustomerInvoice savedInvoice = invoiceLifecycleService.createAndIssue(invoice,
@@ -608,6 +639,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
Notification.show(getTranslation("createinvoice.notification.saved", invoiceNumber), 4000, Notification.show(getTranslation("createinvoice.notification.saved", invoiceNumber), 4000,
Notification.Position.BOTTOM_END); Notification.Position.BOTTOM_END);
sendInvoiceEmail(savedInvoice);
} catch (InvoiceLifecycleException lifecycleEx) { } catch (InvoiceLifecycleException lifecycleEx) {
log.warn("Lifecycle-Verstoß beim Speichern der Rechnung: {}", lifecycleEx.getMessage()); log.warn("Lifecycle-Verstoß beim Speichern der Rechnung: {}", lifecycleEx.getMessage());
Notification.show(lifecycleEx.getMessage(), 5000, Notification.Position.MIDDLE); Notification.show(lifecycleEx.getMessage(), 5000, Notification.Position.MIDDLE);
@@ -618,6 +651,66 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
} }
} }
/**
* Versendet die gespeicherte Rechnung als E-Rechnungs-ZIP per E-Mail.
* Besteht die Rechnung die Validierung nicht, wird der Versand abgebrochen —
* die Rechnung bleibt gespeichert und kann aus der Rechnungsliste erneut
* versendet werden.
*/
private void sendInvoiceEmail(CustomerInvoice savedInvoice) {
String recipientEmail = savedInvoice.getRecipientEmail();
if (recipientEmail == null || recipientEmail.isBlank()) {
Notification.show(getTranslation("invoices.notification.email.missing"), 6000,
Notification.Position.MIDDLE);
return;
}
try {
PdfToolService.SignedInvoice signed = userInvoiceZugferdService.sign(savedInvoice);
if (!signed.valid()) {
log.warn("Versand der Rechnung {} abgebrochen, ZUGFeRD-Validierung fehlgeschlagen",
savedInvoice.getInvoiceNumber());
Notification.show(
getTranslation("invoices.notification.zugferd.invalidsend", savedInvoice.getInvoiceNumber()),
7000, Notification.Position.MIDDLE);
return;
}
emailService.sendEmailWithAttachment(recipientEmail,
getTranslation("invoices.mail.subject", savedInvoice.getInvoiceNumber()),
getTranslation("invoices.mail.body", savedInvoice.getInvoiceNumber(),
safe(savedInvoice.getSenderName())),
signed.zip(), signed.fileName());
invoiceLifecycleService.markAsSent(savedInvoice.getId(),
"Als E-Rechnung per E-Mail versendet an " + recipientEmail);
Notification.show(
getTranslation("createinvoice.notification.sent", savedInvoice.getInvoiceNumber(), recipientEmail),
5000, Notification.Position.BOTTOM_END);
} catch (Exception ex) {
log.error("Fehler beim Versand der Rechnung {} an {}: {}", savedInvoice.getInvoiceNumber(), recipientEmail,
ex.getMessage(), ex);
Notification.show(getTranslation("invoices.notification.zugferd.error", ex.getMessage()), 7000,
Notification.Position.MIDDLE);
}
}
/**
* Extrahiert das signierte PDF (PDF/A-3) aus dem vom PDF Tool gelieferten
* ZIP; {@code null}, wenn kein PDF-Eintrag enthalten ist.
*/
private byte[] extractPdfFromZip(byte[] zipBytes) throws java.io.IOException {
try (java.util.zip.ZipInputStream zip = new java.util.zip.ZipInputStream(
new java.io.ByteArrayInputStream(zipBytes))) {
java.util.zip.ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
if (!entry.isDirectory() && entry.getName().toLowerCase().endsWith(".pdf")) {
return zip.readAllBytes();
}
}
}
return null;
}
/** /**
* Absenderdaten aus dem Nutzerprofil und den Rechnungs-Stammdaten * Absenderdaten aus dem Nutzerprofil und den Rechnungs-Stammdaten
* (USt-IdNr./Steuernummer, Bankverbindung, Zahlungsbedingungen) übernehmen. * (USt-IdNr./Steuernummer, Bankverbindung, Zahlungsbedingungen) übernehmen.
@@ -844,21 +937,21 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close()); Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close());
closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
Button saveButton = new Button(getTranslation("createinvoice.button.save"), e -> { Button sendButton = new Button(getTranslation("createinvoice.button.send"), e -> {
Dialog confirm = DialogStylingHelper.createConfirmationDialog( Dialog confirm = DialogStylingHelper.createConfirmationDialog(
getTranslation("createinvoice.confirm.save.title"), getTranslation("createinvoice.confirm.send.title"),
getTranslation("createinvoice.confirm.save.message"), getTranslation("createinvoice.confirm.send.message"),
"560px", "560px",
getTranslation("button.cancel"), getTranslation("button.cancel"),
getTranslation("createinvoice.confirm.save.confirm"), getTranslation("createinvoice.confirm.send.confirm"),
() -> saveInvoice(templateData, user, pdfDialog), () -> saveAndSendInvoice(templateData, user, pdfDialog),
ButtonVariant.LUMO_PRIMARY); ButtonVariant.LUMO_PRIMARY);
confirm.open(); confirm.open();
}); });
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS); sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
pdfDialog.add(DialogStylingHelper.wrapContent(pdfFrame, true)); pdfDialog.add(DialogStylingHelper.wrapContent(pdfFrame, true));
pdfDialog.getFooter().add(closeButton, saveButton); pdfDialog.getFooter().add(closeButton, sendButton);
pdfDialog.open(); pdfDialog.open();
} }
@@ -146,6 +146,8 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
binder.forField(taxNumberField).asRequired(getTranslation("profile.validation.taxnumber.required")) binder.forField(taxNumberField).asRequired(getTranslation("profile.validation.taxnumber.required"))
.bind(Customer::getTaxNumber, Customer::setTaxNumber); .bind(Customer::getTaxNumber, Customer::setTaxNumber);
binder.forField(ustIdField).asRequired(getTranslation("profile.validation.ustid.required")) binder.forField(ustIdField).asRequired(getTranslation("profile.validation.ustid.required"))
.withValidator(de.assecutor.votianlt.util.VatIdUtil::isValid,
getTranslation("profile.validation.ustid.invalid"))
.bind(Customer::getUstId, Customer::setUstId); .bind(Customer::getUstId, Customer::setUstId);
binder.forField(firstnameField).bind(Customer::getFirstname, Customer::setFirstname); binder.forField(firstnameField).bind(Customer::getFirstname, Customer::setFirstname);
binder.forField(lastNameField).bind(Customer::getLastName, Customer::setLastName); binder.forField(lastNameField).bind(Customer::getLastName, Customer::setLastName);
@@ -378,7 +378,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
ustIdField.setPlaceholder("DE123456789"); ustIdField.setPlaceholder("DE123456789");
ustIdField.setRequiredIndicatorVisible(true); ustIdField.setRequiredIndicatorVisible(true);
ustIdField.addBlurListener(e -> { ustIdField.addBlurListener(e -> {
validateField(ustIdField, getTranslation("profile.validation.ustid.required")); validateUstIdField();
saveInvoiceData(); saveInvoiceData();
}); });
taxNumberField.setLabel(getTranslation("profile.billing.taxnumber")); taxNumberField.setLabel(getTranslation("profile.billing.taxnumber"));
@@ -650,7 +650,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
// Steuernummer und USt-IdNr. sind Pflicht, sobald die Rechnungslegung // Steuernummer und USt-IdNr. sind Pflicht, sobald die Rechnungslegung
// aktiviert ist // aktiviert ist
if (billingEnabled.getValue()) { if (billingEnabled.getValue()) {
validateField(ustIdField, getTranslation("profile.validation.ustid.required")); validateUstIdField();
validateField(taxNumberField, getTranslation("profile.validation.taxnumber.required")); validateField(taxNumberField, getTranslation("profile.validation.taxnumber.required"));
isValid = isValid && !ustIdField.isInvalid() && !taxNumberField.isInvalid(); isValid = isValid && !ustIdField.isInvalid() && !taxNumberField.isInvalid();
} }
@@ -864,6 +864,24 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
return value != null ? value : ""; return value != null ? value : "";
} }
/**
* Prüft die USt-IdNr. auf Pflichtangabe und gültiges Format
* (Länderkennzeichen + länderspezifischer Aufbau, z. B. DE123456789).
*/
private void validateUstIdField() {
String value = ustIdField.getValue();
if (value == null || value.trim().isEmpty()) {
ustIdField.setInvalid(true);
ustIdField.setErrorMessage(getTranslation("profile.validation.ustid.required"));
} else if (!de.assecutor.votianlt.util.VatIdUtil.isValid(value)) {
ustIdField.setInvalid(true);
ustIdField.setErrorMessage(getTranslation("profile.validation.ustid.invalid"));
} else {
ustIdField.setInvalid(false);
ustIdField.setErrorMessage("");
}
}
private void validateField(TextField field, String errorMessage) { private void validateField(TextField field, String errorMessage) {
String value = field.getValue(); String value = field.getValue();
if (value == null || value.trim().isEmpty()) { if (value == null || value.trim().isEmpty()) {
@@ -4,6 +4,8 @@ import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI; import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Anchor; import com.vaadin.flow.component.html.Anchor;
@@ -186,13 +188,15 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
actions.setSpacing(true); actions.setSpacing(true);
actions.setPadding(false); actions.setPadding(false);
Button viewBtn = new Button(getTranslation("invoices.action.view"), e -> downloadInvoicePdf(invoice)); Button viewBtn = actionIconButton(VaadinIcon.EYE, "invoices.action.view",
viewBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL); e -> downloadInvoicePdf(invoice));
viewBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
viewBtn.setEnabled(invoice.getPdfData() != null && invoice.getPdfData().length > 0); viewBtn.setEnabled(invoice.getPdfData() != null && invoice.getPdfData().length > 0);
actions.add(viewBtn); actions.add(viewBtn);
Button historyBtn = new Button(getTranslation("invoices.action.history"), e -> openHistoryDialog(invoice)); Button historyBtn = actionIconButton(VaadinIcon.TIME_BACKWARD, "invoices.action.history",
historyBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL); e -> openHistoryDialog(invoice));
historyBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
actions.add(historyBtn); actions.add(historyBtn);
InvoiceStatus status = invoice.getStatus() != null ? invoice.getStatus() : InvoiceStatus.ISSUED; InvoiceStatus status = invoice.getStatus() != null ? invoice.getStatus() : InvoiceStatus.ISSUED;
@@ -205,40 +209,38 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
if (type == InvoiceType.INVOICE && status == InvoiceStatus.ISSUED if (type == InvoiceType.INVOICE && status == InvoiceStatus.ISSUED
&& invoicePermissionService.canMarkAsSent(currentUser)) { && invoicePermissionService.canMarkAsSent(currentUser)) {
Button sentBtn = new Button(getTranslation("invoices.action.marksent"), Button sentBtn = actionIconButton(VaadinIcon.CHECK, "invoices.action.marksent",
e -> markAsSent(invoice)); e -> markAsSent(invoice));
sentBtn.addThemeVariants(ButtonVariant.LUMO_SMALL);
actions.add(sentBtn); actions.add(sentBtn);
} }
// E-Rechnung (ZUGFeRD) über das externe PDF Tool — analog zu den // E-Rechnung (ZUGFeRD) über das externe PDF Tool — analog zu den
// System-Rechnungen der Admin-Seite // System-Rechnungen der Admin-Seite
if (type == InvoiceType.INVOICE && invoice.getPdfData() != null && invoice.getPdfData().length > 0) { if (type == InvoiceType.INVOICE && invoice.getPdfData() != null && invoice.getPdfData().length > 0) {
Button zugferdBtn = new Button(getTranslation("invoices.action.zugferd"), Button zugferdBtn = actionIconButton(VaadinIcon.FILE_ZIP, "invoices.action.zugferd",
e -> downloadZugferdZip(invoice)); e -> downloadZugferdZip(invoice));
zugferdBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL); zugferdBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
actions.add(zugferdBtn); actions.add(zugferdBtn);
if (isLiveInvoice && invoicePermissionService.canMarkAsSent(currentUser)) { if (isLiveInvoice && invoicePermissionService.canMarkAsSent(currentUser)) {
Button sendBtn = new Button(getTranslation("invoices.action.sendinvoice"), Button sendBtn = actionIconButton(VaadinIcon.PAPERPLANE, "invoices.action.sendinvoice",
e -> sendAsEInvoice(invoice)); e -> sendAsEInvoice(invoice));
sendBtn.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_PRIMARY); sendBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
actions.add(sendBtn); actions.add(sendBtn);
} }
} }
if (isLiveInvoice) { if (isLiveInvoice) {
if (invoicePermissionService.canCorrect(currentUser)) { if (invoicePermissionService.canCorrect(currentUser)) {
Button correctBtn = new Button(getTranslation("invoices.action.correct"), Button correctBtn = actionIconButton(VaadinIcon.EDIT, "invoices.action.correct",
e -> openCorrectionDialog(invoice)); e -> openCorrectionDialog(invoice));
correctBtn.addThemeVariants(ButtonVariant.LUMO_SMALL);
actions.add(correctBtn); actions.add(correctBtn);
} }
if (invoicePermissionService.canCancel(currentUser)) { if (invoicePermissionService.canCancel(currentUser)) {
Button cancelBtn = new Button(getTranslation("invoices.action.cancel"), Button cancelBtn = actionIconButton(VaadinIcon.BAN, "invoices.action.cancel",
e -> openCancellationDialog(invoice)); e -> openCancellationDialog(invoice));
cancelBtn.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_ERROR); cancelBtn.addThemeVariants(ButtonVariant.LUMO_ERROR);
actions.add(cancelBtn); actions.add(cancelBtn);
} }
} }
@@ -246,20 +248,33 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
// Zahlung erfassen: nur für reguläre Rechnungen (R-25) // Zahlung erfassen: nur für reguläre Rechnungen (R-25)
if (type == InvoiceType.INVOICE && status != InvoiceStatus.DRAFT if (type == InvoiceType.INVOICE && status != InvoiceStatus.DRAFT
&& invoicePermissionService.canRecordPayment(currentUser)) { && invoicePermissionService.canRecordPayment(currentUser)) {
Button payBtn = new Button(getTranslation("invoices.action.payment"), Button payBtn = actionIconButton(VaadinIcon.EURO, "invoices.action.payment",
e -> openPaymentDialog(invoice)); e -> openPaymentDialog(invoice));
payBtn.addThemeVariants(ButtonVariant.LUMO_SMALL);
actions.add(payBtn); actions.add(payBtn);
} }
// Belegpaket exportieren (R-33/R-34) // Belegpaket exportieren (R-33/R-34)
Button exportBtn = new Button(getTranslation("invoices.action.export"), e -> exportPackage(invoice)); Button exportBtn = actionIconButton(VaadinIcon.DOWNLOAD, "invoices.action.export",
exportBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL); e -> exportPackage(invoice));
exportBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
actions.add(exportBtn); actions.add(exportBtn);
return actions; return actions;
} }
/**
* Icon-Button für die Aktionsspalte: die bisherige Button-Beschriftung
* bleibt als Tooltip (Bubblehelp) und Aria-Label erhalten.
*/
private Button actionIconButton(VaadinIcon icon, String labelKey,
com.vaadin.flow.component.ComponentEventListener<com.vaadin.flow.component.ClickEvent<Button>> listener) {
Button button = new Button(new Icon(icon), listener);
button.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_ICON);
button.setTooltipText(getTranslation(labelKey));
button.setAriaLabel(getTranslation(labelKey));
return button;
}
private void openPaymentDialog(CustomerInvoice invoice) { private void openPaymentDialog(CustomerInvoice invoice) {
Dialog dialog = DialogStylingHelper.createStyledDialog( Dialog dialog = DialogStylingHelper.createStyledDialog(
getTranslation("invoices.payment.title", invoice.getInvoiceNumber()), "480px"); getTranslation("invoices.payment.title", invoice.getInvoiceNumber()), "480px");
@@ -70,8 +70,10 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
endDate.setLabel(getTranslation("jobs.filter.enddate")); endDate.setLabel(getTranslation("jobs.filter.enddate"));
searchField.setLabel(getTranslation("jobs.filter.search")); searchField.setLabel(getTranslation("jobs.filter.search"));
statusFilter.setLabel(getTranslation("jobs.filter.status")); statusFilter.setLabel(getTranslation("jobs.filter.status"));
Button exportButton = new Button(getTranslation("jobs.button.csvexport")); Button exportButton = new Button(new Icon(VaadinIcon.DOWNLOAD));
exportButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); exportButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
exportButton.setTooltipText(getTranslation("jobs.button.csvexport"));
exportButton.setAriaLabel(getTranslation("jobs.button.csvexport"));
ViewToolbar toolbar = new ViewToolbar(getTranslation("nav.jobs"), exportButton); ViewToolbar toolbar = new ViewToolbar(getTranslation("nav.jobs"), exportButton);
add(toolbar); add(toolbar);
@@ -85,9 +87,7 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
searchField.setClearButtonVisible(true); searchField.setClearButtonVisible(true);
// Filterleiste mit Export-Button am rechten Rand // Filterleiste mit Export-Button am rechten Rand
Button applyFilter = new Button(getTranslation("jobs.filter.apply")); HorizontalLayout leftFilters = new HorizontalLayout(startDate, endDate, searchField, statusFilter);
applyFilter.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
HorizontalLayout leftFilters = new HorizontalLayout(startDate, endDate, searchField, statusFilter, applyFilter);
leftFilters.setAlignItems(Alignment.END); leftFilters.setAlignItems(Alignment.END);
leftFilters.getStyle().set("flex-wrap", "wrap"); leftFilters.getStyle().set("flex-wrap", "wrap");
@@ -105,7 +105,6 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
java.time.LocalDate today = java.time.LocalDate.now(); java.time.LocalDate today = java.time.LocalDate.now();
startDate.setValue(today.minusDays(30)); startDate.setValue(today.minusDays(30));
endDate.setValue(today); endDate.setValue(today);
applyFilter.addClickListener(e -> loadData());
exportButton.addClickListener(e -> exportToCsv()); exportButton.addClickListener(e -> exportToCsv());
// Add real-time filtering // Add real-time filtering
@@ -0,0 +1,81 @@
package de.assecutor.votianlt.util;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Formatvalidierung für Umsatzsteuer-Identifikationsnummern (USt-IdNr.).
*
* Prüft den Aufbau nach Länderkennzeichen gemäß den offiziellen Formaten der
* EU-Mitgliedstaaten (z. B. DE + 9 Ziffern). Für Länderkennzeichen ohne
* hinterlegtes Muster wird ein generisches Format (2 Buchstaben + 212
* alphanumerische Zeichen) akzeptiert. Es findet keine Online-Prüfung (VIES)
* statt — validiert wird ausschließlich das Format.
*/
public final class VatIdUtil {
/** Offizielle USt-IdNr.-Formate der EU-Mitgliedstaaten (ohne Länderpräfix). */
private static final Map<String, Pattern> COUNTRY_PATTERNS = Map.ofEntries(
Map.entry("AT", Pattern.compile("U\\d{8}")),
Map.entry("BE", Pattern.compile("[01]\\d{9}")),
Map.entry("BG", Pattern.compile("\\d{9,10}")),
Map.entry("CY", Pattern.compile("\\d{8}[A-Z]")),
Map.entry("CZ", Pattern.compile("\\d{8,10}")),
Map.entry("DE", Pattern.compile("\\d{9}")),
Map.entry("DK", Pattern.compile("\\d{8}")),
Map.entry("EE", Pattern.compile("\\d{9}")),
Map.entry("EL", Pattern.compile("\\d{9}")),
Map.entry("ES", Pattern.compile("[A-Z0-9]\\d{7}[A-Z0-9]")),
Map.entry("FI", Pattern.compile("\\d{8}")),
Map.entry("FR", Pattern.compile("[A-Z0-9]{2}\\d{9}")),
Map.entry("HR", Pattern.compile("\\d{11}")),
Map.entry("HU", Pattern.compile("\\d{8}")),
Map.entry("IE", Pattern.compile("\\d{7}[A-Z]{1,2}|\\d[A-Z0-9+*]\\d{5}[A-Z]")),
Map.entry("IT", Pattern.compile("\\d{11}")),
Map.entry("LT", Pattern.compile("(\\d{9}|\\d{12})")),
Map.entry("LU", Pattern.compile("\\d{8}")),
Map.entry("LV", Pattern.compile("\\d{11}")),
Map.entry("MT", Pattern.compile("\\d{8}")),
Map.entry("NL", Pattern.compile("\\d{9}B\\d{2}")),
Map.entry("PL", Pattern.compile("\\d{10}")),
Map.entry("PT", Pattern.compile("\\d{9}")),
Map.entry("RO", Pattern.compile("\\d{2,10}")),
Map.entry("SE", Pattern.compile("\\d{12}")),
Map.entry("SI", Pattern.compile("\\d{8}")),
Map.entry("SK", Pattern.compile("\\d{10}")));
/** Rückfallmuster für Länderkennzeichen ohne hinterlegtes Format. */
private static final Pattern GENERIC_PATTERN = Pattern.compile("[A-Z]{2}[A-Z0-9]{2,12}");
private VatIdUtil() {
}
/**
* Prüft, ob der Wert eine formal gültige USt-IdNr. ist. Leerzeichen,
* Punkte und Bindestriche werden vor der Prüfung entfernt,
* Kleinbuchstaben akzeptiert. {@code null} oder leer gilt als ungültig.
*/
public static boolean isValid(String vatId) {
String normalized = normalize(vatId);
if (normalized == null || normalized.length() < 4) {
return false;
}
String countryCode = normalized.substring(0, 2);
if (!countryCode.chars().allMatch(Character::isLetter)) {
return false;
}
Pattern pattern = COUNTRY_PATTERNS.get(countryCode);
if (pattern != null) {
return pattern.matcher(normalized.substring(2)).matches();
}
return GENERIC_PATTERN.matcher(normalized).matches();
}
/** Entfernt Trennzeichen und wandelt in Großbuchstaben um. */
public static String normalize(String vatId) {
if (vatId == null) {
return null;
}
return vatId.replaceAll("[\\s.\\-]", "").toUpperCase(java.util.Locale.ROOT);
}
}
@@ -86,6 +86,7 @@ profile.validation.zip.required=Postleitzahl ist erforderlich
profile.validation.city.required=Stadt ist erforderlich profile.validation.city.required=Stadt ist erforderlich
profile.validation.taxnumber.required=Steuernummer ist erforderlich profile.validation.taxnumber.required=Steuernummer ist erforderlich
profile.validation.ustid.required=USt-IdNr. ist erforderlich profile.validation.ustid.required=USt-IdNr. ist erforderlich
profile.validation.ustid.invalid=Ungültiges Format der USt-IdNr. (z. B. DE123456789)
profile.validation.firstname.required=Vorname ist erforderlich profile.validation.firstname.required=Vorname ist erforderlich
profile.validation.lastname.required=Nachname ist erforderlich profile.validation.lastname.required=Nachname ist erforderlich
profile.validation.phone.required=Telefonnummer ist erforderlich profile.validation.phone.required=Telefonnummer ist erforderlich
@@ -699,6 +700,11 @@ createinvoice.button.save=Speichern
createinvoice.confirm.save.title=Rechnung speichern createinvoice.confirm.save.title=Rechnung speichern
createinvoice.confirm.save.message=Diese Rechnung wird dauerhaft gespeichert und kann anschlie\u00dfend nicht mehr ver\u00e4ndert werden. Fortfahren? createinvoice.confirm.save.message=Diese Rechnung wird dauerhaft gespeichert und kann anschlie\u00dfend nicht mehr ver\u00e4ndert werden. Fortfahren?
createinvoice.confirm.save.confirm=Ja, speichern createinvoice.confirm.save.confirm=Ja, speichern
createinvoice.button.send=Senden
createinvoice.confirm.send.title=Rechnung senden
createinvoice.confirm.send.message=Diese Rechnung wird dauerhaft gespeichert und als E-Rechnung (ZIP) per E-Mail an den Kunden versendet. Fortfahren?
createinvoice.confirm.send.confirm=Ja, senden
createinvoice.notification.sent=Rechnung {0} wurde als E-Rechnung an {1} versendet
# Invoices # Invoices
invoices.title=Rechnungen invoices.title=Rechnungen
@@ -75,6 +75,7 @@ profile.validation.zip.required=Postiindeks on kohustuslik
profile.validation.city.required=Linn on kohustuslik profile.validation.city.required=Linn on kohustuslik
profile.validation.taxnumber.required=Maksunumber on kohustuslik profile.validation.taxnumber.required=Maksunumber on kohustuslik
profile.validation.ustid.required=KMKR nr on kohustuslik profile.validation.ustid.required=KMKR nr on kohustuslik
profile.validation.ustid.invalid=KMKR numbri vorming on vale (nt DE123456789)
profile.validation.firstname.required=Eesnimi on kohustuslik profile.validation.firstname.required=Eesnimi on kohustuslik
profile.validation.lastname.required=Perekonnanimi on kohustuslik profile.validation.lastname.required=Perekonnanimi on kohustuslik
profile.validation.phone.required=Telefoninumber on kohustuslik profile.validation.phone.required=Telefoninumber on kohustuslik
@@ -639,6 +640,11 @@ createinvoice.button.save=Salvesta
createinvoice.confirm.save.title=Arve salvestamine createinvoice.confirm.save.title=Arve salvestamine
createinvoice.confirm.save.message=See arve salvestatakse p\u00fcsivalt ja seda ei saa hiljem muuta. Kas j\u00e4tkata? createinvoice.confirm.save.message=See arve salvestatakse p\u00fcsivalt ja seda ei saa hiljem muuta. Kas j\u00e4tkata?
createinvoice.confirm.save.confirm=Jah, salvesta createinvoice.confirm.save.confirm=Jah, salvesta
createinvoice.button.send=Saada
createinvoice.confirm.send.title=Saada arve
createinvoice.confirm.send.message=Arve salvestatakse jäädavalt ja saadetakse kliendile e-posti teel e-arvena (ZIP). Kas jätkata?
createinvoice.confirm.send.confirm=Jah, saada
createinvoice.notification.sent=Arve {0} saadeti e-arvena aadressile {1}
invoices.title=Arved invoices.title=Arved
invoices.column.number=Number invoices.column.number=Number
invoices.column.customer=Klient invoices.column.customer=Klient
@@ -86,6 +86,7 @@ profile.validation.zip.required=Postal code is required
profile.validation.city.required=City is required profile.validation.city.required=City is required
profile.validation.taxnumber.required=Tax number is required profile.validation.taxnumber.required=Tax number is required
profile.validation.ustid.required=VAT ID is required profile.validation.ustid.required=VAT ID is required
profile.validation.ustid.invalid=Invalid VAT ID format (e.g. DE123456789)
profile.validation.firstname.required=First name is required profile.validation.firstname.required=First name is required
profile.validation.lastname.required=Last name is required profile.validation.lastname.required=Last name is required
profile.validation.phone.required=Phone number is required profile.validation.phone.required=Phone number is required
@@ -699,6 +700,11 @@ createinvoice.button.save=Save
createinvoice.confirm.save.title=Save Invoice createinvoice.confirm.save.title=Save Invoice
createinvoice.confirm.save.message=This invoice will be permanently saved and cannot be modified afterwards. Continue? createinvoice.confirm.save.message=This invoice will be permanently saved and cannot be modified afterwards. Continue?
createinvoice.confirm.save.confirm=Yes, save createinvoice.confirm.save.confirm=Yes, save
createinvoice.button.send=Send
createinvoice.confirm.send.title=Send Invoice
createinvoice.confirm.send.message=This invoice will be permanently saved and sent to the customer by email as an e-invoice (ZIP). Continue?
createinvoice.confirm.send.confirm=Yes, send
createinvoice.notification.sent=Invoice {0} has been sent as an e-invoice to {1}
# Invoices # Invoices
invoices.title=Invoices invoices.title=Invoices
@@ -85,6 +85,7 @@ profile.validation.zip.required=C\u00f3digo postal es obligatorio
profile.validation.city.required=Ciudad es obligatorio profile.validation.city.required=Ciudad es obligatorio
profile.validation.taxnumber.required=Número fiscal es obligatorio profile.validation.taxnumber.required=Número fiscal es obligatorio
profile.validation.ustid.required=NIF-IVA es obligatorio profile.validation.ustid.required=NIF-IVA es obligatorio
profile.validation.ustid.invalid=Formato de NIF-IVA no válido (p. ej. DE123456789)
profile.validation.firstname.required=Nombre es obligatorio profile.validation.firstname.required=Nombre es obligatorio
profile.validation.lastname.required=Apellido es obligatorio profile.validation.lastname.required=Apellido es obligatorio
profile.validation.phone.required=N\u00famero de tel\u00e9fono es obligatorio profile.validation.phone.required=N\u00famero de tel\u00e9fono es obligatorio
@@ -696,6 +697,11 @@ createinvoice.button.save=Guardar
createinvoice.confirm.save.title=Guardar factura createinvoice.confirm.save.title=Guardar factura
createinvoice.confirm.save.message=Esta factura se guardar\u00e1 de forma permanente y no podr\u00e1 ser modificada posteriormente. \u00bfDesea continuar? createinvoice.confirm.save.message=Esta factura se guardar\u00e1 de forma permanente y no podr\u00e1 ser modificada posteriormente. \u00bfDesea continuar?
createinvoice.confirm.save.confirm=S\u00ed, guardar createinvoice.confirm.save.confirm=S\u00ed, guardar
createinvoice.button.send=Enviar
createinvoice.confirm.send.title=Enviar factura
createinvoice.confirm.send.message=La factura se guardará de forma permanente y se enviará al cliente por correo electrónico como factura electrónica (ZIP). ¿Continuar?
createinvoice.confirm.send.confirm=Sí, enviar
createinvoice.notification.sent=La factura {0} se ha enviado como factura electrónica a {1}
# Invoices # Invoices
invoices.title=Facturas invoices.title=Facturas
@@ -85,6 +85,7 @@ profile.validation.zip.required=Le code postal est obligatoire
profile.validation.city.required=La ville est obligatoire profile.validation.city.required=La ville est obligatoire
profile.validation.taxnumber.required=Le numéro fiscal est obligatoire profile.validation.taxnumber.required=Le numéro fiscal est obligatoire
profile.validation.ustid.required=Le n° TVA intracommunautaire est obligatoire profile.validation.ustid.required=Le n° TVA intracommunautaire est obligatoire
profile.validation.ustid.invalid=Format de numéro de TVA invalide (p. ex. DE123456789)
profile.validation.firstname.required=Le pr\u00e9nom est obligatoire profile.validation.firstname.required=Le pr\u00e9nom est obligatoire
profile.validation.lastname.required=Le nom est obligatoire profile.validation.lastname.required=Le nom est obligatoire
profile.validation.phone.required=Le num\u00e9ro de t\u00e9l\u00e9phone est obligatoire profile.validation.phone.required=Le num\u00e9ro de t\u00e9l\u00e9phone est obligatoire
@@ -696,6 +697,11 @@ createinvoice.button.save=Enregistrer
createinvoice.confirm.save.title=Enregistrer la facture createinvoice.confirm.save.title=Enregistrer la facture
createinvoice.confirm.save.message=Cette facture sera enregistr\u00e9e de mani\u00e8re permanente et ne pourra plus \u00eatre modifi\u00e9e par la suite. Continuer ? createinvoice.confirm.save.message=Cette facture sera enregistr\u00e9e de mani\u00e8re permanente et ne pourra plus \u00eatre modifi\u00e9e par la suite. Continuer ?
createinvoice.confirm.save.confirm=Oui, enregistrer createinvoice.confirm.save.confirm=Oui, enregistrer
createinvoice.button.send=Envoyer
createinvoice.confirm.send.title=Envoyer la facture
createinvoice.confirm.send.message=La facture sera enregistrée définitivement et envoyée au client par e-mail en tant que facture électronique (ZIP). Continuer ?
createinvoice.confirm.send.confirm=Oui, envoyer
createinvoice.notification.sent=La facture {0} a été envoyée en tant que facture électronique à {1}
# Invoices # Invoices
invoices.title=Factures invoices.title=Factures
@@ -85,6 +85,7 @@ profile.validation.zip.required=Pašto kodas yra privalomas
profile.validation.city.required=Miestas yra privalomas profile.validation.city.required=Miestas yra privalomas
profile.validation.taxnumber.required=Mokesčių mokėtojo numeris yra privalomas profile.validation.taxnumber.required=Mokesčių mokėtojo numeris yra privalomas
profile.validation.ustid.required=PVM mokėtojo kodas yra privalomas profile.validation.ustid.required=PVM mokėtojo kodas yra privalomas
profile.validation.ustid.invalid=Neteisingas PVM mokėtojo kodo formatas (pvz., DE123456789)
profile.validation.firstname.required=Vardas yra privalomas profile.validation.firstname.required=Vardas yra privalomas
profile.validation.lastname.required=Pavardė yra privaloma profile.validation.lastname.required=Pavardė yra privaloma
profile.validation.phone.required=Telefono numeris yra privalomas profile.validation.phone.required=Telefono numeris yra privalomas
@@ -698,6 +699,11 @@ createinvoice.button.save=Išsaugoti
createinvoice.confirm.save.title=Išsaugoti sąskaitą createinvoice.confirm.save.title=Išsaugoti sąskaitą
createinvoice.confirm.save.message=Ši sąskaita bus išsaugota visam laikui ir vėliau jos nebebus galima keisti. Tęsti? createinvoice.confirm.save.message=Ši sąskaita bus išsaugota visam laikui ir vėliau jos nebebus galima keisti. Tęsti?
createinvoice.confirm.save.confirm=Taip, išsaugoti createinvoice.confirm.save.confirm=Taip, išsaugoti
createinvoice.button.send=Siųsti
createinvoice.confirm.send.title=Siųsti sąskaitą
createinvoice.confirm.send.message=Sąskaita bus visam laikui išsaugota ir išsiųsta klientui el. paštu kaip e. sąskaita (ZIP). Tęsti?
createinvoice.confirm.send.confirm=Taip, siųsti
createinvoice.notification.sent=Sąskaita {0} išsiųsta kaip e. sąskaita adresu {1}
# Invoices # Invoices
invoices.title=Sąskaitos faktūros invoices.title=Sąskaitos faktūros
@@ -85,6 +85,7 @@ profile.validation.zip.required=Pasta indekss ir obligāts
profile.validation.city.required=Pilsēta ir obligāta profile.validation.city.required=Pilsēta ir obligāta
profile.validation.taxnumber.required=Nodokļu maksātāja numurs ir obligāts profile.validation.taxnumber.required=Nodokļu maksātāja numurs ir obligāts
profile.validation.ustid.required=PVN numurs ir obligāts profile.validation.ustid.required=PVN numurs ir obligāts
profile.validation.ustid.invalid=Nederīgs PVN numura formāts (piem., DE123456789)
profile.validation.firstname.required=Vārds ir obligāts profile.validation.firstname.required=Vārds ir obligāts
profile.validation.lastname.required=Uzvārds ir obligāts profile.validation.lastname.required=Uzvārds ir obligāts
profile.validation.phone.required=Tālruņa numurs ir obligāts profile.validation.phone.required=Tālruņa numurs ir obligāts
@@ -696,6 +697,11 @@ createinvoice.button.save=Saglabāt
createinvoice.confirm.save.title=Saglabāt rēķinu createinvoice.confirm.save.title=Saglabāt rēķinu
createinvoice.confirm.save.message=Šis rēķins tiks neatgriezeniski saglabāts un pēc tam to vairs nevarēs mainīt. Turpināt? createinvoice.confirm.save.message=Šis rēķins tiks neatgriezeniski saglabāts un pēc tam to vairs nevarēs mainīt. Turpināt?
createinvoice.confirm.save.confirm=Jā, saglabāt createinvoice.confirm.save.confirm=Jā, saglabāt
createinvoice.button.send=Sūtīt
createinvoice.confirm.send.title=Sūtīt rēķinu
createinvoice.confirm.send.message=Rēķins tiks neatgriezeniski saglabāts un nosūtīts klientam pa e-pastu kā e-rēķins (ZIP). Vai turpināt?
createinvoice.confirm.send.confirm=Jā, sūtīt
createinvoice.notification.sent=Rēķins {0} nosūtīts kā e-rēķins uz {1}
# Invoices # Invoices
invoices.title=Rēķini invoices.title=Rēķini
@@ -85,6 +85,7 @@ profile.validation.zip.required=Kod pocztowy jest wymagany
profile.validation.city.required=Miasto jest wymagane profile.validation.city.required=Miasto jest wymagane
profile.validation.taxnumber.required=Numer podatkowy jest wymagany profile.validation.taxnumber.required=Numer podatkowy jest wymagany
profile.validation.ustid.required=NIP UE jest wymagany profile.validation.ustid.required=NIP UE jest wymagany
profile.validation.ustid.invalid=Nieprawidłowy format numeru VAT UE (np. DE123456789)
profile.validation.firstname.required=Imi\u0119 jest wymagane profile.validation.firstname.required=Imi\u0119 jest wymagane
profile.validation.lastname.required=Nazwisko jest wymagane profile.validation.lastname.required=Nazwisko jest wymagane
profile.validation.phone.required=Numer telefonu jest wymagany profile.validation.phone.required=Numer telefonu jest wymagany
@@ -696,6 +697,11 @@ createinvoice.button.save=Zapisz
createinvoice.confirm.save.title=Zapisz faktur\u0119 createinvoice.confirm.save.title=Zapisz faktur\u0119
createinvoice.confirm.save.message=Ta faktura zostanie trwale zapisana i nie b\u0119dzie mog\u0142a by\u0107 p\u00f3\u017aniej zmieniona. Kontynuowa\u0107? createinvoice.confirm.save.message=Ta faktura zostanie trwale zapisana i nie b\u0119dzie mog\u0142a by\u0107 p\u00f3\u017aniej zmieniona. Kontynuowa\u0107?
createinvoice.confirm.save.confirm=Tak, zapisz createinvoice.confirm.save.confirm=Tak, zapisz
createinvoice.button.send=Wyślij
createinvoice.confirm.send.title=Wyślij fakturę
createinvoice.confirm.send.message=Faktura zostanie trwale zapisana i wysłana do klienta e-mailem jako e-faktura (ZIP). Kontynuować?
createinvoice.confirm.send.confirm=Tak, wyślij
createinvoice.notification.sent=Faktura {0} została wysłana jako e-faktura na adres {1}
# Invoices # Invoices
invoices.title=Faktury invoices.title=Faktury
@@ -85,6 +85,7 @@ profile.validation.zip.required=Почтовый индекс обязателе
profile.validation.city.required=Город обязателен profile.validation.city.required=Город обязателен
profile.validation.taxnumber.required=Налоговый номер обязателен profile.validation.taxnumber.required=Налоговый номер обязателен
profile.validation.ustid.required=Идентификатор плательщика НДС обязателен profile.validation.ustid.required=Идентификатор плательщика НДС обязателен
profile.validation.ustid.invalid=Неверный формат идентификационного номера НДС (напр. DE123456789)
profile.validation.firstname.required=Имя обязательно profile.validation.firstname.required=Имя обязательно
profile.validation.lastname.required=Фамилия обязательна profile.validation.lastname.required=Фамилия обязательна
profile.validation.phone.required=Номер телефона обязателен profile.validation.phone.required=Номер телефона обязателен
@@ -696,6 +697,11 @@ createinvoice.button.save=Сохранить
createinvoice.confirm.save.title=Сохранить счёт createinvoice.confirm.save.title=Сохранить счёт
createinvoice.confirm.save.message=Этот счёт будет сохранён навсегда и не может быть изменён впоследствии. Продолжить? createinvoice.confirm.save.message=Этот счёт будет сохранён навсегда и не может быть изменён впоследствии. Продолжить?
createinvoice.confirm.save.confirm=Да, сохранить createinvoice.confirm.save.confirm=Да, сохранить
createinvoice.button.send=Отправить
createinvoice.confirm.send.title=Отправить счёт
createinvoice.confirm.send.message=Счёт будет окончательно сохранён и отправлен клиенту по электронной почте как электронный счёт (ZIP). Продолжить?
createinvoice.confirm.send.confirm=Да, отправить
createinvoice.notification.sent=Счёт {0} отправлен как электронный счёт на {1}
# Invoices # Invoices
invoices.title=Счета invoices.title=Счета
@@ -85,6 +85,7 @@ profile.validation.zip.required=Posta kodu gereklidir
profile.validation.city.required=\u015eehir gereklidir profile.validation.city.required=\u015eehir gereklidir
profile.validation.taxnumber.required=Vergi numarası gereklidir profile.validation.taxnumber.required=Vergi numarası gereklidir
profile.validation.ustid.required=KDV kimlik no gereklidir profile.validation.ustid.required=KDV kimlik no gereklidir
profile.validation.ustid.invalid=Geçersiz KDV kimlik numarası biçimi (örn. DE123456789)
profile.validation.firstname.required=Ad gereklidir profile.validation.firstname.required=Ad gereklidir
profile.validation.lastname.required=Soyad gereklidir profile.validation.lastname.required=Soyad gereklidir
profile.validation.phone.required=Telefon numaras\u0131 gereklidir profile.validation.phone.required=Telefon numaras\u0131 gereklidir
@@ -696,6 +697,11 @@ createinvoice.button.save=Kaydet
createinvoice.confirm.save.title=Faturay\u0131 Kaydet createinvoice.confirm.save.title=Faturay\u0131 Kaydet
createinvoice.confirm.save.message=Bu fatura kal\u0131c\u0131 olarak kaydedilecek ve sonras\u0131nda de\u011fi\u015ftirilemeyecektir. Devam edilsin mi? createinvoice.confirm.save.message=Bu fatura kal\u0131c\u0131 olarak kaydedilecek ve sonras\u0131nda de\u011fi\u015ftirilemeyecektir. Devam edilsin mi?
createinvoice.confirm.save.confirm=Evet, kaydet createinvoice.confirm.save.confirm=Evet, kaydet
createinvoice.button.send=Gönder
createinvoice.confirm.send.title=Faturayı gönder
createinvoice.confirm.send.message=Fatura kalıcı olarak kaydedilecek ve müşteriye e-posta ile e-fatura (ZIP) olarak gönderilecektir. Devam edilsin mi?
createinvoice.confirm.send.confirm=Evet, gönder
createinvoice.notification.sent=Fatura {0}, e-fatura olarak {1} adresine gönderildi
# Invoices # Invoices
invoices.title=Faturalar invoices.title=Faturalar
@@ -0,0 +1,45 @@
package de.assecutor.votianlt.util;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class VatIdUtilTest {
@ParameterizedTest
@ValueSource(strings = { "DE123456789", "ATU12345678", "NL123456789B01", "FRXX123456789", "PL1234567890",
"IT12345678901", "BE0123456789", "SE123456789012", "IE1234567AB" })
void validVatIdsAreAccepted(String vatId) {
assertTrue(VatIdUtil.isValid(vatId), vatId + " sollte gültig sein");
}
@Test
void separatorsAndLowercaseAreNormalized() {
assertTrue(VatIdUtil.isValid("de 123.456-789"));
assertEquals("DE123456789", VatIdUtil.normalize("de 123.456-789"));
}
@ParameterizedTest
@ValueSource(strings = { "DE12345678", "DE1234567890", "DE12345678A", "ATU1234567", "NL123456789A01",
"123456789", "D123456789", "XX", "DE" })
void invalidVatIdsAreRejected(String vatId) {
assertFalse(VatIdUtil.isValid(vatId), vatId + " sollte ungültig sein");
}
@Test
void nullAndBlankAreInvalid() {
assertFalse(VatIdUtil.isValid(null));
assertFalse(VatIdUtil.isValid(""));
assertFalse(VatIdUtil.isValid(" "));
}
@Test
void unknownCountryFallsBackToGenericPattern() {
assertTrue(VatIdUtil.isValid("XI123456789"));
assertFalse(VatIdUtil.isValid("XI1"));
}
}