События календаря Битрикс: что такое секция, событие и как они связаны
Календарь Битрикс24 активно используется в проектах автоматизации. При реализации разработчик сталкивается с документацией, где под словом «событие» скрываются разные сущности. Для работы через REST API доступны методы calendar.event.add, get и update, а для глубокой кастомизации в коробке предусмотрены события вроде OnAfterCalendarEventEdit или OnAfterCalendarEventUserFieldsUpdate. Важно не путать их с методами других модулей, имеющими схожие названия в API.
Дальше начинается путаница. В REST API «событие» — это запись в календаре (встреча, звонок, напоминание). В ядре D7 «событие» — это хук вида OnAfterCalendarEventEdit, на который вешают обработчик. А где-то рядом лежит CStatEvent::AddCurrent — и он вообще про статистику. Человек, который ищет, как поймать изменение встречи, уходит читать про модуль статистики и теряет полдня. Мы сами через это проходили, поэтому в статье сразу разводим понятия, чтобы дальше работать с методами и хуками без разночтений.
Разберём три сущности, между которыми чаще всего путаются.
Событие календаря — это конкретная запись: встреча, звонок, напоминание с датой, временем, участниками и описанием. Именно её создаёт calendar.event.add, читает calendar.event.get и обновляет calendar.event.update.
Событие ядра — это точка расширения в PHP-коде: OnAfterCalendarEventEdit, OnAfterCalendarEventDelete, OnAfterCalendarEventUserFieldsUpdate. Они стреляют, когда что-то произошло с записью, и позволяют навесить свою логику — уведомление, запись в лог, синхронизацию с внешней системой.
Чтобы разложить по полкам, держите матрицу соответствия:
| Сущность | Что это | REST-метод | Событие ядра |
|---|---|---|---|
| Секция календаря | Сам календарь-контейнер | calendar.section.* |
- |
| Событие | Запись: встреча, звонок | calendar.event.* |
OnAfterCalendarEventEdit |
| Пользовательские поля | Доп. атрибуты события | calendar.event.update |
OnAfterCalendarEventUserFieldsUpdate |
| CRM-связь | Привязка к сделке, лиду | calendar.event.add + UF_CRM_CAL_EVENT |
OnAfterCalendarEventEdit |
Подробнее: init.php обработчик формы.
Подробнее: массовое редактирование задач Битрикс24.
calendar.event.get Битрикс24: как получить список событий
Типичный сценарий: есть внешний сервис — таск-трекер, ERP или корпоративный портал — и нам нужно регулярно выгружать оттуда занятость сотрудников в Битрикс24 либо, наоборот, отдавать события календаря наружу. Другой частый случай — построить отчёт по загрузке отдела за период: сколько встреч, у кого перегруз, где пересечения. В обоих случаях нужен не один конкретный ивент, а список событий с фильтром, и здесь как раз работает calendar.event.get.
Метод возвращает массив событий. Каждое содержит ID и SECTION_ID — идентификатор календаря (секции), к которому событие привязано. Фильтровать можно по типу календаря (type: user, group, company), по владельцу (ownerId) и по диапазону дат (from, to). Комбинация этих трёх параметров и превращает метод из «вывали всё» в рабочий инструмент выборки.
getCalendarScope()->calendar()->event()->get(
[
'type' => 'user', // user | group | company
'ownerId' => 42, // ID сотрудника-владельца календаря
'from' => '2025-01-01',
'to' => '2025-01-31',
]
);
foreach ($result->getEvents() as $event) {
// $event->ID, $event->SECTION_ID, $event->NAME, $event->DATE_FROM, $event->DATE_TO
echo $event->ID . ' — ' . $event->NAME . PHP_EOL;
}При использовании метода calendar.event.get важно учитывать, что параметры from и to являются обязательными для корректной фильтрации списка событий. Согласно официальной документации, без указания этих дат запрос может завершиться ошибкой, так как период не задан. Мы рекомендуем устанавливать окно в пределах месяца-двух — этого достаточно для большинства задач по отчетности и синхронизации.
В ответе приходят SECTION_ID и ID — по ним дальше строится вся работа. Нужно полное описание одного события (участники, напоминания, UF-поля)? Не тяните весь список повторно: запросите конкретную запись через calendar.event.getbyid. Дешевле и по нагрузке, и по времени ответа.
getCalendarScope()->calendar()->event()->getById($eventId);
// $event->getEvent()->ID
// $event->getEvent()->NAME
// $event->getEvent()->DATE_FROM
// $event->getEvent()->ATTENDEES_CODES — список участниковПро пагинацию: calendar.event.get отдаёт данные порциями, и если вы запрашиваете период в несколько месяцев по всем сотрудникам сразу, готовьтесь к нескольким последовательным вызовам. Тянуть «весь год целиком» в одном запросе не стоит даже там, где технически проходит. Получите долгий ответ, риск таймаута и лишнюю нагрузку на MySQL — под капотом джойнятся таблицы событий, участников и секций.
Не запрашивайте список событий без фильтра по датам. На портале с активной командой это сотни и тысячи записей в одном ответе: PHP-скрипт упирается в max_execution_time, а MySQL получает full scan по таблице b_calendar_event. Обходится просто — всегда передавайте from и to, а длинные периоды разбивайте на месячные окна.
Создать событие календаря Битрикс24: calendar.event.add и calendar.section.add
Самый частый запрос на эту тему — автоматическая запись события без участия человека. Менеджер закрывает сделку — в календаре ответственного появляется встреча с клиентом. Задача переходит в статус «в работе» — у исполнителя встаёт напоминание на завтра. Внешняя система бронирования отправляет webhook — Битрикс24 заводит занятость сотрудника. Во всех трёх сценариях мы работаем парой методов REST: calendar.section.add создаёт сам календарь, calendar.event.add кладёт в него событие.
До первого вызова стоит разобраться с двумя вещами. Первая — секция: это и есть календарь как сущность (личный, групповой, компании), а не папка внутри одного календаря. Событие без секции не существует, поэтому создавать надо в правильном порядке. Вторая — от чьего имени выполняется вызов: права на запись в личный календарь другого пользователя зависят от контекста, и об этом мы скажем ниже отдельно. Если эти два момента учтены, дальше всё сводится к корректному заполнению полей.
Полученный $sectionId — это тот самый идентификатор, который потом пойдёт в поле sectionId при создании события. Если секция уже есть (например, дефолтный личный календарь пользователя), её можно получить через calendar.section.get и не плодить дубликаты.
post(
'https://your-portal.bitrix24.ru/rest/1/xxxxxxxxxxxx/calendar.event.add.json',
[
'type' => 'user',
'ownerId' => 42,
'sectionId' => $sectionId,
'name' => 'Созвон по сделке #1234',
'description' => 'Обсудить условия и сроки поставки',
'from' => '2025-06-15T10:00:00+03:00',
'to' => '2025-06-15T11:00:00+03:00',
'attendees' => [42, 57], // участники события
'UF_CRM_CAL_EVENT' => ['DEAL_1234'], // связь с CRM (24.600.0+)
]
);Про «запускать от имени» — это не поле в запросе, а выбор токена. Если вызов идёт вебхуком от администратора, событие создастся в календаре админа, а не того пользователя, которому оно предназначено. Поэтому в проектах мы либо берём OAuth-токен конкретного сотрудника, либо явно передаём ownerId и sectionId целевого пользователя. По опыту подрядчиков, отсутствие этого разделения — источник большинства ошибок прав доступа: запрос формально проходит, но событие падает «не туда».
Синхронизация календаря Битрикс24: REST, Exchange ActiveSync или CSV
Типичный сценарий: у холдинга два портала Битрикс24 — головная компания и производство, и встречи из календаря директора должны быть видны обеим командам. Или другой расклад: сотрудники работают в Битрикс24, а часть руководства принципиально живёт в Google Calendar, и расписание нужно держать синхронным в обе стороны. Третий частый случай — связка с 1С: графики отпусков и командировок приходят из учётной системы, а в портале должны превращаться в события для ответственных.
Во всех трёх задачах вопрос не «какой метод вызвать», а какой механизм синхронизации выбрать. От этого зависит, будут ли события обновляться в реальном времени, кто отвечает за конфликты при одновременной правке, и что произойдёт, когда внешний сервис изменит формат данных. У нас в практике выбор почти всегда сводится к четырём вариантам. Один из них мы сразу отсекаем.
| Подход | Когда подходит | Минусы |
|---|---|---|
| REST API | Обмен между двумя Битрикс24, кастомные сценарии | Нужен токен и обработка ошибок |
| Exchange ActiveSync | Связка с MS Exchange / Outlook | Ограничения зависят от модуля |
| CSV | Разовая выгрузка или миграция | Не годится для постоянного обмена |
| Прямой доступ к БД | Не рекомендуем | Обходим события ядра, ломаем кеш |
Теперь про последний пункт подробнее — именно к нему чаще всего тянет, когда REST кажется «медленным».
Прямой доступ к БД соблазняет простотой: написал UPDATE в таблицу событий — и вроде бы всё синхронизировалось. На деле мы обходим весь слой логики, который Битрикс навесил поверх хранения. События ядра вроде OnAfterCalendarEventEdit не сработают — а на них у вас, скорее всего, уже висят уведомления, пересчёт занятости и интеграции с CRM. Кеш календаря останется со старыми данными, и пользователь увидит не то, что лежит в таблице. А при одновременной записи из двух процессов легко получить нарушение целостности — например, событие без валидной привязки к секции.
Оправдан прямой доступ ровно в одном сценарии: разовая миграция, когда нужно перенести большой объём исторических данных, а после переноса вы всё равно сбрасываете кеш и переиндексируете. И то — если объём небольшой, дешевле прогнать через REST-методы и не рисковать.
Rest api календарь Битрикс24: callMethod, callBatch и BX.Promise
Есть класс задач, где серверный PHP-обработчик бесполезен: интерфейс живёт прямо в браузере. Виджет в карточке сделки, который подтягивает ближайшие встречи клиента; свой планировщик поверх календаря отдела; кнопка «забронировать слот» в публичной форме. Логика уже на JS, а данные лежат в Битрикс24 — значит, нужен локальный REST-клиент, который умеет дёргать методы календаря прямо из фронта.
В проектах с кастомными интерфейсами мы почти всегда опираемся на три вещи: BX24.callMethod, BX24.callBatch и BX.Promise. Первый делает одиночный вызов, второй упаковывает несколько методов в один HTTP-запрос, третий даёт нормальную асинхронную работу вместо вложенных колбэков. Комбинируются они свободно: можно собрать цепочку из одиночных вызовов, а можно отправить пачку и разобрать её результат по ключам. Ниже — рабочие заготовки, которые мы обычно копируем в проект и адаптируем под поля конкретного события.
BX24.callMethod(
'calendar.event.add', {
type: 'user',
ownerId: BX24.getAuth().user_id,
name: 'Встреча по сделке',
from: '2025-06-10T10:00:00+03:00',
to: '2025-06-10T11:00:00+03:00',
section: 1, // замените на ID секции из calendar.section.get
description: 'Обсуждение условий поставки'
},
function(result) {
if (result.error()) {
console.error(result.error().ex.error_description);
return;
}
const eventId = result.data().id;
console.log('Событие создано, ID:', eventId);
}
);const calls = [
['calendar.event.add', {
type: 'user',
ownerId: 12,
name: 'Слот 1',
from: '2025-06-11T09:00:00+03:00',
to: '2025-06-11T10:00:00+03:00',
section: 1
}],
['calendar.event.add', {
type: 'user',
ownerId: 12,
name: 'Слот 2',
from: '2025-06-11T10:00:00+03:00',
to: '2025-06-11T11:00:00+03:00',
section: 1
}],
['calendar.event.add', {
type: 'user',
ownerId: 12,
name: 'Слот 3',
from: '2025-06-11T11:00:00+03:00',
to: '2025-06-11T12:00:00+03:00',
section: 1
}]
];
BX24.callBatch(calls, function(batchResult) {
// batchResult — объект, где ключ = имя метода, значение = результат вызова
Object.keys(batchResult).forEach(function(key) {
const res = batchResult[key];
if (res.error()) {
console.warn(key, 'упал:', res.error().ex.error_description);
} else {
console.log(key, 'создано с ID', res.data().id);
}
});
}, false); // false — не останавливать пакет при первой ошибкеКлючевое отличие callMethod от callBatch — количество HTTP-запросов. Одиночный вызов на каждое событие в цикле из десяти штук даст десять round-trip'ов к порталу, и на медленном канале это заметно. Batch упаковывает всё в один запрос и возвращает массив результатов.
Но экономия реальна только когда методов много и они независимы. Если второй вызов зависит от ID, полученного в первом, batch не поможет, здесь нужна цепочка промисов.
При использовании локального REST-клиента методы callMethod и callBatch поддерживают синхронизацию через BX.Promise , если не указывать callback-функцию. Это позволяет выстраивать цепочки .then() и обрабатывать ошибки в едином .catch() . В callBatch с параметром halt: false выполнение пакета не прерывается при ошибке в одном из запросов, что критично при массовом создании событий в календаре.
Событие OnAfterCalendarEventDelete: как поймать удаление и не сломать данные
Удаление события в календаре почти никогда не бывает «просто удалением». В реальных проектах с ним связаны задачи, сделки, напоминания, внешние брони. И когда пользователь нажимает «Удалить», эти связи остаются висеть в воздухе. OnAfterCalendarEventDelete — единственная точка, где ядро сообщает нам: событие уже ушло, можно подчищать хвосты. Событие доступно с версии 11.5.6 и вызывается из CCalendarEvent::Delete.
Типовые задачи, которые мы вешаем на этот хендлер:
- снять связанные задачи или отменить напоминания, привязанные к событию;
- обновить статус сделки в CRM, если встреча была этапом воронки;
- отправить уведомление внешней системе (Exchange, Google Calendar, самописный бэкенд);
- записать факт удаления в лог — для аудита и разбора инцидентов.
Без обработчика все эти действия приходится делать вручную или через REST-обёртку. Ненадёжно: событие может удалиться и через интерфейс, и через API, и через синхронизацию.
<?php
use Bitrix\Main\EventManager;
$eventManager = EventManager::getInstance();
$eventManager->addEventHandler(
'calendar',
'OnAfterCalendarEventDelete',
'onCalendarEventDeleted'
);
function onCalendarEventDeleted($eventId, $arParams = [])
{
// $eventId — ID удалённого события
// $arParams — контекст: SECTION_ID, OWNER_ID, TYPE и т.п.
if (!$eventId) {
return;
}
// Пример: снять флаг «встреча запланирована» у сделки
$dealId = (int)($arParams['UF_CRM_CAL_EVENT'] ?? 0);
if ($dealId > 0) {
\CCrmDeal::Update($dealId, ['STAGE_ID' => 'NEW']);
}
// Записать в лог — для аудита
\Bitrix\Main\Diag\Debug::writeToFile(
['event_id' => $eventId, 'ts' => time()],
'calendar_delete',
'/local/logs/calendar.log'
);
}Разберём, что приходит в аргументах. Первый параметр — $eventId, идентификатор уже удалённой записи. Данных из БД по этому ID больше нет — если нужно знать SECTION_ID, владельца или привязку к CRM, их надо передавать в $arParams или хранить в собственном индексе заранее. Мы в проектах с интеграцией с CRM обычно держим отдельную таблицу соответствий «event_id → deal_id», потому что после удаления уже не восстановить связь.
Второй момент — идемпотентность. Если откат связанных сущностей упадёт на середине, повторный запуск хендлера (например, при откате транзакции в другом модуле) не должен ломать данные. Проверяйте статус перед обновлением: «уже отменено — пропускаем». И не пытайтесь удалять событие повторно — его нет.
Если связанных сущностей несколько, безопаснее делать откат через отдельный агент с задержкой, а не внутри хендлера. Так вы развяжете транзакцию удаления календаря и собственную логику.
Грабля: Обработчик OnAfterCalendarEventDelete (доступен с версии 11.5.6) вызывается уже после фактического удаления события из базы данных. Использование throw в этом хендлере не откатит транзакцию удаления в стандартном Bitrix Framework, однако может привести к прерыванию выполнения скрипта и ошибкам в UI. Если вам необходимо выполнить дополнительные действия (например, очистку связанных данных), логируйте ошибки, но не блокируйте завершение хита. Для критичных операций, требующих гарантированного исполнения без риска для интерфейса, рекомендуется использовать агенты или асинхронную обработку вне контекста удаления события.
OnAfterCalendarEventEdit и OnAfterCalendarEventUserFieldsUpdate: ловим изменения
Самый частый сценарий, где всплывает OnAfterCalendarEventEdit: у компании есть внешняя система — CRM, ERP, таск-трекер — и встречи из календаря Битрикс24 должны в ней отражаться. Пользователь перенёс совещание, поменял место, добавил участника — внешняя система обязана узнать об этом. Второй сценарий — аудит: кто и когда правил событие, какие поля менялись. Третий — пересчёт связанных сущностей: событие привязано к сделке или задаче, и после правки времени нужно пересобрать напоминания.
Все три задачи решаются одним способом: повесить обработчик на событие ядра и не трогать логику редактирования напрямую.
OnAfterCalendarEventEdit доступно с версии 12.0.2 — это основной хук на изменение самого события. OnAfterCalendarEventUserFieldsUpdate появилось позже и стреляет только тогда, когда обновляются пользовательские поля через CCalendarEvent::UpdateUserFields. У нас в проектах оба хендлера регистрируются в паре — иначе часть правок пройдёт мимо синхронизации.
<?php
// init.php — регистрируем оба хендлера
use Bitrix\Main\EventManager;
$eventManager = EventManager::getInstance();
// Изменение самого события (перенос, смена названия, участников)
$eventManager->addEventHandler(
'calendar',
'OnAfterCalendarEventEdit',
['CalendarSyncHandler', 'onEventEdit']
);
// Изменение пользовательских полей события (UF_*)
$eventManager->addEventHandler(
'calendar',
'OnAfterCalendarEventUserFieldsUpdate',
['CalendarSyncHandler', 'onUserFieldsUpdate']
);
class CalendarSyncHandler
{
/**
* @param int $eventId ID события
* @param array $arFields изменённые поля события
* @param array $arParams параметры вызова
*/
public static function onEventEdit($eventId, $arFields, $arParams)
{
// Забираем актуальное состояние события
$event = CCalendarEvent::GetById($eventId);
if (!$event) {
return;
}
// Отправляем во внешнюю систему
ExternalCalendarApi::pushEvent([
'id' => $event['ID'],
'name' => $event['NAME'],
'from' => $event['DATE_FROM'],
'to' => $event['DATE_TO'],
'section_id' => $event['SECTION_ID'],
]);
}
public static function onUserFieldsUpdate($eventId, $arFields)
{
// Здесь только UF_*-поля — например, UF_CRM_CAL_EVENT
ExternalCalendarApi::pushUserFields($eventId, $arFields);
}
}Ключевое отличие видно уже из сигнатур: Edit ловит изменение самого события — название, время, место, участников, привязку к секции. UserFieldsUpdate ловит только пользовательские поля — те, что начинаются с UF_. Это два разных потока данных внутри одного и того же редактирования.
Почему нельзя заменить один хендлер другим: CCalendarEvent::Update не дёргает UpdateUserFields автоматически, а UpdateUserFields не вызывает OnAfterCalendarEventEdit. Это разные методы с разными событиями. В проектах, где мы видим только один зарегистрированный хендлер, регулярно всплывает баг: менеджер меняет привязку события к сделке через UF-поле, а внешняя система об этом не узнаёт — потому что слушают только Edit.
Второй момент: оба события стреляют после сохранения в БД, поэтому внутри обработчика можно смело читать актуальное состояние через CCalendarEvent::GetById. Менять что-то в самом событии здесь уже поздно — для этого есть OnBeforeCalendarEventEdit, о котором мы говорили выше.
OnRestServiceBuildDescription: свой метод в REST API календаря
Иногда штатных методов calendar.event.* не хватает. Триггеры, которые мы регулярно видим: нужна сложная фильтрация по нескольким секциям сразу, агрегация занятости команды за период в одном вызове, или кастомная бизнес-логика, которую нельзя вынести на клиент — например, проверка пересечений с внешней ERP. Штатный REST в такой ситуации молчит: calendar.event.get отдаёт плоский список, а собрать из него сводку на фронте — это три-четыре запроса и ручная склейка.
Решение — расширить REST собственным методом через событие OnRestServiceBuildDescription. Это штатный способ добавить метод в REST API без правки ядра: вы объявляете свой scope, свой метод и обработчик, а Битрикс сам подхватывает их при построении описания сервиса. Никаких хардкод-патчей в /bitrix/modules/rest/, никаких «заплаток», которые отвалятся при первом обновлении.
<?php
// init.php или module.php вашего модуля
use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;
Loader::includeModule('rest');
EventManager::getInstance()->addEventHandler(
'rest',
'OnRestServiceBuildDescription',
['CalendarRest', 'onBuildDescription']
);
class CalendarRest
{
public static function onBuildDescription(): array
{
return [
'calendar' => [
'calendar.team.busyness' => [
'callback' => [__CLASS__, 'getTeamBusyness'],
'options' => [],
],
],
];
}
public static function getTeamBusyness(array $query, int $nav, $userId): array
{
// $query — параметры вызова, $userId — кто вызвал
$sectionId = (int)($query['sectionId'] ?? 0);
$from = $query['from'] ?? date('c');
$to = $query['to'] ?? date('c', strtotime('+7 days'));
$result = [];
$events = \CCalendarEvent::GetList(
['arFilter' => [
'SECTION_ID' => $sectionId,
'FROM' => $from,
'TO' => $to,
]],
false, false, [], false
);
foreach ($events as $event) {
$result[] = [
'id' => (int)$event['ID'],
'name' => $event['NAME'],
'from' => $event['DATE_FROM'],
'to' => $event['DATE_TO'],
];
}
return ['items' => $result];
}
}Ключевое здесь — структура массива, который возвращает onBuildDescription. Верхний уровень — имя scope (calendar), внутри — имя метода, callback на статический метод-обработчик и зарезервированный options. Обработчик получает три аргумента: массив параметров вызова, навигацию и ID пользователя, от имени которого выполняется запрос. По нему вы и проверяете права через \Bitrix\Rest\AccessException или через свои проверки секции.
Что читатель захочет поменять под свой проект: имя scope — если делаете отдельный модуль, заводите свой (mycompany.calendar), чтобы не конфликтовать с апдейтами. Имя метода — по конвенции entity.action. Проверку прав — вынести в отдельный приватный метод и вызывать из каждого обработчика. Если методов несколько — объявляйте их массивом в одном OnRestServiceBuildDescription, не плодите обработчики.
Почему это правильнее хака ядра: обновления модуля rest не затрут ваш код, метод виден в rest.methods и в документации портала, а скоуп корректно проходит через OAuth-авторизацию приложений. Хак с прямым дописыванием в rest/methods.php ломается на первом же обновлении и не проходит проверку при публикации в Маркетплейс.
Где разместить обработчик календаря: init.php, модуль или агент
Распространённая ошибка: зарегистрировать обработчик OnAfterCalendarEventEdit прямо в файле компонента или в шаблоне, где он «вроде бы нужен». Работает — пока страница открывается. Стоит событию прийти из REST, из мобильного приложения или из агента, и хендлер не срабатывает: компонент не подключён, init.php шаблона не выполнился. Мы регулярно вычищаем такие «полурабочие» интеграции в проектах после предыдущих подрядчиков.
Место регистрации определяет три вещи одновременно:
- Порядок загрузки — успеет ли класс события быть доступен к моменту вызова.
- Доступность на всех хитах — сработает ли хендлер при REST-запросе, агенте, cron, а не только в браузере.
- Риск при обновлении — переживёт ли код обновление ядра и не потеряется ли при деплое.
Разберём варианты по возрастанию «правильности»:
init.phpв/local/php_interface/— плюсы: загружается на каждом хите, прост в отладке, не требует модуля. Минусы: код вне структуры, тяжело версионировать отдельно, при переезде на другой портал легко забыть.- Свой модуль — плюсы: изолированная логика, свой
module.php, обновляемость, чистое подключение черезEventManager. Минусы: нужна обвязка модуля, чуть больше порога входа. - Агент — плюсы: асинхронность, разгрузка основного хита. Минусы: не для событий — агент не ловит
OnAfterCalendarEventDelete, он для отложенной обработки. - Компонент — плюсы: локальная логика вывода. Минусы: не годится для хендлеров — компонент живёт только на своей странице.
- REST-приложение — плюсы: изоляция от ядра, обновляется независимо. Минусы: не подходит для событий ядра, только для методов через
OnRestServiceBuildDescription.
Наша рекомендация такая. Критичные хендлеры — только в модуле: те, от которых зависит целостность данных (синхронизация с CRM, внешние брони, каскадное удаление связанных сущностей). Модуль гарантирует, что обработчик поднимется на любом хите, переживёт обновление и его можно отключить одной строкой.
Разовые и отладочные — в init.php: логирование, быстрый тест, прототип интеграции. У нас в практике init.php — это песочница, из которой рабочий код переезжает в модуль.
Чего делать точно не стоит — править bitrix/modules/. Любое обновление ядра затрёт ваши изменения, а восстановить их без бэкапа не получится. Если очень нужно вмешаться в чужой модуль — используйте EventManager с приоритетом и подпиской на его события, а не редактирование файлов вендора.
Самые частые ошибки при работе с календарём Битрикс24
За годы работы с календарём Битрикс24 у нас сложилась устойчивая коллекция грабель — тех, что встречаются в проектах снова и снова, независимо от размера компании и стека интеграции. Чаще всего они не про код как таковой, а про понимание того, как устроена модель доступа и кеширования. Права, «Запускать от имени», кеш каталога событий — вот три кита, на которых спотыкаются почти все. Разберём их и ещё несколько типовых промахов.
Отдельно стоит помнить про путаницу с модулями: календарь — это не статистика, и методы CStatEvent к нему отношения не имеют, несмотря на похожее слово «событие» в названии. А синхронизация с Google.Calendar без настроенного модуля «Социальные сервисы» просто не заведётся — это частая причина «а почему ничего не работает». Ниже — сводка по самым частым ошибкам с причинами и тем, что реально помогает.
| Грабля | Причина | Что делать |
|---|---|---|
| Неверный «Запускать от имени» | Событие создаётся от пользователя, не участвующего во встрече — падают права доступа | Указывать в поле пользователя, который реально участвует в событии |
| Тянуть год без фильтра | calendar.event.get без ограничения периода возвращает весь каталог событий |
Всегда передавать диапазон дат и sectionId |
| Править БД в обход методов | Прямой UPDATE по таблицам календаря ломает кеш и связи |
Использовать calendar.event.update и события ядра |
Путать CStatEvent с календарём |
Схожее название «событие», но это модуль статистики | Работать с CCalendarEvent и OnAfterCalendarEvent* |
| Забыть про «Социальные сервисы» | Google.Calendar не синхронизируется без настроенного модуля | Проверить модуль перед настройкой синхронизации |
Подробнее об услуге: внедрение и доработка Битрикс24 CRM.
Частые вопросы
Можно ли получить события календаря сразу по нескольким пользователям одним запросом?
Да, в calendar.event.get передайте массив в параметр ownerId — например, [1, 15, 27]. Если нужны события из конкретных календарей, дополнительно укажите type ('user', 'group', 'company_calendar') и section.
Что делать, если calendar.event.add возвращает ошибку доступа, хотя метод вызван от имени пользователя?
Проверьте, что у пользователя есть право на запись в указанную секцию (calendar.section.get вернёт поле PERM). Также убедитесь, что в OAuth-токене есть scope 'calendar' и вы не пытаетесь писать в чужой личный календарь без прав администратора.
Чем отличается calendar.event.add от calendar.event.edit при обновлении повторяющегося события?
calendar.event.add создаёт новое событие, а для изменения серии используйте calendar.event.edit с параметром id и, при необходимости, current_date для правки одного вхождения. Полная замена серии делается без current_date.
А если у меня уже есть Exchange-календарь — нужно ли писать свой синхронизатор через REST?
Нет, для Exchange/Outlook используйте встроенный Exchange ActiveSync в Битрикс24 (раздел «Почта и календарь»). REST-синхронизация нужна, когда источник — внешняя система без ActiveSync (Google Calendar, самописный сервис, CRM).
Можно ли в OnAfterCalendarEventDelete получить данные удалённого события, а не только его ID?
В обработчик приходит ID события, поэтому полные данные нужно читать заранее — например, кэшировать их в OnBeforeCalendarEventDelete или в своей таблице. После удаления calendar.event.get по этому ID уже ничего не вернёт.
Что делать, если callBatch с несколькими calendar.event.add падает целиком из-за одной ошибки?
callBatch по умолчанию останавливается на первой ошибке — используйте параметр halt: false, тогда в ответе придёт массив result с отдельным result_error по каждому вызову. Так вы увидите, какие события создались, а какие нет.