Дело превращает договоренность с клиентом в действие. После создания сделки можно запланировать встречу, назначить ответственного, указать время и связать ее со сделкой.
В карточке CRM также видны история изменений и таймлайн. Они показывают ход работы со сделкой, но изменение поля не создает встречу для сотрудника.
Отличить дело от истории изменений и таймлайна
Выбирайте механизм по результату, который нужен после выполнения кода.
|
|| Задача | Что использовать || || Сотрудник должен провести встречу к определенному времени | Дело с ответственным, сроком и привязкой к элементу CRM || || Изменить сумму, стадию или другое поле сделки | Операцию изменения элемента из статьи Работа с элементами CRM || || Понять, когда и как менялись данные | Историю изменений элемента || || Видеть ход работы в карточке | Таймлайн, который показывает записи разных источников, в том числе дела || |#
История изменений отвечает на вопрос «что поменялось», а дело — «что нужно сделать или уже сделано». Не создавайте дело только ради фиксации изменения поля. Оно окажется среди рабочих действий и может попасть в список незавершенных дел.
Если нужен комментарий без задачи и срока, используйте механизм комментариев таймлайна отдельно от дел.
Подготовить тип, ID элемента и пользователя
Для привязки нужны тип и ID элемента. Числового ID недостаточно. Сделка и контакт могут иметь одинаковый ID. Для готового типа используйте константу CCrmOwnerType, для смарт-процесса — его entityTypeId. Подробности читайте в статье Схема работы CRM и основные объекты API.
Примеры рассчитаны на серверный обработчик авторизованного запроса в коробочном Битрикс24. Если код работает вне портала или портал находится в облаке, выберите REST API по статье Как выбрать API CRM.
Перед кодом подключите служебную часть пролога. Она подготавливает ядро, соединение с базой данных и текущего пользователя. Если запрос меняет данные CRM, до запуска примера проверьте его входные данные и защиту от CSRF.
До запуска первого примера подготовьте два значения:
-
$dealId— положительный ID сделки, к которой нужно привязать встречу -
$responsibleId— положительный ID сотрудника, который ее проведет
Возьмите оба ID из своего сценария.
Код получает $userId из текущего запроса. Этот ID нужен для выборки сделки с учетом прав. Вызовы CCrmActivity также выполняются в контексте текущего пользователя.
Не переносите пример в фоновое задание без отдельной настройки пользователя и проверки прав. В таком задании авторизованного запроса может не быть. Общие правила доступа объясняет статья Права доступа в PHP API CRM.
Первый пример проверяет доступность сделки до создания дела. Пустая выборка означает, что сделка не найдена или пользователь не может ее читать. Подробнее о таком чтении рассказано в статье Работа с элементами CRM.
Создать и привязать дело
Дело хранится отдельно от сделки, поэтому его создают через CCrmActivity. Если нужно изменить поле самой сделки, используйте операцию ее фабрики. Как выбирать API для разных данных CRM, показано в статье Как выбрать API CRM.
Метод CCrmActivity::Add() принимает поля дела и возвращает ID созданной записи. Для встречи укажите тип TYPE_ID, тему SUBJECT, ответственного RESPONSIBLE_ID, время и признак COMPLETED.
Основной элемент задают полями OWNER_TYPE_ID и OWNER_ID. Массив BINDINGS задает привязки к элементам CRM. Если привязку к основному элементу не указать в массиве, метод добавит ее сам. Здесь основную привязку указывают явно, чтобы показать формат BINDINGS для дополнительных привязок. Их задают парами типа и ID элемента.
Второй аргумент Add() включает проверку прав. Регистрацией события в истории CRM управляет третий аргумент regEvent. В примере он не указан, поэтому используется значение по умолчанию true. Регистрация события не добавляет произвольную запись в таймлайн.
Пример. Запланируйте часовую встречу через 24 часа для выбранной сделки.
if (!\Bitrix\Main\Loader::includeModule('crm'))
{
throw new \RuntimeException('Не удалось подключить модуль CRM');
}
$dealId = (int)($dealId ?? 0);
$responsibleId = (int)($responsibleId ?? 0);
if ($dealId <= 0 || $responsibleId <= 0)
{
throw new \RuntimeException('Укажите ID сделки и ответственного');
}
$userId = (int)$GLOBALS['USER']->GetID();
if ($userId <= 0)
{
throw new \RuntimeException('Требуется авторизованный пользователь');
}
// Проверяем, что текущий пользователь может прочитать сделку
$dealFactory = \Bitrix\Crm\Service\Container::getInstance()->getFactory(\CCrmOwnerType::Deal);
if ($dealFactory === null || $dealFactory->getItemsFilteredByPermissions([
'filter' => ['=ID' => $dealId],
'select' => [\Bitrix\Crm\Item::FIELD_NAME_ID],
'limit' => 1,
], $userId) === [])
{
throw new \RuntimeException('Сделка не найдена или недоступна');
}
// Округляем время до минуты и планируем встречу на следующий день
$startTime = (int)(ceil(time() / 60) * 60) + 24 * 60 * 60;
$endTime = $startTime + 60 * 60;
$startTimeFormatted = \Bitrix\Main\Type\DateTime::createFromTimestamp($startTime)->toString();
$endTimeFormatted = \Bitrix\Main\Type\DateTime::createFromTimestamp($endTime)->toString();
// Создаем дело и привязываем его к сделке
$activityFields = [
'TYPE_ID' => \CCrmActivityType::Meeting,
'OWNER_TYPE_ID' => \CCrmOwnerType::Deal,
'OWNER_ID' => $dealId,
'BINDINGS' => [
[
'OWNER_TYPE_ID' => \CCrmOwnerType::Deal,
'OWNER_ID' => $dealId,
],
],
'SUBJECT' => 'Обсудить условия поставки',
'DESCRIPTION' => 'Согласовать сроки и следующий шаг',
'RESPONSIBLE_ID' => $responsibleId,
'START_TIME' => $startTimeFormatted,
'END_TIME' => $endTimeFormatted,
'COMPLETED' => 'N',
];
$activityId = \CCrmActivity::Add($activityFields, true);
if ((int)$activityId <= 0)
{
throw new \RuntimeException('Не удалось создать дело');
}
// Читаем сохраненное дело с учетом прав и проверяем основные поля
$savedActivity = \CCrmActivity::GetList(
[],
['=ID' => (int)$activityId, 'CHECK_PERMISSIONS' => 'Y'],
false,
false,
['ID', 'OWNER_TYPE_ID', 'OWNER_ID', 'SUBJECT', 'RESPONSIBLE_ID', 'START_TIME', 'END_TIME', 'COMPLETED']
)->Fetch();
if (
!$savedActivity
|| (int)$savedActivity['OWNER_TYPE_ID'] !== \CCrmOwnerType::Deal
|| (int)$savedActivity['OWNER_ID'] !== $dealId
|| (int)$savedActivity['RESPONSIBLE_ID'] !== $responsibleId
|| $savedActivity['SUBJECT'] !== 'Обсудить условия поставки'
|| empty($savedActivity['START_TIME'])
|| empty($savedActivity['END_TIME'])
|| \Bitrix\Main\Type\DateTime::createFromUserTime($savedActivity['START_TIME'])->getTimestamp() !== $startTime
|| \Bitrix\Main\Type\DateTime::createFromUserTime($savedActivity['END_TIME'])->getTimestamp() !== $endTime
|| $savedActivity['COMPLETED'] !== 'N'
)
{
throw new \RuntimeException('Созданное дело не соответствует заданным данным');
}
После создания переменная $activityId содержит ID встречи. Повторное чтение подтверждает привязку, ответственного, тему, время и состояние 'N'. Код округляет время начала до минуты. Перед записью он преобразует его во время пользователя, а после чтения переводит обратно в метку времени для сравнения. В карточке сделки дополнительно проверьте, как встреча выглядит для сотрудника.
Два следующих примера показывают другие варианты создания. Для одной встречи выберите только один вариант. Если нужен внешний ключ или привязка к контакту, подготовьте переменные и $activityFields до вызова Add() в первом примере, а сам вызов и проверку после него замените соответствующим блоком ниже.
Если встреча приходит из внешней системы, сохраните ее идентификатор в поле ORIGIN_ID. Следующий блок найдет дело по внешнему ключу и привязке, а при первом получении встречи создаст его. Например, для внешней встречи meeting-42:
// Ключ одной встречи во внешнем календаре
$originId = 'calendar:meeting-42';
// Проверяем, не сохранили ли эту встречу раньше
$existingActivity = \CCrmActivity::GetList(
[],
[
'ORIGIN_ID' => $originId,
'OWNER_TYPE_ID' => \CCrmOwnerType::Deal,
'OWNER_ID' => $dealId,
'CHECK_PERMISSIONS' => 'Y',
],
false,
false,
['ID']
)->Fetch();
if ($existingActivity)
{
$activityId = (int)$existingActivity['ID'];
}
else
{
// Сохраняем внешний ключ вместе с новой встречей
$activityFields['ORIGIN_ID'] = $originId;
$activityId = \CCrmActivity::Add($activityFields, true);
if ((int)$activityId <= 0)
{
throw new \RuntimeException('Не удалось создать дело для внешней встречи');
}
}
Переменная $activityId содержит ID найденного или созданного дела. Если дело уже существовало, этот код не меняет его тему, время и ответственного. Префикс calendar: отличает ключ встречи от идентификаторов других источников.
Параллельная обработка одного внешнего события может создать дубли. Два запроса могут одновременно не найти дело и создать по одному. Поэтому обрабатывайте события последовательно для каждого внешнего идентификатора. Подробный сценарий обмена приведен в статье Синхронизация CRM с внешней системой.
Если встреча относится также к контакту, добавьте вторую пару OWNER_TYPE_ID и OWNER_ID в BINDINGS. Сначала проверьте, что контакт существует и доступен пользователю. Привязка дела к контакту не заменяет связь контакта со сделкой.
Пример. Для встречи со сделкой и контактом задайте $contactId и добавьте его к подготовленным полям дела:
$contactId = (int)($contactId ?? 0);
if ($contactId <= 0)
{
throw new \RuntimeException('Укажите ID контакта');
}
// Проверяем, что текущий пользователь видит контакт
$contactFactory = \Bitrix\Crm\Service\Container::getInstance()->getFactory(\CCrmOwnerType::Contact);
if ($contactFactory === null || $contactFactory->getItemsFilteredByPermissions([
'filter' => ['=ID' => $contactId],
'select' => [\Bitrix\Crm\Item::FIELD_NAME_ID],
'limit' => 1,
], $userId) === [])
{
throw new \RuntimeException('Контакт не найден или недоступен');
}
// Добавляем контакт, оставляя сделку основным элементом дела
$activityFields['BINDINGS'][] = [
'OWNER_TYPE_ID' => \CCrmOwnerType::Contact,
'OWNER_ID' => $contactId,
];
$activityId = \CCrmActivity::Add($activityFields, true);
if ((int)$activityId <= 0)
{
throw new \RuntimeException('Не удалось создать дело с привязкой к контакту');
}
В результате встреча связана со сделкой и контактом. Основным элементом дела по-прежнему остается сделка.
Дело типа «письмо» или «звонок» фиксирует действие в CRM. Вызов CCrmActivity::Add() сам не отправляет сообщение и не начинает разговор. Чтобы письмо ушло или звонок состоялся, используйте отдельно почту или телефонию.
Найти дела по основному элементу и привязке
Метод CCrmActivity::GetList() выбирает дела по типу и ID основного элемента, а также по состоянию COMPLETED. Передавайте оба поля владельца, иначе одинаковые числовые ID разных типов смешаются в выборке. Фильтр CHECK_PERMISSIONS => 'Y' сохраняет проверку прав.
В примерах GetList() получает пять аргументов: порядок сортировки, фильтр, группировку, параметры постраничной выборки и список возвращаемых полей. Значение false на третьем и четвертом месте означает, что группировка и постраничная выборка не нужны.
Пример. Получите открытые дела выбранной сделки по времени начала. Переменная $dealId задана перед первым примером.
$result = \CCrmActivity::GetList(
['START_TIME' => 'ASC'],
[
'OWNER_TYPE_ID' => \CCrmOwnerType::Deal,
'OWNER_ID' => $dealId,
'COMPLETED' => 'N',
'CHECK_PERMISSIONS' => 'Y',
],
false,
false,
['ID', 'SUBJECT', 'START_TIME', 'RESPONSIBLE_ID', 'COMPLETED']
);
$openActivities = [];
while ($activity = $result->Fetch())
{
$openActivities[] = $activity;
}
Массив $openActivities содержит доступные пользователю незавершенные дела. Для завершенных дел измените фильтр COMPLETED на 'Y'. Такая выборка находит дела, у которых сделка указана основным элементом.
Для поиска по любой привязке передайте пару типа и ID в фильтре BINDINGS. Например, следующий код найдет доступные пользователю дела выбранного контакта, включая те, у которых основным элементом указана сделка. До запуска примера задайте $contactId — ID этого контакта. Право читать контакт проверяйте отдельно, прежде чем показывать его данные.
$contactId = (int)($contactId ?? 0);
if ($contactId <= 0)
{
throw new \RuntimeException('Укажите ID контакта');
}
$contactActivities = \CCrmActivity::GetList(
['ID' => 'ASC'],
[
'BINDINGS' => [
[
'OWNER_TYPE_ID' => \CCrmOwnerType::Contact,
// Здесь OWNER_ID — ID контакта, к которому привязаны дела
'OWNER_ID' => $contactId,
],
],
'CHECK_PERMISSIONS' => 'Y',
],
false,
false,
['ID', 'SUBJECT', 'OWNER_TYPE_ID', 'OWNER_ID']
);
$linkedActivities = [];
while ($activity = $contactActivities->Fetch())
{
$linkedActivities[] = $activity;
}
Массив $linkedActivities содержит найденные дела. Фильтр BINDINGS выбирает их по привязке к контакту. Поля OWNER_TYPE_ID и OWNER_ID в найденной строке по-прежнему показывают основной элемент дела.
Завершить дело и проверить результат
Завершайте дело по его ID, а не по ID сделки. Сначала получите запись с учетом прав, проверьте привязку и состояние. Это защищает от завершения чужого дела и от повторного изменения уже выполненного.
Метод CCrmActivity::Update() принимает ID дела и изменяемые поля. По умолчанию он проверяет права и регистрирует событие. После вызова проверьте возвращенное значение и повторно прочитайте дело. Так можно убедиться, что оно действительно завершено.
Пример. Завершите встречу, созданную выше. При повторном запуске выполненное дело останется без изменений.
// ID встречи ищем отдельно от ID сделки, указанного в OWNER_ID
$activity = \CCrmActivity::GetList(
[],
[
'=ID' => (int)$activityId,
'OWNER_TYPE_ID' => \CCrmOwnerType::Deal,
'OWNER_ID' => $dealId,
'CHECK_PERMISSIONS' => 'Y',
],
false,
false,
['ID', 'OWNER_TYPE_ID', 'OWNER_ID', 'COMPLETED']
)->Fetch();
if (!$activity)
{
throw new \RuntimeException('Дело не найдено или недоступно');
}
// Не обновляем дело повторно, если оно уже выполнено
if ($activity['COMPLETED'] !== 'Y')
{
$updated = \CCrmActivity::Update((int)$activity['ID'], [
'COMPLETED' => 'Y',
]);
if (!$updated)
{
throw new \RuntimeException('Не удалось завершить дело');
}
}
// Повторное чтение подтверждает результат обновления
$savedActivity = \CCrmActivity::GetList(
[],
['=ID' => (int)$activityId, 'CHECK_PERMISSIONS' => 'Y'],
false,
false,
['ID', 'COMPLETED']
)->Fetch();
if (!$savedActivity || $savedActivity['COMPLETED'] !== 'Y')
{
throw new \RuntimeException('Не удалось подтвердить завершение дела');
}
Повторное чтение подтверждает значение 'Y' в поле COMPLETED. Если проверить его не удалось, не считайте внешнюю команду обработанной. Для повторных сигналов из внешней системы используйте сохраненное соответствие ID внешней встречи и ID дела.
При создании и изменении дела CRM вызывает события. Система может также обновить связанную запись календаря. Это зависит от типа дела и настроек CRM. Если приложение использует календарь, проверьте эту запись на своей установке.
Пример не настраивает отдельное напоминание. Если интеграция должна уведомить ответственного, настройте напоминание отдельно.
В результате встреча остается связанной со сделкой. Код находит ее по элементу CRM и проверяет, выполнена ли она.