Actualizar backend/services.py

This commit is contained in:
2026-09-02 11:06:16 -05:00
parent aebbbf74db
commit 07c7c29e54

View File

@@ -745,9 +745,10 @@ 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 la tabla de cobranza para recalcular la columna SALDO POR RETIRAR # 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")
return {"status": "ok"} return {"status": "ok"}
def alumnos_por_retirar(ano, mes): def alumnos_por_retirar(ano, mes):
@@ -758,6 +759,188 @@ def alumnos_por_retirar(ano, mes):
.execute()) .execute())
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):
"""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():
try:
mats_ret = set(alumnos_por_retirar(ano, mes).get("matriculas", []))
except Exception:
mats_ret = set()
if not mats_ret:
return {"filas": []}
dm = get_dm()
proc = CobranzaLogic(dm).processor # para clasificar SEDE por programa (REPLACE_META)
crono = dm.ejecutar_consulta_cronograma_cobranza(str(ano)) or []
# primera fila por matrícula (todas comparten los datos de cabecera)
idx = {}
for f in crono:
m = str(f.get("num_matricula", "")).strip()
if m and m not in idx:
idx[m] = f
# dsc_vendedor + cod_detalle por matrícula (consulta ligera, solo las POR RETIRAR)
try:
extra = dm.traer_vendedor_cod_detalle(list(mats_ret))
except Exception:
extra = {}
from datetime import datetime as _dtnow
_hoy = _dtnow.now()
cob_idx = {}
try:
alum = cobranza_detalle_todos(str(ano), str(_hoy.month), "TODOS").get("alumnos", [])
for a in alum:
cob_idx[str(a.get("matricula", "")).strip()] = a
except Exception:
cob_idx = {}
# Info manual guardada en Supabase de ESTE mes (una sola lectura). Indexado por matrícula.
try:
info_idx = retiros_info_obtener(ano, mes)
except Exception:
info_idx = {}
def _d(v):
return str(v or "")[:10]
filas = []
for m in mats_ret:
f = idx.get(str(m))
if not f:
continue
ex = extra.get(str(m), {})
cb = cob_idx.get(str(m), {})
inf = info_idx.get(str(m), {})
monto_ant = round((float(cb.get("cta_ant", 0) or 0) - float(cb.get("cob_ant", 0) or 0)), 2)
monto_cur = round((float(cb.get("cta_cur", 0) or 0) - float(cb.get("cob_cur", 0) or 0)), 2)
_prog = str(f.get("dsc_det_programa", "") or f.get("dsc_programa", "") or "").strip()
filas.append({
"matricula": str(m),
"sede": str(proc.clasificar_sede_meta(_prog)),
"fch_matricula": _d(f.get("fch_matricula")),
"asesor": str(f.get("dsc_sectorista", "") or "").strip(),
"vendedor": ex.get("dsc_vendedor", ""),
"programa": str(f.get("dsc_det_programa", "") or f.get("dsc_programa", "") or "").strip(),
"cod_detalle": ex.get("cod_detalle", ""),
"fch_inicio": _d(f.get("fch_inicio")),
"fch_retiro": _d(f.get("fch_retiro")),
"alumno": ex.get("dsc_alumno", ""),
"num_cuotas_pagadas": ex.get("num_cuotas_pagadas", 0),
"num_cuotas_vencidas": ex.get("num_cuotas_vencidas", 0),
"fch_vencimiento": ex.get("fch_venc_mas_antigua", ""),
"monto_venc_ant": monto_ant,
"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),
"n_asistencia": inf.get("n_asistencia"),
"n_faltas": inf.get("n_faltas"),
"ultimo_dia_asist": inf.get("ultimo_dia_asist") or "",
"hasta_sesion": inf.get("hasta_sesion") or "",
"fch_ult_resp": inf.get("fch_ult_resp") or "",
"fch_envio_correo": inf.get("fch_envio_correo") or "",
"retiro_wsp_class": inf.get("retiro_wsp_class") or "",
"motivo_retiro": inf.get("motivo_retiro") or "",
"detalle_retiro": inf.get("detalle_retiro") or "",
})
filas.sort(key=lambda x: x["fch_matricula"], reverse=True)
return {"filas": filas}
return cache_get_or_set("retiros", (str(ano), str(mes)), _load)
_RETIROS_INFO_CAMPOS = [
"n_asistencia", "n_faltas", "ultimo_dia_asist", "hasta_sesion",
"fch_ult_resp", "fch_envio_correo", "retiro_wsp_class",
"motivo_retiro", "detalle_retiro",
]
def _ri_num(v):
if v is None or v == "":
return None
try:
return int(v)
except Exception:
return None
def _ri_manual_payload(campos):
"""Solo las 9 columnas manuales, normalizadas para Supabase."""
c = campos or {}
return {
"n_asistencia": _ri_num(c.get("n_asistencia")),
"n_faltas": _ri_num(c.get("n_faltas")),
"ultimo_dia_asist": c.get("ultimo_dia_asist") or None,
"hasta_sesion": c.get("hasta_sesion") or None,
"fch_ult_resp": c.get("fch_ult_resp") or None,
"fch_envio_correo": c.get("fch_envio_correo") or None,
"retiro_wsp_class": c.get("retiro_wsp_class") or None,
"motivo_retiro": c.get("motivo_retiro") or None,
"detalle_retiro": c.get("detalle_retiro") or None,
}
def retiros_info_obtener(ano, mes):
"""Lee la info manual guardada de (ano, mes) en una sola consulta.
Devuelve un dict indexado por matrícula (egress mínimo: 1 lectura)."""
sb = _sb()
cols = "matricula," + ",".join(_RETIROS_INFO_CAMPOS)
res = (sb.table("retiros_info").select(cols)
.eq("ano", int(ano)).eq("mes", int(mes)).execute())
out = {}
for r in (res.data or []):
out[str(r.get("matricula", "")).strip()] = r
return out
def retiros_info_guardar(matricula, ano, mes, campos, usuario=None):
"""INFO: guarda SOLO las 9 columnas manuales de un alumno en (matricula, ano, mes)."""
sb = _sb()
from datetime import datetime, timezone
payload = {
"matricula": str(matricula),
"ano": int(ano),
"mes": int(mes),
**_ri_manual_payload(campos),
"actualizado_en": datetime.now(timezone.utc).isoformat(),
"actualizado_por": usuario or None,
}
sb.table("retiros_info").upsert(payload, on_conflict="matricula,ano,mes").execute()
from cache_manager import cache_invalidate
cache_invalidate("retiros")
return {"status": "ok"}
def retiros_snapshot_guardar(ano, mes, filas, usuario=None):
"""GUARDAR: reescribe TODO el mes. Borra los registros de (ano, mes) y reinserta
las filas recibidas con TODAS las columnas (snapshot calculado + 9 manuales)."""
sb = _sb()
from datetime import datetime, timezone
ano = int(ano); mes = int(mes)
now = datetime.now(timezone.utc).isoformat()
registros = []
for f in (filas or []):
registros.append({
"matricula": str(f.get("matricula", "")).strip(),
"ano": ano, "mes": mes,
"sede": f.get("sede") or None,
"fch_matricula": f.get("fch_matricula") or None,
"sectorista": f.get("asesor") or None,
"asesor_comercial": f.get("vendedor") or None,
"programa": f.get("programa") or None,
"cod_detalle": (str(f.get("cod_detalle")) if f.get("cod_detalle") not in (None, "") else None),
"fch_inicio": f.get("fch_inicio") or None,
"fch_retiro": f.get("fch_retiro") or None,
"alumno": f.get("alumno") or None,
"num_cuotas_pagadas": _ri_num(f.get("num_cuotas_pagadas")),
"num_cuotas_vencidas": _ri_num(f.get("num_cuotas_vencidas")),
"fch_vencimiento": f.get("fch_vencimiento") or None,
"monto_venc_ant": f.get("monto_venc_ant"),
"monto_venc_cur": f.get("monto_venc_cur"),
"monto_dejo_pagar": f.get("monto_dejo_pagar"),
**_ri_manual_payload(f),
"actualizado_en": now,
"actualizado_por": usuario or None,
})
sb.table("retiros_info").delete().eq("ano", ano).eq("mes", mes).execute()
if registros:
sb.table("retiros_info").insert(registros).execute()
from cache_manager import cache_invalidate
cache_invalidate("retiros")
return {"status": "ok", "guardados": len(registros)}
def usuarios_listar(): def usuarios_listar():
sb = _sb() sb = _sb()
perfiles = sb.table("perfiles").select("id, nombre, rol, activo").execute().data or [] perfiles = sb.table("perfiles").select("id, nombre, rol, activo").execute().data or []