301 lines
14 KiB
Python
301 lines
14 KiB
Python
# -*- 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()
|