feat: Preistabelle auf App-Nutzer-Gebühr umgestellt, Rechnungsprüf-Dialog mit E-Mail-Versand und Rabatt-Erfassung
- Preistabelle: App-Nutzungslizenz und Umsatzbeteiligung durch Gebühr pro App-Nutzer ersetzt, Abrechnung pro konfiguriertem App-Nutzer (countByErstelltVon) - Rechnungen erstellen: Prüf-Dialog mit Vor/Zurück-Navigation, Entfernen einzelner Rechnungen und Versand als PDF-Anhang an die Kunden (EmailService mit Anhang-Unterstützung) - Rabatt pro Kunde über Icon-Dialog mit Prozentwert und Grund, als eigene Position auf der Rechnung berücksichtigt - E-Mail-Spalte entfernt, Rechnung-erstellen-Button durch Icon ersetzt, (0 App-Nutzer)-Zusatz entfällt - Version 0.9.20 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,18 +10,18 @@ public class PriceTable {
|
||||
private String id;
|
||||
|
||||
private String monthlyBasePackage;
|
||||
private String appUsageLicense;
|
||||
private String revenueParticipation;
|
||||
|
||||
/** Fee billed per app user set up by the customer (unit price per month). */
|
||||
private String appUserFee;
|
||||
|
||||
private String statisticalEvaluation;
|
||||
|
||||
public PriceTable() {
|
||||
}
|
||||
|
||||
public PriceTable(String monthlyBasePackage, String appUsageLicense, String revenueParticipation,
|
||||
String statisticalEvaluation) {
|
||||
public PriceTable(String monthlyBasePackage, String appUserFee, String statisticalEvaluation) {
|
||||
this.monthlyBasePackage = monthlyBasePackage;
|
||||
this.appUsageLicense = appUsageLicense;
|
||||
this.revenueParticipation = revenueParticipation;
|
||||
this.appUserFee = appUserFee;
|
||||
this.statisticalEvaluation = statisticalEvaluation;
|
||||
}
|
||||
|
||||
@@ -41,20 +41,12 @@ public class PriceTable {
|
||||
this.monthlyBasePackage = monthlyBasePackage;
|
||||
}
|
||||
|
||||
public String getAppUsageLicense() {
|
||||
return appUsageLicense;
|
||||
public String getAppUserFee() {
|
||||
return appUserFee;
|
||||
}
|
||||
|
||||
public void setAppUsageLicense(String appUsageLicense) {
|
||||
this.appUsageLicense = appUsageLicense;
|
||||
}
|
||||
|
||||
public String getRevenueParticipation() {
|
||||
return revenueParticipation;
|
||||
}
|
||||
|
||||
public void setRevenueParticipation(String revenueParticipation) {
|
||||
this.revenueParticipation = revenueParticipation;
|
||||
public void setAppUserFee(String appUserFee) {
|
||||
this.appUserFee = appUserFee;
|
||||
}
|
||||
|
||||
public String getStatisticalEvaluation() {
|
||||
|
||||
@@ -8,9 +8,15 @@ import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.IFrame;
|
||||
import com.vaadin.flow.component.html.Main;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.select.Select;
|
||||
import com.vaadin.flow.component.textfield.NumberField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.HasDynamicTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
@@ -20,9 +26,11 @@ import de.assecutor.votianlt.model.invoices.SystemInvoiceData;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
|
||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||
import de.assecutor.votianlt.repository.PriceTableRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||
import de.assecutor.votianlt.service.EmailService;
|
||||
import de.assecutor.votianlt.service.InvoiceTemplateService;
|
||||
import de.assecutor.votianlt.service.SystemCompanyProfileService;
|
||||
import de.assecutor.votianlt.util.DateTimeFormatUtil;
|
||||
@@ -59,21 +67,33 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PriceTableRepository priceTableRepository;
|
||||
private final AppUserRepository appUserRepository;
|
||||
private final InvoiceTemplateService invoiceTemplateService;
|
||||
private final CustomerInvoiceService customerInvoiceService;
|
||||
private final SystemCompanyProfileService systemCompanyProfileService;
|
||||
private final EmailService emailService;
|
||||
|
||||
/** Billing month, selectable by the user; defaults to the current month. */
|
||||
private YearMonth billingMonth = YearMonth.now();
|
||||
|
||||
/** Discount with reason per customer (user id), entered via the dialog. */
|
||||
private final Map<org.bson.types.ObjectId, DiscountEntry> discounts = new HashMap<>();
|
||||
|
||||
/** A discount entered for one customer: percent value plus reason. */
|
||||
private record DiscountEntry(Double percent, String reason) {
|
||||
}
|
||||
|
||||
public AdminCreateInvoicesView(UserRepository userRepository, PriceTableRepository priceTableRepository,
|
||||
InvoiceTemplateService invoiceTemplateService, CustomerInvoiceService customerInvoiceService,
|
||||
SystemCompanyProfileService systemCompanyProfileService) {
|
||||
AppUserRepository appUserRepository, InvoiceTemplateService invoiceTemplateService,
|
||||
CustomerInvoiceService customerInvoiceService, SystemCompanyProfileService systemCompanyProfileService,
|
||||
EmailService emailService) {
|
||||
this.userRepository = userRepository;
|
||||
this.priceTableRepository = priceTableRepository;
|
||||
this.appUserRepository = appUserRepository;
|
||||
this.invoiceTemplateService = invoiceTemplateService;
|
||||
this.customerInvoiceService = customerInvoiceService;
|
||||
this.systemCompanyProfileService = systemCompanyProfileService;
|
||||
this.emailService = emailService;
|
||||
|
||||
setSizeFull();
|
||||
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
|
||||
@@ -110,8 +130,6 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
grid.setItems(loadBillingInfos());
|
||||
grid.addColumn(info -> customerDisplayName(info.user()))
|
||||
.setHeader(getTranslation("admincreateinvoices.column.customer")).setFlexGrow(2).setSortable(true);
|
||||
grid.addColumn(info -> info.user().getEmail()).setHeader(getTranslation("admincreateinvoices.column.email"))
|
||||
.setFlexGrow(2);
|
||||
grid.addColumn(info -> info.startDate() != null ? DateTimeFormatUtil.formatDate(info.startDate()) : "")
|
||||
.setHeader(getTranslation("admincreateinvoices.column.start")).setFlexGrow(1).setSortable(true);
|
||||
grid.addColumn(info -> info.billedDays() < info.daysInMonth()
|
||||
@@ -120,12 +138,29 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
.setHeader(getTranslation("admincreateinvoices.column.days")).setFlexGrow(1);
|
||||
grid.addColumn(info -> DateTimeFormatUtil.formatDate(info.dueDate()))
|
||||
.setHeader(getTranslation("admincreateinvoices.column.due")).setFlexGrow(1);
|
||||
grid.addColumn(info -> formatAmount(info.netTotal()) + " €")
|
||||
grid.addColumn(info -> formatAmount(applyDiscount(info).netTotal()) + " €")
|
||||
.setHeader(getTranslation("admincreateinvoices.column.net")).setFlexGrow(1)
|
||||
.setTextAlign(com.vaadin.flow.component.grid.ColumnTextAlign.END);
|
||||
grid.addComponentColumn(info -> {
|
||||
Button createButton = new Button(getTranslation("admincreateinvoices.button.create"));
|
||||
createButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SMALL);
|
||||
DiscountEntry entry = discounts.get(info.user().getId());
|
||||
Button discountButton = new Button(new Icon(VaadinIcon.TAG));
|
||||
discountButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_ICON,
|
||||
entry != null ? ButtonVariant.LUMO_PRIMARY : ButtonVariant.LUMO_TERTIARY);
|
||||
String tooltip = entry != null
|
||||
? getTranslation("admincreateinvoices.discount.position", formatPercent(entry.percent()))
|
||||
+ (entry.reason().isBlank() ? "" : " – " + entry.reason())
|
||||
: getTranslation("admincreateinvoices.discount.dialog.title");
|
||||
discountButton.setTooltipText(tooltip);
|
||||
discountButton.setAriaLabel(getTranslation("admincreateinvoices.discount.dialog.title"));
|
||||
discountButton.addClickListener(e -> openDiscountDialog(info, grid));
|
||||
return discountButton;
|
||||
}).setHeader(getTranslation("admincreateinvoices.column.discount")).setAutoWidth(true).setFlexGrow(0);
|
||||
grid.addComponentColumn(info -> {
|
||||
Button createButton = new Button(new Icon(VaadinIcon.INVOICE));
|
||||
createButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SMALL,
|
||||
ButtonVariant.LUMO_ICON);
|
||||
createButton.setTooltipText(getTranslation("admincreateinvoices.button.create"));
|
||||
createButton.setAriaLabel(getTranslation("admincreateinvoices.button.create"));
|
||||
createButton.addClickListener(e -> createInvoicePdf(info));
|
||||
return createButton;
|
||||
}).setAutoWidth(true).setFlexGrow(0).setFrozenToEnd(true);
|
||||
@@ -135,10 +170,11 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
createSelectedButton.setText(getTranslation("admincreateinvoices.button.createselected", count));
|
||||
createSelectedButton.setEnabled(count > 0);
|
||||
});
|
||||
createSelectedButton.addClickListener(e -> createInvoicesPdf(new ArrayList<>(grid.getSelectedItems())));
|
||||
createSelectedButton.addClickListener(e -> openInvoiceReviewDialog(new ArrayList<>(grid.getSelectedItems())));
|
||||
monthSelect.addValueChangeListener(event -> {
|
||||
if (event.getValue() != null) {
|
||||
billingMonth = event.getValue();
|
||||
discounts.clear();
|
||||
grid.deselectAll();
|
||||
grid.setItems(loadBillingInfos());
|
||||
}
|
||||
@@ -213,23 +249,32 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
BigDecimal factor = new BigDecimal(billedDays).divide(new BigDecimal(daysInMonth), 10, RoundingMode.HALF_UP);
|
||||
boolean proRata = billedDays < daysInMonth;
|
||||
|
||||
// Number of app users belonging to this customer in app_user: the
|
||||
// per-app-user fee is billed once per configured app user
|
||||
long appUserCount = user.getId() != null ? appUserRepository.countByErstelltVon(user.getId()) : 0;
|
||||
|
||||
List<Map<String, String>> positions = new ArrayList<>();
|
||||
BigDecimal netTotal = BigDecimal.ZERO;
|
||||
String[][] entries = {
|
||||
// Position entries: label, configured value, quantity
|
||||
Object[][] entries = {
|
||||
{ getTranslation("adminpricetable.field.monthly"),
|
||||
priceTable != null ? priceTable.getMonthlyBasePackage() : null },
|
||||
{ getTranslation("adminpricetable.field.applicense"),
|
||||
priceTable != null ? priceTable.getAppUsageLicense() : null },
|
||||
{ getTranslation("adminpricetable.field.revenue"),
|
||||
priceTable != null ? priceTable.getRevenueParticipation() : null } };
|
||||
priceTable != null ? priceTable.getMonthlyBasePackage() : null, 1L },
|
||||
{ getTranslation("adminpricetable.field.appuserfee"),
|
||||
priceTable != null ? priceTable.getAppUserFee() : null, appUserCount } };
|
||||
|
||||
for (String[] entry : entries) {
|
||||
String rawValue = entry[1] != null ? entry[1].trim() : "";
|
||||
for (Object[] entry : entries) {
|
||||
String label = (String) entry[0];
|
||||
String rawValue = entry[1] != null ? ((String) entry[1]).trim() : "";
|
||||
long quantity = (Long) entry[2];
|
||||
BigDecimal amount = parseAmount(rawValue);
|
||||
Map<String, String> position = new HashMap<>();
|
||||
if (amount != null) {
|
||||
BigDecimal billed = amount.multiply(factor).setScale(2, RoundingMode.HALF_UP);
|
||||
String name = entry[0];
|
||||
BigDecimal billed = amount.multiply(new BigDecimal(quantity)).multiply(factor).setScale(2,
|
||||
RoundingMode.HALF_UP);
|
||||
String name = label;
|
||||
if (quantity > 1) {
|
||||
name += " " + getTranslation("admincreateinvoices.appusers.suffix", quantity);
|
||||
}
|
||||
if (proRata) {
|
||||
name += " " + getTranslation("admincreateinvoices.prorata.suffix", billedDays, daysInMonth);
|
||||
}
|
||||
@@ -237,9 +282,9 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
position.put("netAmount", formatAmount(billed));
|
||||
netTotal = netTotal.add(billed);
|
||||
} else {
|
||||
// Non-monetary values (e.g. "15 %" revenue participation) are
|
||||
// shown as-is and are not part of the totals
|
||||
position.put("name", entry[0]);
|
||||
// Non-monetary values are shown as-is and are not part of the
|
||||
// totals
|
||||
position.put("name", label);
|
||||
position.put("netAmount", rawValue.isEmpty() ? "-" : rawValue);
|
||||
}
|
||||
positions.add(position);
|
||||
@@ -252,6 +297,85 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
grossTotal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the dialog to enter (or change) the discount and its reason for
|
||||
* one customer. An empty or zero discount removes the entry.
|
||||
*/
|
||||
private void openDiscountDialog(BillingInfo info, Grid<BillingInfo> grid) {
|
||||
Dialog dialog = DialogStylingHelper
|
||||
.createStyledDialog(getTranslation("admincreateinvoices.discount.dialog.title"), "420px");
|
||||
|
||||
DiscountEntry entry = discounts.get(info.user().getId());
|
||||
|
||||
NumberField percentField = new NumberField(getTranslation("admincreateinvoices.discount.dialog.percent"));
|
||||
percentField.setMin(0);
|
||||
percentField.setMax(100);
|
||||
percentField.setSuffixComponent(new Span("%"));
|
||||
percentField.setWidthFull();
|
||||
if (entry != null) {
|
||||
percentField.setValue(entry.percent());
|
||||
}
|
||||
|
||||
TextField reasonField = new TextField(getTranslation("admincreateinvoices.discount.dialog.reason"));
|
||||
reasonField.setWidthFull();
|
||||
if (entry != null) {
|
||||
reasonField.setValue(entry.reason());
|
||||
}
|
||||
|
||||
VerticalLayout content = DialogStylingHelper.createContentLayout("380px");
|
||||
content.add(percentField, reasonField);
|
||||
|
||||
Button cancelButton = new Button(getTranslation("button.cancel"), e -> dialog.close());
|
||||
cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
Button applyButton = new Button(getTranslation("admincreateinvoices.discount.dialog.apply"), e -> {
|
||||
Double percent = percentField.getValue();
|
||||
// Clamp manual input to the valid range 0-100
|
||||
if (percent != null && percent > 100) {
|
||||
percent = 100d;
|
||||
}
|
||||
if (percent != null && percent > 0) {
|
||||
discounts.put(info.user().getId(), new DiscountEntry(percent,
|
||||
reasonField.getValue() != null ? reasonField.getValue().trim() : ""));
|
||||
} else {
|
||||
discounts.remove(info.user().getId());
|
||||
}
|
||||
dialog.close();
|
||||
grid.getDataProvider().refreshItem(info);
|
||||
});
|
||||
applyButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
dialog.add(DialogStylingHelper.wrapContent(content));
|
||||
dialog.getFooter().add(cancelButton, applyButton);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the discount entered for this customer: adds a negative
|
||||
* discount position (including the reason) and recalculates the totals.
|
||||
* Returns the info unchanged if no discount is set.
|
||||
*/
|
||||
private BillingInfo applyDiscount(BillingInfo info) {
|
||||
DiscountEntry entry = discounts.get(info.user().getId());
|
||||
if (entry == null || entry.percent() == null || entry.percent() <= 0) {
|
||||
return info;
|
||||
}
|
||||
BigDecimal discountAmount = info.netTotal().multiply(BigDecimal.valueOf(entry.percent()))
|
||||
.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
|
||||
List<Map<String, String>> positions = new ArrayList<>(info.positions());
|
||||
Map<String, String> position = new HashMap<>();
|
||||
String name = getTranslation("admincreateinvoices.discount.position", formatPercent(entry.percent()));
|
||||
if (!entry.reason().isBlank()) {
|
||||
name += " – " + entry.reason();
|
||||
}
|
||||
position.put("name", name);
|
||||
position.put("netAmount", "-" + formatAmount(discountAmount));
|
||||
positions.add(position);
|
||||
BigDecimal netTotal = info.netTotal().subtract(discountAmount);
|
||||
BigDecimal vatTotal = netTotal.multiply(SYSTEM_VAT_RATE).setScale(2, RoundingMode.HALF_UP);
|
||||
return new BillingInfo(info.user(), info.startDate(), info.billedDays(), info.daysInMonth(), info.dueDate(),
|
||||
positions, netTotal, vatTotal, netTotal.add(vatTotal));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PDF generation
|
||||
// ==========================================
|
||||
@@ -261,6 +385,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
if (templateData == null) {
|
||||
return;
|
||||
}
|
||||
info = applyDiscount(info);
|
||||
try {
|
||||
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData,
|
||||
buildInvoiceVariables(info), null);
|
||||
@@ -274,11 +399,16 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
}
|
||||
|
||||
/** One generated invoice held in the review dialog. */
|
||||
private record InvoicePreview(BillingInfo info, String invoiceNumber, byte[] pdf) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the invoices of all selected customers as one merged PDF, one
|
||||
* invoice per page.
|
||||
* Opens the review dialog for the selected customers: one invoice per
|
||||
* page, navigable via previous/next, removable from the list and sendable
|
||||
* to the customers by email.
|
||||
*/
|
||||
private void createInvoicesPdf(List<BillingInfo> infos) {
|
||||
private void openInvoiceReviewDialog(List<BillingInfo> infos) {
|
||||
if (infos.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -286,20 +416,142 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
if (templateData == null) {
|
||||
return;
|
||||
}
|
||||
List<InvoicePreview> previews = new ArrayList<>();
|
||||
try {
|
||||
List<byte[]> pdfs = new ArrayList<>();
|
||||
for (BillingInfo info : infos) {
|
||||
pdfs.add(customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData,
|
||||
buildInvoiceVariables(info), null));
|
||||
BillingInfo billed = applyDiscount(info);
|
||||
previews.add(new InvoicePreview(billed, buildInvoiceNumber(billed.user()), customerInvoiceService
|
||||
.generatePdfFromCanvasTemplateWithData(templateData, buildInvoiceVariables(billed), null)));
|
||||
}
|
||||
String fileName = "rechnungen-" + billingMonth.format(DateTimeFormatter.ofPattern("yyyyMM"));
|
||||
showPdfInDialog(mergePdfs(pdfs), fileName);
|
||||
} catch (Exception ex) {
|
||||
log.error("Error creating system invoices for {} users: {}", infos.size(), ex.getMessage(), ex);
|
||||
Notification
|
||||
.show(getTranslation("admincreateinvoices.notification.error", ex.getMessage()), 5000,
|
||||
Notification.Position.BOTTOM_CENTER)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
Dialog dialog = DialogStylingHelper.createStyledDialog(getTranslation("admincreateinvoices.dialog.title"),
|
||||
"90vw");
|
||||
dialog.setHeight("90vh");
|
||||
|
||||
Paragraph positionLabel = new Paragraph();
|
||||
positionLabel.addClassNames(LumoUtility.Margin.NONE, LumoUtility.FontWeight.SEMIBOLD,
|
||||
LumoUtility.Padding.SMALL);
|
||||
positionLabel.getStyle().set("flex-shrink", "0");
|
||||
|
||||
IFrame pdfFrame = new IFrame();
|
||||
pdfFrame.setWidth("100%");
|
||||
pdfFrame.getStyle().set("flex", "1").set("min-height", "0").set("border", "none");
|
||||
|
||||
Div content = new Div(positionLabel, pdfFrame);
|
||||
content.getStyle().set("display", "flex").set("flex-direction", "column");
|
||||
content.setWidth("100%");
|
||||
content.setHeight("100%");
|
||||
|
||||
Button previousButton = new Button(getTranslation("admincreateinvoices.dialog.button.previous"));
|
||||
previousButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
Button nextButton = new Button(getTranslation("admincreateinvoices.dialog.button.next"));
|
||||
nextButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
Button deleteButton = new Button(getTranslation("admincreateinvoices.dialog.button.delete"));
|
||||
deleteButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR);
|
||||
Button sendButton = new Button(getTranslation("admincreateinvoices.dialog.button.send"));
|
||||
sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
Button closeButton = new Button(getTranslation("button.close"), e -> dialog.close());
|
||||
closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
|
||||
int[] index = { 0 };
|
||||
Runnable updateView = () -> {
|
||||
InvoicePreview current = previews.get(index[0]);
|
||||
positionLabel.setText(getTranslation("admincreateinvoices.dialog.position", index[0] + 1, previews.size())
|
||||
+ " – " + customerDisplayName(current.info().user()) + " (" + current.invoiceNumber() + ")");
|
||||
pdfFrame.getElement().setAttribute("src",
|
||||
"data:application/pdf;base64," + Base64.getEncoder().encodeToString(current.pdf()));
|
||||
previousButton.setEnabled(index[0] > 0);
|
||||
nextButton.setEnabled(index[0] < previews.size() - 1);
|
||||
};
|
||||
previousButton.addClickListener(e -> {
|
||||
if (index[0] > 0) {
|
||||
index[0]--;
|
||||
updateView.run();
|
||||
}
|
||||
});
|
||||
nextButton.addClickListener(e -> {
|
||||
if (index[0] < previews.size() - 1) {
|
||||
index[0]++;
|
||||
updateView.run();
|
||||
}
|
||||
});
|
||||
deleteButton.addClickListener(e -> {
|
||||
previews.remove(index[0]);
|
||||
Notification.show(getTranslation("admincreateinvoices.dialog.notification.removed"), 3000,
|
||||
Notification.Position.BOTTOM_CENTER);
|
||||
if (previews.isEmpty()) {
|
||||
dialog.close();
|
||||
return;
|
||||
}
|
||||
index[0] = Math.min(index[0], previews.size() - 1);
|
||||
updateView.run();
|
||||
});
|
||||
sendButton.addClickListener(e -> {
|
||||
sendButton.setEnabled(false);
|
||||
sendInvoices(previews);
|
||||
dialog.close();
|
||||
});
|
||||
updateView.run();
|
||||
|
||||
dialog.add(DialogStylingHelper.wrapContent(content, true));
|
||||
dialog.getFooter().add(previousButton, nextButton, deleteButton, sendButton, closeButton);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends each invoice as PDF attachment to the respective customer's email
|
||||
* address and reports how many mails were (not) sent.
|
||||
*/
|
||||
private void sendInvoices(List<InvoicePreview> previews) {
|
||||
String issuerName = "";
|
||||
try {
|
||||
issuerName = safe(systemCompanyProfileService.createSystemInvoiceData().getCompanyName());
|
||||
} catch (Exception ex) {
|
||||
log.warn("Could not load system company profile for invoice mail: {}", ex.getMessage());
|
||||
}
|
||||
String monthLabel = billingMonth.format(DateTimeFormatter.ofPattern("LLLL yyyy", getLocale()));
|
||||
|
||||
int sent = 0;
|
||||
int failed = 0;
|
||||
for (InvoicePreview preview : previews) {
|
||||
String email = preview.info().user().getEmail();
|
||||
if (email == null || email.isBlank()) {
|
||||
log.warn("Customer {} has no email address, invoice {} not sent",
|
||||
customerDisplayName(preview.info().user()), preview.invoiceNumber());
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
emailService.sendEmailWithAttachment(email,
|
||||
getTranslation("admincreateinvoices.mail.subject", preview.invoiceNumber()),
|
||||
getTranslation("admincreateinvoices.mail.body", preview.invoiceNumber(), monthLabel,
|
||||
issuerName),
|
||||
preview.pdf(), preview.invoiceNumber() + ".pdf");
|
||||
sent++;
|
||||
} catch (Exception ex) {
|
||||
log.error("Error sending invoice {} to {}: {}", preview.invoiceNumber(), email, ex.getMessage(), ex);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
if (sent > 0) {
|
||||
Notification
|
||||
.show(getTranslation("admincreateinvoices.notification.sent", sent), 5000,
|
||||
Notification.Position.BOTTOM_CENTER)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
}
|
||||
if (failed > 0) {
|
||||
Notification
|
||||
.show(getTranslation("admincreateinvoices.notification.sendfailed", failed), 5000,
|
||||
Notification.Position.BOTTOM_CENTER)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,24 +573,6 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
return template.get().getTemplateData();
|
||||
}
|
||||
|
||||
private byte[] mergePdfs(List<byte[]> pdfs) throws Exception {
|
||||
if (pdfs.size() == 1) {
|
||||
return pdfs.get(0);
|
||||
}
|
||||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||||
com.itextpdf.kernel.pdf.PdfDocument merged = new com.itextpdf.kernel.pdf.PdfDocument(
|
||||
new com.itextpdf.kernel.pdf.PdfWriter(baos));
|
||||
com.itextpdf.kernel.utils.PdfMerger merger = new com.itextpdf.kernel.utils.PdfMerger(merged);
|
||||
for (byte[] pdf : pdfs) {
|
||||
com.itextpdf.kernel.pdf.PdfDocument source = new com.itextpdf.kernel.pdf.PdfDocument(
|
||||
new com.itextpdf.kernel.pdf.PdfReader(new java.io.ByteArrayInputStream(pdf)));
|
||||
merger.merge(source, 1, source.getNumberOfPages());
|
||||
source.close();
|
||||
}
|
||||
merged.close();
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private Map<String, String> buildInvoiceVariables(BillingInfo info) throws Exception {
|
||||
SystemInvoiceData issuer = systemCompanyProfileService.createSystemInvoiceData();
|
||||
User user = info.user();
|
||||
@@ -468,6 +702,11 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
||||
return amount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",");
|
||||
}
|
||||
|
||||
/** Formats a percent value without trailing zeros, e.g. 10 or 12,5. */
|
||||
private String formatPercent(Double percent) {
|
||||
return new BigDecimal(percent.toString()).stripTrailingZeros().toPlainString().replace('.', ',');
|
||||
}
|
||||
|
||||
private String safe(String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.assecutor.votianlt.pages.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
@@ -19,8 +18,7 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit
|
||||
|
||||
private final PriceTableRepository priceTableRepository;
|
||||
private final TextField monthlyBasePackage;
|
||||
private final TextField appUsageLicense;
|
||||
private final TextField revenueParticipation;
|
||||
private final TextField appUserFee;
|
||||
|
||||
public AdminPricetableView(PriceTableRepository priceTableRepository) {
|
||||
this.priceTableRepository = priceTableRepository;
|
||||
@@ -41,29 +39,20 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit
|
||||
monthlyBasePackage.setWidth("40%");
|
||||
monthlyBasePackage.setMaxWidth("40%");
|
||||
|
||||
appUsageLicense = new TextField();
|
||||
appUsageLicense.setLabel(getTranslation("adminpricetable.field.applicense"));
|
||||
appUsageLicense.setWidth("40%");
|
||||
appUsageLicense.setMaxWidth("40%");
|
||||
|
||||
revenueParticipation = new TextField();
|
||||
revenueParticipation.setLabel(getTranslation("adminpricetable.field.revenue"));
|
||||
revenueParticipation.setWidth("40%");
|
||||
revenueParticipation.setMaxWidth("40%");
|
||||
|
||||
fieldsLayout.add(monthlyBasePackage, appUsageLicense, revenueParticipation);
|
||||
|
||||
add(fieldsLayout);
|
||||
appUserFee = new TextField();
|
||||
appUserFee.setLabel(getTranslation("adminpricetable.field.appuserfee"));
|
||||
appUserFee.setHelperText(getTranslation("adminpricetable.field.appuserfee.helper"));
|
||||
appUserFee.setWidth("40%");
|
||||
appUserFee.setMaxWidth("40%");
|
||||
|
||||
Button saveButton = new Button(getTranslation("button.savechanges"));
|
||||
saveButton.getStyle().set("margin-top", "20px");
|
||||
saveButton.addClickListener(e -> savePriceTable());
|
||||
saveButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
Div actions = new Div(saveButton);
|
||||
actions.addClassNames("form-shell", "simple-card");
|
||||
fieldsLayout.add(monthlyBasePackage, appUserFee, saveButton);
|
||||
|
||||
add(fieldsLayout, actions);
|
||||
add(fieldsLayout);
|
||||
|
||||
// Load existing data
|
||||
loadPriceTable();
|
||||
@@ -75,8 +64,7 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit
|
||||
PriceTable priceTable = priceTableRepository.findAll().stream().findFirst().orElse(new PriceTable());
|
||||
|
||||
priceTable.setMonthlyBasePackage(monthlyBasePackage.getValue());
|
||||
priceTable.setAppUsageLicense(appUsageLicense.getValue());
|
||||
priceTable.setRevenueParticipation(revenueParticipation.getValue());
|
||||
priceTable.setAppUserFee(appUserFee.getValue());
|
||||
|
||||
priceTableRepository.save(priceTable);
|
||||
Notification.show(getTranslation("adminpricetable.notification.saved"), 3000,
|
||||
@@ -94,10 +82,7 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit
|
||||
if (priceTable != null) {
|
||||
monthlyBasePackage
|
||||
.setValue(priceTable.getMonthlyBasePackage() != null ? priceTable.getMonthlyBasePackage() : "");
|
||||
appUsageLicense
|
||||
.setValue(priceTable.getAppUsageLicense() != null ? priceTable.getAppUsageLicense() : "");
|
||||
revenueParticipation.setValue(
|
||||
priceTable.getRevenueParticipation() != null ? priceTable.getRevenueParticipation() : "");
|
||||
appUserFee.setValue(priceTable.getAppUserFee() != null ? priceTable.getAppUserFee() : "");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Notification.show(getTranslation("adminpricetable.notification.load.error", ex.getMessage()), 5000,
|
||||
|
||||
@@ -202,29 +202,40 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
|
||||
variables.put("customer.email", "nutzer@beispiel.de");
|
||||
variables.put("customer.phone", "0123 456789");
|
||||
|
||||
// Positionen: die Rechnung an die Nutzer enthält ausschließlich die drei
|
||||
// Positionen: die Rechnung an die Nutzer enthält ausschließlich die
|
||||
// Parameter der Preistabelle (/admin-price-table), keine Leistungen
|
||||
addPriceTablePositions(variables);
|
||||
return variables;
|
||||
}
|
||||
|
||||
/** Example app user count for the template preview (Musterfirma). */
|
||||
private static final int SAMPLE_APP_USER_COUNT = 3;
|
||||
|
||||
/**
|
||||
* The three positions of the price table (/admin-price-table): label plus
|
||||
* configured value. These are the only positions on system invoices.
|
||||
* The positions of the price table (/admin-price-table): label plus
|
||||
* configured value. These are the only positions on system invoices. The
|
||||
* per-app-user fee is shown multiplied by an example user count, since the
|
||||
* preview works with sample data anyway.
|
||||
*/
|
||||
private String[][] priceTableEntries() {
|
||||
PriceTable priceTable = priceTableRepository.findAll().stream().findFirst().orElse(null);
|
||||
|
||||
String feeLabel = getTranslation("adminpricetable.field.appuserfee");
|
||||
String feeValue = priceTable != null ? priceTable.getAppUserFee() : null;
|
||||
BigDecimal unitAmount = parseAmount(feeValue != null ? feeValue.trim() : "");
|
||||
if (unitAmount != null) {
|
||||
feeLabel += " " + getTranslation("admincreateinvoices.appusers.suffix", SAMPLE_APP_USER_COUNT);
|
||||
feeValue = formatAmount(unitAmount.multiply(new BigDecimal(SAMPLE_APP_USER_COUNT))) + " €";
|
||||
}
|
||||
|
||||
return new String[][] {
|
||||
{ getTranslation("adminpricetable.field.monthly"),
|
||||
priceTable != null ? priceTable.getMonthlyBasePackage() : null },
|
||||
{ getTranslation("adminpricetable.field.applicense"),
|
||||
priceTable != null ? priceTable.getAppUsageLicense() : null },
|
||||
{ getTranslation("adminpricetable.field.revenue"),
|
||||
priceTable != null ? priceTable.getRevenueParticipation() : null } };
|
||||
{ feeLabel, feeValue } };
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON for the canvas rendering of the "services.list" block: the three
|
||||
* JSON for the canvas rendering of the "services.list" block: the
|
||||
* price table positions plus totals (window.profileInvoiceServiceData).
|
||||
*/
|
||||
private String buildCanvasPositionsJson() {
|
||||
@@ -277,7 +288,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
|
||||
position.put("netAmount", formatAmount(amount));
|
||||
netTotal = netTotal.add(amount);
|
||||
} else {
|
||||
// Nicht-numerische Werte (z.B. "15 %" Umsatzbeteiligung) unverändert anzeigen
|
||||
// Nicht-numerische Werte unverändert anzeigen
|
||||
position.put("netAmount", rawValue.isEmpty() ? "-" : rawValue);
|
||||
}
|
||||
positions.add(position);
|
||||
|
||||
@@ -23,4 +23,8 @@ public interface AppUserRepository extends MongoRepository<AppUser, ObjectId> {
|
||||
|
||||
// Find AppUser by bezeichnung
|
||||
AppUser findByBezeichnung(String bezeichnung);
|
||||
|
||||
// Number of app users belonging to a customer (for per-app-user billing);
|
||||
// same association as the customer's own app user list (findByErstelltVon)
|
||||
long countByErstelltVon(ObjectId erstelltVon);
|
||||
}
|
||||
|
||||
@@ -6,12 +6,16 @@ import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||
import de.assecutor.votianlt.repository.JobRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -378,6 +382,23 @@ public class EmailService {
|
||||
mailSender.send(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a text email with a single attachment. Synchronous on purpose so
|
||||
* callers can report per-recipient success or failure to the user.
|
||||
*/
|
||||
public void sendEmailWithAttachment(String to, String subject, String body, byte[] attachment,
|
||||
String attachmentName) throws MessagingException {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
helper.setFrom(smtpUsername);
|
||||
helper.setTo(to);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(body);
|
||||
helper.addAttachment(attachmentName, new ByteArrayResource(attachment));
|
||||
mailSender.send(message);
|
||||
log.info("Email with attachment {} sent to {}", attachmentName, to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a simple text email
|
||||
*/
|
||||
|
||||
@@ -1051,8 +1051,8 @@ misc.retry=Erneut versuchen
|
||||
# Admin Price Table
|
||||
adminpricetable.title=Preistabelle
|
||||
adminpricetable.field.monthly=Monatliches Basispaket
|
||||
adminpricetable.field.applicense=App-Nutzungslizenz
|
||||
adminpricetable.field.revenue=Umsatzbeteiligung
|
||||
adminpricetable.field.appuserfee=Gebühr pro App-Nutzer
|
||||
adminpricetable.field.appuserfee.helper=Wird je eingerichtetem App-Nutzer monatlich berechnet
|
||||
adminpricetable.notification.saved=Preistabelle wurde gespeichert
|
||||
adminpricetable.notification.save.error=Fehler beim Speichern: {0}
|
||||
adminpricetable.notification.load.error=Fehler beim Laden: {0}
|
||||
@@ -1112,7 +1112,12 @@ page.title.admin.createinvoices=Rechnungen erstellen
|
||||
admincreateinvoices.title=Rechnungen erstellen
|
||||
admincreateinvoices.hint=Rechnungen werden zum Monatsende (Ultimo) fällig. Beginnt ein Kunde im laufenden Monat, werden nur die verbleibenden Tage bis Ultimo berechnet.
|
||||
admincreateinvoices.column.customer=Kunde
|
||||
admincreateinvoices.column.email=E-Mail
|
||||
admincreateinvoices.column.discount=Rabatt
|
||||
admincreateinvoices.discount.position=Rabatt ({0} %)
|
||||
admincreateinvoices.discount.dialog.title=Rabatt erfassen
|
||||
admincreateinvoices.discount.dialog.percent=Rabatt in %
|
||||
admincreateinvoices.discount.dialog.reason=Grund für den Rabatt
|
||||
admincreateinvoices.discount.dialog.apply=Übernehmen
|
||||
admincreateinvoices.column.start=Kunde seit
|
||||
admincreateinvoices.column.days=Abgerechnete Tage
|
||||
admincreateinvoices.column.due=Fällig am
|
||||
@@ -1120,8 +1125,20 @@ admincreateinvoices.column.net=Nettobetrag
|
||||
admincreateinvoices.days.full=Voller Monat
|
||||
admincreateinvoices.days.partial={0} von {1} Tagen
|
||||
admincreateinvoices.prorata.suffix=(anteilig {0}/{1} Tage)
|
||||
admincreateinvoices.appusers.suffix=({0} App-Nutzer)
|
||||
admincreateinvoices.button.create=Rechnung erstellen
|
||||
admincreateinvoices.notification.notemplate=Kein Rechnungstemplate vorhanden. Bitte zuerst im Rechnungsgenerator ein Template speichern.
|
||||
admincreateinvoices.notification.error=Fehler beim Erstellen der Rechnung: {0}
|
||||
admincreateinvoices.button.createselected=Rechnungen erstellen ({0})
|
||||
admincreateinvoices.field.month=Abrechnungsmonat
|
||||
admincreateinvoices.dialog.title=Rechnungen prüfen
|
||||
admincreateinvoices.dialog.position=Rechnung {0} von {1}
|
||||
admincreateinvoices.dialog.button.previous=Zurück
|
||||
admincreateinvoices.dialog.button.next=Weiter
|
||||
admincreateinvoices.dialog.button.delete=Aus Liste entfernen
|
||||
admincreateinvoices.dialog.button.send=Rechnungen senden
|
||||
admincreateinvoices.dialog.notification.removed=Rechnung aus der Liste entfernt
|
||||
admincreateinvoices.mail.subject=Ihre Rechnung {0}
|
||||
admincreateinvoices.mail.body=Sehr geehrte Damen und Herren,\n\nanbei erhalten Sie Ihre Rechnung {0} für den Abrechnungszeitraum {1}.\n\nMit freundlichen Grüßen\n{2}
|
||||
admincreateinvoices.notification.sent={0} Rechnung(en) per E-Mail versendet
|
||||
admincreateinvoices.notification.sendfailed={0} Rechnung(en) konnten nicht versendet werden
|
||||
|
||||
@@ -881,8 +881,8 @@ misc.error=Ilmnes viga
|
||||
misc.retry=Proovi uuesti
|
||||
adminpricetable.title=Hinnatabel
|
||||
adminpricetable.field.monthly=Igakuine p\u00f5hipakett
|
||||
adminpricetable.field.applicense=\u00c4pi kasutuslitsents
|
||||
adminpricetable.field.revenue=K\u00e4ibest osalus
|
||||
adminpricetable.field.appuserfee=Tasu äpi kasutaja kohta
|
||||
adminpricetable.field.appuserfee.helper=Arvestatakse igakuiselt iga seadistatud äpi kasutaja kohta
|
||||
adminpricetable.notification.saved=Hinnatabel on salvestatud
|
||||
adminpricetable.notification.save.error=Salvestamisel ilmnes viga: {0}
|
||||
adminpricetable.notification.load.error=Laadimisel ilmnes viga: {0}
|
||||
@@ -942,7 +942,12 @@ page.title.admin.createinvoices=Arvete koostamine
|
||||
admincreateinvoices.title=Arvete koostamine
|
||||
admincreateinvoices.hint=Arved kuuluvad tasumisele kuu lõpus. Kui klient alustab kuu keskel, arvestatakse ainult kuu lõpuni jäänud päevad.
|
||||
admincreateinvoices.column.customer=Klient
|
||||
admincreateinvoices.column.email=E-post
|
||||
admincreateinvoices.column.discount=Allahindlus
|
||||
admincreateinvoices.discount.position=Allahindlus ({0}%)
|
||||
admincreateinvoices.discount.dialog.title=Sisesta allahindlus
|
||||
admincreateinvoices.discount.dialog.percent=Allahindlus (%)
|
||||
admincreateinvoices.discount.dialog.reason=Allahindluse põhjus
|
||||
admincreateinvoices.discount.dialog.apply=Rakenda
|
||||
admincreateinvoices.column.start=Klient alates
|
||||
admincreateinvoices.column.days=Arvestatud päevad
|
||||
admincreateinvoices.column.due=Tähtaeg
|
||||
@@ -950,8 +955,20 @@ admincreateinvoices.column.net=Netosumma
|
||||
admincreateinvoices.days.full=Terve kuu
|
||||
admincreateinvoices.days.partial={0} / {1} päeva
|
||||
admincreateinvoices.prorata.suffix=(proportsionaalselt {0}/{1} päeva)
|
||||
admincreateinvoices.appusers.suffix=({0} äpi kasutajat)
|
||||
admincreateinvoices.button.create=Koosta arve
|
||||
admincreateinvoices.notification.notemplate=Arve mall puudub. Palun salvestage kõigepealt mall arvegeneraatoris.
|
||||
admincreateinvoices.notification.error=Viga arve koostamisel: {0}
|
||||
admincreateinvoices.button.createselected=Koosta arved ({0})
|
||||
admincreateinvoices.field.month=Arveldusperiood
|
||||
admincreateinvoices.dialog.title=Arvete ülevaatus
|
||||
admincreateinvoices.dialog.position=Arve {0} / {1}
|
||||
admincreateinvoices.dialog.button.previous=Tagasi
|
||||
admincreateinvoices.dialog.button.next=Edasi
|
||||
admincreateinvoices.dialog.button.delete=Eemalda loendist
|
||||
admincreateinvoices.dialog.button.send=Saada arved
|
||||
admincreateinvoices.dialog.notification.removed=Arve eemaldati loendist
|
||||
admincreateinvoices.mail.subject=Teie arve {0}
|
||||
admincreateinvoices.mail.body=Lugupeetud klient,\n\nmanuses on Teie arve {0} arveldusperioodi {1} eest.\n\nLugupidamisega\n{2}
|
||||
admincreateinvoices.notification.sent={0} arve(t) saadeti e-postiga
|
||||
admincreateinvoices.notification.sendfailed={0} arve(t) ei õnnestunud saata
|
||||
|
||||
@@ -1051,8 +1051,8 @@ misc.retry=Retry
|
||||
# Admin Price Table
|
||||
adminpricetable.title=Price Table
|
||||
adminpricetable.field.monthly=Monthly Base Package
|
||||
adminpricetable.field.applicense=App Usage License
|
||||
adminpricetable.field.revenue=Revenue Share
|
||||
adminpricetable.field.appuserfee=Fee per App User
|
||||
adminpricetable.field.appuserfee.helper=Billed monthly per app user set up
|
||||
adminpricetable.notification.saved=Price table has been saved
|
||||
adminpricetable.notification.save.error=Error saving: {0}
|
||||
adminpricetable.notification.load.error=Error loading: {0}
|
||||
@@ -1112,7 +1112,12 @@ page.title.admin.createinvoices=Create invoices
|
||||
admincreateinvoices.title=Create invoices
|
||||
admincreateinvoices.hint=Invoices are due at the end of the month. If a customer starts mid-month, only the remaining days until the end of the month are billed.
|
||||
admincreateinvoices.column.customer=Customer
|
||||
admincreateinvoices.column.email=Email
|
||||
admincreateinvoices.column.discount=Discount
|
||||
admincreateinvoices.discount.position=Discount ({0}%)
|
||||
admincreateinvoices.discount.dialog.title=Enter discount
|
||||
admincreateinvoices.discount.dialog.percent=Discount in %
|
||||
admincreateinvoices.discount.dialog.reason=Reason for the discount
|
||||
admincreateinvoices.discount.dialog.apply=Apply
|
||||
admincreateinvoices.column.start=Customer since
|
||||
admincreateinvoices.column.days=Billed days
|
||||
admincreateinvoices.column.due=Due on
|
||||
@@ -1120,8 +1125,20 @@ admincreateinvoices.column.net=Net amount
|
||||
admincreateinvoices.days.full=Full month
|
||||
admincreateinvoices.days.partial={0} of {1} days
|
||||
admincreateinvoices.prorata.suffix=(pro rata {0}/{1} days)
|
||||
admincreateinvoices.appusers.suffix=({0} app users)
|
||||
admincreateinvoices.button.create=Create invoice
|
||||
admincreateinvoices.notification.notemplate=No invoice template available. Please save a template in the invoice generator first.
|
||||
admincreateinvoices.notification.error=Error creating the invoice: {0}
|
||||
admincreateinvoices.button.createselected=Create invoices ({0})
|
||||
admincreateinvoices.field.month=Billing month
|
||||
admincreateinvoices.dialog.title=Review invoices
|
||||
admincreateinvoices.dialog.position=Invoice {0} of {1}
|
||||
admincreateinvoices.dialog.button.previous=Back
|
||||
admincreateinvoices.dialog.button.next=Next
|
||||
admincreateinvoices.dialog.button.delete=Remove from list
|
||||
admincreateinvoices.dialog.button.send=Send invoices
|
||||
admincreateinvoices.dialog.notification.removed=Invoice removed from the list
|
||||
admincreateinvoices.mail.subject=Your invoice {0}
|
||||
admincreateinvoices.mail.body=Dear Sir or Madam,\n\nplease find attached your invoice {0} for the billing period {1}.\n\nKind regards\n{2}
|
||||
admincreateinvoices.notification.sent={0} invoice(s) sent by email
|
||||
admincreateinvoices.notification.sendfailed={0} invoice(s) could not be sent
|
||||
|
||||
@@ -982,8 +982,8 @@ misc.retry=Reintentar
|
||||
# Admin Price Table
|
||||
adminpricetable.title=Tabla de precios
|
||||
adminpricetable.field.monthly=Paquete b\u00e1sico mensual
|
||||
adminpricetable.field.applicense=Licencia de uso de la app
|
||||
adminpricetable.field.revenue=Participaci\u00f3n en los ingresos
|
||||
adminpricetable.field.appuserfee=Tarifa por usuario de la app
|
||||
adminpricetable.field.appuserfee.helper=Se factura mensualmente por cada usuario de la app configurado
|
||||
adminpricetable.notification.saved=La tabla de precios ha sido guardada
|
||||
adminpricetable.notification.save.error=Error al guardar: {0}
|
||||
adminpricetable.notification.load.error=Error al cargar: {0}
|
||||
@@ -1043,7 +1043,12 @@ page.title.admin.createinvoices=Crear facturas
|
||||
admincreateinvoices.title=Crear facturas
|
||||
admincreateinvoices.hint=Las facturas vencen a fin de mes. Si un cliente comienza a mitad de mes, solo se facturan los días restantes hasta fin de mes.
|
||||
admincreateinvoices.column.customer=Cliente
|
||||
admincreateinvoices.column.email=Correo electrónico
|
||||
admincreateinvoices.column.discount=Descuento
|
||||
admincreateinvoices.discount.position=Descuento ({0} %)
|
||||
admincreateinvoices.discount.dialog.title=Registrar descuento
|
||||
admincreateinvoices.discount.dialog.percent=Descuento en %
|
||||
admincreateinvoices.discount.dialog.reason=Motivo del descuento
|
||||
admincreateinvoices.discount.dialog.apply=Aplicar
|
||||
admincreateinvoices.column.start=Cliente desde
|
||||
admincreateinvoices.column.days=Días facturados
|
||||
admincreateinvoices.column.due=Vence el
|
||||
@@ -1051,8 +1056,20 @@ admincreateinvoices.column.net=Importe neto
|
||||
admincreateinvoices.days.full=Mes completo
|
||||
admincreateinvoices.days.partial={0} de {1} días
|
||||
admincreateinvoices.prorata.suffix=(prorrateado {0}/{1} días)
|
||||
admincreateinvoices.appusers.suffix=({0} usuarios de la app)
|
||||
admincreateinvoices.button.create=Crear factura
|
||||
admincreateinvoices.notification.notemplate=No hay plantilla de factura. Guarde primero una plantilla en el generador de facturas.
|
||||
admincreateinvoices.notification.error=Error al crear la factura: {0}
|
||||
admincreateinvoices.button.createselected=Crear facturas ({0})
|
||||
admincreateinvoices.field.month=Mes de facturación
|
||||
admincreateinvoices.dialog.title=Revisar facturas
|
||||
admincreateinvoices.dialog.position=Factura {0} de {1}
|
||||
admincreateinvoices.dialog.button.previous=Atrás
|
||||
admincreateinvoices.dialog.button.next=Siguiente
|
||||
admincreateinvoices.dialog.button.delete=Quitar de la lista
|
||||
admincreateinvoices.dialog.button.send=Enviar facturas
|
||||
admincreateinvoices.dialog.notification.removed=Factura eliminada de la lista
|
||||
admincreateinvoices.mail.subject=Su factura {0}
|
||||
admincreateinvoices.mail.body=Estimados señores,\n\nadjunto encontrará su factura {0} correspondiente al período de facturación {1}.\n\nAtentamente\n{2}
|
||||
admincreateinvoices.notification.sent={0} factura(s) enviada(s) por correo electrónico
|
||||
admincreateinvoices.notification.sendfailed=No se pudieron enviar {0} factura(s)
|
||||
|
||||
@@ -982,8 +982,8 @@ misc.retry=R\u00e9essayer
|
||||
# Admin Price Table
|
||||
adminpricetable.title=Tableau des prix
|
||||
adminpricetable.field.monthly=Forfait mensuel de base
|
||||
adminpricetable.field.applicense=Licence d'utilisation de l'app
|
||||
adminpricetable.field.revenue=Participation au chiffre d'affaires
|
||||
adminpricetable.field.appuserfee=Frais par utilisateur de l'app
|
||||
adminpricetable.field.appuserfee.helper=Facturé mensuellement par utilisateur de l'app configuré
|
||||
adminpricetable.notification.saved=Le tableau des prix a \u00e9t\u00e9 enregistr\u00e9
|
||||
adminpricetable.notification.save.error=Erreur lors de l'enregistrement : {0}
|
||||
adminpricetable.notification.load.error=Erreur lors du chargement : {0}
|
||||
@@ -1043,7 +1043,12 @@ page.title.admin.createinvoices=Créer des factures
|
||||
admincreateinvoices.title=Créer des factures
|
||||
admincreateinvoices.hint=Les factures sont exigibles en fin de mois. Si un client commence en cours de mois, seuls les jours restants jusqu'à la fin du mois sont facturés.
|
||||
admincreateinvoices.column.customer=Client
|
||||
admincreateinvoices.column.email=E-mail
|
||||
admincreateinvoices.column.discount=Remise
|
||||
admincreateinvoices.discount.position=Remise ({0} %)
|
||||
admincreateinvoices.discount.dialog.title=Saisir la remise
|
||||
admincreateinvoices.discount.dialog.percent=Remise en %
|
||||
admincreateinvoices.discount.dialog.reason=Motif de la remise
|
||||
admincreateinvoices.discount.dialog.apply=Appliquer
|
||||
admincreateinvoices.column.start=Client depuis
|
||||
admincreateinvoices.column.days=Jours facturés
|
||||
admincreateinvoices.column.due=Échéance le
|
||||
@@ -1051,8 +1056,20 @@ admincreateinvoices.column.net=Montant net
|
||||
admincreateinvoices.days.full=Mois complet
|
||||
admincreateinvoices.days.partial={0} sur {1} jours
|
||||
admincreateinvoices.prorata.suffix=(au prorata {0}/{1} jours)
|
||||
admincreateinvoices.appusers.suffix=({0} utilisateurs de l'app)
|
||||
admincreateinvoices.button.create=Créer la facture
|
||||
admincreateinvoices.notification.notemplate=Aucun modèle de facture disponible. Veuillez d'abord enregistrer un modèle dans le générateur de factures.
|
||||
admincreateinvoices.notification.error=Erreur lors de la création de la facture : {0}
|
||||
admincreateinvoices.button.createselected=Créer les factures ({0})
|
||||
admincreateinvoices.field.month=Mois de facturation
|
||||
admincreateinvoices.dialog.title=Vérifier les factures
|
||||
admincreateinvoices.dialog.position=Facture {0} sur {1}
|
||||
admincreateinvoices.dialog.button.previous=Retour
|
||||
admincreateinvoices.dialog.button.next=Suivant
|
||||
admincreateinvoices.dialog.button.delete=Retirer de la liste
|
||||
admincreateinvoices.dialog.button.send=Envoyer les factures
|
||||
admincreateinvoices.dialog.notification.removed=Facture retirée de la liste
|
||||
admincreateinvoices.mail.subject=Votre facture {0}
|
||||
admincreateinvoices.mail.body=Madame, Monsieur,\n\nveuillez trouver ci-joint votre facture {0} pour la période de facturation {1}.\n\nCordialement\n{2}
|
||||
admincreateinvoices.notification.sent={0} facture(s) envoyée(s) par e-mail
|
||||
admincreateinvoices.notification.sendfailed={0} facture(s) n''ont pas pu être envoyées
|
||||
|
||||
@@ -984,8 +984,8 @@ misc.retry=Bandyti dar kartą
|
||||
# Admin Price Table
|
||||
adminpricetable.title=Kainų lentelė
|
||||
adminpricetable.field.monthly=Mėnesinis bazinis paketas
|
||||
adminpricetable.field.applicense=Programėlės naudojimo licencija
|
||||
adminpricetable.field.revenue=Pajamų dalis
|
||||
adminpricetable.field.appuserfee=Mokestis už programėlės naudotoją
|
||||
adminpricetable.field.appuserfee.helper=Skaičiuojamas kas mėnesį už kiekvieną sukurtą programėlės naudotoją
|
||||
adminpricetable.notification.saved=Kainų lentelė išsaugota
|
||||
adminpricetable.notification.save.error=Klaida išsaugant: {0}
|
||||
adminpricetable.notification.load.error=Klaida įkeliant: {0}
|
||||
@@ -1045,7 +1045,12 @@ page.title.admin.createinvoices=Kurti sąskaitas
|
||||
admincreateinvoices.title=Kurti sąskaitas
|
||||
admincreateinvoices.hint=Sąskaitos apmokamos mėnesio pabaigoje. Jei klientas pradeda mėnesio viduryje, apmokestinamos tik likusios dienos iki mėnesio pabaigos.
|
||||
admincreateinvoices.column.customer=Klientas
|
||||
admincreateinvoices.column.email=El. paštas
|
||||
admincreateinvoices.column.discount=Nuolaida
|
||||
admincreateinvoices.discount.position=Nuolaida ({0} %)
|
||||
admincreateinvoices.discount.dialog.title=Įvesti nuolaidą
|
||||
admincreateinvoices.discount.dialog.percent=Nuolaida %
|
||||
admincreateinvoices.discount.dialog.reason=Nuolaidos priežastis
|
||||
admincreateinvoices.discount.dialog.apply=Taikyti
|
||||
admincreateinvoices.column.start=Klientas nuo
|
||||
admincreateinvoices.column.days=Apmokestintos dienos
|
||||
admincreateinvoices.column.due=Terminas
|
||||
@@ -1053,8 +1058,20 @@ admincreateinvoices.column.net=Neto suma
|
||||
admincreateinvoices.days.full=Visas mėnuo
|
||||
admincreateinvoices.days.partial={0} iš {1} dienų
|
||||
admincreateinvoices.prorata.suffix=(proporcingai {0}/{1} dienų)
|
||||
admincreateinvoices.appusers.suffix=({0} programėlės naudotojų)
|
||||
admincreateinvoices.button.create=Sukurti sąskaitą
|
||||
admincreateinvoices.notification.notemplate=Nėra sąskaitos šablono. Pirmiausia išsaugokite šabloną sąskaitų generatoriuje.
|
||||
admincreateinvoices.notification.error=Klaida kuriant sąskaitą: {0}
|
||||
admincreateinvoices.button.createselected=Sukurti sąskaitas ({0})
|
||||
admincreateinvoices.field.month=Atsiskaitymo mėnuo
|
||||
admincreateinvoices.dialog.title=Peržiūrėti sąskaitas
|
||||
admincreateinvoices.dialog.position=Sąskaita {0} iš {1}
|
||||
admincreateinvoices.dialog.button.previous=Atgal
|
||||
admincreateinvoices.dialog.button.next=Toliau
|
||||
admincreateinvoices.dialog.button.delete=Pašalinti iš sąrašo
|
||||
admincreateinvoices.dialog.button.send=Siųsti sąskaitas
|
||||
admincreateinvoices.dialog.notification.removed=Sąskaita pašalinta iš sąrašo
|
||||
admincreateinvoices.mail.subject=Jūsų sąskaita {0}
|
||||
admincreateinvoices.mail.body=Gerbiami klientai,\n\npridedame Jūsų sąskaitą {0} už atsiskaitymo laikotarpį {1}.\n\nPagarbiai\n{2}
|
||||
admincreateinvoices.notification.sent=El. paštu išsiųsta sąskaitų: {0}
|
||||
admincreateinvoices.notification.sendfailed=Nepavyko išsiųsti sąskaitų: {0}
|
||||
|
||||
@@ -982,8 +982,8 @@ misc.retry=Mēģināt vēlreiz
|
||||
# Admin Price Table
|
||||
adminpricetable.title=Cenu tabula
|
||||
adminpricetable.field.monthly=Ikmēneša pamatpakete
|
||||
adminpricetable.field.applicense=Lietotnes licence
|
||||
adminpricetable.field.revenue=Ieņēmumu daļa
|
||||
adminpricetable.field.appuserfee=Maksa par lietotnes lietotāju
|
||||
adminpricetable.field.appuserfee.helper=Tiek aprēķināta katru mēnesi par katru izveidoto lietotnes lietotāju
|
||||
adminpricetable.notification.saved=Cenu tabula saglabāta
|
||||
adminpricetable.notification.save.error=Kļūda saglabājot: {0}
|
||||
adminpricetable.notification.load.error=Kļūda ielādējot: {0}
|
||||
@@ -1043,7 +1043,12 @@ page.title.admin.createinvoices=Izveidot rēķinus
|
||||
admincreateinvoices.title=Izveidot rēķinus
|
||||
admincreateinvoices.hint=Rēķini jāapmaksā mēneša beigās. Ja klients sāk mēneša vidū, tiek aprēķinātas tikai atlikušās dienas līdz mēneša beigām.
|
||||
admincreateinvoices.column.customer=Klients
|
||||
admincreateinvoices.column.email=E-pasts
|
||||
admincreateinvoices.column.discount=Atlaide
|
||||
admincreateinvoices.discount.position=Atlaide ({0} %)
|
||||
admincreateinvoices.discount.dialog.title=Ievadīt atlaidi
|
||||
admincreateinvoices.discount.dialog.percent=Atlaide %
|
||||
admincreateinvoices.discount.dialog.reason=Atlaides iemesls
|
||||
admincreateinvoices.discount.dialog.apply=Piemērot
|
||||
admincreateinvoices.column.start=Klients kopš
|
||||
admincreateinvoices.column.days=Aprēķinātās dienas
|
||||
admincreateinvoices.column.due=Termiņš
|
||||
@@ -1051,8 +1056,20 @@ admincreateinvoices.column.net=Neto summa
|
||||
admincreateinvoices.days.full=Pilns mēnesis
|
||||
admincreateinvoices.days.partial={0} no {1} dienām
|
||||
admincreateinvoices.prorata.suffix=(proporcionāli {0}/{1} dienas)
|
||||
admincreateinvoices.appusers.suffix=({0} lietotnes lietotāji)
|
||||
admincreateinvoices.button.create=Izveidot rēķinu
|
||||
admincreateinvoices.notification.notemplate=Nav rēķina veidnes. Lūdzu, vispirms saglabājiet veidni rēķinu ģeneratorā.
|
||||
admincreateinvoices.notification.error=Kļūda, veidojot rēķinu: {0}
|
||||
admincreateinvoices.button.createselected=Izveidot rēķinus ({0})
|
||||
admincreateinvoices.field.month=Norēķinu mēnesis
|
||||
admincreateinvoices.dialog.title=Pārskatīt rēķinus
|
||||
admincreateinvoices.dialog.position=Rēķins {0} no {1}
|
||||
admincreateinvoices.dialog.button.previous=Atpakaļ
|
||||
admincreateinvoices.dialog.button.next=Tālāk
|
||||
admincreateinvoices.dialog.button.delete=Noņemt no saraksta
|
||||
admincreateinvoices.dialog.button.send=Sūtīt rēķinus
|
||||
admincreateinvoices.dialog.notification.removed=Rēķins noņemts no saraksta
|
||||
admincreateinvoices.mail.subject=Jūsu rēķins {0}
|
||||
admincreateinvoices.mail.body=Cienījamie klienti,\n\npielikumā ir Jūsu rēķins {0} par norēķinu periodu {1}.\n\nAr cieņu\n{2}
|
||||
admincreateinvoices.notification.sent=Pa e-pastu nosūtīti rēķini: {0}
|
||||
admincreateinvoices.notification.sendfailed=Neizdevās nosūtīt rēķinus: {0}
|
||||
|
||||
@@ -982,8 +982,8 @@ misc.retry=Spr\u00f3buj ponownie
|
||||
# Admin Price Table
|
||||
adminpricetable.title=Cennik
|
||||
adminpricetable.field.monthly=Miesi\u0119czny pakiet podstawowy
|
||||
adminpricetable.field.applicense=Licencja aplikacji
|
||||
adminpricetable.field.revenue=Udzia\u0142 w przychodach
|
||||
adminpricetable.field.appuserfee=Opłata za użytkownika aplikacji
|
||||
adminpricetable.field.appuserfee.helper=Naliczana miesięcznie za każdego skonfigurowanego użytkownika aplikacji
|
||||
adminpricetable.notification.saved=Cennik zosta\u0142 zapisany
|
||||
adminpricetable.notification.save.error=B\u0142\u0105d podczas zapisywania: {0}
|
||||
adminpricetable.notification.load.error=B\u0142\u0105d podczas \u0142adowania: {0}
|
||||
@@ -1043,7 +1043,12 @@ page.title.admin.createinvoices=Utwórz faktury
|
||||
admincreateinvoices.title=Utwórz faktury
|
||||
admincreateinvoices.hint=Faktury są płatne na koniec miesiąca. Jeśli klient rozpoczyna w trakcie miesiąca, rozliczane są tylko dni pozostałe do końca miesiąca.
|
||||
admincreateinvoices.column.customer=Klient
|
||||
admincreateinvoices.column.email=E-mail
|
||||
admincreateinvoices.column.discount=Rabat
|
||||
admincreateinvoices.discount.position=Rabat ({0} %)
|
||||
admincreateinvoices.discount.dialog.title=Wprowadź rabat
|
||||
admincreateinvoices.discount.dialog.percent=Rabat w %
|
||||
admincreateinvoices.discount.dialog.reason=Powód rabatu
|
||||
admincreateinvoices.discount.dialog.apply=Zastosuj
|
||||
admincreateinvoices.column.start=Klient od
|
||||
admincreateinvoices.column.days=Rozliczone dni
|
||||
admincreateinvoices.column.due=Termin płatności
|
||||
@@ -1051,8 +1056,20 @@ admincreateinvoices.column.net=Kwota netto
|
||||
admincreateinvoices.days.full=Pełny miesiąc
|
||||
admincreateinvoices.days.partial={0} z {1} dni
|
||||
admincreateinvoices.prorata.suffix=(proporcjonalnie {0}/{1} dni)
|
||||
admincreateinvoices.appusers.suffix=({0} użytkowników aplikacji)
|
||||
admincreateinvoices.button.create=Utwórz fakturę
|
||||
admincreateinvoices.notification.notemplate=Brak szablonu faktury. Najpierw zapisz szablon w generatorze faktur.
|
||||
admincreateinvoices.notification.error=Błąd podczas tworzenia faktury: {0}
|
||||
admincreateinvoices.button.createselected=Utwórz faktury ({0})
|
||||
admincreateinvoices.field.month=Miesiąc rozliczeniowy
|
||||
admincreateinvoices.dialog.title=Przegląd faktur
|
||||
admincreateinvoices.dialog.position=Faktura {0} z {1}
|
||||
admincreateinvoices.dialog.button.previous=Wstecz
|
||||
admincreateinvoices.dialog.button.next=Dalej
|
||||
admincreateinvoices.dialog.button.delete=Usuń z listy
|
||||
admincreateinvoices.dialog.button.send=Wyślij faktury
|
||||
admincreateinvoices.dialog.notification.removed=Faktura usunięta z listy
|
||||
admincreateinvoices.mail.subject=Państwa faktura {0}
|
||||
admincreateinvoices.mail.body=Szanowni Państwo,\n\nw załączeniu przesyłamy fakturę {0} za okres rozliczeniowy {1}.\n\nZ poważaniem\n{2}
|
||||
admincreateinvoices.notification.sent=Wysłano e-mailem faktur: {0}
|
||||
admincreateinvoices.notification.sendfailed=Nie udało się wysłać faktur: {0}
|
||||
|
||||
@@ -982,8 +982,8 @@ misc.retry=Повторить попытку
|
||||
# Admin Price Table
|
||||
adminpricetable.title=Таблица цен
|
||||
adminpricetable.field.monthly=Ежемесячный базовый пакет
|
||||
adminpricetable.field.applicense=Лицензия на приложение
|
||||
adminpricetable.field.revenue=Участие в выручке
|
||||
adminpricetable.field.appuserfee=Плата за пользователя приложения
|
||||
adminpricetable.field.appuserfee.helper=Начисляется ежемесячно за каждого настроенного пользователя приложения
|
||||
adminpricetable.notification.saved=Таблица цен сохранена
|
||||
adminpricetable.notification.save.error=Ошибка при сохранении: {0}
|
||||
adminpricetable.notification.load.error=Ошибка при загрузке: {0}
|
||||
@@ -1043,7 +1043,12 @@ page.title.admin.createinvoices=Создать счета
|
||||
admincreateinvoices.title=Создать счета
|
||||
admincreateinvoices.hint=Счета подлежат оплате в конце месяца. Если клиент начинает в середине месяца, оплачиваются только оставшиеся дни до конца месяца.
|
||||
admincreateinvoices.column.customer=Клиент
|
||||
admincreateinvoices.column.email=Эл. почта
|
||||
admincreateinvoices.column.discount=Скидка
|
||||
admincreateinvoices.discount.position=Скидка ({0} %)
|
||||
admincreateinvoices.discount.dialog.title=Указать скидку
|
||||
admincreateinvoices.discount.dialog.percent=Скидка в %
|
||||
admincreateinvoices.discount.dialog.reason=Причина скидки
|
||||
admincreateinvoices.discount.dialog.apply=Применить
|
||||
admincreateinvoices.column.start=Клиент с
|
||||
admincreateinvoices.column.days=Расчетные дни
|
||||
admincreateinvoices.column.due=Срок оплаты
|
||||
@@ -1051,8 +1056,20 @@ admincreateinvoices.column.net=Сумма нетто
|
||||
admincreateinvoices.days.full=Полный месяц
|
||||
admincreateinvoices.days.partial={0} из {1} дней
|
||||
admincreateinvoices.prorata.suffix=(пропорционально {0}/{1} дней)
|
||||
admincreateinvoices.appusers.suffix=({0} пользователей приложения)
|
||||
admincreateinvoices.button.create=Создать счет
|
||||
admincreateinvoices.notification.notemplate=Шаблон счета отсутствует. Сначала сохраните шаблон в генераторе счетов.
|
||||
admincreateinvoices.notification.error=Ошибка при создании счета: {0}
|
||||
admincreateinvoices.button.createselected=Создать счета ({0})
|
||||
admincreateinvoices.field.month=Расчетный месяц
|
||||
admincreateinvoices.dialog.title=Проверка счетов
|
||||
admincreateinvoices.dialog.position=Счет {0} из {1}
|
||||
admincreateinvoices.dialog.button.previous=Назад
|
||||
admincreateinvoices.dialog.button.next=Далее
|
||||
admincreateinvoices.dialog.button.delete=Удалить из списка
|
||||
admincreateinvoices.dialog.button.send=Отправить счета
|
||||
admincreateinvoices.dialog.notification.removed=Счет удален из списка
|
||||
admincreateinvoices.mail.subject=Ваш счет {0}
|
||||
admincreateinvoices.mail.body=Уважаемые дамы и господа!\n\nВо вложении Вы найдете счет {0} за расчетный период {1}.\n\nС уважением\n{2}
|
||||
admincreateinvoices.notification.sent=Отправлено счетов по эл. почте: {0}
|
||||
admincreateinvoices.notification.sendfailed=Не удалось отправить счетов: {0}
|
||||
|
||||
@@ -982,8 +982,8 @@ misc.retry=Tekrar Dene
|
||||
# Admin Price Table
|
||||
adminpricetable.title=Fiyat Tablosu
|
||||
adminpricetable.field.monthly=Ayl\u0131k Temel Paket
|
||||
adminpricetable.field.applicense=Uygulama Kullan\u0131m Lisans\u0131
|
||||
adminpricetable.field.revenue=Gelir Pay\u0131
|
||||
adminpricetable.field.appuserfee=Uygulama kullanıcısı başına ücret
|
||||
adminpricetable.field.appuserfee.helper=Kurulan her uygulama kullanıcısı için aylık olarak faturalandırılır
|
||||
adminpricetable.notification.saved=Fiyat tablosu kaydedildi
|
||||
adminpricetable.notification.save.error=Kaydetme hatas\u0131: {0}
|
||||
adminpricetable.notification.load.error=Y\u00fckleme hatas\u0131: {0}
|
||||
@@ -1043,7 +1043,12 @@ page.title.admin.createinvoices=Fatura oluştur
|
||||
admincreateinvoices.title=Fatura oluştur
|
||||
admincreateinvoices.hint=Faturalar ay sonunda ödenir. Bir müşteri ay ortasında başlarsa, yalnızca ay sonuna kadar kalan günler faturalandırılır.
|
||||
admincreateinvoices.column.customer=Müşteri
|
||||
admincreateinvoices.column.email=E-posta
|
||||
admincreateinvoices.column.discount=İndirim
|
||||
admincreateinvoices.discount.position=İndirim (%{0})
|
||||
admincreateinvoices.discount.dialog.title=İndirim gir
|
||||
admincreateinvoices.discount.dialog.percent=İndirim %
|
||||
admincreateinvoices.discount.dialog.reason=İndirim nedeni
|
||||
admincreateinvoices.discount.dialog.apply=Uygula
|
||||
admincreateinvoices.column.start=Müşteri başlangıcı
|
||||
admincreateinvoices.column.days=Faturalanan günler
|
||||
admincreateinvoices.column.due=Vade tarihi
|
||||
@@ -1051,8 +1056,20 @@ admincreateinvoices.column.net=Net tutar
|
||||
admincreateinvoices.days.full=Tam ay
|
||||
admincreateinvoices.days.partial={0} / {1} gün
|
||||
admincreateinvoices.prorata.suffix=(orantılı {0}/{1} gün)
|
||||
admincreateinvoices.appusers.suffix=({0} uygulama kullanıcısı)
|
||||
admincreateinvoices.button.create=Fatura oluştur
|
||||
admincreateinvoices.notification.notemplate=Fatura şablonu yok. Lütfen önce fatura oluşturucuda bir şablon kaydedin.
|
||||
admincreateinvoices.notification.error=Fatura oluşturulurken hata: {0}
|
||||
admincreateinvoices.button.createselected=Fatura oluştur ({0})
|
||||
admincreateinvoices.field.month=Fatura ayı
|
||||
admincreateinvoices.dialog.title=Faturaları incele
|
||||
admincreateinvoices.dialog.position=Fatura {0} / {1}
|
||||
admincreateinvoices.dialog.button.previous=Geri
|
||||
admincreateinvoices.dialog.button.next=İleri
|
||||
admincreateinvoices.dialog.button.delete=Listeden kaldır
|
||||
admincreateinvoices.dialog.button.send=Faturaları gönder
|
||||
admincreateinvoices.dialog.notification.removed=Fatura listeden kaldırıldı
|
||||
admincreateinvoices.mail.subject=Faturanız {0}
|
||||
admincreateinvoices.mail.body=Sayın yetkili,\n\n{1} fatura dönemine ait {0} numaralı faturanız ektedir.\n\nSaygılarımızla\n{2}
|
||||
admincreateinvoices.notification.sent={0} fatura e-posta ile gönderildi
|
||||
admincreateinvoices.notification.sendfailed={0} fatura gönderilemedi
|
||||
|
||||
Reference in New Issue
Block a user