fix invoice and support costs
This commit is contained in:
@@ -35,6 +35,10 @@ dependencies {
|
|||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
implementation 'io.github.openhtmltopdf:openhtmltopdf-pdfbox:1.1.37'
|
implementation 'io.github.openhtmltopdf:openhtmltopdf-pdfbox:1.1.37'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||||
|
implementation 'net.codecrete.qrbill:qrbill-generator:3.4.0'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import com.printcalculator.dto.*;
|
|||||||
import com.printcalculator.entity.*;
|
import com.printcalculator.entity.*;
|
||||||
import com.printcalculator.repository.*;
|
import com.printcalculator.repository.*;
|
||||||
import com.printcalculator.service.InvoicePdfRenderingService;
|
import com.printcalculator.service.InvoicePdfRenderingService;
|
||||||
|
import com.printcalculator.service.OrderService;
|
||||||
|
import com.printcalculator.service.QrBillService;
|
||||||
import com.printcalculator.service.StorageService;
|
import com.printcalculator.service.StorageService;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -28,6 +30,7 @@ import java.util.stream.Collectors;
|
|||||||
@RequestMapping("/api/orders")
|
@RequestMapping("/api/orders")
|
||||||
public class OrderController {
|
public class OrderController {
|
||||||
|
|
||||||
|
private final OrderService orderService;
|
||||||
private final OrderRepository orderRepo;
|
private final OrderRepository orderRepo;
|
||||||
private final OrderItemRepository orderItemRepo;
|
private final OrderItemRepository orderItemRepo;
|
||||||
private final QuoteSessionRepository quoteSessionRepo;
|
private final QuoteSessionRepository quoteSessionRepo;
|
||||||
@@ -35,15 +38,19 @@ public class OrderController {
|
|||||||
private final CustomerRepository customerRepo;
|
private final CustomerRepository customerRepo;
|
||||||
private final StorageService storageService;
|
private final StorageService storageService;
|
||||||
private final InvoicePdfRenderingService invoiceService;
|
private final InvoicePdfRenderingService invoiceService;
|
||||||
|
private final QrBillService qrBillService;
|
||||||
|
|
||||||
|
|
||||||
public OrderController(OrderRepository orderRepo,
|
public OrderController(OrderService orderService,
|
||||||
|
OrderRepository orderRepo,
|
||||||
OrderItemRepository orderItemRepo,
|
OrderItemRepository orderItemRepo,
|
||||||
QuoteSessionRepository quoteSessionRepo,
|
QuoteSessionRepository quoteSessionRepo,
|
||||||
QuoteLineItemRepository quoteLineItemRepo,
|
QuoteLineItemRepository quoteLineItemRepo,
|
||||||
CustomerRepository customerRepo,
|
CustomerRepository customerRepo,
|
||||||
StorageService storageService,
|
StorageService storageService,
|
||||||
InvoicePdfRenderingService invoiceService) {
|
InvoicePdfRenderingService invoiceService,
|
||||||
|
QrBillService qrBillService) {
|
||||||
|
this.orderService = orderService;
|
||||||
this.orderRepo = orderRepo;
|
this.orderRepo = orderRepo;
|
||||||
this.orderItemRepo = orderItemRepo;
|
this.orderItemRepo = orderItemRepo;
|
||||||
this.quoteSessionRepo = quoteSessionRepo;
|
this.quoteSessionRepo = quoteSessionRepo;
|
||||||
@@ -51,6 +58,7 @@ public class OrderController {
|
|||||||
this.customerRepo = customerRepo;
|
this.customerRepo = customerRepo;
|
||||||
this.storageService = storageService;
|
this.storageService = storageService;
|
||||||
this.invoiceService = invoiceService;
|
this.invoiceService = invoiceService;
|
||||||
|
this.qrBillService = qrBillService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -61,144 +69,9 @@ public class OrderController {
|
|||||||
@PathVariable UUID quoteSessionId,
|
@PathVariable UUID quoteSessionId,
|
||||||
@RequestBody com.printcalculator.dto.CreateOrderRequest request
|
@RequestBody com.printcalculator.dto.CreateOrderRequest request
|
||||||
) {
|
) {
|
||||||
QuoteSession session = quoteSessionRepo.findById(quoteSessionId)
|
Order order = orderService.createOrderFromQuote(quoteSessionId, request);
|
||||||
.orElseThrow(() -> new RuntimeException("Quote Session not found"));
|
List<OrderItem> items = orderItemRepo.findByOrder_Id(order.getId());
|
||||||
|
return ResponseEntity.ok(convertToDto(order, items));
|
||||||
if (session.getConvertedOrderId() != null) {
|
|
||||||
return ResponseEntity.badRequest().body(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
Customer customer = customerRepo.findByEmail(request.getCustomer().getEmail())
|
|
||||||
.orElseGet(() -> {
|
|
||||||
Customer newC = new Customer();
|
|
||||||
newC.setEmail(request.getCustomer().getEmail());
|
|
||||||
newC.setCustomerType(request.getCustomer().getCustomerType());
|
|
||||||
newC.setCreatedAt(OffsetDateTime.now());
|
|
||||||
newC.setUpdatedAt(OffsetDateTime.now());
|
|
||||||
return customerRepo.save(newC);
|
|
||||||
});
|
|
||||||
|
|
||||||
customer.setPhone(request.getCustomer().getPhone());
|
|
||||||
customer.setCustomerType(request.getCustomer().getCustomerType());
|
|
||||||
customer.setUpdatedAt(OffsetDateTime.now());
|
|
||||||
customerRepo.save(customer);
|
|
||||||
|
|
||||||
Order order = new Order();
|
|
||||||
order.setSourceQuoteSession(session);
|
|
||||||
order.setCustomer(customer);
|
|
||||||
order.setCustomerEmail(request.getCustomer().getEmail());
|
|
||||||
order.setCustomerPhone(request.getCustomer().getPhone());
|
|
||||||
order.setStatus("PENDING_PAYMENT");
|
|
||||||
order.setCreatedAt(OffsetDateTime.now());
|
|
||||||
order.setUpdatedAt(OffsetDateTime.now());
|
|
||||||
order.setCurrency("CHF");
|
|
||||||
|
|
||||||
order.setBillingCustomerType(request.getCustomer().getCustomerType());
|
|
||||||
if (request.getBillingAddress() != null) {
|
|
||||||
order.setBillingFirstName(request.getBillingAddress().getFirstName());
|
|
||||||
order.setBillingLastName(request.getBillingAddress().getLastName());
|
|
||||||
order.setBillingCompanyName(request.getBillingAddress().getCompanyName());
|
|
||||||
order.setBillingContactPerson(request.getBillingAddress().getContactPerson());
|
|
||||||
order.setBillingAddressLine1(request.getBillingAddress().getAddressLine1());
|
|
||||||
order.setBillingAddressLine2(request.getBillingAddress().getAddressLine2());
|
|
||||||
order.setBillingZip(request.getBillingAddress().getZip());
|
|
||||||
order.setBillingCity(request.getBillingAddress().getCity());
|
|
||||||
order.setBillingCountryCode(request.getBillingAddress().getCountryCode() != null ? request.getBillingAddress().getCountryCode() : "CH");
|
|
||||||
}
|
|
||||||
|
|
||||||
order.setShippingSameAsBilling(request.isShippingSameAsBilling());
|
|
||||||
if (!request.isShippingSameAsBilling() && request.getShippingAddress() != null) {
|
|
||||||
order.setShippingFirstName(request.getShippingAddress().getFirstName());
|
|
||||||
order.setShippingLastName(request.getShippingAddress().getLastName());
|
|
||||||
order.setShippingCompanyName(request.getShippingAddress().getCompanyName());
|
|
||||||
order.setShippingContactPerson(request.getShippingAddress().getContactPerson());
|
|
||||||
order.setShippingAddressLine1(request.getShippingAddress().getAddressLine1());
|
|
||||||
order.setShippingAddressLine2(request.getShippingAddress().getAddressLine2());
|
|
||||||
order.setShippingZip(request.getShippingAddress().getZip());
|
|
||||||
order.setShippingCity(request.getShippingAddress().getCity());
|
|
||||||
order.setShippingCountryCode(request.getShippingAddress().getCountryCode() != null ? request.getShippingAddress().getCountryCode() : "CH");
|
|
||||||
} else {
|
|
||||||
order.setShippingFirstName(order.getBillingFirstName());
|
|
||||||
order.setShippingLastName(order.getBillingLastName());
|
|
||||||
order.setShippingCompanyName(order.getBillingCompanyName());
|
|
||||||
order.setShippingContactPerson(order.getBillingContactPerson());
|
|
||||||
order.setShippingAddressLine1(order.getBillingAddressLine1());
|
|
||||||
order.setShippingAddressLine2(order.getBillingAddressLine2());
|
|
||||||
order.setShippingZip(order.getBillingZip());
|
|
||||||
order.setShippingCity(order.getBillingCity());
|
|
||||||
order.setShippingCountryCode(order.getBillingCountryCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<QuoteLineItem> quoteItems = quoteLineItemRepo.findByQuoteSessionId(quoteSessionId);
|
|
||||||
|
|
||||||
BigDecimal subtotal = BigDecimal.ZERO;
|
|
||||||
order.setSubtotalChf(BigDecimal.ZERO);
|
|
||||||
order.setTotalChf(BigDecimal.ZERO);
|
|
||||||
order.setDiscountChf(BigDecimal.ZERO);
|
|
||||||
order.setSetupCostChf(session.getSetupCostChf() != null ? session.getSetupCostChf() : BigDecimal.ZERO);
|
|
||||||
order.setShippingCostChf(BigDecimal.valueOf(9.00));
|
|
||||||
|
|
||||||
order = orderRepo.save(order);
|
|
||||||
|
|
||||||
for (QuoteLineItem qItem : quoteItems) {
|
|
||||||
OrderItem oItem = new OrderItem();
|
|
||||||
oItem.setOrder(order);
|
|
||||||
oItem.setOriginalFilename(qItem.getOriginalFilename());
|
|
||||||
oItem.setQuantity(qItem.getQuantity());
|
|
||||||
oItem.setColorCode(qItem.getColorCode());
|
|
||||||
oItem.setMaterialCode(session.getMaterialCode());
|
|
||||||
|
|
||||||
oItem.setUnitPriceChf(qItem.getUnitPriceChf());
|
|
||||||
oItem.setLineTotalChf(qItem.getUnitPriceChf().multiply(BigDecimal.valueOf(qItem.getQuantity())));
|
|
||||||
oItem.setPrintTimeSeconds(qItem.getPrintTimeSeconds());
|
|
||||||
oItem.setMaterialGrams(qItem.getMaterialGrams());
|
|
||||||
|
|
||||||
UUID fileUuid = UUID.randomUUID();
|
|
||||||
String ext = getExtension(qItem.getOriginalFilename());
|
|
||||||
String storedFilename = fileUuid.toString() + "." + ext;
|
|
||||||
|
|
||||||
oItem.setStoredFilename(storedFilename);
|
|
||||||
oItem.setStoredRelativePath("PENDING");
|
|
||||||
oItem.setMimeType("application/octet-stream");
|
|
||||||
oItem.setCreatedAt(OffsetDateTime.now());
|
|
||||||
|
|
||||||
oItem = orderItemRepo.save(oItem);
|
|
||||||
|
|
||||||
String relativePath = "orders/" + order.getId() + "/3d-files/" + oItem.getId() + "/" + storedFilename;
|
|
||||||
oItem.setStoredRelativePath(relativePath);
|
|
||||||
|
|
||||||
if (qItem.getStoredPath() != null) {
|
|
||||||
try {
|
|
||||||
Path sourcePath = Paths.get(qItem.getStoredPath());
|
|
||||||
if (Files.exists(sourcePath)) {
|
|
||||||
storageService.store(sourcePath, Paths.get(relativePath));
|
|
||||||
oItem.setFileSizeBytes(Files.size(sourcePath));
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
orderItemRepo.save(oItem);
|
|
||||||
subtotal = subtotal.add(oItem.getLineTotalChf());
|
|
||||||
}
|
|
||||||
|
|
||||||
order.setSubtotalChf(subtotal);
|
|
||||||
if (order.getShippingCostChf() == null) {
|
|
||||||
order.setShippingCostChf(BigDecimal.valueOf(9.00));
|
|
||||||
}
|
|
||||||
|
|
||||||
BigDecimal total = subtotal.add(order.getSetupCostChf()).add(order.getShippingCostChf()).subtract(order.getDiscountChf() != null ? order.getDiscountChf() : BigDecimal.ZERO);
|
|
||||||
order.setTotalChf(total);
|
|
||||||
|
|
||||||
session.setConvertedOrderId(order.getId());
|
|
||||||
session.setStatus("CONVERTED");
|
|
||||||
quoteSessionRepo.save(session);
|
|
||||||
|
|
||||||
order = orderRepo.save(order);
|
|
||||||
List<OrderItem> finalItems = orderItemRepo.findByOrder_Id(order.getId());
|
|
||||||
|
|
||||||
return ResponseEntity.ok(convertToDto(order, finalItems));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/{orderId}/items/{orderItemId}/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(value = "/{orderId}/items/{orderItemId}/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@@ -295,7 +168,8 @@ public class OrderController {
|
|||||||
vars.put("grandTotalFormatted", String.format("CHF %.2f", order.getTotalChf()));
|
vars.put("grandTotalFormatted", String.format("CHF %.2f", order.getTotalChf()));
|
||||||
vars.put("paymentTermsText", "Pagamento entro 7 giorni via Bonifico o TWINT. Grazie.");
|
vars.put("paymentTermsText", "Pagamento entro 7 giorni via Bonifico o TWINT. Grazie.");
|
||||||
|
|
||||||
byte[] pdf = invoiceService.generateInvoicePdfBytesFromTemplate(vars);
|
String qrBillSvg = new String(qrBillService.generateQrBillSvg(order));
|
||||||
|
byte[] pdf = invoiceService.generateInvoicePdfBytesFromTemplate(vars, qrBillSvg);
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header("Content-Disposition", "attachment; filename=\"invoice-" + orderId + ".pdf\"")
|
.header("Content-Disposition", "attachment; filename=\"invoice-" + orderId + ".pdf\"")
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public class QuoteController {
|
|||||||
@RequestParam(value = "infill_pattern", required = false) String infillPattern,
|
@RequestParam(value = "infill_pattern", required = false) String infillPattern,
|
||||||
@RequestParam(value = "layer_height", required = false) Double layerHeight,
|
@RequestParam(value = "layer_height", required = false) Double layerHeight,
|
||||||
@RequestParam(value = "nozzle_diameter", required = false) Double nozzleDiameter,
|
@RequestParam(value = "nozzle_diameter", required = false) Double nozzleDiameter,
|
||||||
@RequestParam(value = "support_enabled", required = false) Boolean supportEnabled
|
@RequestParam(value = "support_enabled", required = false, defaultValue = "true") Boolean supportEnabled
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
|
|
||||||
// ... process selection logic ...
|
// ... process selection logic ...
|
||||||
@@ -72,6 +72,9 @@ public class QuoteController {
|
|||||||
}
|
}
|
||||||
if (supportEnabled != null) {
|
if (supportEnabled != null) {
|
||||||
processOverrides.put("enable_support", supportEnabled ? "1" : "0");
|
processOverrides.put("enable_support", supportEnabled ? "1" : "0");
|
||||||
|
if (supportEnabled) {
|
||||||
|
processOverrides.put("support_threshold_angle", "45");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nozzleDiameter != null) {
|
if (nozzleDiameter != null) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class QuoteSessionController {
|
|||||||
// Default material/settings will be set when items are added or updated?
|
// Default material/settings will be set when items are added or updated?
|
||||||
// For now set safe defaults
|
// For now set safe defaults
|
||||||
session.setMaterialCode("pla_basic");
|
session.setMaterialCode("pla_basic");
|
||||||
session.setSupportsEnabled(false);
|
session.setSupportsEnabled(true);
|
||||||
session.setCreatedAt(OffsetDateTime.now());
|
session.setCreatedAt(OffsetDateTime.now());
|
||||||
session.setExpiresAt(OffsetDateTime.now().plusDays(30));
|
session.setExpiresAt(OffsetDateTime.now().plusDays(30));
|
||||||
|
|
||||||
@@ -178,7 +178,14 @@ public class QuoteSessionController {
|
|||||||
if (settings.getLayerHeight() != null) processOverrides.put("layer_height", String.valueOf(settings.getLayerHeight()));
|
if (settings.getLayerHeight() != null) processOverrides.put("layer_height", String.valueOf(settings.getLayerHeight()));
|
||||||
if (settings.getInfillDensity() != null) processOverrides.put("sparse_infill_density", settings.getInfillDensity() + "%");
|
if (settings.getInfillDensity() != null) processOverrides.put("sparse_infill_density", settings.getInfillDensity() + "%");
|
||||||
if (settings.getInfillPattern() != null) processOverrides.put("sparse_infill_pattern", settings.getInfillPattern());
|
if (settings.getInfillPattern() != null) processOverrides.put("sparse_infill_pattern", settings.getInfillPattern());
|
||||||
if (Boolean.TRUE.equals(settings.getSupportsEnabled())) processOverrides.put("enable_support", "1");
|
if (settings.getSupportsEnabled() != null) {
|
||||||
|
processOverrides.put("enable_support", settings.getSupportsEnabled() ? "1" : "0");
|
||||||
|
// If enabled, use a more permissive threshold (45 deg) by default
|
||||||
|
// to avoid expensive supports on things that don't strictly need them
|
||||||
|
if (settings.getSupportsEnabled()) {
|
||||||
|
processOverrides.put("support_threshold_angle", "45");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, String> machineOverrides = new HashMap<>();
|
Map<String, String> machineOverrides = new HashMap<>();
|
||||||
if (settings.getNozzleDiameter() != null) {
|
if (settings.getNozzleDiameter() != null) {
|
||||||
@@ -207,8 +214,8 @@ public class QuoteSessionController {
|
|||||||
item.setColorCode(settings.getColor() != null ? settings.getColor() : "#FFFFFF");
|
item.setColorCode(settings.getColor() != null ? settings.getColor() : "#FFFFFF");
|
||||||
item.setStatus("READY"); // or CALCULATED
|
item.setStatus("READY"); // or CALCULATED
|
||||||
|
|
||||||
item.setPrintTimeSeconds((int) stats.printTimeSeconds());
|
item.setPrintTimeSeconds((int) stats.getPrintTimeSeconds());
|
||||||
item.setMaterialGrams(BigDecimal.valueOf(stats.filamentWeightGrams()));
|
item.setMaterialGrams(BigDecimal.valueOf(stats.getFilamentWeightGrams()));
|
||||||
item.setUnitPriceChf(BigDecimal.valueOf(result.getTotalPrice()));
|
item.setUnitPriceChf(BigDecimal.valueOf(result.getTotalPrice()));
|
||||||
|
|
||||||
// Store breakdown
|
// Store breakdown
|
||||||
@@ -267,13 +274,14 @@ public class QuoteSessionController {
|
|||||||
if (settings.getNozzleDiameter() == null) settings.setNozzleDiameter(0.4);
|
if (settings.getNozzleDiameter() == null) settings.setNozzleDiameter(0.4);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (settings.getSupportsEnabled() == null) settings.setSupportsEnabled(true);
|
||||||
} else {
|
} else {
|
||||||
// ADVANCED Mode: Use values from Frontend, set defaults if missing
|
// ADVANCED Mode: Use values from Frontend, set defaults if missing
|
||||||
if (settings.getLayerHeight() == null) settings.setLayerHeight(0.20);
|
if (settings.getLayerHeight() == null) settings.setLayerHeight(0.20);
|
||||||
if (settings.getInfillDensity() == null) settings.setInfillDensity(20.0);
|
if (settings.getInfillDensity() == null) settings.setInfillDensity(20.0);
|
||||||
if (settings.getInfillPattern() == null) settings.setInfillPattern("grid");
|
if (settings.getInfillPattern() == null) settings.setInfillPattern("grid");
|
||||||
if (settings.getNozzleDiameter() == null) settings.setNozzleDiameter(0.4);
|
if (settings.getNozzleDiameter() == null) settings.setNozzleDiameter(0.4);
|
||||||
if (settings.getSupportsEnabled() == null) settings.setSupportsEnabled(false);
|
if (settings.getSupportsEnabled() == null) settings.setSupportsEnabled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class PrintSettingsDto {
|
|||||||
private Double layerHeight;
|
private Double layerHeight;
|
||||||
private Double infillDensity;
|
private Double infillDensity;
|
||||||
private String infillPattern;
|
private String infillPattern;
|
||||||
private Boolean supportsEnabled;
|
private Boolean supportsEnabled = true;
|
||||||
private Double nozzleDiameter;
|
private Double nozzleDiameter;
|
||||||
private String notes;
|
private String notes;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -410,4 +410,5 @@ public class Order {
|
|||||||
this.paidAt = paidAt;
|
this.paidAt = paidAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,29 @@
|
|||||||
package com.printcalculator.model;
|
package com.printcalculator.model;
|
||||||
|
|
||||||
public record PrintStats(
|
import lombok.AllArgsConstructor;
|
||||||
long printTimeSeconds,
|
import lombok.Builder;
|
||||||
String printTimeFormatted,
|
import lombok.Data;
|
||||||
double filamentWeightGrams,
|
import lombok.NoArgsConstructor;
|
||||||
double filamentLengthMm
|
|
||||||
) {}
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class PrintStats {
|
||||||
|
private long printTimeSeconds;
|
||||||
|
private String printTimeFormatted;
|
||||||
|
private double filamentWeightGrams;
|
||||||
|
private double filamentLengthMm;
|
||||||
|
|
||||||
|
// Breakdown if available
|
||||||
|
private Double modelWeightGrams;
|
||||||
|
private Double supportWeightGrams;
|
||||||
|
|
||||||
|
// Legacy constructor for compatibility
|
||||||
|
public PrintStats(long printTimeSeconds, String printTimeFormatted, double filamentWeightGrams, double filamentLengthMm) {
|
||||||
|
this.printTimeSeconds = printTimeSeconds;
|
||||||
|
this.printTimeFormatted = printTimeFormatted;
|
||||||
|
this.filamentWeightGrams = filamentWeightGrams;
|
||||||
|
this.filamentLengthMm = filamentLengthMm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,13 +26,15 @@ public class GCodeParser {
|
|||||||
private static final Pattern TIME_PATTERN = Pattern.compile(
|
private static final Pattern TIME_PATTERN = Pattern.compile(
|
||||||
";\\s*(?:estimated\\s+printing\\s+time|estimated\\s+print\\s+time|print\\s+time).*?[:=]\\s*(.*)",
|
";\\s*(?:estimated\\s+printing\\s+time|estimated\\s+print\\s+time|print\\s+time).*?[:=]\\s*(.*)",
|
||||||
Pattern.CASE_INSENSITIVE);
|
Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern FILAMENT_G_PATTERN = Pattern.compile(";\\s*filament used \\[g\\]\\s*=\\s*(.*)");
|
private static final Pattern FILAMENT_G_PATTERN = Pattern.compile(";\\s*filament used \\[g\\]\\s*=\\s*([^;\\(\\n\\r]+)(?:\\s*\\(([^,]+) model,\\s*([^ ]+) support\\))?");
|
||||||
private static final Pattern FILAMENT_MM_PATTERN = Pattern.compile(";\\s*filament used \\[mm\\]\\s*=\\s*(.*)");
|
private static final Pattern FILAMENT_MM_PATTERN = Pattern.compile(";\\s*filament used \\[mm\\]\\s*=\\s*(.*)");
|
||||||
|
|
||||||
public PrintStats parse(File gcodeFile) throws IOException {
|
public PrintStats parse(File gcodeFile) throws IOException {
|
||||||
long seconds = 0;
|
long seconds = 0;
|
||||||
double weightG = 0;
|
double weightG = 0;
|
||||||
double lengthMm = 0;
|
double lengthMm = 0;
|
||||||
|
Double modelWeightG = null;
|
||||||
|
Double supportWeightG = null;
|
||||||
String timeFormatted = "";
|
String timeFormatted = "";
|
||||||
|
|
||||||
try (BufferedReader reader = new BufferedReader(new FileReader(gcodeFile))) {
|
try (BufferedReader reader = new BufferedReader(new FileReader(gcodeFile))) {
|
||||||
@@ -78,7 +80,14 @@ public class GCodeParser {
|
|||||||
if (weightMatcher.find()) {
|
if (weightMatcher.find()) {
|
||||||
try {
|
try {
|
||||||
weightG = Double.parseDouble(weightMatcher.group(1).trim());
|
weightG = Double.parseDouble(weightMatcher.group(1).trim());
|
||||||
System.out.println("GCodeParser: Found weight: " + weightG + "g");
|
System.out.println("GCodeParser: Found total weight: " + weightG + "g");
|
||||||
|
|
||||||
|
// Check if we have groups 2 and 3 for breakdown
|
||||||
|
if (weightMatcher.groupCount() >= 3 && weightMatcher.group(2) != null) {
|
||||||
|
modelWeightG = Double.parseDouble(weightMatcher.group(2).trim());
|
||||||
|
supportWeightG = Double.parseDouble(weightMatcher.group(3).trim());
|
||||||
|
System.out.println("GCodeParser: Found breakdown - Model: " + modelWeightG + "g, Support: " + supportWeightG + "g");
|
||||||
|
}
|
||||||
} catch (NumberFormatException ignored) {}
|
} catch (NumberFormatException ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +101,14 @@ public class GCodeParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PrintStats(seconds, timeFormatted, weightG, lengthMm);
|
return PrintStats.builder()
|
||||||
|
.printTimeSeconds(seconds)
|
||||||
|
.printTimeFormatted(timeFormatted)
|
||||||
|
.filamentWeightGrams(weightG)
|
||||||
|
.filamentLengthMm(lengthMm)
|
||||||
|
.modelWeightGrams(modelWeightG)
|
||||||
|
.supportWeightGrams(supportWeightG)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private long parseTimeString(String timeStr) {
|
private long parseTimeString(String timeStr) {
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ public class InvoicePdfRenderingService {
|
|||||||
this.thymeleafTemplateEngine = thymeleafTemplateEngine;
|
this.thymeleafTemplateEngine = thymeleafTemplateEngine;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] generateInvoicePdfBytesFromTemplate(Map<String, Object> invoiceTemplateVariables) {
|
public byte[] generateInvoicePdfBytesFromTemplate(Map<String, Object> invoiceTemplateVariables, String qrBillSvg) {
|
||||||
try {
|
try {
|
||||||
Context thymeleafContextWithInvoiceData = new Context(Locale.ITALY);
|
Context thymeleafContextWithInvoiceData = new Context(Locale.ITALY);
|
||||||
thymeleafContextWithInvoiceData.setVariables(invoiceTemplateVariables);
|
thymeleafContextWithInvoiceData.setVariables(invoiceTemplateVariables);
|
||||||
|
thymeleafContextWithInvoiceData.setVariable("qrBillSvg", qrBillSvg);
|
||||||
|
|
||||||
String renderedInvoiceHtml = thymeleafTemplateEngine.process("invoice", thymeleafContextWithInvoiceData);
|
String renderedInvoiceHtml = thymeleafTemplateEngine.process("invoice", thymeleafContextWithInvoiceData);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,292 @@
|
|||||||
|
package com.printcalculator.service;
|
||||||
|
|
||||||
|
import com.printcalculator.dto.AddressDto;
|
||||||
|
import com.printcalculator.dto.CreateOrderRequest;
|
||||||
|
import com.printcalculator.entity.*;
|
||||||
|
import com.printcalculator.repository.CustomerRepository;
|
||||||
|
import com.printcalculator.repository.OrderItemRepository;
|
||||||
|
import com.printcalculator.repository.OrderRepository;
|
||||||
|
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||||
|
import com.printcalculator.repository.QuoteSessionRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OrderService {
|
||||||
|
|
||||||
|
private final OrderRepository orderRepo;
|
||||||
|
private final OrderItemRepository orderItemRepo;
|
||||||
|
private final QuoteSessionRepository quoteSessionRepo;
|
||||||
|
private final QuoteLineItemRepository quoteLineItemRepo;
|
||||||
|
private final CustomerRepository customerRepo;
|
||||||
|
private final StorageService storageService;
|
||||||
|
private final InvoicePdfRenderingService invoiceService;
|
||||||
|
private final QrBillService qrBillService;
|
||||||
|
|
||||||
|
public OrderService(OrderRepository orderRepo,
|
||||||
|
OrderItemRepository orderItemRepo,
|
||||||
|
QuoteSessionRepository quoteSessionRepo,
|
||||||
|
QuoteLineItemRepository quoteLineItemRepo,
|
||||||
|
CustomerRepository customerRepo,
|
||||||
|
StorageService storageService,
|
||||||
|
InvoicePdfRenderingService invoiceService,
|
||||||
|
QrBillService qrBillService) {
|
||||||
|
this.orderRepo = orderRepo;
|
||||||
|
this.orderItemRepo = orderItemRepo;
|
||||||
|
this.quoteSessionRepo = quoteSessionRepo;
|
||||||
|
this.quoteLineItemRepo = quoteLineItemRepo;
|
||||||
|
this.customerRepo = customerRepo;
|
||||||
|
this.storageService = storageService;
|
||||||
|
this.invoiceService = invoiceService;
|
||||||
|
this.qrBillService = qrBillService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Order createOrderFromQuote(UUID quoteSessionId, CreateOrderRequest request) {
|
||||||
|
QuoteSession session = quoteSessionRepo.findById(quoteSessionId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Quote Session not found"));
|
||||||
|
|
||||||
|
if (session.getConvertedOrderId() != null) {
|
||||||
|
throw new IllegalStateException("Quote session already converted to order");
|
||||||
|
}
|
||||||
|
|
||||||
|
Customer customer = customerRepo.findByEmail(request.getCustomer().getEmail())
|
||||||
|
.orElseGet(() -> {
|
||||||
|
Customer newC = new Customer();
|
||||||
|
newC.setEmail(request.getCustomer().getEmail());
|
||||||
|
newC.setCustomerType(request.getCustomer().getCustomerType());
|
||||||
|
newC.setCreatedAt(OffsetDateTime.now());
|
||||||
|
newC.setUpdatedAt(OffsetDateTime.now());
|
||||||
|
return customerRepo.save(newC);
|
||||||
|
});
|
||||||
|
|
||||||
|
customer.setPhone(request.getCustomer().getPhone());
|
||||||
|
customer.setCustomerType(request.getCustomer().getCustomerType());
|
||||||
|
customer.setUpdatedAt(OffsetDateTime.now());
|
||||||
|
customerRepo.save(customer);
|
||||||
|
|
||||||
|
Order order = new Order();
|
||||||
|
order.setSourceQuoteSession(session);
|
||||||
|
order.setCustomer(customer);
|
||||||
|
order.setCustomerEmail(request.getCustomer().getEmail());
|
||||||
|
order.setCustomerPhone(request.getCustomer().getPhone());
|
||||||
|
order.setStatus("PENDING_PAYMENT");
|
||||||
|
order.setCreatedAt(OffsetDateTime.now());
|
||||||
|
order.setUpdatedAt(OffsetDateTime.now());
|
||||||
|
order.setCurrency("CHF");
|
||||||
|
|
||||||
|
order.setBillingCustomerType(request.getCustomer().getCustomerType());
|
||||||
|
if (request.getBillingAddress() != null) {
|
||||||
|
order.setBillingFirstName(request.getBillingAddress().getFirstName());
|
||||||
|
order.setBillingLastName(request.getBillingAddress().getLastName());
|
||||||
|
order.setBillingCompanyName(request.getBillingAddress().getCompanyName());
|
||||||
|
order.setBillingContactPerson(request.getBillingAddress().getContactPerson());
|
||||||
|
order.setBillingAddressLine1(request.getBillingAddress().getAddressLine1());
|
||||||
|
order.setBillingAddressLine2(request.getBillingAddress().getAddressLine2());
|
||||||
|
order.setBillingZip(request.getBillingAddress().getZip());
|
||||||
|
order.setBillingCity(request.getBillingAddress().getCity());
|
||||||
|
order.setBillingCountryCode(request.getBillingAddress().getCountryCode() != null ? request.getBillingAddress().getCountryCode() : "CH");
|
||||||
|
}
|
||||||
|
|
||||||
|
order.setShippingSameAsBilling(request.isShippingSameAsBilling());
|
||||||
|
if (!request.isShippingSameAsBilling() && request.getShippingAddress() != null) {
|
||||||
|
order.setShippingFirstName(request.getShippingAddress().getFirstName());
|
||||||
|
order.setShippingLastName(request.getShippingAddress().getLastName());
|
||||||
|
order.setShippingCompanyName(request.getShippingAddress().getCompanyName());
|
||||||
|
order.setShippingContactPerson(request.getShippingAddress().getContactPerson());
|
||||||
|
order.setShippingAddressLine1(request.getShippingAddress().getAddressLine1());
|
||||||
|
order.setShippingAddressLine2(request.getShippingAddress().getAddressLine2());
|
||||||
|
order.setShippingZip(request.getShippingAddress().getZip());
|
||||||
|
order.setShippingCity(request.getShippingAddress().getCity());
|
||||||
|
order.setShippingCountryCode(request.getShippingAddress().getCountryCode() != null ? request.getShippingAddress().getCountryCode() : "CH");
|
||||||
|
} else {
|
||||||
|
order.setShippingFirstName(order.getBillingFirstName());
|
||||||
|
order.setShippingLastName(order.getBillingLastName());
|
||||||
|
order.setShippingCompanyName(order.getBillingCompanyName());
|
||||||
|
order.setShippingContactPerson(order.getBillingContactPerson());
|
||||||
|
order.setShippingAddressLine1(order.getBillingAddressLine1());
|
||||||
|
order.setShippingAddressLine2(order.getBillingAddressLine2());
|
||||||
|
order.setShippingZip(order.getBillingZip());
|
||||||
|
order.setShippingCity(order.getBillingCity());
|
||||||
|
order.setShippingCountryCode(order.getBillingCountryCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<QuoteLineItem> quoteItems = quoteLineItemRepo.findByQuoteSessionId(quoteSessionId);
|
||||||
|
|
||||||
|
BigDecimal subtotal = BigDecimal.ZERO;
|
||||||
|
order.setSubtotalChf(BigDecimal.ZERO);
|
||||||
|
order.setTotalChf(BigDecimal.ZERO);
|
||||||
|
order.setDiscountChf(BigDecimal.ZERO);
|
||||||
|
order.setSetupCostChf(session.getSetupCostChf() != null ? session.getSetupCostChf() : BigDecimal.ZERO);
|
||||||
|
order.setShippingCostChf(BigDecimal.valueOf(9.00));
|
||||||
|
|
||||||
|
order = orderRepo.save(order);
|
||||||
|
|
||||||
|
List<OrderItem> savedItems = new ArrayList<>();
|
||||||
|
|
||||||
|
for (QuoteLineItem qItem : quoteItems) {
|
||||||
|
OrderItem oItem = new OrderItem();
|
||||||
|
oItem.setOrder(order);
|
||||||
|
oItem.setOriginalFilename(qItem.getOriginalFilename());
|
||||||
|
oItem.setQuantity(qItem.getQuantity());
|
||||||
|
oItem.setColorCode(qItem.getColorCode());
|
||||||
|
oItem.setMaterialCode(session.getMaterialCode());
|
||||||
|
|
||||||
|
oItem.setUnitPriceChf(qItem.getUnitPriceChf());
|
||||||
|
oItem.setLineTotalChf(qItem.getUnitPriceChf().multiply(BigDecimal.valueOf(qItem.getQuantity())));
|
||||||
|
oItem.setPrintTimeSeconds(qItem.getPrintTimeSeconds());
|
||||||
|
oItem.setMaterialGrams(qItem.getMaterialGrams());
|
||||||
|
|
||||||
|
UUID fileUuid = UUID.randomUUID();
|
||||||
|
String ext = getExtension(qItem.getOriginalFilename());
|
||||||
|
String storedFilename = fileUuid.toString() + "." + ext;
|
||||||
|
|
||||||
|
oItem.setStoredFilename(storedFilename);
|
||||||
|
oItem.setStoredRelativePath("PENDING");
|
||||||
|
oItem.setMimeType("application/octet-stream");
|
||||||
|
oItem.setCreatedAt(OffsetDateTime.now());
|
||||||
|
|
||||||
|
oItem = orderItemRepo.save(oItem);
|
||||||
|
|
||||||
|
String relativePath = "orders/" + order.getId() + "/3d-files/" + oItem.getId() + "/" + storedFilename;
|
||||||
|
oItem.setStoredRelativePath(relativePath);
|
||||||
|
|
||||||
|
if (qItem.getStoredPath() != null) {
|
||||||
|
try {
|
||||||
|
Path sourcePath = Paths.get(qItem.getStoredPath());
|
||||||
|
if (Files.exists(sourcePath)) {
|
||||||
|
storageService.store(sourcePath, Paths.get(relativePath));
|
||||||
|
oItem.setFileSizeBytes(Files.size(sourcePath));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oItem = orderItemRepo.save(oItem);
|
||||||
|
savedItems.add(oItem);
|
||||||
|
subtotal = subtotal.add(oItem.getLineTotalChf());
|
||||||
|
}
|
||||||
|
|
||||||
|
order.setSubtotalChf(subtotal);
|
||||||
|
if (order.getShippingCostChf() == null) {
|
||||||
|
order.setShippingCostChf(BigDecimal.valueOf(9.00));
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal total = subtotal.add(order.getSetupCostChf()).add(order.getShippingCostChf()).subtract(order.getDiscountChf() != null ? order.getDiscountChf() : BigDecimal.ZERO);
|
||||||
|
order.setTotalChf(total);
|
||||||
|
|
||||||
|
session.setConvertedOrderId(order.getId());
|
||||||
|
session.setStatus("CONVERTED");
|
||||||
|
quoteSessionRepo.save(session);
|
||||||
|
|
||||||
|
// Generate Invoice and QR Bill
|
||||||
|
generateAndSaveDocuments(order, savedItems);
|
||||||
|
|
||||||
|
return orderRepo.save(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateAndSaveDocuments(Order order, List<OrderItem> items) {
|
||||||
|
try {
|
||||||
|
// 1. Generate QR Bill
|
||||||
|
byte[] qrBillSvgBytes = qrBillService.generateQrBillSvg(order);
|
||||||
|
String qrBillSvg = new String(qrBillSvgBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// Save QR Bill SVG
|
||||||
|
String qrRelativePath = "orders/" + order.getId() + "/documents/qr-bill.svg";
|
||||||
|
saveFileBytes(qrBillSvgBytes, qrRelativePath);
|
||||||
|
|
||||||
|
// 2. Prepare Invoice Variables
|
||||||
|
Map<String, Object> vars = new HashMap<>();
|
||||||
|
vars.put("sellerDisplayName", "3D Fab Switzerland");
|
||||||
|
vars.put("sellerAddressLine1", "Sede Ticino, Svizzera");
|
||||||
|
vars.put("sellerAddressLine2", "Sede Bienne, Svizzera");
|
||||||
|
vars.put("sellerEmail", "info@3dfab.ch");
|
||||||
|
|
||||||
|
vars.put("invoiceNumber", "INV-" + order.getId().toString().substring(0, 8).toUpperCase());
|
||||||
|
vars.put("invoiceDate", order.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE));
|
||||||
|
vars.put("dueDate", order.getCreatedAt().plusDays(7).format(DateTimeFormatter.ISO_LOCAL_DATE));
|
||||||
|
|
||||||
|
String buyerName = "BUSINESS".equals(order.getBillingCustomerType())
|
||||||
|
? order.getBillingCompanyName()
|
||||||
|
: order.getBillingFirstName() + " " + order.getBillingLastName();
|
||||||
|
vars.put("buyerDisplayName", buyerName);
|
||||||
|
vars.put("buyerAddressLine1", order.getBillingAddressLine1());
|
||||||
|
vars.put("buyerAddressLine2", order.getBillingZip() + " " + order.getBillingCity() + ", " + order.getBillingCountryCode());
|
||||||
|
|
||||||
|
List<Map<String, Object>> invoiceLineItems = items.stream().map(i -> {
|
||||||
|
Map<String, Object> line = new HashMap<>();
|
||||||
|
line.put("description", "Stampa 3D: " + i.getOriginalFilename());
|
||||||
|
line.put("quantity", i.getQuantity());
|
||||||
|
line.put("unitPriceFormatted", String.format("CHF %.2f", i.getUnitPriceChf()));
|
||||||
|
line.put("lineTotalFormatted", String.format("CHF %.2f", i.getLineTotalChf()));
|
||||||
|
return line;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
Map<String, Object> setupLine = new HashMap<>();
|
||||||
|
setupLine.put("description", "Costo Setup");
|
||||||
|
setupLine.put("quantity", 1);
|
||||||
|
setupLine.put("unitPriceFormatted", String.format("CHF %.2f", order.getSetupCostChf()));
|
||||||
|
setupLine.put("lineTotalFormatted", String.format("CHF %.2f", order.getSetupCostChf()));
|
||||||
|
invoiceLineItems.add(setupLine);
|
||||||
|
|
||||||
|
Map<String, Object> shippingLine = new HashMap<>();
|
||||||
|
shippingLine.put("description", "Spedizione");
|
||||||
|
shippingLine.put("quantity", 1);
|
||||||
|
shippingLine.put("unitPriceFormatted", String.format("CHF %.2f", order.getShippingCostChf()));
|
||||||
|
shippingLine.put("lineTotalFormatted", String.format("CHF %.2f", order.getShippingCostChf()));
|
||||||
|
invoiceLineItems.add(shippingLine);
|
||||||
|
|
||||||
|
vars.put("invoiceLineItems", invoiceLineItems);
|
||||||
|
vars.put("subtotalFormatted", String.format("CHF %.2f", order.getSubtotalChf()));
|
||||||
|
vars.put("grandTotalFormatted", String.format("CHF %.2f", order.getTotalChf()));
|
||||||
|
vars.put("paymentTermsText", "Appena riceviamo il pagamento l'ordine entrerà nella coda di stampa. Grazie per la fiducia");
|
||||||
|
|
||||||
|
// 3. Generate PDF
|
||||||
|
byte[] pdfBytes = invoiceService.generateInvoicePdfBytesFromTemplate(vars, qrBillSvg);
|
||||||
|
|
||||||
|
// Save PDF
|
||||||
|
String pdfRelativePath = "orders/" + order.getId() + "/documents/invoice-" + order.getId() + ".pdf";
|
||||||
|
saveFileBytes(pdfBytes, pdfRelativePath);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
// Don't fail the order if document generation fails, but log it
|
||||||
|
// TODO: Better error handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveFileBytes(byte[] content, String relativePath) {
|
||||||
|
// Since StorageService takes paths, we might need to write to temp first or check if it supports bytes/streams
|
||||||
|
// Simulating via temp file for now as StorageService.store takes a Path
|
||||||
|
try {
|
||||||
|
Path tempFile = Files.createTempFile("print-calc-upload", ".tmp");
|
||||||
|
Files.write(tempFile, content);
|
||||||
|
storageService.store(tempFile, Paths.get(relativePath));
|
||||||
|
Files.delete(tempFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to save file " + relativePath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getExtension(String filename) {
|
||||||
|
if (filename == null) return "stl";
|
||||||
|
int i = filename.lastIndexOf('.');
|
||||||
|
if (i > 0) {
|
||||||
|
return filename.substring(i + 1);
|
||||||
|
}
|
||||||
|
return "stl";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.printcalculator.service;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.Order;
|
||||||
|
import net.codecrete.qrbill.generator.Bill;
|
||||||
|
import net.codecrete.qrbill.generator.GraphicsFormat;
|
||||||
|
import net.codecrete.qrbill.generator.QRBill;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class QrBillService {
|
||||||
|
|
||||||
|
public byte[] generateQrBillSvg(Order order) {
|
||||||
|
Bill bill = createBillFromOrder(order);
|
||||||
|
return QRBill.generate(bill);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bill createBillFromOrder(Order order) {
|
||||||
|
Bill bill = new Bill();
|
||||||
|
|
||||||
|
// Creditor (Merchant)
|
||||||
|
bill.setAccount("CH7409000000154821581"); // TODO: Configurable IBAN
|
||||||
|
bill.setCreditor(createAddress(
|
||||||
|
"Küng, Joe",
|
||||||
|
"Via G. Pioda 29a",
|
||||||
|
"6710",
|
||||||
|
"Biasca",
|
||||||
|
"CH"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Debtor (Customer)
|
||||||
|
String debtorName;
|
||||||
|
if ("BUSINESS".equals(order.getBillingCustomerType())) {
|
||||||
|
debtorName = order.getBillingCompanyName();
|
||||||
|
} else {
|
||||||
|
debtorName = order.getBillingFirstName() + " " + order.getBillingLastName();
|
||||||
|
}
|
||||||
|
|
||||||
|
bill.setDebtor(createAddress(
|
||||||
|
debtorName,
|
||||||
|
order.getBillingAddressLine1(), // Assuming simple address for now. Splitting might be needed if street/house number are separate
|
||||||
|
order.getBillingZip(),
|
||||||
|
order.getBillingCity(),
|
||||||
|
order.getBillingCountryCode()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
bill.setAmount(order.getTotalChf());
|
||||||
|
bill.setCurrency("CHF");
|
||||||
|
|
||||||
|
// Reference
|
||||||
|
// bill.setReference(QRBill.createCreditorReference("...")); // If using QRR
|
||||||
|
bill.setUnstructuredMessage("Order " + order.getId());
|
||||||
|
|
||||||
|
return bill;
|
||||||
|
}
|
||||||
|
|
||||||
|
private net.codecrete.qrbill.generator.Address createAddress(String name, String street, String zip, String city, String country) {
|
||||||
|
net.codecrete.qrbill.generator.Address address = new net.codecrete.qrbill.generator.Address();
|
||||||
|
address.setName(name);
|
||||||
|
address.setStreet(street);
|
||||||
|
address.setPostalCode(zip);
|
||||||
|
address.setTown(city);
|
||||||
|
address.setCountryCode(country);
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,11 +76,21 @@ public class QuoteCalculator {
|
|||||||
// --- CALCULATIONS ---
|
// --- CALCULATIONS ---
|
||||||
|
|
||||||
// Material Cost: (weight / 1000) * costPerKg
|
// Material Cost: (weight / 1000) * costPerKg
|
||||||
BigDecimal weightKg = BigDecimal.valueOf(stats.filamentWeightGrams()).divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
// DISCOUNTED Support material to avoid penalizing users for default supports
|
||||||
|
BigDecimal weightToCharge;
|
||||||
|
if (stats.getModelWeightGrams() != null && stats.getSupportWeightGrams() != null) {
|
||||||
|
// Charge 100% for model + 20% for support
|
||||||
|
weightToCharge = BigDecimal.valueOf(stats.getModelWeightGrams())
|
||||||
|
.add(BigDecimal.valueOf(stats.getSupportWeightGrams()).multiply(BigDecimal.valueOf(0.2)));
|
||||||
|
} else {
|
||||||
|
weightToCharge = BigDecimal.valueOf(stats.getFilamentWeightGrams());
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal weightKg = weightToCharge.divide(BigDecimal.valueOf(1000), 4, RoundingMode.HALF_UP);
|
||||||
BigDecimal materialCost = weightKg.multiply(variant.getCostChfPerKg());
|
BigDecimal materialCost = weightKg.multiply(variant.getCostChfPerKg());
|
||||||
|
|
||||||
// Machine Cost: Tiered
|
// Machine Cost: Tiered
|
||||||
BigDecimal totalHours = BigDecimal.valueOf(stats.printTimeSeconds()).divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
BigDecimal totalHours = BigDecimal.valueOf(stats.getPrintTimeSeconds()).divide(BigDecimal.valueOf(3600), 4, RoundingMode.HALF_UP);
|
||||||
BigDecimal machineCost = calculateMachineCost(policy, totalHours);
|
BigDecimal machineCost = calculateMachineCost(policy, totalHours);
|
||||||
|
|
||||||
// Energy Cost: (watts / 1000) * hours * costPerKwh
|
// Energy Cost: (watts / 1000) * hours * costPerKwh
|
||||||
|
|||||||
@@ -76,8 +76,9 @@
|
|||||||
Pagamento entro 7 giorni. Grazie.
|
Pagamento entro 7 giorni. Grazie.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagina dedicata alla QR-bill (vuota) -->
|
<!-- Pagina dedicata alla QR-bill -->
|
||||||
<div class="page-break"></div>
|
<div class="page-break"></div>
|
||||||
|
<div th:utext="${qrBillSvg}" style="width: 210mm; height: 105mm; position: absolute; bottom: 0; left: 0;"></div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class UploadFormComponent implements OnInit {
|
|||||||
layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]],
|
layerHeight: [0.2, [Validators.min(0.05), Validators.max(1.0)]],
|
||||||
nozzleDiameter: [0.4, Validators.required],
|
nozzleDiameter: [0.4, Validators.required],
|
||||||
infillPattern: ['grid'],
|
infillPattern: ['grid'],
|
||||||
supportEnabled: [false]
|
supportEnabled: [true]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen to material changes to update variants
|
// Listen to material changes to update variants
|
||||||
|
|||||||
Reference in New Issue
Block a user