# -*- coding: utf-8 -*- import os import sys import csv import sqlite3 import json ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CSV_PATH = os.path.join(ROOT_DIR, "Monte Providencia - Verificador de sucursales y correos - Sucursales.csv") from paths import DB_PATH def load_full_autos_branches(): if not os.path.exists(CSV_PATH): print(f"Error: El archivo CSV de sucursales no existe en {CSV_PATH}") sys.exit(1) branches = [] with open(CSV_PATH, mode="r", encoding="utf-8-sig", newline="") as fh: reader = csv.DictReader(fh) for row in reader: tipo = (row.get("TIPO DE TIENDA") or "").strip().upper() if tipo == "FULL AUTOS": branches.append({ "sucursal": (row.get("SUCURSAL") or "").strip(), "tienda": (row.get("TIENDA") or "").strip(), "correo_tienda": (row.get("CORREO TIENDA") or "").strip(), "sc_bucefalo": (row.get("SC BUCEFALO") or "").strip(), "location_id": (row.get("ID LOCATION BUCEFALO") or "").strip() }) return branches def main(): if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") print("=== CONTROL DE SCRIPTS: full_autos_investigation.py ===") print("Descripción: Investiga y reporta contactos y oportunidades de sucursales FULL AUTOS.") print("-" * 110) # 1. Cargar sucursales FULL AUTOS de la CSV branches = load_full_autos_branches() if not branches: print("No se encontraron sucursales con TIPO DE TIENDA = 'FULL AUTOS' en el archivo CSV.") return print(f"Se detectaron {len(branches)} sucursales clasificadas como FULL AUTOS en el archivo CSV:") for b in branches: print(f" - {b['tienda']} / {b['sucursal']} (Location ID: {b['location_id']})") print("-" * 110) # 2. Conectar a la base de datos if not os.path.exists(DB_PATH): print(f"Base de datos SQLite no detectada en: {DB_PATH}") print("Requiere sincronización previa desde el dashboard.") return conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row try: # Traer todas las pipelines de la DB para poder resolver nombres de pipelines/stages pipelines_rows = conn.execute("SELECT id, location_id, name, stages_json FROM pipelines").fetchall() # Mapeo de (location_id, pipeline_id) -> pipeline_name # Mapeo de (location_id, pipeline_id, stage_id) -> stage_name pipelines_map = {} stages_map = {} for p in pipelines_rows: loc_id = p['location_id'] p_id = p['id'] pipelines_map[(loc_id, p_id)] = p['name'] try: stages = json.loads(p['stages_json']) for s in stages: stages_map[(loc_id, p_id, s['id'])] = s['name'] except Exception: pass # Totales globales de FULL AUTOS grand_total_contacts = 0 grand_total_opps = 0 grand_total_value = 0.0 status_totals = {"open": 0, "won": 0, "lost": 0, "abandoned": 0} # Guardaremos datos por sucursal para armar un resumen branch_reports = [] synced_count = 0 for b in branches: loc_id = b['location_id'] # Verificar si existe la cuenta en la DB acc_row = conn.execute("SELECT nombre FROM accounts WHERE location_id = ?", (loc_id,)).fetchone() if not acc_row: branch_reports.append({ "branch": b, "synced": False, "contacts_count": 0, "opps_count": 0, "monetary_value": 0.0, "status_breakdown": {}, "pipelines_breakdown": {} }) continue synced_count += 1 # Cantidad de contactos contacts_count = conn.execute("SELECT COUNT(*) FROM contacts WHERE location_id = ?", (loc_id,)).fetchone()[0] # Oportunidades y valor total opps_rows = conn.execute(""" SELECT id, name, status, pipeline_id, pipeline_stage_id, monetary_value, contact_id, date_added FROM opportunities WHERE location_id = ? """, (loc_id,)).fetchall() opps_count = len(opps_rows) total_val = 0.0 b_status_counts = {"open": 0, "won": 0, "lost": 0, "abandoned": 0} b_pipelines = {} # pipeline_id -> { "name": ..., "count": ..., "stages": { stage_id -> count } } for opp in opps_rows: status = (opp['status'] or 'open').lower() if status not in b_status_counts: b_status_counts[status] = 0 b_status_counts[status] += 1 # Actualizar acumulador global if status in status_totals: status_totals[status] += 1 val = opp['monetary_value'] or 0.0 total_val += val p_id = opp['pipeline_id'] s_id = opp['pipeline_stage_id'] p_name = pipelines_map.get((loc_id, p_id), f"Pipeline Desconocido ({p_id})") s_name = stages_map.get((loc_id, p_id, s_id), f"Etapa Desconocida ({s_id})") if p_id not in b_pipelines: b_pipelines[p_id] = { "name": p_name, "count": 0, "stages": {} } b_pipelines[p_id]["count"] += 1 if s_id not in b_pipelines[p_id]["stages"]: b_pipelines[p_id]["stages"][s_id] = { "name": s_name, "count": 0 } b_pipelines[p_id]["stages"][s_id]["count"] += 1 grand_total_contacts += contacts_count grand_total_opps += opps_count grand_total_value += total_val branch_reports.append({ "branch": b, "synced": True, "contacts_count": contacts_count, "opps_count": opps_count, "monetary_value": total_val, "status_breakdown": b_status_counts, "pipelines_breakdown": b_pipelines }) # Imprimir reporte detallado por sucursal print("\n=== DETALLE POR SUCURSAL ===") for r in branch_reports: b = r['branch'] print(f"\nSucursal: {b['tienda']} - {b['sucursal']}") print(f"Location ID: {b['location_id']}") print(f"Correo: {b['correo_tienda']}") if not r['synced']: print(" Status Sync: NO SINCRONIZADA en base de datos local (omitiendo estadísticas).") print(" " + "-"*40) continue print(f" Contactos locales: {r['contacts_count']}") print(f" Oportunidades locales: {r['opps_count']} (Valor total: ${r['monetary_value']:,.2f} MXN)") if r['opps_count'] > 0: print(" Desglose por Estado:") for st, count in r['status_breakdown'].items(): if count > 0: print(f" - {st.upper()}: {count}") print(" Desglose por Pipeline / Etapa:") for p_id, p_info in r['pipelines_breakdown'].items(): print(f" - Pipeline: '{p_info['name']}' (ID: {p_id}) - {p_info['count']} opps") sorted_stages = sorted(p_info['stages'].items(), key=lambda x: x[1]['count'], reverse=True) for s_id, s_info in sorted_stages: print(f" * Etapa: '{s_info['name']}' (ID: {s_id}) - {s_info['count']} opps") else: print(" Sin oportunidades registradas.") print(" " + "-"*40) # Imprimir resumen consolidado print("\n" + "="*110) print("=== RESUMEN CONSOLIDADO FULL AUTOS ===") print(f"Total de sucursales FULL AUTOS analizadas: {len(branches)}") print(f"Sucursales sincronizadas en base de datos: {synced_count} de {len(branches)}") print(f"Suma total de contactos: {grand_total_contacts:,}") print(f"Suma total de oportunidades: {grand_total_opps:,} (Valor total acumulado: ${grand_total_value:,.2f} MXN)") if grand_total_opps > 0: print("\nDesglose global por Estado de Oportunidad:") for st, count in status_totals.items(): pct = (count / grand_total_opps) * 100 print(f" - {st.upper():<10} : {count:>5} ({pct:.1f}%)") print("="*110) # Opcional: Mostrar las 10 oportunidades más recientes de estas sucursales print("\n=== ÚLTIMAS 10 OPORTUNIDADES REGISTRADAS EN SUCURSALES FULL AUTOS ===") loc_ids = [b['location_id'] for b in branches if b['location_id']] if loc_ids and synced_count > 0: placeholders = ",".join("?" for _ in loc_ids) recent_opps_sql = f""" SELECT o.id as opp_id, o.name as opp_name, o.status, o.monetary_value, o.date_added, c.first_name, c.last_name, a.nombre as branch_name FROM opportunities o LEFT JOIN contacts c ON o.contact_id = c.id AND o.location_id = c.location_id JOIN accounts a ON o.location_id = a.location_id WHERE o.location_id IN ({placeholders}) ORDER BY o.date_added DESC LIMIT 10 """ recent_opps = conn.execute(recent_opps_sql, loc_ids).fetchall() if recent_opps: print(f"{'#':<3} | {'Sucursal':<15} | {'Cliente':<25} | {'Oportunidad':<30} | {'Estado':<10} | {'Valor':<12} | {'Fecha':<20}") print("-" * 125) for idx, row in enumerate(recent_opps, 1): branch_name = row['branch_name'] # Clean/shorten branch name for token in ["mp", "-", "859", "0001", "qro", "demo", "plaza"]: branch_name = branch_name.lower().replace(token, "") branch_name = branch_name.strip().upper() client_name = f"{row['first_name'] or ''} {row['last_name'] or ''}".strip() or "N/A" if len(client_name) > 24: client_name = client_name[:21] + "..." opp_name = row['opp_name'] or '' if len(opp_name) > 29: opp_name = opp_name[:26] + "..." val_str = f"${row['monetary_value'] or 0.0:,.2f}" date_str = row['date_added'] or 'N/A' status_str = (row['status'] or 'OPEN').upper() print(f"{idx:02d} | {branch_name:<15} | {client_name:<25} | {opp_name:<30} | {status_str:<10} | {val_str:<12} | {date_str:<20}") print("-" * 125) else: print("No se encontraron oportunidades registradas para estas sucursales.") # Opcional: Mostrar los 5 contactos más recientes de estas sucursales print("\n=== ÚLTIMOS 5 CONTACTOS REGISTRADOS EN SUCURSALES FULL AUTOS ===") if loc_ids and synced_count > 0: placeholders = ",".join("?" for _ in loc_ids) recent_contacts_sql = f""" SELECT c.id, c.first_name, c.last_name, c.email, c.phone, c.date_added, a.nombre as branch_name FROM contacts c JOIN accounts a ON c.location_id = a.location_id WHERE c.location_id IN ({placeholders}) ORDER BY c.date_added DESC LIMIT 5 """ recent_contacts = conn.execute(recent_contacts_sql, loc_ids).fetchall() if recent_contacts: print(f"{'#':<3} | {'Sucursal':<15} | {'Cliente':<25} | {'Teléfono':<15} | {'Email':<30} | {'Fecha':<20}") print("-" * 115) for idx, row in enumerate(recent_contacts, 1): branch_name = row['branch_name'] for token in ["mp", "-", "859", "0001", "qro", "demo", "plaza"]: branch_name = branch_name.lower().replace(token, "") branch_name = branch_name.strip().upper() client_name = f"{row['first_name'] or ''} {row['last_name'] or ''}".strip() or "N/A" if len(client_name) > 24: client_name = client_name[:21] + "..." phone = row['phone'] or 'N/A' email = row['email'] or 'N/A' date_str = row['date_added'] or 'N/A' print(f"{idx:02d} | {branch_name:<15} | {client_name:<25} | {phone:<15} | {email:<30} | {date_str:<20}") print("-" * 115) else: print("No se encontraron contactos registrados para estas sucursales.") finally: conn.close() if __name__ == '__main__': main()