Перейти к содержанию

Дело превращает договоренность с клиентом в действие. После создания сделки можно запланировать встречу, назначить ответственного, указать время и связать ее со сделкой.

В карточке 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 и проверяет, выполнена ли она.