Objetivo: que el Sistema de Asistencia
Georeferenciado sea la fuente de verdad del registro en campo y
Odoo consuma esos datos en hr.attendance
para nómina, sin duplicar ni perder eventos. Conexión por
XML-RPC (JSON-RPC compatible) — tecnología nativa de
Odoo, sin costo.
flowchart LR
Eventos[attendance_events] --> Outbox[odoo_sync<br/>pending]
Outbox --> Worker[Worker async<br/>odoo_client.py]
Worker -->|create/write hr.attendance| Odoo[Odoo]
Worker -->|reintento backoff| Outbox
Odoo -->|cron fallback| API[GET /attendance/export]
Patrón outbox + idempotencia:
odoo_sync con status = pending.pending, llama a Odoo y marca sent (con
odoo_attendance_id) o error (con
last_error, reintentos con backoff).pending y se reintentan.hr.attendance
(check_in, check_out,
employee_id).asistencia_geo_odoo que
extiende hr.attendance con campos de trazabilidad:# asistencia_geo_odoo/models/hr_attendance.py
from odoo import models, fields
class HrAttendance(models.Model):
_inherit = "hr.attendance"
x_lat = fields.Float("Latitud")
x_lng = fields.Float("Longitud")
x_accuracy_m = fields.Float("Precisión (m)")
x_geofence_status = fields.Selection([
("inside", "Dentro"), ("outside", "Fuera"), ("not_required", "No aplica")])
x_mock_detected = fields.Boolean("Mock detectado")
x_root_jailbreak = fields.Boolean("Root/Jailbreak")
x_server_time = fields.Datetime("Hora del servidor")
x_source_event_id = fields.Char("ID evento origen (idempotencia)")| Sistema local | Odoo |
|---|---|
users.odoo_employee_id |
hr.employee.id |
attendance_events.event_type = check_in |
hr.attendance.check_in |
attendance_events.event_type = check_out |
hr.attendance.check_out |
attendance_events.server_time |
hr.attendance.check_in/check_out |
attendance_events.idempotency_key |
x_source_event_id (dedupe) |
backend/app/services/odoo_client.py (resumen del
flujo):
def push_check_in(event, employee):
odoo = odoorpc.ODOO(url, port=8069)
odoo.login(db, user, api_key)
attendance_id = odoo.env["hr.attendance"].create({
"employee_id": employee.odoo_employee_id,
"check_in": event.server_time.isoformat(),
"x_lat": event.lat, "x_lng": event.lng,
"x_geofence_status": event.geofence_status.value,
"x_mock_detected": event.mock_detected,
"x_root_jailbreak": event.root_jailbreak_detected,
"x_server_time": event.server_time.isoformat(),
"x_source_event_id": event.idempotency_key,
})
return attendance_idodoo.env["hr.attendance"].write(attendance_id, {"check_out": ...})
localizando la asistencia abierta por employee_id +
x_source_event_id/check_in.create, se
consulta por x_source_event_id; si existe, se
reutiliza.Para robustez, el módulo Odoo incluye un cron que, cada N minutos,
consulta
GET /api/v1/attendance/export?since=<last_sync> del
backend y crea/actualiza hr.attendance. Cubre escenarios
donde el worker saliente no pueda correr.
<!-- asistencia_geo_odoo/data/ir_cron_data.xml -->
<record id="cron_import_attendance" model="ir.cron">
<field name="name">Importar asistencia georreferenciada</field>
<field name="model_id" ref="model_hr_attendance"/>
<field name="interval_number">10</field>
<field name="interval_type">minutes</field>
<field name="code">model._cron_import_geo_attendance()</field>
</record>Variables de entorno del backend:
ODOO_URL=http://odoo.local
ODOO_PORT=8069
ODOO_DB=acme
ODOO_USER=attendance_bot
ODOO_API_KEY=xxxx
hr.attendance; rellenar
users.odoo_employee_id.odoo_client + outbox; sincronizar histórico con
backfill.asistencia_geo_odoo para ver geolocalización/mock en
Odoo.odoo_sync vs
hr.attendance.attempts contabilizado.status = error con
last_error y se visualizan en el panel.