feat: Rechnungsfilter (offen/gesendet), Rabatt-Dialog, Adress-Labels mit voller Adresse und Duplikatsschutz im Adressbuch
- Rechnungen erstellen: Filter Alle/Offene/Gesendete pro Abrechnungsmonat; Versand wird in system_invoice_dispatch protokolliert - Rabatt pro Kunde jetzt über Icon-Dialog mit Prozentwert und Grund; Grund erscheint auf der Rechnungsposition - MessageController: stille catch-Blöcke durch Logging ersetzt, offene Pflicht-Tasks werden bei station_completed geloggt - Auftragserstellung: Abhol- und Lieferadresslisten zeigen die komplette Adresse, Auftraggeber nur den Firmennamen; (2)-Zähler entfernt - Adressbuch: komplett identische Einträge werden beim Anlegen übersprungen bzw. mit Hinweis abgelehnt - App: Versionsanzeige liest zur Laufzeit aus dem Build (package_info_plus), pubspec auf 0.9.20+1; Versionspflege dokumentiert (pom.xml fürs Web, pubspec.yaml für die App) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,8 @@ flutter run -d <device> # Run app on device
|
||||
dart run build_runner build # Generate ObjectBox code after entity changes
|
||||
```
|
||||
|
||||
**Versioning:** The Flutter app version is maintained in `pubspec.yaml` (`version:`); the settings view shows it at runtime via `package_info_plus`. The web app/backend version is maintained separately in `backend/pom.xml` (`<revision>`) and surfaced through the `app.version` property.
|
||||
|
||||
**Important:** Run `flutter analyze` after every task and fix any reported issues before committing.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'app_state.dart';
|
||||
import 'app_theme.dart';
|
||||
@@ -26,11 +27,23 @@ class SettingsView extends StatefulWidget {
|
||||
class _SettingsViewState extends State<SettingsView> {
|
||||
late String _selectedLanguageCode;
|
||||
final AppState _appState = AppState();
|
||||
String _appVersion = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedLanguageCode = _appState.languageCode;
|
||||
_loadAppVersion();
|
||||
}
|
||||
|
||||
Future<void> _loadAppVersion() async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_appVersion = info.version;
|
||||
});
|
||||
}
|
||||
|
||||
void _onLanguageSelected(String languageCode) async {
|
||||
@@ -213,7 +226,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
ListTile(
|
||||
leading: Icon(Icons.info_outline, color: AppColors.textMuted),
|
||||
title: Text(l10n.version),
|
||||
subtitle: const Text('0.9.2'),
|
||||
subtitle: Text(_appVersion),
|
||||
),
|
||||
const Divider(height: 1, indent: 72),
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 0.9.16+1
|
||||
version: 0.9.20+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.0
|
||||
|
||||
@@ -383,7 +383,8 @@ public class MessageController {
|
||||
jobHistoryService.logTaskCompletion(jobId, taskType, taskIdStr, completedBy, taskDisplayName,
|
||||
extraDataSummary);
|
||||
} catch (Exception e) {
|
||||
// Ignore history logging errors
|
||||
log.error("[TASK] Could not write job history for task {} of job {}: {}", taskIdStr, jobId,
|
||||
e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Send email notification for task completion
|
||||
@@ -395,7 +396,8 @@ public class MessageController {
|
||||
// It is now driven by explicit station_completed messages from the app
|
||||
// (see handleStationCompleted).
|
||||
} catch (Exception e) {
|
||||
// Ignore email notification errors
|
||||
log.error("[TASK] Could not send completion notification for task {} of job {}: {}", taskIdStr, jobId,
|
||||
e.getMessage(), e);
|
||||
}
|
||||
|
||||
jobUpdateBroadcaster.broadcast(jobId);
|
||||
@@ -422,11 +424,17 @@ public class MessageController {
|
||||
try {
|
||||
emailService.sendJobCompletionNotification(jobId, completedBy);
|
||||
} catch (Exception e) {
|
||||
// Ignore email notification errors
|
||||
log.error("[JOB] Could not send job completion notification for job {}: {}", jobId, e.getMessage(),
|
||||
e);
|
||||
}
|
||||
} else {
|
||||
List<String> openTaskIds = mandatoryTasks.stream().filter(task -> !task.isCompleted())
|
||||
.map(task -> task.getIdAsString()).toList();
|
||||
log.info("[JOB] Job {} not completed yet, {} of {} mandatory tasks still open: {}", jobId,
|
||||
openTaskIds.size(), mandatoryTasks.size(), openTaskIds);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore job completion check errors
|
||||
log.error("[JOB] Completion check failed for job {}: {}", jobId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,6 +442,7 @@ public class MessageController {
|
||||
try {
|
||||
Optional<Job> jobOpt = jobRepository.findById(jobId);
|
||||
if (jobOpt.isEmpty()) {
|
||||
log.warn("[JOB] Cannot mark job {} as completed: job not found", jobId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -442,9 +451,10 @@ public class MessageController {
|
||||
job.setStatus(JobStatus.COMPLETED);
|
||||
job.setUpdatedAt(LocalDateTime.now());
|
||||
jobRepository.save(job);
|
||||
log.info("[JOB] Job {} marked as COMPLETED", jobId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore job status update errors
|
||||
log.error("[JOB] Could not update status of job {} to COMPLETED: {}", jobId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,7 +521,8 @@ public class MessageController {
|
||||
ChatMessageInboundPayload inboundPayload = ChatMessageInboundPayload.fromPayload(payload);
|
||||
messageService.receiveMessageFromClient(inboundPayload);
|
||||
} catch (Exception e) {
|
||||
// Ignore message handling errors
|
||||
log.error("[MESSAGE] Could not process incoming message from app user {}: {}", appUserId, e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.assecutor.votianlt.model.invoices;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Records that a system invoice has been sent to a customer by email. Used to
|
||||
* filter the monthly invoice list into open and already sent invoices.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Document(collection = "system_invoice_dispatch")
|
||||
public class SystemInvoiceDispatch {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
/** Invoice number, e.g. SYS-202607-42; one dispatch record per invoice. */
|
||||
@Indexed(unique = true)
|
||||
@Field("invoice_number")
|
||||
private String invoiceNumber;
|
||||
|
||||
@Field("user_id")
|
||||
private ObjectId userId;
|
||||
|
||||
/** Billing month in ISO format, e.g. 2026-07. */
|
||||
@Field("billing_month")
|
||||
private String billingMonth;
|
||||
|
||||
@Field("sent_at")
|
||||
private LocalDateTime sentAt;
|
||||
|
||||
@Field("sent_to")
|
||||
private String sentTo;
|
||||
}
|
||||
@@ -8,28 +8,49 @@ public final class CustomerAddressLabelHelper {
|
||||
private CustomerAddressLabelHelper() {
|
||||
}
|
||||
|
||||
public static void putUnique(Map<String, Customer> target, Customer customer, String fallbackLabel) {
|
||||
String label = build(customer, fallbackLabel);
|
||||
String uniqueLabel = label;
|
||||
int counter = 2;
|
||||
while (target.containsKey(uniqueLabel)) {
|
||||
uniqueLabel = label + " (" + counter++ + ")";
|
||||
}
|
||||
target.put(uniqueLabel, customer);
|
||||
/** Adds the customer under its full address label ("Name, Street No, Zip City"). */
|
||||
public static void put(Map<String, Customer> target, Customer customer, String fallbackLabel) {
|
||||
target.put(build(customer, fallbackLabel), customer);
|
||||
}
|
||||
|
||||
/** Adds the customer under its plain name label (company or person name). */
|
||||
public static void putName(Map<String, Customer> target, Customer customer, String fallbackLabel) {
|
||||
target.put(buildName(customer, fallbackLabel), customer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the selection label as full address ("Name, Street No, Zip City")
|
||||
* so that entries with the same name (e.g. branches of one company) remain
|
||||
* distinguishable in the combo boxes.
|
||||
*/
|
||||
public static String build(Customer customer, String fallbackLabel) {
|
||||
if (customer == null) {
|
||||
return fallbackLabel;
|
||||
}
|
||||
|
||||
String companyName = trimToNull(customer.getCompanyName());
|
||||
if (companyName != null) {
|
||||
return companyName;
|
||||
StringBuilder label = new StringBuilder(buildName(customer, fallbackLabel));
|
||||
String street = trimToNull(join(" ", customer.getStreet(), customer.getHouseNumber()));
|
||||
if (street != null) {
|
||||
label.append(", ").append(street);
|
||||
}
|
||||
String city = trimToNull(join(" ", customer.getZip(), customer.getCity()));
|
||||
if (city != null) {
|
||||
label.append(", ").append(city);
|
||||
}
|
||||
return label.toString();
|
||||
}
|
||||
|
||||
String fullName = trimToNull(join(" ", customer.getFirstname(), customer.getLastName()));
|
||||
return fullName != null ? fullName : fallbackLabel;
|
||||
/** Plain name label: company name, otherwise person name, otherwise fallback. */
|
||||
public static String buildName(Customer customer, String fallbackLabel) {
|
||||
if (customer == null) {
|
||||
return fallbackLabel;
|
||||
}
|
||||
|
||||
String name = trimToNull(customer.getCompanyName());
|
||||
if (name == null) {
|
||||
name = trimToNull(join(" ", customer.getFirstname(), customer.getLastName()));
|
||||
}
|
||||
return name != null ? name : fallbackLabel;
|
||||
}
|
||||
|
||||
public static String resolveCompanyValue(Map<String, Customer> addressOptions, String comboValue) {
|
||||
|
||||
@@ -687,7 +687,7 @@ public class DeliveryStationDialog extends Dialog {
|
||||
private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
|
||||
companyAddressOptions.clear();
|
||||
for (Customer customer : customers) {
|
||||
CustomerAddressLabelHelper.putUnique(companyAddressOptions, customer,
|
||||
CustomerAddressLabelHelper.put(companyAddressOptions, customer,
|
||||
translationHelper.getTranslation("addjob.customer.unnamed"));
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ public class DeliveryStationTile extends VerticalLayout {
|
||||
TranslationHelper translationHelper) {
|
||||
companyAddressOptions.clear();
|
||||
for (Customer customer : customers) {
|
||||
CustomerAddressLabelHelper.putUnique(companyAddressOptions, customer,
|
||||
CustomerAddressLabelHelper.put(companyAddressOptions, customer,
|
||||
translationHelper.getTranslation("addjob.customer.unnamed"));
|
||||
}
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ public class PickupStationDialog extends Dialog {
|
||||
|
||||
customerLabelMap.clear();
|
||||
for (Customer c : customers) {
|
||||
CustomerAddressLabelHelper.putUnique(customerLabelMap, c,
|
||||
CustomerAddressLabelHelper.putName(customerLabelMap, c,
|
||||
translationHelper.getTranslation("addjob.customer.unnamed"));
|
||||
}
|
||||
customerComboBox.setItems(new ArrayList<>(customerLabelMap.keySet()));
|
||||
@@ -832,7 +832,7 @@ public class PickupStationDialog extends Dialog {
|
||||
private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
|
||||
companyCustomerMap.clear();
|
||||
for (Customer customer : customers) {
|
||||
CustomerAddressLabelHelper.putUnique(companyCustomerMap, customer,
|
||||
CustomerAddressLabelHelper.put(companyCustomerMap, customer,
|
||||
translationHelper.getTranslation("addjob.customer.unnamed"));
|
||||
}
|
||||
companyField.setItems(new ArrayList<>(companyCustomerMap.keySet()));
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package de.assecutor.votianlt.pages.domain;
|
||||
|
||||
import de.assecutor.votianlt.model.Customer;
|
||||
import java.util.List;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
public interface AddCustomerRepository extends MongoRepository<Customer, ObjectId> {
|
||||
|
||||
List<Customer> findByOwnerAndInternal(ObjectId owner, boolean internal);
|
||||
}
|
||||
|
||||
@@ -21,18 +21,29 @@ public class AddCustomerService {
|
||||
this.sequenceGeneratorService = sequenceGeneratorService;
|
||||
}
|
||||
|
||||
public void addCustomer(Customer customer) {
|
||||
/**
|
||||
* Creates the customer unless a completely identical entry already exists
|
||||
* for the current owner. Returns false if the duplicate was skipped.
|
||||
*/
|
||||
public boolean addCustomer(Customer customer) {
|
||||
validateCustomer(customer);
|
||||
|
||||
// Setze den aktuellen Benutzer als Ersteller - jetzt direkt aus der Session
|
||||
de.assecutor.votianlt.model.User currentUser = securityService.getCurrentDatabaseUser();
|
||||
customer.setCreatedBy(currentUser.getId());
|
||||
customer.setOwner(currentUser.getId());
|
||||
|
||||
// Keine komplett identischen Einträge anlegen
|
||||
if (identicalCustomerExists(customer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (customer.getUsrId() == null) {
|
||||
customer.setUsrId(sequenceGeneratorService.nextCustomerNumber());
|
||||
}
|
||||
|
||||
addCustomerRepository.save(customer);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void addInternalCustomer(Customer customer) {
|
||||
@@ -44,6 +55,28 @@ public class AddCustomerService {
|
||||
addCustomer(customer);
|
||||
}
|
||||
|
||||
private boolean identicalCustomerExists(Customer customer) {
|
||||
return addCustomerRepository.findByOwnerAndInternal(customer.getOwner(), customer.isInternal()).stream()
|
||||
.anyMatch(existing -> isIdentical(existing, customer));
|
||||
}
|
||||
|
||||
private boolean isIdentical(Customer a, Customer b) {
|
||||
return equalsNormalized(a.getCompanyName(), b.getCompanyName()) && equalsNormalized(a.getTitle(), b.getTitle())
|
||||
&& equalsNormalized(a.getFirstname(), b.getFirstname())
|
||||
&& equalsNormalized(a.getLastName(), b.getLastName())
|
||||
&& equalsNormalized(a.getTelephone(), b.getTelephone()) && equalsNormalized(a.getMail(), b.getMail())
|
||||
&& equalsNormalized(a.getStreet(), b.getStreet())
|
||||
&& equalsNormalized(a.getHouseNumber(), b.getHouseNumber())
|
||||
&& equalsNormalized(a.getAddressAddition(), b.getAddressAddition())
|
||||
&& equalsNormalized(a.getZip(), b.getZip()) && equalsNormalized(a.getCity(), b.getCity());
|
||||
}
|
||||
|
||||
private boolean equalsNormalized(String first, String second) {
|
||||
String left = first != null ? first.trim() : "";
|
||||
String right = second != null ? second.trim() : "";
|
||||
return left.equalsIgnoreCase(right);
|
||||
}
|
||||
|
||||
public void updateCustomer(Customer customer) {
|
||||
if (customer == null || customer.getId() == null) {
|
||||
throw new IllegalArgumentException("Kunden-ID fehlt");
|
||||
|
||||
@@ -214,7 +214,13 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
||||
Customer customer = new Customer();
|
||||
binder.writeBean(customer);
|
||||
|
||||
addCustomerService.addCustomer(customer);
|
||||
boolean created = addCustomerService.addCustomer(customer);
|
||||
if (!created) {
|
||||
com.vaadin.flow.component.notification.Notification.show(
|
||||
getTranslation("addcustomer.notification.duplicate"), 5000,
|
||||
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
|
||||
return;
|
||||
}
|
||||
|
||||
com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.success"),
|
||||
3000, com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
|
||||
|
||||
@@ -243,7 +243,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
List<Customer> ownerCustomers = customerService.findAllForCurrentOwner();
|
||||
customerLabelToEntity.clear();
|
||||
for (Customer c : ownerCustomers) {
|
||||
CustomerAddressLabelHelper.putUnique(customerLabelToEntity, c, getTranslation("addjob.customer.unnamed"));
|
||||
CustomerAddressLabelHelper.putName(customerLabelToEntity, c, getTranslation("addjob.customer.unnamed"));
|
||||
}
|
||||
customerSelection.setItems(new ArrayList<>(customerLabelToEntity.keySet()));
|
||||
|
||||
@@ -1415,7 +1415,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
|
||||
Map<String, Customer> addressOptions = new LinkedHashMap<>();
|
||||
for (Customer customer : allCustomers) {
|
||||
CustomerAddressLabelHelper.putUnique(addressOptions, customer, getTranslation("addjob.customer.unnamed"));
|
||||
CustomerAddressLabelHelper.put(addressOptions, customer, getTranslation("addjob.customer.unnamed"));
|
||||
}
|
||||
|
||||
// Set items for autocomplete
|
||||
|
||||
@@ -23,11 +23,13 @@ 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.model.invoices.SystemInvoiceDispatch;
|
||||
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.SystemInvoiceDispatchRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||
import de.assecutor.votianlt.service.EmailService;
|
||||
@@ -40,6 +42,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.YearMonth;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
@@ -72,10 +75,20 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
private final CustomerInvoiceService customerInvoiceService;
|
||||
private final SystemCompanyProfileService systemCompanyProfileService;
|
||||
private final EmailService emailService;
|
||||
private final SystemInvoiceDispatchRepository dispatchRepository;
|
||||
|
||||
private final Grid<BillingInfo> grid = new Grid<>();
|
||||
|
||||
/** Billing month, selectable by the user; defaults to the current month. */
|
||||
private YearMonth billingMonth = YearMonth.now();
|
||||
|
||||
/** Filter for the invoice list: all, only open or only sent invoices. */
|
||||
private enum InvoiceFilter {
|
||||
ALL, OPEN, SENT
|
||||
}
|
||||
|
||||
private InvoiceFilter invoiceFilter = InvoiceFilter.ALL;
|
||||
|
||||
/** Discount with reason per customer (user id), entered via the dialog. */
|
||||
private final Map<org.bson.types.ObjectId, DiscountEntry> discounts = new HashMap<>();
|
||||
|
||||
@@ -86,7 +99,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
public AdminCreateInvoicesView(UserRepository userRepository, PriceTableRepository priceTableRepository,
|
||||
AppUserRepository appUserRepository, InvoiceTemplateService invoiceTemplateService,
|
||||
CustomerInvoiceService customerInvoiceService, SystemCompanyProfileService systemCompanyProfileService,
|
||||
EmailService emailService) {
|
||||
EmailService emailService, SystemInvoiceDispatchRepository dispatchRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.priceTableRepository = priceTableRepository;
|
||||
this.appUserRepository = appUserRepository;
|
||||
@@ -94,6 +107,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
this.customerInvoiceService = customerInvoiceService;
|
||||
this.systemCompanyProfileService = systemCompanyProfileService;
|
||||
this.emailService = emailService;
|
||||
this.dispatchRepository = dispatchRepository;
|
||||
|
||||
setSizeFull();
|
||||
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
|
||||
@@ -116,17 +130,33 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
month -> month.format(DateTimeFormatter.ofPattern("LLLL yyyy", getLocale())));
|
||||
monthSelect.setValue(billingMonth);
|
||||
|
||||
// Filter: all invoices of the month, only open or only sent ones
|
||||
Select<InvoiceFilter> filterSelect = new Select<>();
|
||||
filterSelect.setLabel(getTranslation("admincreateinvoices.filter.label"));
|
||||
filterSelect.setItems(InvoiceFilter.values());
|
||||
filterSelect.setItemLabelGenerator(
|
||||
filter -> getTranslation("admincreateinvoices.filter." + filter.name().toLowerCase()));
|
||||
filterSelect.setValue(invoiceFilter);
|
||||
filterSelect.addValueChangeListener(event -> {
|
||||
if (event.getValue() != null) {
|
||||
invoiceFilter = event.getValue();
|
||||
grid.deselectAll();
|
||||
grid.setItems(loadBillingInfos());
|
||||
}
|
||||
});
|
||||
|
||||
add(new ViewToolbar(getTranslation("admincreateinvoices.title")));
|
||||
|
||||
// Month selection left-aligned below the heading
|
||||
monthSelect.getStyle().set("align-self", "flex-start");
|
||||
add(monthSelect);
|
||||
// Month and filter selection left-aligned below the heading
|
||||
com.vaadin.flow.component.orderedlayout.HorizontalLayout selectionLayout = new com.vaadin.flow.component.orderedlayout.HorizontalLayout(
|
||||
monthSelect, filterSelect);
|
||||
selectionLayout.getStyle().set("align-self", "flex-start");
|
||||
add(selectionLayout);
|
||||
|
||||
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);
|
||||
@@ -211,8 +241,16 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
|
||||
private List<BillingInfo> loadBillingInfos() {
|
||||
PriceTable priceTable = priceTableRepository.findAll().stream().findFirst().orElse(null);
|
||||
return userRepository.findAll().stream().filter(this::isCurrentCustomer)
|
||||
List<BillingInfo> infos = userRepository.findAll().stream().filter(this::isCurrentCustomer)
|
||||
.filter(this::existedInBillingMonth).map(user -> computeBilling(user, priceTable)).toList();
|
||||
if (invoiceFilter == InvoiceFilter.ALL) {
|
||||
return infos;
|
||||
}
|
||||
java.util.Set<String> sentInvoiceNumbers = dispatchRepository.findByBillingMonth(billingMonth.toString())
|
||||
.stream().map(SystemInvoiceDispatch::getInvoiceNumber).collect(java.util.stream.Collectors.toSet());
|
||||
boolean wantSent = invoiceFilter == InvoiceFilter.SENT;
|
||||
return infos.stream()
|
||||
.filter(info -> sentInvoiceNumbers.contains(buildInvoiceNumber(info.user())) == wantSent).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -535,6 +573,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
getTranslation("admincreateinvoices.mail.body", preview.invoiceNumber(), monthLabel,
|
||||
issuerName),
|
||||
preview.pdf(), preview.invoiceNumber() + ".pdf");
|
||||
recordDispatch(preview, email);
|
||||
sent++;
|
||||
} catch (Exception ex) {
|
||||
log.error("Error sending invoice {} to {}: {}", preview.invoiceNumber(), email, ex.getMessage(), ex);
|
||||
@@ -542,6 +581,8 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
}
|
||||
if (sent > 0) {
|
||||
grid.deselectAll();
|
||||
grid.setItems(loadBillingInfos());
|
||||
Notification
|
||||
.show(getTranslation("admincreateinvoices.notification.sent", sent), 5000,
|
||||
Notification.Position.BOTTOM_CENTER)
|
||||
@@ -555,6 +596,25 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists (or refreshes) the dispatch record of a sent invoice so the
|
||||
* list can be filtered into open and sent invoices.
|
||||
*/
|
||||
private void recordDispatch(InvoicePreview preview, String email) {
|
||||
try {
|
||||
SystemInvoiceDispatch dispatch = dispatchRepository.findByInvoiceNumber(preview.invoiceNumber())
|
||||
.orElseGet(SystemInvoiceDispatch::new);
|
||||
dispatch.setInvoiceNumber(preview.invoiceNumber());
|
||||
dispatch.setUserId(preview.info().user().getId());
|
||||
dispatch.setBillingMonth(billingMonth.toString());
|
||||
dispatch.setSentAt(LocalDateTime.now());
|
||||
dispatch.setSentTo(email);
|
||||
dispatchRepository.save(dispatch);
|
||||
} catch (Exception ex) {
|
||||
log.error("Error recording dispatch of invoice {}: {}", preview.invoiceNumber(), ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored system template data, or null (with a notification)
|
||||
* if no template has been saved yet.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.assecutor.votianlt.repository;
|
||||
|
||||
import de.assecutor.votianlt.model.invoices.SystemInvoiceDispatch;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SystemInvoiceDispatchRepository extends MongoRepository<SystemInvoiceDispatch, ObjectId> {
|
||||
|
||||
List<SystemInvoiceDispatch> findByBillingMonth(String billingMonth);
|
||||
|
||||
Optional<SystemInvoiceDispatch> findByInvoiceNumber(String invoiceNumber);
|
||||
}
|
||||
@@ -353,6 +353,7 @@ addcustomer.title=Neue Adresse anlegen
|
||||
addcustomer.button.submit=Adresse anlegen
|
||||
addcustomer.notification.validation=Bitte füllen Sie alle Pflichtfelder aus
|
||||
addcustomer.notification.success=Kunde erfolgreich angelegt
|
||||
addcustomer.notification.duplicate=Ein identischer Adressbucheintrag existiert bereits
|
||||
addcustomer.notification.check=Bitte überprüfen Sie Ihre Eingaben
|
||||
addcustomer.notification.error=Fehler: {0}
|
||||
addcustomer.validation.required=Dieses Feld ist erforderlich
|
||||
@@ -1131,6 +1132,10 @@ admincreateinvoices.notification.notemplate=Kein Rechnungstemplate vorhanden. Bi
|
||||
admincreateinvoices.notification.error=Fehler beim Erstellen der Rechnung: {0}
|
||||
admincreateinvoices.button.createselected=Rechnungen erstellen ({0})
|
||||
admincreateinvoices.field.month=Abrechnungsmonat
|
||||
admincreateinvoices.filter.label=Anzeigen
|
||||
admincreateinvoices.filter.all=Alle Rechnungen
|
||||
admincreateinvoices.filter.open=Nur offene
|
||||
admincreateinvoices.filter.sent=Nur gesendete
|
||||
admincreateinvoices.dialog.title=Rechnungen prüfen
|
||||
admincreateinvoices.dialog.position=Rechnung {0} von {1}
|
||||
admincreateinvoices.dialog.button.previous=Zurück
|
||||
|
||||
@@ -313,6 +313,7 @@ addcustomer.title=Uue aadressi loomine
|
||||
addcustomer.button.submit=Loo aadress
|
||||
addcustomer.notification.validation=Palun t\u00e4itke k\u00f5ik kohustuslikud v\u00e4ljad
|
||||
addcustomer.notification.success=Klient edukalt loodud
|
||||
addcustomer.notification.duplicate=Identne aadressiraamatu kirje on juba olemas
|
||||
addcustomer.notification.check=Palun kontrollige oma sisestusi
|
||||
addcustomer.notification.error=Viga: {0}
|
||||
addcustomer.validation.required=See v\u00e4li on kohustuslik
|
||||
@@ -961,6 +962,10 @@ admincreateinvoices.notification.notemplate=Arve mall puudub. Palun salvestage k
|
||||
admincreateinvoices.notification.error=Viga arve koostamisel: {0}
|
||||
admincreateinvoices.button.createselected=Koosta arved ({0})
|
||||
admincreateinvoices.field.month=Arveldusperiood
|
||||
admincreateinvoices.filter.label=Kuva
|
||||
admincreateinvoices.filter.all=Kõik arved
|
||||
admincreateinvoices.filter.open=Ainult avatud
|
||||
admincreateinvoices.filter.sent=Ainult saadetud
|
||||
admincreateinvoices.dialog.title=Arvete ülevaatus
|
||||
admincreateinvoices.dialog.position=Arve {0} / {1}
|
||||
admincreateinvoices.dialog.button.previous=Tagasi
|
||||
|
||||
@@ -353,6 +353,7 @@ addcustomer.title=Create New Address
|
||||
addcustomer.button.submit=Create Address
|
||||
addcustomer.notification.validation=Please fill in all required fields
|
||||
addcustomer.notification.success=Customer created successfully
|
||||
addcustomer.notification.duplicate=An identical address book entry already exists
|
||||
addcustomer.notification.check=Please check your input
|
||||
addcustomer.notification.error=Error: {0}
|
||||
addcustomer.validation.required=This field is required
|
||||
@@ -1131,6 +1132,10 @@ admincreateinvoices.notification.notemplate=No invoice template available. Pleas
|
||||
admincreateinvoices.notification.error=Error creating the invoice: {0}
|
||||
admincreateinvoices.button.createselected=Create invoices ({0})
|
||||
admincreateinvoices.field.month=Billing month
|
||||
admincreateinvoices.filter.label=Show
|
||||
admincreateinvoices.filter.all=All invoices
|
||||
admincreateinvoices.filter.open=Only open
|
||||
admincreateinvoices.filter.sent=Only sent
|
||||
admincreateinvoices.dialog.title=Review invoices
|
||||
admincreateinvoices.dialog.position=Invoice {0} of {1}
|
||||
admincreateinvoices.dialog.button.previous=Back
|
||||
|
||||
@@ -352,6 +352,7 @@ addcustomer.title=Crear nueva dirección
|
||||
addcustomer.button.submit=Crear dirección
|
||||
addcustomer.notification.validation=Por favor, complete todos los campos obligatorios
|
||||
addcustomer.notification.success=Cliente creado correctamente
|
||||
addcustomer.notification.duplicate=Ya existe una entrada idéntica en la libreta de direcciones
|
||||
addcustomer.notification.check=Por favor, revise sus datos
|
||||
addcustomer.notification.error=Error: {0}
|
||||
addcustomer.validation.required=Este campo es obligatorio
|
||||
@@ -1062,6 +1063,10 @@ admincreateinvoices.notification.notemplate=No hay plantilla de factura. Guarde
|
||||
admincreateinvoices.notification.error=Error al crear la factura: {0}
|
||||
admincreateinvoices.button.createselected=Crear facturas ({0})
|
||||
admincreateinvoices.field.month=Mes de facturación
|
||||
admincreateinvoices.filter.label=Mostrar
|
||||
admincreateinvoices.filter.all=Todas las facturas
|
||||
admincreateinvoices.filter.open=Solo abiertas
|
||||
admincreateinvoices.filter.sent=Solo enviadas
|
||||
admincreateinvoices.dialog.title=Revisar facturas
|
||||
admincreateinvoices.dialog.position=Factura {0} de {1}
|
||||
admincreateinvoices.dialog.button.previous=Atrás
|
||||
|
||||
@@ -352,6 +352,7 @@ addcustomer.title=Cr\u00e9er une nouvelle adresse
|
||||
addcustomer.button.submit=Cr\u00e9er l'adresse
|
||||
addcustomer.notification.validation=Veuillez remplir tous les champs obligatoires
|
||||
addcustomer.notification.success=Client cr\u00e9\u00e9 avec succ\u00e8s
|
||||
addcustomer.notification.duplicate=Une entr\u00e9e identique existe d\u00e9j\u00e0 dans le carnet d'adresses
|
||||
addcustomer.notification.check=Veuillez v\u00e9rifier vos saisies
|
||||
addcustomer.notification.error=Erreur : {0}
|
||||
addcustomer.validation.required=Ce champ est obligatoire
|
||||
@@ -1062,6 +1063,10 @@ admincreateinvoices.notification.notemplate=Aucun modèle de facture disponible.
|
||||
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
|
||||
admincreateinvoices.filter.label=Afficher
|
||||
admincreateinvoices.filter.all=Toutes les factures
|
||||
admincreateinvoices.filter.open=Uniquement ouvertes
|
||||
admincreateinvoices.filter.sent=Uniquement envoyées
|
||||
admincreateinvoices.dialog.title=Vérifier les factures
|
||||
admincreateinvoices.dialog.position=Facture {0} sur {1}
|
||||
admincreateinvoices.dialog.button.previous=Retour
|
||||
|
||||
@@ -352,6 +352,7 @@ addcustomer.title=Sukurti naują adresą
|
||||
addcustomer.button.submit=Sukurti adresą
|
||||
addcustomer.notification.validation=Prašome užpildyti visus privalomus laukus
|
||||
addcustomer.notification.success=Klientas sėkmingai sukurtas
|
||||
addcustomer.notification.duplicate=Identiškas adresų knygos įrašas jau egzistuoja
|
||||
addcustomer.notification.check=Prašome patikrinti savo įvestį
|
||||
addcustomer.notification.error=Klaida: {0}
|
||||
addcustomer.validation.required=Šis laukas yra privalomas
|
||||
@@ -1064,6 +1065,10 @@ admincreateinvoices.notification.notemplate=Nėra sąskaitos šablono. Pirmiausi
|
||||
admincreateinvoices.notification.error=Klaida kuriant sąskaitą: {0}
|
||||
admincreateinvoices.button.createselected=Sukurti sąskaitas ({0})
|
||||
admincreateinvoices.field.month=Atsiskaitymo mėnuo
|
||||
admincreateinvoices.filter.label=Rodyti
|
||||
admincreateinvoices.filter.all=Visos sąskaitos
|
||||
admincreateinvoices.filter.open=Tik atviros
|
||||
admincreateinvoices.filter.sent=Tik išsiųstos
|
||||
admincreateinvoices.dialog.title=Peržiūrėti sąskaitas
|
||||
admincreateinvoices.dialog.position=Sąskaita {0} iš {1}
|
||||
admincreateinvoices.dialog.button.previous=Atgal
|
||||
|
||||
@@ -352,6 +352,7 @@ addcustomer.title=Izveidot jaunu adresi
|
||||
addcustomer.button.submit=Izveidot adresi
|
||||
addcustomer.notification.validation=Lūdzu, aizpildiet visus obligātos laukus
|
||||
addcustomer.notification.success=Klients veiksmīgi izveidots
|
||||
addcustomer.notification.duplicate=Identisks adrešu grāmatas ieraksts jau pastāv
|
||||
addcustomer.notification.check=Lūdzu, pārbaudiet savus ievadītos datus
|
||||
addcustomer.notification.error=Kļūda: {0}
|
||||
addcustomer.validation.required=Šis lauks ir obligāts
|
||||
@@ -1062,6 +1063,10 @@ admincreateinvoices.notification.notemplate=Nav rēķina veidnes. Lūdzu, vispir
|
||||
admincreateinvoices.notification.error=Kļūda, veidojot rēķinu: {0}
|
||||
admincreateinvoices.button.createselected=Izveidot rēķinus ({0})
|
||||
admincreateinvoices.field.month=Norēķinu mēnesis
|
||||
admincreateinvoices.filter.label=Rādīt
|
||||
admincreateinvoices.filter.all=Visi rēķini
|
||||
admincreateinvoices.filter.open=Tikai atvērtie
|
||||
admincreateinvoices.filter.sent=Tikai nosūtītie
|
||||
admincreateinvoices.dialog.title=Pārskatīt rēķinus
|
||||
admincreateinvoices.dialog.position=Rēķins {0} no {1}
|
||||
admincreateinvoices.dialog.button.previous=Atpakaļ
|
||||
|
||||
@@ -352,6 +352,7 @@ addcustomer.title=Dodaj nowy adres
|
||||
addcustomer.button.submit=Dodaj adres
|
||||
addcustomer.notification.validation=Prosz\u0119 wype\u0142ni\u0107 wszystkie wymagane pola
|
||||
addcustomer.notification.success=Klient zosta\u0142 pomy\u015blnie dodany
|
||||
addcustomer.notification.duplicate=Identyczny wpis w ksi\u0105\u017cce adresowej ju\u017c istnieje
|
||||
addcustomer.notification.check=Prosz\u0119 sprawdzi\u0107 wprowadzone dane
|
||||
addcustomer.notification.error=B\u0142\u0105d: {0}
|
||||
addcustomer.validation.required=To pole jest wymagane
|
||||
@@ -1062,6 +1063,10 @@ admincreateinvoices.notification.notemplate=Brak szablonu faktury. Najpierw zapi
|
||||
admincreateinvoices.notification.error=Błąd podczas tworzenia faktury: {0}
|
||||
admincreateinvoices.button.createselected=Utwórz faktury ({0})
|
||||
admincreateinvoices.field.month=Miesiąc rozliczeniowy
|
||||
admincreateinvoices.filter.label=Pokaż
|
||||
admincreateinvoices.filter.all=Wszystkie faktury
|
||||
admincreateinvoices.filter.open=Tylko otwarte
|
||||
admincreateinvoices.filter.sent=Tylko wysłane
|
||||
admincreateinvoices.dialog.title=Przegląd faktur
|
||||
admincreateinvoices.dialog.position=Faktura {0} z {1}
|
||||
admincreateinvoices.dialog.button.previous=Wstecz
|
||||
|
||||
@@ -352,6 +352,7 @@ addcustomer.title=Создать новый адрес
|
||||
addcustomer.button.submit=Создать адрес
|
||||
addcustomer.notification.validation=Пожалуйста, заполните все обязательные поля
|
||||
addcustomer.notification.success=Клиент успешно создан
|
||||
addcustomer.notification.duplicate=Идентичная запись в адресной книге уже существует
|
||||
addcustomer.notification.check=Пожалуйста, проверьте ваши данные
|
||||
addcustomer.notification.error=Ошибка: {0}
|
||||
addcustomer.validation.required=Это поле обязательно для заполнения
|
||||
@@ -1062,6 +1063,10 @@ admincreateinvoices.notification.notemplate=Шаблон счета отсутс
|
||||
admincreateinvoices.notification.error=Ошибка при создании счета: {0}
|
||||
admincreateinvoices.button.createselected=Создать счета ({0})
|
||||
admincreateinvoices.field.month=Расчетный месяц
|
||||
admincreateinvoices.filter.label=Показать
|
||||
admincreateinvoices.filter.all=Все счета
|
||||
admincreateinvoices.filter.open=Только открытые
|
||||
admincreateinvoices.filter.sent=Только отправленные
|
||||
admincreateinvoices.dialog.title=Проверка счетов
|
||||
admincreateinvoices.dialog.position=Счет {0} из {1}
|
||||
admincreateinvoices.dialog.button.previous=Назад
|
||||
|
||||
@@ -352,6 +352,7 @@ addcustomer.title=Yeni Adres Olu\u015ftur
|
||||
addcustomer.button.submit=Adres Olu\u015ftur
|
||||
addcustomer.notification.validation=L\u00fctfen t\u00fcm zorunlu alanlar\u0131 doldurun
|
||||
addcustomer.notification.success=M\u00fc\u015fteri ba\u015far\u0131yla olu\u015fturuldu
|
||||
addcustomer.notification.duplicate=Adres defterinde ayn\u0131 kay\u0131t zaten mevcut
|
||||
addcustomer.notification.check=L\u00fctfen giri\u015flerinizi kontrol edin
|
||||
addcustomer.notification.error=Hata: {0}
|
||||
addcustomer.validation.required=Bu alan gereklidir
|
||||
@@ -1062,6 +1063,10 @@ admincreateinvoices.notification.notemplate=Fatura şablonu yok. Lütfen önce f
|
||||
admincreateinvoices.notification.error=Fatura oluşturulurken hata: {0}
|
||||
admincreateinvoices.button.createselected=Fatura oluştur ({0})
|
||||
admincreateinvoices.field.month=Fatura ayı
|
||||
admincreateinvoices.filter.label=Göster
|
||||
admincreateinvoices.filter.all=Tüm faturalar
|
||||
admincreateinvoices.filter.open=Yalnızca açık
|
||||
admincreateinvoices.filter.sent=Yalnızca gönderilen
|
||||
admincreateinvoices.dialog.title=Faturaları incele
|
||||
admincreateinvoices.dialog.position=Fatura {0} / {1}
|
||||
admincreateinvoices.dialog.button.previous=Geri
|
||||
|
||||
Reference in New Issue
Block a user