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:
@@ -63,7 +63,7 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
||||
ustId.setRequiredIndicatorVisible(true);
|
||||
ustId.setPlaceholder("DE123456789");
|
||||
ustId.setWidthFull();
|
||||
ustId.addBlurListener(e -> validateField(ustId));
|
||||
ustId.addBlurListener(e -> validateUstId());
|
||||
|
||||
// Anrede (Dropdown)
|
||||
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"))
|
||||
.bind(Customer::getTaxNumber, Customer::setTaxNumber);
|
||||
|
||||
binder.forField(ustId).asRequired(getTranslation("profile.validation.ustid.required")).bind(Customer::getUstId,
|
||||
Customer::setUstId);
|
||||
binder.forField(ustId).asRequired(getTranslation("profile.validation.ustid.required"))
|
||||
.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);
|
||||
|
||||
@@ -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) {
|
||||
String value = field.getValue();
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
@@ -286,7 +306,7 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
||||
|
||||
private boolean validateAllFields() {
|
||||
validateField(taxNumber);
|
||||
validateField(ustId);
|
||||
validateUstId();
|
||||
validateField(firstName);
|
||||
validateField(lastName);
|
||||
validateField(telephone);
|
||||
|
||||
@@ -33,9 +33,12 @@ import de.assecutor.votianlt.repository.ServiceRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import de.assecutor.votianlt.security.SecurityService;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||
import de.assecutor.votianlt.service.EmailService;
|
||||
import de.assecutor.votianlt.service.InvoiceLifecycleException;
|
||||
import de.assecutor.votianlt.service.InvoiceLifecycleService;
|
||||
import de.assecutor.votianlt.service.InvoiceTemplateService;
|
||||
import de.assecutor.votianlt.service.PdfToolService;
|
||||
import de.assecutor.votianlt.service.UserInvoiceZugferdService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bson.types.ObjectId;
|
||||
@@ -66,6 +69,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
private final UserInvoiceDataService userInvoiceDataService;
|
||||
private final CustomerService customerService;
|
||||
private final InvoiceLifecycleService invoiceLifecycleService;
|
||||
private final UserInvoiceZugferdService userInvoiceZugferdService;
|
||||
private final EmailService emailService;
|
||||
private User currentUser;
|
||||
private Job currentJob;
|
||||
private List<ServiceRow> gridRows = new ArrayList<>();
|
||||
@@ -120,7 +125,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
UserRepository userRepository, CustomerInvoiceService customerInvoiceService,
|
||||
InvoiceTemplateService invoiceTemplateService, SecurityService securityService,
|
||||
UserInvoiceDataService userInvoiceDataService, CustomerService customerService,
|
||||
InvoiceLifecycleService invoiceLifecycleService) {
|
||||
InvoiceLifecycleService invoiceLifecycleService, UserInvoiceZugferdService userInvoiceZugferdService,
|
||||
EmailService emailService) {
|
||||
this.jobRepository = jobRepository;
|
||||
this.serviceRepository = serviceRepository;
|
||||
this.userRepository = userRepository;
|
||||
@@ -130,6 +136,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
this.userInvoiceDataService = userInvoiceDataService;
|
||||
this.customerService = customerService;
|
||||
this.invoiceLifecycleService = invoiceLifecycleService;
|
||||
this.userInvoiceZugferdService = userInvoiceZugferdService;
|
||||
this.emailService = emailService;
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
@@ -557,7 +565,19 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser,
|
||||
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) {
|
||||
log.error("Fehler beim Erstellen der Rechnungsvorschau", ex);
|
||||
@@ -566,36 +586,47 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
}
|
||||
}
|
||||
|
||||
private void saveInvoice(String templateData, User user, Dialog previewDialog) {
|
||||
/**
|
||||
* Baut die (noch nicht persistierte) Rechnung mit allen Pflichtangaben nach
|
||||
* §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 vatRate = effectiveVatRate();
|
||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||
|
||||
CustomerInvoice invoice = new CustomerInvoice();
|
||||
invoice.setInvoiceNumber(invoiceNumber);
|
||||
invoice.setInvoiceDate(java.time.LocalDate.now());
|
||||
invoice.setJobId(currentJob.getId().toHexString());
|
||||
invoice.setUserId(user.getId().toHexString());
|
||||
invoice.setNetAmount(netAmount);
|
||||
invoice.setVatRate(vatRate);
|
||||
invoice.setVatAmount(vatAmount);
|
||||
invoice.setTotalAmount(totalAmount);
|
||||
|
||||
invoice.setDeliveryDate(currentJob.getDeliveryDate() != null ? currentJob.getDeliveryDate()
|
||||
: java.time.LocalDate.now());
|
||||
applySenderData(invoice, user);
|
||||
applyRecipientData(invoice);
|
||||
invoice.setItems(buildInvoiceItems(vatRate));
|
||||
|
||||
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());
|
||||
|
||||
BigDecimal netAmount = calculateNetAmount();
|
||||
BigDecimal vatRate = effectiveVatRate();
|
||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||
|
||||
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, user, invoiceNumber);
|
||||
|
||||
CustomerInvoice invoice = new CustomerInvoice();
|
||||
invoice.setInvoiceNumber(invoiceNumber);
|
||||
invoice.setInvoiceDate(java.time.LocalDate.now());
|
||||
invoice.setJobId(currentJob.getId().toHexString());
|
||||
invoice.setUserId(user.getId().toHexString());
|
||||
invoice.setNetAmount(netAmount);
|
||||
invoice.setVatRate(vatRate);
|
||||
invoice.setVatAmount(vatAmount);
|
||||
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()
|
||||
: java.time.LocalDate.now());
|
||||
applySenderData(invoice, user);
|
||||
applyRecipientData(invoice);
|
||||
invoice.setItems(buildInvoiceItems(vatRate));
|
||||
|
||||
invoice.setPdfData(pdfBytes);
|
||||
CustomerInvoice invoice = buildInvoice(invoiceNumber, pdfBytes, user);
|
||||
|
||||
// Finalisierung mit Audit-Eintrag und Eindeutigkeitsprüfung der Rechnungsnummer (R-07/R-11/R-36).
|
||||
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.Position.BOTTOM_END);
|
||||
|
||||
sendInvoiceEmail(savedInvoice);
|
||||
|
||||
} catch (InvoiceLifecycleException lifecycleEx) {
|
||||
log.warn("Lifecycle-Verstoß beim Speichern der Rechnung: {}", lifecycleEx.getMessage());
|
||||
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
|
||||
* (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());
|
||||
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(
|
||||
getTranslation("createinvoice.confirm.save.title"),
|
||||
getTranslation("createinvoice.confirm.save.message"),
|
||||
getTranslation("createinvoice.confirm.send.title"),
|
||||
getTranslation("createinvoice.confirm.send.message"),
|
||||
"560px",
|
||||
getTranslation("button.cancel"),
|
||||
getTranslation("createinvoice.confirm.save.confirm"),
|
||||
() -> saveInvoice(templateData, user, pdfDialog),
|
||||
getTranslation("createinvoice.confirm.send.confirm"),
|
||||
() -> saveAndSendInvoice(templateData, user, pdfDialog),
|
||||
ButtonVariant.LUMO_PRIMARY);
|
||||
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.getFooter().add(closeButton, saveButton);
|
||||
pdfDialog.getFooter().add(closeButton, sendButton);
|
||||
pdfDialog.open();
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
|
||||
binder.forField(taxNumberField).asRequired(getTranslation("profile.validation.taxnumber.required"))
|
||||
.bind(Customer::getTaxNumber, Customer::setTaxNumber);
|
||||
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);
|
||||
binder.forField(firstnameField).bind(Customer::getFirstname, Customer::setFirstname);
|
||||
binder.forField(lastNameField).bind(Customer::getLastName, Customer::setLastName);
|
||||
|
||||
@@ -378,7 +378,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
ustIdField.setPlaceholder("DE123456789");
|
||||
ustIdField.setRequiredIndicatorVisible(true);
|
||||
ustIdField.addBlurListener(e -> {
|
||||
validateField(ustIdField, getTranslation("profile.validation.ustid.required"));
|
||||
validateUstIdField();
|
||||
saveInvoiceData();
|
||||
});
|
||||
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
|
||||
// aktiviert ist
|
||||
if (billingEnabled.getValue()) {
|
||||
validateField(ustIdField, getTranslation("profile.validation.ustid.required"));
|
||||
validateUstIdField();
|
||||
validateField(taxNumberField, getTranslation("profile.validation.taxnumber.required"));
|
||||
isValid = isValid && !ustIdField.isInvalid() && !taxNumberField.isInvalid();
|
||||
}
|
||||
@@ -864,6 +864,24 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
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) {
|
||||
String value = field.getValue();
|
||||
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.button.Button;
|
||||
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.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
@@ -186,13 +188,15 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
|
||||
actions.setSpacing(true);
|
||||
actions.setPadding(false);
|
||||
|
||||
Button viewBtn = new Button(getTranslation("invoices.action.view"), e -> downloadInvoicePdf(invoice));
|
||||
viewBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
|
||||
Button viewBtn = actionIconButton(VaadinIcon.EYE, "invoices.action.view",
|
||||
e -> downloadInvoicePdf(invoice));
|
||||
viewBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
viewBtn.setEnabled(invoice.getPdfData() != null && invoice.getPdfData().length > 0);
|
||||
actions.add(viewBtn);
|
||||
|
||||
Button historyBtn = new Button(getTranslation("invoices.action.history"), e -> openHistoryDialog(invoice));
|
||||
historyBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
|
||||
Button historyBtn = actionIconButton(VaadinIcon.TIME_BACKWARD, "invoices.action.history",
|
||||
e -> openHistoryDialog(invoice));
|
||||
historyBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
actions.add(historyBtn);
|
||||
|
||||
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
|
||||
&& invoicePermissionService.canMarkAsSent(currentUser)) {
|
||||
Button sentBtn = new Button(getTranslation("invoices.action.marksent"),
|
||||
Button sentBtn = actionIconButton(VaadinIcon.CHECK, "invoices.action.marksent",
|
||||
e -> markAsSent(invoice));
|
||||
sentBtn.addThemeVariants(ButtonVariant.LUMO_SMALL);
|
||||
actions.add(sentBtn);
|
||||
}
|
||||
|
||||
// E-Rechnung (ZUGFeRD) über das externe PDF Tool — analog zu den
|
||||
// System-Rechnungen der Admin-Seite
|
||||
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));
|
||||
zugferdBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
|
||||
zugferdBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
actions.add(zugferdBtn);
|
||||
|
||||
if (isLiveInvoice && invoicePermissionService.canMarkAsSent(currentUser)) {
|
||||
Button sendBtn = new Button(getTranslation("invoices.action.sendinvoice"),
|
||||
Button sendBtn = actionIconButton(VaadinIcon.PAPERPLANE, "invoices.action.sendinvoice",
|
||||
e -> sendAsEInvoice(invoice));
|
||||
sendBtn.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_PRIMARY);
|
||||
sendBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
actions.add(sendBtn);
|
||||
}
|
||||
}
|
||||
|
||||
if (isLiveInvoice) {
|
||||
if (invoicePermissionService.canCorrect(currentUser)) {
|
||||
Button correctBtn = new Button(getTranslation("invoices.action.correct"),
|
||||
Button correctBtn = actionIconButton(VaadinIcon.EDIT, "invoices.action.correct",
|
||||
e -> openCorrectionDialog(invoice));
|
||||
correctBtn.addThemeVariants(ButtonVariant.LUMO_SMALL);
|
||||
actions.add(correctBtn);
|
||||
}
|
||||
|
||||
if (invoicePermissionService.canCancel(currentUser)) {
|
||||
Button cancelBtn = new Button(getTranslation("invoices.action.cancel"),
|
||||
Button cancelBtn = actionIconButton(VaadinIcon.BAN, "invoices.action.cancel",
|
||||
e -> openCancellationDialog(invoice));
|
||||
cancelBtn.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_ERROR);
|
||||
cancelBtn.addThemeVariants(ButtonVariant.LUMO_ERROR);
|
||||
actions.add(cancelBtn);
|
||||
}
|
||||
}
|
||||
@@ -246,20 +248,33 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
|
||||
// Zahlung erfassen: nur für reguläre Rechnungen (R-25)
|
||||
if (type == InvoiceType.INVOICE && status != InvoiceStatus.DRAFT
|
||||
&& invoicePermissionService.canRecordPayment(currentUser)) {
|
||||
Button payBtn = new Button(getTranslation("invoices.action.payment"),
|
||||
Button payBtn = actionIconButton(VaadinIcon.EURO, "invoices.action.payment",
|
||||
e -> openPaymentDialog(invoice));
|
||||
payBtn.addThemeVariants(ButtonVariant.LUMO_SMALL);
|
||||
actions.add(payBtn);
|
||||
}
|
||||
|
||||
// Belegpaket exportieren (R-33/R-34)
|
||||
Button exportBtn = new Button(getTranslation("invoices.action.export"), e -> exportPackage(invoice));
|
||||
exportBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
|
||||
Button exportBtn = actionIconButton(VaadinIcon.DOWNLOAD, "invoices.action.export",
|
||||
e -> exportPackage(invoice));
|
||||
exportBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
actions.add(exportBtn);
|
||||
|
||||
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) {
|
||||
Dialog dialog = DialogStylingHelper.createStyledDialog(
|
||||
getTranslation("invoices.payment.title", invoice.getInvoiceNumber()), "480px");
|
||||
|
||||
@@ -70,8 +70,10 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
|
||||
endDate.setLabel(getTranslation("jobs.filter.enddate"));
|
||||
searchField.setLabel(getTranslation("jobs.filter.search"));
|
||||
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.setTooltipText(getTranslation("jobs.button.csvexport"));
|
||||
exportButton.setAriaLabel(getTranslation("jobs.button.csvexport"));
|
||||
ViewToolbar toolbar = new ViewToolbar(getTranslation("nav.jobs"), exportButton);
|
||||
add(toolbar);
|
||||
|
||||
@@ -85,9 +87,7 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
|
||||
searchField.setClearButtonVisible(true);
|
||||
|
||||
// Filterleiste mit Export-Button am rechten Rand
|
||||
Button applyFilter = new Button(getTranslation("jobs.filter.apply"));
|
||||
applyFilter.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
HorizontalLayout leftFilters = new HorizontalLayout(startDate, endDate, searchField, statusFilter, applyFilter);
|
||||
HorizontalLayout leftFilters = new HorizontalLayout(startDate, endDate, searchField, statusFilter);
|
||||
leftFilters.setAlignItems(Alignment.END);
|
||||
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();
|
||||
startDate.setValue(today.minusDays(30));
|
||||
endDate.setValue(today);
|
||||
applyFilter.addClickListener(e -> loadData());
|
||||
exportButton.addClickListener(e -> exportToCsv());
|
||||
|
||||
// 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 + 2–12
|
||||
* 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.taxnumber.required=Steuernummer 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.lastname.required=Nachname 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.message=Diese Rechnung wird dauerhaft gespeichert und kann anschlie\u00dfend nicht mehr ver\u00e4ndert werden. Fortfahren?
|
||||
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.title=Rechnungen
|
||||
|
||||
@@ -75,6 +75,7 @@ profile.validation.zip.required=Postiindeks on kohustuslik
|
||||
profile.validation.city.required=Linn on kohustuslik
|
||||
profile.validation.taxnumber.required=Maksunumber 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.lastname.required=Perekonnanimi 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.message=See arve salvestatakse p\u00fcsivalt ja seda ei saa hiljem muuta. Kas j\u00e4tkata?
|
||||
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.column.number=Number
|
||||
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.taxnumber.required=Tax number 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.lastname.required=Last name 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.message=This invoice will be permanently saved and cannot be modified afterwards. Continue?
|
||||
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.title=Invoices
|
||||
|
||||
@@ -85,6 +85,7 @@ profile.validation.zip.required=C\u00f3digo postal es obligatorio
|
||||
profile.validation.city.required=Ciudad es obligatorio
|
||||
profile.validation.taxnumber.required=Número fiscal 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.lastname.required=Apellido 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.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.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.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.taxnumber.required=Le numéro fiscal 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.lastname.required=Le nom 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.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.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.title=Factures
|
||||
|
||||
@@ -85,6 +85,7 @@ profile.validation.zip.required=Pašto kodas yra privalomas
|
||||
profile.validation.city.required=Miestas 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.invalid=Neteisingas PVM mokėtojo kodo formatas (pvz., DE123456789)
|
||||
profile.validation.firstname.required=Vardas yra privalomas
|
||||
profile.validation.lastname.required=Pavardė yra privaloma
|
||||
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.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.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.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.taxnumber.required=Nodokļu maksātāja 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.lastname.required=Uzvārds 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.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.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.title=Rēķini
|
||||
|
||||
@@ -85,6 +85,7 @@ profile.validation.zip.required=Kod pocztowy jest wymagany
|
||||
profile.validation.city.required=Miasto jest wymagane
|
||||
profile.validation.taxnumber.required=Numer podatkowy 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.lastname.required=Nazwisko jest wymagane
|
||||
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.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.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.title=Faktury
|
||||
|
||||
@@ -85,6 +85,7 @@ profile.validation.zip.required=Почтовый индекс обязателе
|
||||
profile.validation.city.required=Город обязателен
|
||||
profile.validation.taxnumber.required=Налоговый номер обязателен
|
||||
profile.validation.ustid.required=Идентификатор плательщика НДС обязателен
|
||||
profile.validation.ustid.invalid=Неверный формат идентификационного номера НДС (напр. DE123456789)
|
||||
profile.validation.firstname.required=Имя обязательно
|
||||
profile.validation.lastname.required=Фамилия обязательна
|
||||
profile.validation.phone.required=Номер телефона обязателен
|
||||
@@ -696,6 +697,11 @@ createinvoice.button.save=Сохранить
|
||||
createinvoice.confirm.save.title=Сохранить счёт
|
||||
createinvoice.confirm.save.message=Этот счёт будет сохранён навсегда и не может быть изменён впоследствии. Продолжить?
|
||||
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.title=Счета
|
||||
|
||||
@@ -85,6 +85,7 @@ profile.validation.zip.required=Posta kodu gereklidir
|
||||
profile.validation.city.required=\u015eehir gereklidir
|
||||
profile.validation.taxnumber.required=Vergi numarası 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.lastname.required=Soyad 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.message=Bu fatura kal\u0131c\u0131 olarak kaydedilecek ve sonras\u0131nda de\u011fi\u015ftirilemeyecektir. Devam edilsin mi?
|
||||
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.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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user