feat: ZUGFeRD-E-Rechnung über externen PDF-Tool-Service mit ZIP-Download und Versand aus der PDF-Vorschau
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ import de.assecutor.votianlt.repository.UserRepository;
|
|||||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||||
import de.assecutor.votianlt.service.EmailService;
|
import de.assecutor.votianlt.service.EmailService;
|
||||||
import de.assecutor.votianlt.service.InvoiceTemplateService;
|
import de.assecutor.votianlt.service.InvoiceTemplateService;
|
||||||
|
import de.assecutor.votianlt.service.PdfToolService;
|
||||||
import de.assecutor.votianlt.service.SystemCompanyProfileService;
|
import de.assecutor.votianlt.service.SystemCompanyProfileService;
|
||||||
import de.assecutor.votianlt.util.DateTimeFormatUtil;
|
import de.assecutor.votianlt.util.DateTimeFormatUtil;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
@@ -76,6 +77,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
private final SystemCompanyProfileService systemCompanyProfileService;
|
private final SystemCompanyProfileService systemCompanyProfileService;
|
||||||
private final EmailService emailService;
|
private final EmailService emailService;
|
||||||
private final SystemInvoiceDispatchRepository dispatchRepository;
|
private final SystemInvoiceDispatchRepository dispatchRepository;
|
||||||
|
private final PdfToolService pdfToolService;
|
||||||
|
|
||||||
private final Grid<BillingInfo> grid = new Grid<>();
|
private final Grid<BillingInfo> grid = new Grid<>();
|
||||||
|
|
||||||
@@ -99,7 +101,8 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
public AdminCreateInvoicesView(UserRepository userRepository, PriceTableRepository priceTableRepository,
|
public AdminCreateInvoicesView(UserRepository userRepository, PriceTableRepository priceTableRepository,
|
||||||
AppUserRepository appUserRepository, InvoiceTemplateService invoiceTemplateService,
|
AppUserRepository appUserRepository, InvoiceTemplateService invoiceTemplateService,
|
||||||
CustomerInvoiceService customerInvoiceService, SystemCompanyProfileService systemCompanyProfileService,
|
CustomerInvoiceService customerInvoiceService, SystemCompanyProfileService systemCompanyProfileService,
|
||||||
EmailService emailService, SystemInvoiceDispatchRepository dispatchRepository) {
|
EmailService emailService, SystemInvoiceDispatchRepository dispatchRepository,
|
||||||
|
PdfToolService pdfToolService) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.priceTableRepository = priceTableRepository;
|
this.priceTableRepository = priceTableRepository;
|
||||||
this.appUserRepository = appUserRepository;
|
this.appUserRepository = appUserRepository;
|
||||||
@@ -108,6 +111,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
this.systemCompanyProfileService = systemCompanyProfileService;
|
this.systemCompanyProfileService = systemCompanyProfileService;
|
||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
this.dispatchRepository = dispatchRepository;
|
this.dispatchRepository = dispatchRepository;
|
||||||
|
this.pdfToolService = pdfToolService;
|
||||||
|
|
||||||
setSizeFull();
|
setSizeFull();
|
||||||
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
|
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
|
||||||
@@ -427,7 +431,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
try {
|
try {
|
||||||
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData,
|
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData,
|
||||||
buildInvoiceVariables(info), null);
|
buildInvoiceVariables(info), null);
|
||||||
showPdfInDialog(pdfBytes, buildInvoiceNumber(info.user()));
|
showPdfInDialog(new InvoicePreview(info, buildInvoiceNumber(info.user()), pdfBytes));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("Error creating system invoice for user {}: {}", info.user().getEmail(), ex.getMessage(), ex);
|
log.error("Error creating system invoice for user {}: {}", info.user().getEmail(), ex.getMessage(), ex);
|
||||||
Notification
|
Notification
|
||||||
@@ -494,6 +498,8 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
nextButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
nextButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
Button deleteButton = new Button(getTranslation("admincreateinvoices.dialog.button.delete"));
|
Button deleteButton = new Button(getTranslation("admincreateinvoices.dialog.button.delete"));
|
||||||
deleteButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR);
|
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"));
|
Button sendButton = new Button(getTranslation("admincreateinvoices.dialog.button.send"));
|
||||||
sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
Button closeButton = new Button(getTranslation("button.close"), e -> dialog.close());
|
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);
|
index[0] = Math.min(index[0], previews.size() - 1);
|
||||||
updateView.run();
|
updateView.run();
|
||||||
});
|
});
|
||||||
|
zugferdButton.addClickListener(e -> downloadZugferdZip(previews.get(index[0])));
|
||||||
sendButton.addClickListener(e -> {
|
sendButton.addClickListener(e -> {
|
||||||
sendButton.setEnabled(false);
|
sendButton.setEnabled(false);
|
||||||
sendInvoices(previews);
|
sendInvoices(previews);
|
||||||
@@ -540,7 +547,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
updateView.run();
|
updateView.run();
|
||||||
|
|
||||||
dialog.add(DialogStylingHelper.wrapContent(content, true));
|
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();
|
dialog.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,7 +699,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
return "SYS-" + monthPart + "-" + userPart;
|
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"),
|
Dialog pdfDialog = DialogStylingHelper.createStyledDialog(getTranslation("invoicegenerator.pdf.preview.title"),
|
||||||
"90vw");
|
"90vw");
|
||||||
pdfDialog.setHeight("90vh");
|
pdfDialog.setHeight("90vh");
|
||||||
@@ -701,7 +708,7 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
pdfContainer.setWidth("100%");
|
pdfContainer.setWidth("100%");
|
||||||
pdfContainer.setHeight("100%");
|
pdfContainer.setHeight("100%");
|
||||||
|
|
||||||
String base64Pdf = Base64.getEncoder().encodeToString(pdfBytes);
|
String base64Pdf = Base64.getEncoder().encodeToString(preview.pdf());
|
||||||
|
|
||||||
IFrame pdfFrame = new IFrame();
|
IFrame pdfFrame = new IFrame();
|
||||||
pdfFrame.setWidth("100%");
|
pdfFrame.setWidth("100%");
|
||||||
@@ -710,18 +717,145 @@ public class AdminCreateInvoicesView extends Main implements HasDynamicTitle {
|
|||||||
pdfContainer.add(pdfFrame);
|
pdfContainer.add(pdfFrame);
|
||||||
|
|
||||||
Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close());
|
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()
|
Button zugferdButton = new Button(getTranslation("admincreateinvoices.button.zugferd"),
|
||||||
.executeJs("const link = document.createElement('a');" + "link.href = 'data:application/pdf;base64,"
|
e -> downloadZugferdZip(preview));
|
||||||
+ base64Pdf + "';" + "link.download = '" + invoiceNumber + ".pdf';" + "link.click();"));
|
zugferdButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
downloadButton.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.add(DialogStylingHelper.wrapContent(pdfContainer, true));
|
||||||
pdfDialog.getFooter().add(downloadButton, closeButton);
|
pdfDialog.getFooter().add(zugferdButton, sendButton, closeButton);
|
||||||
pdfDialog.open();
|
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<PdfToolService.LineItem> items = new ArrayList<>();
|
||||||
|
for (Map<String, String> 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
|
// Helpers
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -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<LineItem> 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<byte[]> 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() + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,6 +75,9 @@ app.version=@project.version@
|
|||||||
# Google Maps API Key
|
# Google Maps API Key
|
||||||
app.google.maps.api-key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE
|
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)
|
# LLM Configuration (Anthropic Claude)
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
@@ -1142,8 +1142,14 @@ admincreateinvoices.dialog.button.previous=Zurück
|
|||||||
admincreateinvoices.dialog.button.next=Weiter
|
admincreateinvoices.dialog.button.next=Weiter
|
||||||
admincreateinvoices.dialog.button.delete=Aus Liste entfernen
|
admincreateinvoices.dialog.button.delete=Aus Liste entfernen
|
||||||
admincreateinvoices.dialog.button.send=Rechnungen senden
|
admincreateinvoices.dialog.button.send=Rechnungen senden
|
||||||
|
admincreateinvoices.pdf.button.send=Senden
|
||||||
admincreateinvoices.dialog.notification.removed=Rechnung aus der Liste entfernt
|
admincreateinvoices.dialog.notification.removed=Rechnung aus der Liste entfernt
|
||||||
admincreateinvoices.mail.subject=Ihre Rechnung {0}
|
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.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.sent={0} Rechnung(en) per E-Mail versendet
|
||||||
admincreateinvoices.notification.sendfailed={0} Rechnung(en) konnten nicht versendet werden
|
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
|
||||||
|
|||||||
@@ -972,8 +972,14 @@ admincreateinvoices.dialog.button.previous=Tagasi
|
|||||||
admincreateinvoices.dialog.button.next=Edasi
|
admincreateinvoices.dialog.button.next=Edasi
|
||||||
admincreateinvoices.dialog.button.delete=Eemalda loendist
|
admincreateinvoices.dialog.button.delete=Eemalda loendist
|
||||||
admincreateinvoices.dialog.button.send=Saada arved
|
admincreateinvoices.dialog.button.send=Saada arved
|
||||||
|
admincreateinvoices.pdf.button.send=Saada
|
||||||
admincreateinvoices.dialog.notification.removed=Arve eemaldati loendist
|
admincreateinvoices.dialog.notification.removed=Arve eemaldati loendist
|
||||||
admincreateinvoices.mail.subject=Teie arve {0}
|
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.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.sent={0} arve(t) saadeti e-postiga
|
||||||
admincreateinvoices.notification.sendfailed={0} arve(t) ei õnnestunud saata
|
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
|
||||||
|
|||||||
@@ -1142,8 +1142,14 @@ admincreateinvoices.dialog.button.previous=Back
|
|||||||
admincreateinvoices.dialog.button.next=Next
|
admincreateinvoices.dialog.button.next=Next
|
||||||
admincreateinvoices.dialog.button.delete=Remove from list
|
admincreateinvoices.dialog.button.delete=Remove from list
|
||||||
admincreateinvoices.dialog.button.send=Send invoices
|
admincreateinvoices.dialog.button.send=Send invoices
|
||||||
|
admincreateinvoices.pdf.button.send=Send
|
||||||
admincreateinvoices.dialog.notification.removed=Invoice removed from the list
|
admincreateinvoices.dialog.notification.removed=Invoice removed from the list
|
||||||
admincreateinvoices.mail.subject=Your invoice {0}
|
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.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.sent={0} invoice(s) sent by email
|
||||||
admincreateinvoices.notification.sendfailed={0} invoice(s) could not be sent
|
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
|
||||||
|
|||||||
@@ -1073,8 +1073,14 @@ admincreateinvoices.dialog.button.previous=Atrás
|
|||||||
admincreateinvoices.dialog.button.next=Siguiente
|
admincreateinvoices.dialog.button.next=Siguiente
|
||||||
admincreateinvoices.dialog.button.delete=Quitar de la lista
|
admincreateinvoices.dialog.button.delete=Quitar de la lista
|
||||||
admincreateinvoices.dialog.button.send=Enviar facturas
|
admincreateinvoices.dialog.button.send=Enviar facturas
|
||||||
|
admincreateinvoices.pdf.button.send=Enviar
|
||||||
admincreateinvoices.dialog.notification.removed=Factura eliminada de la lista
|
admincreateinvoices.dialog.notification.removed=Factura eliminada de la lista
|
||||||
admincreateinvoices.mail.subject=Su factura {0}
|
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.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.sent={0} factura(s) enviada(s) por correo electrónico
|
||||||
admincreateinvoices.notification.sendfailed=No se pudieron enviar {0} factura(s)
|
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
|
||||||
|
|||||||
@@ -1073,8 +1073,14 @@ admincreateinvoices.dialog.button.previous=Retour
|
|||||||
admincreateinvoices.dialog.button.next=Suivant
|
admincreateinvoices.dialog.button.next=Suivant
|
||||||
admincreateinvoices.dialog.button.delete=Retirer de la liste
|
admincreateinvoices.dialog.button.delete=Retirer de la liste
|
||||||
admincreateinvoices.dialog.button.send=Envoyer les factures
|
admincreateinvoices.dialog.button.send=Envoyer les factures
|
||||||
|
admincreateinvoices.pdf.button.send=Envoyer
|
||||||
admincreateinvoices.dialog.notification.removed=Facture retirée de la liste
|
admincreateinvoices.dialog.notification.removed=Facture retirée de la liste
|
||||||
admincreateinvoices.mail.subject=Votre facture {0}
|
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.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.sent={0} facture(s) envoyée(s) par e-mail
|
||||||
admincreateinvoices.notification.sendfailed={0} facture(s) n''ont pas pu être envoyées
|
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
|
||||||
|
|||||||
@@ -1075,8 +1075,14 @@ admincreateinvoices.dialog.button.previous=Atgal
|
|||||||
admincreateinvoices.dialog.button.next=Toliau
|
admincreateinvoices.dialog.button.next=Toliau
|
||||||
admincreateinvoices.dialog.button.delete=Pašalinti iš sąrašo
|
admincreateinvoices.dialog.button.delete=Pašalinti iš sąrašo
|
||||||
admincreateinvoices.dialog.button.send=Siųsti sąskaitas
|
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.dialog.notification.removed=Sąskaita pašalinta iš sąrašo
|
||||||
admincreateinvoices.mail.subject=Jūsų sąskaita {0}
|
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.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.sent=El. paštu išsiųsta sąskaitų: {0}
|
||||||
admincreateinvoices.notification.sendfailed=Nepavyko išsiųsti 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ų
|
||||||
|
|||||||
@@ -1073,8 +1073,14 @@ admincreateinvoices.dialog.button.previous=Atpakaļ
|
|||||||
admincreateinvoices.dialog.button.next=Tālāk
|
admincreateinvoices.dialog.button.next=Tālāk
|
||||||
admincreateinvoices.dialog.button.delete=Noņemt no saraksta
|
admincreateinvoices.dialog.button.delete=Noņemt no saraksta
|
||||||
admincreateinvoices.dialog.button.send=Sūtīt rēķinus
|
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.dialog.notification.removed=Rēķins noņemts no saraksta
|
||||||
admincreateinvoices.mail.subject=Jūsu rēķins {0}
|
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.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.sent=Pa e-pastu nosūtīti rēķini: {0}
|
||||||
admincreateinvoices.notification.sendfailed=Neizdevās nosūtīt rēķinus: {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
|
||||||
|
|||||||
@@ -1073,8 +1073,14 @@ admincreateinvoices.dialog.button.previous=Wstecz
|
|||||||
admincreateinvoices.dialog.button.next=Dalej
|
admincreateinvoices.dialog.button.next=Dalej
|
||||||
admincreateinvoices.dialog.button.delete=Usuń z listy
|
admincreateinvoices.dialog.button.delete=Usuń z listy
|
||||||
admincreateinvoices.dialog.button.send=Wyślij faktury
|
admincreateinvoices.dialog.button.send=Wyślij faktury
|
||||||
|
admincreateinvoices.pdf.button.send=Wyślij
|
||||||
admincreateinvoices.dialog.notification.removed=Faktura usunięta z listy
|
admincreateinvoices.dialog.notification.removed=Faktura usunięta z listy
|
||||||
admincreateinvoices.mail.subject=Państwa faktura {0}
|
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.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.sent=Wysłano e-mailem faktur: {0}
|
||||||
admincreateinvoices.notification.sendfailed=Nie udało się wysłać 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
|
||||||
|
|||||||
@@ -1073,8 +1073,14 @@ admincreateinvoices.dialog.button.previous=Назад
|
|||||||
admincreateinvoices.dialog.button.next=Далее
|
admincreateinvoices.dialog.button.next=Далее
|
||||||
admincreateinvoices.dialog.button.delete=Удалить из списка
|
admincreateinvoices.dialog.button.delete=Удалить из списка
|
||||||
admincreateinvoices.dialog.button.send=Отправить счета
|
admincreateinvoices.dialog.button.send=Отправить счета
|
||||||
|
admincreateinvoices.pdf.button.send=Отправить
|
||||||
admincreateinvoices.dialog.notification.removed=Счет удален из списка
|
admincreateinvoices.dialog.notification.removed=Счет удален из списка
|
||||||
admincreateinvoices.mail.subject=Ваш счет {0}
|
admincreateinvoices.mail.subject=Ваш счет {0}
|
||||||
admincreateinvoices.mail.body=Уважаемые дамы и господа!\n\nВо вложении Вы найдете счет {0} за расчетный период {1}.\n\nС уважением\n{2}
|
admincreateinvoices.mail.body=Уважаемые дамы и господа!\n\nВо вложении Вы найдете счет {0} за расчетный период {1}.\n\nС уважением\n{2}
|
||||||
admincreateinvoices.notification.sent=Отправлено счетов по эл. почте: {0}
|
admincreateinvoices.notification.sent=Отправлено счетов по эл. почте: {0}
|
||||||
admincreateinvoices.notification.sendfailed=Не удалось отправить счетов: {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=Нет позиций для выставления электронного счёта
|
||||||
|
|||||||
@@ -1073,8 +1073,14 @@ admincreateinvoices.dialog.button.previous=Geri
|
|||||||
admincreateinvoices.dialog.button.next=İleri
|
admincreateinvoices.dialog.button.next=İleri
|
||||||
admincreateinvoices.dialog.button.delete=Listeden kaldır
|
admincreateinvoices.dialog.button.delete=Listeden kaldır
|
||||||
admincreateinvoices.dialog.button.send=Faturaları gönder
|
admincreateinvoices.dialog.button.send=Faturaları gönder
|
||||||
|
admincreateinvoices.pdf.button.send=Gönder
|
||||||
admincreateinvoices.dialog.notification.removed=Fatura listeden kaldırıldı
|
admincreateinvoices.dialog.notification.removed=Fatura listeden kaldırıldı
|
||||||
admincreateinvoices.mail.subject=Faturanız {0}
|
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.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.sent={0} fatura e-posta ile gönderildi
|
||||||
admincreateinvoices.notification.sendfailed={0} fatura gönderilemedi
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user