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_inlocal< auto_close_hour→ escribecheck_out = fecha_checkin auto_close_hour:00, convertido a UTC. - Si
check_inlocal>= 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 elcheck_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¶
- Antes de enviar, el payload mezclado se persiste en
data/pending_<odoo>.json. - Si
errorsviene vacío → se limpia el archivo de pendientes. - Si hay errores → los pendientes quedan en disco y se anexan al
unmatched_logpara revisión manual. save_pending()descarta automáticamente registros de días anteriores: los pendientes nunca se acumulan más allá del día en curso.- Cada instancia de Odoo falla y reintenta de forma independiente.