Actualizar backend/services.py
This commit is contained in:
@@ -91,14 +91,16 @@ def ventas(ano, mes, sede="TODOS", programa="TODOS", con_overrides=False):
|
|||||||
dm._fecha_canc_overrides = {}
|
dm._fecha_canc_overrides = {}
|
||||||
dm._inv_neta_overrides = {}
|
dm._inv_neta_overrides = {}
|
||||||
logic = VentasLogic(dm)
|
logic = VentasLogic(dm)
|
||||||
datos = logic.obtener_datos_brutos_filtrado(str(ano), str(mes), sede, programa) \
|
# En VENTAS (con_overrides=False) excluimos los RET aunque hayan pagado. En Comisiones NO.
|
||||||
if hasattr(logic, "obtener_datos_brutos_filtrado") else logic.obtener_datos_brutos(str(ano), str(mes))
|
_excl_ret = not con_overrides
|
||||||
|
datos = logic.obtener_datos_brutos_filtrado(str(ano), str(mes), sede, programa, _excl_ret) \
|
||||||
|
if hasattr(logic, "obtener_datos_brutos_filtrado") else logic.obtener_datos_brutos(str(ano), str(mes), _excl_ret)
|
||||||
datos = [d for d in datos if d.get('VENDEDOR', 'SIN VENDEDOR') != 'SIN VENDEDOR']
|
datos = [d for d in datos if d.get('VENDEDOR', 'SIN VENDEDOR') != 'SIN VENDEDOR']
|
||||||
|
|
||||||
# PENDIENTES: recalcular desde el MISMO detalle del "Ver" (__TODOS__), para que
|
# PENDIENTES: recalcular desde el MISMO detalle del "Ver" (__TODOS__), para que
|
||||||
# la tabla principal y el popup Ver salgan SIEMPRE idénticos (misma fuente/regla).
|
# la tabla principal y el popup Ver salgan SIEMPRE idénticos (misma fuente/regla).
|
||||||
try:
|
try:
|
||||||
det = logic.obtener_detalle_vendedor("__TODOS__", str(ano), str(mes))
|
det = logic.obtener_detalle_vendedor("__TODOS__", str(ano), str(mes), _excl_ret)
|
||||||
pend_filas = det.get("Venta Pendientes", []) if isinstance(det, dict) else []
|
pend_filas = det.get("Venta Pendientes", []) if isinstance(det, dict) else []
|
||||||
pend_por_vend = {}
|
pend_por_vend = {}
|
||||||
for f in pend_filas:
|
for f in pend_filas:
|
||||||
@@ -345,9 +347,22 @@ def rentabilidad(ano, mes, sede="TODOS", filtro_prog="TODOS"):
|
|||||||
logic = RentabilidadLogic(dm)
|
logic = RentabilidadLogic(dm)
|
||||||
datos = logic.obtener_datos_procesados(str(ano), str(mes), sede, filtro_prog)
|
datos = logic.obtener_datos_procesados(str(ano), str(mes), sede, filtro_prog)
|
||||||
filas = logic.formatear_datos_para_tabla(datos)
|
filas = logic.formatear_datos_para_tabla(datos)
|
||||||
|
# Desglose de costos por programa (para exportar). Va aparte para NO alterar
|
||||||
|
# la tabla visible (que solo usa las 13 primeras columnas).
|
||||||
|
extras = {}
|
||||||
|
for f in (filas or []):
|
||||||
|
if len(f) >= 29 and str(f[0]).upper().find("TOTAL") < 0:
|
||||||
|
extras[str(f[0])] = {
|
||||||
|
"costo_epp_cert": f[14], "costo_doc": f[15],
|
||||||
|
"costo_cons": f[16], "costo_mkt": f[17], "mb_ini": f[18],
|
||||||
|
"sede": f[19], "venta_ini": f[20], "costo_ini": f[21],
|
||||||
|
"venta_act": f[22], "costo_act": f[23], "mb_act": f[24],
|
||||||
|
"costo_cert": f[25], "total_ins": f[26], "retirados": f[27],
|
||||||
|
"en_curso": f[28],
|
||||||
|
}
|
||||||
filas_s = _serializar_filas(filas, 13)
|
filas_s = _serializar_filas(filas, 13)
|
||||||
nombres = [str(f[0]) for f in filas_s if "TOTAL" not in str(f[0]).upper()]
|
nombres = [str(f[0]) for f in filas_s if "TOTAL" not in str(f[0]).upper()]
|
||||||
return {"filas": filas_s, "programas": nombres}
|
return {"filas": filas_s, "programas": nombres, "extras": extras}
|
||||||
return cache_get_or_set("rentabilidad", (ano, mes, sede, filtro_prog), _load)
|
return cache_get_or_set("rentabilidad", (ano, mes, sede, filtro_prog), _load)
|
||||||
|
|
||||||
|
|
||||||
@@ -719,6 +734,7 @@ def _sb():
|
|||||||
return dm.supabase_client
|
return dm.supabase_client
|
||||||
|
|
||||||
# ── Estado/observaciones por alumno (tabla cobranza_estado_alumno) ──
|
# ── Estado/observaciones por alumno (tabla cobranza_estado_alumno) ──
|
||||||
|
# Ahora es POR (matricula, ano, mes): el estado/observaciones aplican a ese mes.
|
||||||
def alumno_estado_obtener(matricula, ano, mes):
|
def alumno_estado_obtener(matricula, ano, mes):
|
||||||
"""Devuelve {estado, observaciones} de un alumno en (ano, mes). Default si no existe."""
|
"""Devuelve {estado, observaciones} de un alumno en (ano, mes). Default si no existe."""
|
||||||
sb = _sb()
|
sb = _sb()
|
||||||
@@ -745,7 +761,7 @@ def alumno_estado_guardar(matricula, ano, mes, estado, observaciones, usuario=No
|
|||||||
"actualizado_por": usuario or None,
|
"actualizado_por": usuario or None,
|
||||||
}
|
}
|
||||||
sb.table("cobranza_estado_alumno").upsert(payload, on_conflict="matricula,ano,mes").execute()
|
sb.table("cobranza_estado_alumno").upsert(payload, on_conflict="matricula,ano,mes").execute()
|
||||||
# Invalidar cobranza (columna SALDO POR RETIRAR) y el apartado Retiros
|
# Invalidar cobranza (columna SALDO POR RETIRAR) y el apartado Retiros.
|
||||||
from cache_manager import cache_invalidate
|
from cache_manager import cache_invalidate
|
||||||
cache_invalidate("cobranza")
|
cache_invalidate("cobranza")
|
||||||
cache_invalidate("retiros")
|
cache_invalidate("retiros")
|
||||||
@@ -760,13 +776,12 @@ def alumnos_por_retirar(ano, mes):
|
|||||||
return {"matriculas": [str(r["matricula"]) for r in (res.data or [])]}
|
return {"matriculas": [str(r["matricula"]) for r in (res.data or [])]}
|
||||||
|
|
||||||
|
|
||||||
# ── RETIROS (apartado) ─────────────────────────────────────────────────────
|
|
||||||
def retiros(ano="2026", mes=8):
|
def retiros(ano="2026", mes=8):
|
||||||
"""Apartado RETIROS: alumnos marcados POR RETIRAR en (ano, mes), con sus datos
|
"""Apartado RETIROS: alumnos marcados POR RETIRAR en (ano, mes), con sus datos
|
||||||
del cronograma de cobranza. El estado POR RETIRAR y la info manual son de ese mes."""
|
del cronograma de cobranza. El estado POR RETIRAR y la info manual son de ese mes."""
|
||||||
def _load():
|
def _load():
|
||||||
# ¿Hay SNAPSHOT guardado (GUARDAR TOTAL) de este mes? Si hay al menos una
|
# 1) ¿Hay SNAPSHOT guardado (GUARDAR TOTAL) de este mes? Si hay al menos una
|
||||||
# fila guardada, se muestra ese snapshot congelado (todas las columnas).
|
# fila guardada, se muestra ese snapshot congelado (todas las columnas).
|
||||||
snap = retiros_snapshot_obtener(ano, mes)
|
snap = retiros_snapshot_obtener(ano, mes)
|
||||||
if snap.get("filas"):
|
if snap.get("filas"):
|
||||||
return {"filas": snap["filas"], "guardado": True,
|
return {"filas": snap["filas"], "guardado": True,
|
||||||
@@ -791,6 +806,8 @@ def retiros(ano="2026", mes=8):
|
|||||||
extra = dm.traer_vendedor_cod_detalle(list(mats_ret))
|
extra = dm.traer_vendedor_cod_detalle(list(mats_ret))
|
||||||
except Exception:
|
except Exception:
|
||||||
extra = {}
|
extra = {}
|
||||||
|
# Saldos MES ANTERIOR y MES EN CURSO por matrícula (mismos datos de Cobranza).
|
||||||
|
# cta_ant-cob_ant = saldo mes anterior; cta_cur-cob_cur = saldo mes en curso.
|
||||||
from datetime import datetime as _dtnow
|
from datetime import datetime as _dtnow
|
||||||
_hoy = _dtnow.now()
|
_hoy = _dtnow.now()
|
||||||
cob_idx = {}
|
cob_idx = {}
|
||||||
@@ -836,6 +853,7 @@ def retiros(ano="2026", mes=8):
|
|||||||
"monto_venc_cur": monto_cur,
|
"monto_venc_cur": monto_cur,
|
||||||
"imp_total_pagado": ex.get("imp_total_pagado", 0),
|
"imp_total_pagado": ex.get("imp_total_pagado", 0),
|
||||||
"monto_dejo_pagar": round(float(ex.get("inv_neta", 0) or 0) - float(ex.get("imp_total_pagado", 0) or 0), 2),
|
"monto_dejo_pagar": round(float(ex.get("inv_neta", 0) or 0) - float(ex.get("imp_total_pagado", 0) or 0), 2),
|
||||||
|
# columnas manuales guardadas de este mes
|
||||||
"n_asistencia": inf.get("n_asistencia"),
|
"n_asistencia": inf.get("n_asistencia"),
|
||||||
"n_faltas": inf.get("n_faltas"),
|
"n_faltas": inf.get("n_faltas"),
|
||||||
"ultimo_dia_asist": inf.get("ultimo_dia_asist") or "",
|
"ultimo_dia_asist": inf.get("ultimo_dia_asist") or "",
|
||||||
@@ -846,11 +864,13 @@ def retiros(ano="2026", mes=8):
|
|||||||
"motivo_retiro": inf.get("motivo_retiro") or "",
|
"motivo_retiro": inf.get("motivo_retiro") or "",
|
||||||
"detalle_retiro": inf.get("detalle_retiro") or "",
|
"detalle_retiro": inf.get("detalle_retiro") or "",
|
||||||
})
|
})
|
||||||
|
# ordenar por fecha de matrícula (más reciente primero)
|
||||||
filas.sort(key=lambda x: x["fch_matricula"], reverse=True)
|
filas.sort(key=lambda x: x["fch_matricula"], reverse=True)
|
||||||
return {"filas": filas, "guardado": False}
|
return {"filas": filas, "guardado": False}
|
||||||
return cache_get_or_set("retiros", (str(ano), str(mes)), _load)
|
return cache_get_or_set("retiros", (str(ano), str(mes)), _load)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Info manual de retiros (tabla retiros_info en Supabase) ──
|
||||||
_RETIROS_INFO_CAMPOS = [
|
_RETIROS_INFO_CAMPOS = [
|
||||||
"n_asistencia", "n_faltas", "ultimo_dia_asist", "hasta_sesion",
|
"n_asistencia", "n_faltas", "ultimo_dia_asist", "hasta_sesion",
|
||||||
"fch_ult_resp", "fch_envio_correo", "retiro_wsp_class",
|
"fch_ult_resp", "fch_envio_correo", "retiro_wsp_class",
|
||||||
@@ -905,6 +925,7 @@ def retiros_snapshot_obtener(ano, mes):
|
|||||||
filas = []
|
filas = []
|
||||||
guardado_en = None
|
guardado_en = None
|
||||||
for r in (res.data or []):
|
for r in (res.data or []):
|
||||||
|
# Fila de snapshot = tiene alumno o programa (las de solo-INFO no los tienen).
|
||||||
if not (r.get("alumno") or r.get("programa")):
|
if not (r.get("alumno") or r.get("programa")):
|
||||||
continue
|
continue
|
||||||
if not guardado_en and r.get("actualizado_en"):
|
if not guardado_en and r.get("actualizado_en"):
|
||||||
@@ -952,6 +973,7 @@ def retiros_info_guardar(matricula, ano, mes, campos, usuario=None):
|
|||||||
"actualizado_en": datetime.now(timezone.utc).isoformat(),
|
"actualizado_en": datetime.now(timezone.utc).isoformat(),
|
||||||
"actualizado_por": usuario or None,
|
"actualizado_por": usuario or None,
|
||||||
}
|
}
|
||||||
|
# upsert por la llave (matricula, ano, mes)
|
||||||
sb.table("retiros_info").upsert(payload, on_conflict="matricula,ano,mes").execute()
|
sb.table("retiros_info").upsert(payload, on_conflict="matricula,ano,mes").execute()
|
||||||
from cache_manager import cache_invalidate
|
from cache_manager import cache_invalidate
|
||||||
cache_invalidate("retiros")
|
cache_invalidate("retiros")
|
||||||
@@ -989,6 +1011,7 @@ def retiros_snapshot_guardar(ano, mes, filas, usuario=None):
|
|||||||
"actualizado_en": now,
|
"actualizado_en": now,
|
||||||
"actualizado_por": usuario or None,
|
"actualizado_por": usuario or None,
|
||||||
})
|
})
|
||||||
|
# 1) borrar todo el mes 2) insertar lo nuevo (2 operaciones, egress mínimo)
|
||||||
sb.table("retiros_info").delete().eq("ano", ano).eq("mes", mes).execute()
|
sb.table("retiros_info").delete().eq("ano", ano).eq("mes", mes).execute()
|
||||||
if registros:
|
if registros:
|
||||||
sb.table("retiros_info").insert(registros).execute()
|
sb.table("retiros_info").insert(registros).execute()
|
||||||
@@ -1051,16 +1074,121 @@ def usuarios_eliminar(user_id):
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── HISTORIAL DE ESTADO DE ASESORES (Chatwoot -> tabla historial_estado_asesor) ──
|
||||||
|
def registrar_estado_asesores():
|
||||||
|
"""Lee el estado actual de cada agente en Chatwoot (account_users) y, si cambió
|
||||||
|
respecto a lo último guardado, inserta una fila en historial_estado_asesor.
|
||||||
|
availability: 0 = ON (conectado), 1 = OFF (desconectado)."""
|
||||||
|
dm = get_dm()
|
||||||
|
conn = dm.get_pg_connection()
|
||||||
|
if conn is None:
|
||||||
|
return {"status": "error", "msg": "sin conexion postgre"}
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
# Estado actual de cada asesor (solo correos de la escuela, ignora tests)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT u.name, u.uid, au.availability, au.updated_at
|
||||||
|
FROM account_users au
|
||||||
|
JOIN users u ON au.user_id = u.id
|
||||||
|
WHERE u.uid LIKE '%@escueladerefrigeracion.edu.pe'
|
||||||
|
""")
|
||||||
|
actuales = cur.fetchall() # [(name, email, availability, updated_at), ...]
|
||||||
|
|
||||||
|
# Ultimo estado guardado por asesor (para comparar)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT DISTINCT ON (email) email, accion
|
||||||
|
FROM historial_estado_asesor
|
||||||
|
ORDER BY email, fecha_hora DESC
|
||||||
|
""")
|
||||||
|
ultimo = {r[0]: r[1] for r in cur.fetchall()} # {email: 'ON'/'OFF'}
|
||||||
|
|
||||||
|
nuevos = 0
|
||||||
|
for (name, email, avail, updated_at) in actuales:
|
||||||
|
accion = "ON" if int(avail) == 0 else "OFF" # 0=on, 1=off
|
||||||
|
if ultimo.get(email) == accion:
|
||||||
|
continue # no cambió -> no se guarda
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO historial_estado_asesor (asesor, email, accion, origen, quien, fecha_hora)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s)
|
||||||
|
""", (name, email, accion, "CHATWOOT", name, updated_at))
|
||||||
|
nuevos += 1
|
||||||
|
conn.commit()
|
||||||
|
return {"status": "ok", "nuevos": nuevos}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[historial asesores] {e}")
|
||||||
|
return {"status": "error", "msg": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def desactivar_auto_offline_todos():
|
||||||
|
"""Revisa TODOS los agentes de Chatwoot y, si alguno tiene auto_offline activado,
|
||||||
|
lo desactiva. Pensado para correr 1 vez al día (5-6am)."""
|
||||||
|
import requests
|
||||||
|
CHATWOOT_URL = "https://gestor.escueladerefrigeracion.edu.pe"
|
||||||
|
ACCESS_TOKEN = "4anazHvZnvKLtup8biu5Zuoh"
|
||||||
|
ACCOUNT_ID = "1"
|
||||||
|
headers = {"api_access_token": ACCESS_TOKEN, "Content-Type": "application/json"}
|
||||||
|
try:
|
||||||
|
r = requests.get(f"{CHATWOOT_URL}/api/v1/accounts/{ACCOUNT_ID}/agents",
|
||||||
|
headers=headers, timeout=15)
|
||||||
|
r.raise_for_status()
|
||||||
|
agentes = r.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[auto_offline] no se pudo leer agentes: {e}")
|
||||||
|
return {"status": "error", "msg": str(e)}
|
||||||
|
|
||||||
|
desactivados = 0
|
||||||
|
for ag in agentes:
|
||||||
|
ao = ag.get("auto_offline")
|
||||||
|
if ao is True or str(ao).lower() == "true":
|
||||||
|
try:
|
||||||
|
requests.patch(f"{CHATWOOT_URL}/api/v1/accounts/{ACCOUNT_ID}/agents/{ag.get('id')}",
|
||||||
|
headers=headers, json={"auto_offline": False}, timeout=15)
|
||||||
|
desactivados += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[auto_offline] fallo al desactivar {ag.get('id')}: {e}")
|
||||||
|
print(f"[auto_offline] desactivados: {desactivados}")
|
||||||
|
return {"status": "ok", "desactivados": desactivados}
|
||||||
|
|
||||||
|
|
||||||
|
def historial_asesor(email, limite=100):
|
||||||
|
"""Devuelve el historial de estados de un asesor (por email), más reciente primero."""
|
||||||
|
dm = get_dm()
|
||||||
|
conn = dm.get_pg_connection()
|
||||||
|
if conn is None:
|
||||||
|
return {"historial": []}
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT accion, origen, quien, fecha_hora
|
||||||
|
FROM historial_estado_asesor
|
||||||
|
WHERE email = %s
|
||||||
|
ORDER BY fecha_hora DESC
|
||||||
|
LIMIT %s
|
||||||
|
""", (email, int(limite)))
|
||||||
|
filas = [{"accion": r[0], "origen": r[1], "quien": r[2],
|
||||||
|
"fecha_hora": r[3].isoformat() if r[3] else None} for r in cur.fetchall()]
|
||||||
|
return {"historial": filas}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[historial_asesor] {e}")
|
||||||
|
return {"historial": []}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
# ── ROLES Y PERMISOS (tabla 'permisos': rol, modulo, puede_ver) ────────────
|
# ── ROLES Y PERMISOS (tabla 'permisos': rol, modulo, puede_ver) ────────────
|
||||||
def roles_modulos_disponibles():
|
def roles_modulos_disponibles():
|
||||||
"""Lista de módulos existentes (todos los que aparecen en la tabla permisos)."""
|
"""Lista de módulos existentes (todos los que aparecen en la tabla permisos),
|
||||||
|
para armar las casillas al crear un rol. Ordenados alfabéticamente."""
|
||||||
sb = _sb()
|
sb = _sb()
|
||||||
res = sb.table("permisos").select("modulo").execute()
|
res = sb.table("permisos").select("modulo").execute()
|
||||||
mods = sorted({str(r.get("modulo", "")).strip() for r in (res.data or []) if r.get("modulo")})
|
mods = sorted({str(r.get("modulo", "")).strip() for r in (res.data or []) if r.get("modulo")})
|
||||||
return {"modulos": mods}
|
return {"modulos": mods}
|
||||||
|
|
||||||
def roles_listar():
|
def roles_listar():
|
||||||
"""Devuelve todos los roles con la lista de módulos que puede ver cada uno."""
|
"""Devuelve todos los roles con la lista de módulos que puede ver cada uno.
|
||||||
|
[{rol, modulos: [..]}]"""
|
||||||
sb = _sb()
|
sb = _sb()
|
||||||
res = sb.table("permisos").select("rol,modulo,puede_ver").execute()
|
res = sb.table("permisos").select("rol,modulo,puede_ver").execute()
|
||||||
por_rol = {}
|
por_rol = {}
|
||||||
@@ -1074,7 +1202,8 @@ def roles_listar():
|
|||||||
return {"roles": [{"rol": k, "modulos": sorted(v)} for k, v in sorted(por_rol.items())]}
|
return {"roles": [{"rol": k, "modulos": sorted(v)} for k, v in sorted(por_rol.items())]}
|
||||||
|
|
||||||
def rol_guardar(rol, modulos):
|
def rol_guardar(rol, modulos):
|
||||||
"""Crea o actualiza un rol: borra sus filas y reinserta una por módulo marcado."""
|
"""Crea o actualiza un rol: borra sus filas y reinserta una por módulo marcado.
|
||||||
|
modulos = lista de módulos que el rol PUEDE ver."""
|
||||||
sb = _sb()
|
sb = _sb()
|
||||||
rol = str(rol or "").strip().upper()
|
rol = str(rol or "").strip().upper()
|
||||||
if not rol:
|
if not rol:
|
||||||
@@ -1212,73 +1341,3 @@ def comisiones_override_restaurar(num_matricula):
|
|||||||
cache_invalidate("ventas")
|
cache_invalidate("ventas")
|
||||||
cache_invalidate("comisiones_det_todos")
|
cache_invalidate("comisiones_det_todos")
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
# ── HISTORIAL DE ESTADO DE ASESORES (Chatwoot -> historial_estado_asesor) ──
|
|
||||||
def registrar_estado_asesores():
|
|
||||||
"""Lee el estado actual de cada agente en Chatwoot (account_users) y, si cambió
|
|
||||||
respecto a lo último guardado, inserta una fila en historial_estado_asesor.
|
|
||||||
availability: 0 = ON (conectado), 1 = OFF (desconectado)."""
|
|
||||||
dm = get_dm()
|
|
||||||
conn = dm.get_pg_connection()
|
|
||||||
if conn is None:
|
|
||||||
return {"status": "error", "msg": "sin conexion postgre"}
|
|
||||||
try:
|
|
||||||
cur = conn.cursor()
|
|
||||||
cur.execute("""
|
|
||||||
SELECT u.name, u.uid, au.availability, au.updated_at
|
|
||||||
FROM account_users au
|
|
||||||
JOIN users u ON au.user_id = u.id
|
|
||||||
WHERE u.uid LIKE '%@escueladerefrigeracion.edu.pe'
|
|
||||||
""")
|
|
||||||
actuales = cur.fetchall()
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT DISTINCT ON (email) email, accion
|
|
||||||
FROM historial_estado_asesor
|
|
||||||
ORDER BY email, fecha_hora DESC
|
|
||||||
""")
|
|
||||||
ultimo = {r[0]: r[1] for r in cur.fetchall()}
|
|
||||||
|
|
||||||
nuevos = 0
|
|
||||||
for (name, email, avail, updated_at) in actuales:
|
|
||||||
accion = "ON" if int(avail) == 0 else "OFF"
|
|
||||||
if ultimo.get(email) == accion:
|
|
||||||
continue
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO historial_estado_asesor (asesor, email, accion, origen, quien, fecha_hora)
|
|
||||||
VALUES (%s, %s, %s, %s, %s, %s)
|
|
||||||
""", (name, email, accion, "CHATWOOT", name, updated_at))
|
|
||||||
nuevos += 1
|
|
||||||
conn.commit()
|
|
||||||
return {"status": "ok", "nuevos": nuevos}
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[historial asesores] {e}")
|
|
||||||
return {"status": "error", "msg": str(e)}
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
def historial_asesor(email, limite=100):
|
|
||||||
"""Devuelve el historial de estados de un asesor (por email), más reciente primero."""
|
|
||||||
dm = get_dm()
|
|
||||||
conn = dm.get_pg_connection()
|
|
||||||
if conn is None:
|
|
||||||
return {"historial": []}
|
|
||||||
try:
|
|
||||||
cur = conn.cursor()
|
|
||||||
cur.execute("""
|
|
||||||
SELECT accion, origen, quien, fecha_hora
|
|
||||||
FROM historial_estado_asesor
|
|
||||||
WHERE email = %s
|
|
||||||
ORDER BY fecha_hora DESC
|
|
||||||
LIMIT %s
|
|
||||||
""", (email, int(limite)))
|
|
||||||
filas = [{"accion": r[0], "origen": r[1], "quien": r[2],
|
|
||||||
"fecha_hora": r[3].isoformat() if r[3] else None} for r in cur.fetchall()]
|
|
||||||
return {"historial": filas}
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[historial_asesor] {e}")
|
|
||||||
return {"historial": []}
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user