Основные методы работы

Сделка это комплексная структура состоящая из нескольких составных частей:

  • основные данные,
  • регулярность
  • пользовательские поля.

Получение списка полей

Получить большую часть полей, можно через php код:

use \Bitrix\Main;

global $USER_FIELD_MANAGER;

if ( \Bitrix\Main\Loader::IncludeModule('crm') )
{
    $fieldsInfo = \CCrmDeal::GetFieldsInfo();

    foreach ($fieldsInfo as $code => &$field)
    {
        $field['CAPTION'] = \CCrmDeal::GetFieldCaption($code);
    }

    $userType = new \CCrmUserType(
        $USER_FIELD_MANAGER,
        \CCrmDeal::$sUFEntityID
    );
    $userType->PrepareFieldsInfo($fieldsInfo);

    /**
     * @array
     */
    var_dump($fieldsInfo);
}

Однако следует учитывать некоторые разночтения после начала ввода новой архитектуры D7, в систему были добавлены DataManager’ы, которые в свою очередь имеют свой допустимый перечень полей. Можно посмотреть их в \Bitrix\Crm\DealTable::getMap()

Получение списка

Существует несколько способов получить перечень сделок из системы: старое API и DataManager. Каждый способ имеет свои преимущества и не достатки, что в конечном счете использовать должен выбрать разработчик.

Сравнение Старое API DataManager
Возможность получить данные из БД v v
Учет прав при получении v x
Простая фильтрация данных v v
Использование под запросов для сложной фильтрации x v

Резюмируя, в случае DataManager как преимущество можно выделить скорость работы и гибкость за счет трансляции кода в SQL запрос, в то время как использование старого api дает возможность использовать проверку прав.

Используя старое API

Для получения списка элементов используя старое API используется метод CCrmDeal::GetListEx.

Сигнатура метода:

$entityResult = \CCrmDeal::GetListEx(
    $arOrder  = array(),
    $arFilter = array(),
    $arGroupBy = false,
    $arNavStartParams = false,
    $arSelectFields = array(),
    $arOptions = array()
);

Механизм получения данных аналогичен выборке из инфоблоков. По-умолчанию при выборке данных проверяются права текущего пользователя, поэтому для получения всех значений без проверки прав необходимо передать в фильтре флаг CHECK_PERMISSIONS со значением N.

Пример: необходимо получить Название и ID сделок, которые созданы 1 января 2021 года без учета прав пользователя, отсортированные по источнику (убыванию)

$entityResult = \CCrmDeal::GetListEx(
    [
        'SOURCE_ID' => 'DESC'
    ],
    [
        '><DATE_CREATE' => [
            '01.01.2021 00:00:00',
            '01.01.2021 23:59:59'
        ],
        'CHECK_PERMISSIONS' => 'N'
    ],
    false,
    false,
    [
        'ID',
        'TITLE'
    ]
);

while( $entity = $entityResult->fetch() )
{
    /**
     * [ 'ID' => ..., 'TITLE' => ... ]
     * @var array
     */
    var_dump($entity);
    var_dump($entity['ID']);
}

Отдельно стоит упомянуть наличие дополнительных возможностей фильтра и опций при работе с методом.

Прямое указание LIMIT и OFFSET

При помощи параметра QUERY_OPTIONS в $arOptions имеется возможность задания таких параметров. Это очень удобно для двухступенчатой постраничной навигации, когда сначала выбираются ID элементов, а потом уже данные для полученных ID.

Пример запроса:

$entityResult = \CCrmDeal::GetListEx(
    ['ID' => 'DESC'],
    [],
    false,
    false,
    [
        'ID',
        'TITLE'
    ],
    [
        'QUERY_OPTIONS' => [
            'LIMIT' => 100,
            'OFFSET' => 200
        ]
    ]
);

Запросы с участием внешних таблиц в фильтре

Иногда бывает необходимость достать только те элементы, для которых есть запись в сторонней таблице. Для этого можно воспользоваться __JOINS ключом.


/**
 * Entity alias. D - deal (default)
 * @var string
 */
$entityAlias = 'D';

$entityResult = \CCrmDeal::GetListEx(
    ['ID' => 'DESC'],
    [
        '__JOINS' => [
            [
                'TYPE' => 'INNER',
                'SQL' => 'INNER JOIN SOME_TABLE ST ON ST.ENTITY_ID = '.$entityAlias.'.ID'
            ]
        ],
    ],
    false,
    false,
    [
        'ID',
        'TITLE'
    ]
);

/**
 * Query:
 * SELECT
 *  D.ID as ID, D.TITLE as TITLE
 * FROM b_crm_deal D
 * INNER JOIN SOME_TABLE ST ON ST.ENTITY_ID = D.ID
 * ORDER BY D.ID DESC
 */

