From ff2df90dbe8f909a8f82f0ee7dd01d43f56d19e6 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Fri, 9 Jan 2026 08:26:44 -0600 Subject: [PATCH] WIP: Guardando trabajo antes de actualizar con development --- backend/Dockerfile | 25 + .../importacion/facturas/mex/schemas.py | 78 ++ .../importacion/facturas/mex/service.py | 296 ++++++ .../reports/importacion/facturas/routes.py | 50 + .../importacion/facturas/tem/schemas.py | 0 .../importacion/facturas/tem/service.py | 0 .../facturas/templates/factura_mex_ver.html | 367 +++++++ .../facturas/templates/factura_tem_hor.html | 0 backend/requirements.txt | 3 + .../general_catalogs/ports/+page.svelte | 2 +- .../routes/dashboard/invoices/+page.svelte | 943 +++++++++--------- 11 files changed, 1315 insertions(+), 449 deletions(-) create mode 100644 backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py create mode 100644 backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py create mode 100644 backend/api/v1/modules/a76/reports/importacion/facturas/routes.py create mode 100644 backend/api/v1/modules/a76/reports/importacion/facturas/tem/schemas.py create mode 100644 backend/api/v1/modules/a76/reports/importacion/facturas/tem/service.py create mode 100644 backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html create mode 100644 backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_tem_hor.html diff --git a/backend/Dockerfile b/backend/Dockerfile index cff9b109..89d5822d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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 . diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py new file mode 100644 index 00000000..a7e765e1 --- /dev/null +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py @@ -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 \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py new file mode 100644 index 00000000..c188fe09 --- /dev/null +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py @@ -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" \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/routes.py b/backend/api/v1/modules/a76/reports/importacion/facturas/routes.py new file mode 100644 index 00000000..0638ea41 --- /dev/null +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/routes.py @@ -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)}") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/tem/schemas.py b/backend/api/v1/modules/a76/reports/importacion/facturas/tem/schemas.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/tem/service.py b/backend/api/v1/modules/a76/reports/importacion/facturas/tem/service.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html b/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html new file mode 100644 index 00000000..eff919ee --- /dev/null +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html @@ -0,0 +1,367 @@ + + + + + Factura Importacion Mexicana - {{ factura.numero }} + + + + +
+
+
+

Factura de Importacion

+
+

+
+
+

+


+
+
+
+
+
+

{{ cliente_proveedor.header }}

+

{{ cliente_proveedor.nombre }}

+

