feat: Admin-Firmendaten, Rechnungserstellung für Kunden mit Pro-rata-Abrechnung und Ultimo-Erinnerung

- Neue Seite /admin-profile: Firmendaten des Systembetreibers bearbeiten (MongoDB-persistiert), erreichbar über "Profil anzeigen" im Admin-Menü; Rechnungssteller-Daten aller System-Rechnungen kommen aus dem gespeicherten Profil
- Neue Seite /admin-create-invoices mit Sidebar-Menüpunkt "Rechnungen erstellen": alle aktiven Kunden mit wählbarem Abrechnungsmonat (aktuell bis 12 Monate zurück), Fälligkeit zum Ultimo, anteilige Berechnung bei Monatsstart mitten im Monat, Einzel- und Sammel-PDF (zusammengeführt, eine Rechnung pro Seite) aus dem System-Template
- Rechnungsgenerator: Undo-Vorbereitung aus Vorsession ergänzt um Firmendaten-Anbindung; Menüpunkt "Admin-Einstellungen" entfernt
- MonthlySchedulerService: tägliche Prüfung, drei Tage vor Ultimo Erinnerungsmail an alle Administratoren
- Session-Persistenz profilabhängig: dev und production aktiviert, Basis-Default aus (behebt SESSIONS.ser-Deserialisierungsfehler in anderen Kontexten)
- ShowJobsView: überflüssiges Feld entfernt; AssertJ catchThrowableOfType auf neue Signatur umgestellt; tsconfig ignoreDeprecations
- Übersetzungen für alle neuen Oberflächen in 10 Sprachen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 17:53:25 +02:00
parent 2313618c05
commit a7ea4fa803
26 changed files with 1283 additions and 31 deletions

View File

@@ -0,0 +1,52 @@
package de.assecutor.votianlt.model;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
/**
* Editable company data of the system operator (admin). Used as issuer data
* on system invoices to the users; falls back to the defaults hardcoded in
* SystemInvoiceData as long as no profile has been saved.
*/
@Data
@Document(collection = "system_company_profile")
public class SystemCompanyProfile {
@Id
private String id;
@Field("company_name")
private String companyName;
@Field("company_subtitle")
private String companySubtitle;
@Field("company_street")
private String companyStreet;
@Field("company_city")
private String companyCity;
@Field("company_phone")
private String companyPhone;
@Field("company_fax")
private String companyFax;
@Field("company_email")
private String companyEmail;
@Field("company_website")
private String companyWebsite;
@Field("sender_line")
private String senderLine;
@Field("payment_terms")
private String paymentTerms;
@Field("footer_text")
private String footerText;
}

View File

@@ -19,7 +19,6 @@ import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
import de.assecutor.votianlt.pages.view.EditProfileView;
import de.assecutor.votianlt.security.SecurityService;
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@@ -105,6 +104,8 @@ public final class AdminLayout extends AppLayout {
SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD));
SideNavItem invoiceGenerator = new SideNavItem("Rechnungsgenerator", "invoice-generator",
new Icon(VaadinIcon.FILE_PROCESS));
SideNavItem createInvoices = new SideNavItem("Rechnungen erstellen", "admin-create-invoices",
new Icon(VaadinIcon.INVOICE));
SideNavItem priceTable = new SideNavItem("Preis-Tabelle", "admin-price-table", new Icon(VaadinIcon.COG));
// SideNavItem systemSettings = new SideNavItem("Systemeinstellungen",
// "admin-settings", new Icon(VaadinIcon.COG));
@@ -115,6 +116,7 @@ public final class AdminLayout extends AppLayout {
nav.addItem(dashboard);
nav.addItem(invoiceGenerator);
nav.addItem(createInvoices);
nav.addItem(priceTable);
// nav.addItem(systemSettings);
// nav.addItem(userManagement);
@@ -149,9 +151,8 @@ public final class AdminLayout extends AppLayout {
var userMenuItem = userMenu.addItem(avatar);
userMenuItem.add(userNameSpan);
// Profile display with navigation
userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> UI.getCurrent().navigate(EditProfileView.class));
userMenuItem.getSubMenu().addItem("Admin-Einstellungen");
// Profile display with navigation to the admin company profile
userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> UI.getCurrent().navigate("admin-profile"));
userMenuItem.getSubMenu().addItem("Abmelden", e -> openLogoutConfirmDialog());
// Update function for username and avatar

View File

