Saltar a contenido

Reglas de negocio — Control de Asistencia

Determinación de entrada y salida — group_by_day()

Para cada par (zk_user_id, fecha):

Campo Regla
Entrada (first_punch) Primer punch del día, a cualquier hora
Salida (last_punch) Primer punch con hora >= exit_hour (default 17)
Resto de punches Ignorados (almuerzo, salidas cortas, doble-tap)

Si no existe ningún punch >= exit_hour, entonces last_punch == first_punch y la asistencia queda abierta en Odoo (sin check_out). La siguiente corrida que encuentre un punch de salida corrige el registro, porque en merge_records() los registros nuevos siempre pisan a los pendientes.

Hora local

Los timestamps salen en hora local, tal cual los entrega el reloj. La conversión a UTC la hace el módulo zk.attendance.import del lado de Odoo.

Exclusión de empleados

Dos niveles, ambos por zk_user_id:

Config Alcance Efecto
[sync] exclude_zk_ids Global El registro nunca sale del script
[odoo.<n>] exclude_zk_ids Por instancia Se omite solo en esa empresa

Sirve para empleados que no existen en Odoo (evita reintentos infinitos por "empleado no encontrado") y para separar personal entre empresas cuando se sincroniza a más de una instancia.

Manejo de zonas horarias

Odoo almacena todo en UTC; el reloj guarda hora local. utc_to_local() y local_to_utc() aplican [sync] utc_offset_hours (Panamá = -5).

Limpieza previa en Odoo

Antes de enviar el payload, sync_to_odoo() ejecuta tres correcciones sobre hr.attendance, en este orden:

1. close_open_odoo_attendances()

Busca check_out = False con check_in < hoy — asistencias que quedaron abiertas porque el empleado no marcó salida o el servicio estaba caído.

  • Si check_in local < auto_close_hour → escribe check_out = fecha_checkin auto_close_hour:00, convertido a UTC.
  • Si check_in local >= auto_close_hour → es un punch de salida que Odoo registró como entrada: se elimina (unlink) en lugar de cerrarse.

2. delete_midnight_phantoms()

Odoo HR crea asistencias automáticas a las 00:00 locales (05:00 UTC en Panamá) para empleados activos con horario asignado, aunque no hayan marcado. Se eliminan todos los registros de hoy con check_in dentro de una ventana de ±10 min de esa hora.

3. open_phantom_attendances()

Asistencias de hoy con duración 0 <= dur < 5 min — doble-tap en la entrada que un ciclo anterior interpretó como entrada + salida. Se les borra el check_out (check_out = False) para que Odoo acepte la salida real cuando llegue.

Envío a Odoo

Transporte: XML-RPC, modelo zk.attendance.import, método import_punches.

Payload por registro:

{
  "zk_user_id":  "5",
  "date":        "2026-06-04",
  "first_punch": "2026-06-04 08:02:00",
  "last_punch":  "2026-06-04 17:03:00"
}

Respuesta esperada: { "created": n, "skipped": n, "errors": [...] }.

Semántica:

  • first_punch == last_punch → asistencia abierta (empleado aún en oficina).
  • first_punch != last_punch → jornada cerrada.
  • Idempotente por (employee_id, check_in): reenviar actualiza el check_out.

Requisito en Odoo

El empleado debe tener el campo zk_user_id poblado en hr.employee. Sin él, el registro cae en errors y queda en el unmatched_log.

Errores y reintentos

  1. Antes de enviar, el payload mezclado se persiste en data/pending_<odoo>.json.
  2. Si errors viene vacío → se limpia el archivo de pendientes.
  3. Si hay errores → los pendientes quedan en disco y se anexan al unmatched_log para revisión manual.
  4. save_pending() descarta automáticamente registros de días anteriores: los pendientes nunca se acumulan más allá del día en curso.
  5. Cada instancia de Odoo falla y reintenta de forma independiente.