Продавать пакеты часов без надёжного остатка — значит работать вслепую: клиент оспаривает счёт, специалист не знает, сколько ещё осталось, а закрытие месяца превращается в археологию по таблицам. Модуль Contract Hours от NexTool решает это прямо в GLPI: засекает время в ленте заявки и списывает его с нужного договора. В этом руководстве показано, как его настроить, какие поля чаще всего путают и какая эксплуатационная ошибка останавливает всё выставление счетов.
Чего не даёт стандартный GLPI
Поле «Продолжительность» задачи (хранится в секундах в столбце actiontime) фиксирует время, но ничего не знает о договоре, квоте или остатке. Вы вводите минуты вручную, без таймера, без округления оплачиваемого времени и без ответа на единственный вопрос, который клиент задаёт каждый месяц: «сколько я уже израсходовал и сколько ещё осталось?». Contract Hours добавляет этот слой поверх встроенного поля, не заменяя его.
Как фиксируется время
Специалист управляет таймером прямо в ленте заявки:
- Start запускает отсчёт; плавающий значок остаётся видимым на всех страницах GLPI, пока идёт замер.
- Pause / Resume исключают паузы (совещание, обед), не теряя накопленного времени.
- Stop округляет время, создаёт приватную TicketTask и списывает время с привязанного договора.
При решении или закрытии заявки хук автоматически останавливает таймер (auto-stop). Именно эта деталь не даёт забытому таймеру работать всю ночь и раздувать расход.
Пошагово: настройка договора
- Установите и активируйте модуль в NexTool -> Модули -> Contract Hours. Установка создаёт таблицы, сводное представление (VIEW) и регистрирует три CronTask (billing, cleanup, reconcile).
- Задайте минимальное оплачиваемое время в настройках (по умолчанию 15 мин). Округление выполняется в два шага: сначала до кратного минимуму, затем до кратного 5 (для совместимости с
actiontimeв GLPI). - Создайте договор на вкладке «Договоры часов»: название, организация (связь 1:1), «Всего часов», день выставления счёта, периодичность и порог оповещения.
- Настройте критерии на вкладке «Условия»: группы и/или категории ITIL, по которым заявка «попадает» в этот договор.
- Настройте видимость на вкладке «Видимость»: пользователи, профили и группы, которые видят договор на панели клиента. Пустой список означает, что договор виден всем, у кого есть право на чтение.
Типичная ошибка здесь: поле «Всего часов» хранится в МИНУТАХ. Тот, кто вводит «40», имея в виду 40 часов, на самом деле создаёт договор на 40 минут и видит, как остаток уходит в минус уже на первой заявке. Для 40 часов укажите 2400.
Правило сопоставления — И, а не ИЛИ
Определение того, когда заявка относится к договору, — место, где мы чаще всего видим неправильную настройку. Логика сочетает организацию (всегда обязательна, 1:1) с необязательными критериями по группе и категории:
| Критерии в договоре | Применяемое правило | Заявка попадает в договор, когда |
|---|---|---|
| Нет | Только организация | Всегда (достаточно принадлежать организации договора) |
| Только группы | Принадлежность внутри типа | Одна из назначенных групп есть в списке договора |
| Только категории | Принадлежность внутри типа | Категория ITIL есть в правилах договора |
| Группы И категории | AND между типами | Группа в списке И категория в правилах (оба условия) |
Иными словами: если заполнены и группа, и категория, заявка попадает в договор, только когда совпадают оба критерия. Настройка обоих в расчёте на поведение «ИЛИ» — вот что отправляет половину заявок в карточку «Без договора».
actiontime — источник истины
Начиная с версии 3.4.2 модуль считает actiontime из TicketTask первичным источником. billable_time таймера поддерживается в синхронизации тремя уровнями: хук onTicketTaskUpdated (в реальном времени, когда вы редактируете задачу в стандартном GLPI), cron reconcile (каждые 4 ч) и кнопка «Пересчитать договоры». На практике изменение продолжительности задачи прямо в GLPI отражается в остатке договора — при условии, что cron работает.
Cron должен работать во внешнем режиме
Вот эксплуатационная ошибка, которая останавливает всё. Contract Hours зависит от трёх CronTask: billing (закрывает цикл и обнуляет расход), cleanup (закрывает «осиротевшие» таймеры) и reconcile (синхронизирует и заново сопоставляет). Они выполняются, только если автоматические действия GLPI работают во внешнем режиме (CLI). В режиме GLPI (запуск при веб-обращениях) среда с низким трафиком может днями не закрывать цикл выставления счетов, а забытые таймеры продолжают накапливать фантомные часы.
Убедитесь, что системный cron указывает на исполнитель автоматических действий GLPI:
# /etc/cron.d/glpi - fires GLPI automatic actions every minute (external mode)
# Adjust the path to your install (in GLPI 11 the document root is public/)
* * * * * www-data /usr/bin/php /var/www/glpi/front/cron.php >/dev/null 2>&1
Затем убедитесь, что задачи модуля запланированы, работают во внешнем режиме и недавно выполнялись:
-- State of the 3 Contract Hours CronTasks
SELECT name, frequency, mode, state, lastrun
FROM glpi_crontasks
WHERE itemtype = 'PluginNextoolContracthoursCron';
-- mode=2 (external/CLI), state=1 (scheduled) and a recent lastrun = healthy.
Диагностика расхождения остатка
При сопровождении клиентских сред картина повторяется: договор создан правильно, первые заявки списываются корректно, а через три недели остаток «замирает». Почти во всех случаях причина одна — автоматические действия работали в веб-режиме, а не во внешнем CLI. Без внешнего cron billing никогда не закрывает цикл, reconcile никогда не синхронизирует правки задач, а cleanup никогда не завершает «осиротевшие» таймеры тех, кто забыл нажать Stop в пятницу. Поэтому первый запрос, который мы выполняем при диагностике Contract Hours, — по glpi_crontasks, а не по остатку: если lastrun всех трёх задач не свежий, проблема не в модуле, а в инфраструктуре GLPI.
Убедившись в работе cron, второй подозреваемый — billable_time, рассинхронизированный с actiontime после задним числом внесённой правки. Измерьте расхождение, прежде чем вслепую запускать пересчёт:
-- Timers whose billed time drifted from the task actiontime (source of truth)
SELECT t.id, t.tickets_id,
t.billable_time/60 AS billed_min,
tt.actiontime/60 AS task_min,
(t.billable_time - tt.actiontime) AS diff_seconds
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;
Если расхождения есть, кнопка «Пересчитать договоры» (вкладка «Данные») запускает синхронизацию и пересчитывает расход. Чтобы проверить остаток, помните, что поля хранятся в минутах:
-- Balance per active contract (values in minutes, converted to hours)
SELECT name, entities_id,
total_hours/60 AS quota_h,
used_hours/60 AS used_h,
(total_hours - used_hours)/60 AS balance_h
FROM glpi_plugin_nextool_contracthours_contracts
WHERE is_active = 1
ORDER BY balance_h;
Аудит, оповещения и бесплатные работы
Каждая правка времени задним числом регистрируется — кто изменил, когда и каким было прежнее значение — на вкладке «Журналы». Это та прослеживаемость, за которую вы скажете спасибо, когда клиент оспорит внесённый час. Оповещения срабатывают по проценту расхода (порог по умолчанию 80%) и по двум состояниям договора: истёк (дата окончания в прошлом) и превышены часы. Для каждого состояния можно выбрать «ничего не делать», «оповещение» (жёлтое всплывающее окно, которое можно закрыть) или «блокировка». Важная тонкость: красная блокировка действует только при создании заявки через helpdesk (ServiceCatalog); уже открытая заявка никогда не блокируется, а лишь получает оповещение. Начиная с версии 3.6 есть также переключатель «не оплачивается» (бесплатно): время учитывается, но не списывается с остатка — удобно для работ, которые вы хотите зафиксировать, не уменьшая квоту клиента.
Превратить часы по заявкам в обоснованные счета — с остатком, выпиской и панелью, понятной клиенту, — именно эту задачу решает модуль Contract Hours плагина NexTool. Если вы сопровождаете GLPI и всё ещё закрываете месяц в таблице, свяжитесь с нами.
Проверено командой NexTool Solutions.