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
|
||||
|
||||
Reference in New Issue
Block a user