From 210f2d85d87e955b4b9d47c93712b53a18e58501 Mon Sep 17 00:00:00 2001
From: Kevin_Ramirez
Date: Fri, 16 Jan 2026 13:18:34 -0600
Subject: [PATCH] fix(reports): update invoice headers, pedimento formatting
and value priority
---
.../importacion/facturas/mex/schemas.py | 7 +
.../importacion/facturas/mex/service.py | 129 +++++++++++++++---
.../facturas/templates/factura_mex_ver.html | 28 +++-
3 files changed, 137 insertions(+), 27 deletions(-)
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
index a74709dc..9d4455b8 100644
--- a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py
+++ b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py
@@ -18,6 +18,9 @@ class ClienteSchema(BaseModel):
tax_id: str
programa: Optional[str] = ""
autorizacion: Optional[str] = ""
+ prosec: Optional[str] = ""
+ reg_emp: Optional[str] = ""
+ cert: Optional[str] = ""
# Si llega un None, lo convertimos en "" automáticamente
@field_validator('direccion', 'nombre', mode='before')
@@ -46,6 +49,10 @@ class FacturaSchema(BaseModel):
transporte: str = ""
num_transporte: str = ""
placas: str = ""
+ placas_remolque: str = ""
+ licencia_conductor: str = ""
+ caat: str = ""
+ scac: str = ""
aduana: str = ""
destino: str = ""
observaciones: str = ""
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
index bd0cd750..f8f37a6a 100644
--- a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py
+++ b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py
@@ -23,6 +23,12 @@ 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
+# --- TRANSPORTATION MODELS ---
+from api.v1.modules.a76.transportation.transporters.models import Transporter
+from api.v1.modules.a76.transportation.vehicles.models import Vehicle
+from api.v1.modules.a76.transportation.trailers.models import Trailer
+from api.v1.modules.a76.transportation.drivers.models import Driver
+
# --- MODELO DE FRACCIONES ---
from api.v1.modules.a76.general_catalogs.tariff_fractions.models import TariffFraction
@@ -79,7 +85,12 @@ class FacturaImportacionMexService:
pais=(addr.country or "MEX") if addr else "MEX",
tax_id=prog.tax_id if (prog and prog.tax_id) else (getattr(main, 'rfc', "") or ""),
programa="IMMEX" if (prog and prog.program) else "",
- autorizacion=prog.program_number if prog else ""
+ autorizacion=prog.program_number if prog else "",
+ prosec=prog.prosec_authorization if (prog and prog.prosec and prog.prosec_authorization) else "",
+ reg_emp=prog.val_certified_company_registry if (prog and hasattr(prog, 'val_certified_company_registry')) else (
+ prog.certified_company_registry if (prog and prog.certified_company_registry) else ""
+ ),
+ cert=prog.is_certified_company if (prog and prog.is_certified_company) else ""
)
def obtener_datos(self, db: Session, invoice_id: int, company_id: int, progress_callback: Optional[Callable] = None) -> FacturaImportacionCompleta:
@@ -90,13 +101,14 @@ class FacturaImportacionMexService:
compliance = header.compliance_mx
logistics = header.logistics if header.logistics else None
+ financials = header.financials if header.financials else None
if progress_callback: progress_callback(20, "Obteniendo datos de pedimento...")
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
if progress_callback: progress_callback(30, "Obteniendo cliente y proveedor...")
proveedor_id = compliance.provider_id if compliance else None
- 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="")
+ 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="")
nombre_agente = ""
if compliance and compliance.customs_broker_id:
@@ -106,7 +118,7 @@ class FacturaImportacionMexService:
company = db.query(Company).filter(Company.id == header.company_id).first()
# Datos Default (Company/Importer) - Used for fallback or Right Side (Enviado A)
cliente_default = ClienteSchema(
- header="Importador / Consignatario",
+ header="Importador / consignatario:",
nombre=getattr(company, 'name', "Empresa Local"),
direccion="DOMICILIO FISCAL",
num_exterior="", colonia="", codigo_postal="", ciudad="", estado="", pais="MEX",
@@ -117,16 +129,19 @@ class FacturaImportacionMexService:
# Left Side Logic (Consignatario / Sold To)
cliente_vendido = cliente_default
if compliance and compliance.sold_to_id:
- # Clean header: "vendido_a" -> "VENDIDO A"
raw_header = compliance.sold_to_header or "CONSIGNATARIO"
- clean_header = raw_header.replace("_", " ").upper()
-
- # Fetch client data
+ clean_header = raw_header.replace("_", " ").capitalize() + ":"
cliente_vendido = self._obtener_datos_cliente(db, compliance.sold_to_id, clean_header)
- # Right Side Logic (Enviado A) - Currently keeping default/company info as requested "solo la izquierda"
- # If standard logic dictates this should be Shipped To, we could add that later.
+ # Right Side Logic (Enviado A / Shipped To)
cliente_enviado = cliente_default
+ if compliance and compliance.shipped_to_id:
+ # Clean header: "enviado_a" -> "Enviado a:"
+ raw_header_shipped = compliance.shipped_to_header or "DESTINATARIO"
+ clean_header_shipped = raw_header_shipped.replace("_", " ").capitalize() + ":"
+
+ # Fetch client data
+ cliente_enviado = self._obtener_datos_cliente(db, compliance.shipped_to_id, clean_header_shipped)
remesa_valor = str(compliance.remesa) if (compliance and compliance.remesa) else ""
acuse_valor = str(compliance.edocument) if (compliance and compliance.edocument) else "N/A"
@@ -137,23 +152,75 @@ class FacturaImportacionMexService:
elif 'broker' in locals() and broker and broker.license:
patente_val = broker.license
+
+ # --- Transport Data Fetching ---
+ transporte_txt = str(logistics.transport_type) if (logistics and logistics.transport_type) else ""
+ num_transporte_val = (logistics.trailer_num or "") if logistics else ""
+
+ # Init values
+ placas_val = (logistics.license_plate or "") if logistics else "" # Placas Tracto
+ placas_remolque_val = ""
+ transportista_val = (logistics.carrier_id or "") if logistics else ""
+ caat_val = ""
+ scac_val = ""
+ licencia_cond_val = ""
+
+ if logistics:
+ # 1. Transporter (CAAT / SCAC)
+ if logistics.carrier_id:
+ transporter_obj = db.query(Transporter).filter(Transporter.transporter_key == logistics.carrier_id).first()
+ if transporter_obj:
+ caat_val = transporter_obj.caat_code or ""
+ scac_val = transporter_obj.transport_code or "" # Mapping transport_code to SCAC
+ transportista_val = transporter_obj.name or logistics.carrier_id
+
+ # 2. Vehicle (Placas Tracto) - Try transport_id first
+ if logistics.transport_id:
+ veh_obj = db.query(Vehicle).filter(Vehicle.vehicle_key == logistics.transport_id).first()
+ if veh_obj:
+ placas_val = veh_obj.plate_number or placas_val
+ elif logistics.vehicle_num: # Fallback to vehicle_num if populated and transport_id failed/empty
+ veh_obj = db.query(Vehicle).filter(Vehicle.vehicle_key == logistics.vehicle_num).first()
+ if veh_obj:
+ placas_val = veh_obj.plate_number or placas_val
+
+ # 3. Trailer (Placas Remolque)
+ if logistics.trailer_num:
+ trl_obj = db.query(Trailer).filter(Trailer.trailer_number == logistics.trailer_num).first()
+ if trl_obj:
+ placas_remolque_val = trl_obj.plate_number or ""
+
+ # 4. Driver (License)
+ if logistics.carrier_id and logistics.driver_name:
+ # Attempt to find driver by name + carrier
+ drv_obj = db.query(Driver).filter(
+ Driver.transporter_key == logistics.carrier_id,
+ Driver.driver_name == logistics.driver_name
+ ).first()
+ if drv_obj:
+ licencia_cond_val = drv_obj.license_number or ""
+
factura_schema = FacturaSchema(
numero=header.invoice_number or "S/N",
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,
+ tipo_cambio=float(financials.exchange_rate) if (financials and financials.exchange_rate) else (float(pedimento.exchange_rate) if pedimento and pedimento.exchange_rate else 1.0),
moneda=getattr(header, 'currency', "USD") or "USD",
incoterm=(logistics.incoterm or "") if logistics else "",
observaciones=header.observation_es or header.observation_en or "",
- pedimento=pedimento.pedimento_number if pedimento else "",
+ pedimento=f"{pedimento.year} {pedimento.customs_office[:2] if pedimento.customs_office else ''} {pedimento.license} {pedimento.pedimento_number}" if pedimento else "",
clave_pedimento=pedimento.pedimento_code if pedimento else "",
- regimen=pedimento.regime if pedimento else "",
+ regimen=header.document_type or "",
patente=patente_val,
agente_aduanal=nombre_agente,
- transporte=str(logistics.transport_type) if (logistics and logistics.transport_type) else "",
- num_transporte=(logistics.trailer_num or "") if logistics else "",
- placas=(logistics.license_plate or "") if logistics else "",
- transportista=(logistics.carrier_id or "") if logistics else "",
- aduana=pedimento.customs_office if pedimento else "",
+ transporte=transporte_txt,
+ num_transporte=num_transporte_val,
+ placas=placas_val,
+ placas_remolque=placas_remolque_val,
+ transportista=transportista_val,
+ caat=caat_val,
+ scac=scac_val,
+ licencia_conductor=licencia_cond_val,
+ aduana=compliance.aduana if (compliance and compliance.aduana) else (pedimento.customs_office[:2] if (pedimento and pedimento.customs_office) else ""),
precinto=(logistics.seal_number or "") if logistics else "",
destino=(logistics.destination_goods or "") if logistics else "",
remesa=remesa_valor, acuse_electronico=acuse_valor
@@ -197,13 +264,33 @@ class FacturaImportacionMexService:
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
+ advalorem_txt = "0%"
fraccion_imprimir = fraccion_db.fraction or fraccion_raw
else:
fraccion_imprimir = self._format_fraccion_fallback(fraccion_limpia)
+ # Logic to determine values - Prioritize Specific Currency Columns
+ v_unitario = 0.0
+ v_total = 0.0
+
+ if fin:
+ is_mxn = (factura_schema.moneda == 'MXN')
+
+ # 1. Try Specific Currency Columns First
+ if is_mxn:
+ v_unitario = fin.unit_cost_commercial_mxn or 0.0
+ v_total = fin.value_commercial_mxn or 0.0
+ else:
+ v_unitario = fin.unit_cost_commercial_usd or 0.0
+ v_total = fin.value_commercial_usd or 0.0
+
+ # 2. Fallback to Generic if Specific is 0
+ if v_unitario == 0 or v_total == 0:
+ v_unitario = fin.commercial_unit_cost or 0.0
+ v_total = fin.total_commercial_value or 0.0
+
partidas_list.append(PartidaSchema(
numero_parte=num_parte_final,
descripcion=desc_final,
@@ -217,8 +304,8 @@ class FacturaImportacionMexService:
clave_bultos=qty.package_key if qty else "",
peso_neto=self.formatear_numero(qty.net_weight if qty else 0),
peso_bruto=self.formatear_numero(qty.gross_weight if qty else 0),
- valor_costo_unitario=self.formatear_numero(fin.commercial_unit_cost if fin else 0),
- valor_total=self.formatear_numero(fin.total_commercial_value if fin else 0)
+ valor_costo_unitario=self.formatear_numero(v_unitario),
+ valor_total=self.formatear_numero(v_total)
))
totales = self.calcular_totales(partidas_list, Decimal(factura_schema.tipo_cambio))
@@ -259,7 +346,7 @@ class FacturaImportacionMexService:
logo_b64 = None
try:
# Fetch company to get logo path
- # We use the passed company_id which corresponds to the active company
+
comp_logo = db.query(Company).filter(Company.id == company_id).first()
if comp_logo and comp_logo.logo:
p = Path(comp_logo.logo)
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
index d73740b6..9a7479e5 100644
--- 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
@@ -418,6 +418,11 @@
{{ cliente_vendido.programa }}: {{ cliente_vendido.autorizacion }}
{% endif %}
+
+ {% if cliente_vendido.prosec %}PROSEC: {{ cliente_vendido.prosec }} {% endif %}
+ {% if cliente_vendido.reg_emp %}REG EMP: {{ cliente_vendido.reg_emp }} {% endif %}
+ {% if cliente_vendido.cert %}CERT: {{ cliente_vendido.cert }}{% endif %}
+
@@ -431,7 +436,16 @@
cliente_enviado.codigo_postal }}{% endif %}
{{ cliente_enviado.ciudad }}, {{ cliente_enviado.estado }}, {{ cliente_enviado.pais }}
-
RFC: {{ cliente_enviado.tax_id }}
+
RFC: {{ cliente_enviado.tax_id }}
+ {% if cliente_enviado.programa and cliente_enviado.programa != 'Ninguno' %}
+ {{ cliente_enviado.programa }}: {{ cliente_enviado.autorizacion }}
+ {% endif %}
+
+
+ {% if cliente_enviado.prosec %}PROSEC: {{ cliente_enviado.prosec }} {% endif %}
+ {% if cliente_enviado.reg_emp %}REG EMP: {{ cliente_enviado.reg_emp }} {% endif %}
+ {% if cliente_enviado.cert %}CERT: {{ cliente_enviado.cert }}{% endif %}
+
@@ -456,7 +470,8 @@
{{ factura.incoterm }}
- Aduana: {{ factura.aduana }}
+ Aduana: {{ factura.aduana }} / Ped: {{ factura.pedimento }}
|
@@ -473,15 +488,16 @@
CAAT: {{ factura.caat }}
|
- Placas: {{ factura.placas or 'N/A' }}
+ Placas: {{ factura.placas or '' }} / Rem: {{ factura.placas_remolque or
+ '' }}
|
- |
-
+ |
+ Chofer/Licencia:
|
-
+ {{ factura.licencia_conductor or 'N/A' }}
|