WIP: Guardando trabajo antes de actualizar con development

This commit is contained in:
2026-01-09 08:26:44 -06:00
parent c95c7f7c55
commit ff2df90dbe
11 changed files with 1315 additions and 449 deletions

View File

@@ -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 .

View File

@@ -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

View File

@@ -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"

View File

@@ -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)}")

View File

@@ -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>

View File

@@ -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