From 4e411c727e27241b0798b93de5e90c0c0224722e Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Fri, 10 Jul 2026 11:28:58 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20ZUGFeRD-E-Rechnung=20=C3=BCber=20extern?= =?UTF-8?q?en=20PDF-Tool-Service=20mit=20ZIP-Download=20und=20Versand=20au?= =?UTF-8?q?s=20der=20PDF-Vorschau?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../pages/view/AdminCreateInvoicesView.java | 156 ++++++++++++++++-- .../votianlt/service/PdfToolService.java | 114 +++++++++++++ .../src/main/resources/application.properties | 3 + .../src/main/resources/messages_de.properties | 6 + .../src/main/resources/messages_ee.properties | 6 + .../src/main/resources/messages_en.properties | 6 + .../src/main/resources/messages_es.properties | 6 + .../src/main/resources/messages_fr.properties | 6 + .../src/main/resources/messages_lt.properties | 6 + .../src/main/resources/messages_lv.properties | 6 + .../src/main/resources/messages_pl.properties | 6 + .../src/main/resources/messages_ru.properties | 6 + .../src/main/resources/messages_tr.properties | 6 + 13 files changed, 322 insertions(+), 11 deletions(-) create mode 100644 backend/src/main/java/de/assecutor/votianlt/service/PdfToolService.java diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/AdminCreateInvoicesView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/AdminCreateInvoicesView.java index 43117f6..d56ae75 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/AdminCreateInvoicesView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/AdminCreateInvoicesView.java @@ -34,6 +34,7 @@ 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.PdfToolService; import de.assecutor.votianlt.service.SystemCompanyProfileService; import de.assecutor.votianlt.util.DateTimeFormatUtil; import jakarta.annotation.security.RolesAllowed; @@ -76,6 +77,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle { private final SystemCompanyProfileService systemCompanyProfileService; private final EmailService emailService; private final SystemInvoiceDispatchRepository dispatchRepository; + private final PdfToolService pdfToolService; private final Grid grid = new Grid<>(); @@ -99,7 +101,8 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle { public AdminCreateInvoicesView(UserRepository userRepository, PriceTableRepository priceTableRepository, AppUserRepository appUserRepository, InvoiceTemplateService invoiceTemplateService, CustomerInvoiceService customerInvoiceService, SystemCompanyProfileService systemCompanyProfileService, - EmailService emailService, SystemInvoiceDispatchRepository dispatchRepository) { + EmailService emailService, SystemInvoiceDispatchRepository dispatchRepository, + PdfToolService pdfToolService) { this.userRepository = userRepository; this.priceTableRepository = priceTableRepository; this.appUserRepository = appUserRepository; @@ -108,6 +111,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle { this.systemCompanyProfileService = systemCompanyProfileService; this.emailService = emailService; this.dispatchRepository = dispatchRepository; + this.pdfToolService = pdfToolService; setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, @@ -427,7 +431,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle { try { byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData, buildInvoiceVariables(info), null); - showPdfInDialog(pdfBytes, buildInvoiceNumber(info.user())); + showPdfInDialog(new InvoicePreview(info, buildInvoiceNumber(info.user()), pdfBytes)); } catch (Exception ex) { log.error("Error creating system invoice for user {}: {}", info.user().getEmail(), ex.getMessage(), ex); Notification @@ -494,6 +498,8 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle { nextButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); Button deleteButton = new Button(getTranslation("admincreateinvoices.dialog.button.delete")); deleteButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR); + Button zugferdButton = new Button(getTranslation("admincreateinvoices.button.zugferd")); + zugferdButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); Button sendButton = new Button(getTranslation("admincreateinvoices.dialog.button.send")); sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); Button closeButton = new Button(getTranslation("button.close"), e -> dialog.close()); @@ -532,6 +538,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle { index[0] = Math.min(index[0], previews.size() - 1); updateView.run(); }); + zugferdButton.addClickListener(e -> downloadZugferdZip(previews.get(index[0]))); sendButton.addClickListener(e -> { sendButton.setEnabled(false); sendInvoices(previews); @@ -540,7 +547,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle { updateView.run(); dialog.add(DialogStylingHelper.wrapContent(content, true)); - dialog.getFooter().add(previousButton, nextButton, deleteButton, sendButton, closeButton); + dialog.getFooter().add(previousButton, nextButton, deleteButton, zugferdButton, sendButton, closeButton); dialog.open(); } @@ -692,7 +699,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle { return "SYS-" + monthPart + "-" + userPart; } - private void showPdfInDialog(byte[] pdfBytes, String invoiceNumber) { + private void showPdfInDialog(InvoicePreview preview) { Dialog pdfDialog = DialogStylingHelper.createStyledDialog(getTranslation("invoicegenerator.pdf.preview.title"), "90vw"); pdfDialog.setHeight("90vh"); @@ -701,7 +708,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle { pdfContainer.setWidth("100%"); pdfContainer.setHeight("100%"); - String base64Pdf = Base64.getEncoder().encodeToString(pdfBytes); + String base64Pdf = Base64.getEncoder().encodeToString(preview.pdf()); IFrame pdfFrame = new IFrame(); pdfFrame.setWidth("100%"); @@ -710,18 +717,145 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle { pdfContainer.add(pdfFrame); Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close()); - closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - Button downloadButton = new Button(getTranslation("button.download"), e -> getElement() - .executeJs("const link = document.createElement('a');" + "link.href = 'data:application/pdf;base64," - + base64Pdf + "';" + "link.download = '" + invoiceNumber + ".pdf';" + "link.click();")); - downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + Button zugferdButton = new Button(getTranslation("admincreateinvoices.button.zugferd"), + e -> downloadZugferdZip(preview)); + zugferdButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + Button sendButton = new Button(getTranslation("admincreateinvoices.pdf.button.send"), e -> { + e.getSource().setEnabled(false); + sendInvoices(List.of(preview)); + pdfDialog.close(); + }); + sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); pdfDialog.add(DialogStylingHelper.wrapContent(pdfContainer, true)); - pdfDialog.getFooter().add(downloadButton, closeButton); + pdfDialog.getFooter().add(zugferdButton, sendButton, closeButton); pdfDialog.open(); } + // ========================================== + // ZUGFeRD e-invoice (external PDF Tool) + // ========================================== + + /** + * Converts the invoice via the external PDF Tool into a ZUGFeRD e-invoice + * and offers the resulting ZIP (signed PDF/A-3, factur-x.xml, validation + * report) as browser download. + */ + private void downloadZugferdZip(InvoicePreview preview) { + try { + PdfToolService.SignedInvoice signed = pdfToolService.signInvoice(preview.pdf(), + buildZugferdMetadata(preview)); + String base64Zip = Base64.getEncoder().encodeToString(signed.zip()); + 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()) { + Notification + .show(getTranslation("admincreateinvoices.notification.zugferd.created", + preview.invoiceNumber()), 5000, Notification.Position.BOTTOM_CENTER) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + } else { + Notification + .show(getTranslation("admincreateinvoices.notification.zugferd.invalid"), 7000, + Notification.Position.BOTTOM_CENTER) + .addThemeVariants(NotificationVariant.LUMO_WARNING); + } + } catch (Exception ex) { + log.error("Error creating ZUGFeRD e-invoice {}: {}", preview.invoiceNumber(), ex.getMessage(), ex); + Notification + .show(getTranslation("admincreateinvoices.notification.zugferd.error", ex.getMessage()), 7000, + Notification.Position.BOTTOM_CENTER) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + } + } + + /** + * Builds the EN16931 metadata for the PDF Tool from the billing data: the + * system operator as sender, the customer as recipient and the billed + * positions (including a possible negative discount position) as lines. + */ + private PdfToolService.InvoiceMetadata buildZugferdMetadata(InvoicePreview preview) throws Exception { + SystemInvoiceData issuer = systemCompanyProfileService.createSystemInvoiceData(); + BillingInfo info = preview.info(); + User user = info.user(); + + String[] senderZipCity = splitZipCity(issuer.getCompanyCity()); + String senderName = (safe(issuer.getCompanyName()) + " " + safe(issuer.getCompanySubtitle())).trim(); + String footer = safe(issuer.getFooterText()); + PdfToolService.Party sender = new PdfToolService.Party(senderName, safe(issuer.getCompanyStreet()), + senderZipCity[0], senderZipCity[1], "DE", extractByPattern(footer, "USt-IdNr\\.?:?\\s*([A-Z]{2}[0-9A-Za-z]{2,12})"), + safe(issuer.getCompanyEmail()), safe(issuer.getCompanyPhone())); + + PdfToolService.Party recipient; + if (user.isDiffInvoiceAddress()) { + recipient = new PdfToolService.Party( + firstNonBlank(user.getInvCompany(), user.getCompany(), + (safe(user.getInvFirstname()) + " " + safe(user.getInvLastname())).trim()), + (safe(user.getInvStreet()) + " " + safe(user.getInvHouseNumber())).trim(), safe(user.getInvZip()), + safe(user.getInvCity()), "DE", null, safe(user.getEmail()), safe(user.getPhone())); + } else { + recipient = new PdfToolService.Party( + firstNonBlank(user.getCompany(), (safe(user.getFirstname()) + " " + safe(user.getName())).trim()), + (safe(user.getStreet()) + " " + safe(user.getHouseNumber())).trim(), safe(user.getZip()), + safe(user.getCity()), "DE", null, safe(user.getEmail()), safe(user.getPhone())); + } + + BigDecimal vatPercent = SYSTEM_VAT_RATE.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP); + List items = new ArrayList<>(); + for (Map position : info.positions()) { + BigDecimal amount = parseSignedAmount(position.get("netAmount")); + if (amount == null) { + // Non-monetary positions cannot be part of an EN16931 invoice + continue; + } + items.add(new PdfToolService.LineItem(position.get("name"), BigDecimal.ONE, amount, vatPercent)); + } + if (items.isEmpty()) { + throw new IllegalStateException(getTranslation("admincreateinvoices.notification.zugferd.nopositions")); + } + + return new PdfToolService.InvoiceMetadata(preview.invoiceNumber(), LocalDate.now(), + billingMonth.atEndOfMonth(), info.dueDate(), "EUR", safe(issuer.getPaymentTerms()), null, + extractByPattern(footer, "IBAN\\s*:?\\s*([A-Z]{2}[0-9A-Za-z]{13,32})"), + extractByPattern(footer, "BIC\\s*:?\\s*([A-Za-z0-9]{8,11})"), sender, recipient, items); + } + + /** + * Parses a formatted position amount like "99,00" or "-49,50" including + * its sign. Returns null for non-monetary values such as "-" or "15 %". + */ + private BigDecimal parseSignedAmount(String value) { + if (value == null || value.isBlank() || value.contains("%")) { + return null; + } + boolean negative = value.trim().startsWith("-"); + BigDecimal amount = parseAmount(value.trim().replaceFirst("^-", "")); + if (amount == null) { + return null; + } + return negative ? amount.negate() : amount; + } + + /** Splits a combined "zip city" value like "21502 Geesthacht". */ + private String[] splitZipCity(String zipCity) { + String trimmed = safe(zipCity).trim(); + int space = trimmed.indexOf(' '); + if (space < 0) { + return new String[] { trimmed, trimmed }; + } + return new String[] { trimmed.substring(0, space), trimmed.substring(space + 1).trim() }; + } + + /** Returns the first regex group found in the text, or null. */ + private String extractByPattern(String text, String pattern) { + Matcher matcher = Pattern.compile(pattern).matcher(text); + return matcher.find() ? matcher.group(1) : null; + } + // ========================================== // Helpers // ========================================== diff --git a/backend/src/main/java/de/assecutor/votianlt/service/PdfToolService.java b/backend/src/main/java/de/assecutor/votianlt/service/PdfToolService.java new file mode 100644 index 0000000..98ee522 --- /dev/null +++ b/backend/src/main/java/de/assecutor/votianlt/service/PdfToolService.java @@ -0,0 +1,114 @@ +package de.assecutor.votianlt.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.MultipartBodyBuilder; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientResponseException; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * Client for the external PDF Tool (ZUGFeRD converter). Sends an invoice PDF + * together with the structured invoice data to {@code POST /api/invoices/sign} + * and receives a ZIP file containing the signed e-invoice (PDF/A-3 with + * embedded EN16931 XML), the factur-x.xml and the Mustang validation report. + */ +@Service +@Slf4j +public class PdfToolService { + + /** Response header of the PDF Tool with the Mustang validation result. */ + private static final String VALIDATION_HEADER = "X-Zugferd-Valid"; + + private final RestClient restClient; + private final ObjectMapper objectMapper; + + public PdfToolService(@Value("${app.pdftool.base-url:http://localhost:8083}") String baseUrl, + ObjectMapper objectMapper) { + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(5000); + // PDF/A conversion and veraPDF validation can take a while + requestFactory.setReadTimeout(120000); + this.restClient = RestClient.builder().baseUrl(baseUrl).requestFactory(requestFactory).build(); + this.objectMapper = objectMapper; + } + + /** Party (sender or recipient) of the invoice metadata. */ + public record Party(String name, String street, String zip, String city, String countryCode, String vatId, + String email, String phone) { + } + + /** One invoice line of the invoice metadata. */ + public record LineItem(String description, BigDecimal quantity, BigDecimal unitPriceNet, BigDecimal vatPercent) { + } + + /** Invoice metadata as expected by the PDF Tool (EN16931 fields). */ + public record InvoiceMetadata(String invoiceNumber, LocalDate issueDate, LocalDate deliveryDate, LocalDate dueDate, + String currency, String paymentTerms, String buyerReference, String iban, String bic, Party sender, + Party recipient, List items) { + } + + /** Result of a sign call: the ZIP, its file name and the validation flag. */ + public record SignedInvoice(byte[] zip, String fileName, boolean valid) { + } + + /** + * 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 + * conversion is rejected (e.g. incomplete metadata). + */ + public SignedInvoice signInvoice(byte[] pdfBytes, InvoiceMetadata metadata) throws Exception { + String metadataJson = objectMapper.writeValueAsString(metadata); + + MultipartBodyBuilder body = new MultipartBodyBuilder(); + body.part("file", new ByteArrayResource(pdfBytes) { + @Override + public String getFilename() { + return metadata.invoiceNumber() + ".pdf"; + } + }, MediaType.APPLICATION_PDF); + body.part("metadata", metadataJson, MediaType.APPLICATION_JSON); + + ResponseEntity response; + try { + response = restClient.post().uri("/api/invoices/sign").contentType(MediaType.MULTIPART_FORM_DATA) + .body(body.build()).retrieve().toEntity(byte[].class); + } catch (RestClientResponseException ex) { + throw new IllegalStateException(extractErrorMessage(ex), ex); + } + + byte[] zip = response.getBody(); + if (zip == null || zip.length == 0) { + throw new IllegalStateException("PDF Tool returned an empty response"); + } + String fileName = response.getHeaders().getContentDisposition().getFilename(); + if (fileName == null || fileName.isBlank()) { + fileName = metadata.invoiceNumber() + "-zugferd.zip"; + } + boolean valid = Boolean.parseBoolean(response.getHeaders().getFirst(VALIDATION_HEADER)); + return new SignedInvoice(zip, fileName, valid); + } + + /** The PDF Tool reports errors as {@code {"error": "..."}} with HTTP 400. */ + private String extractErrorMessage(RestClientResponseException ex) { + try { + JsonNode node = objectMapper.readTree(ex.getResponseBodyAsString()); + if (node.hasNonNull("error")) { + return node.get("error").asText(); + } + } catch (Exception ignored) { + // fall through to the generic message + } + return ex.getStatusText() + " (" + ex.getStatusCode().value() + ")"; + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 5d6336a..c90250a 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -75,6 +75,9 @@ app.version=@project.version@ # Google Maps API Key app.google.maps.api-key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE +# PDF Tool (ZUGFeRD-E-Rechnungs-Konverter), Basis-URL des externen Service +app.pdftool.base-url=${PDFTOOL_BASE_URL:http://localhost:8083} + # =========================================== # LLM Configuration (Anthropic Claude) # =========================================== diff --git a/backend/src/main/resources/messages_de.properties b/backend/src/main/resources/messages_de.properties index b9da999..a451728 100644 --- a/backend/src/main/resources/messages_de.properties +++ b/backend/src/main/resources/messages_de.properties @@ -1142,8 +1142,14 @@ 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.pdf.button.send=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 +admincreateinvoices.button.zugferd=E-Rechnung (ZIP) +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.error=Fehler beim Erstellen der E-Rechnung: {0} +admincreateinvoices.notification.zugferd.nopositions=Keine abrechenbaren Positionen für die E-Rechnung vorhanden diff --git a/backend/src/main/resources/messages_ee.properties b/backend/src/main/resources/messages_ee.properties index 04a8fd1..33b3ee4 100644 --- a/backend/src/main/resources/messages_ee.properties +++ b/backend/src/main/resources/messages_ee.properties @@ -972,8 +972,14 @@ admincreateinvoices.dialog.button.previous=Tagasi admincreateinvoices.dialog.button.next=Edasi admincreateinvoices.dialog.button.delete=Eemalda loendist admincreateinvoices.dialog.button.send=Saada arved +admincreateinvoices.pdf.button.send=Saada 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 +admincreateinvoices.button.zugferd=E-arve (ZIP) +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.error=Viga e-arve loomisel: {0} +admincreateinvoices.notification.zugferd.nopositions=E-arve jaoks puuduvad arveldatavad positsioonid diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties index 66b06ea..a27e4bd 100644 --- a/backend/src/main/resources/messages_en.properties +++ b/backend/src/main/resources/messages_en.properties @@ -1142,8 +1142,14 @@ admincreateinvoices.dialog.button.previous=Back admincreateinvoices.dialog.button.next=Next admincreateinvoices.dialog.button.delete=Remove from list admincreateinvoices.dialog.button.send=Send invoices +admincreateinvoices.pdf.button.send=Send 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 +admincreateinvoices.button.zugferd=E-invoice (ZIP) +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.error=Error creating the e-invoice: {0} +admincreateinvoices.notification.zugferd.nopositions=No billable positions available for the e-invoice diff --git a/backend/src/main/resources/messages_es.properties b/backend/src/main/resources/messages_es.properties index 866e08f..264d817 100644 --- a/backend/src/main/resources/messages_es.properties +++ b/backend/src/main/resources/messages_es.properties @@ -1073,8 +1073,14 @@ 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.pdf.button.send=Enviar 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) +admincreateinvoices.button.zugferd=Factura electrónica (ZIP) +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.error=Error al crear la factura electrónica: {0} +admincreateinvoices.notification.zugferd.nopositions=No hay posiciones facturables disponibles para la factura electrónica diff --git a/backend/src/main/resources/messages_fr.properties b/backend/src/main/resources/messages_fr.properties index 2d32c29..3f8b69d 100644 --- a/backend/src/main/resources/messages_fr.properties +++ b/backend/src/main/resources/messages_fr.properties @@ -1073,8 +1073,14 @@ 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.pdf.button.send=Envoyer 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 +admincreateinvoices.button.zugferd=Facture électronique (ZIP) +admincreateinvoices.notification.zugferd.created=Facture électronique {0} créée +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.nopositions=Aucune position facturable disponible pour la facture électronique diff --git a/backend/src/main/resources/messages_lt.properties b/backend/src/main/resources/messages_lt.properties index 79e4e28..00d6730 100644 --- a/backend/src/main/resources/messages_lt.properties +++ b/backend/src/main/resources/messages_lt.properties @@ -1075,8 +1075,14 @@ 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.pdf.button.send=Siųsti 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} +admincreateinvoices.button.zugferd=E. sąskaita (ZIP) +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.error=Klaida kuriant e. sąskaitą: {0} +admincreateinvoices.notification.zugferd.nopositions=E. sąskaitai nėra apmokestinamų pozicijų diff --git a/backend/src/main/resources/messages_lv.properties b/backend/src/main/resources/messages_lv.properties index 019a771..4322be8 100644 --- a/backend/src/main/resources/messages_lv.properties +++ b/backend/src/main/resources/messages_lv.properties @@ -1073,8 +1073,14 @@ 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.pdf.button.send=Sūtīt 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} +admincreateinvoices.button.zugferd=E-rēķins (ZIP) +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.error=Kļūda, veidojot e-rēķinu: {0} +admincreateinvoices.notification.zugferd.nopositions=E-rēķinam nav pieejamu norēķinu pozīciju diff --git a/backend/src/main/resources/messages_pl.properties b/backend/src/main/resources/messages_pl.properties index 77f61d6..13f1068 100644 --- a/backend/src/main/resources/messages_pl.properties +++ b/backend/src/main/resources/messages_pl.properties @@ -1073,8 +1073,14 @@ 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.pdf.button.send=Wyślij 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} +admincreateinvoices.button.zugferd=E-faktura (ZIP) +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.error=Błąd podczas tworzenia e-faktury: {0} +admincreateinvoices.notification.zugferd.nopositions=Brak pozycji rozliczeniowych dla e-faktury diff --git a/backend/src/main/resources/messages_ru.properties b/backend/src/main/resources/messages_ru.properties index daf5281..59a22bf 100644 --- a/backend/src/main/resources/messages_ru.properties +++ b/backend/src/main/resources/messages_ru.properties @@ -1073,8 +1073,14 @@ admincreateinvoices.dialog.button.previous=Назад admincreateinvoices.dialog.button.next=Далее admincreateinvoices.dialog.button.delete=Удалить из списка admincreateinvoices.dialog.button.send=Отправить счета +admincreateinvoices.pdf.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} +admincreateinvoices.button.zugferd=Электронный счёт (ZIP) +admincreateinvoices.notification.zugferd.created=Электронный счёт {0} создан +admincreateinvoices.notification.zugferd.invalid=Электронный счёт создан, но проверка не пройдена. Подробности в отчёте о проверке в ZIP-файле. +admincreateinvoices.notification.zugferd.error=Ошибка при создании электронного счёта: {0} +admincreateinvoices.notification.zugferd.nopositions=Нет позиций для выставления электронного счёта diff --git a/backend/src/main/resources/messages_tr.properties b/backend/src/main/resources/messages_tr.properties index 8cc615f..ecec7bf 100644 --- a/backend/src/main/resources/messages_tr.properties +++ b/backend/src/main/resources/messages_tr.properties @@ -1073,8 +1073,14 @@ 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.pdf.button.send=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 +admincreateinvoices.button.zugferd=E-fatura (ZIP) +admincreateinvoices.notification.zugferd.created={0} numaralı e-fatura oluşturuldu +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.nopositions=E-fatura için faturalandırılabilir pozisyon yok