mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-28 06:38:51 +00:00
series de facturación terminadas (vista en configuración)
This commit is contained in:
3747
logs/erp.log
3747
logs/erp.log
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@
|
|||||||
|
package com.imprimelibros.erp.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableMethodSecurity
|
||||||
|
public class MethodSecurityConfig {
|
||||||
|
}
|
||||||
@ -18,7 +18,7 @@ public class SerieFactura extends AbstractAuditedEntitySoftTs {
|
|||||||
private TipoSerieFactura tipo = TipoSerieFactura.facturacion;
|
private TipoSerieFactura tipo = TipoSerieFactura.facturacion;
|
||||||
|
|
||||||
@Column(name = "numero_actual", nullable = false)
|
@Column(name = "numero_actual", nullable = false)
|
||||||
private Integer numeroActual = 1;
|
private Long numeroActual = 1L;
|
||||||
|
|
||||||
public String getNombreSerie() { return nombreSerie; }
|
public String getNombreSerie() { return nombreSerie; }
|
||||||
public void setNombreSerie(String nombreSerie) { this.nombreSerie = nombreSerie; }
|
public void setNombreSerie(String nombreSerie) { this.nombreSerie = nombreSerie; }
|
||||||
@ -29,6 +29,6 @@ public class SerieFactura extends AbstractAuditedEntitySoftTs {
|
|||||||
public TipoSerieFactura getTipo() { return tipo; }
|
public TipoSerieFactura getTipo() { return tipo; }
|
||||||
public void setTipo(TipoSerieFactura tipo) { this.tipo = tipo; }
|
public void setTipo(TipoSerieFactura tipo) { this.tipo = tipo; }
|
||||||
|
|
||||||
public Integer getNumeroActual() { return numeroActual; }
|
public Long getNumeroActual() { return numeroActual; }
|
||||||
public void setNumeroActual(Integer numeroActual) { this.numeroActual = numeroActual; }
|
public void setNumeroActual(Long numeroActual) { this.numeroActual = numeroActual; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,201 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion.controller;
|
||||||
|
|
||||||
|
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.facturacion.SerieFactura;
|
||||||
|
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
||||||
|
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
|
||||||
|
import com.imprimelibros.erp.i18n.TranslationService;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/configuracion/series-facturacion")
|
||||||
|
@PreAuthorize("hasRole('SUPERADMIN')")
|
||||||
|
public class SeriesFacturacionController {
|
||||||
|
|
||||||
|
private final SerieFacturaRepository repo;
|
||||||
|
private final TranslationService translationService;
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
|
||||||
|
public SeriesFacturacionController(SerieFacturaRepository repo, TranslationService translationService,
|
||||||
|
MessageSource messageSource) {
|
||||||
|
this.repo = repo;
|
||||||
|
this.translationService = translationService;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// VISTA
|
||||||
|
// -----------------------------
|
||||||
|
@GetMapping
|
||||||
|
public String listView(Model model, Locale locale) {
|
||||||
|
|
||||||
|
List<String> keys = List.of(
|
||||||
|
"series-facturacion.modal.title.add",
|
||||||
|
"series-facturacion.modal.title.edit",
|
||||||
|
"app.guardar",
|
||||||
|
"app.cancelar",
|
||||||
|
"app.eliminar",
|
||||||
|
"series-facturacion.delete.title",
|
||||||
|
"series-facturacion.delete.text",
|
||||||
|
"series-facturacion.delete.ok.title",
|
||||||
|
"series-facturacion.delete.ok.text");
|
||||||
|
|
||||||
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
|
model.addAttribute("languageBundle", translations);
|
||||||
|
|
||||||
|
return "imprimelibros/configuracion/series-facturas/series-facturas-list";
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// API: DataTables (server-side)
|
||||||
|
// -----------------------------
|
||||||
|
@GetMapping("/api/datatables")
|
||||||
|
@ResponseBody
|
||||||
|
public DataTablesResponse<Map<String, Object>> datatables(HttpServletRequest request, Locale locale) {
|
||||||
|
|
||||||
|
DataTablesRequest dt = DataTablesParser.from(request);
|
||||||
|
|
||||||
|
Specification<SerieFactura> notDeleted = (root, q, cb) -> cb.isNull(root.get("deletedAt"));
|
||||||
|
long total = repo.count(notDeleted);
|
||||||
|
|
||||||
|
return DataTable
|
||||||
|
.of(repo, SerieFactura.class, dt, List.of("nombreSerie", "prefijo"))
|
||||||
|
.where(notDeleted)
|
||||||
|
.orderable(List.of("id", "nombreSerie", "prefijo", "tipo", "numeroActual"))
|
||||||
|
.onlyAddedColumns()
|
||||||
|
.add("id", SerieFactura::getId)
|
||||||
|
.add("nombre_serie", SerieFactura::getNombreSerie)
|
||||||
|
.add("prefijo", SerieFactura::getPrefijo)
|
||||||
|
.add("tipo", s -> s.getTipo() != null ? s.getTipo().name() : null)
|
||||||
|
.add("tipo_label", s -> {
|
||||||
|
if (s.getTipo() == null)
|
||||||
|
return null;
|
||||||
|
return messageSource.getMessage(
|
||||||
|
"series-facturacion.tipo." + s.getTipo().name(),
|
||||||
|
null,
|
||||||
|
s.getTipo().name(),
|
||||||
|
locale);
|
||||||
|
})
|
||||||
|
.add("numero_actual", SerieFactura::getNumeroActual)
|
||||||
|
.add("actions", s -> """
|
||||||
|
<div class="hstack gap-3 flex-wrap">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-link p-0 link-success btn-edit-serie fs-15"
|
||||||
|
data-id="%d">
|
||||||
|
<i class="ri-edit-2-line"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-link p-0 link-danger btn-delete-serie fs-15"
|
||||||
|
data-id="%d">
|
||||||
|
<i class="ri-delete-bin-5-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
""".formatted(s.getId(), s.getId()))
|
||||||
|
.toJson(total);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// API: CREATE
|
||||||
|
// -----------------------------
|
||||||
|
@PostMapping(value = "/api", consumes = "application/json")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> create(@RequestBody SerieFacturaPayload payload) {
|
||||||
|
validate(payload);
|
||||||
|
|
||||||
|
SerieFactura s = new SerieFactura();
|
||||||
|
s.setNombreSerie(payload.nombre_serie.trim());
|
||||||
|
s.setPrefijo(payload.prefijo.trim());
|
||||||
|
s.setTipo(TipoSerieFactura.facturacion); // fijo
|
||||||
|
s.setNumeroActual(payload.numero_actual);
|
||||||
|
|
||||||
|
repo.save(s);
|
||||||
|
return Map.of("ok", true, "id", s.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// API: UPDATE
|
||||||
|
// -----------------------------
|
||||||
|
@PutMapping(value = "/api/{id}", consumes = "application/json")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> update(@PathVariable Long id, @RequestBody SerieFacturaPayload payload) {
|
||||||
|
validate(payload);
|
||||||
|
|
||||||
|
SerieFactura s = repo.findById(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + id));
|
||||||
|
|
||||||
|
if (s.getDeletedAt() != null) {
|
||||||
|
throw new IllegalStateException("No se puede editar una serie eliminada.");
|
||||||
|
}
|
||||||
|
|
||||||
|
s.setNombreSerie(payload.nombre_serie.trim());
|
||||||
|
s.setPrefijo(payload.prefijo.trim());
|
||||||
|
s.setTipo(TipoSerieFactura.facturacion);
|
||||||
|
s.setNumeroActual(payload.numero_actual);
|
||||||
|
|
||||||
|
repo.save(s);
|
||||||
|
return Map.of("ok", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// API: DELETE (soft)
|
||||||
|
// -----------------------------
|
||||||
|
@DeleteMapping("/api/{id}")
|
||||||
|
@ResponseBody
|
||||||
|
public ResponseEntity<?> delete(@PathVariable Long id) {
|
||||||
|
SerieFactura s = repo.findById(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + id));
|
||||||
|
|
||||||
|
if (s.getDeletedAt() == null) {
|
||||||
|
s.setDeletedAt(Instant.now());
|
||||||
|
s.setDeletedBy(null); // luego lo conectamos al usuario actual
|
||||||
|
repo.save(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("ok", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Payload + validación
|
||||||
|
// -----------------------------
|
||||||
|
public static class SerieFacturaPayload {
|
||||||
|
public String nombre_serie;
|
||||||
|
public String prefijo;
|
||||||
|
public String tipo; // lo manda UI, pero en backend lo fijamos
|
||||||
|
public Long numero_actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validate(SerieFacturaPayload p) {
|
||||||
|
if (p == null)
|
||||||
|
throw new IllegalArgumentException("Body requerido.");
|
||||||
|
if (p.nombre_serie == null || p.nombre_serie.trim().isBlank()) {
|
||||||
|
throw new IllegalArgumentException("nombre_serie es obligatorio.");
|
||||||
|
}
|
||||||
|
if (p.prefijo == null || p.prefijo.trim().isBlank()) {
|
||||||
|
throw new IllegalArgumentException("prefijo es obligatorio.");
|
||||||
|
}
|
||||||
|
if (p.prefijo.trim().length() > 10) {
|
||||||
|
throw new IllegalArgumentException("prefijo máximo 10 caracteres.");
|
||||||
|
}
|
||||||
|
if (p.numero_actual == null || p.numero_actual < 1) {
|
||||||
|
throw new IllegalArgumentException("numero_actual debe ser >= 1.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
public class SerieFacturaForm {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(max = 100)
|
||||||
|
private String nombreSerie;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(max = 10)
|
||||||
|
private String prefijo;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long numeroActual;
|
||||||
|
|
||||||
|
public String getNombreSerie() { return nombreSerie; }
|
||||||
|
public void setNombreSerie(String nombreSerie) { this.nombreSerie = nombreSerie; }
|
||||||
|
|
||||||
|
public String getPrefijo() { return prefijo; }
|
||||||
|
public void setPrefijo(String prefijo) { this.prefijo = prefijo; }
|
||||||
|
|
||||||
|
public Long getNumeroActual() { return numeroActual; }
|
||||||
|
public void setNumeroActual(Long numeroActual) { this.numeroActual = numeroActual; }
|
||||||
|
}
|
||||||
@ -4,15 +4,19 @@ import com.imprimelibros.erp.facturacion.SerieFactura;
|
|||||||
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
||||||
import org.springframework.data.jpa.repository.*;
|
import org.springframework.data.jpa.repository.*;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
import jakarta.persistence.LockModeType;
|
import jakarta.persistence.LockModeType;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface SerieFacturaRepository extends JpaRepository<SerieFactura, Long> {
|
public interface SerieFacturaRepository extends JpaRepository<SerieFactura, Long>, JpaSpecificationExecutor<SerieFactura> {
|
||||||
|
|
||||||
Optional<SerieFactura> findByTipo(TipoSerieFactura tipo);
|
Optional<SerieFactura> findByTipo(TipoSerieFactura tipo);
|
||||||
|
|
||||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||||
@Query("select s from SerieFactura s where s.id = :id")
|
@Query("select s from SerieFactura s where s.id = :id")
|
||||||
Optional<SerieFactura> findByIdForUpdate(@Param("id") Long id);
|
Optional<SerieFactura> findByIdForUpdate(@Param("id") Long id);
|
||||||
|
List<SerieFactura> findAllByDeletedAtIsNullOrderByNombreSerieAsc();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,7 @@ public class FacturacionService {
|
|||||||
factura.setNumeroFactura(numeroFactura);
|
factura.setNumeroFactura(numeroFactura);
|
||||||
|
|
||||||
// Incrementar contador para la siguiente
|
// Incrementar contador para la siguiente
|
||||||
serieLocked.setNumeroActual((int) (next + 1)); // si cambias numero_actual a BIGINT en entidad, quita el cast
|
serieLocked.setNumeroActual(next + 1);
|
||||||
serieRepo.save(serieLocked);
|
serieRepo.save(serieLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,3 +30,5 @@ app.sidebar.direcciones-admin=Administrar Direcciones
|
|||||||
app.sidebar.gestion-pagos=Gestión de Pagos
|
app.sidebar.gestion-pagos=Gestión de Pagos
|
||||||
|
|
||||||
app.errors.403=No tienes permiso para acceder a esta página.
|
app.errors.403=No tienes permiso para acceder a esta página.
|
||||||
|
|
||||||
|
app.validation.required=Campo obligatorio
|
||||||
26
src/main/resources/i18n/series_facturacion_es.properties
Normal file
26
src/main/resources/i18n/series_facturacion_es.properties
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
series-facturacion.title=Series de Facturación
|
||||||
|
series-facturacion.breadcrumb=Series de Facturación
|
||||||
|
|
||||||
|
series-facturacion.tabla.id=ID
|
||||||
|
series-facturacion.tabla.nombre=Nombre
|
||||||
|
series-facturacion.tabla.prefijo=Prefijo
|
||||||
|
series-facturacion.tabla.tipo=Tipo
|
||||||
|
series-facturacion.tabla.numero-actual=Número Actual
|
||||||
|
series-facturacion.tabla.acciones=Acciones
|
||||||
|
|
||||||
|
series-facturacion.delete.title=¿Estás seguro de que deseas eliminar esta serie de facturación?
|
||||||
|
series-facturacion.delete.text=Esta acción no se puede deshacer.
|
||||||
|
series-facturacion.delete.ok.title=Serie de facturación eliminada
|
||||||
|
series-facturacion.delete.ok.text=La serie de facturación ha sido eliminada correctamente.
|
||||||
|
|
||||||
|
series-facturacion.tipo.facturacion=Facturación
|
||||||
|
|
||||||
|
series-facturacion.form.nombre=Nombre
|
||||||
|
series-facturacion.form.prefijo=Prefijo
|
||||||
|
series-facturacion.form.prefijo.help=Ej: FAC, DIG, REC...
|
||||||
|
series-facturacion.form.tipo=Tipo
|
||||||
|
series-facturacion.tipo.facturacion=Facturación
|
||||||
|
series-facturacion.form.numero-actual=Número actual
|
||||||
|
|
||||||
|
series-facturacion.modal.title.add=Nueva Serie de Facturación
|
||||||
|
series-facturacion.modal.title.edit=Editar Serie de Facturación
|
||||||
@ -0,0 +1,222 @@
|
|||||||
|
/* global $, bootstrap, window */
|
||||||
|
$(() => {
|
||||||
|
// si jQuery está cargado, añade CSRF a AJAX
|
||||||
|
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||||
|
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||||
|
if (window.$ && csrfToken && csrfHeader) {
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function (xhr) {
|
||||||
|
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = document.documentElement.lang || 'es-ES';
|
||||||
|
|
||||||
|
const $table = $('#series-datatable'); // en tu HTML está así, aunque el id sea raro
|
||||||
|
const $addBtn = $('#addButton');
|
||||||
|
|
||||||
|
const $modal = $('#serieFacturacionModal');
|
||||||
|
const modal = new bootstrap.Modal($modal[0]);
|
||||||
|
|
||||||
|
const $form = $('#serieFacturacionForm');
|
||||||
|
const $alert = $('#serieFacturacionAlert');
|
||||||
|
const $saveBtn = $('#serieFacturacionSaveBtn');
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
$alert.removeClass('d-none').text(msg || 'Error');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearError() {
|
||||||
|
$alert.addClass('d-none').text('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
clearError();
|
||||||
|
$form[0].reset();
|
||||||
|
$form.removeClass('was-validated');
|
||||||
|
$('#serie_id').val('');
|
||||||
|
$('#numero_actual').val('1');
|
||||||
|
$('#tipo').val('facturacion');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAddModal() {
|
||||||
|
resetForm();
|
||||||
|
$('#serieFacturacionModalTitle').text(window.languageBundle?.['series-facturacion.modal.title.add'] || 'Añadir serie');
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal(row) {
|
||||||
|
resetForm();
|
||||||
|
$('#serieFacturacionModalTitle').text(window.languageBundle?.['series-facturacion.modal.title.edit'] || 'Editar serie');
|
||||||
|
|
||||||
|
$('#serie_id').val(row.id);
|
||||||
|
$('#nombre_serie').val(row.nombre_serie);
|
||||||
|
$('#prefijo').val(row.prefijo);
|
||||||
|
$('#tipo').val(row.tipo || 'facturacion');
|
||||||
|
$('#numero_actual').val(row.numero_actual);
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// DataTable server-side
|
||||||
|
// -----------------------------
|
||||||
|
const dt = $table.DataTable({
|
||||||
|
processing: true,
|
||||||
|
serverSide: true,
|
||||||
|
searching: true,
|
||||||
|
orderMulti: false,
|
||||||
|
pageLength: 10,
|
||||||
|
lengthMenu: [10, 25, 50, 100],
|
||||||
|
|
||||||
|
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||||
|
|
||||||
|
ajax: {
|
||||||
|
url: '/configuracion/series-facturacion/api/datatables',
|
||||||
|
type: 'GET',
|
||||||
|
dataSrc: function (json) {
|
||||||
|
// DataTables espera {draw, recordsTotal, recordsFiltered, data}
|
||||||
|
return json.data || [];
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
console.error('DataTables error', xhr);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
columns: [
|
||||||
|
{ data: 'id' },
|
||||||
|
{ data: 'nombre_serie' },
|
||||||
|
{ data: 'prefijo' },
|
||||||
|
{ data: 'tipo_label', name: 'tipo' },
|
||||||
|
{ data: 'numero_actual' },
|
||||||
|
{
|
||||||
|
data: 'actions',
|
||||||
|
orderable: false,
|
||||||
|
searchable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
order: [[0, 'desc']]
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Add
|
||||||
|
// -----------------------------
|
||||||
|
$addBtn.on('click', () => openAddModal());
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Edit click
|
||||||
|
// -----------------------------
|
||||||
|
$table.on('click', '.btn-edit-serie', function () {
|
||||||
|
const row = dt.row($(this).closest('tr')).data();
|
||||||
|
openEditModal(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Delete click
|
||||||
|
// -----------------------------
|
||||||
|
$table.on('click', '.btn-delete-serie', function () {
|
||||||
|
const row = dt.row($(this).closest('tr')).data();
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: window.languageBundle.get(['series-facturacion.delete.title']) || 'Eliminar serie',
|
||||||
|
html: window.languageBundle.get(['series-facturacion.delete.text']) || 'Esta acción no se puede deshacer.',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-danger w-xs mt-2',
|
||||||
|
cancelButton: 'btn btn-light w-xs mt-2'
|
||||||
|
},
|
||||||
|
confirmButtonText: window.languageBundle.get(['app.eliminar']) || 'Eliminar',
|
||||||
|
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `/configuracion/series-facturacion/api/${row.id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
success: function () {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success', title: window.languageBundle.get(['series-facturacion.delete.ok.title']) || 'Eliminado',
|
||||||
|
text: window.languageBundle.get(['series-facturacion.delete.ok.text']) || 'La serie de facturación ha sido eliminada correctamente.',
|
||||||
|
showConfirmButton: false,
|
||||||
|
timer: 1800,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary w-xs mt-2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dt.ajax.reload(null, false);
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||||
|
|| 'Error al eliminar la serie de facturación.';
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'No se pudo eliminar',
|
||||||
|
text: msg,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||||
|
cancelButton: 'btn btn-light' // clases para cancelar
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Save (create/update)
|
||||||
|
// -----------------------------
|
||||||
|
$saveBtn.on('click', function () {
|
||||||
|
clearError();
|
||||||
|
|
||||||
|
// Validación Bootstrap
|
||||||
|
const formEl = $form[0];
|
||||||
|
if (!formEl.checkValidity()) {
|
||||||
|
$form.addClass('was-validated');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = $('#serie_id').val();
|
||||||
|
const payload = {
|
||||||
|
nombre_serie: $('#nombre_serie').val().trim(),
|
||||||
|
prefijo: $('#prefijo').val().trim(),
|
||||||
|
tipo: $('#tipo').val(),
|
||||||
|
numero_actual: Number($('#numero_actual').val())
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEdit = !!id;
|
||||||
|
const url = isEdit
|
||||||
|
? `/configuracion/series-facturacion/api/${id}`
|
||||||
|
: `/configuracion/series-facturacion/api`;
|
||||||
|
|
||||||
|
const method = isEdit ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
$saveBtn.prop('disabled', true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
contentType: 'application/json; charset=utf-8',
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
success: function () {
|
||||||
|
modal.hide();
|
||||||
|
dt.ajax.reload(null, false);
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
const msg = xhr.responseJSON?.message || xhr.responseText || 'No se pudo guardar.';
|
||||||
|
showError(msg);
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
$saveBtn.prop('disabled', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// limpiar estado al cerrar
|
||||||
|
$modal.on('hidden.bs.modal', () => resetForm());
|
||||||
|
});
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Fragment: Modal para Alta/Edición de Serie de Facturación -->
|
||||||
|
<th:block th:fragment="modal">
|
||||||
|
|
||||||
|
<div class="modal fade" id="serieFacturacionModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="serieFacturacionModalTitle" th:text="#{series-facturacion.modal.title.add}">
|
||||||
|
Añadir serie
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<!-- Alert placeholder (JS lo rellena) -->
|
||||||
|
<div id="serieFacturacionAlert" class="alert alert-danger d-none" role="alert"></div>
|
||||||
|
|
||||||
|
<form id="serieFacturacionForm" novalidate>
|
||||||
|
<!-- Para editar: el JS setea este id -->
|
||||||
|
<input type="hidden" id="serie_id" name="id" value="">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nombre_serie" class="form-label" th:text="#{series-facturacion.form.nombre}">
|
||||||
|
Nombre
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="nombre_serie"
|
||||||
|
name="nombre_serie"
|
||||||
|
maxlength="100"
|
||||||
|
required>
|
||||||
|
<div class="invalid-feedback" th:text="#{app.validation.required}">
|
||||||
|
Campo obligatorio
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="prefijo" class="form-label" th:text="#{series-facturacion.form.prefijo}">
|
||||||
|
Prefijo
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="prefijo"
|
||||||
|
name="prefijo"
|
||||||
|
maxlength="10"
|
||||||
|
required>
|
||||||
|
<div class="invalid-feedback" th:text="#{app.validation.required}">
|
||||||
|
Campo obligatorio
|
||||||
|
</div>
|
||||||
|
<div class="form-text" th:text="#{series-facturacion.form.prefijo.help}">
|
||||||
|
Ej: FAC, F25...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tipo" class="form-label" th:text="#{series-facturacion.form.tipo}">
|
||||||
|
Tipo
|
||||||
|
</label>
|
||||||
|
<!-- En BD solo hay facturacion, pero lo dejamos como select por UI -->
|
||||||
|
<select class="form-select" id="tipo" name="tipo" required>
|
||||||
|
<option value="facturacion" th:text="#{series-facturacion.tipo.facturacion}">
|
||||||
|
Facturación
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="numero_actual" class="form-label" th:text="#{series-facturacion.form.numero-actual}">
|
||||||
|
Número actual
|
||||||
|
</label>
|
||||||
|
<input type="number"
|
||||||
|
class="form-control"
|
||||||
|
id="numero_actual"
|
||||||
|
name="numero_actual"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
value="1"
|
||||||
|
required>
|
||||||
|
<div class="invalid-feedback" th:text="#{app.validation.required}">
|
||||||
|
Campo obligatorio
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-light"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
th:text="#{app.cancelar}">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
id="serieFacturacionSaveBtn">
|
||||||
|
<i class="ri-save-line align-bottom me-1"></i>
|
||||||
|
<span th:text="#{app.guardar}">Guardar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{imprimelibros/layout}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block layout:fragment="pagetitle" />
|
||||||
|
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
||||||
|
<th:block layout:fragment="pagecss">
|
||||||
|
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet"
|
||||||
|
th:unless="${#authorization.expression('isAuthenticated()')}" />
|
||||||
|
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
|
||||||
|
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}" />
|
||||||
|
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
|
||||||
|
<!-- Modales-->
|
||||||
|
<div th:replace="~{imprimelibros/configuracion/series-facturas/series-facturacion-modal :: modal}" />
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page" th:text="#{series-facturacion.breadcrumb}">
|
||||||
|
Series de Facturación</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-secondary mb-3" id="addButton">
|
||||||
|
<i class="ri-add-line align-bottom me-1"></i> <span
|
||||||
|
th:text="#{app.add}">Añadir</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<table id="series-datatable" class="table table-striped table-nowrap responsive w-100">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-start" scope="col" th:text="#{series-facturacion.tabla.id}">ID</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{series-facturacion.tabla.nombre}">Nombre</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{series-facturacion.tabla.prefijo}">Prefijo</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{series-facturacion.tabla.tipo}">Tipo</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{series-facturacion.tabla.numero-actual}">Número Actual</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{series-facturacion.tabla.acciones}">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<th:block layout:fragment="modal" />
|
||||||
|
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||||
|
<th:block layout:fragment="pagejs">
|
||||||
|
<script th:inline="javascript">
|
||||||
|
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
||||||
|
</script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/datatables.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/dataTables.bootstrap5.min.js}"></script>
|
||||||
|
|
||||||
|
<!-- JS de Buttons y dependencias -->
|
||||||
|
<script th:src="@{/assets/libs/datatables/dataTables.buttons.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/jszip/jszip.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/pdfmake/pdfmake.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/pdfmake/vfs_fonts.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/buttons.html5.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
|
||||||
|
|
||||||
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/configuracion/series-facturacion/list.js}"></script>
|
||||||
|
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -88,6 +88,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
|
<div th:if="${#authentication.principal.role == 'SUPERADMIN'}">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/configuracion/series-facturacion" class="nav-link">
|
||||||
|
<i class="ri-file-list-3-line"></i>
|
||||||
|
<span th:text="#{series-facturacion.title}">Series de facturación</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user