Compare commits
2 Commits
4e411c727e
...
d146d2f319
| Author | SHA1 | Date | |
|---|---|---|---|
| d146d2f319 | |||
| 3d408e43c9 |
@@ -66,6 +66,7 @@ public class CustomerInvoice {
|
|||||||
private String recipientCity;
|
private String recipientCity;
|
||||||
private String recipientCountry;
|
private String recipientCountry;
|
||||||
private String recipientVatId; // USt-IdNr. des Empfängers (falls vorhanden)
|
private String recipientVatId; // USt-IdNr. des Empfängers (falls vorhanden)
|
||||||
|
private String recipientEmail; // E-Mail für den Rechnungsversand
|
||||||
|
|
||||||
// Rechnungsdetails
|
// Rechnungsdetails
|
||||||
private String description; // Beschreibung der Leistung
|
private String description; // Beschreibung der Leistung
|
||||||
@@ -274,6 +275,14 @@ public class CustomerInvoice {
|
|||||||
this.recipientVatId = recipientVatId;
|
this.recipientVatId = recipientVatId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRecipientEmail() {
|
||||||
|
return recipientEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecipientEmail(String recipientEmail) {
|
||||||
|
this.recipientEmail = recipientEmail;
|
||||||
|
}
|
||||||
|
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -552,10 +552,42 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends each invoice as PDF attachment to the respective customer's email
|
* Sends the invoices as signed ZUGFeRD e-invoice (ZIP) to the respective
|
||||||
* address and reports how many mails were (not) sent.
|
* customers. First all signed ZIPs are created and cached; only when every
|
||||||
|
* ZIP could be created and passed the validation the mails are sent.
|
||||||
|
* Afterwards a combined ZIP containing all individual invoice ZIPs is
|
||||||
|
* offered as browser download.
|
||||||
*/
|
*/
|
||||||
private void sendInvoices(List<InvoicePreview> previews) {
|
private void sendInvoices(List<InvoicePreview> previews) {
|
||||||
|
// Phase 1: create and cache the signed ZIP of every invoice; abort
|
||||||
|
// without sending anything if a single one fails
|
||||||
|
Map<InvoicePreview, PdfToolService.SignedInvoice> signedZips = new LinkedHashMap<>();
|
||||||
|
for (InvoicePreview preview : previews) {
|
||||||
|
try {
|
||||||
|
signedZips.put(preview, pdfToolService.signInvoice(preview.pdf(), buildZugferdMetadata(preview)));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Error creating ZUGFeRD e-invoice {} for batch send: {}", preview.invoiceNumber(),
|
||||||
|
ex.getMessage(), ex);
|
||||||
|
Notification
|
||||||
|
.show(getTranslation("admincreateinvoices.notification.zugferd.batcherror",
|
||||||
|
preview.invoiceNumber(), ex.getMessage()), 7000, Notification.Position.BOTTOM_CENTER)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Invoices failing the Mustang validation stop the whole batch: no
|
||||||
|
// mail is sent as long as a single invoice is invalid
|
||||||
|
String invalidNumbers = signedZips.entrySet().stream().filter(entry -> !entry.getValue().valid())
|
||||||
|
.map(entry -> entry.getKey().invoiceNumber()).collect(java.util.stream.Collectors.joining(", "));
|
||||||
|
if (!invalidNumbers.isEmpty()) {
|
||||||
|
log.warn("Invoice batch send aborted, validation failed for: {}", invalidNumbers);
|
||||||
|
Notification
|
||||||
|
.show(getTranslation("admincreateinvoices.notification.zugferd.invalidsend", invalidNumbers),
|
||||||
|
7000, Notification.Position.BOTTOM_CENTER)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String issuerName = "";
|
String issuerName = "";
|
||||||
try {
|
try {
|
||||||
issuerName = safe(systemCompanyProfileService.createSystemInvoiceData().getCompanyName());
|
issuerName = safe(systemCompanyProfileService.createSystemInvoiceData().getCompanyName());
|
||||||
@@ -564,9 +596,12 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
}
|
}
|
||||||
String monthLabel = billingMonth.format(DateTimeFormatter.ofPattern("LLLL yyyy", getLocale()));
|
String monthLabel = billingMonth.format(DateTimeFormatter.ofPattern("LLLL yyyy", getLocale()));
|
||||||
|
|
||||||
|
// Phase 2: all ZIPs exist, send one mail per invoice with its ZIP
|
||||||
int sent = 0;
|
int sent = 0;
|
||||||
int failed = 0;
|
int failed = 0;
|
||||||
for (InvoicePreview preview : previews) {
|
for (Map.Entry<InvoicePreview, PdfToolService.SignedInvoice> entry : signedZips.entrySet()) {
|
||||||
|
InvoicePreview preview = entry.getKey();
|
||||||
|
PdfToolService.SignedInvoice signed = entry.getValue();
|
||||||
String email = preview.info().user().getEmail();
|
String email = preview.info().user().getEmail();
|
||||||
if (email == null || email.isBlank()) {
|
if (email == null || email.isBlank()) {
|
||||||
log.warn("Customer {} has no email address, invoice {} not sent",
|
log.warn("Customer {} has no email address, invoice {} not sent",
|
||||||
@@ -579,7 +614,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
getTranslation("admincreateinvoices.mail.subject", preview.invoiceNumber()),
|
getTranslation("admincreateinvoices.mail.subject", preview.invoiceNumber()),
|
||||||
getTranslation("admincreateinvoices.mail.body", preview.invoiceNumber(), monthLabel,
|
getTranslation("admincreateinvoices.mail.body", preview.invoiceNumber(), monthLabel,
|
||||||
issuerName),
|
issuerName),
|
||||||
preview.pdf(), preview.invoiceNumber() + ".pdf");
|
signed.zip(), signed.fileName());
|
||||||
recordDispatch(preview, email);
|
recordDispatch(preview, email);
|
||||||
sent++;
|
sent++;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -587,6 +622,22 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
failed++;
|
failed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 3: bundle all individual invoice ZIPs and offer the download
|
||||||
|
if (signedZips.size() > 1) {
|
||||||
|
try {
|
||||||
|
String archiveName = "rechnungen-" + billingMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))
|
||||||
|
+ "-zugferd.zip";
|
||||||
|
triggerZipDownload(buildCombinedZip(signedZips.values()), archiveName);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Error creating combined invoice ZIP: {}", ex.getMessage(), ex);
|
||||||
|
Notification
|
||||||
|
.show(getTranslation("admincreateinvoices.notification.zip.combined.error", ex.getMessage()),
|
||||||
|
7000, Notification.Position.BOTTOM_CENTER)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sent > 0) {
|
if (sent > 0) {
|
||||||
grid.deselectAll();
|
grid.deselectAll();
|
||||||
grid.setItems(loadBillingInfos());
|
grid.setItems(loadBillingInfos());
|
||||||
@@ -748,11 +799,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
try {
|
try {
|
||||||
PdfToolService.SignedInvoice signed = pdfToolService.signInvoice(preview.pdf(),
|
PdfToolService.SignedInvoice signed = pdfToolService.signInvoice(preview.pdf(),
|
||||||
buildZugferdMetadata(preview));
|
buildZugferdMetadata(preview));
|
||||||
String base64Zip = Base64.getEncoder().encodeToString(signed.zip());
|
triggerZipDownload(signed.zip(), signed.fileName());
|
||||||
getElement().executeJs("const link = document.createElement('a');"
|
|
||||||
+ "link.href = 'data:application/zip;base64," + base64Zip + "';" + "link.download = '"
|
|
||||||
+ signed.fileName().replace("'", "") + "';"
|
|
||||||
+ "document.body.appendChild(link); link.click(); document.body.removeChild(link);");
|
|
||||||
if (signed.valid()) {
|
if (signed.valid()) {
|
||||||
Notification
|
Notification
|
||||||
.show(getTranslation("admincreateinvoices.notification.zugferd.created",
|
.show(getTranslation("admincreateinvoices.notification.zugferd.created",
|
||||||
@@ -773,6 +820,32 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Packs the individual invoice ZIPs into one combined ZIP so all sent
|
||||||
|
* e-invoices can be archived with a single download.
|
||||||
|
*/
|
||||||
|
private byte[] buildCombinedZip(java.util.Collection<PdfToolService.SignedInvoice> signedInvoices)
|
||||||
|
throws java.io.IOException {
|
||||||
|
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
|
||||||
|
try (java.util.zip.ZipOutputStream zipStream = new java.util.zip.ZipOutputStream(buffer)) {
|
||||||
|
for (PdfToolService.SignedInvoice signed : signedInvoices) {
|
||||||
|
zipStream.putNextEntry(new java.util.zip.ZipEntry(signed.fileName()));
|
||||||
|
zipStream.write(signed.zip());
|
||||||
|
zipStream.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Offers the given ZIP bytes as browser download. */
|
||||||
|
private void triggerZipDownload(byte[] zip, String fileName) {
|
||||||
|
String base64Zip = Base64.getEncoder().encodeToString(zip);
|
||||||
|
getElement().executeJs("const link = document.createElement('a');"
|
||||||
|
+ "link.href = 'data:application/zip;base64," + base64Zip + "';" + "link.download = '"
|
||||||
|
+ fileName.replace("'", "") + "';"
|
||||||
|
+ "document.body.appendChild(link); link.click(); document.body.removeChild(link);");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the EN16931 metadata for the PDF Tool from the billing data: the
|
* Builds the EN16931 metadata for the PDF Tool from the billing data: the
|
||||||
* system operator as sender, the customer as recipient and the billed
|
* system operator as sender, the customer as recipient and the billed
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import de.assecutor.votianlt.model.Service;
|
|||||||
import de.assecutor.votianlt.model.User;
|
import de.assecutor.votianlt.model.User;
|
||||||
import de.assecutor.votianlt.model.InvoiceTemplate;
|
import de.assecutor.votianlt.model.InvoiceTemplate;
|
||||||
import de.assecutor.votianlt.model.invoices.CustomerInvoice;
|
import de.assecutor.votianlt.model.invoices.CustomerInvoice;
|
||||||
|
import de.assecutor.votianlt.model.invoices.CustomerInvoiceItem;
|
||||||
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
|
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
|
||||||
import de.assecutor.votianlt.pages.service.CustomerService;
|
import de.assecutor.votianlt.pages.service.CustomerService;
|
||||||
import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
|
import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
|
||||||
@@ -586,6 +587,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
invoice.setVatAmount(vatAmount);
|
invoice.setVatAmount(vatAmount);
|
||||||
invoice.setTotalAmount(totalAmount);
|
invoice.setTotalAmount(totalAmount);
|
||||||
|
|
||||||
|
// Pflichtangaben nach §14 UStG: Leistungsdatum, Absender, Empfänger und
|
||||||
|
// Positionen — auch Grundlage der ZUGFeRD-Metadaten für die E-Rechnung.
|
||||||
|
invoice.setDeliveryDate(currentJob.getDeliveryDate() != null ? currentJob.getDeliveryDate()
|
||||||
|
: java.time.LocalDate.now());
|
||||||
|
applySenderData(invoice, user);
|
||||||
|
applyRecipientData(invoice);
|
||||||
|
invoice.setItems(buildInvoiceItems(vatRate));
|
||||||
|
|
||||||
invoice.setPdfData(pdfBytes);
|
invoice.setPdfData(pdfBytes);
|
||||||
|
|
||||||
// Finalisierung mit Audit-Eintrag und Eindeutigkeitsprüfung der Rechnungsnummer (R-07/R-11/R-36).
|
// Finalisierung mit Audit-Eintrag und Eindeutigkeitsprüfung der Rechnungsnummer (R-07/R-11/R-36).
|
||||||
@@ -609,6 +618,105 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absenderdaten aus dem Nutzerprofil und den Rechnungs-Stammdaten
|
||||||
|
* (USt-IdNr./Steuernummer, Bankverbindung, Zahlungsbedingungen) übernehmen.
|
||||||
|
*/
|
||||||
|
private void applySenderData(CustomerInvoice invoice, User user) {
|
||||||
|
invoice.setSenderName(firstNonBlank(user.getCompany(),
|
||||||
|
(safe(user.getFirstname()) + " " + safe(user.getName())).trim()));
|
||||||
|
invoice.setSenderAddress((safe(user.getStreet()) + " " + safe(user.getHouseNumber())).trim());
|
||||||
|
invoice.setSenderPostcode(safe(user.getZip()));
|
||||||
|
invoice.setSenderCity(safe(user.getCity()));
|
||||||
|
invoice.setSenderCountry("DE");
|
||||||
|
invoice.setSenderPhone(safe(user.getPhone()));
|
||||||
|
invoice.setSenderEmail(safe(user.getEmail()));
|
||||||
|
|
||||||
|
userInvoiceDataService.findByUserId(user.getId()).ifPresent(invoiceData -> {
|
||||||
|
invoice.setSenderVatId(safe(invoiceData.getUstId()));
|
||||||
|
invoice.setSenderTaxNumber(safe(invoiceData.getTaxNumber()));
|
||||||
|
invoice.setIban(safe(invoiceData.getIban()));
|
||||||
|
invoice.setBankAccount(safe(invoiceData.getBankName()));
|
||||||
|
invoice.setPaymentTerms(safe(invoiceData.getPaymentTerms()));
|
||||||
|
});
|
||||||
|
if (invoice.getPaymentDueDate() == null) {
|
||||||
|
invoice.setPaymentDueDate(invoice.getInvoiceDate().plusDays(14));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empfängerdaten aus der Kundenauswahl des Auftrags übernehmen; die volle
|
||||||
|
* Anschrift und E-Mail kommen aus dem passenden Adressbuch-Eintrag.
|
||||||
|
*/
|
||||||
|
private void applyRecipientData(CustomerInvoice invoice) {
|
||||||
|
String customerSelection = currentJob.getCustomerSelection();
|
||||||
|
if (customerSelection == null || customerSelection.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Format: "Firmenname | Vorname Nachname"
|
||||||
|
String[] parts = customerSelection.split("\\|");
|
||||||
|
String companyName = parts.length > 0 ? parts[0].trim() : "";
|
||||||
|
String contactName = parts.length > 1 ? parts[1].trim() : "";
|
||||||
|
|
||||||
|
invoice.setRecipientCompany(companyName);
|
||||||
|
invoice.setRecipientName(firstNonBlank(contactName, companyName));
|
||||||
|
|
||||||
|
Customer matchedCustomer = customerService.findAllForCurrentOwner().stream()
|
||||||
|
.filter(c -> companyName.equalsIgnoreCase(c.getCompanyName())).findFirst().orElse(null);
|
||||||
|
if (matchedCustomer != null) {
|
||||||
|
invoice.setRecipientAddress(
|
||||||
|
(safe(matchedCustomer.getStreet()) + " " + safe(matchedCustomer.getHouseNumber())).trim());
|
||||||
|
invoice.setRecipientPostcode(safe(matchedCustomer.getZip()));
|
||||||
|
invoice.setRecipientCity(safe(matchedCustomer.getCity()));
|
||||||
|
invoice.setRecipientCountry("DE");
|
||||||
|
invoice.setRecipientEmail(safe(matchedCustomer.getMail()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rechnungspositionen aus den ausgewählten Leistungen aufbauen — mit
|
||||||
|
* denselben Mengen-/Preisregeln wie {@link #calculateNetAmount()}, damit
|
||||||
|
* die Positionssumme exakt zum Rechnungs-Netto passt.
|
||||||
|
*/
|
||||||
|
private List<CustomerInvoiceItem> buildInvoiceItems(BigDecimal vatRate) {
|
||||||
|
List<CustomerInvoiceItem> items = new ArrayList<>();
|
||||||
|
for (ServiceRow row : getSelectedServices()) {
|
||||||
|
Service service = row.getService();
|
||||||
|
BigDecimal quantity = null;
|
||||||
|
BigDecimal unitPrice = null;
|
||||||
|
String unit = null;
|
||||||
|
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
|
||||||
|
quantity = BigDecimal.ONE;
|
||||||
|
unitPrice = service.getPrice();
|
||||||
|
unit = "pauschal";
|
||||||
|
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
|
||||||
|
&& service.getPricePerKilometer() != null && getRouteDistanceKm(row.getSelection()) != null) {
|
||||||
|
quantity = BigDecimal.valueOf(getRouteDistanceKm(row.getSelection()));
|
||||||
|
unitPrice = service.getPricePerKilometer();
|
||||||
|
unit = "km";
|
||||||
|
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
|
||||||
|
&& service.getPricePer15Minutes() != null && getTimeIn15MinUnits(row.getSelection()) != null) {
|
||||||
|
quantity = new BigDecimal(getTimeIn15MinUnits(row.getSelection()));
|
||||||
|
unitPrice = service.getPricePer15Minutes();
|
||||||
|
unit = "x 15 Min.";
|
||||||
|
}
|
||||||
|
if (quantity == null || quantity.signum() <= 0 || unitPrice == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
items.add(new CustomerInvoiceItem(quantity, unit, service.getName(), unitPrice, vatRate));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstNonBlank(String... values) {
|
||||||
|
for (String value : values) {
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates an invoice PDF from the template with actual job data.
|
* Generates an invoice PDF from the template with actual job data.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -372,6 +372,24 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
billingHeaderLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
|
billingHeaderLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
|
||||||
billingTab.add(billingHeaderLayout);
|
billingTab.add(billingHeaderLayout);
|
||||||
|
|
||||||
|
// Steuer- und Bankdaten: Pflichtangaben nach §14 UStG bzw. für die
|
||||||
|
// ZUGFeRD-E-Rechnung (Absender-USt-IdNr./Steuernummer, IBAN)
|
||||||
|
ustIdField.setLabel(getTranslation("profile.billing.ustid"));
|
||||||
|
ustIdField.setPlaceholder("DE123456789");
|
||||||
|
ustIdField.addBlurListener(e -> saveInvoiceData());
|
||||||
|
taxNumberField.setLabel(getTranslation("profile.billing.taxnumber"));
|
||||||
|
taxNumberField.addBlurListener(e -> saveInvoiceData());
|
||||||
|
bankNameField.setLabel(getTranslation("profile.billing.bankname"));
|
||||||
|
bankNameField.addBlurListener(e -> saveInvoiceData());
|
||||||
|
ibanField.setLabel(getTranslation("profile.billing.iban"));
|
||||||
|
ibanField.setPlaceholder("DE00 0000 0000 0000 0000 00");
|
||||||
|
ibanField.addBlurListener(e -> saveInvoiceData());
|
||||||
|
|
||||||
|
HorizontalLayout billingTaxLayout = new HorizontalLayout(ustIdField, taxNumberField, bankNameField, ibanField);
|
||||||
|
billingTaxLayout.setSpacing(true);
|
||||||
|
billingTaxLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
|
||||||
|
billingTab.add(billingTaxLayout);
|
||||||
|
|
||||||
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
||||||
final HorizontalLayout mainLayout = new HorizontalLayout();
|
final HorizontalLayout mainLayout = new HorizontalLayout();
|
||||||
mainLayout.setWidthFull();
|
mainLayout.setWidthFull();
|
||||||
|
|||||||
@@ -33,11 +33,15 @@ import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
|
|||||||
import de.assecutor.votianlt.repository.UserRepository;
|
import de.assecutor.votianlt.repository.UserRepository;
|
||||||
import de.assecutor.votianlt.security.SecurityService;
|
import de.assecutor.votianlt.security.SecurityService;
|
||||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||||
|
import de.assecutor.votianlt.service.EmailService;
|
||||||
import de.assecutor.votianlt.service.InvoiceExportService;
|
import de.assecutor.votianlt.service.InvoiceExportService;
|
||||||
import de.assecutor.votianlt.service.InvoiceLifecycleException;
|
import de.assecutor.votianlt.service.InvoiceLifecycleException;
|
||||||
import de.assecutor.votianlt.service.InvoiceLifecycleService;
|
import de.assecutor.votianlt.service.InvoiceLifecycleService;
|
||||||
import de.assecutor.votianlt.service.InvoicePermissionService;
|
import de.assecutor.votianlt.service.InvoicePermissionService;
|
||||||
|
import de.assecutor.votianlt.service.PdfToolService;
|
||||||
|
import de.assecutor.votianlt.service.UserInvoiceZugferdService;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -49,6 +53,7 @@ import java.util.Optional;
|
|||||||
|
|
||||||
@Route(value = "invoices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
@Route(value = "invoices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||||
@RolesAllowed({ "USER", "ADMIN" })
|
@RolesAllowed({ "USER", "ADMIN" })
|
||||||
|
@Slf4j
|
||||||
public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
|
public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm",
|
private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm",
|
||||||
@@ -63,12 +68,15 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
private final InvoicePermissionService invoicePermissionService;
|
private final InvoicePermissionService invoicePermissionService;
|
||||||
private final UserInvoiceDataService userInvoiceDataService;
|
private final UserInvoiceDataService userInvoiceDataService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final UserInvoiceZugferdService userInvoiceZugferdService;
|
||||||
|
private final EmailService emailService;
|
||||||
|
|
||||||
public InvoicesView(CustomerInvoiceRepository customerInvoiceRepository, SecurityService securityService,
|
public InvoicesView(CustomerInvoiceRepository customerInvoiceRepository, SecurityService securityService,
|
||||||
InvoiceLifecycleService invoiceLifecycleService, CustomerInvoiceService customerInvoiceService,
|
InvoiceLifecycleService invoiceLifecycleService, CustomerInvoiceService customerInvoiceService,
|
||||||
InvoiceExportService invoiceExportService, InvoicePermissionService invoicePermissionService,
|
InvoiceExportService invoiceExportService, InvoicePermissionService invoicePermissionService,
|
||||||
UserInvoiceDataService userInvoiceDataService,
|
UserInvoiceDataService userInvoiceDataService,
|
||||||
UserRepository userRepository) {
|
UserRepository userRepository, UserInvoiceZugferdService userInvoiceZugferdService,
|
||||||
|
EmailService emailService) {
|
||||||
this.customerInvoiceRepository = customerInvoiceRepository;
|
this.customerInvoiceRepository = customerInvoiceRepository;
|
||||||
this.securityService = securityService;
|
this.securityService = securityService;
|
||||||
this.invoiceLifecycleService = invoiceLifecycleService;
|
this.invoiceLifecycleService = invoiceLifecycleService;
|
||||||
@@ -77,6 +85,8 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
this.invoicePermissionService = invoicePermissionService;
|
this.invoicePermissionService = invoicePermissionService;
|
||||||
this.userInvoiceDataService = userInvoiceDataService;
|
this.userInvoiceDataService = userInvoiceDataService;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.userInvoiceZugferdService = userInvoiceZugferdService;
|
||||||
|
this.emailService = emailService;
|
||||||
|
|
||||||
setSizeFull();
|
setSizeFull();
|
||||||
setPadding(true);
|
setPadding(true);
|
||||||
@@ -201,6 +211,22 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
actions.add(sentBtn);
|
actions.add(sentBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// E-Rechnung (ZUGFeRD) über das externe PDF Tool — analog zu den
|
||||||
|
// System-Rechnungen der Admin-Seite
|
||||||
|
if (type == InvoiceType.INVOICE && invoice.getPdfData() != null && invoice.getPdfData().length > 0) {
|
||||||
|
Button zugferdBtn = new Button(getTranslation("invoices.action.zugferd"),
|
||||||
|
e -> downloadZugferdZip(invoice));
|
||||||
|
zugferdBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
|
||||||
|
actions.add(zugferdBtn);
|
||||||
|
|
||||||
|
if (isLiveInvoice && invoicePermissionService.canMarkAsSent(currentUser)) {
|
||||||
|
Button sendBtn = new Button(getTranslation("invoices.action.sendinvoice"),
|
||||||
|
e -> sendAsEInvoice(invoice));
|
||||||
|
sendBtn.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_PRIMARY);
|
||||||
|
actions.add(sendBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isLiveInvoice) {
|
if (isLiveInvoice) {
|
||||||
if (invoicePermissionService.canCorrect(currentUser)) {
|
if (invoicePermissionService.canCorrect(currentUser)) {
|
||||||
Button correctBtn = new Button(getTranslation("invoices.action.correct"),
|
Button correctBtn = new Button(getTranslation("invoices.action.correct"),
|
||||||
@@ -318,6 +344,91 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wandelt die Rechnung über das externe PDF Tool in eine signierte
|
||||||
|
* ZUGFeRD-E-Rechnung um und bietet das ZIP (PDF/A-3, factur-x.xml,
|
||||||
|
* Prüfbericht) als Download an.
|
||||||
|
*/
|
||||||
|
private void downloadZugferdZip(CustomerInvoice invoice) {
|
||||||
|
try {
|
||||||
|
PdfToolService.SignedInvoice signed = userInvoiceZugferdService.sign(invoice);
|
||||||
|
triggerZipDownload(signed.zip(), signed.fileName());
|
||||||
|
if (signed.valid()) {
|
||||||
|
Notification.show(getTranslation("invoices.notification.zugferd.created", invoice.getInvoiceNumber()),
|
||||||
|
5000, Notification.Position.BOTTOM_END);
|
||||||
|
} else {
|
||||||
|
Notification.show(getTranslation("invoices.notification.zugferd.invalid"), 7000,
|
||||||
|
Notification.Position.MIDDLE);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Fehler beim Erstellen der ZUGFeRD-E-Rechnung {}: {}", invoice.getInvoiceNumber(),
|
||||||
|
ex.getMessage(), ex);
|
||||||
|
Notification.show(getTranslation("invoices.notification.zugferd.error", ex.getMessage()), 7000,
|
||||||
|
Notification.Position.MIDDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versendet die Rechnung als signierte ZUGFeRD-E-Rechnung (ZIP) per E-Mail
|
||||||
|
* an den Kunden. Besteht die Rechnung die Validierung nicht (HTTP 422 des
|
||||||
|
* PDF Tools), wird der Versand abgebrochen — wie beim Sammelversand der
|
||||||
|
* System-Rechnungen. Nach erfolgreichem Versand wird der Status auf
|
||||||
|
* "Versendet" gesetzt.
|
||||||
|
*/
|
||||||
|
private void sendAsEInvoice(CustomerInvoice invoice) {
|
||||||
|
String recipientEmail = invoice.getRecipientEmail();
|
||||||
|
if (recipientEmail == null || recipientEmail.isBlank()) {
|
||||||
|
Notification.show(getTranslation("invoices.notification.email.missing"), 6000,
|
||||||
|
Notification.Position.MIDDLE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
invoicePermissionService.requireSend(invoicePermissionService.currentUser());
|
||||||
|
|
||||||
|
PdfToolService.SignedInvoice signed = userInvoiceZugferdService.sign(invoice);
|
||||||
|
if (!signed.valid()) {
|
||||||
|
log.warn("Versand der Rechnung {} abgebrochen, ZUGFeRD-Validierung fehlgeschlagen",
|
||||||
|
invoice.getInvoiceNumber());
|
||||||
|
Notification.show(
|
||||||
|
getTranslation("invoices.notification.zugferd.invalidsend", invoice.getInvoiceNumber()), 7000,
|
||||||
|
Notification.Position.MIDDLE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emailService.sendEmailWithAttachment(recipientEmail,
|
||||||
|
getTranslation("invoices.mail.subject", invoice.getInvoiceNumber()),
|
||||||
|
getTranslation("invoices.mail.body", invoice.getInvoiceNumber(),
|
||||||
|
firstNonBlank(invoice.getSenderName(), "")),
|
||||||
|
signed.zip(), signed.fileName());
|
||||||
|
|
||||||
|
if (invoice.getStatus() == InvoiceStatus.ISSUED) {
|
||||||
|
invoiceLifecycleService.markAsSent(invoice.getId(),
|
||||||
|
"Als E-Rechnung per E-Mail versendet an " + recipientEmail);
|
||||||
|
}
|
||||||
|
Notification.show(
|
||||||
|
getTranslation("invoices.notification.emailsent", invoice.getInvoiceNumber(), recipientEmail),
|
||||||
|
5000, Notification.Position.BOTTOM_END);
|
||||||
|
loadInvoices();
|
||||||
|
} catch (InvoiceLifecycleException ex) {
|
||||||
|
Notification.show(ex.getMessage(), 6000, Notification.Position.MIDDLE);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Fehler beim Versand der Rechnung {} an {}: {}", invoice.getInvoiceNumber(), recipientEmail,
|
||||||
|
ex.getMessage(), ex);
|
||||||
|
Notification.show(getTranslation("invoices.notification.zugferd.error", ex.getMessage()), 7000,
|
||||||
|
Notification.Position.MIDDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bietet die ZIP-Bytes als Browser-Download an. */
|
||||||
|
private void triggerZipDownload(byte[] zip, String fileName) {
|
||||||
|
StreamResource resource = new StreamResource(fileName, () -> new ByteArrayInputStream(zip));
|
||||||
|
resource.setContentType("application/zip");
|
||||||
|
resource.setCacheTime(0);
|
||||||
|
StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry()
|
||||||
|
.registerResource(resource);
|
||||||
|
UI.getCurrent().getPage().open(registration.getResourceUri().toString());
|
||||||
|
}
|
||||||
|
|
||||||
private void markAsSent(CustomerInvoice invoice) {
|
private void markAsSent(CustomerInvoice invoice) {
|
||||||
try {
|
try {
|
||||||
invoicePermissionService.requireSend(invoicePermissionService.currentUser());
|
invoicePermissionService.requireSend(invoicePermissionService.currentUser());
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ public class PdfToolService {
|
|||||||
/**
|
/**
|
||||||
* Converts the given invoice PDF into a ZUGFeRD e-invoice. Returns the ZIP
|
* Converts the given invoice PDF into a ZUGFeRD e-invoice. Returns the ZIP
|
||||||
* provided by the PDF Tool; throws with the tool's error message if the
|
* provided by the PDF Tool; throws with the tool's error message if the
|
||||||
* conversion is rejected (e.g. incomplete metadata).
|
* conversion is rejected (e.g. incomplete metadata). A failed Mustang
|
||||||
|
* validation is reported by the tool as HTTP 422 but still carries the ZIP
|
||||||
|
* (including the validation report) in the body; it is returned as a
|
||||||
|
* {@link SignedInvoice} with {@code valid=false} instead of throwing.
|
||||||
*/
|
*/
|
||||||
public SignedInvoice signInvoice(byte[] pdfBytes, InvoiceMetadata metadata) throws Exception {
|
public SignedInvoice signInvoice(byte[] pdfBytes, InvoiceMetadata metadata) throws Exception {
|
||||||
String metadataJson = objectMapper.writeValueAsString(metadata);
|
String metadataJson = objectMapper.writeValueAsString(metadata);
|
||||||
@@ -79,23 +82,30 @@ public class PdfToolService {
|
|||||||
}, MediaType.APPLICATION_PDF);
|
}, MediaType.APPLICATION_PDF);
|
||||||
body.part("metadata", metadataJson, MediaType.APPLICATION_JSON);
|
body.part("metadata", metadataJson, MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
ResponseEntity<byte[]> response;
|
|
||||||
try {
|
try {
|
||||||
response = restClient.post().uri("/api/invoices/sign").contentType(MediaType.MULTIPART_FORM_DATA)
|
ResponseEntity<byte[]> response = restClient.post().uri("/api/invoices/sign")
|
||||||
.body(body.build()).retrieve().toEntity(byte[].class);
|
.contentType(MediaType.MULTIPART_FORM_DATA).body(body.build()).retrieve()
|
||||||
|
.toEntity(byte[].class);
|
||||||
|
return toSignedInvoice(response.getBody(), response.getHeaders(), metadata);
|
||||||
} catch (RestClientResponseException ex) {
|
} catch (RestClientResponseException ex) {
|
||||||
|
if (ex.getStatusCode().value() == 422 && ex.getResponseBodyAsByteArray().length > 0) {
|
||||||
|
return toSignedInvoice(ex.getResponseBodyAsByteArray(), ex.getResponseHeaders(), metadata);
|
||||||
|
}
|
||||||
throw new IllegalStateException(extractErrorMessage(ex), ex);
|
throw new IllegalStateException(extractErrorMessage(ex), ex);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
byte[] zip = response.getBody();
|
/** Builds the result from the response ZIP, file name and validation header. */
|
||||||
|
private SignedInvoice toSignedInvoice(byte[] zip, org.springframework.http.HttpHeaders headers,
|
||||||
|
InvoiceMetadata metadata) {
|
||||||
if (zip == null || zip.length == 0) {
|
if (zip == null || zip.length == 0) {
|
||||||
throw new IllegalStateException("PDF Tool returned an empty response");
|
throw new IllegalStateException("PDF Tool returned an empty response");
|
||||||
}
|
}
|
||||||
String fileName = response.getHeaders().getContentDisposition().getFilename();
|
String fileName = headers != null ? headers.getContentDisposition().getFilename() : null;
|
||||||
if (fileName == null || fileName.isBlank()) {
|
if (fileName == null || fileName.isBlank()) {
|
||||||
fileName = metadata.invoiceNumber() + "-zugferd.zip";
|
fileName = metadata.invoiceNumber() + "-zugferd.zip";
|
||||||
}
|
}
|
||||||
boolean valid = Boolean.parseBoolean(response.getHeaders().getFirst(VALIDATION_HEADER));
|
boolean valid = headers != null && Boolean.parseBoolean(headers.getFirst(VALIDATION_HEADER));
|
||||||
return new SignedInvoice(zip, fileName, valid);
|
return new SignedInvoice(zip, fileName, valid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package de.assecutor.votianlt.service;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.invoices.CustomerInvoice;
|
||||||
|
import de.assecutor.votianlt.model.invoices.CustomerInvoiceItem;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wandelt Nutzer-Rechnungen (Rechnungen der Systemnutzer an ihre eigenen
|
||||||
|
* Kunden) über das externe PDF Tool in signierte ZUGFeRD-E-Rechnungen um —
|
||||||
|
* dasselbe Verfahren, das {@code AdminCreateInvoicesView} für die
|
||||||
|
* System-Rechnungen verwendet. Die EN16931-Metadaten werden vollständig aus
|
||||||
|
* der persistierten {@link CustomerInvoice} aufgebaut (Absender, Empfänger,
|
||||||
|
* Positionen), die beim Erstellen der Rechnung in {@code CreateInvoiceView}
|
||||||
|
* befüllt werden.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class UserInvoiceZugferdService {
|
||||||
|
|
||||||
|
private final PdfToolService pdfToolService;
|
||||||
|
|
||||||
|
public UserInvoiceZugferdService(PdfToolService pdfToolService) {
|
||||||
|
this.pdfToolService = pdfToolService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signiert die Rechnung über das PDF Tool. Wirft eine
|
||||||
|
* {@link IllegalStateException} mit verständlicher Meldung, wenn der
|
||||||
|
* Rechnung Daten fehlen, die für eine EN16931-E-Rechnung zwingend sind.
|
||||||
|
*/
|
||||||
|
public PdfToolService.SignedInvoice sign(CustomerInvoice invoice) throws Exception {
|
||||||
|
if (invoice.getPdfData() == null || invoice.getPdfData().length == 0) {
|
||||||
|
throw new IllegalStateException("Für diese Rechnung ist kein PDF gespeichert.");
|
||||||
|
}
|
||||||
|
return pdfToolService.signInvoice(invoice.getPdfData(), buildMetadata(invoice));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PdfToolService.InvoiceMetadata buildMetadata(CustomerInvoice invoice) {
|
||||||
|
if (isBlank(invoice.getSenderName()) || isBlank(invoice.getRecipientName())) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Absender- oder Empfängerdaten fehlen — diese Rechnung wurde vor der E-Rechnungs-Umstellung "
|
||||||
|
+ "erstellt und kann nicht als E-Rechnung aufbereitet werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
PdfToolService.Party sender = new PdfToolService.Party(invoice.getSenderName(),
|
||||||
|
safe(invoice.getSenderAddress()), safe(invoice.getSenderPostcode()), safe(invoice.getSenderCity()),
|
||||||
|
firstNonBlank(invoice.getSenderCountry(), "DE"), blankToNull(invoice.getSenderVatId()),
|
||||||
|
safe(invoice.getSenderEmail()), safe(invoice.getSenderPhone()));
|
||||||
|
|
||||||
|
PdfToolService.Party recipient = new PdfToolService.Party(
|
||||||
|
firstNonBlank(invoice.getRecipientCompany(), invoice.getRecipientName()),
|
||||||
|
safe(invoice.getRecipientAddress()), safe(invoice.getRecipientPostcode()),
|
||||||
|
safe(invoice.getRecipientCity()), firstNonBlank(invoice.getRecipientCountry(), "DE"),
|
||||||
|
blankToNull(invoice.getRecipientVatId()), safe(invoice.getRecipientEmail()), null);
|
||||||
|
|
||||||
|
List<PdfToolService.LineItem> items = buildLineItems(invoice);
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Die Rechnung enthält keine Positionen mit Betrag.");
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate issueDate = invoice.getInvoiceDate() != null ? invoice.getInvoiceDate() : LocalDate.now();
|
||||||
|
LocalDate deliveryDate = invoice.getDeliveryDate() != null ? invoice.getDeliveryDate() : issueDate;
|
||||||
|
LocalDate dueDate = invoice.getPaymentDueDate() != null ? invoice.getPaymentDueDate() : issueDate.plusDays(14);
|
||||||
|
|
||||||
|
return new PdfToolService.InvoiceMetadata(invoice.getInvoiceNumber(), issueDate, deliveryDate, dueDate, "EUR",
|
||||||
|
safe(invoice.getPaymentTerms()), null, blankToNull(invoice.getIban()), blankToNull(invoice.getBic()),
|
||||||
|
sender, recipient, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PdfToolService.LineItem> buildLineItems(CustomerInvoice invoice) {
|
||||||
|
BigDecimal vatPercent = invoice.getVatRate() != null
|
||||||
|
? invoice.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP)
|
||||||
|
: new BigDecimal("19");
|
||||||
|
|
||||||
|
List<PdfToolService.LineItem> items = new ArrayList<>();
|
||||||
|
if (invoice.getItems() != null) {
|
||||||
|
for (CustomerInvoiceItem item : invoice.getItems()) {
|
||||||
|
if (item == null || item.getQuantity() == null || item.getUnitPrice() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal itemVatPercent = item.getVatRate() != null
|
||||||
|
? item.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP)
|
||||||
|
: vatPercent;
|
||||||
|
items.add(new PdfToolService.LineItem(safe(item.getDescription()), item.getQuantity(),
|
||||||
|
item.getUnitPrice(), itemVatPercent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ältere Rechnungen ohne Positionsliste: eine Sammelposition aus dem
|
||||||
|
// Rechnungs-Netto, damit sie trotzdem als E-Rechnung versendbar bleiben.
|
||||||
|
if (items.isEmpty() && invoice.getNetAmount() != null && invoice.getNetAmount().signum() > 0) {
|
||||||
|
items.add(new PdfToolService.LineItem(firstNonBlank(invoice.getDescription(), "Leistung gemäß Rechnung"),
|
||||||
|
BigDecimal.ONE, invoice.getNetAmount(), vatPercent));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safe(String value) {
|
||||||
|
return value != null ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blankToNull(String value) {
|
||||||
|
return value == null || value.isBlank() ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstNonBlank(String... values) {
|
||||||
|
for (String value : values) {
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBlank(String value) {
|
||||||
|
return value == null || value.isBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1153,3 +1153,22 @@ admincreateinvoices.notification.zugferd.created=E-Rechnung {0} erstellt
|
|||||||
admincreateinvoices.notification.zugferd.invalid=E-Rechnung erstellt, aber die Validierung ist fehlgeschlagen. Details im Prüfbericht in der ZIP-Datei.
|
admincreateinvoices.notification.zugferd.invalid=E-Rechnung erstellt, aber die Validierung ist fehlgeschlagen. Details im Prüfbericht in der ZIP-Datei.
|
||||||
admincreateinvoices.notification.zugferd.error=Fehler beim Erstellen der E-Rechnung: {0}
|
admincreateinvoices.notification.zugferd.error=Fehler beim Erstellen der E-Rechnung: {0}
|
||||||
admincreateinvoices.notification.zugferd.nopositions=Keine abrechenbaren Positionen für die E-Rechnung vorhanden
|
admincreateinvoices.notification.zugferd.nopositions=Keine abrechenbaren Positionen für die E-Rechnung vorhanden
|
||||||
|
admincreateinvoices.notification.zugferd.batcherror=E-Rechnung {0} konnte nicht erstellt werden, es wurden keine Rechnungen versendet: {1}
|
||||||
|
admincreateinvoices.notification.zugferd.invalidsend=Validierung fehlgeschlagen für: {0}. Es wurden keine Rechnungen versendet.
|
||||||
|
admincreateinvoices.notification.zip.combined.error=Fehler beim Erstellen der Sammel-ZIP-Datei: {0}
|
||||||
|
|
||||||
|
# E-Rechnung (ZUGFeRD) für Nutzer-Rechnungen an ihre Kunden
|
||||||
|
invoices.action.zugferd=E-Rechnung (ZIP)
|
||||||
|
invoices.action.sendinvoice=Per E-Mail senden
|
||||||
|
invoices.notification.zugferd.created=E-Rechnung {0} erstellt
|
||||||
|
invoices.notification.zugferd.invalid=E-Rechnung erstellt, aber die Validierung ist fehlgeschlagen. Details im Prüfbericht in der ZIP-Datei.
|
||||||
|
invoices.notification.zugferd.invalidsend=Versand abgebrochen: Die Validierung der E-Rechnung {0} ist fehlgeschlagen.
|
||||||
|
invoices.notification.zugferd.error=Fehler bei der E-Rechnung: {0}
|
||||||
|
invoices.notification.email.missing=Für den Rechnungsempfänger ist keine E-Mail-Adresse hinterlegt.
|
||||||
|
invoices.notification.emailsent=Rechnung {0} wurde als E-Rechnung an {1} versendet.
|
||||||
|
invoices.mail.subject=Ihre Rechnung {0}
|
||||||
|
invoices.mail.body=Sehr geehrte Damen und Herren,\n\nanbei erhalten Sie Ihre Rechnung {0} als E-Rechnung (ZIP mit PDF und Rechnungsdaten).\n\nMit freundlichen Grüßen\n{1}
|
||||||
|
profile.billing.ustid=USt-IdNr.
|
||||||
|
profile.billing.taxnumber=Steuernummer
|
||||||
|
profile.billing.bankname=Bank
|
||||||
|
profile.billing.iban=IBAN
|
||||||
|
|||||||
@@ -983,3 +983,22 @@ admincreateinvoices.notification.zugferd.created=E-arve {0} loodud
|
|||||||
admincreateinvoices.notification.zugferd.invalid=E-arve on loodud, kuid valideerimine ebaõnnestus. Üksikasjad on ZIP-failis olevas valideerimisaruandes.
|
admincreateinvoices.notification.zugferd.invalid=E-arve on loodud, kuid valideerimine ebaõnnestus. Üksikasjad on ZIP-failis olevas valideerimisaruandes.
|
||||||
admincreateinvoices.notification.zugferd.error=Viga e-arve loomisel: {0}
|
admincreateinvoices.notification.zugferd.error=Viga e-arve loomisel: {0}
|
||||||
admincreateinvoices.notification.zugferd.nopositions=E-arve jaoks puuduvad arveldatavad positsioonid
|
admincreateinvoices.notification.zugferd.nopositions=E-arve jaoks puuduvad arveldatavad positsioonid
|
||||||
|
admincreateinvoices.notification.zugferd.batcherror=E-arvet {0} ei õnnestunud luua, ühtegi arvet ei saadetud: {1}
|
||||||
|
admincreateinvoices.notification.zugferd.invalidsend=Valideerimine ebaõnnestus: {0}. Ühtegi arvet ei saadetud.
|
||||||
|
admincreateinvoices.notification.zip.combined.error=Viga koond-ZIP-faili loomisel: {0}
|
||||||
|
|
||||||
|
# E-arve (ZUGFeRD) kasutajate arvetele nende klientidele
|
||||||
|
invoices.action.zugferd=E-arve (ZIP)
|
||||||
|
invoices.action.sendinvoice=Saada e-postiga
|
||||||
|
invoices.notification.zugferd.created=E-arve {0} loodud
|
||||||
|
invoices.notification.zugferd.invalid=E-arve loodi, kuid valideerimine ebaõnnestus. Vaadake kontrolliaruannet ZIP-failis.
|
||||||
|
invoices.notification.zugferd.invalidsend=Saatmine katkestati: e-arve {0} valideerimine ebaõnnestus.
|
||||||
|
invoices.notification.zugferd.error=E-arve viga: {0}
|
||||||
|
invoices.notification.email.missing=Arve saajale pole e-posti aadressi salvestatud.
|
||||||
|
invoices.notification.emailsent=Arve {0} saadeti e-arvena aadressile {1}.
|
||||||
|
invoices.mail.subject=Teie arve {0}
|
||||||
|
invoices.mail.body=Lugupeetud klient,\n\nmanuses on Teie arve {0} e-arvena (ZIP koos PDF-i ja arveandmetega).\n\nLugupidamisega\n{1}
|
||||||
|
profile.billing.ustid=KMKR nr
|
||||||
|
profile.billing.taxnumber=Maksunumber
|
||||||
|
profile.billing.bankname=Pank
|
||||||
|
profile.billing.iban=IBAN
|
||||||
|
|||||||
@@ -1153,3 +1153,22 @@ admincreateinvoices.notification.zugferd.created=E-invoice {0} created
|
|||||||
admincreateinvoices.notification.zugferd.invalid=E-invoice created, but validation failed. See the validation report in the ZIP file.
|
admincreateinvoices.notification.zugferd.invalid=E-invoice created, but validation failed. See the validation report in the ZIP file.
|
||||||
admincreateinvoices.notification.zugferd.error=Error creating the e-invoice: {0}
|
admincreateinvoices.notification.zugferd.error=Error creating the e-invoice: {0}
|
||||||
admincreateinvoices.notification.zugferd.nopositions=No billable positions available for the e-invoice
|
admincreateinvoices.notification.zugferd.nopositions=No billable positions available for the e-invoice
|
||||||
|
admincreateinvoices.notification.zugferd.batcherror=E-invoice {0} could not be created, no invoices were sent: {1}
|
||||||
|
admincreateinvoices.notification.zugferd.invalidsend=Validation failed for: {0}. No invoices were sent.
|
||||||
|
admincreateinvoices.notification.zip.combined.error=Error creating the combined ZIP file: {0}
|
||||||
|
|
||||||
|
# E-invoice (ZUGFeRD) for user invoices to their customers
|
||||||
|
invoices.action.zugferd=E-invoice (ZIP)
|
||||||
|
invoices.action.sendinvoice=Send by email
|
||||||
|
invoices.notification.zugferd.created=E-invoice {0} created
|
||||||
|
invoices.notification.zugferd.invalid=E-invoice created, but validation failed. See the validation report in the ZIP file.
|
||||||
|
invoices.notification.zugferd.invalidsend=Sending aborted: validation of e-invoice {0} failed.
|
||||||
|
invoices.notification.zugferd.error=E-invoice error: {0}
|
||||||
|
invoices.notification.email.missing=No email address is stored for the invoice recipient.
|
||||||
|
invoices.notification.emailsent=Invoice {0} was sent as an e-invoice to {1}.
|
||||||
|
invoices.mail.subject=Your invoice {0}
|
||||||
|
invoices.mail.body=Dear Sir or Madam,\n\nPlease find attached your invoice {0} as an e-invoice (ZIP with PDF and invoice data).\n\nKind regards\n{1}
|
||||||
|
profile.billing.ustid=VAT ID
|
||||||
|
profile.billing.taxnumber=Tax number
|
||||||
|
profile.billing.bankname=Bank
|
||||||
|
profile.billing.iban=IBAN
|
||||||
|
|||||||
@@ -1084,3 +1084,22 @@ admincreateinvoices.notification.zugferd.created=Factura electrónica {0} creada
|
|||||||
admincreateinvoices.notification.zugferd.invalid=Factura electrónica creada, pero la validación ha fallado. Consulte el informe de validación en el archivo ZIP.
|
admincreateinvoices.notification.zugferd.invalid=Factura electrónica creada, pero la validación ha fallado. Consulte el informe de validación en el archivo ZIP.
|
||||||
admincreateinvoices.notification.zugferd.error=Error al crear la factura electrónica: {0}
|
admincreateinvoices.notification.zugferd.error=Error al crear la factura electrónica: {0}
|
||||||
admincreateinvoices.notification.zugferd.nopositions=No hay posiciones facturables disponibles para la factura electrónica
|
admincreateinvoices.notification.zugferd.nopositions=No hay posiciones facturables disponibles para la factura electrónica
|
||||||
|
admincreateinvoices.notification.zugferd.batcherror=No se pudo crear la factura electrónica {0}, no se envió ninguna factura: {1}
|
||||||
|
admincreateinvoices.notification.zugferd.invalidsend=La validación falló para: {0}. No se envió ninguna factura.
|
||||||
|
admincreateinvoices.notification.zip.combined.error=Error al crear el archivo ZIP combinado: {0}
|
||||||
|
|
||||||
|
# Factura electrónica (ZUGFeRD) para facturas de usuarios a sus clientes
|
||||||
|
invoices.action.zugferd=Factura electrónica (ZIP)
|
||||||
|
invoices.action.sendinvoice=Enviar por correo
|
||||||
|
invoices.notification.zugferd.created=Factura electrónica {0} creada
|
||||||
|
invoices.notification.zugferd.invalid=Factura electrónica creada, pero la validación ha fallado. Consulte el informe de validación en el archivo ZIP.
|
||||||
|
invoices.notification.zugferd.invalidsend=Envío cancelado: la validación de la factura electrónica {0} ha fallado.
|
||||||
|
invoices.notification.zugferd.error=Error en la factura electrónica: {0}
|
||||||
|
invoices.notification.email.missing=No hay una dirección de correo electrónico registrada para el destinatario de la factura.
|
||||||
|
invoices.notification.emailsent=La factura {0} se envió como factura electrónica a {1}.
|
||||||
|
invoices.mail.subject=Su factura {0}
|
||||||
|
invoices.mail.body=Estimados señores:\n\nAdjunto encontrará su factura {0} como factura electrónica (ZIP con PDF y datos de la factura).\n\nAtentamente\n{1}
|
||||||
|
profile.billing.ustid=NIF-IVA
|
||||||
|
profile.billing.taxnumber=Número fiscal
|
||||||
|
profile.billing.bankname=Banco
|
||||||
|
profile.billing.iban=IBAN
|
||||||
|
|||||||
@@ -1084,3 +1084,22 @@ admincreateinvoices.notification.zugferd.created=Facture électronique {0} cré
|
|||||||
admincreateinvoices.notification.zugferd.invalid=Facture électronique créée, mais la validation a échoué. Détails dans le rapport de validation du fichier ZIP.
|
admincreateinvoices.notification.zugferd.invalid=Facture électronique créée, mais la validation a échoué. Détails dans le rapport de validation du fichier ZIP.
|
||||||
admincreateinvoices.notification.zugferd.error=Erreur lors de la création de la facture électronique : {0}
|
admincreateinvoices.notification.zugferd.error=Erreur lors de la création de la facture électronique : {0}
|
||||||
admincreateinvoices.notification.zugferd.nopositions=Aucune position facturable disponible pour la facture électronique
|
admincreateinvoices.notification.zugferd.nopositions=Aucune position facturable disponible pour la facture électronique
|
||||||
|
admincreateinvoices.notification.zugferd.batcherror=La facture électronique {0} n''a pas pu être créée, aucune facture n''a été envoyée : {1}
|
||||||
|
admincreateinvoices.notification.zugferd.invalidsend=La validation a échoué pour : {0}. Aucune facture n''a été envoyée.
|
||||||
|
admincreateinvoices.notification.zip.combined.error=Erreur lors de la création du fichier ZIP combiné : {0}
|
||||||
|
|
||||||
|
# Facture électronique (ZUGFeRD) pour les factures des utilisateurs à leurs clients
|
||||||
|
invoices.action.zugferd=Facture électronique (ZIP)
|
||||||
|
invoices.action.sendinvoice=Envoyer par e-mail
|
||||||
|
invoices.notification.zugferd.created=Facture électronique {0} créée
|
||||||
|
invoices.notification.zugferd.invalid=Facture électronique créée, mais la validation a échoué. Voir le rapport de validation dans le fichier ZIP.
|
||||||
|
invoices.notification.zugferd.invalidsend=Envoi annulé : la validation de la facture électronique {0} a échoué.
|
||||||
|
invoices.notification.zugferd.error=Erreur de facture électronique : {0}
|
||||||
|
invoices.notification.email.missing=Aucune adresse e-mail n'est enregistrée pour le destinataire de la facture.
|
||||||
|
invoices.notification.emailsent=La facture {0} a été envoyée en tant que facture électronique à {1}.
|
||||||
|
invoices.mail.subject=Votre facture {0}
|
||||||
|
invoices.mail.body=Madame, Monsieur,\n\nVeuillez trouver ci-joint votre facture {0} sous forme de facture électronique (ZIP avec PDF et données de facturation).\n\nCordialement\n{1}
|
||||||
|
profile.billing.ustid=N° TVA intracommunautaire
|
||||||
|
profile.billing.taxnumber=Numéro fiscal
|
||||||
|
profile.billing.bankname=Banque
|
||||||
|
profile.billing.iban=IBAN
|
||||||
|
|||||||
@@ -1086,3 +1086,22 @@ admincreateinvoices.notification.zugferd.created=E. sąskaita {0} sukurta
|
|||||||
admincreateinvoices.notification.zugferd.invalid=E. sąskaita sukurta, tačiau patikra nepavyko. Išsami informacija patikros ataskaitoje ZIP faile.
|
admincreateinvoices.notification.zugferd.invalid=E. sąskaita sukurta, tačiau patikra nepavyko. Išsami informacija patikros ataskaitoje ZIP faile.
|
||||||
admincreateinvoices.notification.zugferd.error=Klaida kuriant e. sąskaitą: {0}
|
admincreateinvoices.notification.zugferd.error=Klaida kuriant e. sąskaitą: {0}
|
||||||
admincreateinvoices.notification.zugferd.nopositions=E. sąskaitai nėra apmokestinamų pozicijų
|
admincreateinvoices.notification.zugferd.nopositions=E. sąskaitai nėra apmokestinamų pozicijų
|
||||||
|
admincreateinvoices.notification.zugferd.batcherror=Nepavyko sukurti e. sąskaitos {0}, sąskaitos nebuvo išsiųstos: {1}
|
||||||
|
admincreateinvoices.notification.zugferd.invalidsend=Patikrinimas nepavyko: {0}. Sąskaitos nebuvo išsiųstos.
|
||||||
|
admincreateinvoices.notification.zip.combined.error=Klaida kuriant bendrą ZIP failą: {0}
|
||||||
|
|
||||||
|
# E. sąskaita (ZUGFeRD) naudotojų sąskaitoms jų klientams
|
||||||
|
invoices.action.zugferd=E. sąskaita (ZIP)
|
||||||
|
invoices.action.sendinvoice=Siųsti el. paštu
|
||||||
|
invoices.notification.zugferd.created=E. sąskaita {0} sukurta
|
||||||
|
invoices.notification.zugferd.invalid=E. sąskaita sukurta, bet patikra nepavyko. Žr. patikros ataskaitą ZIP faile.
|
||||||
|
invoices.notification.zugferd.invalidsend=Siuntimas nutrauktas: e. sąskaitos {0} patikra nepavyko.
|
||||||
|
invoices.notification.zugferd.error=E. sąskaitos klaida: {0}
|
||||||
|
invoices.notification.email.missing=Sąskaitos gavėjui nėra išsaugoto el. pašto adreso.
|
||||||
|
invoices.notification.emailsent=Sąskaita {0} išsiųsta kaip e. sąskaita adresu {1}.
|
||||||
|
invoices.mail.subject=Jūsų sąskaita {0}
|
||||||
|
invoices.mail.body=Gerbiamieji,\n\npridedame Jūsų sąskaitą {0} kaip e. sąskaitą (ZIP su PDF ir sąskaitos duomenimis).\n\nPagarbiai\n{1}
|
||||||
|
profile.billing.ustid=PVM mokėtojo kodas
|
||||||
|
profile.billing.taxnumber=Mokesčių mokėtojo numeris
|
||||||
|
profile.billing.bankname=Bankas
|
||||||
|
profile.billing.iban=IBAN
|
||||||
|
|||||||
@@ -1084,3 +1084,22 @@ admincreateinvoices.notification.zugferd.created=E-rēķins {0} izveidots
|
|||||||
admincreateinvoices.notification.zugferd.invalid=E-rēķins izveidots, bet validācija neizdevās. Detaļas skatiet validācijas atskaitē ZIP failā.
|
admincreateinvoices.notification.zugferd.invalid=E-rēķins izveidots, bet validācija neizdevās. Detaļas skatiet validācijas atskaitē ZIP failā.
|
||||||
admincreateinvoices.notification.zugferd.error=Kļūda, veidojot e-rēķinu: {0}
|
admincreateinvoices.notification.zugferd.error=Kļūda, veidojot e-rēķinu: {0}
|
||||||
admincreateinvoices.notification.zugferd.nopositions=E-rēķinam nav pieejamu norēķinu pozīciju
|
admincreateinvoices.notification.zugferd.nopositions=E-rēķinam nav pieejamu norēķinu pozīciju
|
||||||
|
admincreateinvoices.notification.zugferd.batcherror=Neizdevās izveidot e-rēķinu {0}, rēķini netika nosūtīti: {1}
|
||||||
|
admincreateinvoices.notification.zugferd.invalidsend=Validācija neizdevās: {0}. Rēķini netika nosūtīti.
|
||||||
|
admincreateinvoices.notification.zip.combined.error=Kļūda, veidojot apvienoto ZIP failu: {0}
|
||||||
|
|
||||||
|
# E-rēķins (ZUGFeRD) lietotāju rēķiniem viņu klientiem
|
||||||
|
invoices.action.zugferd=E-rēķins (ZIP)
|
||||||
|
invoices.action.sendinvoice=Sūtīt pa e-pastu
|
||||||
|
invoices.notification.zugferd.created=E-rēķins {0} izveidots
|
||||||
|
invoices.notification.zugferd.invalid=E-rēķins izveidots, bet validācija neizdevās. Skatiet pārbaudes atskaiti ZIP failā.
|
||||||
|
invoices.notification.zugferd.invalidsend=Sūtīšana pārtraukta: e-rēķina {0} validācija neizdevās.
|
||||||
|
invoices.notification.zugferd.error=E-rēķina kļūda: {0}
|
||||||
|
invoices.notification.email.missing=Rēķina saņēmējam nav saglabāta e-pasta adrese.
|
||||||
|
invoices.notification.emailsent=Rēķins {0} nosūtīts kā e-rēķins uz {1}.
|
||||||
|
invoices.mail.subject=Jūsu rēķins {0}
|
||||||
|
invoices.mail.body=Cienījamie klienti!\n\nPielikumā Jūsu rēķins {0} kā e-rēķins (ZIP ar PDF un rēķina datiem).\n\nAr cieņu\n{1}
|
||||||
|
profile.billing.ustid=PVN numurs
|
||||||
|
profile.billing.taxnumber=Nodokļu maksātāja numurs
|
||||||
|
profile.billing.bankname=Banka
|
||||||
|
profile.billing.iban=IBAN
|
||||||
|
|||||||
@@ -1084,3 +1084,22 @@ admincreateinvoices.notification.zugferd.created=Utworzono e-fakturę {0}
|
|||||||
admincreateinvoices.notification.zugferd.invalid=E-faktura została utworzona, ale walidacja nie powiodła się. Szczegóły w raporcie walidacji w pliku ZIP.
|
admincreateinvoices.notification.zugferd.invalid=E-faktura została utworzona, ale walidacja nie powiodła się. Szczegóły w raporcie walidacji w pliku ZIP.
|
||||||
admincreateinvoices.notification.zugferd.error=Błąd podczas tworzenia e-faktury: {0}
|
admincreateinvoices.notification.zugferd.error=Błąd podczas tworzenia e-faktury: {0}
|
||||||
admincreateinvoices.notification.zugferd.nopositions=Brak pozycji rozliczeniowych dla e-faktury
|
admincreateinvoices.notification.zugferd.nopositions=Brak pozycji rozliczeniowych dla e-faktury
|
||||||
|
admincreateinvoices.notification.zugferd.batcherror=Nie udało się utworzyć e-faktury {0}, żadne faktury nie zostały wysłane: {1}
|
||||||
|
admincreateinvoices.notification.zugferd.invalidsend=Walidacja nie powiodła się dla: {0}. Żadne faktury nie zostały wysłane.
|
||||||
|
admincreateinvoices.notification.zip.combined.error=Błąd podczas tworzenia zbiorczego pliku ZIP: {0}
|
||||||
|
|
||||||
|
# E-faktura (ZUGFeRD) dla faktur użytkowników dla ich klientów
|
||||||
|
invoices.action.zugferd=E-faktura (ZIP)
|
||||||
|
invoices.action.sendinvoice=Wyślij e-mailem
|
||||||
|
invoices.notification.zugferd.created=E-faktura {0} utworzona
|
||||||
|
invoices.notification.zugferd.invalid=E-faktura została utworzona, ale walidacja nie powiodła się. Zobacz raport walidacji w pliku ZIP.
|
||||||
|
invoices.notification.zugferd.invalidsend=Wysyłka przerwana: walidacja e-faktury {0} nie powiodła się.
|
||||||
|
invoices.notification.zugferd.error=Błąd e-faktury: {0}
|
||||||
|
invoices.notification.email.missing=Dla odbiorcy faktury nie zapisano adresu e-mail.
|
||||||
|
invoices.notification.emailsent=Faktura {0} została wysłana jako e-faktura na adres {1}.
|
||||||
|
invoices.mail.subject=Państwa faktura {0}
|
||||||
|
invoices.mail.body=Szanowni Państwo,\n\nw załączeniu przesyłamy fakturę {0} jako e-fakturę (ZIP z plikiem PDF i danymi faktury).\n\nZ poważaniem\n{1}
|
||||||
|
profile.billing.ustid=NIP UE
|
||||||
|
profile.billing.taxnumber=Numer podatkowy
|
||||||
|
profile.billing.bankname=Bank
|
||||||
|
profile.billing.iban=IBAN
|
||||||
|
|||||||
@@ -1084,3 +1084,22 @@ admincreateinvoices.notification.zugferd.created=Электронный счёт
|
|||||||
admincreateinvoices.notification.zugferd.invalid=Электронный счёт создан, но проверка не пройдена. Подробности в отчёте о проверке в ZIP-файле.
|
admincreateinvoices.notification.zugferd.invalid=Электронный счёт создан, но проверка не пройдена. Подробности в отчёте о проверке в ZIP-файле.
|
||||||
admincreateinvoices.notification.zugferd.error=Ошибка при создании электронного счёта: {0}
|
admincreateinvoices.notification.zugferd.error=Ошибка при создании электронного счёта: {0}
|
||||||
admincreateinvoices.notification.zugferd.nopositions=Нет позиций для выставления электронного счёта
|
admincreateinvoices.notification.zugferd.nopositions=Нет позиций для выставления электронного счёта
|
||||||
|
admincreateinvoices.notification.zugferd.batcherror=Не удалось создать электронный счёт {0}, счета не были отправлены: {1}
|
||||||
|
admincreateinvoices.notification.zugferd.invalidsend=Проверка не пройдена для: {0}. Счета не были отправлены.
|
||||||
|
admincreateinvoices.notification.zip.combined.error=Ошибка при создании общего ZIP-файла: {0}
|
||||||
|
|
||||||
|
# Электронный счёт (ZUGFeRD) для счетов пользователей их клиентам
|
||||||
|
invoices.action.zugferd=Электронный счёт (ZIP)
|
||||||
|
invoices.action.sendinvoice=Отправить по эл. почте
|
||||||
|
invoices.notification.zugferd.created=Электронный счёт {0} создан
|
||||||
|
invoices.notification.zugferd.invalid=Электронный счёт создан, но проверка не пройдена. См. отчёт о проверке в ZIP-файле.
|
||||||
|
invoices.notification.zugferd.invalidsend=Отправка отменена: проверка электронного счёта {0} не пройдена.
|
||||||
|
invoices.notification.zugferd.error=Ошибка электронного счёта: {0}
|
||||||
|
invoices.notification.email.missing=Для получателя счёта не сохранён адрес электронной почты.
|
||||||
|
invoices.notification.emailsent=Счёт {0} отправлен как электронный счёт на {1}.
|
||||||
|
invoices.mail.subject=Ваш счёт {0}
|
||||||
|
invoices.mail.body=Уважаемые дамы и господа!\n\nВо вложении Ваш счёт {0} в виде электронного счёта (ZIP с PDF и данными счёта).\n\nС уважением\n{1}
|
||||||
|
profile.billing.ustid=Идентификатор плательщика НДС
|
||||||
|
profile.billing.taxnumber=Налоговый номер
|
||||||
|
profile.billing.bankname=Банк
|
||||||
|
profile.billing.iban=IBAN
|
||||||
|
|||||||
@@ -1084,3 +1084,22 @@ admincreateinvoices.notification.zugferd.created={0} numaralı e-fatura oluştur
|
|||||||
admincreateinvoices.notification.zugferd.invalid=E-fatura oluşturuldu ancak doğrulama başarısız oldu. Ayrıntılar ZIP dosyasındaki doğrulama raporunda.
|
admincreateinvoices.notification.zugferd.invalid=E-fatura oluşturuldu ancak doğrulama başarısız oldu. Ayrıntılar ZIP dosyasındaki doğrulama raporunda.
|
||||||
admincreateinvoices.notification.zugferd.error=E-fatura oluşturulurken hata: {0}
|
admincreateinvoices.notification.zugferd.error=E-fatura oluşturulurken hata: {0}
|
||||||
admincreateinvoices.notification.zugferd.nopositions=E-fatura için faturalandırılabilir pozisyon yok
|
admincreateinvoices.notification.zugferd.nopositions=E-fatura için faturalandırılabilir pozisyon yok
|
||||||
|
admincreateinvoices.notification.zugferd.batcherror=E-fatura {0} oluşturulamadı, hiçbir fatura gönderilmedi: {1}
|
||||||
|
admincreateinvoices.notification.zugferd.invalidsend=Doğrulama başarısız oldu: {0}. Hiçbir fatura gönderilmedi.
|
||||||
|
admincreateinvoices.notification.zip.combined.error=Toplu ZIP dosyası oluşturulurken hata: {0}
|
||||||
|
|
||||||
|
# Kullanıcıların müşterilerine kestiği faturalar için e-fatura (ZUGFeRD)
|
||||||
|
invoices.action.zugferd=E-fatura (ZIP)
|
||||||
|
invoices.action.sendinvoice=E-posta ile gönder
|
||||||
|
invoices.notification.zugferd.created=E-fatura {0} oluşturuldu
|
||||||
|
invoices.notification.zugferd.invalid=E-fatura oluşturuldu, ancak doğrulama başarısız oldu. ZIP dosyasındaki doğrulama raporuna bakın.
|
||||||
|
invoices.notification.zugferd.invalidsend=Gönderim iptal edildi: {0} e-faturasının doğrulaması başarısız oldu.
|
||||||
|
invoices.notification.zugferd.error=E-fatura hatası: {0}
|
||||||
|
invoices.notification.email.missing=Fatura alıcısı için kayıtlı bir e-posta adresi yok.
|
||||||
|
invoices.notification.emailsent={0} numaralı fatura e-fatura olarak {1} adresine gönderildi.
|
||||||
|
invoices.mail.subject=Faturanız {0}
|
||||||
|
invoices.mail.body=Sayın yetkili,\n\nekte {0} numaralı faturanızı e-fatura olarak (PDF ve fatura verilerini içeren ZIP) bulabilirsiniz.\n\nSaygılarımızla\n{1}
|
||||||
|
profile.billing.ustid=KDV kimlik no
|
||||||
|
profile.billing.taxnumber=Vergi numarası
|
||||||
|
profile.billing.bankname=Banka
|
||||||
|
profile.billing.iban=IBAN
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package de.assecutor.votianlt.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.invoices.CustomerInvoice;
|
||||||
|
import de.assecutor.votianlt.model.invoices.CustomerInvoiceItem;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class UserInvoiceZugferdServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PdfToolService pdfToolService;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<PdfToolService.InvoiceMetadata> metadataCaptor;
|
||||||
|
|
||||||
|
private UserInvoiceZugferdService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
service = new UserInvoiceZugferdService(pdfToolService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CustomerInvoice fullyPopulatedInvoice() {
|
||||||
|
CustomerInvoice invoice = new CustomerInvoice();
|
||||||
|
invoice.setInvoiceNumber("RE-2026-0001");
|
||||||
|
invoice.setInvoiceDate(LocalDate.of(2026, 7, 14));
|
||||||
|
invoice.setDeliveryDate(LocalDate.of(2026, 7, 10));
|
||||||
|
invoice.setPaymentDueDate(LocalDate.of(2026, 7, 28));
|
||||||
|
invoice.setSenderName("Spedition Muster GmbH");
|
||||||
|
invoice.setSenderAddress("Musterstraße 1");
|
||||||
|
invoice.setSenderPostcode("20095");
|
||||||
|
invoice.setSenderCity("Hamburg");
|
||||||
|
invoice.setSenderVatId("DE123456789");
|
||||||
|
invoice.setSenderEmail("info@muster.de");
|
||||||
|
invoice.setSenderPhone("040 123456");
|
||||||
|
invoice.setRecipientCompany("Kunde AG");
|
||||||
|
invoice.setRecipientName("Erika Beispiel");
|
||||||
|
invoice.setRecipientAddress("Beispielweg 2");
|
||||||
|
invoice.setRecipientPostcode("10115");
|
||||||
|
invoice.setRecipientCity("Berlin");
|
||||||
|
invoice.setRecipientEmail("rechnung@kunde-ag.de");
|
||||||
|
invoice.setVatRate(new BigDecimal("0.19"));
|
||||||
|
invoice.setNetAmount(new BigDecimal("100.00"));
|
||||||
|
invoice.setIban("DE02120300000000202051");
|
||||||
|
invoice.setPaymentTerms("Zahlbar innerhalb von 14 Tagen");
|
||||||
|
invoice.setItems(List.of(
|
||||||
|
new CustomerInvoiceItem(new BigDecimal("12"), "km", "Transport", new BigDecimal("5.00"),
|
||||||
|
new BigDecimal("0.19")),
|
||||||
|
new CustomerInvoiceItem(BigDecimal.ONE, "pauschal", "Grundpreis", new BigDecimal("40.00"),
|
||||||
|
new BigDecimal("0.19"))));
|
||||||
|
invoice.setPdfData(new byte[] { 1, 2, 3 });
|
||||||
|
return invoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void signBuildsMetadataFromInvoice() throws Exception {
|
||||||
|
CustomerInvoice invoice = fullyPopulatedInvoice();
|
||||||
|
PdfToolService.SignedInvoice signed = new PdfToolService.SignedInvoice(new byte[] { 9 }, "re.zip", true);
|
||||||
|
when(pdfToolService.signInvoice(eq(invoice.getPdfData()), any())).thenReturn(signed);
|
||||||
|
|
||||||
|
PdfToolService.SignedInvoice result = service.sign(invoice);
|
||||||
|
|
||||||
|
assertThat(result).isSameAs(signed);
|
||||||
|
verify(pdfToolService).signInvoice(eq(invoice.getPdfData()), metadataCaptor.capture());
|
||||||
|
PdfToolService.InvoiceMetadata metadata = metadataCaptor.getValue();
|
||||||
|
assertThat(metadata.invoiceNumber()).isEqualTo("RE-2026-0001");
|
||||||
|
assertThat(metadata.issueDate()).isEqualTo(LocalDate.of(2026, 7, 14));
|
||||||
|
assertThat(metadata.deliveryDate()).isEqualTo(LocalDate.of(2026, 7, 10));
|
||||||
|
assertThat(metadata.dueDate()).isEqualTo(LocalDate.of(2026, 7, 28));
|
||||||
|
assertThat(metadata.currency()).isEqualTo("EUR");
|
||||||
|
assertThat(metadata.iban()).isEqualTo("DE02120300000000202051");
|
||||||
|
assertThat(metadata.sender().name()).isEqualTo("Spedition Muster GmbH");
|
||||||
|
assertThat(metadata.sender().vatId()).isEqualTo("DE123456789");
|
||||||
|
assertThat(metadata.recipient().name()).isEqualTo("Kunde AG");
|
||||||
|
assertThat(metadata.recipient().zip()).isEqualTo("10115");
|
||||||
|
assertThat(metadata.recipient().email()).isEqualTo("rechnung@kunde-ag.de");
|
||||||
|
assertThat(metadata.items()).hasSize(2);
|
||||||
|
assertThat(metadata.items().get(0).description()).isEqualTo("Transport");
|
||||||
|
assertThat(metadata.items().get(0).quantity()).isEqualByComparingTo("12");
|
||||||
|
assertThat(metadata.items().get(0).unitPriceNet()).isEqualByComparingTo("5.00");
|
||||||
|
assertThat(metadata.items().get(0).vatPercent()).isEqualByComparingTo("19");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void signFallsBackToSingleLineForLegacyInvoiceWithoutItems() throws Exception {
|
||||||
|
CustomerInvoice invoice = fullyPopulatedInvoice();
|
||||||
|
invoice.setItems(null);
|
||||||
|
invoice.setDescription("Kurierfahrt Hamburg–Berlin");
|
||||||
|
when(pdfToolService.signInvoice(any(), any()))
|
||||||
|
.thenReturn(new PdfToolService.SignedInvoice(new byte[] { 9 }, "re.zip", true));
|
||||||
|
|
||||||
|
service.sign(invoice);
|
||||||
|
|
||||||
|
verify(pdfToolService).signInvoice(any(), metadataCaptor.capture());
|
||||||
|
List<PdfToolService.LineItem> items = metadataCaptor.getValue().items();
|
||||||
|
assertThat(items).hasSize(1);
|
||||||
|
assertThat(items.get(0).description()).isEqualTo("Kurierfahrt Hamburg–Berlin");
|
||||||
|
assertThat(items.get(0).quantity()).isEqualByComparingTo("1");
|
||||||
|
assertThat(items.get(0).unitPriceNet()).isEqualByComparingTo("100.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void signUsesDefaultDatesWhenMissing() throws Exception {
|
||||||
|
CustomerInvoice invoice = fullyPopulatedInvoice();
|
||||||
|
invoice.setDeliveryDate(null);
|
||||||
|
invoice.setPaymentDueDate(null);
|
||||||
|
when(pdfToolService.signInvoice(any(), any()))
|
||||||
|
.thenReturn(new PdfToolService.SignedInvoice(new byte[] { 9 }, "re.zip", true));
|
||||||
|
|
||||||
|
service.sign(invoice);
|
||||||
|
|
||||||
|
verify(pdfToolService).signInvoice(any(), metadataCaptor.capture());
|
||||||
|
PdfToolService.InvoiceMetadata metadata = metadataCaptor.getValue();
|
||||||
|
assertThat(metadata.deliveryDate()).isEqualTo(invoice.getInvoiceDate());
|
||||||
|
assertThat(metadata.dueDate()).isEqualTo(invoice.getInvoiceDate().plusDays(14));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void signRejectsInvoiceWithoutPdf() {
|
||||||
|
CustomerInvoice invoice = fullyPopulatedInvoice();
|
||||||
|
invoice.setPdfData(null);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.sign(invoice)).isInstanceOf(IllegalStateException.class)
|
||||||
|
.hasMessageContaining("kein PDF");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void signRejectsInvoiceWithoutParties() {
|
||||||
|
CustomerInvoice invoice = fullyPopulatedInvoice();
|
||||||
|
invoice.setSenderName(null);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.sign(invoice)).isInstanceOf(IllegalStateException.class)
|
||||||
|
.hasMessageContaining("Absender");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void signRejectsInvoiceWithoutAnyAmount() {
|
||||||
|
CustomerInvoice invoice = fullyPopulatedInvoice();
|
||||||
|
invoice.setItems(null);
|
||||||
|
invoice.setNetAmount(null);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.sign(invoice)).isInstanceOf(IllegalStateException.class)
|
||||||
|
.hasMessageContaining("Positionen");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user