mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-23 17:20:21 +00:00
haciendo vista de facturas
This commit is contained in:
@ -6,11 +6,10 @@ import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
import com.imprimelibros.erp.facturacion.EstadoFactura;
|
||||
import com.imprimelibros.erp.facturacion.Factura;
|
||||
import com.imprimelibros.erp.facturacion.SerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
|
||||
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@ -28,6 +27,9 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/facturas")
|
||||
@ -37,14 +39,17 @@ public class FacturasController {
|
||||
private final FacturaRepository repo;
|
||||
private final TranslationService translationService;
|
||||
private final MessageSource messageSource;
|
||||
private final PedidoService pedidoService;
|
||||
|
||||
public FacturasController(
|
||||
FacturaRepository repo,
|
||||
TranslationService translationService,
|
||||
MessageSource messageSource) {
|
||||
MessageSource messageSource,
|
||||
PedidoService pedidoService) {
|
||||
this.repo = repo;
|
||||
this.translationService = translationService;
|
||||
this.messageSource = messageSource;
|
||||
this.pedidoService = pedidoService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@ -66,12 +71,76 @@ public class FacturasController {
|
||||
public String facturaDetail(@PathVariable Long id, Model model, Locale locale) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
model.addAttribute("factura", factura);
|
||||
|
||||
PedidoDireccion direccionFacturacion = pedidoService
|
||||
.getPedidoDireccionFacturacionByPedidoId(factura.getPedidoId());
|
||||
|
||||
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||
model.addAttribute("factura", factura);
|
||||
|
||||
return "imprimelibros/facturas/facturas-form";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/container")
|
||||
public String facturaContainer(@PathVariable Long id, Model model, Locale locale) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
PedidoDireccion direccionFacturacion = pedidoService
|
||||
.getPedidoDireccionFacturacionByPedidoId(factura.getPedidoId());
|
||||
|
||||
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||
model.addAttribute("factura", factura);
|
||||
|
||||
return "imprimelibros/facturas/partials/factura-container :: factura-container";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/validar")
|
||||
public ResponseEntity<?> validarFactura(@PathVariable Long id) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||
return ResponseEntity.badRequest().body("Solo se pueden validar facturas en estado 'borrador'.");
|
||||
}
|
||||
|
||||
factura.setEstado(EstadoFactura.validada);
|
||||
repo.save(factura);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/borrador")
|
||||
public ResponseEntity<?> marcarBorrador(@PathVariable Long id) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
if (factura.getEstado() != EstadoFactura.validada) {
|
||||
return ResponseEntity.badRequest().body("Solo se pueden marcar como borrador facturas en estado 'validada'.");
|
||||
}
|
||||
|
||||
factura.setEstado(EstadoFactura.borrador);
|
||||
repo.save(factura);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/notas")
|
||||
public ResponseEntity<?> setNotas(
|
||||
@PathVariable Long id,
|
||||
@RequestBody Map<String, String> payload,
|
||||
Model model,
|
||||
Locale locale
|
||||
) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
String notas = payload.get("notas");
|
||||
factura.setNotas(notas);
|
||||
repo.save(factura);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------
|
||||
// API: DataTables (server-side)
|
||||
|
||||
@ -21,6 +21,7 @@ import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@ -172,6 +173,30 @@ public class SeriesFacturacionController {
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: GET for select2
|
||||
// -----------------------------
|
||||
@GetMapping("/api/get-series")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getSeriesForSelect(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
Locale locale) {
|
||||
String query = (q1 != null && !q1.isBlank()) ? q1
|
||||
: (q2 != null && !q2.isBlank()) ? q2
|
||||
: "";
|
||||
List<Map<String, Object>> results = repo.searchForSelectSeriesFacturacion(query).stream()
|
||||
.map(s -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", s.getId());
|
||||
m.put("text", s.getNombreSerie());
|
||||
return m;
|
||||
})
|
||||
.toList();
|
||||
|
||||
return Map.of("results", results);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Payload + validación
|
||||
// -----------------------------
|
||||
|
||||
@ -4,19 +4,29 @@ import com.imprimelibros.erp.facturacion.SerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
||||
import org.springframework.data.jpa.repository.*;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import jakarta.persistence.LockModeType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SerieFacturaRepository extends JpaRepository<SerieFactura, Long>, JpaSpecificationExecutor<SerieFactura> {
|
||||
public interface SerieFacturaRepository
|
||||
extends JpaRepository<SerieFactura, Long>, JpaSpecificationExecutor<SerieFactura> {
|
||||
|
||||
Optional<SerieFactura> findByTipo(TipoSerieFactura tipo);
|
||||
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
@Query("select s from SerieFactura s where s.id = :id")
|
||||
Optional<SerieFactura> findByIdForUpdate(@Param("id") Long id);
|
||||
|
||||
List<SerieFactura> findAllByDeletedAtIsNullOrderByNombreSerieAsc();
|
||||
|
||||
@Query("""
|
||||
select s
|
||||
from SerieFactura s
|
||||
where s.deletedAt is null
|
||||
and (:query is null or :query = '' or lower(s.nombreSerie) like lower(concat('%', :query, '%')))
|
||||
order by s.nombreSerie
|
||||
""")
|
||||
List<SerieFactura> searchForSelectSeriesFacturacion(@Param("query") String query);
|
||||
}
|
||||
|
||||
@ -16,8 +16,6 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.datatables.DataTable;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
|
||||
@ -17,7 +17,6 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.imprimelibros.erp.payments.repo.WebhookEventRepository;
|
||||
import com.imprimelibros.erp.pedidos.Pedido;
|
||||
import com.imprimelibros.erp.pedidos.PedidoLinea;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@ -64,6 +64,9 @@ public class PedidoService {
|
||||
return pedidoRepository.findById(pedidoId).orElse(null);
|
||||
}
|
||||
|
||||
public PedidoDireccion getPedidoDireccionFacturacionByPedidoId(Long pedidoId) {
|
||||
return pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Pedido crearPedido(
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
package com.imprimelibros.erp.redsys;
|
||||
|
||||
import com.imprimelibros.erp.cart.Cart;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.payments.PaymentService;
|
||||
import com.imprimelibros.erp.payments.model.Payment;
|
||||
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
||||
import com.imprimelibros.erp.pedidos.Pedido;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
import com.imprimelibros.erp.redsys.RedsysService.FormPayload;
|
||||
|
||||
import groovy.util.logging.Log;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@ -18,6 +18,39 @@ facturas.estado-pago.cancelada=Cancelada
|
||||
facturas.estado.borrador=Borrador
|
||||
facturas.estado.validada=Validada
|
||||
|
||||
facturas.form.numero-factura=Número de Factura
|
||||
facturas.form.serie=Serie de facturación
|
||||
facturas.form.fecha-emision=Fecha de Emisión
|
||||
facturas.form.cliente=Cliente
|
||||
facturas.form.notas=Notas
|
||||
|
||||
facturas.form.btn.validar=Validar Factura
|
||||
facturas.form.btn.borrador=Pasar a Borrador
|
||||
facturas.form.btn.guardar=Guardar
|
||||
facturas.form.btn.imprimir=Imprimir Factura
|
||||
|
||||
facturas.lineas.acciones=Acciones
|
||||
facturas.lineas.acciones.editar=Editar
|
||||
facturas.lineas.acciones.eliminar=Eliminar
|
||||
facturas.lineas.acciones.agregar=Agregar línea
|
||||
facturas.lineas.descripcion=Descripción
|
||||
facturas.lineas.base=Base Imponible
|
||||
facturas.lineas.iva_4=I.V.A. 4%
|
||||
facturas.lineas.iva_21=I.V.A. 21%
|
||||
facturas.lineas.total=Total
|
||||
|
||||
|
||||
facturas.direccion.titulo=Dirección de Facturación
|
||||
facturas.direccion.razon-social=Razón Social
|
||||
facturas.direccion.identificacion-fiscal=Identificación Fiscal
|
||||
facturas.direccion.direccion=Dirección
|
||||
facturas.direccion.codigo-postal=Código Postal
|
||||
facturas.direccion.ciudad=Ciudad
|
||||
facturas.direccion.provincia=Provincia
|
||||
facturas.direccion.pais=País
|
||||
facturas.direccion.telefono=Teléfono
|
||||
|
||||
|
||||
facturas.delete.title=¿Estás seguro de que deseas eliminar esta factura?
|
||||
facturas.delete.text=Esta acción no se puede deshacer.
|
||||
facturas.delete.ok.title=Factura eliminada
|
||||
|
||||
@ -1,3 +1,183 @@
|
||||
$(() => {
|
||||
|
||||
});
|
||||
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 $container = $('#factura-container');
|
||||
|
||||
const MIN_LOADER_TIME = 500; // ms (ajusta a gusto)
|
||||
let loaderStartTime = 0;
|
||||
|
||||
|
||||
function showLoader() {
|
||||
loaderStartTime = Date.now();
|
||||
$('#factura-loader').removeClass('d-none');
|
||||
}
|
||||
|
||||
function hideLoader() {
|
||||
const elapsed = Date.now() - loaderStartTime;
|
||||
const remaining = Math.max(MIN_LOADER_TIME - elapsed, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
$('#factura-loader').addClass('d-none');
|
||||
}, remaining);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getFacturaId() {
|
||||
return $container.data('factura-id');
|
||||
}
|
||||
|
||||
function reloadFacturaContainer() {
|
||||
const id = getFacturaId();
|
||||
if (!id) return $.Deferred().reject('No factura id').promise();
|
||||
|
||||
showLoader();
|
||||
|
||||
return $.ajax({
|
||||
url: `/facturas/${id}/container`,
|
||||
method: 'GET',
|
||||
cache: false,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.done((html) => {
|
||||
const $c = $('#factura-container');
|
||||
|
||||
// conserva loader dentro del container
|
||||
const $loader = $c.find('#factura-loader').detach();
|
||||
|
||||
// reemplaza el contenido completo
|
||||
$c.empty().append($loader).append(html);
|
||||
|
||||
afterFacturaRender();
|
||||
})
|
||||
.fail((xhr) => {
|
||||
console.error('Error recargando container:', xhr.status, xhr.responseText);
|
||||
})
|
||||
.always(() => {
|
||||
hideLoader();
|
||||
});
|
||||
}
|
||||
|
||||
function postAndReload(url, data) {
|
||||
showLoader();
|
||||
const isJson = data !== undefined && data !== null;
|
||||
|
||||
return $.ajax({
|
||||
url,
|
||||
method: 'POST',
|
||||
data: isJson ? JSON.stringify(data) : null,
|
||||
contentType: isJson ? 'application/json; charset=UTF-8' : undefined,
|
||||
processData: !isJson, // importante: si es JSON, no procesar
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.done(() => reloadFacturaContainer())
|
||||
.fail((xhr) => {
|
||||
console.error('Error en acción:', xhr.status, xhr.responseText);
|
||||
})
|
||||
.always(() => {
|
||||
hideLoader();
|
||||
});
|
||||
}
|
||||
|
||||
// Delegación (funciona aunque reemplacemos el contenido interno)
|
||||
$container.on('click', '#btn-validar-factura', function () {
|
||||
const id = getFacturaId();
|
||||
postAndReload(`/facturas/${id}/validar`);
|
||||
});
|
||||
|
||||
$container.on('click', '#btn-borrador-factura', function () {
|
||||
const id = getFacturaId();
|
||||
postAndReload(`/facturas/${id}/borrador`);
|
||||
});
|
||||
|
||||
$container.on('focusin', 'textarea[name="notas"]', function () {
|
||||
$(this).data('initial', $(this).val() ?? '');
|
||||
});
|
||||
|
||||
$container.on('focusout', 'textarea[name="notas"]', function () {
|
||||
const before = $(this).data('initial') ?? '';
|
||||
const now = $(this).val() ?? '';
|
||||
if (before === now) return;
|
||||
|
||||
const id = getFacturaId();
|
||||
postAndReload(`/facturas/${id}/notas`, { notas: now });
|
||||
});
|
||||
|
||||
|
||||
$container.on('click', '#btn-guardar-factura', function () {
|
||||
});
|
||||
|
||||
function destroySelect2($root) {
|
||||
$root.find('.js-select2-factura').each(function () {
|
||||
const $el = $(this);
|
||||
if ($el.hasClass('select2-hidden-accessible')) {
|
||||
$el.select2('destroy');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initSelect2ForDraft($root) {
|
||||
$root.find('.js-select2-factura').each(function () {
|
||||
const $el = $(this);
|
||||
|
||||
// evita doble init
|
||||
if ($el.hasClass('select2-hidden-accessible')) return;
|
||||
|
||||
const url = $el.data('url');
|
||||
const placeholder = $el.data('placeholder') || '';
|
||||
|
||||
$el.select2({
|
||||
width: '100%',
|
||||
placeholder,
|
||||
allowClear: true,
|
||||
minimumInputLength: 0,
|
||||
|
||||
ajax: {
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
|
||||
data: function (params) {
|
||||
return {
|
||||
q: params.term, // texto buscado
|
||||
page: params.page || 1
|
||||
};
|
||||
},
|
||||
|
||||
processResults: function (data) {
|
||||
return data;
|
||||
},
|
||||
|
||||
cache: true
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function afterFacturaRender() {
|
||||
const $root = $('#factura-container');
|
||||
|
||||
// el data-factura-estado viene del fragmento factura-container
|
||||
const estado = $root.find('[data-factura-estado]').first().data('factura-estado');
|
||||
|
||||
// siempre limpia antes para evitar restos si vienes de borrador->validada
|
||||
destroySelect2($root);
|
||||
|
||||
if (estado === 'borrador') {
|
||||
initSelect2ForDraft($root);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
afterFacturaRender();
|
||||
});
|
||||
|
||||
@ -31,59 +31,20 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="container-fluid position-relative" id="factura-container"
|
||||
th:attr="data-factura-id=${factura.id}">
|
||||
|
||||
<div class="accordion accordion-fill-imprimelibros mb-3" id="cabeceraFactura">
|
||||
<div class="accordion-item material-shadow">
|
||||
<h2 class="accordion-header" id="cabeceraHeader">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#cabecera" aria-expanded="true" aria-controls="cabecera">
|
||||
Datos de la factura
|
||||
</button>
|
||||
</h2>
|
||||
<div id="cabecera" class="accordion-collapse collapse show" aria-labelledby="cabeceraHeader"
|
||||
data-bs-parent="#cabeceraFactura">
|
||||
<div class="accordion-body">
|
||||
<div th:replace="~{imprimelibros/facturas/partials/factura-cabecera :: factura-cabecera (factura=${factura})}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion accordion-fill-imprimelibros" id="lineasFactura">
|
||||
<div class="accordion-item material-shadow">
|
||||
<h2 class="accordion-header" id="lineasHeader">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#lineas" aria-expanded="true" aria-controls="lineas">
|
||||
Líneas de factura
|
||||
</button>
|
||||
</h2>
|
||||
<div id="lineas" class="accordion-collapse collapse show" aria-labelledby="lineasHeader"
|
||||
data-bs-parent="#lineasFactura">
|
||||
<div class="accordion-body">
|
||||
<!-- <div th:replace="~{imprimelibros/facturas/partials/factura-lineas :: factura-lineas (factura=${factura})}" /> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion accordion-fill-imprimelibros" id="pagosFactura">
|
||||
<div class="accordion-item material-shadow">
|
||||
<h2 class="accordion-header" id="pagosHeader">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#pagos" aria-expanded="true" aria-controls="pagos">
|
||||
Pagos de factura
|
||||
</button>
|
||||
</h2>
|
||||
<div id="pagos" class="accordion-collapse collapse show" aria-labelledby="pagosHeader"
|
||||
data-bs-parent="#pagosFactura">
|
||||
<div class="accordion-body">
|
||||
<!-- <div th:replace="~{imprimelibros/facturas/partials/factura-cabecera :: factura-cabecera (factura=${factura})}" /> -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- overlay loader -->
|
||||
<div id="factura-loader" class="d-none position-absolute top-0 start-0 w-100 h-100"
|
||||
style="background: rgba(255,255,255,.6); z-index: 10;">
|
||||
<div class="position-absolute top-50 start-50 translate-middle">
|
||||
<div class="spinner-border" role="status" aria-hidden="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="factura-inner"
|
||||
th:replace="~{imprimelibros/facturas/partials/factura-container :: factura-container (factura=${factura})}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -17,40 +17,135 @@
|
||||
|
||||
<!-- Número -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Número</label>
|
||||
<label class="form-label" th:text="#{facturas.form.numero-factura}">Número</label>
|
||||
<input type="text" class="form-control" th:value="${factura.numeroFactura}"
|
||||
th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
</div>
|
||||
|
||||
<!-- Serie -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Serie facturación</label>
|
||||
<input type="text" class="form-control"
|
||||
th:value="${factura.serie != null ? factura.serie.nombreSerie : ''}" readonly>
|
||||
<label class="form-label" th:text="#{facturas.form.serie}">Serie facturación</label>
|
||||
<select class="form-control js-select2-factura" data-url="/configuracion/series-facturacion/api/get-series" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
<option th:value="${factura.serie != null ? factura.serie.id : ''}"
|
||||
th:text="${factura.serie != null ? factura.serie.nombreSerie : ''}" selected>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Cliente -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Cliente</label>
|
||||
<input type="text" class="form-control" th:value="${factura.cliente.fullName}" readonly>
|
||||
<label class="form-label" th:text="#{facturas.form.cliente}">Cliente</label>
|
||||
<select class="form-control js-select2-factura" data-url="/users/api/get-users" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
<option th:value="${factura.cliente != null ? factura.cliente.id : ''}"
|
||||
th:text="${factura.cliente != null ? factura.cliente.fullName : ''}" selected>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Fecha emisión -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Fecha</label>
|
||||
<label class="form-label" th:text="#{facturas.form.fecha-emision}">Fecha</label>
|
||||
<input type="text" class="form-control" th:value="${factura.fechaEmision != null
|
||||
? #temporals.format(factura.fechaEmision, 'dd/MM/yyyy')
|
||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
</div>
|
||||
|
||||
<!-- Notas -->
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Notas</label>
|
||||
<textarea class="form-control" rows="3" th:text="${factura.notas}"
|
||||
th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
<div class="col-md-9">
|
||||
<label class="form-label" th:text="#{facturas.form.notas}">Notas</label>
|
||||
<textarea class="form-control" rows="3" name="notas" th:text="${factura.notas}">
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-md-12">
|
||||
<h5 class="mt-4" th:text="#{facturas.direccion.titulo}">Dirección de facturación</h5>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" th:text="#{facturas.direccion.razon-social}">Razón Social</label>
|
||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
||||
? direccionFacturacion.razonSocial
|
||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" th:text="#{facturas.direccion.identificacion-fiscal}">Identificacion
|
||||
Fiscal</label>
|
||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
||||
? direccionFacturacion.identificacionFiscal
|
||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-9">
|
||||
<label class="form-label" th:text="#{facturas.direccion.direccion}">Dirección</label>
|
||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
||||
? direccionFacturacion.direccion
|
||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" th:text="#{facturas.direccion.codigo-postal}">Código Postal</label>
|
||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
||||
? direccionFacturacion.cp
|
||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" th:text="#{facturas.direccion.ciudad}">Ciudad</label>
|
||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
||||
? direccionFacturacion.ciudad
|
||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" th:text="#{facturas.direccion.provincia}">Provincia</label>
|
||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
||||
? direccionFacturacion.provincia
|
||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" th:text="#{facturas.direccion.pais}">País</label>
|
||||
<select class="form-control js-select2-factura" data-url="/api/paises" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
<option th:value="${direccionFacturacion != null
|
||||
? direccionFacturacion.pais.keyword
|
||||
: ''}"
|
||||
th:text="${direccionFacturacion != null
|
||||
? #messages.msg('paises.' + direccionFacturacion.pais.keyword)
|
||||
: ''}" selected>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" th:text="#{facturas.direccion.telefono}">Teléfono</label>
|
||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
||||
? direccionFacturacion.telefono
|
||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-4 justify-content-end">
|
||||
|
||||
<div class="col-md-12 text-end">
|
||||
<th:block th:if="${factura.estado.name() == 'borrador'}">
|
||||
<button type="button" class="btn btn-secondary me-2" id="btn-validar-factura"
|
||||
th:text="#{facturas.form.btn.validar}">Validar factura</button>
|
||||
<button type="button" class="btn btn-secondary me-2" id="btn-guardar-factura"
|
||||
th:text="#{facturas.form.btn.guardar}">Guardar</button>
|
||||
</th:block>
|
||||
<th:block th:if="${factura.estado.name() == 'validada'}">
|
||||
<button type="button" class="btn btn-secondary me-2" id="btn-borrador-factura"
|
||||
th:text="#{facturas.form.btn.borrador}">Pasar a borrador</button>
|
||||
</th:block>
|
||||
<button type="button" class="btn btn-secondary me-2" id="btn-imprimir-factura"
|
||||
th:text="#{facturas.form.btn.imprimir}">Imprimir factura</button>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
@ -0,0 +1,56 @@
|
||||
<div th:fragment="factura-container (factura)"
|
||||
th:attr="data-factura-estado=${factura.estado.name()}">
|
||||
|
||||
|
||||
<div class="accordion accordion-fill-imprimelibros mb-3" id="cabeceraFactura">
|
||||
<div class="accordion-item material-shadow">
|
||||
<h2 class="accordion-header" id="cabeceraHeader">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#cabecera" aria-expanded="true" aria-controls="cabecera">
|
||||
Datos de la factura
|
||||
</button>
|
||||
</h2>
|
||||
<div id="cabecera" class="accordion-collapse collapse show" aria-labelledby="cabeceraHeader"
|
||||
data-bs-parent="#cabeceraFactura">
|
||||
<div class="accordion-body">
|
||||
<div th:replace="~{imprimelibros/facturas/partials/factura-cabecera :: factura-cabecera (factura=${factura})}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion accordion-fill-imprimelibros mb-3" id="lineasFactura">
|
||||
<div class="accordion-item material-shadow">
|
||||
<h2 class="accordion-header" id="lineasHeader">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#lineas" aria-expanded="true" aria-controls="lineas">
|
||||
Líneas de factura
|
||||
</button>
|
||||
</h2>
|
||||
<div id="lineas" class="accordion-collapse collapse show" aria-labelledby="lineasHeader"
|
||||
data-bs-parent="#lineasFactura">
|
||||
<div class="accordion-body">
|
||||
<div th:replace="~{imprimelibros/facturas/partials/factura-lineas :: factura-lineas (factura=${factura})}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion accordion-fill-imprimelibros mb-3" id="pagosFactura">
|
||||
<div class="accordion-item material-shadow">
|
||||
<h2 class="accordion-header" id="pagosHeader">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#pagos" aria-expanded="true" aria-controls="pagos">
|
||||
Pagos de factura
|
||||
</button>
|
||||
</h2>
|
||||
<div id="pagos" class="accordion-collapse collapse show" aria-labelledby="pagosHeader"
|
||||
data-bs-parent="#pagosFactura">
|
||||
<div class="accordion-body">
|
||||
<!-- pagos -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1,59 @@
|
||||
<div th:fragment="factura-lineas (factura)">
|
||||
<th:block th:if="${factura.estado != null && factura.estado.name() == 'borrador'}">
|
||||
<div class="mb-3">
|
||||
<button type="button" class="btn btn-secondary" id="btn-add-linea-factura">
|
||||
<i class="fas fa-plus-circle me-2"></i>
|
||||
<span th:text="#{facturas.lineas.acciones.agregar}">Agregar línea</span>
|
||||
</button>
|
||||
</div>
|
||||
</th:block>
|
||||
<table class="table table-bordered table-striped table-nowrap w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th th:if="${factura.estado != null && factura.estado.name() == 'borrador'}" th:text="#{facturas.lineas.acciones}">Acciones</th>
|
||||
<th class="w-75" th:text="#{facturas.lineas.descripcion}">Descripción</th>
|
||||
<th th:text="#{facturas.lineas.base}">Base</th>
|
||||
<th th:text="#{facturas.lineas.iva_4}">I.V.A. 4%</th>
|
||||
<th th:text="#{facturas.lineas.iva_21}">I.V.A. 21%</th>
|
||||
<th th:text="#{facturas.lineas.total}">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="lineaFactura : ${factura.lineas}">
|
||||
<td th:if="${factura.estado != null && factura.estado.name() == 'borrador'}">
|
||||
<button type="button" class="btn btn-secondary btn-sm me-2"
|
||||
th:attr="data-linea-id=${lineaFactura.id}" th:text="#{facturas.lineas.acciones.editar}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
th:attr="data-linea-id=${lineaFactura.id}" th:text="#{facturas.lineas.acciones.eliminar}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
|
||||
</td>
|
||||
<td th:utext="${lineaFactura.descripcion}">Descripción de la línea</td>
|
||||
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.baseLinea)}">0.00</td>
|
||||
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.iva4Linea)}">0.00</td>
|
||||
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.iva21Linea)}">0.00</td>
|
||||
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.totalLinea)}">0.00</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="text-end fw-bold" th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}" th:text="#{facturas.lineas.base}">Base</td>
|
||||
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.baseImponible)}">0.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-end fw-bold" th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}" th:text="#{facturas.lineas.iva_4}">I.V.A. 4%</td>
|
||||
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.iva4)}">0.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-end fw-bold" th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}" th:text="#{facturas.lineas.iva_21}">I.V.A. 21%</td>
|
||||
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.iva21)}">0.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-end fw-bold text-uppercase" th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}" th:text="#{facturas.lineas.total}">Total</td>
|
||||
<td class="text-end fw-bold" colspan="1" th:text="${#numbers.formatCurrency(factura.totalFactura)}">0.00</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
Reference in New Issue
Block a user