@@ -0,0 +1,488 @@
package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.IFrame;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.PriceTable;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.model.invoices.SystemInvoiceData;
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.repository.PriceTableRepository;
import de.assecutor.votianlt.repository.UserRepository;
import de.assecutor.votianlt.service.CustomerInvoiceService;
import de.assecutor.votianlt.service.InvoiceTemplateService;
import de.assecutor.votianlt.service.SystemCompanyProfileService;
import de.assecutor.votianlt.util.DateTimeFormatUtil;
import jakarta.annotation.security.RolesAllowed;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Admin view to create the monthly system invoices for all current customers
* (the users of the system). Invoices are always due at the end of the month;
* customers starting mid-month are only billed pro rata for the remaining days
* until the last day of the month.
*/
@Route(value = "admin-create-invoices", layout = AdminLayout.class)
@RolesAllowed("ADMIN")
@Slf4j
public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
private static final BigDecimal SYSTEM_VAT_RATE = new BigDecimal("0.19");
private final UserRepository userRepository;
private final PriceTableRepository priceTableRepository;
private final InvoiceTemplateService invoiceTemplateService;
private final CustomerInvoiceService customerInvoiceService;
private final SystemCompanyProfileService systemCompanyProfileService;
/** Billing month, selectable by the user; defaults to the current month. */
private YearMonth billingMonth = YearMonth.now();
public AdminCreateInvoicesView(UserRepository userRepository, PriceTableRepository priceTableRepository,
InvoiceTemplateService invoiceTemplateService, CustomerInvoiceService customerInvoiceService,
SystemCompanyProfileService systemCompanyProfileService) {
this.userRepository = userRepository;
this.priceTableRepository = priceTableRepository;
this.invoiceTemplateService = invoiceTemplateService;
this.customerInvoiceService = customerInvoiceService;
this.systemCompanyProfileService = systemCompanyProfileService;
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
addClassName("data-view");
Button createSelectedButton = new Button(getTranslation("admincreateinvoices.button.createselected", 0));
createSelectedButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
createSelectedButton.setEnabled(false);
// Billing month selection: current month and up to 12 months back
Select<YearMonth> monthSelect = new Select<>();
monthSelect.setLabel(getTranslation("admincreateinvoices.field.month"));
List<YearMonth> months = new ArrayList<>();
for (int offset = 0; offset >= -12; offset--) {
months.add(YearMonth.now().plusMonths(offset));
}
monthSelect.setItems(months);
monthSelect.setItemLabelGenerator(
month -> month.format(DateTimeFormatter.ofPattern("LLLL yyyy", getLocale())));
monthSelect.setValue(billingMonth);
add(new ViewToolbar(getTranslation("admincreateinvoices.title")));
// Month selection left-aligned below the heading
monthSelect.getStyle().set("align-self", "flex-start");
add(monthSelect);
Paragraph hint = new Paragraph(getTranslation("admincreateinvoices.hint"));
hint.addClassNames(LumoUtility.TextColor.SECONDARY, LumoUtility.FontSize.SMALL, LumoUtility.Margin.NONE);
add(hint);
Grid<BillingInfo> grid = new Grid<>();
grid.setItems(loadBillingInfos());
grid.addColumn(info -> customerDisplayName(info.user()))
.setHeader(getTranslation("admincreateinvoices.column.customer")).setFlexGrow(2).setSortable(true);
grid.addColumn(info -> info.user().getEmail()).setHeader(getTranslation("admincreateinvoices.column.email"))
.setFlexGrow(2);
grid.addColumn(info -> info.startDate() != null ? DateTimeFormatUtil.formatDate(info.startDate()) : "")
.setHeader(getTranslation("admincreateinvoices.column.start")).setFlexGrow(1).setSortable(true);
grid.addColumn(info -> info.billedDays() < info.daysInMonth()
? getTranslation("admincreateinvoices.days.partial", info.billedDays(), info.daysInMonth())
: getTranslation("admincreateinvoices.days.full"))
.setHeader(getTranslation("admincreateinvoices.column.days")).setFlexGrow(1);
grid.addColumn(info -> DateTimeFormatUtil.formatDate(info.dueDate()))
.setHeader(getTranslation("admincreateinvoices.column.due")).setFlexGrow(1);
grid.addColumn(info -> formatAmount(info.netTotal()) + "")
.setHeader(getTranslation("admincreateinvoices.column.net")).setFlexGrow(1)
.setTextAlign(com.vaadin.flow.component.grid.ColumnTextAlign.END);
grid.addComponentColumn(info -> {
Button createButton = new Button(getTranslation("admincreateinvoices.button.create"));
createButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SMALL);
createButton.addClickListener(e -> createInvoicePdf(info));
return createButton;
}).setAutoWidth(true).setFlexGrow(0).setFrozenToEnd(true);
grid.setSelectionMode(Grid.SelectionMode.MULTI);
grid.addSelectionListener(event -> {
int count = event.getAllSelectedItems().size();
createSelectedButton.setText(getTranslation("admincreateinvoices.button.createselected", count));
createSelectedButton.setEnabled(count > 0);
});
createSelectedButton.addClickListener(e -> createInvoicesPdf(new ArrayList<>(grid.getSelectedItems())));
monthSelect.addValueChangeListener(event -> {
if (event.getValue() != null) {
billingMonth = event.getValue();
grid.deselectAll();
grid.setItems(loadBillingInfos());
}
});
grid.setSizeFull();
grid.addClassName("data-grid");
Div gridPanel = new Div(grid);
gridPanel.addClassNames("surface-panel", "data-grid-panel");
gridPanel.setSizeFull();
gridPanel.getStyle().set("min-width", "0");
add(gridPanel);
// Action bar below the table
com.vaadin.flow.component.orderedlayout.HorizontalLayout actionLayout = new com.vaadin.flow.component.orderedlayout.HorizontalLayout(
createSelectedButton);
actionLayout.setWidthFull();
actionLayout.setJustifyContentMode(
com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode.START);
actionLayout.getStyle().set("flex-shrink", "0");
add(actionLayout);
}
// ==========================================
// Billing calculation
// ==========================================
/**
* Billing data of one customer for the current month.
*/
public record BillingInfo(User user, LocalDate startDate, int billedDays, int daysInMonth, LocalDate dueDate,
List<Map<String, String>> positions, BigDecimal netTotal, BigDecimal vatTotal, BigDecimal grossTotal) {
}
private List<BillingInfo> loadBillingInfos() {
PriceTable priceTable = priceTableRepository.findAll().stream().findFirst().orElse(null);
return userRepository.findAll().stream().filter(this::isCurrentCustomer)
.filter(this::existedInBillingMonth).map(user -> computeBilling(user, priceTable)).toList();
}
/**
* Customers who only started after the billing month are not billed for it.
*/
private boolean existedInBillingMonth(User user) {
if (user.getCreatedAt() == null) {
return true;
}
return !user.getCreatedAt().toLocalDate().isAfter(billingMonth.atEndOfMonth());
}
/**
* Current customers are all activated users without the ADMIN role.
*/
private boolean isCurrentCustomer(User user) {
boolean isAdmin = user.getRoles() != null && user.getRoles().contains("ADMIN");
return !isAdmin && user.getIsActivated() == 1;
}
private BillingInfo computeBilling(User user, PriceTable priceTable) {
YearMonth month = billingMonth;
int daysInMonth = month.lengthOfMonth();
LocalDate dueDate = month.atEndOfMonth();
LocalDate startDate = user.getCreatedAt() != null ? user.getCreatedAt().toLocalDate() : null;
int billedDays = daysInMonth;
if (startDate != null && YearMonth.from(startDate).equals(month)) {
// Customer started mid-month: bill only the remaining days until
// the last day of the month (start day included)
billedDays = daysInMonth - startDate.getDayOfMonth() + 1;
}
BigDecimal factor = new BigDecimal(billedDays).divide(new BigDecimal(daysInMonth), 10, RoundingMode.HALF_UP);
boolean proRata = billedDays < daysInMonth;
List<Map<String, String>> positions = new ArrayList<>();
BigDecimal netTotal = BigDecimal.ZERO;
String[][] entries = {
{ getTranslation("adminpricetable.field.monthly"),
priceTable != null ? priceTable.getMonthlyBasePackage() : null },
{ getTranslation("adminpricetable.field.applicense"),
priceTable != null ? priceTable.getAppUsageLicense() : null },
{ getTranslation("adminpricetable.field.revenue"),
priceTable != null ? priceTable.getRevenueParticipation() : null } };
for (String[] entry : entries) {
String rawValue = entry[1] != null ? entry[1].trim() : "";
BigDecimal amount = parseAmount(rawValue);
Map<String, String> position = new HashMap<>();
if (amount != null) {
BigDecimal billed = amount.multiply(factor).setScale(2, RoundingMode.HALF_UP);
String name = entry[0];
if (proRata) {
name += " " + getTranslation("admincreateinvoices.prorata.suffix", billedDays, daysInMonth);
}
position.put("name", name);
position.put("netAmount", formatAmount(billed));
netTotal = netTotal.add(billed);
} else {
// Non-monetary values (e.g. "15 %" revenue participation) are
// shown as-is and are not part of the totals
position.put("name", entry[0]);
position.put("netAmount", rawValue.isEmpty() ? "-" : rawValue);
}
positions.add(position);
}
BigDecimal vatTotal = netTotal.multiply(SYSTEM_VAT_RATE).setScale(2, RoundingMode.HALF_UP);
BigDecimal grossTotal = netTotal.add(vatTotal);
return new BillingInfo(user, startDate, billedDays, daysInMonth, dueDate, positions, netTotal, vatTotal,
grossTotal);
}
// ==========================================
// PDF generation
// ==========================================
private void createInvoicePdf(BillingInfo info) {
String templateData = loadSystemTemplateData();
if (templateData == null) {
return;
}
try {
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData,
buildInvoiceVariables(info), null);
showPdfInDialog(pdfBytes, buildInvoiceNumber(info.user()));
} catch (Exception ex) {
log.error("Error creating system invoice for user {}: {}", info.user().getEmail(), ex.getMessage(), ex);
Notification
.show(getTranslation("admincreateinvoices.notification.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_CENTER)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
}
/**
* Creates the invoices of all selected customers as one merged PDF, one
* invoice per page.
*/
private void createInvoicesPdf(List<BillingInfo> infos) {
if (infos.isEmpty()) {
return;
}
String templateData = loadSystemTemplateData();
if (templateData == null) {
return;
}
try {
List<byte[]> pdfs = new ArrayList<>();
for (BillingInfo info : infos) {
pdfs.add(customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData,
buildInvoiceVariables(info), null));
}
String fileName = "rechnungen-" + billingMonth.format(DateTimeFormatter.ofPattern("yyyyMM"));
showPdfInDialog(mergePdfs(pdfs), fileName);
} catch (Exception ex) {
log.error("Error creating system invoices for {} users: {}", infos.size(), ex.getMessage(), ex);
Notification
.show(getTranslation("admincreateinvoices.notification.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_CENTER)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
}
/**
* Returns the stored system template data, or null (with a notification)
* if no template has been saved yet.
*/
private String loadSystemTemplateData() {
Optional<de.assecutor.votianlt.model.InvoiceTemplate> template = invoiceTemplateService
.getTemplateByUserId(InvoiceGeneratorView.SYSTEM_TEMPLATE_USER_ID);
if (template.isEmpty() || template.get().getTemplateData() == null
|| template.get().getTemplateData().isEmpty()) {
Notification
.show(getTranslation("admincreateinvoices.notification.notemplate"), 5000,
Notification.Position.BOTTOM_CENTER)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return null;
}
return template.get().getTemplateData();
}
private byte[] mergePdfs(List<byte[]> pdfs) throws Exception {
if (pdfs.size() == 1) {
return pdfs.get(0);
}
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
com.itextpdf.kernel.pdf.PdfDocument merged = new com.itextpdf.kernel.pdf.PdfDocument(
new com.itextpdf.kernel.pdf.PdfWriter(baos));
com.itextpdf.kernel.utils.PdfMerger merger = new com.itextpdf.kernel.utils.PdfMerger(merged);
for (byte[] pdf : pdfs) {
com.itextpdf.kernel.pdf.PdfDocument source = new com.itextpdf.kernel.pdf.PdfDocument(
new com.itextpdf.kernel.pdf.PdfReader(new java.io.ByteArrayInputStream(pdf)));
merger.merge(source, 1, source.getNumberOfPages());
source.close();
}
merged.close();
return baos.toByteArray();
}
private Map<String, String> buildInvoiceVariables(BillingInfo info) throws Exception {
SystemInvoiceData issuer = systemCompanyProfileService.createSystemInvoiceData();
User user = info.user();
Map<String, String> variables = new LinkedHashMap<>();
String companyFull = (safe(issuer.getCompanyName()) + " " + safe(issuer.getCompanySubtitle())).trim();
variables.put("masterdata.company_name", companyFull);
variables.put("masterdata.street", safe(issuer.getCompanyStreet()));
variables.put("masterdata.city", safe(issuer.getCompanyCity()));
variables.put("masterdata.phone", safe(issuer.getCompanyPhone()));
variables.put("masterdata.email", safe(issuer.getCompanyEmail()));
variables.put("masterdata.website", safe(issuer.getCompanyWebsite()));
variables.put("masterdata.sender_line", safe(issuer.getSenderLine()));
variables.put("masterdata.payment_terms", safe(issuer.getPaymentTerms()));
variables.put("masterdata.footer", safe(issuer.getFooterText()).replace("<br>", "\n"));
variables.put("masterdata.invoice_number", buildInvoiceNumber(user));
variables.put("masterdata.invoice_date",
LocalDate.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy")));
// Recipient: the user's invoice address if a different one is set
if (user.isDiffInvoiceAddress()) {
variables.put("customer.company_name", firstNonBlank(user.getInvCompany(), user.getCompany()));
variables.put("customer.contact_name",
(safe(user.getInvFirstname()) + " " + safe(user.getInvLastname())).trim());
variables.put("customer.street",
(safe(user.getInvStreet()) + " " + safe(user.getInvHouseNumber())).trim());
variables.put("customer.city", (safe(user.getInvZip()) + " " + safe(user.getInvCity())).trim());
} else {
variables.put("customer.company_name", safe(user.getCompany()));
variables.put("customer.contact_name", (safe(user.getFirstname()) + " " + safe(user.getName())).trim());
variables.put("customer.street", (safe(user.getStreet()) + " " + safe(user.getHouseNumber())).trim());
variables.put("customer.city", (safe(user.getZip()) + " " + safe(user.getCity())).trim());
}
variables.put("customer.email", safe(user.getEmail()));
variables.put("customer.phone", safe(user.getPhone()));
// Positions and totals
variables.put("services.json",
new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(info.positions()));
variables.put("services.count", String.valueOf(info.positions().size()));
variables.put("invoice.net_total", formatAmount(info.netTotal()) + "");
variables.put("invoice.vat_total", formatAmount(info.vatTotal()) + "");
variables.put("invoice.gross_total", formatAmount(info.grossTotal()) + "");
variables.put("invoice.vat_rate",
SYSTEM_VAT_RATE.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + "%");
variables.put("invoice.due_date", DateTimeFormatUtil.formatDate(info.dueDate()));
variables.put("services.net_total", formatAmount(info.netTotal()) + "");
variables.put("services.gross_total", formatAmount(info.grossTotal()) + "");
return variables;
}
private String buildInvoiceNumber(User user) {
String monthPart = billingMonth.format(DateTimeFormatter.ofPattern("yyyyMM"));
String userPart = user.getUsrId() > 0 ? String.valueOf(user.getUsrId())
: (user.getId() != null ? user.getId().toHexString().substring(20) : "0");
return "SYS-" + monthPart + "-" + userPart;
}
private void showPdfInDialog(byte[] pdfBytes, String invoiceNumber) {
Dialog pdfDialog = DialogStylingHelper.createStyledDialog(getTranslation("invoicegenerator.pdf.preview.title"),
"90vw");
pdfDialog.setHeight("90vh");
Div pdfContainer = new Div();
pdfContainer.setWidth("100%");
pdfContainer.setHeight("100%");
String base64Pdf = Base64.getEncoder().encodeToString(pdfBytes);
IFrame pdfFrame = new IFrame();
pdfFrame.setWidth("100%");
pdfFrame.setHeight("100%");
pdfFrame.getElement().setAttribute("src", "data:application/pdf;base64," + base64Pdf);
pdfContainer.add(pdfFrame);
Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close());
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button downloadButton = new Button(getTranslation("button.download"), e -> getElement()
.executeJs("const link = document.createElement('a');" + "link.href = 'data:application/pdf;base64,"
+ base64Pdf + "';" + "link.download = '" + invoiceNumber + ".pdf';" + "link.click();"));
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
pdfDialog.add(DialogStylingHelper.wrapContent(pdfContainer, true));
pdfDialog.getFooter().add(downloadButton, closeButton);
pdfDialog.open();
}
// ==========================================
// Helpers
// ==========================================
private String customerDisplayName(User user) {
String company = safe(user.getCompany()).trim();
if (!company.isEmpty()) {
return company;
}
return (safe(user.getFirstname()) + " " + safe(user.getName())).trim();
}
/**
* Parse a price table value like "99,00 €" or "1.234,56" into an amount.
* Returns null for non-monetary values such as "15 %".
*/
private BigDecimal parseAmount(String value) {
if (value == null || value.isBlank() || value.contains("%")) {
return null;
}
Matcher matcher = Pattern.compile("([0-9]{1,3}(?:\\.[0-9]{3})*(?:,[0-9]+)?|[0-9]+(?:[.,][0-9]+)?)")
.matcher(value);
if (!matcher.find()) {
return null;
}
String number = matcher.group(1);
if (number.contains(",")) {
number = number.replace(".", "").replace(',', '.');
}
try {
return new BigDecimal(number);
} catch (NumberFormatException ex) {
return null;
}
}
private String formatAmount(BigDecimal amount) {
return amount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",");
}
private String safe(String value) {
return value != null ? value : "";
}
private String firstNonBlank(String... values) {
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return "";
}
@Override
public String getPageTitle() {
return getTranslation("page.title.admin.createinvoices");
}
}

View File

@@ -0,0 +1,155 @@
package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.SystemCompanyProfile;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.service.SystemCompanyProfileService;
import jakarta.annotation.security.RolesAllowed;
import lombok.extern.slf4j.Slf4j;
/**
* Admin profile page for editing the company data of the system operator.
* These values are used as issuer data on system invoices to the users.
*/
@Route(value = "admin-profile", layout = AdminLayout.class)
@RolesAllowed("ADMIN")
@Slf4j
public class AdminProfileView extends VerticalLayout implements HasDynamicTitle {
private final SystemCompanyProfileService profileService;
private final TextField companyName = new TextField();
private final TextField companySubtitle = new TextField();
private final TextField companyStreet = new TextField();
private final TextField companyCity = new TextField();
private final TextField companyPhone = new TextField();
private final TextField companyFax = new TextField();
private final TextField companyEmail = new TextField();
private final TextField companyWebsite = new TextField();
private final TextField senderLine = new TextField();
private final TextArea paymentTerms = new TextArea();
private final TextArea footerText = new TextArea();
private SystemCompanyProfile profile;
public AdminProfileView(SystemCompanyProfileService profileService) {
this.profileService = profileService;
setSpacing(false);
setPadding(false);
setWidthFull();
addClassName("admin-form-view");
add(new ViewToolbar(getTranslation("adminprofile.title")));
companyName.setLabel(getTranslation("adminprofile.field.companyname"));
companySubtitle.setLabel(getTranslation("adminprofile.field.companysubtitle"));
companyStreet.setLabel(getTranslation("adminprofile.field.street"));
companyCity.setLabel(getTranslation("adminprofile.field.city"));
companyPhone.setLabel(getTranslation("adminprofile.field.phone"));
companyFax.setLabel(getTranslation("adminprofile.field.fax"));
companyEmail.setLabel(getTranslation("adminprofile.field.email"));
companyWebsite.setLabel(getTranslation("adminprofile.field.website"));
senderLine.setLabel(getTranslation("adminprofile.field.senderline"));
senderLine.setHelperText(getTranslation("adminprofile.field.senderline.helper"));
paymentTerms.setLabel(getTranslation("adminprofile.field.paymentterms"));
paymentTerms.setMinHeight("80px");
footerText.setLabel(getTranslation("adminprofile.field.footer"));
footerText.setHelperText(getTranslation("adminprofile.field.footer.helper"));
footerText.setMinHeight("120px");
FormLayout form = new FormLayout();
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("600px", 2));
form.add(companyName, companySubtitle, companyStreet, companyCity, companyPhone, companyFax, companyEmail,
companyWebsite);
form.add(senderLine, 2);
form.add(paymentTerms, 2);
form.add(footerText, 2);
VerticalLayout fieldsLayout = new VerticalLayout(form);
fieldsLayout.setSpacing(true);
fieldsLayout.setPadding(false);
fieldsLayout.addClassNames("form-shell", "form-card");
add(fieldsLayout);
Button saveButton = new Button(getTranslation("button.savechanges"), e -> saveProfile());
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Div actions = new Div(saveButton);
actions.addClassNames("form-shell", "simple-card");
add(actions);
loadProfile();
}
private void loadProfile() {
try {
profile = profileService.getOrCreateProfile();
companyName.setValue(nvl(profile.getCompanyName()));
companySubtitle.setValue(nvl(profile.getCompanySubtitle()));
companyStreet.setValue(nvl(profile.getCompanyStreet()));
companyCity.setValue(nvl(profile.getCompanyCity()));
companyPhone.setValue(nvl(profile.getCompanyPhone()));
companyFax.setValue(nvl(profile.getCompanyFax()));
companyEmail.setValue(nvl(profile.getCompanyEmail()));
companyWebsite.setValue(nvl(profile.getCompanyWebsite()));
senderLine.setValue(nvl(profile.getSenderLine()));
paymentTerms.setValue(nvl(profile.getPaymentTerms()));
// Footer is stored with <br> line breaks; edit it as multi-line text
footerText.setValue(nvl(profile.getFooterText()).replace("<br>", "\n"));
} catch (Exception ex) {
log.error("Error loading system company profile: {}", ex.getMessage(), ex);
Notification
.show(getTranslation("adminprofile.notification.load.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_CENTER)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
}
private void saveProfile() {
try {
profile.setCompanyName(companyName.getValue().trim());
profile.setCompanySubtitle(companySubtitle.getValue().trim());
profile.setCompanyStreet(companyStreet.getValue().trim());
profile.setCompanyCity(companyCity.getValue().trim());
profile.setCompanyPhone(companyPhone.getValue().trim());
profile.setCompanyFax(companyFax.getValue().trim());
profile.setCompanyEmail(companyEmail.getValue().trim());
profile.setCompanyWebsite(companyWebsite.getValue().trim());
profile.setSenderLine(senderLine.getValue().trim());
profile.setPaymentTerms(paymentTerms.getValue().trim());
profile.setFooterText(footerText.getValue().trim().replace("\n", "<br>"));
profile = profileService.save(profile);
Notification
.show(getTranslation("adminprofile.notification.saved"), 3000,
Notification.Position.BOTTOM_CENTER)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} catch (Exception ex) {
log.error("Error saving system company profile: {}", ex.getMessage(), ex);
Notification
.show(getTranslation("adminprofile.notification.save.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_CENTER)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
}
private String nvl(String value) {
return value != null ? value : "";
}
@Override
public String getPageTitle() {
return getTranslation("page.title.admin.profile");
}
}

View File

@@ -27,6 +27,7 @@ import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.repository.PriceTableRepository;
import de.assecutor.votianlt.service.CustomerInvoiceService;
import de.assecutor.votianlt.service.InvoiceTemplateService;
import de.assecutor.votianlt.service.SystemCompanyProfileService;
import jakarta.annotation.security.RolesAllowed;
import lombok.extern.slf4j.Slf4j;
@@ -68,14 +69,17 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
private final CustomerInvoiceService customerInvoiceService;
private final InvoiceTemplateService invoiceTemplateService;
private final PriceTableRepository priceTableRepository;
private final SystemCompanyProfileService systemCompanyProfileService;
private VerticalLayout propertiesPanel;
public InvoiceGeneratorView(CustomerInvoiceService customerInvoiceService,
InvoiceTemplateService invoiceTemplateService, PriceTableRepository priceTableRepository) {
InvoiceTemplateService invoiceTemplateService, PriceTableRepository priceTableRepository,
SystemCompanyProfileService systemCompanyProfileService) {
this.customerInvoiceService = customerInvoiceService;
this.invoiceTemplateService = invoiceTemplateService;
this.priceTableRepository = priceTableRepository;
this.systemCompanyProfileService = systemCompanyProfileService;
setId("invoice-generator-view");
addClassNames("invoice-generator-view", "admin-form-view");
@@ -174,7 +178,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
* invoice is generated.
*/
private Map<String, String> buildSystemVariables() {
SystemInvoiceData data = new SystemInvoiceData();
SystemInvoiceData data = systemCompanyProfileService.createSystemInvoiceData();
Map<String, String> variables = new LinkedHashMap<>();
String companyFull = (safe(data.getCompanyName()) + " " + safe(data.getCompanySubtitle())).trim();
variables.put("masterdata.company_name", companyFull);
@@ -853,13 +857,6 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
layout.setAlignItems(Alignment.CENTER);
layout.addClassName("invoice-generator-actionbar");
Button clearButton = new Button(getTranslation("invoicegenerator.button.clear"), new Icon(VaadinIcon.TRASH));
clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
clearButton.addClickListener(e -> {
getElement().executeJs("if (window.clearProfileCanvas) { window.clearProfileCanvas(); }");
showNotification(getTranslation("invoicegenerator.notification.cleared"));
});
Button undoButton = new Button(getTranslation("button.undo"), new Icon(VaadinIcon.ARROW_BACKWARD));
undoButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
undoButton.addClickListener(
@@ -873,7 +870,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
saveTemplateButton.addClickListener(e -> saveSystemTemplate());
layout.add(undoButton, clearButton, previewButton, saveTemplateButton);
layout.add(undoButton, previewButton, saveTemplateButton);
return layout;
}

View File

@@ -23,6 +23,7 @@ import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.server.StreamRegistration;
import de.assecutor.votianlt.service.SystemCompanyProfileService;
import de.assecutor.votianlt.service.SystemInvoiceService;
import de.assecutor.votianlt.util.DateTimeFormatUtil;
import de.assecutor.votianlt.model.invoices.SystemInvoiceData;
@@ -49,11 +50,14 @@ public class MyInvoicesView extends Main implements HasDynamicTitle {
private final List<MyInvoiceRow> allRows = new ArrayList<>(); // zunächst leer
private final Div emptyState = new Div();
private final SystemInvoiceService systemInvoiceService;
private final SystemCompanyProfileService systemCompanyProfileService;
private static final NumberFormat CURRENCY_FMT = NumberFormat.getCurrencyInstance(Locale.GERMANY);
public MyInvoicesView(SystemInvoiceService systemInvoiceService) {
public MyInvoicesView(SystemInvoiceService systemInvoiceService,
SystemCompanyProfileService systemCompanyProfileService) {
this.systemInvoiceService = systemInvoiceService;
this.systemCompanyProfileService = systemCompanyProfileService;
addClassName("data-view");
getStyle().set("max-width", "1100px");
getStyle().set("margin-left", "auto");
@@ -272,7 +276,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle {
}
private byte[] generateSystemInvoicePdf(MyInvoiceRow row) throws Exception {
SystemInvoiceData data = new SystemInvoiceData();
SystemInvoiceData data = systemCompanyProfileService.createSystemInvoiceData();
data.setInvoiceNumber(row.invoiceNumber());
data.setInvoiceDate(DateTimeFormatUtil.formatDate(row.date()));
data.setInvoiceText("Rechnung " + row.invoiceNumber());

View File

@@ -50,7 +50,6 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
private final SecurityService securityService;
private final ClientConnectionService clientConnectionService;
private final MessagingPublisher messagingPublisher;
private final CustomerInvoiceRepository customerInvoiceRepository;
private final Grid<Job> grid = new Grid<>(Job.class, false);
@Autowired
@@ -62,7 +61,6 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
this.securityService = securityService;
this.clientConnectionService = clientConnectionService;
this.messagingPublisher = messagingPublisher;
this.customerInvoiceRepository = customerInvoiceRepository;
setSizeFull();
setPadding(true);
setSpacing(true);

View File

@@ -0,0 +1,9 @@
package de.assecutor.votianlt.repository;
import de.assecutor.votianlt.model.SystemCompanyProfile;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SystemCompanyProfileRepository extends MongoRepository<SystemCompanyProfile, String> {
}

View File

@@ -1,5 +1,7 @@
package de.assecutor.votianlt.service;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
@@ -7,6 +9,9 @@ import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
/**
* Service für monatliche Scheduler-Aufgaben, die am letzten Tag des Monats
@@ -17,6 +22,61 @@ public class MonthlySchedulerService {
private static final Logger logger = LoggerFactory.getLogger(MonthlySchedulerService.class);
private final EmailService emailService;
private final UserRepository userRepository;
public MonthlySchedulerService(EmailService emailService, UserRepository userRepository) {
this.emailService = emailService;
this.userRepository = userRepository;
}
/**
* Läuft täglich um 08:00 Uhr und erinnert die Administratoren drei Tage vor
* Ultimo per E-Mail daran, die Monatsrechnungen fertigzustellen.
*/
@Scheduled(cron = "0 0 8 * * *")
public void checkAndSendInvoiceReminder() {
LocalDate today = LocalDate.now();
LocalDate lastDayOfMonth = YearMonth.from(today).atEndOfMonth();
if (!today.equals(lastDayOfMonth.minusDays(3))) {
return;
}
logger.info("Drei Tage vor Ultimo ({}): Sende Rechnungs-Erinnerung an die Administratoren.", lastDayOfMonth);
sendInvoiceReminderToAdmins(lastDayOfMonth);
}
private void sendInvoiceReminderToAdmins(LocalDate lastDayOfMonth) {
List<User> admins = userRepository.findAll().stream()
.filter(user -> user.getRoles() != null && user.getRoles().contains("ADMIN"))
.filter(user -> user.getEmail() != null && !user.getEmail().isBlank()).toList();
if (admins.isEmpty()) {
logger.warn("Keine Administratoren mit E-Mail-Adresse gefunden, Rechnungs-Erinnerung entfällt.");
return;
}
String monthLabel = YearMonth.from(lastDayOfMonth)
.format(DateTimeFormatter.ofPattern("LLLL yyyy", Locale.GERMAN));
String dueDateLabel = lastDayOfMonth.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
String subject = "Erinnerung: Rechnungen für " + monthLabel + " erstellen";
String body = "Hallo,\n\n" //
+ "in drei Tagen ist Ultimo (" + dueDateLabel + ").\n"
+ "Bitte die Monatsrechnungen für " + monthLabel
+ " fertigstellen: Seite \"Rechnungen erstellen\" im Admin-Bereich (/admin-create-invoices).\n\n"
+ "Diese Nachricht wurde automatisch von VotianLT versendet.";
for (User admin : admins) {
try {
emailService.sendSimpleEmail(admin.getEmail(), subject, body);
logger.info("Rechnungs-Erinnerung an {} gesendet.", admin.getEmail());
} catch (Exception e) {
logger.error("Rechnungs-Erinnerung an {} fehlgeschlagen: {}", admin.getEmail(), e.getMessage(), e);
}
}
}
/**
* Scheduler, der täglich um 23:00 Uhr läuft und prüft, ob heute der letzte Tag
* des Monats ist. Wenn ja, wird die monatliche Aufgabe ausgeführt.

View File

@@ -0,0 +1,78 @@
package de.assecutor.votianlt.service;
import de.assecutor.votianlt.model.SystemCompanyProfile;
import de.assecutor.votianlt.model.invoices.SystemInvoiceData;
import de.assecutor.votianlt.repository.SystemCompanyProfileRepository;
import org.springframework.stereotype.Service;
import java.util.Optional;
/**
* Manages the company data of the system operator. As long as no profile has
* been saved, the defaults from SystemInvoiceData are used.
*/
@Service
public class SystemCompanyProfileService {
private final SystemCompanyProfileRepository repository;
public SystemCompanyProfileService(SystemCompanyProfileRepository repository) {
this.repository = repository;
}
/**
* Returns the stored profile, or a new unsaved instance prefilled with the
* SystemInvoiceData defaults for first-time editing.
*/
public SystemCompanyProfile getOrCreateProfile() {
Optional<SystemCompanyProfile> stored = repository.findAll().stream().findFirst();
if (stored.isPresent()) {
return stored.get();
}
SystemInvoiceData defaults = new SystemInvoiceData();
SystemCompanyProfile profile = new SystemCompanyProfile();
profile.setCompanyName(defaults.getCompanyName());
profile.setCompanySubtitle(defaults.getCompanySubtitle());
profile.setCompanyStreet(defaults.getCompanyStreet());
profile.setCompanyCity(defaults.getCompanyCity());
profile.setCompanyPhone(defaults.getCompanyPhone());
profile.setCompanyFax(defaults.getCompanyFax());
profile.setCompanyEmail(defaults.getCompanyEmail());
profile.setCompanyWebsite(defaults.getCompanyWebsite());
profile.setSenderLine(defaults.getSenderLine());
profile.setPaymentTerms(defaults.getPaymentTerms());
profile.setFooterText(defaults.getFooterText());
return profile;
}
public SystemCompanyProfile save(SystemCompanyProfile profile) {
return repository.save(profile);
}
/**
* Creates a SystemInvoiceData whose issuer fields come from the stored
* profile. Once a profile exists it is the source of truth for all issuer
* fields; without one the hardcoded defaults remain.
*/
public SystemInvoiceData createSystemInvoiceData() {
SystemInvoiceData data = new SystemInvoiceData();
repository.findAll().stream().findFirst().ifPresent(profile -> {
data.setCompanyName(nvl(profile.getCompanyName()));
data.setCompanySubtitle(nvl(profile.getCompanySubtitle()));
data.setCompanyStreet(nvl(profile.getCompanyStreet()));
data.setCompanyCity(nvl(profile.getCompanyCity()));
data.setCompanyPhone(nvl(profile.getCompanyPhone()));
data.setCompanyFax(nvl(profile.getCompanyFax()));
data.setCompanyEmail(nvl(profile.getCompanyEmail()));
data.setCompanyWebsite(nvl(profile.getCompanyWebsite()));
data.setSenderLine(nvl(profile.getSenderLine()));
data.setPaymentTerms(nvl(profile.getPaymentTerms()));
data.setFooterText(nvl(profile.getFooterText()));
});
return data;
}
private String nvl(String value) {
return value != null ? value : "";
}
}

View File

@@ -13,6 +13,12 @@ import java.util.List;
@Service
public class SystemInvoiceService {
private final SystemCompanyProfileService systemCompanyProfileService;
public SystemInvoiceService(SystemCompanyProfileService systemCompanyProfileService) {
this.systemCompanyProfileService = systemCompanyProfileService;
}
public byte[] generateInvoicePdfFromHtml() throws Exception {
// Read the HTML template
String htmlContent = readHtmlTemplate();
@@ -35,7 +41,7 @@ public class SystemInvoiceService {
}
public SystemInvoiceData createSampleInvoiceData() {
SystemInvoiceData data = new SystemInvoiceData();
SystemInvoiceData data = systemCompanyProfileService.createSystemInvoiceData();
// Set sample data
data.setInvoiceNumber("HHA-2021-007");

View File

@@ -6,6 +6,11 @@ spring.data.mongodb.uri=${MONGODB_URI}
# Enable browser launch in development
vaadin.launch-browser=true
# Sessions über Neustarts persistieren (eingeloggt bleiben nach DevTools-Restart).
# Hinweis: Nach inkompatiblen Code-Änderungen kann beim Start eine harmlose
# SESSIONS.ser-Deserialisierungs-Warnung im Log erscheinen.
server.servlet.session.persistent=true
# Development logging levels
logging.level.de.assecutor.votianlt=DEBUG
logging.level.root=INFO

View File

@@ -6,6 +6,11 @@ spring.data.mongodb.uri=${MONGODB_URI}
# Disable browser launch in production
vaadin.launch-browser=false
# Sessions über Neustarts persistieren: Nutzer bleiben nach einem
# Deployment/Neustart eingeloggt. Nach inkompatiblen Code-Änderungen verwirft
# Tomcat die alte SESSIONS.ser mit einer harmlosen Log-Warnung.
server.servlet.session.persistent=true
# 2FA Configuration - Enabled for production
app.security.two-factor.enabled=true

View File

@@ -1,5 +1,8 @@
server.port=${PORT:8082}
server.address=0.0.0.0
# Sessions standardmäßig nicht über Neustarts persistieren; nur im dev-Profil aktiviert
# (siehe application-dev.properties)
server.servlet.session.persistent=false
# Default active profile
spring.profiles.active=dev

View File

@@ -1086,3 +1086,42 @@ admindashboard.customers.empty=Keine Kunden vorhanden
invoicegenerator.positions.header=Positionen (Preistabelle)
invoicegenerator.positions.list=Positionen auflisten
button.undo=Rückgängig
# Admin-Profil (Firmendaten)
page.title.admin.profile=Firmendaten bearbeiten
adminprofile.title=Firmendaten
adminprofile.field.companyname=Firmenname
adminprofile.field.companysubtitle=Firmenzusatz
adminprofile.field.street=Straße und Hausnummer
adminprofile.field.city=PLZ und Ort
adminprofile.field.phone=Telefon
adminprofile.field.fax=Fax
adminprofile.field.email=E-Mail
adminprofile.field.website=Webseite
adminprofile.field.senderline=Absenderzeile
adminprofile.field.senderline.helper=Einzeilige Absenderangabe über der Empfängeradresse auf Rechnungen
adminprofile.field.paymentterms=Zahlungsbedingungen
adminprofile.field.footer=Fußzeile
adminprofile.field.footer.helper=Geschäftsführer, Steuernummer, Bankverbindung jede Zeile erscheint als eigene Zeile auf der Rechnung
adminprofile.notification.saved=Firmendaten gespeichert
adminprofile.notification.save.error=Fehler beim Speichern: {0}
adminprofile.notification.load.error=Fehler beim Laden: {0}
# Rechnungen erstellen (Admin)
page.title.admin.createinvoices=Rechnungen erstellen
admincreateinvoices.title=Rechnungen erstellen
admincreateinvoices.hint=Rechnungen werden zum Monatsende (Ultimo) fällig. Beginnt ein Kunde im laufenden Monat, werden nur die verbleibenden Tage bis Ultimo berechnet.
admincreateinvoices.column.customer=Kunde
admincreateinvoices.column.email=E-Mail
admincreateinvoices.column.start=Kunde seit
admincreateinvoices.column.days=Abgerechnete Tage
admincreateinvoices.column.due=Fällig am
admincreateinvoices.column.net=Nettobetrag
admincreateinvoices.days.full=Voller Monat
admincreateinvoices.days.partial={0} von {1} Tagen
admincreateinvoices.prorata.suffix=(anteilig {0}/{1} Tage)
admincreateinvoices.button.create=Rechnung erstellen
admincreateinvoices.notification.notemplate=Kein Rechnungstemplate vorhanden. Bitte zuerst im Rechnungsgenerator ein Template speichern.
admincreateinvoices.notification.error=Fehler beim Erstellen der Rechnung: {0}
admincreateinvoices.button.createselected=Rechnungen erstellen ({0})
admincreateinvoices.field.month=Abrechnungsmonat

View File

@@ -916,3 +916,42 @@ admindashboard.customers.empty=Kliente pole
invoicegenerator.positions.header=Positsioonid (hinnatabel)
invoicegenerator.positions.list=Positsioonide loend
button.undo=Võta tagasi
# Admini profiil (ettevõtte andmed)
page.title.admin.profile=Ettevõtte andmete muutmine
adminprofile.title=Ettevõtte andmed
adminprofile.field.companyname=Ettevõtte nimi
adminprofile.field.companysubtitle=Ettevõtte lisand
adminprofile.field.street=Tänav ja majanumber
adminprofile.field.city=Sihtnumber ja linn
adminprofile.field.phone=Telefon
adminprofile.field.fax=Faks
adminprofile.field.email=E-post
adminprofile.field.website=Veebileht
adminprofile.field.senderline=Saatja rida
adminprofile.field.senderline.helper=Üherealine saatja teave arve saaja aadressi kohal
adminprofile.field.paymentterms=Maksetingimused
adminprofile.field.footer=Jalus
adminprofile.field.footer.helper=Tegevjuhid, maksunumber, pangaandmed iga rida kuvatakse arvel eraldi reana
adminprofile.notification.saved=Ettevõtte andmed salvestatud
adminprofile.notification.save.error=Viga salvestamisel: {0}
adminprofile.notification.load.error=Viga laadimisel: {0}
# Arvete koostamine (admin)
page.title.admin.createinvoices=Arvete koostamine
admincreateinvoices.title=Arvete koostamine
admincreateinvoices.hint=Arved kuuluvad tasumisele kuu lõpus. Kui klient alustab kuu keskel, arvestatakse ainult kuu lõpuni jäänud päevad.
admincreateinvoices.column.customer=Klient
admincreateinvoices.column.email=E-post
admincreateinvoices.column.start=Klient alates
admincreateinvoices.column.days=Arvestatud päevad
admincreateinvoices.column.due=Tähtaeg
admincreateinvoices.column.net=Netosumma
admincreateinvoices.days.full=Terve kuu
admincreateinvoices.days.partial={0} / {1} päeva
admincreateinvoices.prorata.suffix=(proportsionaalselt {0}/{1} päeva)
admincreateinvoices.button.create=Koosta arve
admincreateinvoices.notification.notemplate=Arve mall puudub. Palun salvestage kõigepealt mall arvegeneraatoris.
admincreateinvoices.notification.error=Viga arve koostamisel: {0}
admincreateinvoices.button.createselected=Koosta arved ({0})
admincreateinvoices.field.month=Arveldusperiood

View File

@@ -1086,3 +1086,42 @@ admindashboard.customers.empty=No customers available
invoicegenerator.positions.header=Positions (price table)
invoicegenerator.positions.list=List positions
button.undo=Undo
# Admin profile (company data)
page.title.admin.profile=Edit company data
adminprofile.title=Company data
adminprofile.field.companyname=Company name
adminprofile.field.companysubtitle=Company addition
adminprofile.field.street=Street and number
adminprofile.field.city=ZIP and city
adminprofile.field.phone=Phone
adminprofile.field.fax=Fax
adminprofile.field.email=Email
adminprofile.field.website=Website
adminprofile.field.senderline=Sender line
adminprofile.field.senderline.helper=Single-line sender information above the recipient address on invoices
adminprofile.field.paymentterms=Payment terms
adminprofile.field.footer=Footer
adminprofile.field.footer.helper=Managing directors, tax number, bank details each line appears as its own line on the invoice
adminprofile.notification.saved=Company data saved
adminprofile.notification.save.error=Error while saving: {0}
adminprofile.notification.load.error=Error while loading: {0}
# Create invoices (admin)
page.title.admin.createinvoices=Create invoices
admincreateinvoices.title=Create invoices
admincreateinvoices.hint=Invoices are due at the end of the month. If a customer starts mid-month, only the remaining days until the end of the month are billed.
admincreateinvoices.column.customer=Customer
admincreateinvoices.column.email=Email
admincreateinvoices.column.start=Customer since
admincreateinvoices.column.days=Billed days
admincreateinvoices.column.due=Due on
admincreateinvoices.column.net=Net amount
admincreateinvoices.days.full=Full month
admincreateinvoices.days.partial={0} of {1} days
admincreateinvoices.prorata.suffix=(pro rata {0}/{1} days)
admincreateinvoices.button.create=Create invoice
admincreateinvoices.notification.notemplate=No invoice template available. Please save a template in the invoice generator first.
admincreateinvoices.notification.error=Error creating the invoice: {0}
admincreateinvoices.button.createselected=Create invoices ({0})
admincreateinvoices.field.month=Billing month

View File

@@ -1017,3 +1017,42 @@ admindashboard.customers.empty=No hay clientes
invoicegenerator.positions.header=Posiciones (tabla de precios)
invoicegenerator.positions.list=Listar posiciones
button.undo=Deshacer
# Perfil de administrador (datos de la empresa)
page.title.admin.profile=Editar datos de la empresa
adminprofile.title=Datos de la empresa
adminprofile.field.companyname=Nombre de la empresa
adminprofile.field.companysubtitle=Complemento de la empresa
adminprofile.field.street=Calle y número
adminprofile.field.city=Código postal y ciudad
adminprofile.field.phone=Teléfono
adminprofile.field.fax=Fax
adminprofile.field.email=Correo electrónico
adminprofile.field.website=Sitio web
adminprofile.field.senderline=Línea de remitente
adminprofile.field.senderline.helper=Información del remitente en una línea sobre la dirección del destinatario en las facturas
adminprofile.field.paymentterms=Condiciones de pago
adminprofile.field.footer=Pie de página
adminprofile.field.footer.helper=Gerentes, número fiscal, datos bancarios cada línea aparece como línea propia en la factura
adminprofile.notification.saved=Datos de la empresa guardados
adminprofile.notification.save.error=Error al guardar: {0}
adminprofile.notification.load.error=Error al cargar: {0}
# Crear facturas (admin)
page.title.admin.createinvoices=Crear facturas
admincreateinvoices.title=Crear facturas
admincreateinvoices.hint=Las facturas vencen a fin de mes. Si un cliente comienza a mitad de mes, solo se facturan los días restantes hasta fin de mes.
admincreateinvoices.column.customer=Cliente
admincreateinvoices.column.email=Correo electrónico
admincreateinvoices.column.start=Cliente desde
admincreateinvoices.column.days=Días facturados
admincreateinvoices.column.due=Vence el
admincreateinvoices.column.net=Importe neto
admincreateinvoices.days.full=Mes completo
admincreateinvoices.days.partial={0} de {1} días
admincreateinvoices.prorata.suffix=(prorrateado {0}/{1} días)
admincreateinvoices.button.create=Crear factura
admincreateinvoices.notification.notemplate=No hay plantilla de factura. Guarde primero una plantilla en el generador de facturas.
admincreateinvoices.notification.error=Error al crear la factura: {0}
admincreateinvoices.button.createselected=Crear facturas ({0})
admincreateinvoices.field.month=Mes de facturación

View File

@@ -1017,3 +1017,42 @@ admindashboard.customers.empty=Aucun client
invoicegenerator.positions.header=Positions (grille tarifaire)
invoicegenerator.positions.list=Lister les positions
button.undo=Annuler
# Profil administrateur (données de l'entreprise)
page.title.admin.profile=Modifier les données de l'entreprise
adminprofile.title=Données de l'entreprise
adminprofile.field.companyname=Nom de l'entreprise
adminprofile.field.companysubtitle=Complément de l'entreprise
adminprofile.field.street=Rue et numéro
adminprofile.field.city=Code postal et ville
adminprofile.field.phone=Téléphone
adminprofile.field.fax=Fax
adminprofile.field.email=E-mail
adminprofile.field.website=Site web
adminprofile.field.senderline=Ligne d'expéditeur
adminprofile.field.senderline.helper=Mention de l'expéditeur sur une ligne au-dessus de l'adresse du destinataire sur les factures
adminprofile.field.paymentterms=Conditions de paiement
adminprofile.field.footer=Pied de page
adminprofile.field.footer.helper=Gérants, numéro fiscal, coordonnées bancaires chaque ligne apparaît comme une ligne distincte sur la facture
adminprofile.notification.saved=Données de l'entreprise enregistrées
adminprofile.notification.save.error=Erreur lors de l'enregistrement : {0}
adminprofile.notification.load.error=Erreur lors du chargement : {0}
# Créer des factures (admin)
page.title.admin.createinvoices=Créer des factures
admincreateinvoices.title=Créer des factures
admincreateinvoices.hint=Les factures sont exigibles en fin de mois. Si un client commence en cours de mois, seuls les jours restants jusqu'à la fin du mois sont facturés.
admincreateinvoices.column.customer=Client
admincreateinvoices.column.email=E-mail
admincreateinvoices.column.start=Client depuis
admincreateinvoices.column.days=Jours facturés
admincreateinvoices.column.due=Échéance le
admincreateinvoices.column.net=Montant net
admincreateinvoices.days.full=Mois complet
admincreateinvoices.days.partial={0} sur {1} jours
admincreateinvoices.prorata.suffix=(au prorata {0}/{1} jours)
admincreateinvoices.button.create=Créer la facture
admincreateinvoices.notification.notemplate=Aucun modèle de facture disponible. Veuillez d'abord enregistrer un modèle dans le générateur de factures.
admincreateinvoices.notification.error=Erreur lors de la création de la facture : {0}
admincreateinvoices.button.createselected=Créer les factures ({0})
admincreateinvoices.field.month=Mois de facturation

View File

@@ -1019,3 +1019,42 @@ admindashboard.customers.empty=Klientų nėra
invoicegenerator.positions.header=Pozicijos (kainų lentelė)
invoicegenerator.positions.list=Pozicijų sąrašas
button.undo=Anuliuoti
# Administratoriaus profilis (įmonės duomenys)
page.title.admin.profile=Redaguoti įmonės duomenis
adminprofile.title=Įmonės duomenys
adminprofile.field.companyname=Įmonės pavadinimas
adminprofile.field.companysubtitle=Įmonės priedas
adminprofile.field.street=Gatvė ir namo numeris
adminprofile.field.city=Pašto kodas ir miestas
adminprofile.field.phone=Telefonas
adminprofile.field.fax=Faksas
adminprofile.field.email=El. paštas
adminprofile.field.website=Svetainė
adminprofile.field.senderline=Siuntėjo eilutė
adminprofile.field.senderline.helper=Vienos eilutės siuntėjo informacija virš gavėjo adreso sąskaitose
adminprofile.field.paymentterms=Mokėjimo sąlygos
adminprofile.field.footer=Poraštė
adminprofile.field.footer.helper=Vadovai, mokesčių numeris, banko rekvizitai kiekviena eilutė sąskaitoje rodoma atskirai
adminprofile.notification.saved=Įmonės duomenys išsaugoti
adminprofile.notification.save.error=Klaida išsaugant: {0}
adminprofile.notification.load.error=Klaida įkeliant: {0}
# Sąskaitų kūrimas (admin)
page.title.admin.createinvoices=Kurti sąskaitas
admincreateinvoices.title=Kurti sąskaitas
admincreateinvoices.hint=Sąskaitos apmokamos mėnesio pabaigoje. Jei klientas pradeda mėnesio viduryje, apmokestinamos tik likusios dienos iki mėnesio pabaigos.
admincreateinvoices.column.customer=Klientas
admincreateinvoices.column.email=El. paštas
admincreateinvoices.column.start=Klientas nuo
admincreateinvoices.column.days=Apmokestintos dienos
admincreateinvoices.column.due=Terminas
admincreateinvoices.column.net=Neto suma
admincreateinvoices.days.full=Visas mėnuo
admincreateinvoices.days.partial={0} iš {1} dienų
admincreateinvoices.prorata.suffix=(proporcingai {0}/{1} dienų)
admincreateinvoices.button.create=Sukurti sąskaitą
admincreateinvoices.notification.notemplate=Nėra sąskaitos šablono. Pirmiausia išsaugokite šabloną sąskaitų generatoriuje.
admincreateinvoices.notification.error=Klaida kuriant sąskaitą: {0}
admincreateinvoices.button.createselected=Sukurti sąskaitas ({0})
admincreateinvoices.field.month=Atsiskaitymo mėnuo

View File

@@ -1017,3 +1017,42 @@ admindashboard.customers.empty=Nav klientu
invoicegenerator.positions.header=Pozīcijas (cenu tabula)
invoicegenerator.positions.list=Pozīciju saraksts
button.undo=Atsaukt
# Administratora profils (uzņēmuma dati)
page.title.admin.profile=Rediģēt uzņēmuma datus
adminprofile.title=Uzņēmuma dati
adminprofile.field.companyname=Uzņēmuma nosaukums
adminprofile.field.companysubtitle=Uzņēmuma papildinājums
adminprofile.field.street=Iela un mājas numurs
adminprofile.field.city=Pasta indekss un pilsēta
adminprofile.field.phone=Tālrunis
adminprofile.field.fax=Fakss
adminprofile.field.email=E-pasts
adminprofile.field.website=Tīmekļa vietne
adminprofile.field.senderline=Sūtītāja rinda
adminprofile.field.senderline.helper=Vienas rindas sūtītāja informācija virs saņēmēja adreses rēķinos
adminprofile.field.paymentterms=Maksājuma nosacījumi
adminprofile.field.footer=Kājene
adminprofile.field.footer.helper=Vadītāji, nodokļu numurs, bankas rekvizīti katra rinda rēķinā parādās atsevišķi
adminprofile.notification.saved=Uzņēmuma dati saglabāti
adminprofile.notification.save.error=Kļūda saglabājot: {0}
adminprofile.notification.load.error=Kļūda ielādējot: {0}
# Rēķinu izveide (admin)
page.title.admin.createinvoices=Izveidot rēķinus
admincreateinvoices.title=Izveidot rēķinus
admincreateinvoices.hint=Rēķini jāapmaksā mēneša beigās. Ja klients sāk mēneša vidū, tiek aprēķinātas tikai atlikušās dienas līdz mēneša beigām.
admincreateinvoices.column.customer=Klients
admincreateinvoices.column.email=E-pasts
admincreateinvoices.column.start=Klients kopš
admincreateinvoices.column.days=Aprēķinātās dienas
admincreateinvoices.column.due=Termiņš
admincreateinvoices.column.net=Neto summa
admincreateinvoices.days.full=Pilns mēnesis
admincreateinvoices.days.partial={0} no {1} dienām
admincreateinvoices.prorata.suffix=(proporcionāli {0}/{1} dienas)
admincreateinvoices.button.create=Izveidot rēķinu
admincreateinvoices.notification.notemplate=Nav rēķina veidnes. Lūdzu, vispirms saglabājiet veidni rēķinu ģeneratorā.
admincreateinvoices.notification.error=Kļūda, veidojot rēķinu: {0}
admincreateinvoices.button.createselected=Izveidot rēķinus ({0})
admincreateinvoices.field.month=Norēķinu mēnesis

View File

@@ -1017,3 +1017,42 @@ admindashboard.customers.empty=Brak klientów
invoicegenerator.positions.header=Pozycje (cennik)
invoicegenerator.positions.list=Lista pozycji
button.undo=Cofnij
# Profil administratora (dane firmy)
page.title.admin.profile=Edytuj dane firmy
adminprofile.title=Dane firmy
adminprofile.field.companyname=Nazwa firmy
adminprofile.field.companysubtitle=Dodatek do nazwy
adminprofile.field.street=Ulica i numer
adminprofile.field.city=Kod pocztowy i miasto
adminprofile.field.phone=Telefon
adminprofile.field.fax=Faks
adminprofile.field.email=E-mail
adminprofile.field.website=Strona internetowa
adminprofile.field.senderline=Linia nadawcy
adminprofile.field.senderline.helper=Jednoliniowa informacja o nadawcy nad adresem odbiorcy na fakturach
adminprofile.field.paymentterms=Warunki płatności
adminprofile.field.footer=Stopka
adminprofile.field.footer.helper=Zarząd, numer podatkowy, dane bankowe każda linia pojawia się jako osobny wiersz na fakturze
adminprofile.notification.saved=Dane firmy zapisane
adminprofile.notification.save.error=Błąd podczas zapisywania: {0}
adminprofile.notification.load.error=Błąd podczas ładowania: {0}
# Tworzenie faktur (admin)
page.title.admin.createinvoices=Utwórz faktury
admincreateinvoices.title=Utwórz faktury
admincreateinvoices.hint=Faktury są płatne na koniec miesiąca. Jeśli klient rozpoczyna w trakcie miesiąca, rozliczane są tylko dni pozostałe do końca miesiąca.
admincreateinvoices.column.customer=Klient
admincreateinvoices.column.email=E-mail
admincreateinvoices.column.start=Klient od
admincreateinvoices.column.days=Rozliczone dni
admincreateinvoices.column.due=Termin płatności
admincreateinvoices.column.net=Kwota netto
admincreateinvoices.days.full=Pełny miesiąc
admincreateinvoices.days.partial={0} z {1} dni
admincreateinvoices.prorata.suffix=(proporcjonalnie {0}/{1} dni)
admincreateinvoices.button.create=Utwórz fakturę
admincreateinvoices.notification.notemplate=Brak szablonu faktury. Najpierw zapisz szablon w generatorze faktur.
admincreateinvoices.notification.error=Błąd podczas tworzenia faktury: {0}
admincreateinvoices.button.createselected=Utwórz faktury ({0})
admincreateinvoices.field.month=Miesiąc rozliczeniowy

View File

@@ -1017,3 +1017,42 @@ admindashboard.customers.empty=Клиенты отсутствуют
invoicegenerator.positions.header=Позиции (прайс-лист)
invoicegenerator.positions.list=Список позиций
button.undo=Отменить
# Профиль администратора (данные компании)
page.title.admin.profile=Редактировать данные компании
adminprofile.title=Данные компании
adminprofile.field.companyname=Название компании
adminprofile.field.companysubtitle=Дополнение к названию
adminprofile.field.street=Улица и номер дома
adminprofile.field.city=Индекс и город
adminprofile.field.phone=Телефон
adminprofile.field.fax=Факс
adminprofile.field.email=Эл. почта
adminprofile.field.website=Веб-сайт
adminprofile.field.senderline=Строка отправителя
adminprofile.field.senderline.helper=Однострочная информация об отправителе над адресом получателя в счетах
adminprofile.field.paymentterms=Условия оплаты
adminprofile.field.footer=Нижний колонтитул
adminprofile.field.footer.helper=Руководители, налоговый номер, банковские реквизиты каждая строка отображается отдельной строкой в счете
adminprofile.notification.saved=Данные компании сохранены
adminprofile.notification.save.error=Ошибка при сохранении: {0}
adminprofile.notification.load.error=Ошибка при загрузке: {0}
# Создание счетов (админ)
page.title.admin.createinvoices=Создать счета
admincreateinvoices.title=Создать счета
admincreateinvoices.hint=Счета подлежат оплате в конце месяца. Если клиент начинает в середине месяца, оплачиваются только оставшиеся дни до конца месяца.
admincreateinvoices.column.customer=Клиент
admincreateinvoices.column.email=Эл. почта
admincreateinvoices.column.start=Клиент с
admincreateinvoices.column.days=Расчетные дни
admincreateinvoices.column.due=Срок оплаты
admincreateinvoices.column.net=Сумма нетто
admincreateinvoices.days.full=Полный месяц
admincreateinvoices.days.partial={0} из {1} дней
admincreateinvoices.prorata.suffix=(пропорционально {0}/{1} дней)
admincreateinvoices.button.create=Создать счет
admincreateinvoices.notification.notemplate=Шаблон счета отсутствует. Сначала сохраните шаблон в генераторе счетов.
admincreateinvoices.notification.error=Ошибка при создании счета: {0}
admincreateinvoices.button.createselected=Создать счета ({0})
admincreateinvoices.field.month=Расчетный месяц

View File

@@ -1017,3 +1017,42 @@ admindashboard.customers.empty=Müşteri yok
invoicegenerator.positions.header=Kalemler (fiyat tablosu)
invoicegenerator.positions.list=Kalemleri listele
button.undo=Geri al
# Yönetici profili (şirket verileri)
page.title.admin.profile=Şirket verilerini düzenle
adminprofile.title=Şirket verileri
adminprofile.field.companyname=Şirket adı
adminprofile.field.companysubtitle=Şirket eki
adminprofile.field.street=Cadde ve numara
adminprofile.field.city=Posta kodu ve şehir
adminprofile.field.phone=Telefon
adminprofile.field.fax=Faks
adminprofile.field.email=E-posta
adminprofile.field.website=Web sitesi
adminprofile.field.senderline=Gönderen satırı
adminprofile.field.senderline.helper=Faturalarda alıcı adresinin üzerindeki tek satırlık gönderen bilgisi
adminprofile.field.paymentterms=Ödeme koşulları
adminprofile.field.footer=Alt bilgi
adminprofile.field.footer.helper=Yöneticiler, vergi numarası, banka bilgileri her satır faturada ayrı satır olarak görünür
adminprofile.notification.saved=Şirket verileri kaydedildi
adminprofile.notification.save.error=Kaydetme hatası: {0}
adminprofile.notification.load.error=Yükleme hatası: {0}
# Fatura oluşturma (admin)
page.title.admin.createinvoices=Fatura oluştur
admincreateinvoices.title=Fatura oluştur
admincreateinvoices.hint=Faturalar ay sonunda ödenir. Bir müşteri ay ortasında başlarsa, yalnızca ay sonuna kadar kalan günler faturalandırılır.
admincreateinvoices.column.customer=Müşteri
admincreateinvoices.column.email=E-posta
admincreateinvoices.column.start=Müşteri başlangıcı
admincreateinvoices.column.days=Faturalanan günler
admincreateinvoices.column.due=Vade tarihi
admincreateinvoices.column.net=Net tutar
admincreateinvoices.days.full=Tam ay
admincreateinvoices.days.partial={0} / {1} gün
admincreateinvoices.prorata.suffix=(orantılı {0}/{1} gün)
admincreateinvoices.button.create=Fatura oluştur
admincreateinvoices.notification.notemplate=Fatura şablonu yok. Lütfen önce fatura oluşturucuda bir şablon kaydedin.
admincreateinvoices.notification.error=Fatura oluşturulurken hata: {0}
admincreateinvoices.button.createselected=Fatura oluştur ({0})
admincreateinvoices.field.month=Fatura ayı

View File

@@ -141,8 +141,8 @@ class InvoiceComplianceValidatorTest {
void rejectsItemWithNegativeUnitPrice() {
CustomerInvoice invoice = validInvoice();
invoice.getItems().get(0).setUnitPrice(new BigDecimal("-5.00"));
InvoiceComplianceException ex = catchThrowableOfType(
() -> validator.validateForIssuance(invoice), InvoiceComplianceException.class);
InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
() -> validator.validateForIssuance(invoice));
assertThat(ex).isNotNull();
assertThat(ex.getViolations()).anyMatch(v -> v.contains("Einzelpreis"));
}
@@ -151,8 +151,8 @@ class InvoiceComplianceValidatorTest {
void rejectsInconsistentTotals() {
CustomerInvoice invoice = validInvoice();
invoice.setTotalAmount(new BigDecimal("999.99"));
InvoiceComplianceException ex = catchThrowableOfType(
() -> validator.validateForIssuance(invoice), InvoiceComplianceException.class);
InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
() -> validator.validateForIssuance(invoice));
assertThat(ex).isNotNull();
assertThat(ex.getViolations()).anyMatch(v -> v.contains("Bruttobetrag passt nicht"));
}
@@ -164,8 +164,8 @@ class InvoiceComplianceValidatorTest {
invoice.setNetAmount(new BigDecimal("80.00"));
invoice.setVatAmount(new BigDecimal("15.20"));
invoice.setTotalAmount(new BigDecimal("95.20"));
InvoiceComplianceException ex = catchThrowableOfType(
() -> validator.validateForIssuance(invoice), InvoiceComplianceException.class);
InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
() -> validator.validateForIssuance(invoice));
assertThat(ex).isNotNull();
assertThat(ex.getViolations()).anyMatch(v -> v.contains("Summe der Positionen"));
}
@@ -207,8 +207,8 @@ class InvoiceComplianceValidatorTest {
// Erwartet wären 19,00 € — wir tragen 25,00 € ein.
invoice.setVatAmount(new BigDecimal("25.00"));
invoice.setTotalAmount(new BigDecimal("125.00"));
InvoiceComplianceException ex = catchThrowableOfType(
() -> validator.validateForIssuance(invoice), InvoiceComplianceException.class);
InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
() -> validator.validateForIssuance(invoice));
assertThat(ex).isNotNull();
assertThat(ex.getViolations()).anyMatch(v -> v.contains("Steuerbetrag"));
}
@@ -219,8 +219,8 @@ class InvoiceComplianceValidatorTest {
invoice.setInvoiceNumber(null);
invoice.setSenderName(null);
invoice.setItems(new ArrayList<>());
InvoiceComplianceException ex = catchThrowableOfType(
() -> validator.validateForIssuance(invoice), InvoiceComplianceException.class);
InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
() -> validator.validateForIssuance(invoice));
assertThat(ex).isNotNull();
assertThat(ex.getViolations())
.as("Validator soll alle Verstöße sammeln, nicht beim ersten abbrechen")
@@ -250,8 +250,8 @@ class InvoiceComplianceValidatorTest {
}
private void assertSingleViolation(CustomerInvoice invoice, String fragment) {
InvoiceComplianceException ex = catchThrowableOfType(
() -> validator.validateForIssuance(invoice), InvoiceComplianceException.class);
InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
() -> validator.validateForIssuance(invoice));
assertThat(ex).as("erwartete InvoiceComplianceException").isNotNull();
assertThat(ex.getViolations())
.as("Verstoß mit Fragment '%s' erwartet, war: %s", fragment, ex.getViolations())

View File

@@ -6,6 +6,7 @@
{
"_version": "9.1",
"compilerOptions": {
"ignoreDeprecations": "6.0",
"sourceMap": true,
"jsx": "react-jsx",
"inlineSources": true,