Actualizar backend/services.py
This commit is contained in:
@@ -166,6 +166,227 @@ def _clasif_filtro_programa(dm, dscp):
|
|||||||
return fdata.get("DEFAULT", "OTROS") if isinstance(fdata.get("DEFAULT"), str) else "OTROS"
|
return fdata.get("DEFAULT", "OTROS") if isinstance(fdata.get("DEFAULT"), str) else "OTROS"
|
||||||
|
|
||||||
|
|
||||||
|
def _clasif_sede_fact(dm, dscp):
|
||||||
|
"""Clasifica la sede del programa con sede.json (igual que Cobranza/Ocupabilidad)."""
|
||||||
|
up = str(dscp or "").upper()
|
||||||
|
if not up:
|
||||||
|
return ""
|
||||||
|
for s, data in dm.sede_data.get("clasificacion_sedes", {}).items():
|
||||||
|
if s == "DEFAULT":
|
||||||
|
continue
|
||||||
|
for pat in data.get("patrones", []):
|
||||||
|
if pat.upper() in up:
|
||||||
|
return s
|
||||||
|
_def = dm.sede_data.get("clasificacion_default", {})
|
||||||
|
return _def.get("sede", "LIMA") if isinstance(_def, dict) else "LIMA"
|
||||||
|
|
||||||
|
|
||||||
|
def _fact_ym(fch):
|
||||||
|
"""Devuelve (año, mes) de una fecha (datetime o str 'YYYY-MM-DD')."""
|
||||||
|
if fch is None:
|
||||||
|
return (None, None)
|
||||||
|
try:
|
||||||
|
return (fch.year, fch.month)
|
||||||
|
except Exception:
|
||||||
|
s = str(fch)
|
||||||
|
try:
|
||||||
|
return (int(s[0:4]), int(s[5:7]))
|
||||||
|
except Exception:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def facturacion(ano="TODOS", mes="TODOS", cuota="TODOS"):
|
||||||
|
"""Matriz de Facturación: Ingresos (facturas) − Notas de Crédito = Neto,
|
||||||
|
agrupado Sede -> Tipo. Clasifica con los JSON (sede.json / REPLACE_META.json).
|
||||||
|
Filtros por fch_emision (año/mes) y num_cuota. La NC hereda programa y cuota
|
||||||
|
de su factura (enlace num_comprobante_BF = num_comprobante). Replica el Power BI."""
|
||||||
|
dm = get_dm()
|
||||||
|
_ano = None if str(ano).upper() == "TODOS" else int(ano)
|
||||||
|
# mes y cuota pueden venir como lista separada por comas (multi-selección).
|
||||||
|
# None = TODOS (sin filtro).
|
||||||
|
_mes = None
|
||||||
|
if str(mes).upper() != "TODOS":
|
||||||
|
_mes = {int(x) for x in str(mes).split(",") if str(x).strip().isdigit()}
|
||||||
|
if not _mes: _mes = None
|
||||||
|
def _ncuo(v):
|
||||||
|
# Normaliza num_cuota a string entero: 0 / 0.0 / "0.0" -> "0".
|
||||||
|
s = str(v if v is not None else "").strip()
|
||||||
|
if s == "": return ""
|
||||||
|
try: return str(int(float(s)))
|
||||||
|
except Exception: return s
|
||||||
|
_cuo = None
|
||||||
|
if str(cuota).upper() != "TODOS":
|
||||||
|
_cuo = {_ncuo(x) for x in str(cuota).split(",") if str(x).strip() != ""}
|
||||||
|
if not _cuo: _cuo = None
|
||||||
|
|
||||||
|
facturas = dm.ejecutar_consulta_facturas_cobranza("ALL") or []
|
||||||
|
notas = dm.ejecutar_consulta_notascredito() or []
|
||||||
|
|
||||||
|
def _num(v):
|
||||||
|
try: return float(v) if v is not None else 0.0
|
||||||
|
except Exception: return 0.0
|
||||||
|
|
||||||
|
# ── Índice de facturas por num_comprobante: programa, matrícula, min cuota ──
|
||||||
|
# (para enlazar NC -> factura). Guardamos el primer programa/matrícula visto
|
||||||
|
# y el MIN(num_cuota) del comprobante (num_cuota_NC del Power BI).
|
||||||
|
fact_idx = {}
|
||||||
|
# Cuántas veces aparece cada num_comprobante (regla IMP_SUBTOTAL_FINAL).
|
||||||
|
comp_rep = {}
|
||||||
|
for f in facturas:
|
||||||
|
if str(f.get("cod_estado") or "").strip().upper() == "ANU": continue
|
||||||
|
nc = str(f.get("num_comprobante") or "").strip()
|
||||||
|
if nc:
|
||||||
|
comp_rep[nc] = comp_rep.get(nc, 0) + 1
|
||||||
|
g = fact_idx.setdefault(nc, {"prog": f.get("dsc_det_programa") or "",
|
||||||
|
"glosa": f.get("dsc_glosa") or "",
|
||||||
|
"matricula": str(f.get("num_matricula") or "").strip(),
|
||||||
|
"min_cuota": None})
|
||||||
|
cu = f.get("num_cuota")
|
||||||
|
if cu is not None:
|
||||||
|
try:
|
||||||
|
cu = int(cu)
|
||||||
|
if g["min_cuota"] is None or cu < g["min_cuota"]:
|
||||||
|
g["min_cuota"] = cu
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── acumulador: {(sede,tipo,programa,comprobante): {"ing":x, "nc":y}} ──
|
||||||
|
from collections import defaultdict
|
||||||
|
acc = defaultdict(lambda: {"ing": 0.0, "nc": 0.0})
|
||||||
|
|
||||||
|
# ── INGRESOS ──
|
||||||
|
# 1) Filtrar por año/mes (SIN cuota). El conteo de repeticiones del comprobante
|
||||||
|
# (regla IMP_SUBTOTAL_FINAL) se hace sobre ESTE conjunto, igual que el Power BI:
|
||||||
|
# el slicer de cuota NO afecta cuántas cuotas tiene realmente la factura.
|
||||||
|
fact_am = []
|
||||||
|
for f in facturas:
|
||||||
|
if str(f.get("cod_estado") or "").strip().upper() == "ANU": continue # excluir anuladas
|
||||||
|
a, m = _fact_ym(f.get("fch_emision"))
|
||||||
|
if _ano is not None and a != _ano: continue
|
||||||
|
if _mes is not None and m not in _mes: continue
|
||||||
|
fact_am.append(f)
|
||||||
|
# 2) Contar repeticiones de cada comprobante sobre año/mes (SIN filtro de cuota).
|
||||||
|
rep_filt = {}
|
||||||
|
for f in fact_am:
|
||||||
|
comp = str(f.get("num_comprobante") or "").strip()
|
||||||
|
if comp:
|
||||||
|
rep_filt[comp] = rep_filt.get(comp, 0) + 1
|
||||||
|
# 3) Aplicar el filtro de cuota solo para elegir QUÉ filas se suman.
|
||||||
|
fact_filt = fact_am if _cuo is None else [
|
||||||
|
f for f in fact_am if _ncuo(f.get("num_cuota")) in _cuo]
|
||||||
|
# 4) Calcular IMP_SUBTOTAL_FINAL por fila (de las cuotas que pasan el filtro).
|
||||||
|
for f in fact_filt:
|
||||||
|
prog = str(f.get("dsc_det_programa") or "").strip()
|
||||||
|
if not prog:
|
||||||
|
glosa = str(f.get("dsc_glosa") or "").strip() or "(SIN GLOSA)"
|
||||||
|
sede, tipo, pnom = "OTROS INGRESOS", "SIN CLASIFICAR", glosa
|
||||||
|
else:
|
||||||
|
sede = _clasif_sede_fact(dm, prog)
|
||||||
|
tipo = _clasif_filtro_programa(dm, prog)
|
||||||
|
pnom = prog
|
||||||
|
# único -> imp_subtotal; repetido -> imp_emitido
|
||||||
|
comp = str(f.get("num_comprobante") or "").strip()
|
||||||
|
base = _num(f.get("imp_subtotal")) if rep_filt.get(comp, 0) == 1 else _num(f.get("imp_emitido"))
|
||||||
|
if not base:
|
||||||
|
base = _num(f.get("imp_subtotal"))
|
||||||
|
if str(f.get("cod_moneda") or "").upper() != "SOL":
|
||||||
|
base *= _num(f.get("imp_tc"))
|
||||||
|
acc[(sede, tipo, pnom, comp or "(SIN COMPROBANTE)")]["ing"] += base
|
||||||
|
|
||||||
|
# ── NOTAS DE CRÉDITO ──
|
||||||
|
for n in notas:
|
||||||
|
a, m = _fact_ym(n.get("fch_emision"))
|
||||||
|
if _ano is not None and a != _ano: continue
|
||||||
|
if _mes is not None and m not in _mes: continue
|
||||||
|
comp_bf = str(n.get("num_comprobante_BF") or "").strip()
|
||||||
|
fi = fact_idx.get(comp_bf)
|
||||||
|
if not fi:
|
||||||
|
continue # NC sin factura enlazada -> no clasifica (como BLANK del PBI)
|
||||||
|
# El enlace NC->factura por num_comprobante_BF ya identifica la factura;
|
||||||
|
# de ahí se hereda programa y cuota (como el LOOKUPVALUE del Power BI).
|
||||||
|
# Filtro cuota: usa la cuota de la factura enlazada (num_cuota_NC = MIN)
|
||||||
|
if _cuo is not None and _ncuo(fi["min_cuota"]) not in _cuo:
|
||||||
|
continue
|
||||||
|
prog = str(fi["prog"] or "").strip()
|
||||||
|
if not prog:
|
||||||
|
glosa = str(fi.get("glosa") or "").strip() or "(SIN GLOSA)"
|
||||||
|
sede, tipo, pnom = "OTROS INGRESOS", "SIN CLASIFICAR", glosa
|
||||||
|
else:
|
||||||
|
sede = _clasif_sede_fact(dm, prog)
|
||||||
|
tipo = _clasif_filtro_programa(dm, prog)
|
||||||
|
pnom = prog
|
||||||
|
base = _num(n.get("imp_subtotal"))
|
||||||
|
if str(n.get("cod_moneda") or "").upper() != "SOL":
|
||||||
|
base *= _num(n.get("imp_tc"))
|
||||||
|
acc[(sede, tipo, pnom, comp_bf or "(SIN COMPROBANTE)")]["nc"] += base
|
||||||
|
|
||||||
|
# ── armar jerarquía Sede -> Tipo -> Programa -> Comprobante ──
|
||||||
|
def _n0(): return {"ing": 0.0, "nc": 0.0}
|
||||||
|
def _nprog(): return {"ing": 0.0, "nc": 0.0, "comps": defaultdict(_n0)}
|
||||||
|
por_sede = defaultdict(lambda: {"ing": 0.0, "nc": 0.0,
|
||||||
|
"tipos": defaultdict(lambda: {"ing": 0.0, "nc": 0.0, "progs": defaultdict(_nprog)})})
|
||||||
|
for (sede, tipo, pnom, comp), v in acc.items():
|
||||||
|
s = por_sede[sede]
|
||||||
|
s["ing"] += v["ing"]; s["nc"] += v["nc"]
|
||||||
|
t = s["tipos"][tipo]
|
||||||
|
t["ing"] += v["ing"]; t["nc"] += v["nc"]
|
||||||
|
p = t["progs"][pnom]
|
||||||
|
p["ing"] += v["ing"]; p["nc"] += v["nc"]
|
||||||
|
c = p["comps"][comp]
|
||||||
|
c["ing"] += v["ing"]; c["nc"] += v["nc"]
|
||||||
|
|
||||||
|
filas = []
|
||||||
|
for sede in sorted(por_sede.keys()):
|
||||||
|
s = por_sede[sede]
|
||||||
|
hijos = []
|
||||||
|
for tipo in sorted(s["tipos"].keys()):
|
||||||
|
t = s["tipos"][tipo]
|
||||||
|
progs = []
|
||||||
|
for pnom in sorted(t["progs"].keys()):
|
||||||
|
p = t["progs"][pnom]
|
||||||
|
comps = []
|
||||||
|
for comp in sorted(p["comps"].keys()):
|
||||||
|
cc = p["comps"][comp]
|
||||||
|
comps.append({"comprobante": comp, "ingresos": round(cc["ing"], 2),
|
||||||
|
"notas_credito": round(cc["nc"], 2),
|
||||||
|
"neto": round(cc["ing"] - cc["nc"], 2)})
|
||||||
|
progs.append({"programa": pnom, "ingresos": round(p["ing"], 2),
|
||||||
|
"notas_credito": round(p["nc"], 2),
|
||||||
|
"neto": round(p["ing"] - p["nc"], 2), "comprobantes": comps})
|
||||||
|
hijos.append({"tipo": tipo, "ingresos": round(t["ing"], 2),
|
||||||
|
"notas_credito": round(t["nc"], 2),
|
||||||
|
"neto": round(t["ing"] - t["nc"], 2), "programas": progs})
|
||||||
|
filas.append({"sede": sede, "ingresos": round(s["ing"], 2),
|
||||||
|
"notas_credito": round(s["nc"], 2),
|
||||||
|
"neto": round(s["ing"] - s["nc"], 2), "tipos": hijos})
|
||||||
|
|
||||||
|
tot = {"ingresos": round(sum(f["ingresos"] for f in filas), 2),
|
||||||
|
"notas_credito": round(sum(f["notas_credito"] for f in filas), 2),
|
||||||
|
"neto": round(sum(f["neto"] for f in filas), 2)}
|
||||||
|
return {"filas": filas, "total": tot}
|
||||||
|
|
||||||
|
|
||||||
|
def facturacion_cuotas():
|
||||||
|
"""Lista de num_cuota disponibles (para el filtro CUOTA)."""
|
||||||
|
dm = get_dm()
|
||||||
|
facturas = dm.ejecutar_consulta_facturas_cobranza("ALL") or []
|
||||||
|
def _ncuo(v):
|
||||||
|
s = str(v if v is not None else "").strip()
|
||||||
|
if s == "": return ""
|
||||||
|
try: return str(int(float(s)))
|
||||||
|
except Exception: return s
|
||||||
|
cuotas = set()
|
||||||
|
for f in facturas:
|
||||||
|
c = _ncuo(f.get("num_cuota"))
|
||||||
|
if c != "":
|
||||||
|
cuotas.add(c)
|
||||||
|
try:
|
||||||
|
orden = sorted(cuotas, key=lambda x: int(x))
|
||||||
|
except Exception:
|
||||||
|
orden = sorted(cuotas)
|
||||||
|
return {"cuotas": orden}
|
||||||
|
|
||||||
|
|
||||||
def ventas_detalle(vendedor, ano, mes, tipo_lista, sede="TODOS", programa="TODOS", excluir_ret=False):
|
def ventas_detalle(vendedor, ano, mes, tipo_lista, sede="TODOS", programa="TODOS", excluir_ret=False):
|
||||||
def _load():
|
def _load():
|
||||||
dm = get_dm()
|
dm = get_dm()
|
||||||
@@ -1222,8 +1443,9 @@ def roles_modulos_disponibles():
|
|||||||
para armar las casillas al crear un rol. Ordenados alfabéticamente."""
|
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 = {str(r.get("modulo", "")).strip() for r in (res.data or []) if r.get("modulo")}
|
||||||
return {"modulos": mods}
|
mods.add("facturacion") # módulo nuevo: siempre asignable aunque no esté en la tabla
|
||||||
|
return {"modulos": sorted(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.
|
||||||
@@ -1763,7 +1985,7 @@ def comisiones_export_excel(ano, mes):
|
|||||||
cd.font = total_font; cd.fill = total_fill; cd.border = border
|
cd.font = total_font; cd.fill = total_fill; cd.border = border
|
||||||
ce = ws.cell(row=rtot, column=5, value=round(inv1 + inv2, 2))
|
ce = ws.cell(row=rtot, column=5, value=round(inv1 + inv2, 2))
|
||||||
ce.font = total_font; ce.fill = total_fill; ce.border = border
|
ce.font = total_font; ce.fill = total_fill; ce.border = border
|
||||||
return rtot + 1, (bool(c1) or bool(c2))
|
return rtot + 1, (bool(c1) or bool(c2)), round(inv1 + inv2, 2)
|
||||||
|
|
||||||
for vend in vendedores:
|
for vend in vendedores:
|
||||||
try:
|
try:
|
||||||
@@ -1777,10 +1999,13 @@ def comisiones_export_excel(ano, mes):
|
|||||||
filas_pc = _aplicar_overrides_comisiones(list(filas_pc))
|
filas_pc = _aplicar_overrides_comisiones(list(filas_pc))
|
||||||
filas_pend = _aplicar_overrides_comisiones(list(filas_pend))
|
filas_pend = _aplicar_overrides_comisiones(list(filas_pend))
|
||||||
|
|
||||||
# Si el asesor NO tiene datos en ninguno de los dos bloques, NO crear su hoja.
|
# Si el asesor NO tiene datos en ningún bloque (ventas ni deudores), NO crear hoja.
|
||||||
c1_pc, c2_pc = _clasificar(filas_pc)
|
c1_pc, c2_pc = _clasificar(filas_pc)
|
||||||
c1_pp, c2_pp = _clasificar(filas_pend)
|
c1_pp, c2_pp = _clasificar(filas_pend)
|
||||||
if not (c1_pc or c2_pc or c1_pp or c2_pp):
|
_dm_raw = det.get("Deudores Mes", []) if isinstance(det, dict) else []
|
||||||
|
_da_raw = det.get("Deudores Anterior", []) if isinstance(det, dict) else []
|
||||||
|
_tiene_deudores = any(f and str(f[0]).strip().upper() != "TOTAL GENERAL" for f in (_dm_raw + _da_raw))
|
||||||
|
if not (c1_pc or c2_pc or c1_pp or c2_pp or _tiene_deudores):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Acumular para el RESUMEN (MES EN CURSO = Venta P.C; MES ANTERIOR = Pendientes)
|
# Acumular para el RESUMEN (MES EN CURSO = Venta P.C; MES ANTERIOR = Pendientes)
|
||||||
@@ -1800,9 +2025,34 @@ def comisiones_export_excel(ano, mes):
|
|||||||
ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = w
|
ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = w
|
||||||
|
|
||||||
# BLOQUE 1: MES EN CURSO (Venta P.C)
|
# BLOQUE 1: MES EN CURSO (Venta P.C)
|
||||||
prox, _ = _escribir_bloque(ws, 1, "MES EN CURSO", filas_pc)
|
prox, _, tot_cur = _escribir_bloque(ws, 1, "MES EN CURSO", filas_pc)
|
||||||
# BLOQUE 2: MESES PASADOS (Venta Pendientes) — 3 filas más abajo
|
# BLOQUE 2: MESES PASADOS (Venta Pendientes) — 3 filas más abajo
|
||||||
_escribir_bloque(ws, prox + 3, "MESES PASADOS", filas_pend)
|
prox2, _, tot_ant = _escribir_bloque(ws, prox + 3, "MESES PASADOS", filas_pend)
|
||||||
|
|
||||||
|
# TOTAL COMISIÓN = suma de los totales de los 2 bloques de arriba
|
||||||
|
rtc = prox2 + 1
|
||||||
|
c_tc = ws.cell(row=rtc, column=4, value="TOTAL COMISIÓN")
|
||||||
|
c_tc.font = Font(bold=True, size=12, color="1F4E79"); c_tc.fill = total_fill; c_tc.border = border
|
||||||
|
c_tcv = ws.cell(row=rtc, column=5, value=round(tot_cur + tot_ant, 2))
|
||||||
|
c_tcv.font = Font(bold=True, size=12); c_tcv.fill = total_fill; c_tcv.border = border
|
||||||
|
|
||||||
|
# Separador visual + título SALDO PENDIENTE antes de los deudores
|
||||||
|
r_sep = rtc + 3
|
||||||
|
c_sp = ws.cell(row=r_sep, column=1, value="SALDO PENDIENTE")
|
||||||
|
c_sp.font = Font(bold=True, size=13, color="FFFFFF")
|
||||||
|
c_sp.fill = PatternFill("solid", fgColor="992B2B")
|
||||||
|
for _c in range(1, 15):
|
||||||
|
cc = ws.cell(row=r_sep, column=_c)
|
||||||
|
if _c != 1:
|
||||||
|
cc.fill = PatternFill("solid", fgColor="992B2B")
|
||||||
|
|
||||||
|
# BLOQUES DE DEUDORES (debían al cierre del mes del filtro; solo ALU/PRE):
|
||||||
|
# - "NO PAGADOS — MES" : matriculados en el mes del filtro
|
||||||
|
# - "NO PAGADOS — ANTERIOR" : matriculados antes del filtro
|
||||||
|
filas_dm = _aplicar_overrides_comisiones(list(det.get("Deudores Mes", []) if isinstance(det, dict) else []))
|
||||||
|
filas_da = _aplicar_overrides_comisiones(list(det.get("Deudores Anterior", []) if isinstance(det, dict) else []))
|
||||||
|
prox3, _, _ = _escribir_bloque(ws, r_sep + 2, "NO PAGADOS — MES", filas_dm)
|
||||||
|
_escribir_bloque(ws, prox3 + 3, "NO PAGADOS — MESES ANTERIORES", filas_da)
|
||||||
|
|
||||||
# Agregar VENDEDORES MANUALES al final del RESUMEN: todo su monto va a
|
# Agregar VENDEDORES MANUALES al final del RESUMEN: todo su monto va a
|
||||||
# OTROS → MES EN CURSO. Agrupados por nombre de vendedor manual.
|
# OTROS → MES EN CURSO. Agrupados por nombre de vendedor manual.
|
||||||
|
|||||||
Reference in New Issue
Block a user