Vender paquetes de horas sin un saldo fiable es operar a ciegas: el cliente cuestiona la factura, el técnico no sabe cuánto queda y el cierre de mes se convierte en arqueología de hoja de cálculo. El módulo Contract Hours de NexTool resuelve esto dentro del propio GLPI, cronometrando el tiempo en la línea de tiempo del ticket y descontándolo del contrato correcto. Esta guía muestra cómo configurarlo, qué campos generan más confusión y el error de operación que atasca toda la facturación.
Lo que GLPI nativo no entrega
El campo Duración de la tarea (almacenado en segundos en la columna actiontime) registra tiempo, pero no conoce contrato, cuota ni saldo. Tecleas los minutos a mano, sin cronómetro, sin redondeo facturable y sin responder la única pregunta que el cliente hace cada mes: "¿cuánto he usado y cuánto queda?". Contract Hours añade esa capa sobre el campo nativo, sin sustituirlo.
Cómo se captura el tiempo
El técnico controla un cronómetro directamente en la línea de tiempo del ticket:
- Start inicia el conteo; una insignia flotante permanece visible en todas las páginas de GLPI mientras haya una medición activa.
- Pause / Resume descuentan las pausas (reuniones, almuerzo) sin perder el tiempo acumulado.
- Stop redondea el tiempo, crea una TicketTask privada y descuenta el contrato vinculado.
Al resolver o cerrar el ticket, un hook detiene el cronómetro automáticamente (auto-stop). Es el detalle que evita que un cronómetro olvidado corra toda la noche e infle el consumo.
Paso a paso: configurar un contrato
- Instala y activa el módulo en NexTool -> Módulos -> Contract Hours. La instalación crea las tablas, la VIEW de resumen y registra tres CronTasks (billing, cleanup, reconcile).
- Define el mínimo facturable en Configuración (por defecto 15 min). El redondeo ocurre en dos pasos: primero al múltiplo del mínimo, luego a múltiplo de 5 (compatibilidad con el
actiontimede GLPI). - Crea el contrato en la pestaña Contratos de Horas: nombre, entidad (relación 1:1), Total de horas, día de facturación, periodicidad y umbral de alerta.
- Configura los criterios en la pestaña Condiciones: los grupos y/o categorías ITIL que hacen que un ticket "caiga" en ese contrato.
- Ajusta la visibilidad en la pestaña Visibilidad: usuarios, perfiles y grupos que ven el contrato en el panel del cliente. Una lista vacía significa visible para todos con permiso de lectura.
El error común aquí: el campo "Total de horas" se almacena en MINUTOS. Quien teclea "40" pensando en 40 horas crea, en realidad, un contrato de 40 minutos y ve el saldo reventar en el primer ticket. Para 40 horas, indica 2400.
La regla de resolución es Y, no O
Decidir cuándo un ticket pertenece a un contrato es donde más vemos mala configuración. La lógica combina la entidad (siempre obligatoria, 1:1) con criterios opcionales de grupo y categoría:
| Criterios en el contrato | Regla aplicada | El ticket entra en el contrato cuando |
|---|---|---|
| Ninguno | Solo la entidad | Siempre (basta con ser de la entidad del contrato) |
| Solo grupos | Pertenencia dentro del tipo | Un grupo asignado está en la lista del contrato |
| Solo categorías | Pertenencia dentro del tipo | La categoría ITIL está en las reglas del contrato |
| Grupos Y categorías | AND entre los tipos | Grupo en la lista Y categoría en las reglas (ambos) |
Es decir: con grupo Y categoría configurados, el ticket solo entra en el contrato si ambos coinciden. Configurar los dos esperando comportamiento "O" es lo que lanza la mitad de los tickets a la tarjeta "Sin Contrato".
actiontime es la fuente de verdad
Desde la versión 3.4.2 el módulo trata el actiontime de la TicketTask como fuente primaria. El billable_time del cronómetro se mantiene sincronizado por tres capas: el hook onTicketTaskUpdated (en tiempo real, cuando editas la tarea desde GLPI nativo), el cron reconcile (cada 4h) y el botón "Recalcular Contratos". En la práctica, editar la duración de la tarea directamente en GLPI se refleja en el saldo del contrato - siempre que el cron esté funcionando.
El cron debe estar en modo externo
Aquí está el error de operación que lo atasca todo. Contract Hours depende de tres CronTasks: billing (cierra el ciclo y reinicia el consumo), cleanup (cierra cronómetros huérfanos) y reconcile (sincroniza y vuelve a resolver). Solo se ejecutan si las acciones automáticas de GLPI están en modo externo (CLI). En modo GLPI (disparado por acceso web), un entorno de poco tráfico puede pasar días sin cerrar el ciclo de facturación, mientras los cronómetros olvidados suman horas fantasma.
Asegura el cron del sistema apuntando al ejecutor de acciones automáticas de GLPI:
# /etc/cron.d/glpi - dispara las acciones automaticas de GLPI cada minuto (modo externo)
# Ajusta la ruta segun la instalacion (en GLPI 11 el document root es public/)
* * * * * www-data /usr/bin/php /var/www/glpi/front/cron.php >/dev/null 2>&1
Luego confirma que las tareas del módulo están programadas, en modo externo y con ejecución reciente:
-- Estado de las 3 CronTasks de Contract Hours
SELECT name, frequency, mode, state, lastrun
FROM glpi_crontasks
WHERE itemtype = 'PluginNextoolContracthoursCron';
-- mode=2 (externo/CLI), state=1 (programada) y lastrun reciente = saludable.
Diagnóstico de saldo divergente
En el mantenimiento de entornos de clientes, el patrón se repite: el contrato se crea bien, los primeros tickets descuentan correctamente y, tres semanas después, el saldo "se congela". En casi todos los casos la causa es la misma: las acciones automáticas estaban en modo web, no en CLI externo. Sin el cron externo, billing nunca cierra el ciclo, reconcile nunca sincroniza las ediciones hechas en la tarea y cleanup nunca elimina los cronómetros huérfanos de quien olvidó pulsar Stop el viernes. Por eso la primera consulta que ejecutamos en un diagnóstico de Contract Hours es la de glpi_crontasks, no la del saldo: si el lastrun de las tres tareas no es reciente, el problema no es el módulo, es la infraestructura de GLPI.
Confirmado el cron, el segundo sospechoso es el billable_time desincronizado del actiontime tras una edición retroactiva. Mide la divergencia antes de recalcular a ciegas:
-- Cronometros cuyo tiempo facturado divergio del actiontime de la tarea (fuente de verdad)
SELECT t.id, t.tickets_id,
t.billable_time/60 AS min_facturado,
tt.actiontime/60 AS min_tarea,
(t.billable_time - tt.actiontime) AS diff_segundos
FROM glpi_plugin_nextool_contracthours_timers t
JOIN glpi_tickettasks tt ON tt.id = t.tickettasks_id
WHERE t.status = 'stopped'
AND t.billable_time != tt.actiontime
LIMIT 20;
Si hay divergencias, el botón "Recalcular Contratos" (pestaña Datos) ejecuta el sync y recalcula el consumo. Para comprobar el saldo, recordando que los campos están en minutos:
-- Saldo por contrato activo (valores en minutos, convertidos a horas)
SELECT name, entities_id,
total_hours/60 AS cuota_h,
used_hours/60 AS consumido_h,
(total_hours - used_hours)/60 AS saldo_h
FROM glpi_plugin_nextool_contracthours_contracts
WHERE is_active = 1
ORDER BY saldo_h;
Auditoría, alertas y cortesía
Toda edición retroactiva de tiempo queda registrada - quién la editó, cuándo y cuál era el valor anterior - en la pestaña Logs. Es la trazabilidad que agradeces cuando el cliente impugna una hora imputada. Las alertas se disparan por porcentaje de consumo (umbral por defecto 80%) y por dos condiciones del contrato: expirado (fecha final en el pasado) y horas excedidas. Cada condición puede configurarse como "no hacer nada", "alerta" (popup amarillo descartable) o "bloqueo". Aplica una regla de letra pequeña: el bloqueo rojo solo actúa en la creación de tickets por el servicio de asistencia (Service Catalog); un ticket ya abierto nunca se bloquea, solo recibe la alerta. Desde la versión 3.6 existe además el interruptor "no facturable" (cortesía): el tiempo se contabiliza pero no descuenta el saldo - útil para la atención que quieres registrar sin restar la cuota del cliente.
Convertir horas de ticket en facturación defendible - con saldo, extracto y un panel que el cliente entiende - es exactamente lo que resuelve el módulo Contract Hours del plugin NexTool. Si mantienes GLPI y aún cierras el mes en una hoja de cálculo, habla con nosotros.
Este contenido fue producido con ayuda de inteligencia artificial y revisado por el equipo de Nextool Solutions.