Но порой возникает необходимость не просто JOIN к таблице, а полноценные сложные запросы. Для реализации таких запросов в фильтрации существует ключ __CONDITIONS который добавляет любую строку к SQL запросу.

$entityResult = \CCrmDeal::GetListEx(
    ['ID' => 'DESC'],
    [
        '__CONDITIONS' => [
            [
                'SQL' => 'SOME_DUMP = 132'
            ]
        ],
    ],
    false,
    false,
    [
        'ID',
        'TITLE'
    ]
);

/**
 * SQL:
 * SELECT
 *  D.ID as ID, D.TITLE as TITLE
 * FROM b_crm_deal D
 * WHERE SOME_DUMP = 132
 * ORDER BY D.ID DESC
 */

Помните что это опасные операции из-за непроверяемых параметров, они открыты для SQL Injection.

Используя DataManager

Для получения сделок через DataManager используется класс \Bitrix\Crm\DealTable который является представлением для таблицы b_crm_deal. Для получения данных используется универсальный метод, который хорошо описан в документации.

Примечание: реализация DataManager архитектурно отличается от старого API, поэтому перечень полей для сделки старого и нового API может разниться. Актуальный список уже доступных полей можно найти получить из метода getMap(), а так же используя класс CCrmUserType (см. “Получение списка полей”).

Рассмотрим пример из старой сделки в реализации DataManager.

use \Bitrix\Crm;

$entityResult = Crm\DealTable::getList([
    'select' => [
        'ID',
        'TITLE'
    ],
    'filter' => [
        '><DATE_CREATE' => [
            new \Bitrix\Main\Type\DateTime('01.01.2021 00:00:00'),
            new \Bitrix\Main\Type\DateTime('01.01.2021 23:59:59')
        ]
    ],
    'order' => [
        'ID' => 'DESC'
    ]
]);

foreach ($entityResult as $entity)
{
    /**
     * [ 'ID' => ..., 'TITLE' => ... ]
     * @var array
     */
    var_dump($entity);
    var_dump($entity['ID']);
}

Создание сделки

Добавление сделки производится нестатическим методом Add класса CCrmDeal. Сигнатура метода:

public function Add(array &$arFields, $bUpdateSearch = true, $options = array())

$arFields - передающийся по ссылке массив полей добавляемого элемента. $bUpdateSearch - флаг необходимости перерасчета поискового индекса. $options - дополнительные опции при добавлении элемента.

Общий процесс добавления сделки:

  1. Проверка прав на добавление
  2. Проверка обязательных полей
  3. Подготовка технических полей
  4. Вызов события OnBeforeCrmDealAdd.
  5. Запись в таблицу
  6. Сохранение пользовательских полей
  7. Расчет прав
  8. Сохранение Наблюдателей
  9. Классификация сделки (Повторное обращение, Повторная сделка)
  10. Расчет статистических данных
  11. Запись в timline
  12. Расчет цепочки для сквозной аналитики
  13. Индексация для поиска
  14. Регистрация сообщения соц. сети
  15. Нотификация о создании элемента
  16. Вызов события OnAfterCrmDealAdd
  17. При наличии ORIGIN_ID вызов события OnAfterExternalCrmDealAdd
  18. Отправка push-сообщений для обновления канбана и визуализации добавления элемента

Ниже представлен код иллюстрирующий создание:

/**
 * true, если нужно проверять права текущего пользователя.
 * Текущий пользователь определяется ID в ключе CURRENT_USER
 * $arOptions
 * @var boolean
 */
$bCheckRight = true;

/**
 * Добавляемая сущность
 * @var array
 */
$entityFields = [

    'TITLE'    => 'Сделка года',
    'STAGE_ID' => 'NEW',
    'PROBABILITY' => '100',
    'CURRENCY_ID' => 'RUB',
    'OPPORTUNITY' => 1000000.00,
    'COMPANY_ID' => 1,
    'CONTACT_IDS' => [
        1,
        2,
    ],
    'BEGINDATE' => '01.01.2021',
    'CLOSEDATE' => '31.12.2022',
    'OPENED' => 'Y',
    'COMMENTS' => "Some comment! Yeah!",

    'ASSIGNED_BY_ID' => 1,
    'SOURCE_ID' => 'OTHER',
    'SOURCE_DESCRIPTION' => 'Мой контакт',
];


$entityObject = new \CCrmDeal( $bCheckRight );

