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>
This commit is contained in:
53
tools/tn_inspect.py
Normal file
53
tools/tn_inspect.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user