Введение
В коробочной версии Битрикс24 учёт рабочего времени ведётся модулем timeman. Если сотрудник забыл закрыть рабочий день, система продолжает считать его открытым — в отчётах появляются переработки вида «24ч 0м», а функционал, завязанный на статус рабочего дня (например, распределение звонков телефонии), работает некорректно. Со временем сотрудник и вовсе перестаёт открывать день.
В этой статье разберём корректное программное закрытие рабочего дня через штатный метод CTimeManUser::CloseDay(), покажем подводные камни, с которыми приходится сталкиваться на практике, и расскажем, почему для регулярного запуска по расписанию лучше использовать агент Битрикса, а не cron-скрипт по URL.
Область применения
Задача автозакрытия рабочего дня возникает в следующих сценариях:
- Массовое закрытие дней по отделу в конце рабочего дня (например, в 21:00).
- Закрытие просроченных дней (статус
EXPIRED) у сотрудников, которые забыли завершить день. - Закрытие дней за прошлые даты при разборе инцидентов и корректировке учёта.
- Автоматизация учёта рабочего времени в распределённых командах.
Штатный метод CloseDay
За программное закрытие рабочего дня отвечает метод CTimeManUser::CloseDay() модуля timeman. Он выполняет весь цикл операций: фиксирует статус закрытия, рассчитывает длительность, проставляет метку завершения, снимает паузу, создаёт дневной отчёт.
Сигнатура метода
CloseDay($timestamp = false, $report = '', $bFieldsOnly = false)
Параметры
$timestamp— время закрытия. Передаётся не Unix-время, а wall-time — секунды от начала дня. Для 19:00:00 это19 * 3600 = 68400.$report— текст отчёта или причина закрытия.$bFieldsOnly— режим работы метода. Не отключает проверку прав.
Базовый пример
<?
require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
CModule::IncludeModule("timeman");
$userId = 371;
$seconds = 19 * 3600; // 19:00:00 в wall-time
$timeMan = new CTimeManUser($userId);
$result = $timeMan->CloseDay($seconds, "Автозакрытие", false);
Чтение данных о рабочем дне
Перед закрытием дня нужно прочитать последнюю запись. Есть два инструмента:
CTimeManEntry::GetLast($userId)— возвращает последнюю запись рабочего дня из таблицыb_timeman_entries. Это штатный метод, без прямых SQL-запросов.CTimeManUser::State()— возвращает текущее состояние:OPENED,CLOSED,PAUSED,EXPIRED.
Пример чтения записи
<?
CModule::IncludeModule("timeman");
$userId = 371;
$entry = CTimeManEntry::GetLast($userId);
if ($entry) {
echo "DATE_START: " . $entry['DATE_START'] . "\n";
echo "DATE_FINISH: " . ($entry['DATE_FINISH'] ?: "NULL") . "\n";
echo "CURRENT_STATUS: " . $entry['CURRENT_STATUS'] . "\n";
}
$timeMan = new CTimeManUser($userId);
echo "State(): " . $timeMan->State() . "\n";
Статусы рабочего дня
| Статус | Что означает |
|---|---|
OPENED |
Рабочий день открыт сегодня |
CLOSED |
Рабочий день закрыт |
PAUSED |
Рабочий день на паузе |
EXPIRED |
Рабочий день открыт более 24 часов назад и не закрыт |
Подводный камень №1: wall-time, а не Unix-timestamp
Самая частая ошибка — передать в CloseDay() абсолютный Unix-timestamp, полученный через strtotime() или MakeTimeStamp(). Метод ожидает секунды от начала дня, а не абсолютное время.
Если передать, например, 1790949600 (это 02.10.2026 19:00:00 в Unix-времени), внутри модуля произойдёт сложение этого значения с полем TIME_START записи, и получится строка вида 2026-10-02 497486:00:00. Метод упадёт с ошибкой:
[DateMalformedStringException] Failed to parse time string (2026-10-02 497486:00:00) at position 15 (8): Unexpected character (0)
Правильно — считать секунды от начала дня:
$closeTime = "19:00:00";
list($hours, $minutes, $secondsPart) = explode(":", $closeTime);
$seconds = $hours * 3600 + $minutes * 60 + $secondsPart; // 68400
Подводный камень №2: CloseDay работает только под авторизацией
Без прав администратора или пользователя с правом «Управление рабочим временем» метод молча возвращает false и ничего не пишет в базу. Исключение при этом может быть пустым — диагностировать причину сложно.
Проверено на практике:
- под авторизацией администратора
CloseDay()возвращает массив с полямиDATE_FINISH,TIME_FINISH,CURRENT_STATUS = 'CLOSED',DURATION,TIME_LEAKSи другими — день закрыт корректно; - без авторизации метод возвращает
false, в базе ничего не меняется.
Третий параметр $bFieldsOnly = false проверку прав не отключает. Он управляет только режимом работы метода.
Подводный камень №3: прямое обновление b_timeman_entries
Иногда вместо CloseDay() пытаются напрямую обновить запись в таблице b_timeman_entries:
UPDATE b_timeman_entries SET DATE_FINISH = '...', TIME_FINISH = ..., CURRENT_STATUS = 'CLOSED' WHERE ID = ...
Так делать не стоит. Учёт рабочего времени — это набор связанных данных: длительность, метки старта и остановки, признаки пауз, перерывы, отчёты. Прямое обновление одного поля без остальных приводит к рассогласованию. В интерфейсе «Рабочее время» день может отображаться как «не завершён», а CloseDay() для такой записи в дальнейшем вернёт «Невозможно выполнить действие».
Единственный корректный способ — штатный CloseDay().
Почему агент, а не cron-скрипт по URL
Рабочий день нужно закрывать регулярно — например, каждый день в 21:00. Первое, что приходит в голову, — повесить скрипт на cron и вызывать его по URL через curl.
У этого подхода есть существенный недостаток: скрипт становится доступен извне по URL. Любой, кто узнает адрес, может его дёрнуть. Защита строится только на секретном токене, который легко утекает — из логов, из переписки, из истории браузера. Если токен утёк, злоумышленник получает возможность выполнить скрипт с админскими правами.
Агент Битрикса лишён этого недостатка:
- агент выполняется внутри серверного процесса через
cron_events.php, недоступен извне по URL; - не нужен секретный токен и открытый эндпоинт;
- авторизация администратора выполняется в памяти процесса, без создания сессии в браузере;
- снаружи достучаться до агента невозможно — только через shell сервера.
Как работает агент
Агент срабатывает по cron раз в сутки в 21:00. Внутри агента:
- подключается модуль
timeman; - восстанавливается объект
$USER(при запуске на cron он удаляется системой) и выполняется$USER->Authorize(1)— временная авторизация администратора на время выполнения агента; - через
CUser::GetListвыбираются все активные сотрудники подразделения; - для каждого сотрудника читается последняя запись через
CTimeManEntry::GetLast($userId)и вызываетсяCloseDay(68400, "Автозакрытие", false); - результат по каждому сотруднику пишется в лог.
Важно: на cron $USER сбрасывается
Перед запуском агента система удаляет объект $USER, чтобы не было дыр в безопасности. Поэтому внутри агента его надо создать заново:
global $USER;
if (!is_object($USER)) {
$USER = new CUser;
}
$USER->Authorize(1); // ID пользователя с правом «Управление рабочим временем»
Код агента
<?
function AgentRabDen()
{
if (!CModule::IncludeModule('timeman')) {
return "AgentRabDen();";
}
if (!CModule::IncludeModule('iblock')) {
return "AgentRabDen();";
}
global $USER, $APPLICATION;
if (!is_object($USER)) {
$USER = new CUser;
}
$USER->Authorize(1);
$departmentId = 42;
$filter = Array(
"ACTIVE" => "Y",
"UF_DEPARTMENT" => $departmentId,
);
$params = Array(
"SELECT" => Array("UF_DEPARTMENT"),
);
$rsUsers = CUser::GetList(($sort = "id"), ($order = "asc"), $filter, $params);
$rsUsers->NavStart(500);
$seconds = 19 * 3600; // 19:00:00 в wall-time
while ($user = $rsUsers->Fetch()) {
$userId = intval($user['ID']);
$entry = CTimeManEntry::GetLast($userId);
if (!$entry) {
continue;
}
$APPLICATION->ResetException();
$timeMan = new CTimeManUser($userId);
$result = $timeMan->CloseDay($seconds, "Автозакрытие", false);
$exception = $APPLICATION->GetException();
$exceptionText = $exception ? $exception->GetString() : "нет";
file_put_contents(
__DIR__ . "/agent_log.txt",
date("Y-m-d H:i:s") . " | ID=$userId | CloseDay=" . (is_array($result) ? "OK" : "false")
. " | EXCEPTION=" . $exceptionText . "\n",
FILE_APPEND
);
}
return "AgentRabDen();"; // агент не удаляется
}
?>
Подключение функции агента
Агент в Битриксе вызывается по имени функции, поэтому перед регистрацией агента функцию нужно сделать доступной ядру. Для этого файл с функцией подключается в /local/php_interface/init.php:
require_once($_SERVER["DOCUMENT_ROOT"] . "/local/php_interface/agents/close_workday.php");
Вместо /local/php_interface/agents/close_workday.php укажите свой путь к файлу с функцией агента. Главное — чтобы файл подключался до того, как ядро начнёт искать функцию.
init.php выполняется один раз при загрузке страницы, а require_once кэширует подключение — накладных расходов на производительность почти нет. Без этой строки при запуске агента ядро не найдёт функцию и выдаст ошибку:
Fatal error: Call to undefined function AgentRabDen()
Убирать подключение из init.php можно только после удаления агента из списка или его отключения.
Регистрация агента
Агент регистрируется через CAgent::AddAgent() или через интерфейс «Настройки → Инструменты → Агенты». Важно указать первый запуск в 21:00:
CAgent::AddAgent(
"AgentRabDen();",
"main",
"N",
86400, // интервал — раз в сутки
date("d.m.Y 21:00:00"), // первый запуск — сегодня в 21:00
"Y",
"",
100
);
Функция должна возвращать своё имя (return "AgentRabDen();";), иначе агент удалится после первого выполнения.
Формат лога
Лог удобно вести, чтобы видеть результат по каждому сотруднику:
2026-10-03 14:47:01 | === ЗАКРЫТИЕ ДНЯ ПО ОТДЕЛУ 42 === ID=101 | Иванов Иван | ДО=CLOSED | CloseDay=OK | ПОСЛЕ=CLOSED | EXCEPTION=нет ID=102 | Петров Пётр | ДО=CLOSED | CloseDay=OK | ПОСЛЕ=CLOSED | EXCEPTION=нет ID=103 | Сидоров Сидор | ДО=OPENED | CloseDay=OK | ПОСЛЕ=CLOSED | EXCEPTION=нет ----------------------------------------
Особенности и рекомендации
-
Обязательное подключение модуля: перед вызовом любого метода необходим
CModule::IncludeModule("timeman"). -
Формат
DATE_START: методCTimeManEntry::GetLast()возвращает дату в формате порталаДД.ММ.ГГГГ ЧЧ:МИ:СС.strtotime()его разбирает корректно, аMakeTimeStamp()с константойFORMAT_DATETIME— нет. Для парсинга используйтеstrtotime(). -
Проверка исключений: перед
CloseDay()полезно вызывать$APPLICATION->ResetException(), а после —$APPLICATION->GetException(), чтобы поймать ошибку, если она есть. -
Повторный запуск безопасен: для уже закрытого дня
CloseDay()под авторизацией вернёт массив, но реального изменения в базе не будет. Для просроченного дня запись будет дополнена корректными полями. -
Не обновляйте
b_timeman_entriesнапрямую. Это ломает связи между полями и приводит к некорректному отображению в интерфейсе. - Один агент — один отдел: если отделов несколько, регистрируйте отдельные агенты или передавайте подразделение параметром.
- Первый запуск: через интерфейс «Настройки → Инструменты → Агенты» можно вручную запустить агент, не дожидаясь cron.
-
Логирование: файл лога удобно держать в том же каталоге, что и функцию агента. После отладки логи можно отключить, убрав
file_put_contents.
Заключение
Программное закрытие рабочего дня в коробочном Битрикс24 выполняется штатным методом CTimeManUser::CloseDay(). Ключевые моменты: передавать wall-time (секунды от начала дня), а не Unix-timestamp; выполнять под авторизацией администратора или пользователя с правом «Управление рабочим временем»; не обновлять b_timeman_entries напрямую. Для регулярного запуска по расписанию используйте агент Битрикса — он выполняется внутри серверного процесса, недоступен извне и не требует секретных токенов в URL.
