Учёт часов по договору в GLPI с Contract Hours

Техническое руководство по настройке Contract Hours в GLPI: таймер в ленте заявки, договоры по организациям с правилом «группа И категория», округление оплачиваемого времени, остаток в минутах и внешний cron, без которого останавливается выставление счетов.

В этой статьеРазделов: 8

Продавать пакеты часов без надёжного остатка — значит работать вслепую: клиент оспаривает счёт, специалист не знает, сколько ещё осталось, а закрытие месяца превращается в археологию по таблицам. Модуль Contract Hours от NexTool решает это прямо в GLPI: засекает время в ленте заявки и списывает его с нужного договора. В этом руководстве показано, как его настроить, какие поля чаще всего путают и какая эксплуатационная ошибка останавливает всё выставление счетов.

Чего не даёт стандартный GLPI

Поле «Продолжительность» задачи (хранится в секундах в столбце actiontime) фиксирует время, но ничего не знает о договоре, квоте или остатке. Вы вводите минуты вручную, без таймера, без округления оплачиваемого времени и без ответа на единственный вопрос, который клиент задаёт каждый месяц: «сколько я уже израсходовал и сколько ещё осталось?». Contract Hours добавляет этот слой поверх встроенного поля, не заменяя его.

Как фиксируется время

Специалист управляет таймером прямо в ленте заявки:

  • Start запускает отсчёт; плавающий значок остаётся видимым на всех страницах GLPI, пока идёт замер.
  • Pause / Resume исключают паузы (совещание, обед), не теряя накопленного времени.
  • Stop округляет время, создаёт приватную TicketTask и списывает время с привязанного договора.

При решении или закрытии заявки хук автоматически останавливает таймер (auto-stop). Именно эта деталь не даёт забытому таймеру работать всю ночь и раздувать расход.

Пошагово: настройка договора

  1. Установите и активируйте модуль в NexTool -> Модули -> Contract Hours. Установка создаёт таблицы, сводное представление (VIEW) и регистрирует три CronTask (billing, cleanup, reconcile).
  2. Задайте минимальное оплачиваемое время в настройках (по умолчанию 15 мин). Округление выполняется в два шага: сначала до кратного минимуму, затем до кратного 5 (для совместимости с actiontime в GLPI).
  3. Создайте договор на вкладке «Договоры часов»: название, организация (связь 1:1), «Всего часов», день выставления счёта, периодичность и порог оповещения.
  4. Настройте критерии на вкладке «Условия»: группы и/или категории ITIL, по которым заявка «попадает» в этот договор.
  5. Настройте видимость на вкладке «Видимость»: пользователи, профили и группы, которые видят договор на панели клиента. Пустой список означает, что договор виден всем, у кого есть право на чтение.

Типичная ошибка здесь: поле «Всего часов» хранится в МИНУТАХ. Тот, кто вводит «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.

Часто задаваемые вопросы

В минутах. Для 40 часов укажите 2400. Это самая частая ошибка настройки: ввод «40» создаёт договор на 40 минут, и остаток уходит в минус уже на первой заявке.

Почти всегда потому, что автоматические действия GLPI не работают во внешнем режиме (CLI). CronTask billing, cleanup и reconcile не выполняются, цикл не закрывается, а правки задач не синхронизируются. Проверьте lastrun в glpi_crontasks и настройте системный cron на front/cron.php.

По организации (связь 1:1, обязательна) в сочетании с необязательными критериями по группе и категории. Если заполнены оба, действует логика И (AND): должны совпасть и назначенная группа, и категория. Если настроен только один критерий, достаточно его. Если критериев нет, в договор попадает любая заявка организации.

Да. actiontime задачи — источник истины; хук onTicketTaskUpdated синхронизирует billable_time в реальном времени, а cron reconcile каждые 4 ч исправляет любые расхождения. Если изменение не отразилось, нажмите «Пересчитать договоры» на вкладке «Данные».

Нет. Модуль работает только в GLPI 11. В GLPI 10 время можно учитывать встроенным полем продолжительности или с помощью ActualTime, но учёт остатка по договору требует перехода на GLPI 11.

Да, начиная с версии 3.6. Переключатель «не оплачивается» для задачи учитывает время, но не списывает его с остатка договора — удобно для бесплатных работ, которые вы хотите зафиксировать, не уменьшая квоту.

Нужна помощь?