Merge branch 'mod/margenes-tirada' into 'main'

modificado los margenes por precio en lugar de por tirada y tipos

See merge request jjimenez/erp-imprimelibros!11
This commit is contained in:
2025-10-20 07:39:02 +00:00
14 changed files with 275 additions and 331 deletions

View File

@ -5,18 +5,12 @@ import jakarta.transaction.Transactional;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.common.Utils;

View File

@ -8,6 +8,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
@ -15,11 +17,17 @@ import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imprimelibros.erp.datatables.DataTablesRequest;
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices;
import com.imprimelibros.erp.presupuesto.marcapaginas.Marcapaginas;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import java.util.function.Function;
@Component
public class Utils {
@ -42,6 +50,55 @@ public class Utils {
return currencyFormatter.format(amount);
}
public static String formatNumber(BigDecimal amount, Locale locale) {
NumberFormat numberFormatter = NumberFormat.getNumberInstance(locale);
return numberFormatter.format(amount);
}
public static String formatNumber(Double amount, Locale locale) {
NumberFormat numberFormatter = NumberFormat.getNumberInstance(locale);
return numberFormatter.format(amount);
}
public static Optional<BiFunction<Path<BigDecimal>, CriteriaBuilder, Predicate>> parseNumericFilter(
DataTablesRequest dt, String colName, Locale locale) {
String raw = dt.getColumnSearch(colName); // usa el "name" del DataTable (snake_case)
if (raw == null || raw.isBlank())
return Optional.empty();
String s = raw.trim();
// normaliza número con coma o punto
Function<String, BigDecimal> toBig = x -> {
String t = x.replace(".", "").replace(",", "."); // 1.234,56 -> 1234.56
return new BigDecimal(t);
};
try {
if (s.matches("(?i)^>=?\\s*[-\\d.,]+$")) {
BigDecimal v = toBig.apply(s.replace(">=", "").replace(">", "").trim());
return Optional.of((path, cb) -> cb.greaterThanOrEqualTo(path, v));
}
if (s.matches("(?i)^<=?\\s*[-\\d.,]+$")) {
BigDecimal v = toBig.apply(s.replace("<=", "").replace("<", "").trim());
return Optional.of((path, cb) -> cb.lessThanOrEqualTo(path, v));
}
if (s.contains("-")) { // rango "a-b"
String[] p = s.split("-");
if (p.length == 2) {
BigDecimal a = toBig.apply(p[0].trim());
BigDecimal b = toBig.apply(p[1].trim());
BigDecimal min = a.min(b), max = a.max(b);
return Optional.of((path, cb) -> cb.between(path, min, max));
}
}
// exacto/like numérico
BigDecimal v = toBig.apply(s);
return Optional.of((path, cb) -> cb.equal(path, v));
} catch (Exception ignore) {
return Optional.empty();
}
}
public Map<String, Object> getTextoPresupuesto(Presupuesto presupuesto, Locale locale) {
Map<String, Object> resumen = new HashMap<>();

View File

@ -1,182 +1,126 @@
package com.imprimelibros.erp.configuracion.margenes_presupuestos;
import jakarta.persistence.*;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
import com.imprimelibros.erp.shared.validation.NoRangeOverlap;
@Entity
@Table(name = "margenes_presupuesto")
@SQLDelete(sql = "UPDATE margenes_presupuesto SET deleted = true WHERE id=?")
@SQLRestriction("deleted = false")
@NoRangeOverlap(
min = "tiradaMin",
max = "tiradaMax",
id = "id",
partitionBy = {"tipoEncuadernacion","tipoCubierta"},
partitionBy = {},
deletedFlag = "deleted", // <- si usas soft delete
deletedActiveValue = false, // activo cuando deleted == false
message = "{validation.range.overlaps}",
invalidRangeMessage = "{validation.range.invalid}"
)
@Entity
@Table(name = "margenes_presupuesto")
@SQLDelete(sql = "UPDATE margenes_presupuesto SET deleted = TRUE, deleted_at = NOW() WHERE id = ?")
@SQLRestriction("deleted = false")
public class MargenPresupuesto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name="tipo_encuadernacion", nullable = false, length = 50)
@Column(name="importe_min", nullable=false, precision=12, scale=2)
@NotNull(message="{validation.required}")
@Enumerated(EnumType.STRING)
private TipoEncuadernacion tipoEncuadernacion;
private BigDecimal importeMin;
@Column(name="tipo_cubierta", nullable = false, length = 50)
@Column(name="importe_max", nullable=false, precision=12, scale=2)
@NotNull(message="{validation.required}")
@Enumerated(EnumType.STRING)
private TipoCubierta tipoCubierta;
private BigDecimal importeMax;
@Column(name="tirada_min", nullable = false)
@Column(name="margen_min", nullable=false, precision=6, scale=2)
@NotNull(message="{validation.required}")
@Min(value=1, message="{validation.min}")
private Integer tiradaMin;
private BigDecimal margenMin;
@Column(name="tirada_max", nullable = false)
@Column(name="margen_max", nullable=false, precision=6, scale=2)
@NotNull(message="{validation.required}")
@Min(value=1, message="{validation.min}")
private Integer tiradaMax;
private BigDecimal margenMax;
@Column(name="margen_max", nullable = false)
@NotNull(message="{validation.required}")
@Min(value = 0, message="{validation.min}")
@Max(value = 200, message="{validation.max}")
private Integer margenMax;
@Column(nullable=false)
private boolean deleted = false;
@Column(name = "margen_min", nullable = false)
@NotNull(message="{validation.required}")
@Min(value = 0, message="{validation.min}")
@Max(value = 200, message="{validation.max}")
private Integer margenMin;
@Column(name="created_at", nullable = false, updatable = false)
@Column(name="created_at", nullable=false)
private LocalDateTime createdAt;
@Column(name="updated_at")
@Column(name="updated_at", nullable=false)
private LocalDateTime updatedAt;
@Column(nullable = false)
private boolean deleted = false;
@Column(name="deleted_at")
private LocalDateTime deletedAt;
@PrePersist
void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = createdAt;
}
@PreUpdate
void onUpdate() {
updatedAt = LocalDateTime.now();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public TipoEncuadernacion getTipoEncuadernacion() {
return tipoEncuadernacion;
public BigDecimal getImporteMin() {
return importeMin;
}
public void setTipoEncuadernacion(TipoEncuadernacion tipoEncuadernacion) {
this.tipoEncuadernacion = tipoEncuadernacion;
public void setImporteMin(BigDecimal importeMin) {
this.importeMin = importeMin;
}
public TipoCubierta getTipoCubierta() {
return tipoCubierta;
public BigDecimal getImporteMax() {
return importeMax;
}
public void setTipoCubierta(TipoCubierta tipoCubierta) {
this.tipoCubierta = tipoCubierta;
public void setImporteMax(BigDecimal importeMax) {
this.importeMax = importeMax;
}
public Integer getTiradaMin() {
return tiradaMin;
}
public void setTiradaMin(Integer tiradaMin) {
this.tiradaMin = tiradaMin;
}
public Integer getTiradaMax() {
return tiradaMax;
}
public void setTiradaMax(Integer tiradaMax) {
this.tiradaMax = tiradaMax;
}
public Integer getMargenMax() {
return margenMax;
}
public void setMargenMax(Integer margenMax) {
this.margenMax = margenMax;
}
public Integer getMargenMin() {
public BigDecimal getMargenMin() {
return margenMin;
}
public void setMargenMin(Integer margenMin) {
public void setMargenMin(BigDecimal margenMin) {
this.margenMin = margenMin;
}
public LocalDateTime getCreatedAt() {
return createdAt;
public BigDecimal getMargenMax() {
return margenMax;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
public void setMargenMax(BigDecimal margenMax) {
this.margenMax = margenMax;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
@PrePersist
void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = this.createdAt;
}
@PreUpdate
void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}

View File

@ -1,10 +1,9 @@
package com.imprimelibros.erp.configuracion.margenes_presupuestos;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.context.MessageSource;
import org.springframework.data.jpa.domain.Specification;
@ -23,13 +22,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.datatables.DataTable;
import com.imprimelibros.erp.datatables.DataTablesParser;
import com.imprimelibros.erp.datatables.DataTablesRequest;
import com.imprimelibros.erp.datatables.DataTablesResponse;
import com.imprimelibros.erp.i18n.TranslationService;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
import jakarta.persistence.criteria.Predicate;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -83,25 +82,46 @@ public class MargenPresupuestoController {
List<String> searchable = List.of(
"id",
"tiradaMin", "tiradaMax",
"importeMin", "importeMax",
"margenMin", "margenMax");
List<String> orderable = List.of(
"id",
"tipoEncuadernacion",
"tipoCubierta",
"tiradaMin",
"tiradaMax",
"importeMin",
"importeMax",
"margenMin",
"margenMax");
Specification<MargenPresupuesto> base = (root, query, cb) -> cb.conjunction();
Specification<MargenPresupuesto> filtros = (root, query, cb) -> {
List<Predicate> ps = new ArrayList<>();
Utils.parseNumericFilter(dt, "importe_min", locale)
.ifPresent(f -> ps.add(f.apply(root.get("importeMin"), cb)));
Utils.parseNumericFilter(dt, "importe_max", locale)
.ifPresent(f -> ps.add(f.apply(root.get("importeMax"), cb)));
Utils.parseNumericFilter(dt, "margen_min", locale)
.ifPresent(f -> ps.add(f.apply(root.get("margenMin"), cb)));
Utils.parseNumericFilter(dt, "margen_max", locale)
.ifPresent(f -> ps.add(f.apply(root.get("margenMax"), cb)));
return ps.isEmpty() ? cb.conjunction() : cb.and(ps.toArray(new Predicate[0]));
};
long total = repo.count();
return DataTable
.of(repo, MargenPresupuesto.class, dt, searchable) // 'searchable' en DataTable.java
// edita columnas "reales":
.orderable(orderable)
.edit("importeMin", (margen) -> Utils.formatCurrency(margen.getImporteMin(), locale))
.edit("importeMax", (margen) -> Utils.formatCurrency(margen.getImporteMax(), locale))
.edit("margenMin", (margen) -> Utils.formatNumber(margen.getMargenMin(), locale))
.edit("margenMax", (margen) -> Utils.formatNumber(margen.getMargenMax(), locale))
.add("actions", (margen) -> {
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
" <a href=\"javascript:void(0);\" data-id=\"" + margen.getId()
@ -110,58 +130,8 @@ public class MargenPresupuestoController {
+ "\" class=\"link-danger btn-delete-margen fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>\n"
+ " </div>";
})
.edit("tipoEncuadernacion", (margen) -> {
return messageSource.getMessage("presupuesto." + margen.getTipoEncuadernacion().name(), null,
locale);
})
.edit("tipoCubierta", (margen) -> {
return messageSource.getMessage("presupuesto." + margen.getTipoCubierta().name(), null, locale);
})
.where(base)
// Filtros custom:
.filter((builder, req) -> {
String fEncuadernacion = Optional.ofNullable(req.raw.get("f_encuadernacion")).orElse("").trim();
if (!fEncuadernacion.isEmpty()) {
boolean added = false;
// 1) Si llega el nombre del enum (p.ej. "fresado", "cosido", ...)
try {
var encEnum = TipoEncuadernacion.valueOf(fEncuadernacion);
builder.add((root, q, cb) -> cb.equal(root.get("tipoEncuadernacion"), encEnum));
added = true;
} catch (IllegalArgumentException ignored) {
}
// 2) Si llega la clave i18n (p.ej. "presupuesto.fresado", ...)
if (!added) {
Arrays.stream(TipoEncuadernacion.values())
.filter(e -> e.getMessageKey().equals(fEncuadernacion))
.findFirst()
.ifPresent(encEnum -> builder
.add((root, q, cb) -> cb.equal(root.get("tipoEncuadernacion"), encEnum)));
}
}
// --- Cubierta ---
String fCubierta = Optional.ofNullable(req.raw.get("f_cubierta")).orElse("").trim();
if (!fCubierta.isEmpty()) {
boolean added = false;
// 1) Si llega el nombre del enum (p.ej. "tapaBlanda", "tapaDura",
// "tapaDuraLomoRedondo")
try {
var cubEnum = TipoCubierta.valueOf(fCubierta);
builder.add((root, q, cb) -> cb.equal(root.get("tipoCubierta"), cubEnum));
added = true;
} catch (IllegalArgumentException ignored) {
}
// 2) Si llega la clave i18n (p.ej. "presupuesto.tapa-blanda", ...)
if (!added) {
Arrays.stream(TipoCubierta.values())
.filter(e -> e.getMessageKey().equals(fCubierta))
.findFirst()
.ifPresent(cubEnum -> builder
.add((root, q, cb) -> cb.equal(root.get("tipoCubierta"), cubEnum)));
}
}
})
.toJson(total);
}
@ -202,16 +172,14 @@ public class MargenPresupuestoController {
Locale locale) {
if (binding.hasErrors()) {
response.setStatus(422);
response.setStatus(422);
model.addAttribute("action", "/configuracion/margenes-presupuesto");
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm";
}
MargenPresupuesto data = new MargenPresupuesto();
data.setTipoEncuadernacion(margenPresupuesto.getTipoEncuadernacion());
data.setTipoCubierta(margenPresupuesto.getTipoCubierta());
data.setTiradaMin(margenPresupuesto.getTiradaMin());
data.setTiradaMax(margenPresupuesto.getTiradaMax());
data.setImporteMin(margenPresupuesto.getImporteMin());
data.setImporteMax(margenPresupuesto.getImporteMax());
data.setMargenMax(margenPresupuesto.getMargenMax());
data.setMargenMin(margenPresupuesto.getMargenMin());
@ -243,7 +211,6 @@ public class MargenPresupuestoController {
return null;
}
@PutMapping("/{id}")
public String edit(
@PathVariable Long id,
@ -268,10 +235,8 @@ public class MargenPresupuestoController {
var entity = uOpt.get();
// 3) Copiar solamente campos editables
entity.setTipoEncuadernacion(form.getTipoEncuadernacion());
entity.setTipoCubierta(form.getTipoCubierta());
entity.setTiradaMin(form.getTiradaMin());
entity.setTiradaMax(form.getTiradaMax());
entity.setImporteMin(form.getImporteMin());
entity.setImporteMax(form.getImporteMax());
entity.setMargenMax(form.getMargenMax());
entity.setMargenMin(form.getMargenMin());
@ -317,21 +282,23 @@ public class MargenPresupuestoController {
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
return repo.findById(id).map(u -> {
try {
u.setDeleted(true);
u.setDeletedAt(LocalDateTime.now());
repo.save(u); // ← NO delete(); guardamos el soft delete con deleted_by relleno
return ResponseEntity.ok(Map.of("message",
messageSource.getMessage("margenes-presupuesto.exito.eliminado", null, locale)));
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("message",
messageSource.getMessage("margenes-presupuesto.error.delete-internal-error", null, locale)));
messageSource.getMessage("margenes-presupuesto.error.delete-internal-error", null,
locale)));
}
}).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("message", messageSource.getMessage("margenes-presupuesto.error.not-found", null, locale))));
.body(Map.of("message",
messageSource.getMessage("margenes-presupuesto.error.not-found", null, locale))));
}
}

View File

@ -1,40 +1,36 @@
package com.imprimelibros.erp.configuracion.margenes_presupuestos;
import java.math.BigDecimal;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
public interface MargenPresupuestoDao
extends JpaRepository<MargenPresupuesto, Long>, JpaSpecificationExecutor<MargenPresupuesto> {
@Query("""
SELECT COUNT(m) FROM MargenPresupuesto m
WHERE m.deleted = false
AND m.tipoEncuadernacion = :enc
AND m.tipoCubierta = :cub
AND (:id IS NULL OR m.id <> :id)
AND NOT (m.tiradaMax < :min OR m.tiradaMin > :max)
""")
SELECT COUNT(m) FROM MargenPresupuesto m
WHERE m.deleted = false
AND ( ( :min BETWEEN m.importeMin AND m.importeMax )
OR ( :max BETWEEN m.importeMin AND m.importeMax )
OR ( m.importeMin BETWEEN :min AND :max )
OR ( m.importeMax BETWEEN :min AND :max ) )
AND ( :excludeId IS NULL OR m.id <> :excludeId )
""")
long countOverlaps(
@Param("enc") TipoEncuadernacion enc,
@Param("cub") TipoCubierta cub,
@Param("min") Integer min,
@Param("max") Integer max,
@Param("id") Long id);
@Param("min") BigDecimal min,
@Param("max") BigDecimal max,
@Param("excludeId") Long excludeId);
@Query("""
SELECT m FROM MargenPresupuesto m
WHERE m.deleted = false
AND m.tipoEncuadernacion = :enc
AND m.tipoCubierta = :cub
AND :tirada BETWEEN m.tiradaMin AND m.tiradaMax
""")
MargenPresupuesto findByTipoAndTirada(
@Param("enc") TipoEncuadernacion enc,
@Param("cub") TipoCubierta cub,
@Param("tirada") Integer tirada);
SELECT m FROM MargenPresupuesto m
WHERE m.deleted = false
AND :importe BETWEEN m.importeMin AND m.importeMax
""")
Optional<MargenPresupuesto> findByImporte(@Param("importe") BigDecimal importe);
}

View File

@ -1,14 +1,12 @@
package com.imprimelibros.erp.configuracion.margenes_presupuestos;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
@Service
@Transactional
public class MargenPresupuestoService {
@ -27,17 +25,19 @@ public class MargenPresupuestoService {
return dao.findById(id);
}
public MargenPresupuesto save(MargenPresupuesto entity) {
return dao.save(entity);
public Optional<MargenPresupuesto> findByImporte(BigDecimal importe){
return dao.findByImporte(importe);
}
public MargenPresupuesto save(MargenPresupuesto e) {
return dao.save(e);
}
public void delete(Long id) {
dao.deleteById(id);
}
public boolean hasOverlap(TipoEncuadernacion enc, TipoCubierta cub, Integer min, Integer max, Long excludeId) {
long count = dao.countOverlaps(enc, cub, min, max, excludeId);
return count > 0;
public boolean hasOverlap(BigDecimal min, BigDecimal max, Long excludeId) {
return dao.countOverlaps(min, max, excludeId) > 0;
}
}

View File

@ -9,15 +9,34 @@ public class DataTablesRequest {
public Search search = new Search();
public List<Order> order = new ArrayList<>();
public List<Column> columns = new ArrayList<>();
public Map<String,String> raw = new HashMap<>(); // <- params extra
public Map<String, String> raw = new HashMap<>(); // <- params extra
public static class Search {
public String value = "";
public boolean regex;
}
public static class Order {
public int column;
public String dir;
}
public static class Search { public String value=""; public boolean regex; }
public static class Order { public int column; public String dir; }
public static class Column {
public String data;
public String name;
public boolean searchable=true;
public boolean orderable=true;
public Search search=new Search();
public boolean searchable = true;
public boolean orderable = true;
public Search search = new Search();
}
public String getColumnSearch(String columnName) {
if (columnName == null || columns == null)
return null;
for (Column col : columns) {
if (col != null && col.name != null && col.name.equalsIgnoreCase(columnName)) {
return col.search != null ? col.search.value : null;
}
}
return null;
}
}

View File

@ -18,6 +18,8 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
import java.util.Map;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.List;
import java.util.function.Supplier;
@ -34,7 +36,8 @@ public class skApiClient {
private final MargenPresupuestoDao margenPresupuestoDao;
private final MessageSource messageSource;
public skApiClient(AuthService authService, MargenPresupuestoDao margenPresupuestoDao, MessageSource messageSource) {
public skApiClient(AuthService authService, MargenPresupuestoDao margenPresupuestoDao,
MessageSource messageSource) {
this.authService = authService;
this.restTemplate = new RestTemplate();
this.margenPresupuestoDao = margenPresupuestoDao;
@ -80,23 +83,28 @@ public class skApiClient {
data.get("precios"), new TypeReference<List<Double>>() {
});
for (int i = 0; i < tiradas.size(); i++) {
int tirada = tiradas.get(i);
for (int i = 0; i < precios.size(); i++) {
BigDecimal importe = new BigDecimal(precios.get(i));
MargenPresupuesto margen = margenPresupuestoDao.findByTipoAndTirada(
tipoEncuadernacion, tipoCubierta, tirada);
BigDecimal importeTotal = importe.multiply(BigDecimal.valueOf(tiradas.get(i)));
MargenPresupuesto margen = margenPresupuestoDao
.findByImporte(importeTotal).orElse(null);
if (margen != null) {
double margenValue = calcularMargen(
tirada,
margen.getTiradaMin(),
margen.getTiradaMax(),
BigDecimal margenValue = calcularMargen(
importeTotal,
margen.getImporteMin(),
margen.getImporteMax(),
margen.getMargenMax(),
margen.getMargenMin());
double nuevoPrecio = precios.get(i) * (1 + margenValue / 100.0);
precios.set(i, Math.round(nuevoPrecio * 10000.0) / 10000.0); // redondear a 2 decimales
BigDecimal nuevoPrecio = new BigDecimal(precios.get(i)).multiply(BigDecimal.ONE
.add(margenValue.divide(BigDecimal.valueOf(100), RoundingMode.HALF_UP)));
precios.set(i, nuevoPrecio.setScale(4, RoundingMode.HALF_UP).doubleValue()); // redondear
// a 4
// decimales
} else {
System.out.println("No se encontró margen para tirada " + tirada);
System.out.println("No se encontró margen para importe " + importe);
}
}
@ -154,7 +162,8 @@ public class skApiClient {
JsonNode root = mapper.readTree(jsonResponse);
if (root.get("data") == null || !root.get("data").isInt()) {
throw new RuntimeException(messageSource.getMessage("presupuesto.errores.error-interior", new Object[]{1} , locale));
throw new RuntimeException(
messageSource.getMessage("presupuesto.errores.error-interior", new Object[] { 1 }, locale));
}
return root.get("data").asInt();
@ -227,13 +236,15 @@ public class skApiClient {
}
}
private static double calcularMargen(
int tirada, int tiradaMin, int tiradaMax,
double margenMax, double margenMin) {
if (tirada <= tiradaMin)
private static BigDecimal calcularMargen(
BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax,
BigDecimal margenMax, BigDecimal margenMin) {
if (importe.compareTo(importeMin) <= 0)
return margenMax;
if (tirada >= tiradaMax)
if (importe.compareTo(importeMax) >= 0)
return margenMin;
return margenMax - ((double) (tirada - tiradaMin) / (tiradaMax - tiradaMin)) * (margenMax - margenMin);
return margenMax.subtract(margenMax.subtract(margenMin)
.multiply(importe.subtract(importeMin)
.divide(importeMax.subtract(importeMin), RoundingMode.HALF_UP)));
}
}

View File

@ -26,7 +26,7 @@ public class TamanioValidator implements ConstraintValidator<Tamanio, Presupuest
Integer max = variableService.getValorEntero("ancho_alto_max");
if (presupuesto.getAncho() <= min || presupuesto.getAncho() >= max) {
if (presupuesto.getAncho() < min || presupuesto.getAncho() > max) {
String mensajeInterpolado = messageSource.getMessage(
"presupuesto.errores.ancho.min_max", // clave del mensaje

View File

@ -6,18 +6,14 @@ margenes-presupuesto.editar=Editar margen
margenes-presupuesto.eliminar=Eliminar
margenes-presupuesto.tabla.id=ID
margenes-presupuesto.tabla.tipo_encuadernacion=Tipo encuadernación
margenes-presupuesto.tabla.tipo_cubierta=Tipo cubierta
margenes-presupuesto.tabla.tirada_minima=Tirada Mín.
margenes-presupuesto.tabla.tirada_maxima=Tirada Máx.
margenes-presupuesto.tabla.importe_minimo=Importe Mín.
margenes-presupuesto.tabla.importe_maximo=Importe Máx.
margenes-presupuesto.tabla.margen_minimo=Margen Mín.
margenes-presupuesto.tabla.margen_maximo=Margen Máx.
margenes-presupuesto.tabla.acciones=Acciones
margenes-presupuesto.form.tipo_encuadernacion=Tipo de encuadernación
margenes-presupuesto.form.tipo_cubierta=Tipo de cubierta
margenes-presupuesto.form.tirada_minima=Tirada mínima
margenes-presupuesto.form.tirada_maxima=Tirada máxima
margenes-presupuesto.form.importe_minimo=Importe mínimo
margenes-presupuesto.form.importe_maximo=Importe máximo
margenes-presupuesto.form.margen_minimo=Margen mínimo (%)
margenes-presupuesto.form.margen_maximo=Margen máximo (%)

View File

@ -1,3 +1,5 @@
import {normalizeNumericFilter} from '../../utils.js';
(() => {
// si jQuery está cargado, añade CSRF a AJAX
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
@ -22,7 +24,6 @@
processing: true,
serverSide: true,
orderCellsTop: true,
stateSave: true,
pageLength: 50,
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
responsive: true,
@ -45,18 +46,12 @@
ajax: {
url: '/configuracion/margenes-presupuesto/datatable',
method: 'GET',
data: function (d) {
d.f_encuadernacion = $('#search-encuadernacion').val() || ''; // 'USER' | 'ADMIN' | 'SUPERADMIN' | ''
d.f_cubierta = $('#search-cubierta').val() || ''; // 'true' | 'false' | ''
}
},
order: [[0, 'asc']],
columns: [
{ data: 'id', name: 'id', orderable: true },
{ data: 'tipoEncuadernacion', name: 'tipoEncuadernacion', orderable: true },
{ data: 'tipoCubierta', name: 'tipoCubierta', orderable: true },
{ data: 'tiradaMin', name: 'tiradaMin', orderable: true },
{ data: 'tiradaMax', name: 'tiradaMax', orderable: true },
{ data: 'importeMin', name: 'importeMin', orderable: true },
{ data: 'importeMax', name: 'importeMax', orderable: true },
{ data: 'margenMax', name: 'margenMax', orderable: true },
{ data: 'margenMin', name: 'margenMin', orderable: true },
{ data: 'actions', name: 'actions' }
@ -69,7 +64,7 @@
const colIndex = table.settings()[0].aoColumns.findIndex(c => c.name === colName);
if (colIndex >= 0) {
table.column(colIndex).search(this.value).draw();
table.column(colIndex).search(normalizeNumericFilter(this.value)).draw();
}
});

View File

@ -64,4 +64,17 @@ export function bracketPrefix(obj, prefix) {
out[`${prefix}[${k}]`] = v;
});
return out;
}
export function normalizeNumericFilter(input) {
if (!input) return input;
// Convierte todos los números del string:
// - Quita separadores de miles con punto
// - Cambia coma decimal por punto
// Mantiene operadores (>=, <=, <, >) y rangos con '-'
return input.replace(
/\d{1,3}(?:\.\d{3})*(?:,\d+)?|\d+(?:,\d+)?/g,
(num) => num.replace(/\./g, '').replace(',', '.')
);
}

View File

@ -9,41 +9,17 @@
</div>
<div class="form-group">
<label th:text="#{margenes-presupuesto.form.tipo_encuadernacion}" for="tipo_encuadernacion">Tipo de Encuadernación</label>
<select class="form-control" id="tipo_encuadernacion" th:field="*{tipoEncuadernacion}" required
th:classappend="${#fields.hasErrors('tipoEncuadernacion')} ? ' is-invalid'">
<option value="fresado" th:text="#{presupuesto.fresado}" selected>Fresado</option>
<option value="cosido" th:text="#{presupuesto.cosido}">Cosido</option>
<option value="espiral" th:text="#{presupuesto.espiral}">Espiral</option>
<option value="wireo" th:text="#{presupuesto.wireo}">Wire-O</option>
<option value="grapado" th:text="#{presupuesto.grapado}">Grapado</option>
</select>
<div class="invalid-feedback" th:if="${#fields.hasErrors('tipoEncuadernacion')}" th:errors="*{tipoEncuadernacion}">Error</div>
<label th:text="#{margenes-presupuesto.form.importe_minimo}" for="importe_minimo">Importe Mínimo</label>
<input type="number" class="form-control" id="importe_minimo" th:field="*{importeMin}" min="1"
th:classappend="${#fields.hasErrors('importeMin')} ? ' is-invalid'" required>
<div class="invalid-feedback" th:if="${#fields.hasErrors('importeMin')}" th:errors="*{importeMin}">Error</div>
</div>
<div class="form-group">
<label th:text="#{margenes-presupuesto.form.tipo_cubierta}" for="tipo_cubierta">Tipo de Cubierta</label>
<select class="form-control" id="tipo_cubierta" th:field="*{tipoCubierta}" required
th:classappend="${#fields.hasErrors('tipoCubierta')} ? ' is-invalid'">
<option value="tapaBlanda" th:text="#{presupuesto.tapaBlanda}" selected>Tapa Blanda</option>
<option value="tapaDura" th:text="#{presupuesto.tapaDura}">Tapa Dura</option>
<option value="tapaDuraLomoRedondo" th:text="#{presupuesto.tapaDuraLomoRedondo}">Tapa Dura Lomo Redondo</option>
</select>
<div class="invalid-feedback" th:if="${#fields.hasErrors('tipoCubierta')}" th:errors="*{tipoCubierta}">Error</div>
</div>
<div class="form-group">
<label th:text="#{margenes-presupuesto.form.tirada_minima}" for="tirada_minima">Tirada Mínima</label>
<input type="number" class="form-control" id="tirada_minima" th:field="*{tiradaMin}" min="1"
th:classappend="${#fields.hasErrors('tiradaMin')} ? ' is-invalid'" required>
<div class="invalid-feedback" th:if="${#fields.hasErrors('tiradaMin')}" th:errors="*{tiradaMin}">Error</div>
</div>
<div class="form-group">
<label th:text="#{margenes-presupuesto.form.tirada_maxima}" for="tirada_maxima">Tirada Máxima</label>
<input type="number" class="form-control" id="tirada_maxima" th:field="*{tiradaMax}" min="1"
th:classappend="${#fields.hasErrors('tiradaMax')} ? ' is-invalid'" required>
<div class="invalid-feedback" th:if="${#fields.hasErrors('tiradaMax')}" th:errors="*{tiradaMax}">Error</div>
<label th:text="#{margenes-presupuesto.form.importe_maximo}" for="importe_maximo">Importe Máximo</label>
<input type="number" class="form-control" id="importe_maximo" th:field="*{importeMax}" min="1"
th:classappend="${#fields.hasErrors('importeMax')} ? ' is-invalid'" required>
<div class="invalid-feedback" th:if="${#fields.hasErrors('importeMax')}" th:errors="*{importeMax}">Error</div>
</div>
<div class="form-group">

View File

@ -45,11 +45,8 @@
<thead>
<tr>
<th scope="col" th:text="#{margenes-presupuesto.tabla.id}">ID</th>
<th scope="col" th:text="#{margenes-presupuesto.tabla.tipo_encuadernacion}">Tipo
encuadernación</th>
<th scope="col" th:text="#{margenes-presupuesto.tabla.tipo_cubierta}">Tipo cubierta</th>
<th scope="col" th:text="#{margenes-presupuesto.tabla.tirada_minima}">Tirada Mín.</th>
<th scope="col" th:text="#{margenes-presupuesto.tabla.tirada_maxima}">Tirada Máx.</th>
<th scope="col" th:text="#{margenes-presupuesto.tabla.importe_minimo}">Importe Mín.</th>
<th scope="col" th:text="#{margenes-presupuesto.tabla.importe_maximo}">Importe Máx.</th>
<th scope="col" th:text="#{margenes-presupuesto.tabla.margen_maximo}">Margen Máx.</th>
<th scope="col" th:text="#{margenes-presupuesto.tabla.margen_minimo}">Margen Mín.</th>
<th scope="col" th:text="#{margenes-presupuesto.tabla.acciones}">Acciones</th>
@ -58,33 +55,12 @@
<th><input type="text" class="form-control form-control-sm margenes-presupuesto-filter"
data-col="id" /></th>
<th>
<select class="form-select form-select-sm margenes-presupuesto-select-filter"
id="search-encuadernacion">
<option value="" th:text="#{margenes-presupuesto.todos}">Todos</option>
<option value="fresado" th:text="#{presupuesto.fresado}">Fresado</option>
<option value="cosido" th:text="#{presupuesto.cosido}">Cosido</option>
<option value="espiral" th:text="#{presupuesto.espiral}">Espiral</option>
<option value="wireo" th:text="#{presupuesto.wireo}">Wireo</option>
<option value="grapado" th:text="#{presupuesto.grapado}">Grapado</option>
</select>
</th>
<th>
<select class="form-select form-select-sm margenes-presupuesto-select-filter"
id="search-cubierta">
<option value="" th:text="#{margenes-presupuesto.todos}">Todos</option>
<option value="tapaBlanda" th:text="#{presupuesto.tapa-blanda}"></option>
<option value="tapaDura" th:text="#{presupuesto.tapa-dura}"></option>
<option value="tapaDuraLomoRedondo" th:text="#{presupuesto.tapa-dura-lomo-redondo}">
</option>
</select>
<input type="text" class="form-control form-control-sm margenes-presupuesto-filter"
data-col="importeMin" />
</th>
<th>
<input type="text" class="form-control form-control-sm margenes-presupuesto-filter"
data-col="tiradaMin" />
</th>
<th>
<input type="text" class="form-control form-control-sm margenes-presupuesto-filter"
data-col="tiradaMax" />
data-col="importeMax" />
</th>
<th>
<input type="text" class="form-control form-control-sm margenes-presupuesto-filter"
@ -122,7 +98,7 @@
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
<script th:src="@{/assets/js/pages/imprimelibros/configuracion/margenes-presupuesto/list.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/configuracion/margenes-presupuesto/list.js}"></script>
</th:block>
</body>