Se edito el reporte de factura, se le puso las partidas, ademas se inicio todo el procedo de celery

This commit is contained in:
2026-01-12 16:29:07 -06:00
parent 50ef340df8
commit ce37aa77b2
7 changed files with 194 additions and 93 deletions

View File

@@ -55,6 +55,10 @@ class PartidaSchema(BaseModel):
descripcion: str
fraccion: str
origen: str
advalorem:Optional[str] = ""
preferencia:Optional[str] = ""
cantidad_importacion: Union[float, str]
unidad_medida: str
cantidad_bultos: int

View File

@@ -14,14 +14,16 @@ from api.v1.modules.a76.items.line_financials.models import LineFinancial
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
from api.v1.modules.a76.items.line_items.models import LineItem
from api.v1.modules.a76.clients_and_providers.models import (
ClientProvider,
ClientProviderAddress,
ClientProviderPrograms
ClientProvider, ClientProviderAddress, ClientProviderPrograms
)
from api.v1.modules.a76.parts.models import Part
from api.v1.modules.a76.pedmientos.models import Pedimentos
from api.v1.modules.a76.general_catalogs.company.models import Company
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
from api.v1.modules.a76.items.models import Item
# --- MODELO DE FRACCIONES ---
from api.v1.modules.a76.general_catalogs.tariff_fractions.models import TariffFraction
# --- SCHEMAS ---
from .schemas import (
@@ -50,14 +52,16 @@ class FacturaImportacionMexService:
return round(float(valor), decimales)
except: return 0.0
def _format_fraccion_fallback(self, fraccion_raw: str) -> str:
if not fraccion_raw or len(fraccion_raw) < 8:
return fraccion_raw
return f"{fraccion_raw[:4]}.{fraccion_raw[4:6]}.{fraccion_raw[6:]}"
def _obtener_datos_cliente(self, db: Session, client_id: int, rol: str) -> ClienteSchema:
main = db.query(ClientProvider).filter(ClientProvider.id == client_id).first()
if not main:
return ClienteSchema(
header=rol, nombre="Desconocido", direccion="", tax_id="",
codigo_postal="", ciudad="", estado="", pais="MEX"
)
return ClienteSchema(header=rol, nombre="Desconocido", direccion="", tax_id="", codigo_postal="", ciudad="", estado="", pais="MEX")
addr = db.query(ClientProviderAddress).filter(ClientProviderAddress.client_id == client_id).first()
prog = db.query(ClientProviderPrograms).filter(ClientProviderPrograms.client_id == client_id).first()
@@ -79,61 +83,32 @@ class FacturaImportacionMexService:
def obtener_datos(self, db: Session, invoice_id: int, company_id: int) -> FacturaImportacionCompleta:
try:
# 1. Cabecera
header = db.query(InvoiceHeader).filter(
InvoiceHeader.id == invoice_id,
InvoiceHeader.company_id == company_id
).first()
if not header:
raise HTTPException(status_code=404, detail="Factura no encontrada")
header = db.query(InvoiceHeader).filter(InvoiceHeader.id == invoice_id, InvoiceHeader.company_id == company_id).first()
if not header: raise HTTPException(status_code=404, detail="Factura no encontrada")
# 2. Relaciones Críticas (Compliance y Logistics)
# Nota: Usamos la relación ORM 'compliance_mx' que definiste en el modelo
compliance = header.compliance_mx
# Si logistics es una lista, tomamos el primero, si no, None
logistics = header.logistics[0] if header.logistics else None
# Pedimento: Prioridad al de Compliance, si no, al de Header
pedimento_id = compliance.pedimento_id if (compliance and compliance.pedimento_id) else header.related_doc_id
pedimento = db.query(Pedimentos).filter(Pedimentos.id == pedimento_id).first() if pedimento_id else None
# 3. Mapeo Actores (¡AQUÍ ESTABA EL DETALLE!)
# A) Proveedor: Sacado de compliance.provider_id
proveedor_id = compliance.provider_id if compliance else None
if proveedor_id:
cliente_proveedor = self._obtener_datos_cliente(db, proveedor_id, "Proveedor / Supplier")
else:
cliente_proveedor = ClienteSchema(header="Proveedor", nombre="No Asignado", direccion="", tax_id="",
codigo_postal="", ciudad="", estado="", pais="")
cliente_proveedor = self._obtener_datos_cliente(db, proveedor_id, "Proveedor / Supplier") if proveedor_id else ClienteSchema(header="Proveedor", nombre="No Asignado", direccion="", tax_id="", codigo_postal="", ciudad="", estado="", pais="")
# B) Agente Aduanal: Sacado de compliance.customs_broker_id
nombre_agente = ""
if compliance and compliance.customs_broker_id:
broker = db.query(CustomsBroker).filter(CustomsBroker.id == compliance.customs_broker_id).first()
if broker:
nombre_agente = broker.name # Asumiendo que CustomsBroker tiene 'name'
if broker: nombre_agente = broker.name
# C) Importador (Company)
company = db.query(Company).filter(Company.id == header.company_id).first()
cliente_vendido = ClienteSchema(
header="Importador / Consignatario",
nombre=getattr(company, 'name', "Empresa Local"),
direccion="DOMICILIO FISCAL",
num_exterior="",
colonia="",
codigo_postal="",
ciudad="",
estado="",
pais="MEX",
num_exterior="", colonia="", codigo_postal="", ciudad="", estado="", pais="MEX",
tax_id=getattr(company, 'rfc', ""),
programa=getattr(company, 'program', "IMMEX"),
autorizacion=getattr(company, 'program_number', "")
programa=getattr(company, 'program', "IMMEX"), autorizacion=getattr(company, 'program_number', "")
)
# 4. Mapeo Factura
# Nota: Muchos datos vienen de 'compliance', no de 'header'
remesa_valor = str(compliance.remesa) if (compliance and compliance.remesa) else ""
acuse_valor = str(compliance.edocument) if (compliance and compliance.edocument) else "N/A"
@@ -144,30 +119,22 @@ class FacturaImportacionMexService:
moneda=getattr(header, 'currency', "USD") or "USD",
incoterm=logistics.incoterm if logistics else "",
observaciones=header.observation_es or header.observation_en or "",
# Pedimento y Agente
pedimento=pedimento.pedimento_number if pedimento else "",
clave_pedimento=pedimento.pedimento_code if pedimento else "",
regimen=pedimento.regime if pedimento else "",
patente=pedimento.license if pedimento else "",
agente_aduanal=nombre_agente, # Agregamos el nombre real
# Transporte
agente_aduanal=nombre_agente,
transporte=str(logistics.transport_type.value) if (logistics and logistics.transport_type) else "",
num_transporte=logistics.trailer_num if logistics else "",
placas=logistics.license_plate if logistics else "",
transportista=logistics.carrier_id if logistics else "", # Si carrier_id es ID, aquí habría que buscar nombre
# Otros
transportista=logistics.carrier_id if logistics else "",
aduana=pedimento.customs_office if pedimento else "",
precinto=logistics.seal_number if logistics else "",
destino=logistics.destination_goods if logistics else "",
remesa=remesa_valor,
acuse_electronico=acuse_valor
remesa=remesa_valor, acuse_electronico=acuse_valor
)
# 5. Mapeo Partidas
lines = db.query(LineItem).filter(LineItem.item_id == header.id).all()
lines = db.query(LineItem).join(Item, LineItem.item_id == Item.id).filter(Item.invoice_id == header.id).all()
partidas_list = []
for line in lines:
@@ -175,13 +142,54 @@ class FacturaImportacionMexService:
fin = db.query(LineFinancial).filter(LineFinancial.item_line_id == line.id).first()
part_master = db.query(Part).filter(Part.id == line.part_number).first()
desc_final = "S/D"
num_parte_final = str(line.part_number or "S/N")
fraccion_raw = ""
origen_final = "MEX"
if part_master:
desc_final = part_master.description_spanish or part_master.description_english or "Sin Desc."
num_parte_final = part_master.part_number
fraccion_raw = part_master.fraction if part_master.fraction else ""
# --- LÓGICA DE FRACCIÓN DESDE BASE DE DATOS ---
# Limpiar la fracción de la BD (quitar puntos y asegurar 8 dígitos)
fraccion_limpia = fraccion_raw.replace(".", "").strip()
if fraccion_limpia:
fraccion_limpia = fraccion_limpia[:8].zfill(8)
# Consultar tabla tariff_fractions
fraccion_db = db.query(TariffFraction).filter(TariffFraction.code == fraccion_limpia).first()
# Reglas de negocio solicitadas:
# 1. Preferencia default: "General"
# 2. AdValorem default: "0%" si no existe o es vacío
preferencia_txt = "General"
advalorem_txt = "0%"
fraccion_imprimir = fraccion_raw
if fraccion_db:
# Si el valor en BD es None, "0", o vacío, dejarlo como "0%" o "EXENTO"
adv_db = fraccion_db.adv_impo
if adv_db and adv_db.strip() not in ["0", "0.0", "0.00", ""]:
advalorem_txt = adv_db if "%" in adv_db else f"{adv_db}%"
else:
advalorem_txt = "0%" # O podrías poner "EXENTO" si prefieres
fraccion_imprimir = fraccion_db.fraction or fraccion_raw
else:
# Si no existe en la tabla, aplicamos el fallback visual de puntos
fraccion_imprimir = self._format_fraccion_fallback(fraccion_limpia)
partidas_list.append(PartidaSchema(
numero_parte=part_master.part_number if part_master else str(line.part_number or "S/N"),
descripcion=part_master.description_spanish if part_master else "S/D",
fraccion=part_master.fraction if part_master else "",
origen="MEX",
numero_parte=num_parte_final,
descripcion=desc_final,
fraccion=fraccion_imprimir,
origen=origen_final,
advalorem=advalorem_txt,
preferencia=preferencia_txt,
cantidad_importacion=self.formatear_numero(qty.quantity if qty else 0),
unidad_medida=qty.weight_unit if qty else "KG",
unidad_medida=qty.weight_unit if qty else "PZA",
cantidad_bultos=int(qty.package_quantity) if qty and qty.package_quantity else 0,
clave_bultos=qty.package_key if qty else "",
peso_neto=self.formatear_numero(qty.net_weight if qty else 0),
@@ -190,21 +198,17 @@ class FacturaImportacionMexService:
valor_total=self.formatear_numero(fin.total_commercial_value if fin else 0)
))
# 6. 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_vendido,
factura=factura_schema,
partidas=partidas_list,
totales=totales
cliente_proveedor=cliente_proveedor, cliente_vendido=cliente_vendido,
cliente_enviado=cliente_vendido, 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)}")
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
def calcular_totales(self, partidas: List[PartidaSchema], tipo_cambio: Decimal) -> TotalesSchema:
cant = sum(p.cantidad_importacion for p in partidas)
@@ -214,40 +218,24 @@ class FacturaImportacionMexService:
bultos = sum(p.cantidad_bultos for p in partidas)
claves = [p.clave_bultos for p in partidas if p.clave_bultos]
clave_comun = max(set(claves), key=claves.count) if claves else ""
if bultos > 1 and clave_comun and not clave_comun.endswith("S"): clave_comun += "S"
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)
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)
)
def generar_factura_completa(self, db: Session, invoice_id: int, company_id: int, formato: str = "pdf") -> Tuple[bytes, str, str]:
datos = self.obtener_datos(db, invoice_id, company_id)
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()
'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()
}
html_content = self.template.render(**context)
nombre = f"Factura_{datos.factura.numero}.{formato}"
if formato == "html":
return html_content.encode('utf-8'), nombre, "text/html"
options = {
'page-size': 'Letter', 'margin-top': '0.5in', 'margin-right': '0.5in',
'margin-bottom': '0.5in', 'margin-left': '0.5in', 'encoding': "UTF-8",
'enable-local-file-access': None
}
if formato == "html": return html_content.encode('utf-8'), nombre, "text/html"
options = {'page-size': 'Letter', 'margin-top': '0.5in', 'margin-right': '0.5in', 'margin-bottom': '0.5in', 'margin-left': '0.5in', 'encoding': "UTF-8", 'enable-local-file-access': None}
pdf = pdfkit.from_string(html_content, False, options=options, configuration=self._get_wkhtmltopdf_config())
return pdf, nombre, "application/pdf"

