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
|
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.
|
**Important:** Run `flutter analyze` after every task and fix any reported issues before committing.
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'app_state.dart';
|
import 'app_state.dart';
|
||||||
import 'app_theme.dart';
|
import 'app_theme.dart';
|
||||||
@@ -26,11 +27,23 @@ class SettingsView extends StatefulWidget {
|
|||||||
class _SettingsViewState extends State<SettingsView> {
|
class _SettingsViewState extends State<SettingsView> {
|
||||||
late String _selectedLanguageCode;
|
late String _selectedLanguageCode;
|
||||||
final AppState _appState = AppState();
|
final AppState _appState = AppState();
|
||||||
|
String _appVersion = '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedLanguageCode = _appState.languageCode;
|
_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 {
|
void _onLanguageSelected(String languageCode) async {
|
||||||
@@ -213,7 +226,7 @@ class _SettingsViewState extends State<SettingsView> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(Icons.info_outline, color: AppColors.textMuted),
|
leading: Icon(Icons.info_outline, color: AppColors.textMuted),
|
||||||
title: Text(l10n.version),
|
title: Text(l10n.version),
|
||||||
subtitle: const Text('0.9.2'),
|
subtitle: Text(_appVersion),
|
||||||
),
|
),
|
||||||
const Divider(height: 1, indent: 72),
|
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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.7.0
|
sdk: ^3.7.0
|
||||||
|
|||||||
@@ -383,7 +383,8 @@ public class MessageController {
|
|||||||
jobHistoryService.logTaskCompletion(jobId, taskType, taskIdStr, completedBy, taskDisplayName,
|
jobHistoryService.logTaskCompletion(jobId, taskType, taskIdStr, completedBy, taskDisplayName,
|
||||||
extraDataSummary);
|
extraDataSummary);
|
||||||
} catch (Exception e) {
|
} 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
|
// 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
|
// It is now driven by explicit station_completed messages from the app
|
||||||
// (see handleStationCompleted).
|
// (see handleStationCompleted).
|
||||||
} catch (Exception e) {
|
} 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);
|
jobUpdateBroadcaster.broadcast(jobId);
|
||||||
@@ -422,11 +424,17 @@ public class MessageController {
|
|||||||
try {
|
try {
|
||||||
emailService.sendJobCompletionNotification(jobId, completedBy);
|
emailService.sendJobCompletionNotification(jobId, completedBy);
|
||||||
} catch (Exception e) {
|
} 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) {
|
} 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 {
|
try {
|
||||||
Optional<Job> jobOpt = jobRepository.findById(jobId);
|
Optional<Job> jobOpt = jobRepository.findById(jobId);
|
||||||
if (jobOpt.isEmpty()) {
|
if (jobOpt.isEmpty()) {
|
||||||
|
log.warn("[JOB] Cannot mark job {} as completed: job not found", jobId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,9 +451,10 @@ public class MessageController {
|
|||||||
job.setStatus(JobStatus.COMPLETED);
|
job.setStatus(JobStatus.COMPLETED);
|
||||||
job.setUpdatedAt(LocalDateTime.now());
|
job.setUpdatedAt(LocalDateTime.now());
|
||||||
jobRepository.save(job);
|
jobRepository.save(job);
|
||||||
|
log.info("[JOB] Job {} marked as COMPLETED", jobId);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} 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);
|
ChatMessageInboundPayload inboundPayload = ChatMessageInboundPayload.fromPayload(payload);
|
||||||
messageService.receiveMessageFromClient(inboundPayload);
|
messageService.receiveMessageFromClient(inboundPayload);
|
||||||
} catch (Exception e) {
|
} 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() {
|
private CustomerAddressLabelHelper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void putUnique(Map<String, Customer> target, Customer customer, String fallbackLabel) {
|
/** Adds the customer under its full address label ("Name, Street No, Zip City"). */
|
||||||
String label = build(customer, fallbackLabel);
|
public static void put(Map<String, Customer> target, Customer customer, String fallbackLabel) {
|
||||||
String uniqueLabel = label;
|
target.put(build(customer, fallbackLabel), customer);
|
||||||
int counter = 2;
|
|
||||||
while (target.containsKey(uniqueLabel)) {
|
|
||||||
uniqueLabel = label + " (" + counter++ + ")";
|
|
||||||
}
|
|
||||||
target.put(uniqueLabel, 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) {
|
public static String build(Customer customer, String fallbackLabel) {
|
||||||
if (customer == null) {
|
if (customer == null) {
|
||||||
return fallbackLabel;
|
return fallbackLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
String companyName = trimToNull(customer.getCompanyName());
|
StringBuilder label = new StringBuilder(buildName(customer, fallbackLabel));
|
||||||
if (companyName != null) {
|
String street = trimToNull(join(" ", customer.getStreet(), customer.getHouseNumber()));
|
||||||
return companyName;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Plain name label: company name, otherwise person name, otherwise fallback. */
|
||||||
|
public static String buildName(Customer customer, String fallbackLabel) {
|
||||||
|
if (customer == null) {
|
||||||
|
return fallbackLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
String fullName = trimToNull(join(" ", customer.getFirstname(), customer.getLastName()));
|
String name = trimToNull(customer.getCompanyName());
|
||||||
return fullName != null ? fullName : fallbackLabel;
|
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) {
|
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) {
|
private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
|
||||||
companyAddressOptions.clear();
|
companyAddressOptions.clear();
|
||||||
for (Customer customer : customers) {
|
for (Customer customer : customers) {
|
||||||
CustomerAddressLabelHelper.putUnique(companyAddressOptions, customer,
|
CustomerAddressLabelHelper.put(companyAddressOptions, customer,
|
||||||
translationHelper.getTranslation("addjob.customer.unnamed"));
|
translationHelper.getTranslation("addjob.customer.unnamed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ public class DeliveryStationTile extends VerticalLayout {
|
|||||||
TranslationHelper translationHelper) {
|
TranslationHelper translationHelper) {
|
||||||
companyAddressOptions.clear();
|
companyAddressOptions.clear();
|
||||||
for (Customer customer : customers) {
|
for (Customer customer : customers) {
|
||||||
CustomerAddressLabelHelper.putUnique(companyAddressOptions, customer,
|
CustomerAddressLabelHelper.put(companyAddressOptions, customer,
|
||||||
translationHelper.getTranslation("addjob.customer.unnamed"));
|
translationHelper.getTranslation("addjob.customer.unnamed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ public class PickupStationDialog extends Dialog {
|
|||||||
|
|
||||||
customerLabelMap.clear();
|
customerLabelMap.clear();
|
||||||
for (Customer c : customers) {
|
for (Customer c : customers) {
|
||||||
CustomerAddressLabelHelper.putUnique(customerLabelMap, c,
|
CustomerAddressLabelHelper.putName(customerLabelMap, c,
|
||||||
translationHelper.getTranslation("addjob.customer.unnamed"));
|
translationHelper.getTranslation("addjob.customer.unnamed"));
|
||||||
}
|
}
|
||||||
customerComboBox.setItems(new ArrayList<>(customerLabelMap.keySet()));
|
customerComboBox.setItems(new ArrayList<>(customerLabelMap.keySet()));
|
||||||
@@ -832,7 +832,7 @@ public class PickupStationDialog extends Dialog {
|
|||||||
private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
|
private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
|
||||||
companyCustomerMap.clear();
|
companyCustomerMap.clear();
|
||||||
for (Customer customer : customers) {
|
for (Customer customer : customers) {
|
||||||
CustomerAddressLabelHelper.putUnique(companyCustomerMap, customer,
|
CustomerAddressLabelHelper.put(companyCustomerMap, customer,
|
||||||
translationHelper.getTranslation("addjob.customer.unnamed"));
|
translationHelper.getTranslation("addjob.customer.unnamed"));
|
||||||
}
|
}
|
||||||
companyField.setItems(new ArrayList<>(companyCustomerMap.keySet()));
|
companyField.setItems(new ArrayList<>(companyCustomerMap.keySet()));
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package de.assecutor.votianlt.pages.domain;
|
package de.assecutor.votianlt.pages.domain;
|
||||||
|
|
||||||
import de.assecutor.votianlt.model.Customer;
|
import de.assecutor.votianlt.model.Customer;
|
||||||
|
import java.util.List;
|
||||||
import org.bson.types.ObjectId;
|
import org.bson.types.ObjectId;
|
||||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
|
||||||
public interface AddCustomerRepository extends MongoRepository<Customer, ObjectId> {
|
public interface AddCustomerRepository extends MongoRepository<Customer, ObjectId> {
|
||||||
|
|
||||||
|
List<Customer> findByOwnerAndInternal(ObjectId owner, boolean internal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,18 +21,29 @@ public class AddCustomerService {
|
|||||||
this.sequenceGeneratorService = sequenceGeneratorService;
|
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);
|
validateCustomer(customer);
|
||||||
|
|
||||||
// Setze den aktuellen Benutzer als Ersteller - jetzt direkt aus der Session
|
// Setze den aktuellen Benutzer als Ersteller - jetzt direkt aus der Session
|
||||||
de.assecutor.votianlt.model.User currentUser = securityService.getCurrentDatabaseUser();
|
de.assecutor.votianlt.model.User currentUser = securityService.getCurrentDatabaseUser();
|
||||||
customer.setCreatedBy(currentUser.getId());
|
customer.setCreatedBy(currentUser.getId());
|
||||||
customer.setOwner(currentUser.getId());
|
customer.setOwner(currentUser.getId());
|
||||||
|
|
||||||
|
// Keine komplett identischen Einträge anlegen
|
||||||
|
if (identicalCustomerExists(customer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (customer.getUsrId() == null) {
|
if (customer.getUsrId() == null) {
|
||||||
customer.setUsrId(sequenceGeneratorService.nextCustomerNumber());
|
customer.setUsrId(sequenceGeneratorService.nextCustomerNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
addCustomerRepository.save(customer);
|
addCustomerRepository.save(customer);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addInternalCustomer(Customer customer) {
|
public void addInternalCustomer(Customer customer) {
|
||||||
@@ -44,6 +55,28 @@ public class AddCustomerService {
|
|||||||
addCustomer(customer);
|
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) {
|
public void updateCustomer(Customer customer) {
|
||||||
if (customer == null || customer.getId() == null) {
|
if (customer == null || customer.getId() == null) {
|
||||||
throw new IllegalArgumentException("Kunden-ID fehlt");
|
throw new IllegalArgumentException("Kunden-ID fehlt");
|
||||||
|
|||||||
@@ -214,7 +214,13 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
|||||||
Customer customer = new Customer();
|
Customer customer = new Customer();
|
||||||
binder.writeBean(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"),
|
com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.success"),
|
||||||
3000, com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
|
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();
|
List<Customer> ownerCustomers = customerService.findAllForCurrentOwner();
|
||||||
customerLabelToEntity.clear();
|
customerLabelToEntity.clear();
|
||||||
for (Customer c : ownerCustomers) {
|
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()));
|
customerSelection.setItems(new ArrayList<>(customerLabelToEntity.keySet()));
|
||||||
|
|
||||||
@@ -1415,7 +1415,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
Map<String, Customer> addressOptions = new LinkedHashMap<>();
|
Map<String, Customer> addressOptions = new LinkedHashMap<>();
|
||||||
for (Customer customer : allCustomers) {
|
for (Customer customer : allCustomers) {
|
||||||
CustomerAddressLabelHelper.putUnique(addressOptions, customer, getTranslation("addjob.customer.unnamed"));
|
CustomerAddressLabelHelper.put(addressOptions, customer, getTranslation("addjob.customer.unnamed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set items for autocomplete
|
// 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.PriceTable;
|
||||||
import de.assecutor.votianlt.model.User;
|
import de.assecutor.votianlt.model.User;
|
||||||
import de.assecutor.votianlt.model.invoices.SystemInvoiceData;
|
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.DialogStylingHelper;
|
||||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||||
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
|
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
|
||||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||||
import de.assecutor.votianlt.repository.PriceTableRepository;
|
import de.assecutor.votianlt.repository.PriceTableRepository;
|
||||||
|
import de.assecutor.votianlt.repository.SystemInvoiceDispatchRepository;
|
||||||
import de.assecutor.votianlt.repository.UserRepository;
|
import de.assecutor.votianlt.repository.UserRepository;
|
||||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||||
import de.assecutor.votianlt.service.EmailService;
|
import de.assecutor.votianlt.service.EmailService;
|
||||||
@@ -40,6 +42,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.time.YearMonth;
|
import java.time.YearMonth;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -72,10 +75,20 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
private final CustomerInvoiceService customerInvoiceService;
|
private final CustomerInvoiceService customerInvoiceService;
|
||||||
private final SystemCompanyProfileService systemCompanyProfileService;
|
private final SystemCompanyProfileService systemCompanyProfileService;
|
||||||
private final EmailService emailService;
|
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. */
|
/** Billing month, selectable by the user; defaults to the current month. */
|
||||||
private YearMonth billingMonth = YearMonth.now();
|
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. */
|
/** Discount with reason per customer (user id), entered via the dialog. */
|
||||||
private final Map<org.bson.types.ObjectId, DiscountEntry> discounts = new HashMap<>();
|
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,
|
public AdminCreateInvoicesView(UserRepository userRepository, PriceTableRepository priceTableRepository,
|
||||||
AppUserRepository appUserRepository, InvoiceTemplateService invoiceTemplateService,
|
AppUserRepository appUserRepository, InvoiceTemplateService invoiceTemplateService,
|
||||||
CustomerInvoiceService customerInvoiceService, SystemCompanyProfileService systemCompanyProfileService,
|
CustomerInvoiceService customerInvoiceService, SystemCompanyProfileService systemCompanyProfileService,
|
||||||
EmailService emailService) {
|
EmailService emailService, SystemInvoiceDispatchRepository dispatchRepository) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.priceTableRepository = priceTableRepository;
|
this.priceTableRepository = priceTableRepository;
|
||||||
this.appUserRepository = appUserRepository;
|
this.appUserRepository = appUserRepository;
|
||||||
@@ -94,6 +107,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
this.customerInvoiceService = customerInvoiceService;
|
this.customerInvoiceService = customerInvoiceService;
|
||||||
this.systemCompanyProfileService = systemCompanyProfileService;
|
this.systemCompanyProfileService = systemCompanyProfileService;
|
||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
|
this.dispatchRepository = dispatchRepository;
|
||||||
|
|
||||||
setSizeFull();
|
setSizeFull();
|
||||||
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
|
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())));
|
month -> month.format(DateTimeFormatter.ofPattern("LLLL yyyy", getLocale())));
|
||||||
monthSelect.setValue(billingMonth);
|
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")));
|
add(new ViewToolbar(getTranslation("admincreateinvoices.title")));
|
||||||
|
|
||||||
// Month selection left-aligned below the heading
|
// Month and filter selection left-aligned below the heading
|
||||||
monthSelect.getStyle().set("align-self", "flex-start");
|
com.vaadin.flow.component.orderedlayout.HorizontalLayout selectionLayout = new com.vaadin.flow.component.orderedlayout.HorizontalLayout(
|
||||||
add(monthSelect);
|
monthSelect, filterSelect);
|
||||||
|
selectionLayout.getStyle().set("align-self", "flex-start");
|
||||||
|
add(selectionLayout);
|
||||||
|
|
||||||
Paragraph hint = new Paragraph(getTranslation("admincreateinvoices.hint"));
|
Paragraph hint = new Paragraph(getTranslation("admincreateinvoices.hint"));
|
||||||
hint.addClassNames(LumoUtility.TextColor.SECONDARY, LumoUtility.FontSize.SMALL, LumoUtility.Margin.NONE);
|
hint.addClassNames(LumoUtility.TextColor.SECONDARY, LumoUtility.FontSize.SMALL, LumoUtility.Margin.NONE);
|
||||||
add(hint);
|
add(hint);
|
||||||
|
|
||||||
Grid<BillingInfo> grid = new Grid<>();
|
|
||||||
grid.setItems(loadBillingInfos());
|
grid.setItems(loadBillingInfos());
|
||||||
grid.addColumn(info -> customerDisplayName(info.user()))
|
grid.addColumn(info -> customerDisplayName(info.user()))
|
||||||
.setHeader(getTranslation("admincreateinvoices.column.customer")).setFlexGrow(2).setSortable(true);
|
.setHeader(getTranslation("admincreateinvoices.column.customer")).setFlexGrow(2).setSortable(true);
|
||||||
@@ -211,8 +241,16 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
private List<BillingInfo> loadBillingInfos() {
|
private List<BillingInfo> loadBillingInfos() {
|
||||||
PriceTable priceTable = priceTableRepository.findAll().stream().findFirst().orElse(null);
|
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();
|
.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,
|
getTranslation("admincreateinvoices.mail.body", preview.invoiceNumber(), monthLabel,
|
||||||
issuerName),
|
issuerName),
|
||||||
preview.pdf(), preview.invoiceNumber() + ".pdf");
|
preview.pdf(), preview.invoiceNumber() + ".pdf");
|
||||||
|
recordDispatch(preview, email);
|
||||||
sent++;
|
sent++;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("Error sending invoice {} to {}: {}", preview.invoiceNumber(), email, ex.getMessage(), 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) {
|
if (sent > 0) {
|
||||||
|
grid.deselectAll();
|
||||||
|
grid.setItems(loadBillingInfos());
|
||||||
Notification
|
Notification
|
||||||
.show(getTranslation("admincreateinvoices.notification.sent", sent), 5000,
|
.show(getTranslation("admincreateinvoices.notification.sent", sent), 5000,
|
||||||
Notification.Position.BOTTOM_CENTER)
|
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)
|
* Returns the stored system template data, or null (with a notification)
|
||||||
* if no template has been saved yet.
|
* 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.button.submit=Adresse anlegen
|
||||||
addcustomer.notification.validation=Bitte füllen Sie alle Pflichtfelder aus
|
addcustomer.notification.validation=Bitte füllen Sie alle Pflichtfelder aus
|
||||||
addcustomer.notification.success=Kunde erfolgreich angelegt
|
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.check=Bitte überprüfen Sie Ihre Eingaben
|
||||||
addcustomer.notification.error=Fehler: {0}
|
addcustomer.notification.error=Fehler: {0}
|
||||||
addcustomer.validation.required=Dieses Feld ist erforderlich
|
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.notification.error=Fehler beim Erstellen der Rechnung: {0}
|
||||||
admincreateinvoices.button.createselected=Rechnungen erstellen ({0})
|
admincreateinvoices.button.createselected=Rechnungen erstellen ({0})
|
||||||
admincreateinvoices.field.month=Abrechnungsmonat
|
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.title=Rechnungen prüfen
|
||||||
admincreateinvoices.dialog.position=Rechnung {0} von {1}
|
admincreateinvoices.dialog.position=Rechnung {0} von {1}
|
||||||
admincreateinvoices.dialog.button.previous=Zurück
|
admincreateinvoices.dialog.button.previous=Zurück
|
||||||
|
|||||||
@@ -313,6 +313,7 @@ addcustomer.title=Uue aadressi loomine
|
|||||||
addcustomer.button.submit=Loo aadress
|
addcustomer.button.submit=Loo aadress
|
||||||
addcustomer.notification.validation=Palun t\u00e4itke k\u00f5ik kohustuslikud v\u00e4ljad
|
addcustomer.notification.validation=Palun t\u00e4itke k\u00f5ik kohustuslikud v\u00e4ljad
|
||||||
addcustomer.notification.success=Klient edukalt loodud
|
addcustomer.notification.success=Klient edukalt loodud
|
||||||
|
addcustomer.notification.duplicate=Identne aadressiraamatu kirje on juba olemas
|
||||||
addcustomer.notification.check=Palun kontrollige oma sisestusi
|
addcustomer.notification.check=Palun kontrollige oma sisestusi
|
||||||
addcustomer.notification.error=Viga: {0}
|
addcustomer.notification.error=Viga: {0}
|
||||||
addcustomer.validation.required=See v\u00e4li on kohustuslik
|
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.notification.error=Viga arve koostamisel: {0}
|
||||||
admincreateinvoices.button.createselected=Koosta arved ({0})
|
admincreateinvoices.button.createselected=Koosta arved ({0})
|
||||||
admincreateinvoices.field.month=Arveldusperiood
|
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.title=Arvete ülevaatus
|
||||||
admincreateinvoices.dialog.position=Arve {0} / {1}
|
admincreateinvoices.dialog.position=Arve {0} / {1}
|
||||||
admincreateinvoices.dialog.button.previous=Tagasi
|
admincreateinvoices.dialog.button.previous=Tagasi
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ addcustomer.title=Create New Address
|
|||||||
addcustomer.button.submit=Create Address
|
addcustomer.button.submit=Create Address
|
||||||
addcustomer.notification.validation=Please fill in all required fields
|
addcustomer.notification.validation=Please fill in all required fields
|
||||||
addcustomer.notification.success=Customer created successfully
|
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.check=Please check your input
|
||||||
addcustomer.notification.error=Error: {0}
|
addcustomer.notification.error=Error: {0}
|
||||||
addcustomer.validation.required=This field is required
|
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.notification.error=Error creating the invoice: {0}
|
||||||
admincreateinvoices.button.createselected=Create invoices ({0})
|
admincreateinvoices.button.createselected=Create invoices ({0})
|
||||||
admincreateinvoices.field.month=Billing month
|
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.title=Review invoices
|
||||||
admincreateinvoices.dialog.position=Invoice {0} of {1}
|
admincreateinvoices.dialog.position=Invoice {0} of {1}
|
||||||
admincreateinvoices.dialog.button.previous=Back
|
admincreateinvoices.dialog.button.previous=Back
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ addcustomer.title=Crear nueva dirección
|
|||||||
addcustomer.button.submit=Crear dirección
|
addcustomer.button.submit=Crear dirección
|
||||||
addcustomer.notification.validation=Por favor, complete todos los campos obligatorios
|
addcustomer.notification.validation=Por favor, complete todos los campos obligatorios
|
||||||
addcustomer.notification.success=Cliente creado correctamente
|
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.check=Por favor, revise sus datos
|
||||||
addcustomer.notification.error=Error: {0}
|
addcustomer.notification.error=Error: {0}
|
||||||
addcustomer.validation.required=Este campo es obligatorio
|
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.notification.error=Error al crear la factura: {0}
|
||||||
admincreateinvoices.button.createselected=Crear facturas ({0})
|
admincreateinvoices.button.createselected=Crear facturas ({0})
|
||||||
admincreateinvoices.field.month=Mes de facturación
|
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.title=Revisar facturas
|
||||||
admincreateinvoices.dialog.position=Factura {0} de {1}
|
admincreateinvoices.dialog.position=Factura {0} de {1}
|
||||||
admincreateinvoices.dialog.button.previous=Atrás
|
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.button.submit=Cr\u00e9er l'adresse
|
||||||
addcustomer.notification.validation=Veuillez remplir tous les champs obligatoires
|
addcustomer.notification.validation=Veuillez remplir tous les champs obligatoires
|
||||||
addcustomer.notification.success=Client cr\u00e9\u00e9 avec succ\u00e8s
|
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.check=Veuillez v\u00e9rifier vos saisies
|
||||||
addcustomer.notification.error=Erreur : {0}
|
addcustomer.notification.error=Erreur : {0}
|
||||||
addcustomer.validation.required=Ce champ est obligatoire
|
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.notification.error=Erreur lors de la création de la facture : {0}
|
||||||
admincreateinvoices.button.createselected=Créer les factures ({0})
|
admincreateinvoices.button.createselected=Créer les factures ({0})
|
||||||
admincreateinvoices.field.month=Mois de facturation
|
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.title=Vérifier les factures
|
||||||
admincreateinvoices.dialog.position=Facture {0} sur {1}
|
admincreateinvoices.dialog.position=Facture {0} sur {1}
|
||||||
admincreateinvoices.dialog.button.previous=Retour
|
admincreateinvoices.dialog.button.previous=Retour
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ addcustomer.title=Sukurti naują adresą
|
|||||||
addcustomer.button.submit=Sukurti adresą
|
addcustomer.button.submit=Sukurti adresą
|
||||||
addcustomer.notification.validation=Prašome užpildyti visus privalomus laukus
|
addcustomer.notification.validation=Prašome užpildyti visus privalomus laukus
|
||||||
addcustomer.notification.success=Klientas sėkmingai sukurtas
|
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.check=Prašome patikrinti savo įvestį
|
||||||
addcustomer.notification.error=Klaida: {0}
|
addcustomer.notification.error=Klaida: {0}
|
||||||
addcustomer.validation.required=Šis laukas yra privalomas
|
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.notification.error=Klaida kuriant sąskaitą: {0}
|
||||||
admincreateinvoices.button.createselected=Sukurti sąskaitas ({0})
|
admincreateinvoices.button.createselected=Sukurti sąskaitas ({0})
|
||||||
admincreateinvoices.field.month=Atsiskaitymo mėnuo
|
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.title=Peržiūrėti sąskaitas
|
||||||
admincreateinvoices.dialog.position=Sąskaita {0} iš {1}
|
admincreateinvoices.dialog.position=Sąskaita {0} iš {1}
|
||||||
admincreateinvoices.dialog.button.previous=Atgal
|
admincreateinvoices.dialog.button.previous=Atgal
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ addcustomer.title=Izveidot jaunu adresi
|
|||||||
addcustomer.button.submit=Izveidot adresi
|
addcustomer.button.submit=Izveidot adresi
|
||||||
addcustomer.notification.validation=Lūdzu, aizpildiet visus obligātos laukus
|
addcustomer.notification.validation=Lūdzu, aizpildiet visus obligātos laukus
|
||||||
addcustomer.notification.success=Klients veiksmīgi izveidots
|
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.check=Lūdzu, pārbaudiet savus ievadītos datus
|
||||||
addcustomer.notification.error=Kļūda: {0}
|
addcustomer.notification.error=Kļūda: {0}
|
||||||
addcustomer.validation.required=Šis lauks ir obligāts
|
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.notification.error=Kļūda, veidojot rēķinu: {0}
|
||||||
admincreateinvoices.button.createselected=Izveidot rēķinus ({0})
|
admincreateinvoices.button.createselected=Izveidot rēķinus ({0})
|
||||||
admincreateinvoices.field.month=Norēķinu mēnesis
|
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.title=Pārskatīt rēķinus
|
||||||
admincreateinvoices.dialog.position=Rēķins {0} no {1}
|
admincreateinvoices.dialog.position=Rēķins {0} no {1}
|
||||||
admincreateinvoices.dialog.button.previous=Atpakaļ
|
admincreateinvoices.dialog.button.previous=Atpakaļ
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ addcustomer.title=Dodaj nowy adres
|
|||||||
addcustomer.button.submit=Dodaj adres
|
addcustomer.button.submit=Dodaj adres
|
||||||
addcustomer.notification.validation=Prosz\u0119 wype\u0142ni\u0107 wszystkie wymagane pola
|
addcustomer.notification.validation=Prosz\u0119 wype\u0142ni\u0107 wszystkie wymagane pola
|
||||||
addcustomer.notification.success=Klient zosta\u0142 pomy\u015blnie dodany
|
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.check=Prosz\u0119 sprawdzi\u0107 wprowadzone dane
|
||||||
addcustomer.notification.error=B\u0142\u0105d: {0}
|
addcustomer.notification.error=B\u0142\u0105d: {0}
|
||||||
addcustomer.validation.required=To pole jest wymagane
|
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.notification.error=Błąd podczas tworzenia faktury: {0}
|
||||||
admincreateinvoices.button.createselected=Utwórz faktury ({0})
|
admincreateinvoices.button.createselected=Utwórz faktury ({0})
|
||||||
admincreateinvoices.field.month=Miesiąc rozliczeniowy
|
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.title=Przegląd faktur
|
||||||
admincreateinvoices.dialog.position=Faktura {0} z {1}
|
admincreateinvoices.dialog.position=Faktura {0} z {1}
|
||||||
admincreateinvoices.dialog.button.previous=Wstecz
|
admincreateinvoices.dialog.button.previous=Wstecz
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ addcustomer.title=Создать новый адрес
|
|||||||
addcustomer.button.submit=Создать адрес
|
addcustomer.button.submit=Создать адрес
|
||||||
addcustomer.notification.validation=Пожалуйста, заполните все обязательные поля
|
addcustomer.notification.validation=Пожалуйста, заполните все обязательные поля
|
||||||
addcustomer.notification.success=Клиент успешно создан
|
addcustomer.notification.success=Клиент успешно создан
|
||||||
|
addcustomer.notification.duplicate=Идентичная запись в адресной книге уже существует
|
||||||
addcustomer.notification.check=Пожалуйста, проверьте ваши данные
|
addcustomer.notification.check=Пожалуйста, проверьте ваши данные
|
||||||
addcustomer.notification.error=Ошибка: {0}
|
addcustomer.notification.error=Ошибка: {0}
|
||||||
addcustomer.validation.required=Это поле обязательно для заполнения
|
addcustomer.validation.required=Это поле обязательно для заполнения
|
||||||
@@ -1062,6 +1063,10 @@ admincreateinvoices.notification.notemplate=Шаблон счета отсутс
|
|||||||
admincreateinvoices.notification.error=Ошибка при создании счета: {0}
|
admincreateinvoices.notification.error=Ошибка при создании счета: {0}
|
||||||
admincreateinvoices.button.createselected=Создать счета ({0})
|
admincreateinvoices.button.createselected=Создать счета ({0})
|
||||||
admincreateinvoices.field.month=Расчетный месяц
|
admincreateinvoices.field.month=Расчетный месяц
|
||||||
|
admincreateinvoices.filter.label=Показать
|
||||||
|
admincreateinvoices.filter.all=Все счета
|
||||||
|
admincreateinvoices.filter.open=Только открытые
|
||||||
|
admincreateinvoices.filter.sent=Только отправленные
|
||||||
admincreateinvoices.dialog.title=Проверка счетов
|
admincreateinvoices.dialog.title=Проверка счетов
|
||||||
admincreateinvoices.dialog.position=Счет {0} из {1}
|
admincreateinvoices.dialog.position=Счет {0} из {1}
|
||||||
admincreateinvoices.dialog.button.previous=Назад
|
admincreateinvoices.dialog.button.previous=Назад
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ addcustomer.title=Yeni Adres Olu\u015ftur
|
|||||||
addcustomer.button.submit=Adres Olu\u015ftur
|
addcustomer.button.submit=Adres Olu\u015ftur
|
||||||
addcustomer.notification.validation=L\u00fctfen t\u00fcm zorunlu alanlar\u0131 doldurun
|
addcustomer.notification.validation=L\u00fctfen t\u00fcm zorunlu alanlar\u0131 doldurun
|
||||||
addcustomer.notification.success=M\u00fc\u015fteri ba\u015far\u0131yla olu\u015fturuldu
|
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.check=L\u00fctfen giri\u015flerinizi kontrol edin
|
||||||
addcustomer.notification.error=Hata: {0}
|
addcustomer.notification.error=Hata: {0}
|
||||||
addcustomer.validation.required=Bu alan gereklidir
|
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.notification.error=Fatura oluşturulurken hata: {0}
|
||||||
admincreateinvoices.button.createselected=Fatura oluştur ({0})
|
admincreateinvoices.button.createselected=Fatura oluştur ({0})
|
||||||
admincreateinvoices.field.month=Fatura ayı
|
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.title=Faturaları incele
|
||||||
admincreateinvoices.dialog.position=Fatura {0} / {1}
|
admincreateinvoices.dialog.position=Fatura {0} / {1}
|
||||||
admincreateinvoices.dialog.button.previous=Geri
|
admincreateinvoices.dialog.button.previous=Geri
|
||||||
|
|||||||
Reference in New Issue
Block a user