fix(reports): update invoice headers, pedimento formatting and value priority

This commit is contained in:
2026-01-16 13:18:34 -06:00
parent 238d728305
commit 210f2d85d8
3 changed files with 137 additions and 27 deletions

View File

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

View File

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

View File

@@ -418,6 +418,11 @@
{{ cliente_vendido.programa }}: {{ cliente_vendido.autorizacion }}
{% endif %}
</p>
<p class="p-l-5">
{% 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 %}
</p>
</div>
<div class="width-48-right" style="text-align: left;">
@@ -431,7 +436,16 @@
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>
<p class="p-l-5">RFC: {{ cliente_enviado.tax_id }}
{% if cliente_enviado.programa and cliente_enviado.programa != 'Ninguno' %}
{{ cliente_enviado.programa }}: {{ cliente_enviado.autorizacion }}
{% endif %}
</p>
<p class="p-l-5">
{% 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 %}
</p>
</div>
</div>
<p class="p-t-8"><br /></p>
@@ -456,7 +470,8 @@
<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>
<p class="tiny line-9">Aduana: <span class="mini">{{ factura.aduana }}</span> / Ped: <span
class="mini">{{ factura.pedimento }}</span></p>
</td>
<td bgcolor="#E4E4E4" class="border" style="width:20pt">
<p><br /></p>
@@ -473,15 +488,16 @@
<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>
<p class="tiny p-t-1 line-8">Placas: {{ factura.placas or '' }} / Rem: {{ factura.placas_remolque or
'' }}</p>
</td>
</tr>
<tr class="h-10">
<td class="border" colspan="2" style="width:80pt">
<p><br /></p>
<td bgcolor="#E4E4E4" class="border" colspan="2" style="width:80pt">
<p class="tiny p-l-1 line-9">Chofer/Licencia:</p>
</td>
<td class="border" colspan="8">
<p><br /></p>
<p class="tiny p-l-3">{{ factura.licencia_conductor or 'N/A' }}</p>
</td>
</tr>