$entityId = $entityObject->Add(
    $entityFields,
    $bUpdateSearch = true,
    $arOptions = [
        /**
         * ID пользователя, от лица которого выполняется действие
         * в том числе проверка прав
         * @var integer
         */
        'CURRENT_USER' => \CCrmSecurityHelper::GetCurrentUserID(),

        /**
         * Устанавливайте флаг, только если сущность проходит
         * процедуру восстановления. В случае если флаг есть
         * можно заполнять технические поля DATE_CREATE, DATE_MODIFY
         * @var boolean
         */
        // 'IS_RESTORATION' => true,
        
        /**
         * Флаг жестко определяющий поведение с полем "Дата завершения"
         * При установке в true указывает что при переводе сделку в 
         * финальный статус нужно обновить значение поля "Дата завершения"
         * @var boolean
         */
        //'ENABLE_CLOSE_DATE_SYNC' => true,
        
        /**
         * Флаг обозначающий запрет на создании записи в timeline элемента
         * о создании.
         * @var char
         */
        //'DISABLE_TIMELINE_CREATION' => 'Y'
    
        /**
         * В случае true, битрикс создаст сообщение в ленту о создании
         * @var boolean
         */
        //'REGISTER_SONET_EVENT' => true,
    
        /**
         * В случае если флаг true не будут проверяться:
         * - Пользовательские обязательные поля
         * - Валидация пользовательских полей
         * @var boolean
         */
        'DISABLE_USER_FIELD_CHECK' => false,

        /**
         * В случае если флаг true, не будет проверки
         * обязательности заполнения пользовательских полей
         * 
         * Если флаг 'DISABLE_USER_FIELD_CHECK' установлен в true,
         * данный флаг игнорируется - проверок не будет
         * 
         * Флаг не отменяет необходимость корректного заполнения
         * переданных полей.
         * @var boolean
         */
        'DISABLE_REQUIRED_USER_FIELD_CHECK' => false,
    ]
);

if ( !$entityId )
{
    /**
     * Произошла ошибка при добавлении сущности, посмотреть ее можно
     * через любой из способов ниже:
     * 1. $entityFields['RESULT_MESSAGE']
     * 2. $entityObject->LAST_ERROR
     */
}

Обновление сделки

Обновление (изменение) сделки производится нестатическим методом Update класса CCrmDeal.

Сигнатура метода:

public function Update($ID, array &$arFields, $bCompare = true, $bUpdateSearch = true, $options = array())

$ID - идентификатор изменяемого элемента $arFields - передающийся по ссылке массив полей изменяемого элемента. $bCompare - флаг необходимости проведения сравнения с предыдущими значениями для фиксации события изменений. bUpdateSearch - флаг необходимости расчета старого поиска. $options - дополнительные опции при обновлении элемента.

Общий процесс обновления:

  1. Получение текущих данных
  2. Проверка обязательных полей
  3. Проверка прав на изменение
  4. Вызов события OnBeforeCrmDealUpdate, если опция ENABLE_SYSTEM_EVENTS не определена в false
  5. Подготовка технических полей (стадий, дат обновлений, реляций).
  6. Проведения сравнения с предыдущими значениями (при необходимости)
  7. Запись изменений в таблицу
  8. Сохранение пользовательских полей
  9. Расчет прав
  10. Перепривязка контактов
  11. Сохранение наблюдателей
  12. Классификация сделки
  13. Завершение дел, при закрытии сделки
  14. Запись в статистические таблицы и историю
  15. Обновление UTM полей
  16. Обновление поискового индекса
  17. Запись изменений в timeline
  18. Запись сообщения в ленту сделки/чат
  19. Вызов события OnAfterCrmDealUpdate, если опция ENABLE_SYSTEM_EVENTS не определена в false
  20. Если доступно ML (Machine learning) и это не системное действие отправка в ML
  21. Отправка push-сообщений для обновления канбана и визуализации добавления элемента

Ниже представлен код иллюстрирующий изменение элемента.

/**
 * true, если нужно проверять права текущего пользователя.
 * Текущий пользователь определяется ID в ключе CURRENT_USER
 * $arOptions
 * @var boolean
 */
$bCheckRight = true;

/**
 * Идентификатор изменяемого элемента
 * @var integer
 */
$entityId = 123;

/**
 * Поля изменяемого элемента
 * @var array
 */
$entityFields = [
    // Отметим сделку выполненной
    'STAGE_ID'   => 'WON',
];

$entityObject = new \CCrmDeal( $bCheckRight );

