feat: Neutrale Rechnungsvorlage als Kundenstandard
This commit is contained in:
1 parent
8044d82f04
commit
8758c53e5b
16 files changed
+945
-318
No files matched your search
Binary file not shown.
@@ -7,9 +7,40 @@ window.profileInvoiceState = window.profileInvoiceState || {
|
||||
initialized: false
|
||||
};
|
||||
|
||||
window.initProfileInvoiceGenerator = function() {
|
||||
// A tab can still be hidden when Vaadin sends its selected-change response.
|
||||
// Wait for its actual layout instead of relying on fixed delays and a server callback.
|
||||
window.showProfileInvoiceTemplate = function(container, view, templateData, masterdata, vatRate) {
|
||||
if (window.pendingProfileInvoiceLoad) {
|
||||
window.pendingProfileInvoiceLoad.disconnect();
|
||||
window.pendingProfileInvoiceLoad = null;
|
||||
}
|
||||
function loadWhenVisible() {
|
||||
var rect = container.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) return false;
|
||||
window.invoiceGeneratorViewProfile = view;
|
||||
window.profileInvoiceServiceData = null;
|
||||
window.profileInvoiceVatRate = vatRate;
|
||||
window.masterdataValues = masterdata;
|
||||
window.initProfileInvoiceGenerator(container);
|
||||
window.loadProfileTemplate(templateData);
|
||||
return true;
|
||||
}
|
||||
if (!loadWhenVisible()) {
|
||||
var observer = new ResizeObserver(function() {
|
||||
if (window.pendingProfileInvoiceLoad !== observer) return;
|
||||
if (loadWhenVisible()) {
|
||||
observer.disconnect();
|
||||
if (window.pendingProfileInvoiceLoad === observer) window.pendingProfileInvoiceLoad = null;
|
||||
}
|
||||
});
|
||||
window.pendingProfileInvoiceLoad = observer;
|
||||
observer.observe(container);
|
||||
}
|
||||
};
|
||||
|
||||
window.initProfileInvoiceGenerator = function(container) {
|
||||
var containerId = 'invoice-canvas-container-profile';
|
||||
var container = document.getElementById(containerId);
|
||||
container = container || document.getElementById(containerId);
|
||||
|
||||
if (!container) {
|
||||
console.error('Canvas container not found:', containerId);
|
||||
@@ -1303,7 +1334,7 @@ window.initProfileInvoiceGenerator = function() {
|
||||
} else if (el.variable.startsWith('masterdata.')) {
|
||||
// Masterdata variables - use actual value from masterdata or placeholder
|
||||
var value = masterdata[el.variable];
|
||||
if (value && value.trim() !== '') {
|
||||
if (typeof value === 'string') {
|
||||
el.text = value;
|
||||
} else {
|
||||
el.text = '[' + el.variable.replace('masterdata.', '') + ']';
|
||||
|
||||
@@ -33,6 +33,7 @@ import de.assecutor.votianlt.repository.ServiceRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import de.assecutor.votianlt.security.SecurityService;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceTemplateVariables;
|
||||
import de.assecutor.votianlt.service.EmailService;
|
||||
import de.assecutor.votianlt.service.InvoiceLifecycleException;
|
||||
import de.assecutor.votianlt.service.InvoiceLifecycleService;
|
||||
@@ -546,15 +547,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
|
||||
currentUser = currentUserOpt.get();
|
||||
|
||||
Optional<InvoiceTemplate> templateOpt = invoiceTemplateService
|
||||
.getTemplateByUserId(currentUser.getId().toString());
|
||||
if (templateOpt.isEmpty()) {
|
||||
Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000,
|
||||
Notification.Position.BOTTOM_END);
|
||||
return;
|
||||
}
|
||||
|
||||
String templateData = templateOpt.get().getTemplateData();
|
||||
InvoiceTemplate template = invoiceTemplateService
|
||||
.getCustomerTemplateByUserId(currentUser.getId().toString());
|
||||
String templateData = template.getTemplateData();
|
||||
if (templateData == null || templateData.isBlank()) {
|
||||
Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000,
|
||||
Notification.Position.BOTTOM_END);
|
||||
@@ -822,15 +817,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||
|
||||
// Build variable substitution map
|
||||
Map<String, String> variables = new HashMap<>();
|
||||
|
||||
// Master data (from user profile)
|
||||
variables.put("masterdata.company_name", safe(currentUser.getCompany()));
|
||||
variables.put("masterdata.contact_name", safe(currentUser.getFirstname()) + " " + safe(currentUser.getName()));
|
||||
variables.put("masterdata.street", safe(currentUser.getStreet()) + " " + safe(currentUser.getHouseNumber()));
|
||||
variables.put("masterdata.city", safe(currentUser.getZip()) + " " + safe(currentUser.getCity()));
|
||||
variables.put("masterdata.email", safe(currentUser.getEmail()));
|
||||
variables.put("masterdata.phone", safe(currentUser.getPhone()));
|
||||
Map<String, String> variables = CustomerInvoiceTemplateVariables.create(currentUser,
|
||||
userInvoiceDataService.findByUserId(currentUser.getId()).orElse(null), invoiceNumber);
|
||||
|
||||
// Customer data (from job - look up Customer entity for full address)
|
||||
String customerSelection = currentJob.getCustomerSelection();
|
||||
|
||||
@@ -44,6 +44,7 @@ import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
|
||||
import de.assecutor.votianlt.repository.ServiceRepository;
|
||||
import de.assecutor.votianlt.security.SecurityService;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceTemplateVariables;
|
||||
import de.assecutor.votianlt.service.InvoiceTemplateService;
|
||||
import de.assecutor.votianlt.service.LanguageService;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
@@ -78,6 +79,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
private Checkbox billingEnabled;
|
||||
private NumberField vatRateField;
|
||||
private VerticalLayout propertiesPanelProfile;
|
||||
private final Div canvasContainer;
|
||||
|
||||
private final ServiceRepository serviceRepository;
|
||||
private Grid<Service> servicesGrid;
|
||||
@@ -414,7 +416,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)");
|
||||
|
||||
// Mitte: Canvas
|
||||
Div canvasContainer = new Div();
|
||||
canvasContainer = new Div();
|
||||
canvasContainer.setId("invoice-canvas-container-profile");
|
||||
canvasContainer.setWidth("100%");
|
||||
canvasContainer.setHeight("100%");
|
||||
@@ -513,17 +515,10 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
// Bestehende Rechnungsdaten laden (nur für die Checkbox)
|
||||
loadInvoiceData();
|
||||
|
||||
// Initialize invoice generator when the billing tab is selected
|
||||
// Also register this view instance for JavaScript callbacks
|
||||
// Load directly; the client waits until the billing canvas is visible.
|
||||
tabSheet.addSelectedChangeListener(e -> {
|
||||
if (getTranslation("profile.invoicecreation").equals(e.getSelectedTab().getLabel())) {
|
||||
getElement().executeJs("window.invoiceGeneratorViewProfile = $0;"
|
||||
+ "window.profileInvoiceServiceData = null;" + "setTimeout(function() { "
|
||||
+ " if (window.initProfileInvoiceGenerator) { " + " window.initProfileInvoiceGenerator(); "
|
||||
+ " console.log('Canvas initialized, now loading template...'); "
|
||||
+ " setTimeout(function() { " + " $0.$server.onCanvasReady(); " + " }, 200); "
|
||||
+ " } else { " + " console.error('initProfileInvoiceGenerator not found'); " + " } "
|
||||
+ "}, 300);", getElement());
|
||||
loadInvoiceTemplate();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -858,6 +853,16 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
currentInvoiceData = userInvoiceDataService.createOrUpdate(currentUser.getId(), billingEnabled.getValue(),
|
||||
prefixField.getValue(), ustIdField.getValue(), taxNumberField.getValue(), bankNameField.getValue(),
|
||||
ibanField.getValue(), "19", introTextArea.getValue(), termsTextArea.getValue());
|
||||
try {
|
||||
String valuesJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(
|
||||
CustomerInvoiceTemplateVariables.create(currentUser, currentInvoiceData,
|
||||
prefixField.getValue() + "12345"));
|
||||
getElement().executeJs("if (window.updateProfileMasterdataValue) { "
|
||||
+ "Object.entries(JSON.parse($0)).forEach(([key, value]) => "
|
||||
+ "window.updateProfileMasterdataValue(key, value)); }", valuesJson);
|
||||
} catch (com.fasterxml.jackson.core.JsonProcessingException ex) {
|
||||
throw new IllegalStateException("Could not update invoice template values", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String safe(String value) {
|
||||
@@ -957,7 +962,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
}
|
||||
BigDecimal previewVatRate = getPreviewVatRate();
|
||||
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData,
|
||||
currentUser, prefixField.getValue(), previewVatRate);
|
||||
currentUser, prefixField.getValue(), previewVatRate, currentInvoiceData);
|
||||
showPdfInDialog(pdfBytes);
|
||||
} catch (Exception ex) {
|
||||
Notification.show(getTranslation("profile.invoice.pdf.preview.error", ex.getMessage()),
|
||||
@@ -1479,62 +1484,23 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by JavaScript when the canvas is ready
|
||||
*/
|
||||
@ClientCallable
|
||||
public void onCanvasReady() {
|
||||
System.out.println("Canvas ready, loading template...");
|
||||
loadInvoiceTemplate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the saved invoice template from database and display it on the canvas
|
||||
*/
|
||||
private void loadInvoiceTemplate() {
|
||||
try {
|
||||
java.util.Optional<de.assecutor.votianlt.model.InvoiceTemplate> optionalTemplate = invoiceTemplateService
|
||||
.getTemplateByUserId(currentUser.getId().toString());
|
||||
if (optionalTemplate.isPresent()) {
|
||||
String templateData = optionalTemplate.get().getTemplateData();
|
||||
if (templateData != null && !templateData.isEmpty()) {
|
||||
System.out.println("Loading template data: "
|
||||
+ templateData.substring(0, Math.min(100, templateData.length())) + "...");
|
||||
// Escape the JSON string for JavaScript
|
||||
String escapedJson = templateData.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
// Build masterdata values JSON
|
||||
String company = safe(currentUser.getCompany());
|
||||
String fullName = safe(currentUser.getFirstname()) + " " + safe(currentUser.getName());
|
||||
String street = safe(currentUser.getStreet()) + " " + safe(currentUser.getHouseNumber());
|
||||
String city = safe(currentUser.getZip()) + " " + safe(currentUser.getCity());
|
||||
String email = safe(currentUser.getEmail());
|
||||
String phone = safe(currentUser.getPhone());
|
||||
String prefix = prefixField.getValue();
|
||||
String invoiceNumber = (prefix != null && !prefix.isBlank() ? prefix : "") + "12345";
|
||||
String masterdataJson = "{" + "'masterdata.company_name': '" + company.replace("'", "\\'") + "',"
|
||||
+ "'masterdata.contact_name': '" + fullName.replace("'", "\\'") + "',"
|
||||
+ "'masterdata.street': '" + street.replace("'", "\\'") + "'," + "'masterdata.city': '"
|
||||
+ city.replace("'", "\\'") + "'," + "'masterdata.email': '" + email.replace("'", "\\'")
|
||||
+ "'," + "'masterdata.phone': '" + phone.replace("'", "\\'") + "',"
|
||||
+ "'masterdata.invoice_number': '" + invoiceNumber.replace("'", "\\'") + "'" + "}";
|
||||
BigDecimal rate = currentUser.getVatRate() != null ? currentUser.getVatRate()
|
||||
: Service.FIXED_VAT_RATE;
|
||||
double vatRateJs = rate.doubleValue();
|
||||
getElement().executeJs("setTimeout(function() { "
|
||||
+ " if (window.loadProfileTemplate && document.getElementById('invoice-canvas-container-profile')) { "
|
||||
+ " console.log('Loading template into canvas...'); "
|
||||
+ " window.profileInvoiceServiceData = null; "
|
||||
+ " window.profileInvoiceVatRate = " + vatRateJs + "; "
|
||||
+ " window.masterdataValues = "
|
||||
+ masterdataJson + "; " + " var templateData = JSON.parse('" + escapedJson + "'); "
|
||||
+ " window.loadProfileTemplate(templateData); " + " } else { "
|
||||
+ " console.error('loadProfileTemplate or canvas not available'); " + " } "
|
||||
+ "}, 300);");
|
||||
}
|
||||
} else {
|
||||
System.out.println("No template found for user: " + currentUser.getId());
|
||||
String templateData = invoiceTemplateService.getCustomerTemplateByUserId(currentUser.getId().toString())
|
||||
.getTemplateData();
|
||||
if (templateData == null || templateData.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String prefix = prefixField.getValue();
|
||||
String invoiceNumber = (prefix != null && !prefix.isBlank() ? prefix : "") + "12345";
|
||||
String masterdataJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(
|
||||
CustomerInvoiceTemplateVariables.create(currentUser, currentInvoiceData, invoiceNumber));
|
||||
getElement().executeJs("window.showProfileInvoiceTemplate($0, $1, JSON.parse($2), JSON.parse($3), $4);",
|
||||
canvasContainer.getElement(), getElement(), templateData, masterdataJson,
|
||||
getPreviewVatRate().doubleValue());
|
||||
} catch (Exception ex) {
|
||||
System.err.println("Fehler beim Laden des Templates: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
|
||||
@@ -263,6 +263,12 @@ public class CustomerInvoiceService {
|
||||
|
||||
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
|
||||
String invoicePrefix, BigDecimal vatRate) throws Exception {
|
||||
return generatePdfFromCanvasTemplate(jsonTemplateData, user, invoicePrefix, vatRate, null);
|
||||
}
|
||||
|
||||
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
|
||||
String invoicePrefix, BigDecimal vatRate, de.assecutor.votianlt.model.UserInvoiceData billing)
|
||||
throws Exception {
|
||||
BigDecimal effectiveVatRate = vatRate != null ? vatRate
|
||||
: (user != null && user.getVatRate() != null ? user.getVatRate() : new BigDecimal("0.19"));
|
||||
|
||||
@@ -270,16 +276,8 @@ public class CustomerInvoiceService {
|
||||
java.util.Map<String, String> variables = new java.util.HashMap<>();
|
||||
|
||||
if (user != null) {
|
||||
// Use actual user data
|
||||
String company = user.getCompany();
|
||||
variables.put("masterdata.company_name", (company != null && !company.isEmpty()) ? company : "Ihre Firma");
|
||||
variables.put("masterdata.contact_name", safe(user.getFirstname()) + " " + safe(user.getName()));
|
||||
variables.put("masterdata.street", safe(user.getStreet()) + " " + safe(user.getHouseNumber()));
|
||||
variables.put("masterdata.city", safe(user.getZip()) + " " + safe(user.getCity()));
|
||||
variables.put("masterdata.email", safe(user.getEmail()));
|
||||
variables.put("masterdata.phone", safe(user.getPhone()));
|
||||
variables.put("masterdata.invoice_number",
|
||||
(invoicePrefix != null && !invoicePrefix.isBlank() ? invoicePrefix : "") + "12345");
|
||||
variables.putAll(CustomerInvoiceTemplateVariables.create(user, billing,
|
||||
(invoicePrefix != null && !invoicePrefix.isBlank() ? invoicePrefix : "") + "12345"));
|
||||
} else {
|
||||
// Default values for preview without user
|
||||
variables.put("masterdata.company_name", "Meine Firma GmbH");
|
||||
@@ -575,6 +573,14 @@ public class CustomerInvoiceService {
|
||||
.append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal)).append("</td>");
|
||||
html.append("</tr>");
|
||||
|
||||
html.append("<tr>");
|
||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
||||
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. ")
|
||||
.append(vatLabel).append(" USt:</td>");
|
||||
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>")
|
||||
.append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal * vatRate.doubleValue()))
|
||||
.append("</td></tr>");
|
||||
|
||||
// Gesamtsumme - label in col 2, value in col 3
|
||||
html.append("<tr>");
|
||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.model.UserInvoiceData;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/** Shared issuer values for the customer editor, PDF preview and actual invoice. */
|
||||
public final class CustomerInvoiceTemplateVariables {
|
||||
private CustomerInvoiceTemplateVariables() {
|
||||
}
|
||||
|
||||
public static Map<String, String> create(User user, UserInvoiceData billing, String invoiceNumber) {
|
||||
Map<String, String> values = new LinkedHashMap<>();
|
||||
String contact = join(" ", user.getFirstname(), user.getName());
|
||||
String company = safe(user.getCompany());
|
||||
if (company.isEmpty()) {
|
||||
company = contact;
|
||||
}
|
||||
String street = join(" ", user.getStreet(), user.getHouseNumber());
|
||||
String city = join(" ", user.getZip(), user.getCity());
|
||||
values.put("masterdata.company_name", company);
|
||||
values.put("masterdata.contact_name", contact);
|
||||
values.put("masterdata.street", street);
|
||||
values.put("masterdata.city", city);
|
||||
values.put("masterdata.email", safe(user.getEmail()));
|
||||
values.put("masterdata.phone", safe(user.getPhone()));
|
||||
values.put("masterdata.sender_line", join(" · ", company, street, city));
|
||||
values.put("masterdata.contact_details", join("\n", user.getEmail(), user.getPhone()));
|
||||
values.put("masterdata.invoice_number", safe(invoiceNumber));
|
||||
values.put("masterdata.invoice_date", LocalDate.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy")));
|
||||
values.put("masterdata.tax_details", billing == null ? ""
|
||||
: join("\n", labeled("USt-IdNr.: ", billing.getUstId()),
|
||||
labeled("Steuernummer: ", billing.getTaxNumber())));
|
||||
values.put("masterdata.bank_details", billing == null ? ""
|
||||
: join("\n", billing.getBankName(), labeled("IBAN: ", billing.getIban())));
|
||||
String intro = billing == null ? "" : safe(billing.getIntroText());
|
||||
values.put("masterdata.intro", intro.isEmpty() ? "Wir berechnen Ihnen folgende Leistungen:" : intro);
|
||||
String terms = billing == null ? "" : safe(billing.getPaymentTerms());
|
||||
values.put("masterdata.payment_terms", terms.isEmpty()
|
||||
? "Bitte geben Sie bei der Zahlung die Rechnungsnummer an." : terms);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static String labeled(String label, String value) {
|
||||
return safe(value).isEmpty() ? "" : label + safe(value);
|
||||
}
|
||||
|
||||
private static String join(String separator, String... parts) {
|
||||
return Stream.of(parts).map(CustomerInvoiceTemplateVariables::safe).filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.joining(separator));
|
||||
}
|
||||
|
||||
private static String safe(String value) {
|
||||
return value == null ? "" : value.strip();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.assecutor.votianlt.model.AppUser;
|
||||
import de.assecutor.votianlt.model.Barcode;
|
||||
import de.assecutor.votianlt.model.Comment;
|
||||
import de.assecutor.votianlt.model.Customer;
|
||||
import de.assecutor.votianlt.model.DeliveryStation;
|
||||
import de.assecutor.votianlt.model.InvoiceTemplate;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.JobServiceSelection;
|
||||
import de.assecutor.votianlt.model.JobStatus;
|
||||
@@ -37,9 +35,7 @@ import de.assecutor.votianlt.repository.TaskTemplateRepository;
|
||||
import de.assecutor.votianlt.repository.UserInvoiceDataRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import de.assecutor.votianlt.security.SessionAuthenticationService;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
@@ -54,7 +50,6 @@ import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -68,7 +63,6 @@ public class DemoModeService {
|
||||
public static final String DEMO_PASSWORD = "demo";
|
||||
private static final String DEMO_FIRST_NAME = "Demo";
|
||||
private static final String DEMO_LAST_NAME = "Benutzer";
|
||||
private static final String DEMO_INVOICE_TEMPLATE_RESOURCE = "templates/demo_invoice_template_default.json";
|
||||
|
||||
private final DemoSessionRegistry demoSessionRegistry;
|
||||
private final SessionAuthenticationService sessionAuthenticationService;
|
||||
@@ -194,7 +188,6 @@ public class DemoModeService {
|
||||
List<Service> services = seedDemoServices(demoUser);
|
||||
List<Customer> customers = seedDemoCustomers(demoUser);
|
||||
seedDemoJobs(demoUser, customers, services);
|
||||
seedDemoInvoiceTemplate(demoUser);
|
||||
}
|
||||
|
||||
private void cleanupDemoAppUserData(List<AppUser> demoAppUsers) {
|
||||
@@ -441,23 +434,6 @@ public class DemoModeService {
|
||||
return selection;
|
||||
}
|
||||
|
||||
private void seedDemoInvoiceTemplate(User demoUser) {
|
||||
InvoiceTemplate template = new InvoiceTemplate(demoUser.getId().toHexString(), "Einfach",
|
||||
createDemoInvoiceTemplateData());
|
||||
invoiceTemplateRepository.save(template);
|
||||
}
|
||||
|
||||
private String createDemoInvoiceTemplateData() {
|
||||
try {
|
||||
String templateJson = new String(
|
||||
new ClassPathResource(DEMO_INVOICE_TEMPLATE_RESOURCE).getInputStream().readAllBytes(),
|
||||
StandardCharsets.UTF_8);
|
||||
return new ObjectMapper().writeValueAsString(templateJson);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("Could not load demo invoice template", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCustomerToPickup(Job job, Customer customer) {
|
||||
if (job == null || customer == null) {
|
||||
return;
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import de.assecutor.votianlt.model.InvoiceTemplate;
|
||||
import de.assecutor.votianlt.repository.InvoiceTemplateRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InvoiceTemplateService {
|
||||
|
||||
public static final String CUSTOMER_DEFAULT_USER_ID = "CUSTOMER_DEFAULT";
|
||||
|
||||
private final InvoiceTemplateRepository invoiceTemplateRepository;
|
||||
|
||||
/**
|
||||
@@ -36,6 +43,49 @@ public class InvoiceTemplateService {
|
||||
return invoiceTemplateRepository.findByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer invoices use the neutral default if no usable personal template is
|
||||
* stored. Reading the default never creates or overwrites a database record.
|
||||
*/
|
||||
public InvoiceTemplate getCustomerTemplateByUserId(String userId) {
|
||||
if (userId == null || userId.isBlank() || "SYSTEM".equals(userId) || CUSTOMER_DEFAULT_USER_ID.equals(userId)) {
|
||||
throw new IllegalArgumentException("A customer user ID is required");
|
||||
}
|
||||
return invoiceTemplateRepository.findByUserId(userId)
|
||||
.filter(template -> hasTemplateElements(template.getTemplateData()))
|
||||
.orElseGet(() -> invoiceTemplateRepository.findByUserId(CUSTOMER_DEFAULT_USER_ID)
|
||||
.filter(template -> hasTemplateElements(template.getTemplateData()))
|
||||
// Saving the customer's edits must never change the shared default.
|
||||
.map(template -> new InvoiceTemplate(userId, template.getName(), template.getTemplateData()))
|
||||
.orElseGet(() -> loadBundledCustomerDefault(userId)));
|
||||
}
|
||||
|
||||
private InvoiceTemplate loadBundledCustomerDefault(String userId) {
|
||||
try (var input = new ClassPathResource("templates/customer_invoice_template_neutral.json").getInputStream()) {
|
||||
String json = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
return new InvoiceTemplate(userId, "Neutral", new ObjectMapper().writeValueAsString(json));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("Could not load neutral customer invoice template", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasTemplateElements(String templateData) {
|
||||
if (templateData == null || templateData.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode data = mapper.readTree(templateData);
|
||||
// Templates can be stored as an object or as a JSON-encoded string.
|
||||
if (data != null && data.isTextual()) {
|
||||
data = mapper.readTree(data.textValue());
|
||||
}
|
||||
return data != null && data.path("elements").isArray() && !data.path("elements").isEmpty();
|
||||
} catch (IOException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a template exists for a user
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"id": "neutral-company",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "5.70071",
|
||||
"widthPercent": "61.51261",
|
||||
"heightPercent": "3.56295",
|
||||
"fontSize": 20,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.company_name"
|
||||
},
|
||||
{
|
||||
"id": "neutral-document-label",
|
||||
"type": "text",
|
||||
"text": "RECHNUNG",
|
||||
"xPercent": "71.93277",
|
||||
"yPercent": "6.88836",
|
||||
"widthPercent": "19.32773",
|
||||
"heightPercent": "2.13777",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "right",
|
||||
"color": "#555555",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "neutral-street",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "10.80760",
|
||||
"widthPercent": "57.14286",
|
||||
"heightPercent": "1.66271",
|
||||
"fontSize": 10,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.street"
|
||||
},
|
||||
{
|
||||
"id": "neutral-city",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "12.58907",
|
||||
"widthPercent": "57.14286",
|
||||
"heightPercent": "1.66271",
|
||||
"fontSize": 10,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.city"
|
||||
},
|
||||
{
|
||||
"id": "neutral-header-rule",
|
||||
"type": "line",
|
||||
"text": "",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "16.50831",
|
||||
"widthPercent": "82.52101",
|
||||
"heightPercent": "0.11876",
|
||||
"fontSize": 10,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "neutral-sender",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "20.30879",
|
||||
"widthPercent": "82.52101",
|
||||
"heightPercent": "1.18765",
|
||||
"fontSize": 7,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#555555",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.sender_line"
|
||||
},
|
||||
{
|
||||
"id": "neutral-recipient-company_name",
|
||||
"type": "text",
|
||||
"text": "Kundenfirma GmbH",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "23.27791",
|
||||
"widthPercent": "47.89916",
|
||||
"heightPercent": "1.78147",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": true,
|
||||
"variable": "customer.company_name"
|
||||
},
|
||||
{
|
||||
"id": "neutral-recipient-contact_name",
|
||||
"type": "text",
|
||||
"text": "Erika Mustermann",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "25.05938",
|
||||
"widthPercent": "47.89916",
|
||||
"heightPercent": "1.78147",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": true,
|
||||
"variable": "customer.contact_name"
|
||||
},
|
||||
{
|
||||
"id": "neutral-recipient-street",
|
||||
"type": "text",
|
||||
"text": "Kundenstraße 456",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "26.84086",
|
||||
"widthPercent": "47.89916",
|
||||
"heightPercent": "1.78147",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": true,
|
||||
"variable": "customer.street"
|
||||
},
|
||||
{
|
||||
"id": "neutral-recipient-city",
|
||||
"type": "text",
|
||||
"text": "54321 Kundenstadt",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "28.62233",
|
||||
"widthPercent": "47.89916",
|
||||
"heightPercent": "1.78147",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": true,
|
||||
"variable": "customer.city"
|
||||
},
|
||||
{
|
||||
"id": "neutral-number-label",
|
||||
"type": "text",
|
||||
"text": "RECHNUNGSNUMMER",
|
||||
"xPercent": "63.52941",
|
||||
"yPercent": "23.15914",
|
||||
"widthPercent": "27.73109",
|
||||
"heightPercent": "1.42518",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#555555",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "neutral-number",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "63.52941",
|
||||
"yPercent": "25.05938",
|
||||
"widthPercent": "27.73109",
|
||||
"heightPercent": "1.90024",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.invoice_number"
|
||||
},
|
||||
{
|
||||
"id": "neutral-date-label",
|
||||
"type": "text",
|
||||
"text": "RECHNUNGSDATUM",
|
||||
"xPercent": "63.52941",
|
||||
"yPercent": "28.38480",
|
||||
"widthPercent": "27.73109",
|
||||
"heightPercent": "1.42518",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#555555",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "neutral-date",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "63.52941",
|
||||
"yPercent": "30.28504",
|
||||
"widthPercent": "27.73109",
|
||||
"heightPercent": "1.90024",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.invoice_date"
|
||||
},
|
||||
{
|
||||
"id": "neutral-services-heading",
|
||||
"type": "text",
|
||||
"text": "Leistungen",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "36.34204",
|
||||
"widthPercent": "82.52101",
|
||||
"heightPercent": "2.37530",
|
||||
"fontSize": 14,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "neutral-intro",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "39.90499",
|
||||
"widthPercent": "82.52101",
|
||||
"heightPercent": "3.56295",
|
||||
"fontSize": 10,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.intro"
|
||||
},
|
||||
{
|
||||
"id": "neutral-services",
|
||||
"type": "text",
|
||||
"text": "Rechnungspositionen",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "45.24941",
|
||||
"widthPercent": "82.52101",
|
||||
"heightPercent": "27.67221",
|
||||
"fontSize": 10,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "services.list"
|
||||
},
|
||||
{
|
||||
"id": "neutral-payment-label",
|
||||
"type": "text",
|
||||
"text": "ZAHLUNG",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "78.02850",
|
||||
"widthPercent": "82.52101",
|
||||
"heightPercent": "1.42518",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#555555",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "neutral-payment",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "80.40380",
|
||||
"widthPercent": "82.52101",
|
||||
"heightPercent": "4.98812",
|
||||
"fontSize": 10,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.payment_terms"
|
||||
},
|
||||
{
|
||||
"id": "neutral-footer-rule",
|
||||
"type": "line",
|
||||
"text": "",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "88.83610",
|
||||
"widthPercent": "82.52101",
|
||||
"heightPercent": "0.11876",
|
||||
"fontSize": 10,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#202020",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "neutral-contact-label",
|
||||
"type": "text",
|
||||
"text": "KONTAKT",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "90.61758",
|
||||
"widthPercent": "26.21849",
|
||||
"heightPercent": "1.30641",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#555555",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "neutral-contact",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "8.73950",
|
||||
"yPercent": "92.87411",
|
||||
"widthPercent": "26.21849",
|
||||
"heightPercent": "3.32542",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#444444",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.contact_details"
|
||||
},
|
||||
{
|
||||
"id": "neutral-tax-label",
|
||||
"type": "text",
|
||||
"text": "STEUERANGABEN",
|
||||
"xPercent": "38.65546",
|
||||
"yPercent": "90.61758",
|
||||
"widthPercent": "25.37815",
|
||||
"heightPercent": "1.30641",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#555555",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "neutral-tax",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "38.65546",
|
||||
"yPercent": "92.87411",
|
||||
"widthPercent": "25.37815",
|
||||
"heightPercent": "3.32542",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#444444",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.tax_details"
|
||||
},
|
||||
{
|
||||
"id": "neutral-bank-label",
|
||||
"type": "text",
|
||||
"text": "BANKVERBINDUNG",
|
||||
"xPercent": "67.22689",
|
||||
"yPercent": "90.61758",
|
||||
"widthPercent": "24.03361",
|
||||
"heightPercent": "1.30641",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#555555",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "neutral-bank",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "67.22689",
|
||||
"yPercent": "92.87411",
|
||||
"widthPercent": "24.03361",
|
||||
"heightPercent": "3.32542",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#444444",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.bank_details"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"id": "demo-title",
|
||||
"type": "header",
|
||||
"text": "Rechnung",
|
||||
"xPercent": "7.56",
|
||||
"yPercent": "6.53",
|
||||
"widthPercent": "28.07",
|
||||
"heightPercent": "4.04",
|
||||
"fontSize": 24,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-number-label",
|
||||
"type": "text",
|
||||
"text": "Rechnungsnr.",
|
||||
"xPercent": "67.06",
|
||||
"yPercent": "7.48",
|
||||
"widthPercent": "17.98",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-number-value",
|
||||
"type": "text",
|
||||
"text": "000000",
|
||||
"xPercent": "84.03",
|
||||
"yPercent": "7.48",
|
||||
"widthPercent": "10.08",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "invoice.number"
|
||||
},
|
||||
{
|
||||
"id": "demo-date-label",
|
||||
"type": "text",
|
||||
"text": "Datum",
|
||||
"xPercent": "67.06",
|
||||
"yPercent": "11.52",
|
||||
"widthPercent": "17.98",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-date-value",
|
||||
"type": "text",
|
||||
"text": "2026-01-01",
|
||||
"xPercent": "84.03",
|
||||
"yPercent": "11.52",
|
||||
"widthPercent": "10.08",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "invoice.date"
|
||||
},
|
||||
{
|
||||
"id": "demo-sender",
|
||||
"type": "text",
|
||||
"text": null,
|
||||
"xPercent": "7.56",
|
||||
"yPercent": "16.51",
|
||||
"widthPercent": "30.08",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "masterdata.contact_name"
|
||||
},
|
||||
{
|
||||
"id": "demo-customer",
|
||||
"type": "customer",
|
||||
"text": "Kundenname\nStraße Nr.\nPLZ Ort",
|
||||
"xPercent": "7.56",
|
||||
"yPercent": "25.06",
|
||||
"widthPercent": "33.95",
|
||||
"heightPercent": "12.00",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "normal",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-services",
|
||||
"type": "text",
|
||||
"text": "Leistungen",
|
||||
"xPercent": "7.56",
|
||||
"yPercent": "42.04",
|
||||
"widthPercent": "85.04",
|
||||
"heightPercent": "28.03",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "services.list"
|
||||
},
|
||||
{
|
||||
"id": "demo-net-label",
|
||||
"type": "text",
|
||||
"text": "Netto",
|
||||
"xPercent": "67.06",
|
||||
"yPercent": "73.99",
|
||||
"widthPercent": "11.93",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-net-value",
|
||||
"type": "text",
|
||||
"text": "0,00 €",
|
||||
"xPercent": "80.00",
|
||||
"yPercent": "73.99",
|
||||
"widthPercent": "13.95",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "invoice.net_total"
|
||||
},
|
||||
{
|
||||
"id": "demo-total-label",
|
||||
"type": "text",
|
||||
"text": "Gesamt",
|
||||
"xPercent": "67.06",
|
||||
"yPercent": "78.98",
|
||||
"widthPercent": "11.93",
|
||||
"heightPercent": "3.44",
|
||||
"fontSize": 13,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-total-value",
|
||||
"type": "text",
|
||||
"text": "0,00 €",
|
||||
"xPercent": "80.00",
|
||||
"yPercent": "78.98",
|
||||
"widthPercent": "13.95",
|
||||
"heightPercent": "3.44",
|
||||
"fontSize": 13,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "invoice.gross_total"
|
||||
},
|
||||
{
|
||||
"id": "demo-template-note",
|
||||
"type": "text",
|
||||
"text": "Hinweis: Dieses Rechnungstemplate kann vom Benutzer ",
|
||||
"xPercent": "0.00",
|
||||
"yPercent": "20.78",
|
||||
"widthPercent": "100.00",
|
||||
"heightPercent": "4.51",
|
||||
"fontSize": 20,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "center",
|
||||
"color": "#555555"
|
||||
},
|
||||
{
|
||||
"id": "element-1",
|
||||
"type": "text",
|
||||
"text": "frei angepasst werden.",
|
||||
"xPercent": "31.93",
|
||||
"yPercent": "24.94",
|
||||
"widthPercent": "25.21",
|
||||
"heightPercent": "3.56",
|
||||
"fontSize": 20,
|
||||
"color": "#333333",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,6 +21,12 @@ function createEditor() {
|
||||
addEventListener() {}
|
||||
};
|
||||
const window = { addEventListener() {} };
|
||||
const resizeObservers = [];
|
||||
class ResizeObserver {
|
||||
constructor(callback) { this.callback = callback; resizeObservers.push(this); }
|
||||
observe() {}
|
||||
disconnect() { this.disconnected = true; }
|
||||
}
|
||||
class Image {
|
||||
width = 595;
|
||||
height = 842;
|
||||
@@ -30,6 +36,7 @@ function createEditor() {
|
||||
window,
|
||||
document: { getElementById: () => container, createElement: () => canvas },
|
||||
Image,
|
||||
ResizeObserver,
|
||||
console: { log() {}, error(error) { throw new Error(String(error)); } }
|
||||
});
|
||||
window.initProfileInvoiceGenerator();
|
||||
@@ -37,7 +44,7 @@ function createEditor() {
|
||||
{ id: 'letterhead', type: 'image', imageData: 'data:image/svg+xml;base64,test', x: 0, y: 0, width: 595, height: 842 },
|
||||
{ id: 'invoice-text', type: 'text', text: 'Rechnung', x: 50, y: 250, width: 100, height: 30 }
|
||||
] });
|
||||
return { window, painted, pendingImages };
|
||||
return { window, painted, pendingImages, container, resizeObservers };
|
||||
}
|
||||
|
||||
test('invoice text stays above a letterhead that finishes loading later', () => {
|
||||
@@ -58,3 +65,55 @@ test('a removed letterhead is not painted when its pending image finishes loadin
|
||||
pendingImages[0].onload();
|
||||
assert.deepEqual(painted, [['text', 'Rechnung']]);
|
||||
});
|
||||
|
||||
test('optional billing data stays empty while unknown variables retain a placeholder', () => {
|
||||
const { window } = createEditor();
|
||||
window.masterdataValues = { 'masterdata.bank_details': '' };
|
||||
window.loadProfileTemplate({ elements: [
|
||||
{ id: 'bank', type: 'text', variable: 'masterdata.bank_details', text: 'Old bank', x: 0, y: 0 },
|
||||
{ id: 'unknown', type: 'text', variable: 'masterdata.unknown', x: 0, y: 20 }
|
||||
] });
|
||||
assert.equal(window.profileInvoiceState.elements[0].text, '');
|
||||
assert.equal(window.profileInvoiceState.elements[1].text, '[unknown]');
|
||||
});
|
||||
|
||||
const neutralTemplate = JSON.parse(readFileSync(new URL('../../main/resources/templates/customer_invoice_template_neutral.json', import.meta.url), 'utf8'));
|
||||
|
||||
test('the neutral profile template loads when its initially hidden tab becomes visible', () => {
|
||||
const { window, container, resizeObservers } = createEditor();
|
||||
container.getBoundingClientRect = () => ({ width: 0, height: 0 });
|
||||
window.showProfileInvoiceTemplate(container, {}, JSON.stringify(neutralTemplate), { 'masterdata.company_name': 'Kundenfirma' }, 0.19);
|
||||
assert.equal(resizeObservers.length, 1);
|
||||
assert.equal(window.profileInvoiceState.elements[0].id, 'letterhead');
|
||||
|
||||
container.getBoundingClientRect = () => ({ width: 615, height: 862 });
|
||||
resizeObservers[0].callback();
|
||||
|
||||
assert.equal(window.profileInvoiceState.elements.length, neutralTemplate.elements.length);
|
||||
assert.equal(window.profileInvoiceState.elements[0].id, 'neutral-company');
|
||||
assert.equal(window.profileInvoiceState.elements[0].text, 'Kundenfirma');
|
||||
assert.equal(resizeObservers[0].disconnected, true);
|
||||
assert.equal(window.pendingProfileInvoiceLoad, null);
|
||||
});
|
||||
|
||||
test('opening another customer profile cancels a pending template load', () => {
|
||||
const { window, container, resizeObservers } = createEditor();
|
||||
container.getBoundingClientRect = () => ({ width: 0, height: 0 });
|
||||
window.showProfileInvoiceTemplate(container, {}, neutralTemplate, { 'masterdata.company_name': 'Vorheriger Kunde' }, 0.19);
|
||||
window.showProfileInvoiceTemplate(container, {}, neutralTemplate, { 'masterdata.company_name': 'Aktueller Kunde' }, 0.07);
|
||||
assert.equal(resizeObservers[0].disconnected, true);
|
||||
|
||||
container.getBoundingClientRect = () => ({ width: 615, height: 862 });
|
||||
resizeObservers[0].callback();
|
||||
assert.equal(window.profileInvoiceState.elements[0].id, 'letterhead');
|
||||
resizeObservers[1].callback();
|
||||
assert.equal(window.profileInvoiceState.elements[0].text, 'Aktueller Kunde');
|
||||
assert.equal(window.profileInvoiceVatRate, 0.07);
|
||||
});
|
||||
|
||||
test('a visible profile canvas loads the template immediately', () => {
|
||||
const { window, container, resizeObservers } = createEditor();
|
||||
window.showProfileInvoiceTemplate(container, {}, neutralTemplate, { 'masterdata.company_name': 'Kundenfirma' }, 0.19);
|
||||
assert.equal(resizeObservers.length, 0);
|
||||
assert.equal(window.profileInvoiceState.elements[0].id, 'neutral-company');
|
||||
});
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
||||
import com.itextpdf.kernel.pdf.PdfReader;
|
||||
import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor;
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.model.UserInvoiceData;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
class CustomerInvoiceTemplateRenderingTest {
|
||||
@Test
|
||||
void neutralTemplateResolvesProfilePreviewAndActualInvoiceData() throws Exception {
|
||||
String template;
|
||||
try (var stream = new ClassPathResource("templates/customer_invoice_template_neutral.json").getInputStream()) {
|
||||
template = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
User user = new User();
|
||||
user.setCompany("Logistik & Partner");
|
||||
user.setStreet("Hafenstraße");
|
||||
user.setHouseNumber("12");
|
||||
user.setZip("20457");
|
||||
user.setCity("Hamburg");
|
||||
user.setEmail("rechnung@example.com");
|
||||
UserInvoiceData billing = new UserInvoiceData();
|
||||
billing.setTaxNumber("12/345/67890");
|
||||
billing.setBankName("Musterbank");
|
||||
billing.setIban("DE89 3704 0044 0532 0130 00");
|
||||
var service = new CustomerInvoiceService();
|
||||
|
||||
String preview = extractText(service.generatePdfFromCanvasTemplate(template, user, "RE-",
|
||||
new BigDecimal("0.19"), billing));
|
||||
|
||||
assertThat(preview).contains("Logistik & Partner", "RE-12345", "Kundenfirma GmbH", "124,45 €", "779,45 €",
|
||||
"Steuernummer: 12/345/67890", "IBAN: DE89 3704 0044 0532 0130 00");
|
||||
var values = CustomerInvoiceTemplateVariables.create(user, billing, "RE-2026-001");
|
||||
values.put("customer.company_name", "Kunde & Co.");
|
||||
values.put("customer.contact_name", "Erika Muster");
|
||||
values.put("customer.street", "Kundenstraße 1");
|
||||
values.put("customer.city", "10115 Berlin");
|
||||
values.put("services.json", "[{\"name\":\"Transport\",\"netAmount\":\"100,00\"}]");
|
||||
values.put("invoice.net_total", "100,00 €");
|
||||
values.put("invoice.vat_total", "19,00 €");
|
||||
values.put("invoice.gross_total", "119,00 €");
|
||||
values.put("invoice.vat_rate", "19%");
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
for (var element : mapper.readTree(template).path("elements")) {
|
||||
String variable = element.path("variable").asText("");
|
||||
if (!variable.isBlank() && !"services.list".equals(variable)) {
|
||||
assertThat(values).containsKey(variable);
|
||||
}
|
||||
}
|
||||
String invoice = extractText(service.generatePdfFromCanvasTemplateWithData(
|
||||
mapper.writeValueAsString(template), values, user));
|
||||
|
||||
assertThat(invoice).contains("Logistik & Partner", "Kunde & Co.", "Erika Muster", "RE-2026-001",
|
||||
"Transport", "119,00 €", "Musterbank", "Steuernummer: 12/345/67890")
|
||||
.doesNotContain("Kundenfirma GmbH", "Assecutor", "[invoice_date]", "[bank_details]", "null");
|
||||
}
|
||||
|
||||
private String extractText(byte[] pdfBytes) throws Exception {
|
||||
try (var pdf = new PdfDocument(new PdfReader(new ByteArrayInputStream(pdfBytes)))) {
|
||||
assertThat(pdf.getNumberOfPages()).isEqualTo(1);
|
||||
return PdfTextExtractor.getTextFromPage(pdf.getPage(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.model.UserInvoiceData;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CustomerInvoiceTemplateVariablesTest {
|
||||
@Test
|
||||
void usesCustomerProfileAndBillingDataIncludingMultilineText() {
|
||||
User user = new User();
|
||||
user.setCompany("Müller & Söhne");
|
||||
user.setStreet("Hafenstraße");
|
||||
user.setHouseNumber("12");
|
||||
user.setZip("20457");
|
||||
user.setCity("Hamburg");
|
||||
user.setEmail("rechnung@example.com");
|
||||
user.setPhone("040 12345");
|
||||
UserInvoiceData billing = new UserInvoiceData();
|
||||
billing.setUstId("DE123456789");
|
||||
billing.setTaxNumber("12/345/67890");
|
||||
billing.setBankName("Musterbank");
|
||||
billing.setIban("DE89 3704 0044 0532 0130 00");
|
||||
billing.setIntroText("Vielen Dank für Ihren Auftrag.\nWir berechnen folgende Leistungen:");
|
||||
billing.setPaymentTerms("Bitte binnen 14 Tagen zahlen.");
|
||||
|
||||
var values = CustomerInvoiceTemplateVariables.create(user, billing, "RE-2026-0001");
|
||||
|
||||
assertThat(values).containsEntry("masterdata.company_name", "Müller & Söhne")
|
||||
.containsEntry("masterdata.sender_line", "Müller & Söhne · Hafenstraße 12 · 20457 Hamburg")
|
||||
.containsEntry("masterdata.contact_details", "rechnung@example.com\n040 12345")
|
||||
.containsEntry("masterdata.tax_details", "USt-IdNr.: DE123456789\nSteuernummer: 12/345/67890")
|
||||
.containsEntry("masterdata.bank_details", "Musterbank\nIBAN: DE89 3704 0044 0532 0130 00")
|
||||
.containsEntry("masterdata.intro", billing.getIntroText())
|
||||
.containsEntry("masterdata.payment_terms", billing.getPaymentTerms())
|
||||
.containsEntry("masterdata.invoice_number", "RE-2026-0001");
|
||||
assertThat(values.get("masterdata.invoice_date")).matches("\\d{2}\\.\\d{2}\\.\\d{4}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void absentBillingDataDoesNotInventTaxOrBankDetails() {
|
||||
User user = new User();
|
||||
user.setFirstname("Erika");
|
||||
user.setName("Beispiel");
|
||||
|
||||
var values = CustomerInvoiceTemplateVariables.create(user, null, "RE-1");
|
||||
|
||||
assertThat(values).containsEntry("masterdata.company_name", "Erika Beispiel")
|
||||
.containsEntry("masterdata.tax_details", "").containsEntry("masterdata.bank_details", "")
|
||||
.containsEntry("masterdata.contact_details", "")
|
||||
.containsEntry("masterdata.intro", "Wir berechnen Ihnen folgende Leistungen:")
|
||||
.containsEntry("masterdata.payment_terms", "Bitte geben Sie bei der Zahlung die Rechnungsnummer an.");
|
||||
assertThat(values.values()).noneMatch(value -> value.contains("null") || value.contains("Assecutor"));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package de.assecutor.votianlt.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -10,7 +11,6 @@ import static org.mockito.Mockito.when;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.JobStatus;
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.model.InvoiceTemplate;
|
||||
import de.assecutor.votianlt.pages.domain.CustomerRepository;
|
||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||
import de.assecutor.votianlt.repository.BarcodeRepository;
|
||||
@@ -90,8 +90,6 @@ class DemoModeServiceTest {
|
||||
private ArgumentCaptor<List<Job>> jobsCaptor;
|
||||
@Captor
|
||||
private ArgumentCaptor<List<de.assecutor.votianlt.model.Service>> servicesCaptor;
|
||||
@Captor
|
||||
private ArgumentCaptor<InvoiceTemplate> invoiceTemplateCaptor;
|
||||
|
||||
private DemoSessionRegistry demoSessionRegistry;
|
||||
private DemoModeService demoModeService;
|
||||
@@ -127,17 +125,8 @@ class DemoModeServiceTest {
|
||||
assertThat(prepared).isTrue();
|
||||
verify(serviceRepository).saveAll(servicesCaptor.capture());
|
||||
assertThat(servicesCaptor.getValue()).hasSize(2);
|
||||
verify(invoiceTemplateRepository).save(invoiceTemplateCaptor.capture());
|
||||
assertThat(invoiceTemplateCaptor.getValue().getUserId()).isEqualTo(demoUser.getId().toHexString());
|
||||
assertThat(invoiceTemplateCaptor.getValue().getTemplateData())
|
||||
.contains("elements")
|
||||
.contains("services.list")
|
||||
.contains("invoice.gross_total")
|
||||
.contains("frei angepasst werden")
|
||||
.contains("textAlign")
|
||||
.contains("center")
|
||||
.contains("fontSize")
|
||||
.contains("20");
|
||||
// Demo customers use the same neutral default as all other customers.
|
||||
verify(invoiceTemplateRepository, never()).save(any());
|
||||
verify(jobRepository).saveAll(jobsCaptor.capture());
|
||||
assertThat(jobsCaptor.getValue()).hasSize(2);
|
||||
assertThat(jobsCaptor.getValue()).extracting(Job::getJobNumber)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
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.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.assecutor.votianlt.model.InvoiceTemplate;
|
||||
import de.assecutor.votianlt.repository.InvoiceTemplateRepository;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InvoiceTemplateServiceTest {
|
||||
@Mock
|
||||
private InvoiceTemplateRepository repository;
|
||||
private InvoiceTemplateService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InvoiceTemplateService(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingCustomerTemplateUsesEditableDefaultWithoutWritingToDatabase() throws Exception {
|
||||
when(repository.findByUserId("customer-1")).thenReturn(Optional.empty());
|
||||
|
||||
InvoiceTemplate result = service.getCustomerTemplateByUserId("customer-1");
|
||||
|
||||
assertThat(result.getUserId()).isEqualTo("customer-1");
|
||||
assertThat(result.getName()).isEqualTo("Neutral");
|
||||
assertThat(result.getId()).isNull();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
var template = mapper.readTree(mapper.readValue(result.getTemplateData(), String.class));
|
||||
assertThat(template.path("elements").size()).isGreaterThan(10);
|
||||
assertThat(template.toString()).contains("masterdata.company_name", "customer.company_name", "services.list",
|
||||
"masterdata.bank_details", "masterdata.tax_details").doesNotContain("Assecutor", "HHA", "DE6720");
|
||||
verify(repository).findByUserId("customer-1");
|
||||
verify(repository).findByUserId(InvoiceTemplateService.CUSTOMER_DEFAULT_USER_ID);
|
||||
verifyNoMoreInteractions(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void personalTemplateTakesPrecedence() {
|
||||
InvoiceTemplate personal = new InvoiceTemplate("customer-1", "Mein Briefpapier",
|
||||
"{\"elements\":[{\"type\":\"text\",\"text\":\"Eigene Vorlage\"}]}");
|
||||
when(repository.findByUserId("customer-1")).thenReturn(Optional.of(personal));
|
||||
|
||||
assertThat(service.getCustomerTemplateByUserId("customer-1")).isSameAs(personal);
|
||||
verify(repository).findByUserId("customer-1");
|
||||
verifyNoMoreInteractions(repository);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@NullAndEmptySource
|
||||
@ValueSource(strings = { " ", "null", "{}", "{\"elements\":[]}", "\"{\\\"elements\\\":[]}\"", "invalid JSON" })
|
||||
void unusableSavedTemplateFallsBackWithoutOverwritingIt(String data) {
|
||||
InvoiceTemplate existing = new InvoiceTemplate("customer-1", "Standard", data);
|
||||
when(repository.findByUserId("customer-1")).thenReturn(Optional.of(existing));
|
||||
|
||||
InvoiceTemplate result = service.getCustomerTemplateByUserId("customer-1");
|
||||
|
||||
assertThat(result.getTemplateData()).contains("neutral-company", "services.list");
|
||||
assertThat(existing.getTemplateData()).isEqualTo(data);
|
||||
verify(repository).findByUserId("customer-1");
|
||||
verify(repository).findByUserId(InvoiceTemplateService.CUSTOMER_DEFAULT_USER_ID);
|
||||
verifyNoMoreInteractions(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonEncodedPersonalTemplateTakesPrecedence() throws Exception {
|
||||
InvoiceTemplate personal = new InvoiceTemplate("customer-1", "Standard", new ObjectMapper()
|
||||
.writeValueAsString("{\"elements\":[{\"type\":\"text\",\"text\":\"Eigene Vorlage\"}]}"));
|
||||
when(repository.findByUserId("customer-1")).thenReturn(Optional.of(personal));
|
||||
|
||||
assertThat(service.getCustomerTemplateByUserId("customer-1")).isSameAs(personal);
|
||||
}
|
||||
|
||||
@Test
|
||||
void systemTemplateDoesNotFallBackToCustomerDefault() {
|
||||
when(repository.findByUserId("SYSTEM")).thenReturn(Optional.empty());
|
||||
|
||||
assertThat(service.getTemplateByUserId("SYSTEM")).isEmpty();
|
||||
assertThatThrownBy(() -> service.getCustomerTemplateByUserId("SYSTEM"))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> service.getCustomerTemplateByUserId(InvoiceTemplateService.CUSTOMER_DEFAULT_USER_ID))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sharedDefaultIsLoadedAsAnUnsavedCustomerTemplate() {
|
||||
InvoiceTemplate shared = new InvoiceTemplate(InvoiceTemplateService.CUSTOMER_DEFAULT_USER_ID, "Neutral",
|
||||
"{\"elements\":[{\"type\":\"text\",\"text\":\"Gemeinsamer Standard\"}]}");
|
||||
shared.setId("shared-template-id");
|
||||
when(repository.findByUserId("customer-1")).thenReturn(Optional.empty());
|
||||
when(repository.findByUserId(InvoiceTemplateService.CUSTOMER_DEFAULT_USER_ID)).thenReturn(Optional.of(shared));
|
||||
|
||||
InvoiceTemplate result = service.getCustomerTemplateByUserId("customer-1");
|
||||
|
||||
assertThat(result.getUserId()).isEqualTo("customer-1");
|
||||
assertThat(result.getId()).isNull();
|
||||
assertThat(result.getTemplateData()).isEqualTo(shared.getTemplateData());
|
||||
assertThat(shared.getUserId()).isEqualTo(InvoiceTemplateService.CUSTOMER_DEFAULT_USER_ID);
|
||||
verify(repository).findByUserId("customer-1");
|
||||
verify(repository).findByUserId(InvoiceTemplateService.CUSTOMER_DEFAULT_USER_ID);
|
||||
verifyNoMoreInteractions(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidSharedDefaultUsesBundledNeutralTemplate() {
|
||||
when(repository.findByUserId("customer-1")).thenReturn(Optional.empty());
|
||||
when(repository.findByUserId(InvoiceTemplateService.CUSTOMER_DEFAULT_USER_ID))
|
||||
.thenReturn(Optional.of(new InvoiceTemplate(InvoiceTemplateService.CUSTOMER_DEFAULT_USER_ID, "Neutral", "{}")));
|
||||
|
||||
assertThat(service.getCustomerTemplateByUserId("customer-1").getTemplateData()).contains("neutral-company");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultCanBeSavedAsPersonalTemplate() {
|
||||
when(repository.findByUserId("customer-1")).thenReturn(Optional.empty());
|
||||
String templateData = service.getCustomerTemplateByUserId("customer-1").getTemplateData();
|
||||
var saved = org.mockito.ArgumentCaptor.forClass(InvoiceTemplate.class);
|
||||
|
||||
service.saveTemplate("customer-1", templateData);
|
||||
|
||||
verify(repository).save(saved.capture());
|
||||
assertThat(saved.getValue().getUserId()).isEqualTo("customer-1");
|
||||
assertThat(saved.getValue().getTemplateData()).isEqualTo(templateData);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in new issue
Block a user