Primer commit

This commit is contained in:
2026-05-30 14:31:19 -06:00
commit a35d26fac0
277 changed files with 265240 additions and 0 deletions
+300
View File
@@ -0,0 +1,300 @@
# -*- 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()