Creacion de el codigo de barras, y se ajusto la seccion de importador y exportadores

This commit is contained in:
2026-01-30 12:32:26 -06:00
parent d4674ece23
commit 9ac8d4a4a4
3 changed files with 404 additions and 48 deletions

View File

@@ -8,12 +8,16 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
from fastapi import HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from io import BytesIO
import pdf417gen
# --- MODELOS ---
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceComplianceMx, InvoiceFinancials, InvoiceLogistics
from api.v1.modules.a76.general_catalogs.company.models import Company
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
from api.v1.modules.a76.pedmientos.models.pedimento_validation import PedimentoValidation
from api.v1.modules.a76.customs_brokers.models import CustomsBroker, CustomsBrokerPersonnel
from api.v1.modules.a76.items.models import Item
from api.v1.modules.a76.clients_and_providers.models import ClientProvider, ClientProviderAddress, ClientProviderPrograms
# --- SCHEMAS FOR TEMPLATE CONTEXT ---
@@ -71,6 +75,7 @@ class AvisoConsolidadoExportacionService:
def obtener_datos(self, db: Session, invoice_id: int, company_id: int, progress_callback: Optional[Callable] = None) -> AvisoConsolidadoContext:
try:
with open("/tmp/barcode_debug.log", "a") as f: f.write(f"ENTER obtener_datos ID={invoice_id}\n")
if progress_callback: progress_callback(10, "Buscando factura...")
# Fetch minimal real data if possible, or use placeholders as requested
@@ -104,47 +109,35 @@ class AvisoConsolidadoExportacionService:
pedimento_txt = f"{year} {office} {lic} {num}"
clave_ped = pedimento.pedimento_code or ""
# 2. Company Address
direccion_empresa = "DOMICILIO NO REGISTRADO"
if company and company.addresses:
# Try to find fiscal address or first available
addr = company.addresses[0] # Default
# TODO: Check if there's a specific flag for fiscal address in submodel
parts = []
if addr.street: parts.append(addr.street)
if addr.exterior_number: parts.append(f"No. {addr.exterior_number}")
if addr.interior_number: parts.append(f"Int. {addr.interior_number}")
if addr.neighborhood: parts.append(f"Col. {addr.neighborhood}")
if addr.postal_code: parts.append(f"CP {addr.postal_code}")
if addr.city: parts.append(addr.city)
if addr.state: parts.append(addr.state)
if addr.country: parts.append(addr.country)
if parts:
direccion_empresa = ", ".join(parts).upper()
# Determine Mexican Entity based on Operation Type
# IMP -> Client (Sold To/Consignee)
# EXP -> Company (Tenant)
# 2. Importer/Exporter Data (Clarion 100% Match)
# Logic:
# IF EqiFex:EsCambioRegimen = 'S' THEN
# CliPro:Cliente = EqiFex:VendidoA
# ELSE
# CliPro:Cliente = EqiFex:Proveedor
# END
target_entity_data = {
"rfc": getattr(company, 'rfc', "") or "",
"razon_social": getattr(company, 'name', "") or "",
"direccion_completa": direccion_empresa
"rfc": "",
"razon_social": "",
"direccion_completa": "DOMICILIO NO REGISTRADO"
}
op_type = header.operation_type.upper() if header.operation_type else "EXP"
target_client_id = None
if op_type == "IMP" and compliance and compliance.sold_to_id:
# Fetch Client Data
client_id = compliance.sold_to_id
client_obj = db.query(ClientProvider).filter(ClientProvider.id == client_id).first()
if compliance:
if compliance.is_regime_change:
target_client_id = compliance.sold_to_id
else:
target_client_id = compliance.provider_id
if target_client_id:
client_obj = db.query(ClientProvider).filter(ClientProvider.id == target_client_id).first()
if client_obj:
# Fetch Address
c_addr = db.query(ClientProviderAddress).filter(ClientProviderAddress.client_id == client_id).first()
c_addr = db.query(ClientProviderAddress).filter(ClientProviderAddress.client_id == target_client_id).first()
# Fetch Fiscal Data (RFC)
c_prog = db.query(ClientProviderPrograms).filter(ClientProviderPrograms.client_id == client_id).first()
c_prog = db.query(ClientProviderPrograms).filter(ClientProviderPrograms.client_id == target_client_id).first()
c_rfc = ""
if c_prog and c_prog.tax_id: c_rfc = c_prog.tax_id
@@ -155,10 +148,13 @@ class AvisoConsolidadoExportacionService:
parts_c = []
if c_addr.streets: parts_c.append(c_addr.streets)
if c_addr.exterior_number: parts_c.append(f"No. {c_addr.exterior_number}")
if c_addr.interior_number: parts_c.append(f"Int. {c_addr.interior_number}")
if c_addr.neighborhood: parts_c.append(f"Col. {c_addr.neighborhood}")
if c_addr.postal_code: parts_c.append(f"CP {c_addr.postal_code}")
if c_addr.city: parts_c.append(c_addr.city)
if c_addr.state: parts_c.append(c_addr.state)
if c_addr.postal_code: parts_c.append(f"CP {c_addr.postal_code}")
if c_addr.country: parts_c.append(c_addr.country)
if parts_c:
c_dir_str = ", ".join(parts_c).upper()
@@ -173,6 +169,15 @@ class AvisoConsolidadoExportacionService:
razon_social=target_entity_data["razon_social"],
direccion_completa=target_entity_data["direccion_completa"]
)
# Destino/Origen (Clarion: Loc:DestinoOrigen = 'Destino/Origen: '&EqiFex:DestinoOrigenCOVE)
destino_origen_str = ""
if compliance and compliance.origin_destination_cove:
# Assuming enum value or string is what we want.
# If it's an Enum object, accessing .value is safer.
val = compliance.origin_destination_cove
if hasattr(val, 'value'): val = val.value
destino_origen_str = f"Destino/Origen: {val}"
@@ -180,10 +185,28 @@ class AvisoConsolidadoExportacionService:
financials = header.financials
logistics = header.logistics
# Fetch Items associated with this invoice (MOVED UP FOR WEIGHT CALCULATION)
items = db.query(Item).filter(Item.invoice_id == invoice_id).all()
# Peso Bruto
peso_bruto_val = "0.0"
if financials and financials.gross_weight:
calculated_gross_weight = 0.0
# Calculate sum from items first
if items:
for item in items:
if item.lines:
for line in item.lines:
if line.quantity and line.quantity.gross_weight:
try:
calculated_gross_weight += float(line.quantity.gross_weight)
except (ValueError, TypeError):
pass
if financials and financials.gross_weight and float(financials.gross_weight) > 0:
peso_bruto_val = f"{financials.gross_weight:,.2f}"
elif calculated_gross_weight > 0:
peso_bruto_val = f"{calculated_gross_weight:,.2f}"
elif pedimento and pedimento.gross_weight:
peso_bruto_val = f"{pedimento.gross_weight:,.2f}"
@@ -193,33 +216,363 @@ class AvisoConsolidadoExportacionService:
# Split by comma or space if multiple
candados_list = [s.strip() for s in logistics.seal_number.replace(',', ' ').split() if s.strip()]
# Vehiculo
placas_val = ""
tipo_veh_val = ""
if logistics:
placas_val = logistics.license_plate or logistics.vehicle_num or logistics.trailer_num or ""
tipo_veh_val = logistics.transport_type or ""
# Vehiculo / Contenedor Logic (Replicating Clarion)
# Clarion Logic:
# 1. Check for explicit `DatosVehiculo`.
# 2. Check for `EsFerrocarril`.
# 3. Build string from Trailer + Transport.
# Since we don't have a direct "DatosVehiculo" text field in Logistics (usually), we construct it.
# However, we'll check if `license_plate` is being used as a catch-all or if we should build it.
vehiculo_str = ""
tipo_display = ""
# Basic Logistics Data
l_trailer = logistics.trailer_num.strip() if (logistics and logistics.trailer_num) else ""
l_placa = logistics.license_plate.strip() if (logistics and logistics.license_plate) else ""
l_trans_type = logistics.transport_type.strip() if (logistics and logistics.transport_type) else ""
l_vehicle_num = logistics.vehicle_num.strip() if (logistics and logistics.vehicle_num) else ""
l_container_types = logistics.container_types.strip() if (logistics and logistics.container_types) else ""
# Check for Ferrocarril explicitly
is_rail = False
if "FERRO" in l_trans_type.upper() or "RAIL" in l_trans_type.upper():
is_rail = True
# --- LOGIC NUMERO / TIPO ---
final_numero = ""
final_tipo = ""
# 1. Container Logic (Clarion: ContenedoresTipo parsing)
# Format expected: "CONTENEDOR|TIPO,CONTENEDOR2|TIPO2..."
if l_container_types:
# Take first container
first_cont_group = l_container_types.split(',')[0] # Split by comma
if '|' in first_cont_group:
parts = first_cont_group.split('|')
final_numero = parts[0].strip()
final_tipo = parts[1].strip()
else:
# Fallback if no pipe
final_numero = first_cont_group.strip()
final_tipo = "CONT" # Default?
# 2. Transport = Container Logic
elif l_trans_type.upper() == "CONTENEDOR":
# Use trailer num as container num
if l_trailer:
final_numero = l_trailer
# Try to find Type? In Clarion it does a DB lookup into GTrailers.ClaveContenedor
# We assume 'CONTENEDOR' or a default if not found in simplified logic
final_tipo = "CONT"
# 3. Trailer/General Logic (Fallback)
if not final_numero:
# Construct valid string
parts_veh = []
if l_trailer:
parts_veh.append(f"TRAILER: {l_trailer}")
if not tipo_display: tipo_display = "TRAILER"
if l_trans_type and l_trans_type.upper() != "NINGUNO":
if is_rail:
if l_vehicle_num:
parts_veh.append(f"CONTENEDOR: {l_vehicle_num}")
tipo_display = "FERROCARRIL"
else:
segment = l_trans_type
if l_vehicle_num: segment += f": {l_vehicle_num}"
parts_veh.append(segment)
if not tipo_display: tipo_display = l_trans_type
if not parts_veh and l_placa:
parts_veh.append(f"PLACAS: {l_placa}")
final_numero = ", ".join(parts_veh).upper()
final_tipo = tipo_display.upper()
# Codigo de Aceptacion aka Acuse de Validacion
codigo_aceptacion_val = ""
if pedimento and pedimento.pedimento_validation:
# Assuming relationship "pedimento_validation" exists on Pedimentos model (lazy loaded)
# Or we can query it if relationship is scalar 'uselist=False'
if pedimento.pedimento_validation.validation_ack:
codigo_aceptacion_val = pedimento.pedimento_validation.validation_ack
aviso = AvisoSchema(
pedimento_completo=pedimento_txt,
tipo_operacion=header.operation_type.upper() if header.operation_type else "EXP",
clave_pedimento=clave_ped,
acus_valor=compliance.edocument if (compliance and compliance.edocument) else "",
acus_valor=compliance.edocument.upper() if (compliance and compliance.edocument) else "",
aduana_seccion=compliance.aduana if (compliance and compliance.aduana) else "",
numero_remesa=str(compliance.remesa) if (compliance and compliance.remesa) else "",
peso_bruto=peso_bruto_val,
codigo_aceptacion="", # TODO: Clarify source. Using empty for now or Edocument?
codigo_aceptacion=codigo_aceptacion_val,
codigo_barras_b64=None,
clave_seccion=compliance.aduana if (compliance and compliance.aduana) else "", # Using Aduana as Section Key
marcas_numeros_bultos=f"{financials.bundle_count} BULTOS" if (financials and financials.bundle_count) else "1 BULTOS",
candados=candados_list,
vehiculo_placas=placas_val,
vehiculo_tipo=tipo_veh_val,
observaciones=header.observation_es or "",
vehiculo_placas=final_numero,
vehiculo_tipo=final_tipo,
observaciones=(header.observation_es or "") + ("\n" + destino_origen_str if destino_origen_str else ""),
numero_certificado=compliance.certificate_number if (compliance and compliance.certificate_number) else "",
tipo_documento=header.document_type or "FACTURA", # Default
firma_electronica=compliance.electronic_signature if (compliance and compliance.electronic_signature) else ""
)
# --- BARCODE GENERATION (PDF417) ---
# Replicating Clarion "LLENADOCODIGODEBARRAS" logic
try:
# Debug Log
debug_log = []
debug_log.append(f"Processing Invoice {invoice_id}")
# 1. Patente (4 Digits) - From Pedimento or Compliance
patente_txt = pedimento.license if pedimento and pedimento.license else ""
if not patente_txt and pedimento_txt:
# Fallback parsing "YY OFF LIC NUMBER" -> LIC is index 2 (0, 1, 2)
try:
parts = pedimento_txt.split()
if len(parts) >= 3: patente_txt = parts[2]
except: pass
# 2. Pedimento Number (7 Digits)
pedimento_num = pedimento.pedimento_number if pedimento and pedimento.pedimento_number else ""
if not pedimento_num and pedimento_txt:
try:
parts = pedimento_txt.split()
if len(parts) >= 4: pedimento_num = parts[3]
except: pass
# 3. Recinto (3 chars) - Default to 000 if invalid/missing as per Clarion 'ELSE LINEPRINT('000'...'
# Clarion: Loc:Recinto = EqiFex:Recinto
recinto_txt = "000"
if compliance and compliance.enclosure:
recinto_txt = compliance.enclosure[:3]
if not recinto_txt: recinto_txt = "000"
# 4. E-Document
edoc_txt = aviso.acus_valor # Already uppercased
# 5. Num Contenedor (Rail) or 000...
# Clarion: IF Loc:EsFerrocarril = 'SI' ... LINEPRINT(CLIP(Loc:NumContenedor)) ELSE LINEPRINT('0000000000000')
# We reused logic for 'vehiculo_placas' and 'vehiculo_tipo' earlier.
# Let's re-evaluate "EsFerrocarril" logic safely
is_rail_bar = False
if "FERRO" in final_tipo.upper() or "RAIL" in final_tipo.upper():
is_rail_bar = True
field_5 = "0000000000000"
if is_rail_bar:
# We extracted container into 'parts_veh' earlier but let's grab from raw if possible or from aviso?
# In our logic above: "CONTENEDOR: {l_vehicle_num}" was added to textual description.
# Let's use compliance.container_ids or logistics.vehicle_num
c_num = logistics.vehicle_num if logistics and logistics.vehicle_num else ""
if c_num: field_5 = c_num
# 6. Firma Electronica
firma_txt = aviso.firma_electronica
# 7. Cantidad Comercial (Format @n015.3 -> 15 chars total, 3 decimals?)
# We need to sum quantities.
cant_total = 0.0
if items:
for item in items:
if item.lines:
for line in item.lines:
# Priority: Quantity (UMA or Standard)
q = 0.0
if line.quantity:
try:
if line.quantity.quantity_uma is not None:
q = float(line.quantity.quantity_uma)
elif line.quantity.quantity is not None:
q = float(line.quantity.quantity)
except (ValueError, TypeError):
q = 0.0
cant_total += q
# Format: 15 chars, 3 decimals? Actually Clarion LINEPRINT usually just prints the text.
# Clarion 'CLIP(FORMAT(Loc:CantTotal,@n015.3))' removes spaces.
cant_total_str = f"{cant_total:.3f}"
# 8. Valor Total Dlls
# Clarion: LINEPRINT(FORMAT(Loc:ValorTotalDlls,@n012)) -> Integer? Or just standard?
# Clarion @n012 usually means right justified or just specific length?
# Code says: Loc:ValorTotalDlls = GSQLFile3.SQL3:C1 + (rounding logic).
val_usd = 0.0
if financials:
try:
# Use value_me (Foreign Currency) as primary source for USD amount
if financials.value_me is not None:
val_usd = float(financials.value_me)
elif financials.value_mn is not None:
# Fallback to MN if ME is missing (though technically incorrect for USD field, avoids crash)
val_usd = float(financials.value_mn)
except (ValueError, TypeError):
val_usd = 0.0
# Clarion logic:
# IF GSQLFile3.SQL3:C1 > 0 AND GSQLFile3.SQL3:C1 < 1 THEN
# Loc:ValorTotalDlls = GSQLFile3.SQL3:C1 + (1 - GSQLFile3.SQL3:C1) (Result is 1.0)
# ELSE ... ROUND(...,1) or Raw.
final_val_usd = val_usd
if 0.0 < val_usd < 1.0:
final_val_usd = 1.0
elif (val_usd - int(val_usd)) > 0 and (val_usd - int(val_usd)) < 0.5:
# Clarion: IF Loc:Decimal > 0 AND Loc:Decimal < 0.5 THEN Loc:ValorTotalDlls = ROUND(GSQLFile3.SQL3:C1,1)
# Round to 1 decimal place? Or standard round? Python round matches generally.
final_val_usd = round(val_usd, 1)
val_usd_str = f"{final_val_usd:.2f}"
# 9. Cant Embarques (Rail)
field_9 = "000000000000"
if is_rail_bar:
# Logic for Cant Embarques?
# Clarion: EqiFex:CantGuiasEmbarque
# usage unknown in current DB. Defaulting to 0.
pass
# 10. NIU / DTA (Rail)
field_10 = "0000000000000"
if is_rail_bar:
# Clarion: EqiFex:NumeroNIU
if compliance and compliance.niu:
field_10 = compliance.niu
# 11. Remesa (4 chars)
remesa_txt = str(compliance.remesa) if (compliance and compliance.remesa) else "0"
# 12. Filler
field_12 = "00000000.000"
# Construct Line Prints (Text content for barcode)
# Clarion LINEPRINT separates by NewLine? Or is it one long string?
# "Glo:GeneraTXT" is a file. LINEPRINT appends a line.
# So the Barcode Content is a multi-line string or specific format.
# PDF417 normally encodes the full text block.
# 1. Patente (4 Digits)
# Formatted: @P####P -> 4 digits.
# Assuming simple string slice or pad.
patente_formatted = f"{patente_txt}".strip()[:4]
# 2. Pedimento (7 Digits)
pedimento_formatted = f"{pedimento_num}".strip()[:7]
# 3. Recinto (3 Digits Zero Padded @n03)
# Ensure it's numeric-like for zero padding or just string pad?
# Clarion FORMAT(Loc:Recinto,@n03) implies numeric.
try:
recinto_val = int(recinto_txt)
recinto_formatted = f"{recinto_val:03d}"
except:
recinto_formatted = "000"
# 4. E-Document (Left aligned, clipped)
edoc_formatted = edoc_txt.strip()
# 5. Container (13 chars?) or Rail Logic
# Clarion: IF Rail -> CLIP(Loc:NumContenedor) ELSE '0000000000000'
if is_rail_bar and field_5 and len(field_5) > 0:
field_5_formatted = field_5.strip()
else:
field_5_formatted = "0000000000000"
# 6. Firma (Clipped)
firma_formatted = firma_txt.strip()
# 7. Cantidad (FORMAT(Loc:CantTotal,@n015.3)) -> 15 chars, 3 decimals, Zero Padded?
# Python f"{val:015.3f}" produces 15 chars total (including dot) with zero padding.
cant_total_formatted = f"{cant_total:015.3f}"
# 8. Valor USD (FORMAT(Loc:ValorTotalDlls,@n012)) -> 12 chars, Integer?, Zero Padded?
# If Clarion @n012 means Integer:
# But previously we calculated rounding. If it is integer, we cast to int.
# Clarion default doubles formatted with @n012 usually rounds to integer.
# Let's assume Integer Zero Padded for now based on @n012 (no decimal part).
val_usd_formatted = f"{int(final_val_usd):012d}"
# 9. Cant Embarques (Rail) (FORMAT(...,@n012))
field_9_formatted = "000000000000"
if is_rail_bar:
# If we had a value... assuming 0 generally.
pass
# 10. NIU (Rail) (@s13 -> String 13 chars?) or DTA
# Clarion: LINEPRINT(CLIP(FORMAT(Loc:NumeroNIU,@s13)),Glo:GeneraTXT)
# CLIP removes spaces, FORMAT @s13 makes it string 13?
# Actually CLIP(FORMAT(...,@s13)) might just mean "The string value".
# The ELSE is '0000000000000' (13 chars).
field_10_formatted = "0000000000000"
if is_rail_bar and field_10 != "0000000000000":
field_10_formatted = field_10.strip()
# 11. Remesa (FORMAT(...,@n04) -> 4 digits zero padded)
try:
remesa_val = int(remesa_txt)
remesa_formatted = f"{remesa_val:04d}"
except:
remesa_formatted = "0000"
# 12. Filler / Appendix 17
# Clarion Logic:
# IF TipoFactura = 'IMPOTEMP'/'EXPO'/'IMPODEF' ... IF Apendice17=1 -> '000000000003' ELSE '00000000.000'
# Default '00000000.000'
field_12_formatted = "00000000.000"
if compliance and compliance.appendix_17 == 1:
# Check Doc Type? Assuming broadly for now based on flag.
field_12_formatted = "000000000003"
barcode_lines = [
patente_formatted,
pedimento_formatted,
recinto_formatted,
edoc_formatted,
field_5_formatted,
firma_formatted,
cant_total_formatted,
val_usd_formatted,
field_9_formatted,
field_10_formatted,
remesa_formatted,
field_12_formatted
]
# Join with appropriate separator. Clarion LINEPRINT adds CR/LF (Windows).
barcode_content = "\r\n".join(barcode_lines)
# Generate Image
codes = pdf417gen.encode(barcode_content, columns=14)
image = pdf417gen.render_image(codes, scale=5, padding=5)
# Convert to B64
buffered = BytesIO()
image.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
aviso.codigo_barras_b64 = f"data:image/png;base64,{img_str}"
debug_log.append("SUCCESS: Barcode generated.")
except Exception as e:
import traceback
error_msg = f"Error generando codigo de barras InvID={invoice_id}: {str(e)}\n{traceback.format_exc()}"
print(error_msg)
debug_log.append(f"ERROR: {error_msg}")
aviso.codigo_barras_b64 = None
# Write Debug Log
try:
with open("/tmp/barcode_debug.log", "a") as f:
f.write("\n".join(debug_log) + "\n--------------------------------\n")
except Exception as e_log:
print(f"FAILED TO WRITE LOG: {e_log}")
# 4. Agente Aduanal
nombre_agente = ""

View File

@@ -218,7 +218,7 @@
<p class="tiny-bold">CODIGO DE BARRAS</p>
<div class="center p-t-2">
{% if aviso.codigo_barras_b64 %}
<img src="{{ aviso.codigo_barras_b64 }}" style="height: 40pt; max-width: 90%;" />
<img src="{{ aviso.codigo_barras_b64 }}" style="height: 60pt; max-width: 90%;" />
{% else %}
<br><br><br>
{% endif %}