{{ 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 %} +

+

{{ cliente_proveedor.colonia }} {% if cliente_proveedor.codigo_postal %} CP: {{ cliente_proveedor.codigo_postal }}{% endif %}

+

{{ cliente_proveedor.ciudad }}, {{ cliente_proveedor.estado }}, {{ cliente_proveedor.pais }}

+

TAX ID: {{ cliente_proveedor.tax_id }} + {% if cliente_proveedor.programa and cliente_proveedor.programa != 'Ninguno' %} + {{ cliente_proveedor.programa }}: {{ cliente_proveedor.autorizacion }} + {% endif %} +

+


+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

FACTURA:

+
+

{{ factura.numero }}

+
+

Fecha:

+
+

{{ factura.fecha }}

+
+

T. Cambio:

+
+

{{ factura.tipo_cambio }}

+
+

Pedimento:

+
+

{{ factura.pedimento }}

+
+

Clave:

+
+

{{ factura.clave_pedimento }}

+
+

Remesa:

+
+

{{ factura.remesa }}

+
+

Acuse:

+
+

{{ factura.acuse_electronico or 'N/A' }}

+
+

Agente Aduanal:

+

{{ factura.agente_aduanal or '' }}

+
+

Patente: {{ factura.patente or '' }}

+
+ Regimen:{{ factura.regimen or '' }} + +

INCOTERM:

+

{{ factura.incoterm or '' }}

+
+ {% if factura.precinto %} +

Precinto: {{ factura.precinto }}

+ {% endif %} +
+

Aduana: {{ factura.aduana }}

+
+ {% if factura.destino %} +

Destino: {{ factura.destino }}

+ {% endif %} +
+
+
+ +
+
+

{{ cliente_vendido.header }}

+

{{ cliente_vendido.nombre }}

+

{{ 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 %} +

+

{{ cliente_vendido.colonia }} {% if cliente_vendido.codigo_postal %} CP: {{ cliente_vendido.codigo_postal }}{% endif %}

+

{{ cliente_vendido.ciudad }}, {{ cliente_vendido.estado }}, {{ cliente_vendido.pais }}

+

RFC: {{ cliente_vendido.tax_id }} + {% if cliente_vendido.programa and cliente_vendido.programa != 'Ninguno' %} + {{ cliente_vendido.programa }}: {{ cliente_vendido.autorizacion }} + {% endif %} +

+
+ +
+

{{ cliente_enviado.header }}

+

{{ cliente_enviado.nombre }}

+

{{ 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 %} +

+

{{ cliente_enviado.colonia }} {% if cliente_enviado.codigo_postal %} CP: {{ cliente_enviado.codigo_postal }}{% endif %}

+

{{ cliente_enviado.ciudad }}, {{ cliente_enviado.estado }}, {{ cliente_enviado.pais }}

+

RFC: {{ cliente_enviado.tax_id }}

+
+
+


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% for partida in partidas %} + + + + + + + + + + + + {% endfor %} + + + + + + + + + + + + + + + + + + + + + +

Transportista:

{{ factura.transportista }}

SCAC: {{ factura.scac }}

INCOTERM:

{{ factura.incoterm }}

+

Aduana: {{ factura.aduana }}

+


+

Transporte:

+

{{ factura.transporte }}: {{ factura.num_transporte }}

CAAT: {{ factura.caat }}

+

Placas: {{ factura.placas or 'N/A' }}

+



+

Línea

+
+

Número de Parte

+

Descripción

+
+

Comercial

+
+

Empaque

+
+

Peso (KGS)

+
+

Valores

+
+

Cantidad

+
+

U.M.

+
+

Tipo

+
+

Neto

+
+

Bruto

+
+

Unitario

+
+

Total

+
+

{{ loop.index }}

+
+

{{ partida.numero_parte }}

+

{{ partida.descripcion }}

+

Frac: {{ partida.fraccion }} / Orig: {{ partida.origen or 'MEX' }}

+
+

{{ partida.cantidad_importacion }}

+
+

{{ partida.unidad_medida }}

+
+

+ {% if partida.cantidad_bultos != 0 %}{{ partida.cantidad_bultos }}{% endif %} + {{ partida.clave_bultos }} +

+
+

{{ partida.peso_neto }}

+
+

{{ partida.peso_bruto }}

+
+

${{ partida.valor_costo_unitario }}

+
+

${{ partida.valor_total }}

+
+

+ Observaciones: + TOTALES +

+
+

{{ totales.cantidad_total }}

+
+

+ {% if totales.bultos_total != 0 %}{{ totales.bultos_total }}{% endif %} + {{ totales.clave_bultos or '' }} +

+
+

{{ totales.peso_neto_total }}

+
+

{{ totales.peso_bruto_total }}

+
+

${{ totales.valor_total_total }}

+
+

{{ factura.observaciones }}

+
+

+

{{ cliente_proveedor.nombre }}

+


+

Los valores expresados en esta factura son en: {{ factura.moneda }}

+
+


+

Normal Por Parte

+

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.

+
+ + \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_tem_hor.html b/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_tem_hor.html new file mode 100644 index 00000000..e69de29b diff --git a/backend/requirements.txt b/backend/requirements.txt index 1e600ac7..d33317be 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/frontend/src/routes/dashboard/general_catalogs/ports/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/ports/+page.svelte index e14df2cc..c870f080 100644 --- a/frontend/src/routes/dashboard/general_catalogs/ports/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/ports/+page.svelte @@ -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'; diff --git a/frontend/src/routes/dashboard/invoices/+page.svelte b/frontend/src/routes/dashboard/invoices/+page.svelte index 93f23c50..6fd7fc9d 100644 --- a/frontend/src/routes/dashboard/invoices/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/+page.svelte @@ -1,496 +1,543 @@
- -
-
-

Facturas

-

- Gestiona las facturas del sistema -

-
- -
+
+
+

Facturas

+

+ Gestiona las facturas del sistema +

+
+ +
- - - - Filtros - Filtra las facturas por diferentes criterios (los filtros se aplican automáticamente) - - -
-
- - -
+ + + Filtros + Filtra las facturas por diferentes criterios (los filtros se aplican automáticamente) + + +
+
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- - -
-
-
-
+
+ + +
+
+
+
- - {#if error} - - - Error - {error} - - - {/if} + {#if error} + + + Error + {error} + + + {/if} - - - -
-
- Listado de Facturas - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - -
-
+ + +
+
+ Listado de Facturas + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + +
+ \ No newline at end of file