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._inv_neta_overrides = {}
|
||||
logic = VentasLogic(dm)
|
||||
datos = logic.obtener_datos_brutos_filtrado(str(ano), str(mes), sede, programa) \
|
||||
if hasattr(logic, "obtener_datos_brutos_filtrado") else logic.obtener_datos_brutos(str(ano), str(mes))
|
||||
# En VENTAS (con_overrides=False) excluimos los RET aunque hayan pagado. En Comisiones NO.
|
||||
_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']
|
||||
|
||||
# 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).
|
||||
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_por_vend = {}
|
||||
for f in pend_filas:
|
||||
@@ -345,9 +347,22 @@ def rentabilidad(ano, mes, sede="TODOS", filtro_prog="TODOS"):
|
||||
logic = RentabilidadLogic(dm)
|
||||
datos = logic.obtener_datos_procesados(str(ano), str(mes), sede, filtro_prog)
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@@ -719,6 +734,7 @@ def _sb():
|
||||
return dm.supabase_client
|
||||
|
||||
# ── 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):
|
||||
"""Devuelve {estado, observaciones} de un alumno en (ano, mes). Default si no existe."""
|
||||
sb = _sb()
|
||||
@@ -745,7 +761,7 @@ def alumno_estado_guardar(matricula, ano, mes, estado, observaciones, usuario=No
|
||||
"actualizado_por": usuario or None,
|
||||
}
|
||||
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
|
||||
cache_invalidate("cobranza")
|
||||
cache_invalidate("retiros")
|
||||
@@ -760,12 +776,11 @@ def alumnos_por_retirar(ano, mes):
|
||||
return {"matriculas": [str(r["matricula"]) for r in (res.data or [])]}
|
||||
|
||||
|
||||
# ── RETIROS (apartado) ─────────────────────────────────────────────────────
|
||||
def retiros(ano="2026", mes=8):
|
||||
"""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."""
|
||||
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).
|
||||
snap = retiros_snapshot_obtener(ano, mes)
|
||||
if snap.get("filas"):
|
||||
@@ -791,6 +806,8 @@ def retiros(ano="2026", mes=8):
|
||||
extra = dm.traer_vendedor_cod_detalle(list(mats_ret))
|
||||
except Exception:
|
||||
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
|
||||
_hoy = _dtnow.now()
|
||||
cob_idx = {}
|
||||
@@ -836,6 +853,7 @@ def retiros(ano="2026", mes=8):
|
||||
"monto_venc_cur": monto_cur,
|
||||
"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),
|
||||
# columnas manuales guardadas de este mes
|
||||
"n_asistencia": inf.get("n_asistencia"),
|
||||
"n_faltas": inf.get("n_faltas"),
|
||||
"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 "",
|
||||
"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)
|
||||
return {"filas": filas, "guardado": False}
|
||||
return cache_get_or_set("retiros", (str(ano), str(mes)), _load)
|
||||
|
||||
|
||||
# ── Info manual de retiros (tabla retiros_info en Supabase) ──
|
||||
_RETIROS_INFO_CAMPOS = [
|
||||
"n_asistencia", "n_faltas", "ultimo_dia_asist", "hasta_sesion",
|
||||
"fch_ult_resp", "fch_envio_correo", "retiro_wsp_class",
|
||||
@@ -905,6 +925,7 @@ def retiros_snapshot_obtener(ano, mes):
|
||||
filas = []
|
||||
guardado_en = None
|
||||
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")):
|
||||
continue
|
||||
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_por": usuario or None,
|
||||
}
|
||||
# upsert por la llave (matricula, ano, mes)
|
||||
sb.table("retiros_info").upsert(payload, on_conflict="matricula,ano,mes").execute()
|
||||
from cache_manager import cache_invalidate
|
||||
cache_invalidate("retiros")
|
||||
@@ -989,6 +1011,7 @@ def retiros_snapshot_guardar(ano, mes, filas, usuario=None):
|
||||
"actualizado_en": now,
|
||||
"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()
|
||||
if registros:
|
||||
sb.table("retiros_info").insert(registros).execute()
|
||||
@@ -1051,16 +1074,121 @@ def usuarios_eliminar(user_id):
|
||||
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) ────────────
|
||||
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()
|
||||
res = sb.table("permisos").select("modulo").execute()
|
||||
mods = sorted({str(r.get("modulo", "")).strip() for r in (res.data or []) if r.get("modulo")})
|
||||
return {"modulos": mods}
|
||||
|
||||
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()
|
||||
res = sb.table("permisos").select("rol,modulo,puede_ver").execute()
|
||||
por_rol = {}
|
||||
@@ -1074,7 +1202,8 @@ def roles_listar():
|
||||
return {"roles": [{"rol": k, "modulos": sorted(v)} for k, v in sorted(por_rol.items())]}
|
||||
|
||||
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()
|
||||
rol = str(rol or "").strip().upper()
|
||||
if not rol:
|
||||
@@ -1212,73 +1341,3 @@ def comisiones_override_restaurar(num_matricula):
|
||||
cache_invalidate("ventas")
|
||||
cache_invalidate("comisiones_det_todos")
|
||||
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