$isUpdateSuccess = $entityObject->Update(
    $entityId,
    $entityFields,
    $bCompare = true,
    $bUpdateSearch = true,
    $arOptions = [
        /**
         * ID пользователя, от лица которого выполняется действие
         * в том числе проверка прав
         * @var integer
         */
        'CURRENT_USER' => \CCrmSecurityHelper::GetCurrentUserID(),

        /**
         * Флаг системного действия. В случае true у элемента не будут
         * занесены данные о пользователе который производит действие
         * и дата изменения элемента не изменится.
         * @var boolean
         */
        'IS_SYSTEM_ACTION' => false,

        /**
         * В случае true, битрикс создаст сообщение в ленту о изменении
         * @var boolean
         */
        'REGISTER_SONET_EVENT' => true,

        /**
         * Флаг жестко определяющий поведение с полем "Дата завершения"
         * При установке в true указывает что при переводе сделку в 
         * финальный статус нужно обновить значение поля "Дата завершения"
         * @var boolean
         */
        //'ENABLE_CLOSE_DATE_SYNC' => true,
        
        /**
         * Флаг обозначающий запрет на создании записи в timeline элемента
         * о создании.
         * @var char
         */
        //'DISABLE_TIMELINE_CREATION' => 'Y'
        //
        /**
         * Флаг для вызова системных событий.
         * При установке в false не будут срабатывать событие
         * @var boolean
         */
        'ENABLE_SYSTEM_EVENTS' => true,

        /**
         * Необходимо ли перерасчитывать семантическую стадию сделки
         * @var boolean
         */
        'SYNCHRONIZE_STAGE_SEMANTICS' => true

        /**
         * В случае если флаг true не будут проверяться:
         * - Пользовательские обязательные поля
         * - Валидация пользовательских полей
         * @var boolean
         */
        'DISABLE_USER_FIELD_CHECK' => false,

        /**
         * В случае если флаг true, не будет проверки
         * обязательности заполнения пользовательских полей
         * 
         * Если флаг `DISABLE_USER_FIELD_CHECK` установлен в true,
         * данный флаг игнорируется - проверок не будет
         * 
         * Флаг не отменяет необходимость корректного заполнения
         * переданных полей.
         * @var boolean
         */
        'DISABLE_REQUIRED_USER_FIELD_CHECK' => false,
    ]
);

if ( !$isUpdateSuccess )
{
    /**
     * Произошла ошибка при обновлении элемента, посмотреть ее можно
     * через любой из способов ниже:
     * 1. $entityFields['RESULT_MESSAGE']
     * 2. $entityObject->LAST_ERROR
     */
}

Удаление сделки

Удаление в системе осуществляется исключительно по ID.

Общий процесс удаления:

  1. Проверка существования элемента и прав на него
  2. Вызов события OnBeforeCrmDealDelete.
  3. Удаление элемента
  4. Очистка связанных данных
  5. Вызов события OnAfterCrmDealDelete

За удаление отвечает нестатический метод Delete класса CCrmDeal, который имеет следующую сигнатуру:

public function Delete($ID, $arOptions = array()): bool

Полное описание метода удаления на примере элемента с идентификатором 123:


/**
 * Идентификатор для удаления
 * @var integer
 */
$entityId = 123;

/**
 * true, если нужно проверять права текущего пользователя.
 * Текущий пользователь определяется ID в ключе CURRENT_USER
 * $arOptions
 * @var boolean
 */
$bCheckRight = true;

$entityObject = new \CCrmDeal( $bCheckRight );

$deleteResult = $entityObject->Delete(
    $entityId,
    [
        /**
         * ID пользователя, от лица которого выполняется операция
         * Значение по-умолчанию: ID текущего пользователя
         * @var int
         */
        'CURRENT_USER' => \CCrmSecurityHelper::GetCurrentUserID(),

        /**
         * Удалить связанные бизнес-процессы
         * Значение по-умолчанию: true
         * @var boolean
         */
        'PROCESS_BIZPROC' => true,

        /**
         * Включить отложенное удаление 
         * Значение по-умолчанию хранится в методе \Bitrix\Crm\Settings\DealSettings::getCurrent()->isDeferredCleaningEnabled()
         * @var boolean
         */
        'ENABLE_DEFERRED_MODE' => \Bitrix\Crm\Settings\DealSettings::getCurrent()->isDeferredCleaningEnabled()

        /**
         * Удалить данные сделки из статистических отчетов
         * Значение по-умолчанию: true
         * @var boolean
         */
        'REGISTER_STATISTICS' => true,
    ]
);

if ( !$deleteResult )
{
    // Операция не удалась
    var_dump($entityObject->LAST_ERROR);
}

Это излишнее описание с указанием значений по-умолчанию. В большинстве случае достаточно будет использовать упрощенную форму:

/**
 * Идентификатор для удаления
 * @var integer
 */
$entityId = 123;

/**
 * Идентификатор пользователя, чье имя нужно занести в историю
 * @var integer
 */
$logHistoryUserId = \CCrmSecurityHelper::GetCurrentUserID();

$entityObject = new \CCrmDeal( false );

$deleteResult = $entityObject->Delete(
    $entityId,
    [
        'CURRENT_USER' => $logHistoryUserId,
    ]
);

if ( !$deleteResult )
{
    // Операция не удалась
    var_dump($entityObject->LAST_ERROR);
}