Bankverbindung (Bank, IBAN, BIC) im Adressbuch, IBAN/BIC über den Rechnungssteller statt Freitext

IBAN-Prüfung (ISO 13616) in eigenen IbanValidator ausgelagert; Käuferreferenz
und dev-Testdaten aus der Rechnungsmaske entfernt, Entwurf-Speichern nur bei
gefüllten Pflichtfeldern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 14:07:56 +02:00
co-authored by Claude Fable 5
parent 722699f5d0
commit 48a5831600
4 changed files with 171 additions and 85 deletions
@@ -26,6 +26,11 @@ public class Address {
private String email; private String email;
private String phone; private String phone;
// Bankverbindung (v. a. für Rechnungssteller relevant)
private String bankName;
private String iban;
private String bic;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
@@ -114,6 +119,30 @@ public class Address {
this.phone = phone; this.phone = phone;
} }
public String getBankName() {
return bankName;
}
public void setBankName(String bankName) {
this.bankName = bankName;
}
public String getIban() {
return iban;
}
public void setIban(String iban) {
this.iban = iban;
}
public String getBic() {
return bic;
}
public void setBic(String bic) {
this.bic = bic;
}
public LocalDateTime getCreatedAt() { public LocalDateTime getCreatedAt() {
return createdAt; return createdAt;
} }
@@ -0,0 +1,29 @@
package de.assecutor.pdftool.invoice;
/**
* Prüft IBANs nach ISO 13616 (MOD 97-10).
*/
public final class IbanValidator {
private IbanValidator() {
}
public static boolean isValid(String value) {
String normalized = normalize(value);
if (!normalized.matches("[A-Z]{2}\\d{2}[A-Z0-9]{11,30}")) {
return false;
}
String rearranged = normalized.substring(4) + normalized.substring(0, 4);
int mod = 0;
for (char c : rearranged.toCharArray()) {
int digit = Character.isDigit(c) ? c - '0' : c - 'A' + 10;
mod = digit < 10 ? (mod * 10 + digit) % 97 : (mod * 100 + digit) % 97;
}
return mod == 1;
}
/** Entfernt Leerzeichen und wandelt in Großbuchstaben um. */
public static String normalize(String value) {
return value.replaceAll("\\s", "").toUpperCase();
}
}
@@ -20,6 +20,7 @@ import com.vaadin.flow.router.Route;
import de.assecutor.pdftool.address.Address; import de.assecutor.pdftool.address.Address;
import de.assecutor.pdftool.address.AddressRepository; import de.assecutor.pdftool.address.AddressRepository;
import de.assecutor.pdftool.address.AddressType; import de.assecutor.pdftool.address.AddressType;
import de.assecutor.pdftool.invoice.IbanValidator;
import de.assecutor.pdftool.invoice.VatIdValidator; import de.assecutor.pdftool.invoice.VatIdValidator;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
@@ -122,7 +123,7 @@ public class AddressBookView extends VerticalLayout {
Address target = address == null ? new Address() : address; Address target = address == null ? new Address() : address;
Dialog dialog = new Dialog(); Dialog dialog = new Dialog();
dialog.setHeaderTitle(isNew ? "Neue Adresse" : "Adresse bearbeiten"); dialog.setHeaderTitle(isNew ? "Neues Unternehmen" : "Unternehmen bearbeiten");
ComboBox<AddressType> type = new ComboBox<>("Typ"); ComboBox<AddressType> type = new ComboBox<>("Typ");
type.setItems(AddressType.values()); type.setItems(AddressType.values());
@@ -154,7 +155,17 @@ public class AddressBookView extends VerticalLayout {
TextField phone = new TextField("Telefon"); TextField phone = new TextField("Telefon");
phone.setValue(target.getPhone() != null ? target.getPhone() : ""); phone.setValue(target.getPhone() != null ? target.getPhone() : "");
FormLayout form = new FormLayout(type, name, street, zip, city, countryCode, vatId, email, phone); // Bankverbindung (optional)
TextField bankName = new TextField("Bank");
bankName.setValue(target.getBankName() != null ? target.getBankName() : "");
TextField iban = new TextField("IBAN");
iban.setValue(target.getIban() != null ? target.getIban() : "");
iban.setHelperText("z. B. DE02 1203 0000 0000 2020 51");
TextField bic = new TextField("BIC");
bic.setValue(target.getBic() != null ? target.getBic() : "");
FormLayout form = new FormLayout(type, name, street, zip, city, countryCode, vatId, email, phone,
bankName, iban, bic);
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("500px", 2)); form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("500px", 2));
form.setWidth("560px"); form.setWidth("560px");
dialog.add(form); dialog.add(form);
@@ -189,6 +200,17 @@ public class AddressBookView extends VerticalLayout {
email.setInvalid(true); email.setInvalid(true);
errors.add("E-Mail-Adresse ist ungültig."); errors.add("E-Mail-Adresse ist ungültig.");
} }
if (!iban.getValue().isBlank() && !IbanValidator.isValid(iban.getValue())) {
iban.setErrorMessage("IBAN ist ungültig (Prüfsumme).");
iban.setInvalid(true);
errors.add("IBAN ist ungültig (Prüfsumme).");
}
if (!bic.getValue().isBlank()
&& !bic.getValue().trim().matches("[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?")) {
bic.setErrorMessage("BIC ist ungültig (8 oder 11 Stellen).");
bic.setInvalid(true);
errors.add("BIC ist ungültig (8 oder 11 Stellen).");
}
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
Notification.show(String.join("\n", errors), 5000, Notification.Position.MIDDLE) Notification.show(String.join("\n", errors), 5000, Notification.Position.MIDDLE)
.addThemeVariants(NotificationVariant.LUMO_ERROR); .addThemeVariants(NotificationVariant.LUMO_ERROR);
@@ -204,6 +226,9 @@ public class AddressBookView extends VerticalLayout {
target.setVatId(vatId.getValue().isBlank() ? "" : VatIdValidator.normalize(vatId.getValue())); target.setVatId(vatId.getValue().isBlank() ? "" : VatIdValidator.normalize(vatId.getValue()));
target.setEmail(email.getValue().trim()); target.setEmail(email.getValue().trim());
target.setPhone(phone.getValue().trim()); target.setPhone(phone.getValue().trim());
target.setBankName(bankName.getValue().trim());
target.setIban(iban.getValue().isBlank() ? "" : IbanValidator.normalize(iban.getValue()));
target.setBic(bic.getValue().trim().toUpperCase());
target.touch(); target.touch();
repository.save(target); repository.save(target);
@@ -27,12 +27,11 @@ import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.StreamResource; import com.vaadin.flow.server.StreamResource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import de.assecutor.pdftool.address.Address; import de.assecutor.pdftool.address.Address;
import de.assecutor.pdftool.address.AddressRepository; import de.assecutor.pdftool.address.AddressRepository;
import de.assecutor.pdftool.address.AddressType; import de.assecutor.pdftool.address.AddressType;
import de.assecutor.pdftool.invoice.CapturedInvoiceData; import de.assecutor.pdftool.invoice.CapturedInvoiceData;
import de.assecutor.pdftool.invoice.IbanValidator;
import de.assecutor.pdftool.invoice.InvoiceDraft; import de.assecutor.pdftool.invoice.InvoiceDraft;
import de.assecutor.pdftool.invoice.InvoiceDraftService; import de.assecutor.pdftool.invoice.InvoiceDraftService;
import de.assecutor.pdftool.invoice.InvoiceMetadata; import de.assecutor.pdftool.invoice.InvoiceMetadata;
@@ -107,9 +106,6 @@ public class MainView extends VerticalLayout {
private final DatePicker dueDate = new DatePicker("Fälligkeitsdatum"); private final DatePicker dueDate = new DatePicker("Fälligkeitsdatum");
private final TextField currency = new TextField("Währung"); private final TextField currency = new TextField("Währung");
private final TextField paymentTerms = new TextField("Zahlungsbedingungen"); private final TextField paymentTerms = new TextField("Zahlungsbedingungen");
private final TextField buyerReference = new TextField("Käuferreferenz / Leitweg-ID");
private final TextField iban = new TextField("IBAN (Rechnungssteller)");
private final TextField bic = new TextField("BIC");
private final PartyForm sender = new PartyForm(true); private final PartyForm sender = new PartyForm(true);
private final PartyForm recipient = new PartyForm(false); private final PartyForm recipient = new PartyForm(false);
@@ -128,8 +124,7 @@ public class MainView extends VerticalLayout {
public MainView(ZugferdService zugferdService, CapturedInvoiceData capturedInvoiceData, public MainView(ZugferdService zugferdService, CapturedInvoiceData capturedInvoiceData,
InvoiceTemplateService invoiceTemplateService, TemplatePdfService templatePdfService, InvoiceTemplateService invoiceTemplateService, TemplatePdfService templatePdfService,
AddressRepository addressRepository, InvoiceDraftService invoiceDraftService, AddressRepository addressRepository, InvoiceDraftService invoiceDraftService) {
Environment environment) {
this.zugferdService = zugferdService; this.zugferdService = zugferdService;
this.capturedInvoiceData = capturedInvoiceData; this.capturedInvoiceData = capturedInvoiceData;
this.invoiceTemplateService = invoiceTemplateService; this.invoiceTemplateService = invoiceTemplateService;
@@ -209,10 +204,8 @@ public class MainView extends VerticalLayout {
currency.setPattern("[A-Za-z]{3}"); currency.setPattern("[A-Za-z]{3}");
currency.setErrorMessage("Währung als dreistelliger ISO-Code, z. B. EUR."); currency.setErrorMessage("Währung als dreistelliger ISO-Code, z. B. EUR.");
currency.setValue("EUR"); currency.setValue("EUR");
buyerReference.setHelperText("BT-10 — für Rechnungen an Behörden erforderlich");
iban.setHelperText("Für die Zahlungsangaben (BG-16) empfohlen");
FormLayout invoiceForm = new FormLayout(invoiceNumber, issueDate, deliveryDate, dueDate, currency, FormLayout invoiceForm = new FormLayout(invoiceNumber, issueDate, deliveryDate, dueDate, currency,
paymentTerms, buyerReference, iban, bic); paymentTerms);
invoiceForm.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("600px", 3)); invoiceForm.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("600px", 3));
add(new H3("2. Rechnungsdaten"), invoiceForm); add(new H3("2. Rechnungsdaten"), invoiceForm);
@@ -240,10 +233,6 @@ public class MainView extends VerticalLayout {
registerDraftDirtyTracking(); registerDraftDirtyTracking();
updateSaveDraftEnabled(); updateSaveDraftEnabled();
if (environment.acceptsProfiles(Profiles.of("dev"))) {
prefillDevData();
}
} }
@Override @Override
@@ -325,9 +314,9 @@ public class MainView extends VerticalLayout {
issueDate.getValue(), deliveryDate.getValue(), dueDate.getValue(), issueDate.getValue(), deliveryDate.getValue(), dueDate.getValue(),
currency.getValue().trim(), currency.getValue().trim(),
paymentTerms.getValue(), paymentTerms.getValue(),
buyerReference.getValue().trim(), "",
iban.getValue().trim(), sender.ibanNormalized(),
bic.getValue().trim(), sender.bicNormalized(),
sender.toParty(), recipient.toParty(), sender.toParty(), recipient.toParty(),
List.copyOf(lineItems), List.copyOf(lineItems),
LocalDateTime.now()); LocalDateTime.now());
@@ -358,10 +347,8 @@ public class MainView extends VerticalLayout {
dueDate.setValue(draft.dueDate()); dueDate.setValue(draft.dueDate());
currency.setValue(nullToEmpty(draft.currency())); currency.setValue(nullToEmpty(draft.currency()));
paymentTerms.setValue(nullToEmpty(draft.paymentTerms())); paymentTerms.setValue(nullToEmpty(draft.paymentTerms()));
buyerReference.setValue(nullToEmpty(draft.buyerReference()));
iban.setValue(nullToEmpty(draft.iban()));
bic.setValue(nullToEmpty(draft.bic()));
applyParty(sender, draft.sender()); applyParty(sender, draft.sender());
sender.prefillBank(nullToEmpty(draft.iban()), nullToEmpty(draft.bic()));
applyParty(recipient, draft.recipient()); applyParty(recipient, draft.recipient());
lineItems.clear(); lineItems.clear();
if (draft.items() != null) { if (draft.items() != null) {
@@ -382,7 +369,7 @@ public class MainView extends VerticalLayout {
form.prefill(nullToEmpty(party.name()), nullToEmpty(party.street()), form.prefill(nullToEmpty(party.name()), nullToEmpty(party.street()),
nullToEmpty(party.zip()), nullToEmpty(party.city()), nullToEmpty(party.zip()), nullToEmpty(party.city()),
nullToEmpty(party.countryCode()), nullToEmpty(party.vatId()), nullToEmpty(party.countryCode()), nullToEmpty(party.vatId()),
nullToEmpty(party.email()), nullToEmpty(party.phone())); nullToEmpty(party.email()));
} }
/** Konfiguriert eine Adressbuch-Auswahl, die bei Auswahl das zugehörige Adressformular vorbelegt. */ /** Konfiguriert eine Adressbuch-Auswahl, die bei Auswahl das zugehörige Adressformular vorbelegt. */
@@ -397,7 +384,8 @@ public class MainView extends VerticalLayout {
form.prefill(nullToEmpty(address.getName()), nullToEmpty(address.getStreet()), form.prefill(nullToEmpty(address.getName()), nullToEmpty(address.getStreet()),
nullToEmpty(address.getZip()), nullToEmpty(address.getCity()), nullToEmpty(address.getZip()), nullToEmpty(address.getCity()),
nullToEmpty(address.getCountryCode()), nullToEmpty(address.getVatId()), nullToEmpty(address.getCountryCode()), nullToEmpty(address.getVatId()),
nullToEmpty(address.getEmail()), nullToEmpty(address.getPhone())); nullToEmpty(address.getEmail()));
form.prefillBank(nullToEmpty(address.getIban()), nullToEmpty(address.getBic()));
} }
}); });
} }
@@ -415,22 +403,6 @@ public class MainView extends VerticalLayout {
return List.copyOf(lineItems); return List.copyOf(lineItems);
} }
/** Belegt das Formular im dev-Profil mit Testdaten vor, um manuelles Tippen zu sparen. */
private void prefillDevData() {
invoiceNumber.setValue("11111");
deliveryDate.setValue(LocalDate.now());
dueDate.setValue(LocalDate.now().plusDays(14));
paymentTerms.setValue("14 Tage");
buyerReference.setValue("11111");
iban.setValue("DE64 6001 0070 0379 0907 00");
bic.setValue("PBNKDEFFXXX");
// Rechnungssteller und -empfänger bleiben leer und werden über die
// Adressbuch-Auswahl befüllt.
lineItems.add(new InvoiceMetadata.LineItem("sdf", BigDecimal.ONE, new BigDecimal("100"),
new BigDecimal("19")));
refreshItemsGrid();
}
/** Tabelle der Rechnungspositionen mit Bearbeiten-/Löschen-Icons pro Zeile. */ /** Tabelle der Rechnungspositionen mit Bearbeiten-/Löschen-Icons pro Zeile. */
private void configureItemsGrid() { private void configureItemsGrid() {
itemsGrid.addColumn(item -> item.description()).setHeader("Beschreibung").setFlexGrow(1); itemsGrid.addColumn(item -> item.description()).setHeader("Beschreibung").setFlexGrow(1);
@@ -471,20 +443,28 @@ public class MainView extends VerticalLayout {
dueDate.addValueChangeListener(event -> updateSaveDraftEnabled()); dueDate.addValueChangeListener(event -> updateSaveDraftEnabled());
currency.addValueChangeListener(event -> updateSaveDraftEnabled()); currency.addValueChangeListener(event -> updateSaveDraftEnabled());
paymentTerms.addValueChangeListener(event -> updateSaveDraftEnabled()); paymentTerms.addValueChangeListener(event -> updateSaveDraftEnabled());
buyerReference.addValueChangeListener(event -> updateSaveDraftEnabled());
iban.addValueChangeListener(event -> updateSaveDraftEnabled());
bic.addValueChangeListener(event -> updateSaveDraftEnabled());
templateSelect.addValueChangeListener(event -> updateSaveDraftEnabled()); templateSelect.addValueChangeListener(event -> updateSaveDraftEnabled());
sender.onAnyChange(this::updateSaveDraftEnabled); sender.onAnyChange(this::updateSaveDraftEnabled);
recipient.onAnyChange(this::updateSaveDraftEnabled); recipient.onAnyChange(this::updateSaveDraftEnabled);
} }
/** /**
* Der Speichern-Button ist nur aktiv, wenn keine Vorbelegung geladen wurde * Der Speichern-Button ist nur aktiv, wenn alle Pflichtfelder gefüllt sind
* oder sich die Eingaben seit dem Laden bzw. Speichern geändert haben. * und keine Vorbelegung geladen wurde oder sich die Eingaben seit dem Laden
* bzw. Speichern geändert haben.
*/ */
private void updateSaveDraftEnabled() { private void updateSaveDraftEnabled() {
saveDraftButton.setEnabled(loadedDraftState == null || !comparableState().equals(loadedDraftState)); boolean changed = loadedDraftState == null || !comparableState().equals(loadedDraftState);
saveDraftButton.setEnabled(changed && requiredFieldsFilled());
}
/** @return true, wenn alle Pflichtfelder der Maske gefüllt sind. */
private boolean requiredFieldsFilled() {
return !invoiceNumber.getValue().isBlank()
&& issueDate.getValue() != null
&& !currency.getValue().isBlank()
&& sender.requiredFieldsFilled()
&& recipient.requiredFieldsFilled();
} }
/** Der aktuelle Formularstand ohne Name und Zeitstempel, zum Vergleich mit der geladenen Vorbelegung. */ /** Der aktuelle Formularstand ohne Name und Zeitstempel, zum Vergleich mit der geladenen Vorbelegung. */
@@ -495,9 +475,9 @@ public class MainView extends VerticalLayout {
issueDate.getValue(), deliveryDate.getValue(), dueDate.getValue(), issueDate.getValue(), deliveryDate.getValue(), dueDate.getValue(),
currency.getValue().trim(), currency.getValue().trim(),
paymentTerms.getValue(), paymentTerms.getValue(),
buyerReference.getValue().trim(), "",
iban.getValue().trim(), sender.ibanNormalized(),
bic.getValue().trim(), sender.bicNormalized(),
sender.toParty(), recipient.toParty(), sender.toParty(), recipient.toParty(),
List.copyOf(lineItems), List.copyOf(lineItems),
null); null);
@@ -694,16 +674,6 @@ public class MainView extends VerticalLayout {
&& !currency.getValue().trim().matches("[A-Za-z]{3}")) { && !currency.getValue().trim().matches("[A-Za-z]{3}")) {
markInvalid(currency, "Währung als dreistelliger ISO-Code, z. B. EUR.", errors); markInvalid(currency, "Währung als dreistelliger ISO-Code, z. B. EUR.", errors);
} }
if (!iban.isEmpty() && !isValidIban(iban.getValue())) {
markInvalid(iban, "IBAN ist ungültig (Prüfsumme).", errors);
} else {
iban.setInvalid(false);
}
if (!bic.isEmpty() && !bic.getValue().trim().matches("[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?")) {
markInvalid(bic, "BIC muss 8 oder 11 Stellen haben, z. B. NOLADE21RZB.", errors);
} else {
bic.setInvalid(false);
}
errors.addAll(sender.validateFields("Rechnungssteller")); errors.addAll(sender.validateFields("Rechnungssteller"));
errors.addAll(recipient.validateFields("Rechnungsempfänger")); errors.addAll(recipient.validateFields("Rechnungsempfänger"));
if (lineItems.isEmpty()) { if (lineItems.isEmpty()) {
@@ -720,9 +690,9 @@ public class MainView extends VerticalLayout {
dueDate.getValue(), dueDate.getValue(),
currency.getValue().trim().toUpperCase(), currency.getValue().trim().toUpperCase(),
paymentTerms.getValue(), paymentTerms.getValue(),
buyerReference.getValue().trim(), "",
iban.getValue().replaceAll("\\s", "").toUpperCase(), sender.ibanNormalized(),
bic.getValue().trim().toUpperCase(), sender.bicNormalized(),
sender.toParty(), sender.toParty(),
recipient.toParty(), recipient.toParty(),
lineItems lineItems
@@ -760,21 +730,6 @@ public class MainView extends VerticalLayout {
errors.add(message); errors.add(message);
} }
/** IBAN-Prüfung nach ISO 13616 (MOD 97-10). */
private static boolean isValidIban(String value) {
String normalized = value.replaceAll("\\s", "").toUpperCase();
if (!normalized.matches("[A-Z]{2}\\d{2}[A-Z0-9]{11,30}")) {
return false;
}
String rearranged = normalized.substring(4) + normalized.substring(0, 4);
int mod = 0;
for (char c : rearranged.toCharArray()) {
int digit = Character.isDigit(c) ? c - '0' : c - 'A' + 10;
mod = digit < 10 ? (mod * 10 + digit) % 97 : (mod * 100 + digit) % 97;
}
return mod == 1;
}
private void showErrors(List<String> messages) { private void showErrors(List<String> messages) {
Notification notification = new Notification(); Notification notification = new Notification();
notification.addThemeVariants(NotificationVariant.LUMO_ERROR); notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
@@ -802,7 +757,10 @@ public class MainView extends VerticalLayout {
private final TextField countryCode = new TextField("Land (ISO-Code, z.B. DE)"); private final TextField countryCode = new TextField("Land (ISO-Code, z.B. DE)");
private final TextField vatId = new TextField("USt-IdNr."); private final TextField vatId = new TextField("USt-IdNr.");
private final EmailField email = new EmailField("E-Mail"); private final EmailField email = new EmailField("E-Mail");
private final TextField phone = new TextField("Telefon");
// Bankverbindung; nur beim Rechnungssteller sichtbar und Pflicht
private final TextField iban = new TextField("IBAN");
private final TextField bic = new TextField("BIC");
PartyForm(boolean vatRequired) { PartyForm(boolean vatRequired) {
this.vatRequired = vatRequired; this.vatRequired = vatRequired;
@@ -824,15 +782,35 @@ public class MainView extends VerticalLayout {
email.setErrorMessage("Gültige E-Mail-Adresse angeben."); email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
// E-Mail direkt beim Verlassen des Feldes prüfen // E-Mail direkt beim Verlassen des Feldes prüfen
email.addValueChangeListener(event -> checkEmail()); email.addValueChangeListener(event -> checkEmail());
add(name, street, zip, city, countryCode, vatId, email);
if (vatRequired) { if (vatRequired) {
phone.setHelperText("Für den Verkäufer-Kontakt (BR-DE-6) empfohlen"); iban.setRequired(true);
iban.setHelperText("Für die Zahlungsangaben (BG-16)");
bic.setRequired(true);
bic.setHelperText("8 oder 11 Stellen, z. B. NOLADE21RZB");
add(iban, bic);
} }
add(name, street, zip, city, countryCode, vatId, email, phone);
setResponsiveSteps(new ResponsiveStep("0", 1), new ResponsiveStep("600px", 3)); setResponsiveSteps(new ResponsiveStep("0", 1), new ResponsiveStep("600px", 3));
} }
/** Belegt die Bankverbindung vor, z. B. aus dem Adressbuch. */
void prefillBank(String ibanValue, String bicValue) {
iban.setValue(ibanValue);
bic.setValue(bicValue);
}
/** IBAN ohne Leerzeichen in Großschreibung. */
String ibanNormalized() {
return iban.getValue().replaceAll("\\s", "").toUpperCase();
}
/** BIC ohne führende/abschließende Leerzeichen in Großschreibung. */
String bicNormalized() {
return bic.getValue().trim().toUpperCase();
}
void prefill(String nameValue, String streetValue, String zipValue, String cityValue, void prefill(String nameValue, String streetValue, String zipValue, String cityValue,
String countryValue, String vatIdValue, String emailValue, String phoneValue) { String countryValue, String vatIdValue, String emailValue) {
name.setValue(nameValue); name.setValue(nameValue);
street.setValue(streetValue); street.setValue(streetValue);
zip.setValue(zipValue); zip.setValue(zipValue);
@@ -840,7 +818,18 @@ public class MainView extends VerticalLayout {
countryCode.setValue(countryValue); countryCode.setValue(countryValue);
vatId.setValue(vatIdValue); vatId.setValue(vatIdValue);
email.setValue(emailValue); email.setValue(emailValue);
phone.setValue(phoneValue); }
/** @return true, wenn alle Pflichtfelder des Adressformulars gefüllt sind. */
boolean requiredFieldsFilled() {
return !name.getValue().isBlank()
&& !street.getValue().isBlank()
&& !zip.getValue().isBlank()
&& !city.getValue().isBlank()
&& !countryCode.getValue().isBlank()
&& (!vatRequired || !vatId.getValue().isBlank())
&& !email.getValue().isBlank()
&& (!vatRequired || (!iban.getValue().isBlank() && !bic.getValue().isBlank()));
} }
/** Meldet jede Wertänderung eines Feldes, z. B. für die Dirty-Erkennung. */ /** Meldet jede Wertänderung eines Feldes, z. B. für die Dirty-Erkennung. */
@@ -852,12 +841,14 @@ public class MainView extends VerticalLayout {
countryCode.addValueChangeListener(event -> listener.run()); countryCode.addValueChangeListener(event -> listener.run());
vatId.addValueChangeListener(event -> listener.run()); vatId.addValueChangeListener(event -> listener.run());
email.addValueChangeListener(event -> listener.run()); email.addValueChangeListener(event -> listener.run());
phone.addValueChangeListener(event -> listener.run()); iban.addValueChangeListener(event -> listener.run());
bic.addValueChangeListener(event -> listener.run());
} }
/** Leert alle Felder; das Land behält den Standardwert "DE". */ /** Leert alle Felder; das Land behält den Standardwert "DE". */
void clearFields() { void clearFields() {
prefill("", "", "", "", "DE", "", "", ""); prefill("", "", "", "", "DE", "", "");
prefillBank("", "");
name.setInvalid(false); name.setInvalid(false);
street.setInvalid(false); street.setInvalid(false);
zip.setInvalid(false); zip.setInvalid(false);
@@ -865,6 +856,8 @@ public class MainView extends VerticalLayout {
countryCode.setInvalid(false); countryCode.setInvalid(false);
vatId.setInvalid(false); vatId.setInvalid(false);
email.setInvalid(false); email.setInvalid(false);
iban.setInvalid(false);
bic.setInvalid(false);
} }
/** @return true, wenn die E-Mail-Adresse gültig (oder leer) ist. */ /** @return true, wenn die E-Mail-Adresse gültig (oder leer) ist. */
@@ -915,6 +908,16 @@ public class MainView extends VerticalLayout {
} else if (!checkEmail()) { } else if (!checkEmail()) {
errors.add(partyLabel + ": E-Mail-Adresse ist ungültig."); errors.add(partyLabel + ": E-Mail-Adresse ist ungültig.");
} }
if (vatRequired) {
if (requireFilled(iban, partyLabel + ": IBAN ist ein Pflichtfeld.", errors)
&& !IbanValidator.isValid(iban.getValue())) {
markInvalid(iban, partyLabel + ": IBAN ist ungültig (Prüfsumme).", errors);
}
if (requireFilled(bic, partyLabel + ": BIC ist ein Pflichtfeld.", errors)
&& !bic.getValue().trim().matches("[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?")) {
markInvalid(bic, partyLabel + ": BIC muss 8 oder 11 Stellen haben, z. B. NOLADE21RZB.", errors);
}
}
return errors; return errors;
} }
@@ -927,7 +930,7 @@ public class MainView extends VerticalLayout {
countryCode.getValue().trim().toUpperCase(), countryCode.getValue().trim().toUpperCase(),
vatId.isEmpty() ? "" : VatIdValidator.normalize(vatId.getValue()), vatId.isEmpty() ? "" : VatIdValidator.normalize(vatId.getValue()),
email.getValue().trim(), email.getValue().trim(),
phone.getValue().trim() ""
); );
} }
} }