diff --git a/backend/pom.xml b/backend/pom.xml
index 47ff8a6..6ede0f6 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -11,7 +11,7 @@
jar
- 0.9.18
+ 0.9.20
21
21
21
diff --git a/backend/src/main/java/de/assecutor/votianlt/model/PriceTable.java b/backend/src/main/java/de/assecutor/votianlt/model/PriceTable.java
index fdaad0b..b6a461e 100644
--- a/backend/src/main/java/de/assecutor/votianlt/model/PriceTable.java
+++ b/backend/src/main/java/de/assecutor/votianlt/model/PriceTable.java
@@ -10,18 +10,18 @@ public class PriceTable {
private String id;
private String monthlyBasePackage;
- private String appUsageLicense;
- private String revenueParticipation;
+
+ /** Fee billed per app user set up by the customer (unit price per month). */
+ private String appUserFee;
+
private String statisticalEvaluation;
public PriceTable() {
}
- public PriceTable(String monthlyBasePackage, String appUsageLicense, String revenueParticipation,
- String statisticalEvaluation) {
+ public PriceTable(String monthlyBasePackage, String appUserFee, String statisticalEvaluation) {
this.monthlyBasePackage = monthlyBasePackage;
- this.appUsageLicense = appUsageLicense;
- this.revenueParticipation = revenueParticipation;
+ this.appUserFee = appUserFee;
this.statisticalEvaluation = statisticalEvaluation;
}
@@ -41,20 +41,12 @@ public class PriceTable {
this.monthlyBasePackage = monthlyBasePackage;
}
- public String getAppUsageLicense() {
- return appUsageLicense;
+ public String getAppUserFee() {
+ return appUserFee;
}
- public void setAppUsageLicense(String appUsageLicense) {
- this.appUsageLicense = appUsageLicense;
- }
-
- public String getRevenueParticipation() {
- return revenueParticipation;
- }
-
- public void setRevenueParticipation(String revenueParticipation) {
- this.revenueParticipation = revenueParticipation;
+ public void setAppUserFee(String appUserFee) {
+ this.appUserFee = appUserFee;
}
public String getStatisticalEvaluation() {
diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/AdminCreateInvoicesView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/AdminCreateInvoicesView.java
index 5eebb7b..22b905d 100644
--- a/backend/src/main/java/de/assecutor/votianlt/pages/view/AdminCreateInvoicesView.java
+++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/AdminCreateInvoicesView.java
@@ -8,9 +8,15 @@ 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.html.Span;
+import com.vaadin.flow.component.icon.Icon;
+import com.vaadin.flow.component.icon.VaadinIcon;
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.select.Select;
+import com.vaadin.flow.component.textfield.NumberField;
+import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
@@ -20,9 +26,11 @@ 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.AppUserRepository;
import de.assecutor.votianlt.repository.PriceTableRepository;
import de.assecutor.votianlt.repository.UserRepository;
import de.assecutor.votianlt.service.CustomerInvoiceService;
+import de.assecutor.votianlt.service.EmailService;
import de.assecutor.votianlt.service.InvoiceTemplateService;
import de.assecutor.votianlt.service.SystemCompanyProfileService;
import de.assecutor.votianlt.util.DateTimeFormatUtil;
@@ -59,21 +67,33 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
private final UserRepository userRepository;
private final PriceTableRepository priceTableRepository;
+ private final AppUserRepository appUserRepository;
private final InvoiceTemplateService invoiceTemplateService;
private final CustomerInvoiceService customerInvoiceService;
private final SystemCompanyProfileService systemCompanyProfileService;
+ private final EmailService emailService;
/** Billing month, selectable by the user; defaults to the current month. */
private YearMonth billingMonth = YearMonth.now();
+ /** Discount with reason per customer (user id), entered via the dialog. */
+ private final Map discounts = new HashMap<>();
+
+ /** A discount entered for one customer: percent value plus reason. */
+ private record DiscountEntry(Double percent, String reason) {
+ }
+
public AdminCreateInvoicesView(UserRepository userRepository, PriceTableRepository priceTableRepository,
- InvoiceTemplateService invoiceTemplateService, CustomerInvoiceService customerInvoiceService,
- SystemCompanyProfileService systemCompanyProfileService) {
+ AppUserRepository appUserRepository, InvoiceTemplateService invoiceTemplateService,
+ CustomerInvoiceService customerInvoiceService, SystemCompanyProfileService systemCompanyProfileService,
+ EmailService emailService) {
this.userRepository = userRepository;
this.priceTableRepository = priceTableRepository;
+ this.appUserRepository = appUserRepository;
this.invoiceTemplateService = invoiceTemplateService;
this.customerInvoiceService = customerInvoiceService;
this.systemCompanyProfileService = systemCompanyProfileService;
+ this.emailService = emailService;
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
@@ -110,8 +130,6 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
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()
@@ -120,12 +138,29 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
.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()) + " €")
+ grid.addColumn(info -> formatAmount(applyDiscount(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);
+ DiscountEntry entry = discounts.get(info.user().getId());
+ Button discountButton = new Button(new Icon(VaadinIcon.TAG));
+ discountButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_ICON,
+ entry != null ? ButtonVariant.LUMO_PRIMARY : ButtonVariant.LUMO_TERTIARY);
+ String tooltip = entry != null
+ ? getTranslation("admincreateinvoices.discount.position", formatPercent(entry.percent()))
+ + (entry.reason().isBlank() ? "" : " – " + entry.reason())
+ : getTranslation("admincreateinvoices.discount.dialog.title");
+ discountButton.setTooltipText(tooltip);
+ discountButton.setAriaLabel(getTranslation("admincreateinvoices.discount.dialog.title"));
+ discountButton.addClickListener(e -> openDiscountDialog(info, grid));
+ return discountButton;
+ }).setHeader(getTranslation("admincreateinvoices.column.discount")).setAutoWidth(true).setFlexGrow(0);
+ grid.addComponentColumn(info -> {
+ Button createButton = new Button(new Icon(VaadinIcon.INVOICE));
+ createButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SMALL,
+ ButtonVariant.LUMO_ICON);
+ createButton.setTooltipText(getTranslation("admincreateinvoices.button.create"));
+ createButton.setAriaLabel(getTranslation("admincreateinvoices.button.create"));
createButton.addClickListener(e -> createInvoicePdf(info));
return createButton;
}).setAutoWidth(true).setFlexGrow(0).setFrozenToEnd(true);
@@ -135,10 +170,11 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
createSelectedButton.setText(getTranslation("admincreateinvoices.button.createselected", count));
createSelectedButton.setEnabled(count > 0);
});
- createSelectedButton.addClickListener(e -> createInvoicesPdf(new ArrayList<>(grid.getSelectedItems())));
+ createSelectedButton.addClickListener(e -> openInvoiceReviewDialog(new ArrayList<>(grid.getSelectedItems())));
monthSelect.addValueChangeListener(event -> {
if (event.getValue() != null) {
billingMonth = event.getValue();
+ discounts.clear();
grid.deselectAll();
grid.setItems(loadBillingInfos());
}
@@ -213,23 +249,32 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
BigDecimal factor = new BigDecimal(billedDays).divide(new BigDecimal(daysInMonth), 10, RoundingMode.HALF_UP);
boolean proRata = billedDays < daysInMonth;
+ // Number of app users belonging to this customer in app_user: the
+ // per-app-user fee is billed once per configured app user
+ long appUserCount = user.getId() != null ? appUserRepository.countByErstelltVon(user.getId()) : 0;
+
List