Added and fixed invoice creation
This commit is contained in:
parent
6177976a6f
commit
3d8963e197
|
|
@ -94,9 +94,18 @@
|
|||
<version>8.0.1.Final</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.itextpdf</groupId>
|
||||
<artifactId>itext7-core</artifactId>
|
||||
<version>8.0.3</version>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,26 @@
|
|||
package com.denniseckerskorn.controllers.finance_management;
|
||||
|
||||
|
||||
import com.denniseckerskorn.dtos.finance_management_dtos.CreateInvoiceLineDTO;
|
||||
import com.denniseckerskorn.dtos.finance_management_dtos.CreateInvoiceRequestDTO;
|
||||
import com.denniseckerskorn.dtos.finance_management_dtos.InvoiceDTO;
|
||||
import com.denniseckerskorn.entities.finance.Invoice;
|
||||
import com.denniseckerskorn.entities.finance.InvoiceLine;
|
||||
import com.denniseckerskorn.entities.finance.ProductService;
|
||||
import com.denniseckerskorn.entities.user_managment.users.User;
|
||||
import com.denniseckerskorn.exceptions.DuplicateEntityException;
|
||||
import com.denniseckerskorn.exceptions.EntityNotFoundException;
|
||||
import com.denniseckerskorn.exceptions.InvalidDataException;
|
||||
import com.denniseckerskorn.services.finance_service.InvoicePdfGenerator;
|
||||
import com.denniseckerskorn.services.finance_services.InvoiceService;
|
||||
import com.denniseckerskorn.services.finance_services.ProductServiceService;
|
||||
import com.denniseckerskorn.services.user_managment_services.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
|
@ -24,11 +33,56 @@ import java.util.stream.Collectors;
|
|||
public class InvoiceController {
|
||||
|
||||
private final InvoiceService invoiceService;
|
||||
private final UserService userService;
|
||||
private final ProductServiceService productServiceService;
|
||||
private final InvoicePdfGenerator invoicePdfGenerator;
|
||||
|
||||
public InvoiceController(InvoiceService invoiceService) {
|
||||
public InvoiceController(InvoiceService invoiceService, UserService userService, ProductServiceService productServiceService, InvoicePdfGenerator invoicePdfGenerator) {
|
||||
this.invoiceService = invoiceService;
|
||||
this.userService = userService;
|
||||
this.productServiceService = productServiceService;
|
||||
this.invoicePdfGenerator = invoicePdfGenerator;
|
||||
}
|
||||
|
||||
@PostMapping("/createInvoiceWithLines")
|
||||
@Operation(summary = "Create a new invoice with lines")
|
||||
public ResponseEntity<InvoiceDTO> createInvoiceWithLines(@Valid @RequestBody CreateInvoiceRequestDTO dto) throws EntityNotFoundException, InvalidDataException {
|
||||
User user = userService.findById(dto.getUserId());
|
||||
if (user == null) {
|
||||
throw new EntityNotFoundException("User not found");
|
||||
}
|
||||
|
||||
Invoice invoice = dto.toEntity(user);
|
||||
invoiceService.save(invoice);
|
||||
|
||||
if (dto.getLines() != null && !dto.getLines().isEmpty()) {
|
||||
for (CreateInvoiceLineDTO lineDTO : dto.getLines()) {
|
||||
ProductService product = productServiceService.findById(lineDTO.getProductServiceId());
|
||||
if (product == null) {
|
||||
throw new EntityNotFoundException("Product/Service not found");
|
||||
}
|
||||
InvoiceLine line = lineDTO.toEntity(invoice, product);
|
||||
invoiceService.addLineToInvoiceById(invoice.getId(), line);
|
||||
}
|
||||
}
|
||||
invoiceService.recalculateTotal(invoice);
|
||||
return new ResponseEntity<>(new InvoiceDTO(invoice), HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@GetMapping("/generatePDFById/{id}")
|
||||
@Operation(summary = "Generar y descargar factura en PDF")
|
||||
public ResponseEntity<byte[]> downloadInvoicePdf(@PathVariable Integer id) throws EntityNotFoundException {
|
||||
Invoice invoice = invoiceService.findById(id);
|
||||
|
||||
byte[] pdf = invoicePdfGenerator.generateInvoicePdf(invoice);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=factura_" + id + ".pdf")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(pdf);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "Create a new invoice")
|
||||
public ResponseEntity<InvoiceDTO> createInvoice(@Valid @RequestBody InvoiceDTO dto) throws DuplicateEntityException {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package com.denniseckerskorn.dtos.finance_management_dtos;
|
||||
|
||||
import com.denniseckerskorn.entities.finance.Invoice;
|
||||
import com.denniseckerskorn.entities.finance.InvoiceLine;
|
||||
import com.denniseckerskorn.entities.finance.ProductService;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class CreateInvoiceLineDTO {
|
||||
|
||||
private Integer productServiceId;
|
||||
private int quantity;
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
public CreateInvoiceLineDTO() {
|
||||
}
|
||||
|
||||
public Integer getProductServiceId() {
|
||||
return productServiceId;
|
||||
}
|
||||
|
||||
public void setProductServiceId(Integer productServiceId) {
|
||||
this.productServiceId = productServiceId;
|
||||
}
|
||||
|
||||
public int getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public void setQuantity(int quantity) {
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
public BigDecimal getUnitPrice() {
|
||||
return unitPrice;
|
||||
}
|
||||
|
||||
public void setUnitPrice(BigDecimal unitPrice) {
|
||||
this.unitPrice = unitPrice;
|
||||
}
|
||||
|
||||
// Convierte a entidad con parámetros necesarios
|
||||
public InvoiceLine toEntity(Invoice invoice, ProductService productService) {
|
||||
InvoiceLine entity = new InvoiceLine();
|
||||
entity.setInvoice(invoice);
|
||||
entity.setProductService(productService);
|
||||
entity.setQuantity(this.quantity);
|
||||
entity.setUnitPrice(this.unitPrice);
|
||||
entity.setSubtotal(this.unitPrice.multiply(BigDecimal.valueOf(this.quantity)));
|
||||
entity.setDescription(productService.getName()); // opcional
|
||||
return entity;
|
||||
}
|
||||
|
||||
// Convierte desde una entidad (opcional)
|
||||
public static CreateInvoiceLineDTO fromEntity(InvoiceLine entity) {
|
||||
CreateInvoiceLineDTO dto = new CreateInvoiceLineDTO();
|
||||
dto.setProductServiceId(entity.getProductService() != null ? entity.getProductService().getId() : null);
|
||||
dto.setQuantity(entity.getQuantity());
|
||||
dto.setUnitPrice(entity.getUnitPrice());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.denniseckerskorn.dtos.finance_management_dtos;
|
||||
|
||||
import com.denniseckerskorn.entities.finance.Invoice;
|
||||
import com.denniseckerskorn.entities.user_managment.users.User;
|
||||
import com.denniseckerskorn.enums.StatusValues;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CreateInvoiceRequestDTO {
|
||||
|
||||
private Integer userId;
|
||||
private LocalDateTime date;
|
||||
private StatusValues status;
|
||||
private BigDecimal total;
|
||||
private Set<CreateInvoiceLineDTO> lines;
|
||||
|
||||
public CreateInvoiceRequestDTO() {
|
||||
}
|
||||
|
||||
// Getters y setters
|
||||
|
||||
public Integer getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Integer userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public LocalDateTime getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDateTime date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public StatusValues getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(StatusValues status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public BigDecimal getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(BigDecimal total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public Set<CreateInvoiceLineDTO> getLines() {
|
||||
return lines;
|
||||
}
|
||||
|
||||
public void setLines(Set<CreateInvoiceLineDTO> lines) {
|
||||
this.lines = lines;
|
||||
}
|
||||
|
||||
// Convierte el DTO a entidad base (sin líneas, requiere setUser)
|
||||
public Invoice toEntity(User user) {
|
||||
Invoice invoice = new Invoice();
|
||||
invoice.setUser(user);
|
||||
invoice.setDate(this.date);
|
||||
invoice.setStatus(this.status);
|
||||
invoice.setTotal(this.total);
|
||||
return invoice;
|
||||
}
|
||||
|
||||
// Crea el DTO desde una entidad (opcional)
|
||||
public static CreateInvoiceRequestDTO fromEntity(Invoice invoice) {
|
||||
CreateInvoiceRequestDTO dto = new CreateInvoiceRequestDTO();
|
||||
dto.setUserId(invoice.getUser() != null ? invoice.getUser().getId().intValue() : null);
|
||||
dto.setDate(invoice.getDate());
|
||||
dto.setStatus(invoice.getStatus());
|
||||
dto.setTotal(invoice.getTotal());
|
||||
dto.setLines(invoice.getInvoiceLines() != null ?
|
||||
invoice.getInvoiceLines().stream()
|
||||
.map(CreateInvoiceLineDTO::fromEntity)
|
||||
.collect(Collectors.toSet()) :
|
||||
null);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.denniseckerskorn.services.finance_service;
|
||||
|
||||
import com.denniseckerskorn.entities.finance.Invoice;
|
||||
import com.itextpdf.kernel.pdf.PdfWriter;
|
||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
||||
import com.itextpdf.layout.Document;
|
||||
import com.itextpdf.layout.element.Paragraph;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
@Service
|
||||
public class InvoicePdfGenerator {
|
||||
|
||||
public byte[] generateInvoicePdf(Invoice invoice) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
PdfWriter writer = new PdfWriter(out);
|
||||
PdfDocument pdf = new PdfDocument(writer);
|
||||
Document doc = new Document(pdf);
|
||||
|
||||
doc.add(new Paragraph("Factura #" + invoice.getId()));
|
||||
doc.add(new Paragraph("Fecha: " + invoice.getDate()));
|
||||
doc.add(new Paragraph("Cliente: " + invoice.getUser().getName() + " " + invoice.getUser().getSurname()));
|
||||
doc.add(new Paragraph("Estado: " + invoice.getStatus()));
|
||||
|
||||
doc.add(new Paragraph(" "));
|
||||
doc.add(new Paragraph("Detalle de productos:"));
|
||||
|
||||
invoice.getInvoiceLines().forEach(line -> {
|
||||
String product = line.getProductService().getName();
|
||||
int qty = line.getQuantity();
|
||||
var price = line.getUnitPrice();
|
||||
var total = line.getSubtotal();
|
||||
doc.add(new Paragraph("- " + product + " x" + qty + " - " + price + " € = " + total + " €"));
|
||||
});
|
||||
|
||||
doc.add(new Paragraph(" "));
|
||||
doc.add(new Paragraph("TOTAL: " + invoice.getTotal() + " €"));
|
||||
|
||||
doc.close();
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import InvoiceLineItem from "./InvoiceLineItem";
|
||||
import ErrorMessage from "../common/ErrorMessage";
|
||||
import api from "../../api/axiosConfig";
|
||||
|
||||
const InvoiceForm = () => {
|
||||
|
|
@ -8,10 +9,11 @@ const InvoiceForm = () => {
|
|||
const [date, setDate] = useState(new Date().toISOString().slice(0, 16));
|
||||
const [lines, setLines] = useState([]);
|
||||
const [products, setProducts] = useState([]);
|
||||
const [error, setError] = useState("");
|
||||
const [createdInvoiceId, setCreatedInvoiceId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
api.get("/students/getAll").then((res) => setStudents(res.data));
|
||||
|
||||
api.get("/products-services/getAll").then((res) => setProducts(res.data));
|
||||
}, []);
|
||||
|
||||
|
|
@ -29,96 +31,143 @@ const InvoiceForm = () => {
|
|||
setLines(lines.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const createInvoice = async () => {
|
||||
if (!userId || lines.length === 0) {
|
||||
alert("Selecciona un estudiante y al menos un producto.");
|
||||
return;
|
||||
}
|
||||
|
||||
const invoiceRes = await api.post("/invoices/create", {
|
||||
user: { id: parseInt(userId) },
|
||||
date,
|
||||
total: 0,
|
||||
status: "NOT_PAID",
|
||||
invoiceLineIds: [],
|
||||
});
|
||||
|
||||
const invoice = invoiceRes.data;
|
||||
|
||||
for (const line of lines) {
|
||||
const calculateTotal = () => {
|
||||
return lines.reduce((acc, line) => {
|
||||
const product = products.find(
|
||||
(p) => p.id === parseInt(line.productServiceId)
|
||||
);
|
||||
const subtotal = product.price * line.quantity;
|
||||
if (!product) return acc;
|
||||
return acc + product.price * line.quantity;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
await api.post(`/invoices/addLinesByInvoiceId/${invoice.id}`, {
|
||||
productServiceId: parseInt(line.productServiceId),
|
||||
quantity: parseInt(line.quantity),
|
||||
unitPrice: product.price,
|
||||
subtotal,
|
||||
description: product.description,
|
||||
});
|
||||
const createInvoice = async () => {
|
||||
setError("");
|
||||
setCreatedInvoiceId(null);
|
||||
|
||||
if (!userId || lines.length === 0) {
|
||||
setError("Selecciona un estudiante y al menos un producto.");
|
||||
return;
|
||||
}
|
||||
|
||||
await api.put(`/invoices/recalculateTotalOfInvoiceById/${invoice.id}`);
|
||||
try {
|
||||
const preparedLines = lines.map((line) => {
|
||||
const product = products.find(
|
||||
(p) => p.id === parseInt(line.productServiceId)
|
||||
);
|
||||
return {
|
||||
productServiceId: parseInt(line.productServiceId),
|
||||
quantity: parseInt(line.quantity),
|
||||
unitPrice: product.price,
|
||||
};
|
||||
});
|
||||
|
||||
alert("Factura creada correctamente.");
|
||||
setUserId("");
|
||||
setLines([]);
|
||||
const payload = {
|
||||
userId: parseInt(userId),
|
||||
date: new Date(date).toISOString(),
|
||||
status: "NOT_PAID",
|
||||
total: calculateTotal(),
|
||||
lines: preparedLines,
|
||||
};
|
||||
|
||||
const res = await api.post("/invoices/createInvoiceWithLines", payload);
|
||||
const invoiceId = res.data.id;
|
||||
|
||||
setCreatedInvoiceId(invoiceId);
|
||||
alert("✅ Factura creada correctamente.");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError("❌ Error al crear la factura: " + (err.response?.data?.message || err.message));
|
||||
}
|
||||
};
|
||||
|
||||
const downloadPdf = async () => {
|
||||
try {
|
||||
const response = await api.get(`/invoices/generatePDFById/${createdInvoiceId}`, {
|
||||
responseType: "blob", // para recibir el PDF correctamente
|
||||
});
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", `factura_${createdInvoiceId}.pdf`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError("❌ Error al descargar el PDF.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="content-area">
|
||||
<h2>Crear Factura</h2>
|
||||
<div className="card">
|
||||
<h2>Crear Factura</h2>
|
||||
|
||||
<label>Seleccionar estudiante:</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={userId}
|
||||
onChange={(e) => setUserId(e.target.value)}
|
||||
>
|
||||
<option value="">-- Selecciona un estudiante --</option>
|
||||
{students.map((s) =>
|
||||
s.user ? (
|
||||
<option key={s.id} value={s.user.id}>
|
||||
{s.user.name} {s.user.lastName} ({s.user.email})
|
||||
</option>
|
||||
) : null
|
||||
)}
|
||||
</select>
|
||||
|
||||
<label>Fecha de emisión:</label>
|
||||
<input
|
||||
className="form-input"
|
||||
type="datetime-local"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
/>
|
||||
|
||||
<h3>Productos / Servicios</h3>
|
||||
|
||||
{lines.map((line, i) => (
|
||||
<InvoiceLineItem
|
||||
key={i}
|
||||
index={i}
|
||||
line={line}
|
||||
products={products}
|
||||
onUpdate={updateLine}
|
||||
onRemove={removeLine}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<button className="btn btn-secondary" onClick={addLine}>
|
||||
+ Añadir Producto
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={createInvoice}
|
||||
style={{ marginLeft: "1rem" }}
|
||||
<label>Seleccionar estudiante:</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={userId}
|
||||
onChange={(e) => setUserId(e.target.value)}
|
||||
>
|
||||
✅ Crear Factura
|
||||
</button>
|
||||
<option value="">-- Selecciona un estudiante --</option>
|
||||
{students.map((s) =>
|
||||
s.user ? (
|
||||
<option key={s.id} value={s.user.id}>
|
||||
{s.user.name} {s.user.lastName} ({s.user.email})
|
||||
</option>
|
||||
) : null
|
||||
)}
|
||||
</select>
|
||||
|
||||
<label>Fecha de emisión:</label>
|
||||
<input
|
||||
className="form-input"
|
||||
type="datetime-local"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
/>
|
||||
|
||||
<h3>Productos / Servicios</h3>
|
||||
|
||||
{lines.map((line, i) => (
|
||||
<InvoiceLineItem
|
||||
key={i}
|
||||
index={i}
|
||||
line={line}
|
||||
products={products}
|
||||
onUpdate={updateLine}
|
||||
onRemove={removeLine}
|
||||
/>
|
||||
))}
|
||||
|
||||
<h4 style={{ marginTop: "1rem" }}>
|
||||
Total: <strong>{calculateTotal().toFixed(2)} €</strong>
|
||||
</h4>
|
||||
|
||||
<ErrorMessage message={error} />
|
||||
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<button className="btn btn-secondary" onClick={addLine}>
|
||||
+ Añadir Producto
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={createInvoice}
|
||||
style={{ marginLeft: "1rem" }}
|
||||
>
|
||||
✅ Crear Factura
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{createdInvoiceId && (
|
||||
<div style={{ marginTop: "2rem" }}>
|
||||
<h4>Factura creada: #{createdInvoiceId}</h4>
|
||||
<button className="btn btn-primary" onClick={downloadPdf}>
|
||||
📄 Descargar PDF
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const SidebarAdmin = () => {
|
|||
<div className="submenu">
|
||||
<h4>👥 Administración de Usuarios</h4>
|
||||
<button onClick={() => navigate('/admin/user-management/users/create')}>➕ Crear Usuario</button>
|
||||
<button onClick={() => navigate('/admin/user-management/users/list')}>📋 Ve Usuarios</button>
|
||||
<button onClick={() => navigate('/admin/user-management/users/list')}>📋 Ver Usuarios</button>
|
||||
<button onClick={() => navigate('/admin/user-management/notifications/create')}>🔔 Crear Notificación</button>
|
||||
<button onClick={() => navigate('/admin/user-management/notifications/list')}>📨 Ver Notificaciones</button>
|
||||
<button onClick={() => navigate('/admin/user-management/student-history/create')}>🕒 Crear Historial</button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue