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 %} +
{{ 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 %} +
{{ factura.incoterm }}
Aduana: {{ factura.aduana }}
+Aduana: {{ factura.aduana }} / Ped: {{ factura.pedimento }}
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' }}