WIP: Guardando trabajo antes de actualizar con development
This commit is contained in:
@@ -9,6 +9,31 @@ RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Instalar dependencias para wkhtmltopdf y reportes PDF
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
xvfb \
|
||||
fontconfig \
|
||||
fonts-dejavu-core \
|
||||
libfontconfig1 \
|
||||
libxrender1 \
|
||||
libxtst6 \
|
||||
libxi6 \
|
||||
libxrandr2 \
|
||||
ca-certificates \
|
||||
libjpeg62-turbo \
|
||||
libpng16-16 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Instalar wkhtmltopdf binario oficial con soporte para footers/headers
|
||||
RUN curl -k -L -o /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y /tmp/wkhtmltox.deb \
|
||||
&& rm /tmp/wkhtmltox.deb \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& wkhtmltopdf --version
|
||||
|
||||
|
||||
# Copiar requirements
|
||||
COPY requirements.txt .
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Union
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Nota: Usamos Union[float, str] en los números para poder enviar
|
||||
# strings formateados con comas (ej: "1,200.50") si lo deseamos,
|
||||
# ya que tu HTML no tiene filtros de formato.
|
||||
|
||||
class ClienteSchema(BaseModel):
|
||||
header: str
|
||||
nombre: str
|
||||
direccion: str
|
||||
num_exterior: str = ""
|
||||
num_interior: str = ""
|
||||
colonia: str = ""
|
||||
codigo_postal: str
|
||||
ciudad: str
|
||||
estado: str
|
||||
pais: str
|
||||
tax_id: str
|
||||
programa: str = ""
|
||||
autorizacion: str = ""
|
||||
|
||||
class FacturaSchema(BaseModel):
|
||||
numero: str
|
||||
fecha: str
|
||||
tipo_cambio: float
|
||||
moneda: str
|
||||
# Campos de aduanas (Opcionales por si A76 aún no los tiene)
|
||||
pedimento: str = ""
|
||||
clave_pedimento: str = ""
|
||||
remesa: str = ""
|
||||
acuse_electronico: str = ""
|
||||
agente_aduanal: str = ""
|
||||
patente: str = ""
|
||||
precinto: str = ""
|
||||
regimen: str = ""
|
||||
transportista: str = ""
|
||||
scac: str = ""
|
||||
caat: str = ""
|
||||
incoterm: str = ""
|
||||
transporte: str = ""
|
||||
num_transporte: str = ""
|
||||
placas: str = ""
|
||||
aduana: str = ""
|
||||
destino: str = ""
|
||||
observaciones: str = ""
|
||||
|
||||
class PartidaSchema(BaseModel):
|
||||
numero_parte: str
|
||||
descripcion: str
|
||||
fraccion: str
|
||||
origen: str
|
||||
cantidad_importacion: Union[float, str]
|
||||
unidad_medida: str
|
||||
cantidad_bultos: int
|
||||
clave_bultos: str
|
||||
peso_neto: Union[float, str]
|
||||
peso_bruto: Union[float, str]
|
||||
valor_costo_unitario: Union[float, str]
|
||||
valor_total: Union[float, str]
|
||||
|
||||
class TotalesSchema(BaseModel):
|
||||
cantidad_total: Union[float, str]
|
||||
bultos_total: int
|
||||
clave_bultos: str = ""
|
||||
peso_neto_total: Union[float, str]
|
||||
peso_bruto_total: Union[float, str]
|
||||
valor_total_total: Union[float, str]
|
||||
valor_total_dolares: Union[float, str]
|
||||
|
||||
class FacturaImportacionCompleta(BaseModel):
|
||||
cliente_proveedor: ClienteSchema
|
||||
cliente_vendido: ClienteSchema
|
||||
cliente_enviado: ClienteSchema
|
||||
factura: FacturaSchema
|
||||
partidas: List[PartidaSchema]
|
||||
totales: TotalesSchema
|
||||
@@ -0,0 +1,296 @@
|
||||
import shutil
|
||||
import pdfkit
|
||||
from pathlib import Path
|
||||
from decimal import Decimal
|
||||
from typing import Tuple, List
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# --- IMPORTACIONES DE TUS MODELOS (Asegúrate que las rutas sean correctas) ---
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics
|
||||
from api.v1.modules.a76.items.models import ItemLines, LineQuantities, LineFinancials
|
||||
from api.v1.modules.a76.clients.models import (
|
||||
ClientsAndProviders,
|
||||
ClientsAndProvidersAddress,
|
||||
ClientsAndProvidersPrograms
|
||||
)
|
||||
from api.v1.modules.a76.parts.models import Parts
|
||||
from api.v1.modules.a76.pedimentos.models import Pedimentos
|
||||
from api.v1.modules.a76.companies.models import Company # Tu empresa propia (Tenant)
|
||||
|
||||
from .schemas import (
|
||||
ClienteSchema, PartidaSchema, TotalesSchema,
|
||||
FacturaSchema, FacturaImportacionCompleta
|
||||
)
|
||||
|
||||
class FacturaImportacionMexService:
|
||||
def __init__(self):
|
||||
# Directorio de templates
|
||||
self.template_dir = Path(__file__).parent.parent / "templates"
|
||||
|
||||
self.jinja_env = Environment(
|
||||
loader=FileSystemLoader(self.template_dir),
|
||||
autoescape=select_autoescape(['html', 'xml'])
|
||||
)
|
||||
self.template = self.jinja_env.get_template('factura_mex_ver.html')
|
||||
|
||||
def _get_wkhtmltopdf_config(self):
|
||||
path = shutil.which("wkhtmltopdf")
|
||||
if not path:
|
||||
common_paths = ["/usr/bin/wkhtmltopdf", "/usr/local/bin/wkhtmltopdf"]
|
||||
for p in common_paths:
|
||||
if Path(p).exists():
|
||||
path = p
|
||||
break
|
||||
if not path:
|
||||
raise RuntimeError("wkhtmltopdf no encontrado.")
|
||||
return pdfkit.configuration(wkhtmltopdf=path)
|
||||
|
||||
def formatear_numero(self, valor, decimales: int = 2):
|
||||
if valor is None: return 0.0
|
||||
try:
|
||||
return round(float(valor), decimales)
|
||||
except: return 0.0
|
||||
|
||||
def _obtener_datos_cliente(self, db: Session, client_id: int, rol: str) -> ClienteSchema:
|
||||
"""
|
||||
Busca en las 3 tablas de clientes (Main, Address, Programs) para armar el esquema.
|
||||
"""
|
||||
# 1. Tabla Principal
|
||||
main = db.query(ClientsAndProviders).filter(ClientsAndProviders.id == client_id).first()
|
||||
if not main:
|
||||
return ClienteSchema(
|
||||
header=rol, nombre="Desconocido", direccion="", codigo_postal="",
|
||||
ciudad="", estado="", pais="", tax_id=""
|
||||
)
|
||||
|
||||
# 2. Dirección
|
||||
addr = db.query(ClientsAndProvidersAddress).filter(ClientsAndProvidersAddress.client_id == client_id).first()
|
||||
|
||||
# 3. Programa / Tax ID
|
||||
prog = db.query(ClientsAndProvidersPrograms).filter(ClientsAndProvidersPrograms.client_id == client_id).first()
|
||||
|
||||
direccion_str = addr.streets if addr else ""
|
||||
|
||||
return ClienteSchema(
|
||||
header=rol,
|
||||
nombre=main.name or main.short_name,
|
||||
direccion=direccion_str,
|
||||
num_exterior=addr.exterior_number if addr else "",
|
||||
num_interior=addr.interior_number if addr else "",
|
||||
colonia=addr.neighborhood if addr else "",
|
||||
codigo_postal=addr.postal_code if addr else "",
|
||||
ciudad=addr.city if addr else "",
|
||||
estado=addr.state if addr else "",
|
||||
pais=addr.country if addr else "MEX",
|
||||
tax_id=prog.tax_id if prog else (main.rfc or ""),
|
||||
programa="IMMEX" if prog and prog.program else "",
|
||||
autorizacion=prog.program_number if prog else ""
|
||||
)
|
||||
|
||||
def obtener_datos(self, db: Session, invoice_id: int) -> FacturaImportacionCompleta:
|
||||
try:
|
||||
# 1. CABECERA (InvoiceHeader)
|
||||
header = db.query(InvoiceHeader).filter(InvoiceHeader.id == invoice_id).first()
|
||||
if not header:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
|
||||
# 2. LOGÍSTICA (InvoiceLogistics)
|
||||
logistics = db.query(InvoiceLogistics).filter(InvoiceLogistics.invoice_id == header.id).first()
|
||||
|
||||
# 3. PEDIMENTO (Pedimentos)
|
||||
# Intentamos buscar por relation_doc_id o asumimos una búsqueda por ID si existiera columna pedimento_id
|
||||
# Si no hay link directo, buscamos el pedimento asociado al cliente/fecha (Lógica aproximada)
|
||||
pedimento = None
|
||||
if header.related_doc_id:
|
||||
pedimento = db.query(Pedimentos).filter(Pedimentos.id == header.related_doc_id).first()
|
||||
|
||||
# 4. ACTORES (Clientes/Proveedores)
|
||||
# Importación: Proveedor = Externo (Client ID?), Importador = Nosotros (Company ID)
|
||||
# Nota: Asumo que en invoice_header hay un campo 'client_id' para el proveedor externo.
|
||||
# Si no existe en el modelo, revisa qué campo guarda el ID del proveedor.
|
||||
proveedor_id = getattr(header, 'client_id', None)
|
||||
|
||||
# --- PROVEEDOR (Extranjero) ---
|
||||
if proveedor_id:
|
||||
cliente_proveedor = self._obtener_datos_cliente(db, proveedor_id, "Proveedor / Supplier")
|
||||
else:
|
||||
# Fallback si no encontramos ID de proveedor
|
||||
cliente_proveedor = ClienteSchema(
|
||||
header="Proveedor", nombre="No Asignado", direccion="", codigo_postal="", ciudad="", estado="", pais="", tax_id=""
|
||||
)
|
||||
|
||||
# --- IMPORTADOR (Vendido A - Tu Empresa) ---
|
||||
# Usamos 'company_id' que viene en invoice_header
|
||||
# Ojo: Si tu empresa también está en clients_and_providers, úsala. Si está en 'companies', mapea desde ahí.
|
||||
# Aquí asumo que está en 'companies' como Tenant.
|
||||
company = db.query(Company).filter(Company.id == header.company_id).first()
|
||||
|
||||
cliente_vendido = ClienteSchema(
|
||||
header="Importador / Consignatario",
|
||||
nombre=company.name if company else "Mi Empresa",
|
||||
direccion=company.address_street if company else "", # Ajustar nombres de col company
|
||||
num_exterior=str(company.address_number) if company else "",
|
||||
colonia=company.neighborhood if company else "",
|
||||
codigo_postal=company.zip_code if company else "",
|
||||
ciudad=company.city if company else "",
|
||||
estado=company.state if company else "",
|
||||
pais=company.country if company else "MEX",
|
||||
tax_id=company.tax_id if company else "",
|
||||
programa="IMMEX",
|
||||
autorizacion=company.immex_program if company else ""
|
||||
)
|
||||
|
||||
cliente_enviado = cliente_vendido
|
||||
|
||||
# 5. MAPEO FACTURA
|
||||
factura_schema = FacturaSchema(
|
||||
numero=header.invoice_number,
|
||||
fecha=str(header.invoice_date) if header.invoice_date else "",
|
||||
tipo_cambio=float(pedimento.exchange_rate) if pedimento and pedimento.exchange_rate else 1.0,
|
||||
moneda="USD", # O derivar de header.invoice_type o logistics
|
||||
incoterm=logistics.incoterm if logistics else "",
|
||||
observaciones=header.observation_es or header.observation_en or "",
|
||||
|
||||
# Datos Pedimento
|
||||
pedimento=pedimento.pedimento_number if pedimento else "",
|
||||
clave_pedimento=pedimento.pedimento_code if pedimento else "",
|
||||
regimen=pedimento.regime if pedimento else "",
|
||||
|
||||
# Datos Transporte
|
||||
transportista="", # logistics.carrier_id (habría que hacer join con tabla carriers)
|
||||
transporte=logistics.transport_type if logistics else "",
|
||||
num_transporte=logistics.trailer_num if logistics else "",
|
||||
placas=logistics.license_plate if logistics else "",
|
||||
aduana=pedimento.customs_office if pedimento else "",
|
||||
|
||||
# Otros
|
||||
remesa="", # No vi campo remesa en header
|
||||
acuse_electronico="",
|
||||
agente_aduanal="", # pedimento.license
|
||||
patente=pedimento.license if pedimento else "",
|
||||
precinto=logistics.seal_number if logistics else "",
|
||||
destino=logistics.destination_goods if logistics else ""
|
||||
)
|
||||
|
||||
# 6. PARTIDAS (JOIN item_lines + line_quantities + line_financials)
|
||||
lines = db.query(ItemLines).filter(ItemLines.item_id == header.id).all() # Ojo: item_id suele ser invoice_id en header
|
||||
|
||||
partidas_list = []
|
||||
|
||||
for line in lines:
|
||||
# Datos Cuantitativos
|
||||
qty = db.query(LineQuantities).filter(LineQuantities.item_line_id == line.id).first()
|
||||
# Datos Financieros
|
||||
fin = db.query(LineFinancials).filter(LineFinancials.item_line_id == line.id).first()
|
||||
|
||||
# Parte Maestra (Si line.part_number es ID, buscamos. Si es string, usamos directo)
|
||||
# Tu tabla dice 'part_number' es integer, asumo que es ID hacia tabla 'parts'
|
||||
part_master = db.query(Parts).filter(Parts.id == line.part_number).first()
|
||||
|
||||
# Construcción de valores
|
||||
descripcion = part_master.description_spanish if part_master else "S/D"
|
||||
fraccion = part_master.fraction if part_master else ""
|
||||
|
||||
cantidad = float(qty.quantity) if qty else 0.0
|
||||
# Costo: Unitario Comercial USD
|
||||
precio_unitario = float(fin.commercial_unit_cost) if fin else 0.0
|
||||
valor_total = float(fin.total_commercial_value) if fin else (cantidad * precio_unitario)
|
||||
|
||||
# Pesos y Bultos (LineQuantities tiene todo esto, ¡genial!)
|
||||
peso_n = float(qty.net_weight) if qty else 0.0
|
||||
peso_b = float(qty.gross_weight) if qty else 0.0
|
||||
bultos = int(qty.package_quantity) if qty and qty.package_quantity else 0
|
||||
clave_b = qty.package_key if qty else ""
|
||||
|
||||
partidas_list.append(PartidaSchema(
|
||||
numero_parte=part_master.part_number if part_master else str(line.part_number),
|
||||
descripcion=descripcion,
|
||||
fraccion=fraccion,
|
||||
origen="MEX", # line.country_origin? o part_master
|
||||
cantidad_importacion=self.formatear_numero(cantidad),
|
||||
unidad_medida=qty.weight_unit if qty else "KG", # O qty.package_key
|
||||
cantidad_bultos=bultos,
|
||||
clave_bultos=clave_b,
|
||||
peso_neto=self.formatear_numero(peso_n),
|
||||
peso_bruto=self.formatear_numero(peso_b),
|
||||
valor_costo_unitario=self.formatear_numero(precio_unitario),
|
||||
valor_total=self.formatear_numero(valor_total)
|
||||
))
|
||||
|
||||
# 7. TOTALES
|
||||
totales = self.calcular_totales(partidas_list, Decimal(factura_schema.tipo_cambio))
|
||||
|
||||
return FacturaImportacionCompleta(
|
||||
cliente_proveedor=cliente_proveedor,
|
||||
cliente_vendido=cliente_vendido,
|
||||
cliente_enviado=cliente_enviado,
|
||||
factura=factura_schema,
|
||||
partidas=partidas_list,
|
||||
totales=totales
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error Service A76: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error procesando datos: {str(e)}")
|
||||
|
||||
def calcular_totales(self, partidas: List[PartidaSchema], tipo_cambio: Decimal) -> TotalesSchema:
|
||||
cant = sum(float(p.cantidad_importacion) for p in partidas)
|
||||
valor = sum(float(p.valor_total) for p in partidas)
|
||||
peso_n = sum(float(p.peso_neto) for p in partidas)
|
||||
peso_b = sum(float(p.peso_bruto) for p in partidas)
|
||||
bultos = sum(p.cantidad_bultos for p in partidas)
|
||||
|
||||
# Obtener clave de bulto más común
|
||||
claves = [p.clave_bultos for p in partidas if p.clave_bultos]
|
||||
clave_comun = max(set(claves), key=claves.count) if claves else ""
|
||||
|
||||
tc = float(tipo_cambio) if tipo_cambio else 1.0
|
||||
|
||||
return TotalesSchema(
|
||||
cantidad_total=self.formatear_numero(cant),
|
||||
bultos_total=bultos,
|
||||
clave_bultos=clave_comun,
|
||||
peso_neto_total=self.formatear_numero(peso_n),
|
||||
peso_bruto_total=self.formatear_numero(peso_b),
|
||||
valor_total_total=self.formatear_numero(valor),
|
||||
valor_total_dolares=self.formatear_numero(valor / tc if tc > 0 else 0)
|
||||
)
|
||||
|
||||
# --- GENERACIÓN ---
|
||||
def generar_html(self, datos: FacturaImportacionCompleta) -> str:
|
||||
context = {
|
||||
'cliente_proveedor': datos.cliente_proveedor.model_dump(),
|
||||
'cliente_vendido': datos.cliente_vendido.model_dump(),
|
||||
'cliente_enviado': datos.cliente_enviado.model_dump(),
|
||||
'factura': datos.factura.model_dump(),
|
||||
'partidas': [p.model_dump() for p in datos.partidas],
|
||||
'totales': datos.totales.model_dump()
|
||||
}
|
||||
return self.template.render(**context)
|
||||
|
||||
def generar_pdf(self, html_content: str) -> bytes:
|
||||
options = {
|
||||
'page-size': 'Letter',
|
||||
'margin-top': '0.75in',
|
||||
'margin-right': '0.75in',
|
||||
'margin-bottom': '1.00in',
|
||||
'margin-left': '0.75in',
|
||||
'encoding': "UTF-8",
|
||||
'enable-local-file-access': None
|
||||
}
|
||||
config = self._get_wkhtmltopdf_config()
|
||||
return pdfkit.from_string(html_content, False, options=options, configuration=config)
|
||||
|
||||
def generar_factura_completa(self, db: Session, invoice_id: int, formato: str = "pdf") -> Tuple[bytes, str, str]:
|
||||
datos = self.obtener_datos(db, invoice_id)
|
||||
html = self.generar_html(datos)
|
||||
nombre = f"Factura_{datos.factura.numero}.{formato}"
|
||||
|
||||
if formato == "html":
|
||||
return html.encode('utf-8'), nombre, "text/html"
|
||||
|
||||
pdf = self.generar_pdf(html)
|
||||
return pdf, nombre, "application/pdf"
|
||||
@@ -0,0 +1,50 @@
|
||||
from enum import Enum
|
||||
from fastapi import APIRouter, Depends, Query, Response, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from core.database import get_db
|
||||
|
||||
# Importamos el servicio mexicano
|
||||
from .mex.service import FacturaImportacionMexService
|
||||
|
||||
router = APIRouter()
|
||||
servicio_mex = FacturaImportacionMexService()
|
||||
|
||||
class TipoFactura(str, Enum):
|
||||
mexicana = "mex"
|
||||
# americana = "usa"
|
||||
|
||||
class Formato(str, Enum):
|
||||
html = "html"
|
||||
pdf = "pdf"
|
||||
|
||||
@router.get("/{invoice_id}/download")
|
||||
async def descargar_factura(
|
||||
invoice_id: int,
|
||||
tipo: TipoFactura = Query(TipoFactura.mexicana),
|
||||
formato: Formato = Query(Formato.pdf),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Descargar Factura de Importación (A76)
|
||||
"""
|
||||
if tipo == TipoFactura.mexicana:
|
||||
servicio = servicio_mex
|
||||
else:
|
||||
raise HTTPException(status_code=501, detail="Tipo de factura no implementado")
|
||||
|
||||
try:
|
||||
contenido, nombre, media_type = servicio.generar_factura_completa(
|
||||
db=db,
|
||||
invoice_id=invoice_id,
|
||||
formato=formato.value
|
||||
)
|
||||
|
||||
return Response(
|
||||
content=contenido,
|
||||
media_type=media_type,
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={nombre}"
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error generando reporte: {str(e)}")
|
||||
@@ -0,0 +1,367 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" xml:lang="es">
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
|
||||
<title>Factura Importacion Mexicana - {{ factura.numero }}</title>
|
||||
<style type="text/css">
|
||||
/* ESTILOS EXACTOS DE SCAPII PARA MANTENER EL FORMATO */
|
||||
* { margin: 0; padding: 0; font-family: Tahoma, sans-serif; color: black; }
|
||||
.titulo { font-size: 14pt; padding: 3pt 0 0 6pt; }
|
||||
.grande { font-size: 13pt; padding-left: 5pt; }
|
||||
.medio-bold { font-size: 9pt; font-weight: bold; padding: 3pt 0 0 3pt; }
|
||||
.normal { font-size: 8pt; }
|
||||
.small-bold { font-size: 8pt; font-weight: bold; }
|
||||
.tiny-bold { font-size: 7pt; font-weight: bold; }
|
||||
.tiny { font-size: 7pt; }
|
||||
.mini { font-size: 5pt; }
|
||||
h1 { font-weight: bold; font-size: 8pt; }
|
||||
p { font-size: 7pt; }
|
||||
.cliente { border-bottom: 1pt solid black; text-decoration: none; display: block; width: 100%}
|
||||
.center { text-align: center; }
|
||||
.right { text-align: right; }
|
||||
.border { border: 1pt solid #808080; }
|
||||
.p-t-1 { padding-top: 1pt; } .p-t-2 { padding-top: 2pt; } .p-t-3 { padding-top: 3pt; }
|
||||
.p-t-4 { padding-top: 4pt; } .p-t-5 { padding-top: 5pt; } .p-t-8 { padding-top: 8pt; }
|
||||
.p-t-9 { padding-top: 9pt; }
|
||||
.p-l-2 { padding-left: 2pt; } .p-l-3 { padding-left: 3pt; } .p-l-5 { padding-left: 5pt; }
|
||||
.p-l-6 { padding-left: 6pt; } .p-l-7 { padding-left: 7pt; } .p-l-8 { padding-left: 8pt; }
|
||||
.p-l-9 { padding-left: 9pt; } .p-l-10 { padding-left: 10pt; }
|
||||
.p-r-1 { padding-right: 1pt; } .p-r-2 { padding-right: 2pt; } .p-r-4 { padding-right: 4pt; }
|
||||
.p-r-5 { padding-right: 5pt; } .p-r-9 { padding-right: 9pt; }
|
||||
.p-b-1 { padding-bottom: 1pt; }
|
||||
.h-10 { height: 10pt; } .h-11 { height: 11pt; } .h-14 { height: 14pt; }
|
||||
.h-15 { height: 15pt; } .h-18 { height: 18pt; } .h-22 { height: 22pt; }
|
||||
.h-82 { height: 82pt; } .h-384 { height: 384pt; }
|
||||
.line-1 { line-height: 1pt; } .line-7 { line-height: 7pt; } .line-8 { line-height: 8pt; }
|
||||
.line-9 { line-height: 9pt; } .line-10 { line-height: 10pt; }
|
||||
.m-l-3 { margin-left: 3pt; } .m-l-5 { margin-left: 5.74pt; }
|
||||
.flex-container { width: 100%; overflow: hidden; }
|
||||
.flex-container-end { width: 100%; overflow: hidden; }
|
||||
.width-48 { width: 48%; float: left; }
|
||||
.width-48-right { width: 48%; float: right; text-align: right; }
|
||||
.position-relative-centered { position: relative; width: 48%; float: left; }
|
||||
.full-width-block { display: block; width: 100%; }
|
||||
.clearfix::after { content: ""; display: table; clear: both; }
|
||||
table, tbody { vertical-align: top; overflow: visible; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="flex-container clearfix">
|
||||
<div class="width-48">
|
||||
<p class="titulo">Factura de Importacion</p>
|
||||
<div class="cliente" style="height: 1px;"></div>
|
||||
<p class="cliente p-t-1 p-b-1"></p>
|
||||
</div>
|
||||
<div class="width-48-right">
|
||||
<p class="p-l-5 line-1"><span></span></p>
|
||||
<p class="p-t-2"><br></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container-end clearfix">
|
||||
<div class="position-relative-centered">
|
||||
<div style="float: right; text-align: left;">
|
||||
<h1 class="cliente">{{ cliente_proveedor.header }}</h1>
|
||||
<p>{{ cliente_proveedor.nombre }}</p>
|
||||
<p>{{ cliente_proveedor.direccion }}
|
||||
{% if cliente_proveedor.num_exterior %} Ext: {{ cliente_proveedor.num_exterior }}{% endif %}
|
||||
{% if cliente_proveedor.num_interior %} Int: {{ cliente_proveedor.num_interior }}{% endif %}
|
||||
</p>
|
||||
<p>{{ cliente_proveedor.colonia }} {% if cliente_proveedor.codigo_postal %} CP: {{ cliente_proveedor.codigo_postal }}{% endif %}</p>
|
||||
<p>{{ cliente_proveedor.ciudad }}, {{ cliente_proveedor.estado }}, {{ cliente_proveedor.pais }}</p>
|
||||
<p>TAX ID: {{ cliente_proveedor.tax_id }}
|
||||
{% if cliente_proveedor.programa and cliente_proveedor.programa != 'Ninguno' %}
|
||||
{{ cliente_proveedor.programa }}: {{ cliente_proveedor.autorizacion }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p><br></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: relative;">
|
||||
<table cellspacing="0" class="m-l-3" style="float: right; text-align: left;">
|
||||
<tbody>
|
||||
<tr class="h-18">
|
||||
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
|
||||
<p class="medio-bold">FACTURA:</p>
|
||||
</td>
|
||||
<td class="border" colspan="3" style="width:176pt">
|
||||
<p class="grande">{{ factura.numero }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="h-11">
|
||||
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
|
||||
<p class="normal p-l-3 line-9">Fecha:</p>
|
||||
</td>
|
||||
<td class="border" style="width:72pt">
|
||||
<p class="normal p-l-2 line-9">{{ factura.fecha }}</p>
|
||||
</td>
|
||||
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
|
||||
<p class="normal p-l-2 line-9">T. Cambio:</p>
|
||||
</td>
|
||||
<td class="border" style="width:52pt">
|
||||
<p class="normal center line-9">{{ factura.tipo_cambio }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="h-11">
|
||||
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
|
||||
<p class="small-bold p-l-3 line-9">Pedimento:</p>
|
||||
</td>
|
||||
<td class="border" colspan="1" style="width:109pt">
|
||||
<p class="normal p-l-3 line-9">{{ factura.pedimento }}</p>
|
||||
</td>
|
||||
<td bgcolor="#E4E4E4" class="border">
|
||||
<p class="normal p-l-3 line-9">Clave:</p>
|
||||
</td>
|
||||
<td class="border" style="width:37pt">
|
||||
<p class="normal p-r-2 line-9 center">{{ factura.clave_pedimento }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="h-11">
|
||||
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
|
||||
<p class="normal p-l-3 line-9">Remesa:</p>
|
||||
</td>
|
||||
<td class="border" style="width:38pt">
|
||||
<p class="normal p-l-10 line-9">{{ factura.remesa }}</p>
|
||||
</td>
|
||||
<td bgcolor="#E4E4E4" class="border" style="width:79pt">
|
||||
<p class="normal p-l-6 line-9">Acuse:</p>
|
||||
</td>
|
||||
<td class="border" style="width:59pt">
|
||||
<p class="normal p-l-6 line-9">{{ factura.acuse_electronico or 'N/A' }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="h-22">
|
||||
<td class="border" colspan="4" style="width:228pt">
|
||||
<p class="tiny-bold p-l-3 line-7">Agente Aduanal:</p>
|
||||
<p class="tiny p-l-3 line-7">{{ factura.agente_aduanal or '' }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="h-15">
|
||||
<td class="border" colspan="2">
|
||||
<p><span class="tiny-bold p-l-3 line-7">Patente: </span><span class="tiny p-l-3 line-7">{{ factura.patente or '' }}</span></p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<span class="tiny-bold p-l-3 line-7">Regimen:</span><span class="tiny p-l-3 line-7">{{ factura.regimen or '' }}</span>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="tiny-bold p-l-3 line-7">INCOTERM:</p>
|
||||
<p class="tiny p-l-3 line-7">{{ factura.incoterm or '' }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border" colspan="2">
|
||||
{% if factura.precinto %}
|
||||
<p class="tiny-bold p-l-3 line-7">Precinto: {{ factura.precinto }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="tiny-bold p-l-3 line-7">Aduana: {{ factura.aduana }}</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
{% if factura.destino %}
|
||||
<p class="tiny-bold p-l-3 line-7">Destino: {{ factura.destino }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-container clearfix">
|
||||
<div class="width-48">
|
||||
<h1 class="p-t-5 p-l-5 line-10 cliente">{{ cliente_vendido.header }}</h1>
|
||||
<p class="p-l-5 line-8">{{ cliente_vendido.nombre }}</p>
|
||||
<p class="p-l-5">{{ cliente_vendido.direccion }}
|
||||
{% if cliente_vendido.num_exterior %} Ext: {{ cliente_vendido.num_exterior }}{% endif %}
|
||||
{% if cliente_vendido.num_interior %} Int: {{ cliente_vendido.num_interior }}{% endif %}
|
||||
</p>
|
||||
<p class="p-l-5">{{ cliente_vendido.colonia }} {% if cliente_vendido.codigo_postal %} CP: {{ cliente_vendido.codigo_postal }}{% endif %}</p>
|
||||
<p class="p-l-5">{{ cliente_vendido.ciudad }}, {{ cliente_vendido.estado }}, {{ cliente_vendido.pais }}</p>
|
||||
<p class="p-l-5">RFC: {{ cliente_vendido.tax_id }}
|
||||
{% if cliente_vendido.programa and cliente_vendido.programa != 'Ninguno' %}
|
||||
{{ cliente_vendido.programa }}: {{ cliente_vendido.autorizacion }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="width-48-right" style="text-align: left;">
|
||||
<h1 class="p-t-5 p-l-5 line-10 cliente full-width-block">{{ cliente_enviado.header }}</h1>
|
||||
<p class="p-l-5 line-8">{{ cliente_enviado.nombre }}</p>
|
||||
<p class="p-l-5">{{ cliente_enviado.direccion }}
|
||||
{% if cliente_enviado.num_exterior %} Ext: {{ cliente_enviado.num_exterior }}{% endif %}
|
||||
{% if cliente_enviado.num_interior %} Int: {{ cliente_enviado.num_interior }}{% endif %}
|
||||
</p>
|
||||
<p class="p-l-5">{{ cliente_enviado.colonia }} {% if cliente_enviado.codigo_postal %} CP: {{ cliente_enviado.codigo_postal }}{% endif %}</p>
|
||||
<p class="p-l-5">{{ cliente_enviado.ciudad }}, {{ cliente_enviado.estado }}, {{ cliente_enviado.pais }}</p>
|
||||
<p class="p-l-5">RFC: {{ cliente_enviado.tax_id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="p-t-8"><br /></p>
|
||||
</header>
|
||||
|
||||
<table cellspacing="0" class="m-l-5" style="width: 99%; max-width: 580pt;">
|
||||
<thead>
|
||||
<tr class="h-10">
|
||||
<td bgcolor="#E4E4E4" class="border" style="width:60pt"><p class="tiny line-9">Transportista:</p></td>
|
||||
<td class="border" colspan="4" style="width:120pt"><p class="tiny">{{ factura.transportista }}</p></td>
|
||||
<td class="border" style="width:40pt"><p class="tiny">SCAC: {{ factura.scac }}</p></td>
|
||||
<td bgcolor="#E4E4E4" class="border" style="width:40pt"><p class="tiny">INCOTERM:</p></td>
|
||||
<td class="border" style="width:40pt"><p class="tiny">{{ factura.incoterm }}</p></td>
|
||||
<td class="border" style="width:50pt">
|
||||
<p class="tiny line-9">Aduana: <span class="mini">{{ factura.aduana }}</span></p>
|
||||
</td>
|
||||
<td bgcolor="#E4E4E4" class="border" style="width:20pt"><p><br /></p></td>
|
||||
</tr>
|
||||
<tr class="h-10">
|
||||
<td bgcolor="#E4E4E4" class="border" style="width:60pt">
|
||||
<p class="tiny line-9">Transporte:</p>
|
||||
</td>
|
||||
<td class="border" colspan="4" style="width:120pt"><p class="tiny">{{ factura.transporte }}: {{ factura.num_transporte }}</p></td>
|
||||
<td class="border" colspan="3" style="width:80pt"><p class="tiny">CAAT: {{ factura.caat }}</p></td>
|
||||
<td class="border" colspan="2" style="width:100pt">
|
||||
<p class="tiny p-t-1 line-8">Placas: {{ factura.placas or 'N/A' }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="h-10">
|
||||
<td class="border" colspan="2" style="width:80pt"><p><br /></p></td>
|
||||
<td class="border" colspan="8"><p><br /></p></td>
|
||||
</tr>
|
||||
|
||||
<tr class="h-10">
|
||||
<td class="border" rowspan="2">
|
||||
<p class="tiny-bold p-t-5 center">Línea</p>
|
||||
</td>
|
||||
<td class="border" rowspan="2">
|
||||
<p class="tiny-bold p-l-2 line-10">Número de Parte</p>
|
||||
<p class="tiny-bold p-l-2 line-10">Descripción</p>
|
||||
</td>
|
||||
<td class="border" colspan="3">
|
||||
<p class="tiny-bold center line-9">Comercial</p>
|
||||
</td>
|
||||
<td class="border" style="width:50pt">
|
||||
<p class="tiny-bold center line-9">Empaque</p>
|
||||
</td>
|
||||
<td class="border" colspan="2">
|
||||
<p class="tiny-bold center line-9">Peso (KGS)</p>
|
||||
</td>
|
||||
<td class="border" colspan="2">
|
||||
<p class="tiny-bold line-9 center">Valores</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="h-11">
|
||||
<td class="border center" colspan="2">
|
||||
<p class="mini p-l-2">Cantidad</p>
|
||||
</td>
|
||||
<td class="border center">
|
||||
<p class="mini p-l-1">U.M.</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini center">Tipo</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini center">Neto</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini center">Bruto</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini center">Unitario</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini center">Total</p>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="h-384" style="width: 100%;">
|
||||
{% for partida in partidas %}
|
||||
<tr>
|
||||
<td class="border">
|
||||
<p class="mini p-t-3 center">{{ loop.index }}</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini p-t-1" style="font-weight: bold;">{{ partida.numero_parte }}</p>
|
||||
<p class="mini">{{ partida.descripcion }}</p>
|
||||
<p class="mini">Frac: {{ partida.fraccion }} / Orig: {{ partida.origen or 'MEX' }}</p>
|
||||
</td>
|
||||
<td class="border" colspan="2">
|
||||
<p class="mini p-t-2 center">{{ partida.cantidad_importacion }}</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini p-t-2 center">{{ partida.unidad_medida }}</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini p-t-2 center">
|
||||
{% if partida.cantidad_bultos != 0 %}{{ partida.cantidad_bultos }}{% endif %}
|
||||
{{ partida.clave_bultos }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini p-t-3 center">{{ partida.peso_neto }}</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini p-t-3 center">{{ partida.peso_bruto }}</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini p-t-2 center">${{ partida.valor_costo_unitario }}</p>
|
||||
</td>
|
||||
<td class="border">
|
||||
<p class="mini p-t-2 right p-r-2">${{ partida.valor_total }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
<tr class="h-14">
|
||||
<td bgcolor="#E4E4E4" class="border" colspan="2" style="width:123pt">
|
||||
<p class="small-bold p-t-2 p-l-3 line-10">
|
||||
<span>Observaciones:</span>
|
||||
<span class="small-bold" style="float: right; margin-right: 2pt;">TOTALES</span>
|
||||
</p>
|
||||
</td>
|
||||
<td class="border" colspan="2" style="width:28pt">
|
||||
<p class="tiny p-t-5 p-r-1 line-8 center">{{ totales.cantidad_total }}</p>
|
||||
</td>
|
||||
<td class="border" style="width:22pt"></td>
|
||||
<td class="border" style="width:40pt">
|
||||
<p class="tiny p-t-4 center line-8">
|
||||
{% if totales.bultos_total != 0 %}{{ totales.bultos_total }}{% endif %}
|
||||
<span>{{ totales.clave_bultos or '' }}</span>
|
||||
</p>
|
||||
</td>
|
||||
<td class="border" style="width:27pt">
|
||||
<p class="tiny p-t-5 center line-8">{{ totales.peso_neto_total }}</p>
|
||||
</td>
|
||||
<td class="border" style="width:28pt">
|
||||
<p class="tiny p-t-5 center line-8">{{ totales.peso_bruto_total }}</p>
|
||||
</td>
|
||||
<td style="width:30pt"></td>
|
||||
<td class="border" style="width:35pt">
|
||||
<p class="tiny p-t-5 right line-8 p-r-2">${{ totales.valor_total_total }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="border" style="height: 100pt; text-align:start; vertical-align: top;">
|
||||
<p class="mini p-l-2 p-t-2">{{ factura.observaciones }}</p>
|
||||
</td>
|
||||
<td colspan="8" style="width:336pt; vertical-align: bottom; height: 100%;">
|
||||
<p style="border-bottom: 1pt solid black; width: 80%; margin: 0 auto 2pt auto;"></p>
|
||||
<p class="normal center" style="margin-bottom: 0;">{{ cliente_proveedor.nombre }}</p>
|
||||
<p style="margin-bottom: 0;"><br /></p>
|
||||
<p class="small-bold center line-7" style="margin-bottom: 0;">Los valores expresados en esta factura son en: {{ factura.moneda }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="10" style="width:580pt; vertical-align: top; height: 100%;">
|
||||
<p class="p-t-8"><br /></p>
|
||||
<p class="p-l-5 line-8 tiny-bold">Normal Por Parte</p>
|
||||
<p class="normal p-l-5 line-9">Declaro bajo protesta de decir verdad que la información contenida en este documento es verdadera y me hago responsable de comprobar lo aquí declarado.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -38,3 +38,6 @@ black==25.9.0
|
||||
flake8==7.3.0
|
||||
mypy==1.18.2
|
||||
pylint==4.0.2
|
||||
|
||||
# reportes
|
||||
Jinja2==3.1.6
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/ports/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/ports/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/data-table.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
|
||||
@@ -1,496 +1,543 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { invoicesApi, type Invoice, type OperationType } from '$lib/api/dashboard/a76/invoices';
|
||||
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import type { PageData } from './$types';
|
||||
import { browser } from '$app/environment';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Plus, Filter, Trash2, RefreshCw } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { invoicesApi, type Invoice, type OperationType } from '$lib/api/dashboard/a76/invoices';
|
||||
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import type { PageData } from './$types';
|
||||
import { browser } from '$app/environment';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Plus, Filter, Trash2, RefreshCw } from 'lucide-svelte';
|
||||
|
||||
// IMPORTANTE: Asegúrate de tener instalada svelte-sonner para las notificaciones
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Estado para filtros
|
||||
// Nota: Los query parameters invoice_type y operation_type se pueden usar para filtrar
|
||||
// Ejemplo: /dashboard/invoices?invoice_type=TEM&operation_type=imp
|
||||
let filters = $state({
|
||||
operation_type: (data.filters?.operation_type || '') as '' | OperationType,
|
||||
invoice_type: data.filters?.invoice_type || '',
|
||||
invoice_number: data.filters?.invoice_number || '',
|
||||
project_number: data.filters?.project_number || '',
|
||||
year: data.filters?.year || ''
|
||||
});
|
||||
// Estado para filtros
|
||||
// Nota: Los query parameters invoice_type y operation_type se pueden usar para filtrar
|
||||
// Ejemplo: /dashboard/invoices?invoice_type=TEM&operation_type=imp
|
||||
let filters = $state({
|
||||
operation_type: (data.filters?.operation_type || '') as '' | OperationType,
|
||||
invoice_type: data.filters?.invoice_type || '',
|
||||
invoice_number: data.filters?.invoice_number || '',
|
||||
project_number: data.filters?.project_number || '',
|
||||
year: data.filters?.year || ''
|
||||
});
|
||||
|
||||
// Efecto reactivo para actualizar filtros cuando cambian los query parameters en la URL
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
const searchParams = $page.url.searchParams;
|
||||
const urlOperationType = searchParams.get('operation_type');
|
||||
const urlInvoiceType = searchParams.get('invoice_type');
|
||||
const urlInvoiceNumber = searchParams.get('invoice_number');
|
||||
const urlProjectNumber = searchParams.get('project_number');
|
||||
const urlYear = searchParams.get('year');
|
||||
// Efecto reactivo para actualizar filtros cuando cambian los query parameters en la URL
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
const searchParams = $page.url.searchParams;
|
||||
const urlOperationType = searchParams.get('operation_type');
|
||||
const urlInvoiceType = searchParams.get('invoice_type');
|
||||
const urlInvoiceNumber = searchParams.get('invoice_number');
|
||||
const urlProjectNumber = searchParams.get('project_number');
|
||||
const urlYear = searchParams.get('year');
|
||||
|
||||
// Actualizar filtros si hay cambios en la URL
|
||||
filters.operation_type = (urlOperationType || '') as '' | OperationType;
|
||||
filters.invoice_type = urlInvoiceType || '';
|
||||
filters.invoice_number = urlInvoiceNumber || '';
|
||||
filters.project_number = urlProjectNumber || '';
|
||||
filters.year = urlYear || '';
|
||||
}
|
||||
});
|
||||
// Actualizar filtros si hay cambios en la URL
|
||||
filters.operation_type = (urlOperationType || '') as '' | OperationType;
|
||||
filters.invoice_type = urlInvoiceType || '';
|
||||
filters.invoice_number = urlInvoiceNumber || '';
|
||||
filters.project_number = urlProjectNumber || '';
|
||||
filters.year = urlYear || '';
|
||||
}
|
||||
});
|
||||
|
||||
// Efecto para limpiar invoice_type si no es válido para el operation_type seleccionado
|
||||
$effect(() => {
|
||||
if (filters.invoice_type && filters.operation_type) {
|
||||
const selectedOption = allInvoiceTypeOptions().find(opt => opt.value === filters.invoice_type);
|
||||
if (selectedOption && selectedOption.operation !== 'both' && selectedOption.operation !== filters.operation_type) {
|
||||
// El tipo de factura seleccionado no es válido para esta operación
|
||||
filters.invoice_type = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
// Efecto para limpiar invoice_type si no es válido para el operation_type seleccionado
|
||||
$effect(() => {
|
||||
if (filters.invoice_type && filters.operation_type) {
|
||||
const selectedOption = allInvoiceTypeOptions().find(opt => opt.value === filters.invoice_type);
|
||||
if (selectedOption && selectedOption.operation !== 'both' && selectedOption.operation !== filters.operation_type) {
|
||||
// El tipo de factura seleccionado no es válido para esta operación
|
||||
filters.invoice_type = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Efecto para actualizar la URL cuando cambien los filtros
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters.operation_type) params.set('operation_type', filters.operation_type);
|
||||
if (filters.invoice_type) params.set('invoice_type', filters.invoice_type);
|
||||
if (filters.invoice_number) params.set('invoice_number', filters.invoice_number);
|
||||
if (filters.project_number) params.set('project_number', filters.project_number);
|
||||
if (filters.year) params.set('year', filters.year);
|
||||
|
||||
const queryString = params.toString();
|
||||
const newUrl = queryString ? `?${queryString}` : window.location.pathname;
|
||||
|
||||
// Solo actualizar si la URL es diferente (evitar loops infinitos)
|
||||
if (window.location.search !== (queryString ? `?${queryString}` : '')) {
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Efecto para actualizar la URL cuando cambien los filtros
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters.operation_type) params.set('operation_type', filters.operation_type);
|
||||
if (filters.invoice_type) params.set('invoice_type', filters.invoice_type);
|
||||
if (filters.invoice_number) params.set('invoice_number', filters.invoice_number);
|
||||
if (filters.project_number) params.set('project_number', filters.project_number);
|
||||
if (filters.year) params.set('year', filters.year);
|
||||
|
||||
const queryString = params.toString();
|
||||
const newUrl = queryString ? `?${queryString}` : window.location.pathname;
|
||||
|
||||
// Solo actualizar si la URL es diferente (evitar loops infinitos)
|
||||
if (window.location.search !== (queryString ? `?${queryString}` : '')) {
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Efecto para aplicar filtros automáticamente cuando cambian
|
||||
let filterTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
$effect(() => {
|
||||
// Observar cambios en los filtros
|
||||
const _ = {
|
||||
operation_type: filters.operation_type,
|
||||
invoice_type: filters.invoice_type,
|
||||
invoice_number: filters.invoice_number,
|
||||
project_number: filters.project_number,
|
||||
year: filters.year
|
||||
};
|
||||
// Efecto para aplicar filtros automáticamente cuando cambian
|
||||
let filterTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
$effect(() => {
|
||||
// Observar cambios en los filtros
|
||||
const _ = {
|
||||
operation_type: filters.operation_type,
|
||||
invoice_type: filters.invoice_type,
|
||||
invoice_number: filters.invoice_number,
|
||||
project_number: filters.project_number,
|
||||
year: filters.year
|
||||
};
|
||||
|
||||
// Debounce para evitar múltiples llamadas rápidas
|
||||
if (filterTimeout) clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
applyFilters();
|
||||
}, 300); // Esperar 300ms después del último cambio
|
||||
});
|
||||
// Debounce para evitar múltiples llamadas rápidas
|
||||
if (filterTimeout) clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
applyFilters();
|
||||
}, 300); // Esperar 300ms después del último cambio
|
||||
});
|
||||
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
// Función para obtener el valor de una cookie
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
// Función para obtener el valor de una cookie
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
|
||||
// Verificar si hay token en las cookies
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
// Verificar si hay token en las cookies
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
|
||||
if (cookieToken && cookieToken !== localToken) {
|
||||
localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
if (cookieToken && cookieToken !== localToken) {
|
||||
localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
|
||||
// También sincronizar refresh_token si existe
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
// También sincronizar refresh_token si existe
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
|
||||
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
|
||||
localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
|
||||
localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
|
||||
// Escuchar cambios de compañía
|
||||
const handleCompanyChange = (event: CustomEvent) => {
|
||||
// Recargar los datos sin recargar la página completa
|
||||
reloadData();
|
||||
};
|
||||
// Escuchar cambios de compañía
|
||||
const handleCompanyChange = (event: CustomEvent) => {
|
||||
// Recargar los datos sin recargar la página completa
|
||||
reloadData();
|
||||
};
|
||||
|
||||
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
};
|
||||
}
|
||||
});
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<Invoice[]>(data.items || []);
|
||||
let currentPage = $state(data.page);
|
||||
let pageSize = $state(50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<Invoice[]>(data.items || []);
|
||||
let currentPage = $state(data.page);
|
||||
let pageSize = $state(50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, currentPage + 1, pageSize, filterParams);
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, currentPage + 1, pageSize, filterParams);
|
||||
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error en loadMore:', response.error, 'Status:', response.status);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error en loadMore:', response.error, 'Status:', response.status);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
// Agregar los nuevos items al array existente
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Invoices] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
if (response.data?.items) {
|
||||
// Agregar los nuevos items al array existente
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Invoices] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyFilters() {
|
||||
// Reset y recargar con filtros
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
// Construir query parameters para el endpoint
|
||||
// Los filtros se mapean a los parámetros del API
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
|
||||
async function applyFilters() {
|
||||
// Reset y recargar con filtros
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
// Construir query parameters para el endpoint
|
||||
// Los filtros se mapean a los parámetros del API
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
|
||||
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error aplicando filtros:', response.error);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error aplicando filtros:', response.error);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('📊 [Invoices] Error applying filters:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
if (response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error aplicando filtros';
|
||||
console.error('📊 [Invoices] Error applying filters:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
filters = {
|
||||
operation_type: '',
|
||||
invoice_type: '',
|
||||
invoice_number: '',
|
||||
project_number: '',
|
||||
year: ''
|
||||
};
|
||||
applyFilters();
|
||||
}
|
||||
function clearFilters() {
|
||||
filters = {
|
||||
operation_type: '',
|
||||
invoice_type: '',
|
||||
invoice_number: '',
|
||||
project_number: '',
|
||||
year: ''
|
||||
};
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
// Reset y recargar desde el principio usando la API
|
||||
if (!companyStore.activeCompany) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
async function reloadData() {
|
||||
// Reset y recargar desde el principio usando la API
|
||||
if (!companyStore.activeCompany) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany.id;
|
||||
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
try {
|
||||
const companyId = companyStore.activeCompany.id;
|
||||
|
||||
const filterParams = {
|
||||
operation_type: filters.operation_type || undefined,
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
|
||||
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
|
||||
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error en reloadData:', response.error);
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
console.error('📊 [Invoices] Error en reloadData:', response.error);
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
// Reemplazar todos los items con los nuevos datos
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error recargando datos';
|
||||
console.error('📊 [Invoices] Error reloading:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
if (response.data?.items) {
|
||||
// Reemplazar todos los items con los nuevos datos
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error recargando datos';
|
||||
console.error('📊 [Invoices] Error reloading:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateClick() {
|
||||
// Leer los filtros actuales desde la URL (que ya se actualizó con el $effect)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
// Mapear operation_type de 'imp'/'exp' a números 1/2
|
||||
const operationType = params.get('operation_type');
|
||||
if (operationType) {
|
||||
const operationTypeNumber = operationType === 'exp' ? 1 : 2;
|
||||
params.set('operation_type', operationTypeNumber.toString());
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = queryString
|
||||
? `/dashboard/invoices/edit/new?${queryString}`
|
||||
: '/dashboard/invoices/edit/new';
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
// --- NUEVA FUNCIÓN: DESCARGAR PDF ---
|
||||
async function handleDownloadPdf(invoice: any) {
|
||||
const toastId = toast.loading("Generando PDF...");
|
||||
|
||||
try {
|
||||
// Obtenemos el token del localStorage
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) throw new Error('No hay sesión activa');
|
||||
|
||||
function handleSuccess() {
|
||||
// Recargar datos después de crear/editar/eliminar
|
||||
reloadData();
|
||||
}
|
||||
// Determinar tipo: Si es importación (imp) -> mex, Si es exportación (exp) -> usa (o lo que definas)
|
||||
// Por ahora hardcodeamos 'mex' como pediste
|
||||
const tipo = 'mex';
|
||||
|
||||
const endpoint = `/api/v1/a76/reports/importacion/facturas/${invoice.id}/download?tipo=${tipo}&formato=pdf`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
// Opciones de tipo de operación para el filtro
|
||||
const operationTypeOptions = [
|
||||
{ value: "", label: "Todas" },
|
||||
{ value: 'imp', label: 'Importación' },
|
||||
{ value: 'exp', label: 'Exportación' }
|
||||
];
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => ({}));
|
||||
throw new Error(errData.detail || 'Error al generar el reporte');
|
||||
}
|
||||
|
||||
// Todas las opciones de tipo de factura con su operación correspondiente
|
||||
// Ahora se cargan desde el servidor en lugar de estar hardcodeadas
|
||||
const allInvoiceTypeOptions = $derived(() => {
|
||||
const options = [{ value: "", label: "Todas", operation: "both" }];
|
||||
|
||||
// Agregar los tipos de factura del servidor
|
||||
if (data.invoiceTypes) {
|
||||
data.invoiceTypes.forEach((type: any) => {
|
||||
options.push({
|
||||
value: type.key,
|
||||
label: type.description,
|
||||
operation: type.operation
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
// Convertir respuesta a Blob
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Crear link fantasma y descargar
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
// Intentar usar nombre del header o fallback
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let fileName = `Factura_${invoice.invoice_number || invoice.id}.pdf`;
|
||||
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="?([^"]+)"?/);
|
||||
if (match && match[1]) fileName = match[1];
|
||||
}
|
||||
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Limpieza
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
toast.success("Factura descargada", { id: toastId });
|
||||
|
||||
// Opciones de tipo de factura filtradas según el tipo de operación seleccionado
|
||||
const invoiceTypeOptions = $derived(() => {
|
||||
const allOptions = allInvoiceTypeOptions();
|
||||
|
||||
if (!filters.operation_type) {
|
||||
return allOptions;
|
||||
}
|
||||
|
||||
return allOptions.filter(option =>
|
||||
option.operation === 'both' ||
|
||||
option.operation === filters.operation_type
|
||||
);
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.message || "No se pudo descargar la factura", { id: toastId });
|
||||
}
|
||||
}
|
||||
|
||||
// Crear columnas con el callback onSuccess
|
||||
const columns = createColumns(handleSuccess);
|
||||
function handleCreateClick() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const operationType = params.get('operation_type');
|
||||
if (operationType) {
|
||||
const operationTypeNumber = operationType === 'exp' ? 1 : 2;
|
||||
params.set('operation_type', operationTypeNumber.toString());
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = queryString
|
||||
? `/dashboard/invoices/edit/new?${queryString}`
|
||||
: '/dashboard/invoices/edit/new';
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Opciones de tipo de operación para el filtro
|
||||
const operationTypeOptions = [
|
||||
{ value: "", label: "Todas" },
|
||||
{ value: 'imp', label: 'Importación' },
|
||||
{ value: 'exp', label: 'Exportación' }
|
||||
];
|
||||
|
||||
// Todas las opciones de tipo de factura con su operación correspondiente
|
||||
const allInvoiceTypeOptions = $derived(() => {
|
||||
const options = [{ value: "", label: "Todas", operation: "both" }];
|
||||
if (data.invoiceTypes) {
|
||||
data.invoiceTypes.forEach((type: any) => {
|
||||
options.push({
|
||||
value: type.key,
|
||||
label: type.description,
|
||||
operation: type.operation
|
||||
});
|
||||
});
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
const invoiceTypeOptions = $derived(() => {
|
||||
const allOptions = allInvoiceTypeOptions();
|
||||
if (!filters.operation_type) {
|
||||
return allOptions;
|
||||
}
|
||||
return allOptions.filter(option =>
|
||||
option.operation === 'both' ||
|
||||
option.operation === filters.operation_type
|
||||
);
|
||||
});
|
||||
|
||||
// --- AQUÍ PASAMOS LA FUNCIÓN DE DESCARGA A LAS COLUMNAS ---
|
||||
const columns = createColumns(handleSuccess, handleDownloadPdf);
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona las facturas del sistema
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={handleCreateClick}>
|
||||
<Plus class="mr-2" size={16} />
|
||||
Nueva Factura
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona las facturas del sistema
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={handleCreateClick}>
|
||||
<Plus class="mr-2" size={16} />
|
||||
Nueva Factura
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Filtros -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Filtros</Card.Title>
|
||||
<Card.Description>Filtra las facturas por diferentes criterios (los filtros se aplican automáticamente)</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-operation-type">Tipo de Operación</Label>
|
||||
<select
|
||||
id="filter-operation-type"
|
||||
bind:value={filters.operation_type}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
{#each operationTypeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Filtros</Card.Title>
|
||||
<Card.Description>Filtra las facturas por diferentes criterios (los filtros se aplican automáticamente)</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-operation-type">Tipo de Operación</Label>
|
||||
<select
|
||||
id="filter-operation-type"
|
||||
bind:value={filters.operation_type}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
{#each operationTypeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-invoice-type">Tipo de Factura</Label>
|
||||
<select
|
||||
id="filter-invoice-type"
|
||||
bind:value={filters.invoice_type}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
{#each invoiceTypeOptions() as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-invoice-type">Tipo de Factura</Label>
|
||||
<select
|
||||
id="filter-invoice-type"
|
||||
bind:value={filters.invoice_type}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
{#each invoiceTypeOptions() as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-invoice-number">Número de Factura</Label>
|
||||
<Input
|
||||
id="filter-invoice-number"
|
||||
bind:value={filters.invoice_number}
|
||||
placeholder="Ej: INV-2024-001"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-invoice-number">Número de Factura</Label>
|
||||
<Input
|
||||
id="filter-invoice-number"
|
||||
bind:value={filters.invoice_number}
|
||||
placeholder="Ej: INV-2024-001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-project-number">Número de Proyecto</Label>
|
||||
<Input
|
||||
id="filter-project-number"
|
||||
bind:value={filters.project_number}
|
||||
placeholder="Ej: PROJ-001"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-project-number">Número de Proyecto</Label>
|
||||
<Input
|
||||
id="filter-project-number"
|
||||
bind:value={filters.project_number}
|
||||
placeholder="Ej: PROJ-001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-year">Año</Label>
|
||||
<Input
|
||||
id="filter-year"
|
||||
bind:value={filters.year}
|
||||
placeholder="Ej: 2024"
|
||||
maxlength={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="space-y-2">
|
||||
<Label for="filter-year">Año</Label>
|
||||
<Input
|
||||
id="filter-year"
|
||||
bind:value={filters.year}
|
||||
placeholder="Ej: 2024"
|
||||
maxlength={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<Card.Root class="border-destructive">
|
||||
<Card.Header>
|
||||
<Card.Title class="text-destructive">Error</Card.Title>
|
||||
<Card.Description>{error}</Card.Description>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
{#if error}
|
||||
<Card.Root class="border-destructive">
|
||||
<Card.Header>
|
||||
<Card.Title class="text-destructive">Error</Card.Title>
|
||||
<Card.Description>{error}</Card.Description>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
|
||||
<!-- Data Table -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Listado de Facturas</Card.Title>
|
||||
<Card.Description>
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button variant="outline" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2" size={16} />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<!-- TanStack DataTable con Infinite Scroll -->
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Listado de Facturas</Card.Title>
|
||||
<Card.Description>
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button variant="outline" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2" size={16} />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
Reference in New Issue
Block a user