Files
autoficher/tools/tn_inspect.py
omar 9294474066 feat: nucleo nuevo: cliente API, calendario laboral, planificador y jitter
Timenet recorta el tiempo a la ventana teorica del horario:

    trabajado = min(salida, fin_teorico) - max(entrada, inicio_teorico)

Verificado sobre 6 meses (104 dias laborables, 376 fichajes). La API guarda dos
horas por fichaje: HR (la que fichas) y H (la que cuenta). De 376 fichajes, 136
llevaban ajuste: todos salidas, todos recortados hacia atras. Ninguna entrada
ajustada jamas. Resultado: una entrada tardia se debita al minuto y una salida
tardia no acredita nada, asi que el jitter aleatorio positivo que habia perdia
unos 3,2 min/dia (-776 min acumulados, casi 13 horas).

Y acabar el mes en positivo es imposible: 0 dias con saldo positivo en 108, y
timeExtraHours=false en todos los tipos de jornada. El objetivo es 0 clavado.

- timenet_api: cliente limpio. Documenta que Tn-D y dStr son obligatorias (sin
  ellas el servidor responde 403 hasta en endpoints publicos).
- schedule: calendario laboral desde cp/calendar. Festivos, vacaciones y tramos
  horarios los da Timenet, no hace falta adivinarlos.
- planner + jitter: entradas siempre ANTES de la hora, salidas siempre DESPUES,
  con costumbre que deriva lenta y cola larga hacia el lado seguro. Invariantes
  que fallan en duro si una entrada se planifica tarde.
- jitter_geo: las coordenadas del historico son identicas al sexto decimal en
  los 376 fichajes; ahora se desplazan dentro de un disco.
- store: SQLite con plan diario, overrides, auditoria y estado del drift.
- settings: config sin el acoplamiento a argparse del config_parser original.

tools/dryrun.py simula el año entero: saldo +0 en 226 dias laborables, con
~3,5 min/dia de presencia extra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 18:51:33 +02:00

54 lines
2.2 KiB
Python

#!/usr/bin/env python3
"""Inspecciona el calendario laboral y el saldo tal y como los ve Timenet. Solo lectura."""
import os, sys
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import settings, schedule
from app.timenet_api import TimenetClient
def main():
cfg = settings.load()
cli = TimenetClient(cfg["api_url"], cfg["user"])
print("servidor Timenet: %s (cliente manda %s)" % (cli.version(), cli.wcp_version))
cli.login(settings.pin(cfg))
print("login OK\n")
cal = schedule.Calendar(cli.raw_calendar())
lo, hi = cal.span
print("=== TIPOS DE JORNADA (%d) ===" % len(cal.day_types))
for dt in sorted(cal.day_types.values(), key=lambda d: d.id):
flag = " <- laborable" if dt.is_workday else ""
warn = "" if (not dt.windows or dt.consistent) else " !! tramos no cuadran con minutos"
print(" %s%s%s" % (dt, flag, warn))
print("\ncalendario cargado: %s -> %s (%d dias)" % (lo, hi, len(cal.days)))
print("\n=== PROXIMOS 14 DIAS ===")
hoy = date.today()
for i in range(14):
d = hoy + timedelta(days=i)
dt = cal.day_type(d)
if cal.is_workday(d):
tramos = " ".join(str(w) for w in cal.windows(d))
margenes = ", ".join("ent-%d/sal+%d" % (w.entry_margin, w.exit_margin) for w in cal.windows(d))
print(" %s %-3s LABORABLE %-24s %4dmin margenes: %s" % (
d, d.strftime("%a"), tramos, dt.minutes, margenes))
else:
print(" %s %-3s --- %s" % (d, d.strftime("%a"), cal.why_not_workday(d)))
print("\n=== SALDO (cp/checks del mes en curso) ===")
first = hoy.replace(day=1)
nxt = (first + timedelta(days=32)).replace(day=1) - timedelta(days=1)
ck = cli.raw_checks(first.strftime("%d/%m/%Y"), nxt.strftime("%d/%m/%Y"))
print(" previsto mes P = %s min" % ck.get("P"))
print(" trabajado W = %s min" % ck.get("W"))
print(" saldo del mes X = %+s min" % ck.get("X"))
print(" saldo anterior XAnt= %+s min" % ck.get("XAnt"))
tot = (ck.get("XAnt") or 0) + (ck.get("X") or 0)
print(" SALDO ACUMULADO = %+d min (%+.2f h)" % (tot, tot / 60))
if __name__ == "__main__":
main()