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:
2026-07-07 17:41:21 +02:00
parent ef019eecee
commit f4c2fa6ba3
25 changed files with 290 additions and 35 deletions

View File

@@ -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

View File

@@ -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),
],

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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"));
}

View File

@@ -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"));
}

View File

@@ -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()));

View File

@@ -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);
}

View File

@@ -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");

View File

@@ -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);

View File

@@ -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

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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ļ

View File

@@ -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

View File

@@ -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=Назад

View File

@@ -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