Saltar a contenido

Arquitectura — Control de Asistencia

Diagrama

graph TD
    Reloj[(ZKTeco F22 · LAN 4370)]
    Sched[[APScheduler · cada interval_minutes]] --> Run[run_sync]
    Run -->|1. fetch_punches por cada zk.*| Reloj
    Reloj -->|punches crudos desde medianoche| Group[2. group_by_day]
    Group -->|entrada / salida por empleado-día| Filtro[3. filtro exclude_zk_ids global]
    Filtro -->|all_new · payload común| Push[4. sync_to_odoo · 1 thread por instancia]
    Push -->|XML-RPC zk.attendance.import| Odoo1[(Odoo · instancia A)]
    Push -->|XML-RPC zk.attendance.import| Odoo2[(Odoo · instancia B)]
    Push --> WM[5. save_watermark por dispositivo]

Ciclo de una corrida

  1. fetch_punches() por cada sección [zk.<nombre>], desde la medianoche del día actual.
  2. group_by_day() reduce los punches a un par entrada/salida por (zk_user_id, fecha) — ver Reglas de negocio.
  3. Filtro global [sync] exclude_zk_ids: los IDs excluidos nunca salen del script.
  4. sync_to_odoo() por cada sección [odoo.<nombre>]: ejecuta la limpieza previa en Odoo y envía el payload.
  5. save_watermark() por dispositivo.

Componentes

Componente Responsabilidad
sync.py Servicio: scheduler, ciclo de vida y orquestación de la corrida
group_by_day() Determina entrada y salida de cada empleado-día
sync_to_odoo() Limpieza previa + envío por instancia de Odoo
push_to_odoo() Transporte XML-RPC a zk.attendance.import
merge_records() Mezcla pendientes con nuevos (los nuevos pisan a los pendientes)
utils/status.py Snapshot de conectividad, última sync, pendientes y errores
zk.attendance.import (Odoo) Recibe el payload y escribe en hr.attendance

Multi-dispositivo y multi-instancia

  • Cada sección [zk.<nombre>] es un reloj. Los punches de todos los relojes se agregan a un único payload compartido (all_new).
  • Cada sección [odoo.<nombre>] es una instancia destino. Con una sola instancia se ejecuta en línea; con dos o más se usa ThreadPoolExecutor, un worker por instancia.
  • La sección legacy [odoo] sin sufijo se lee con el nombre default.
  • Si un reloj falla al conectar, se registra el error y la corrida continúa con los demás dispositivos.
  • Una excepción dentro de sync_to_odoo() se registra pero no aborta las demás instancias: cada Odoo falla y reintenta de forma independiente.

Archivos de estado (data/)

Archivo Contenido
watermark_<device>.txt Timestamp del último punch procesado (%Y-%m-%d %H:%M:%S)
pending_<odoo>.json Registros del día que no llegaron a Odoo, para reintento

Al arrancar se aplican migraciones automáticas de esquemas previos: watermark.txtwatermark_<device>.txt y pending.jsonpending_<odoo>.json.

Si no existe watermark, load_watermark() devuelve hace 7 días — aunque en la práctica run_sync() no lo usa como punto de partida.

Ciclo de vida del proceso

  • Al arrancar: una corrida inmediata, luego BackgroundScheduler cada interval_minutes.
  • El hilo principal bloquea en stop_event.wait(), sin consumir CPU.
  • Señales de parada: SIGINT (Ctrl+C), SIGTERM (kill) y SIGBREAK (lo envía NSSM al detener el servicio en Windows). Todas ejecutan scheduler.shutdown(wait=False) y liberan el hilo principal.

Observabilidad

Fuente Contenido
log_file (logs/sync.log) Log completo, %(asctime)s [%(levelname)s] %(message)s, a archivo y stdout. Cada registro se loguea con entrada, salida y punches ignorados
unmatched_log (logs/unmatched.log) Solo errores devueltos por Odoo, con timestamp y etiqueta de instancia
logs/service.log stdout/stderr del proceso cuando corre como servicio de Windows
python utils/status.py Snapshot de conectividad, última sync, pendientes y últimos errores