View File

@@ -0,0 +1,45 @@
import base64
import logging
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from .mex.service import FacturaImportacionMexService
logger = logging.getLogger(__name__)
@celery_app.task(name="generar_pdf_factura_async")
def generar_pdf_factura_async(invoice_id: int, company_id: int):
"""
Esta tarea la ejecuta el Worker. No usa FastAPI, usa directamente SQLAlchemy.
"""
# 1. Abrimos conexión a la DB
db = CoreSessionLocal()
try:
logger.info(f"Worker procesando factura {invoice_id}...")
# 2. Instanciamos el servicio de reportes
service = FacturaImportacionMexService()
# 3. Generamos los bytes del PDF
pdf_bytes, nombre, media_type = service.generar_factura_completa(
db=db,
invoice_id=invoice_id,
company_id=company_id
)
# 4. Codificamos a base64 para que viaje seguro por Valkey
pdf_base64 = base64.b64encode(pdf_bytes).decode('utf-8')
return {
"status": "success",
"file_name": nombre,
"content": pdf_base64,
"media_type": media_type
}
except Exception as e:
logger.error(f"Error en Celery Worker: {str(e)}")
return {"status": "error", "message": str(e)}
finally:
# 5. MUY IMPORTANTE: Cerramos la conexión para no saturar Postgres
db.close()

View File

@@ -285,6 +285,10 @@
<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>
<p class="mini">
{% if partida.advalorem %}ADV: {{ partida.advalorem }}{% endif %}
{% if partida.preferencia %} / PREF: {{ partida.preferencia }}{% endif %}
</p>
</td>
<td class="border" colspan="2">
<p class="mini p-t-2 center">{{ partida.cantidad_importacion }}</p>