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:
@@ -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()
|
""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user