← К документации

CRM

Сделки

Работа со сделками CRM через PHP и фабрики: получение, добавление, изменение, удаление, стадии, воронки, контакты и события.

Сделка — основная рабочая сущность CRM. При изменении стадии или важных полей лучше использовать операции фабрики, чтобы сработали проверки, история, таймлайн, роботы и бизнес-процессы.

PHP для коробки с подключённым ядром. Примеры чтения через фабрику рассчитаны на авторизованного пользователя и используют getItemsFilteredByPermissions. В CLI нужен отдельно заданный доверенный контекст. Примеры на странице самостоятельны: одинаковые вспомогательные функции не нужно объявлять вместе.

Основные операции

Базовая работа со сделками через фабрику CRM.

Получить сделку

<?php

use Bitrix\Main\Loader;
use Bitrix\Crm\Service;


if (!Loader::includeModule('crm')) {
    throw new \RuntimeException('Модуль crm не загружен');
}

global $USER;
if (!$USER || !$USER->IsAuthorized()) {
    throw new \RuntimeException('Нужен авторизованный пользователь');
}
const CRM_DEAL_TYPE_ID = \CCrmOwnerType::Deal;

function fetchDealFactory() {
    return Service\Container::getInstance()->getFactory(CRM_DEAL_TYPE_ID);
}

function fetchDealItem(int $deal_id) {
    $factory = fetchDealFactory();

    if ($factory === null) {
        return null;
    }

    return ($factory->getItemsFilteredByPermissions(['filter' => ['=ID' => $deal_id], 'limit' => 1])[0] ?? null);
}

$deal_item = fetchDealItem($deal_id);

if ($deal_item !== null) {
    $deal_data = $deal_item->toArray();
    $title = $deal_item->getTitle();
    $stage_id = $deal_item->getStageId();

    print_r($deal_data);
}

Получить список сделок

<?php

use Bitrix\Main\Loader;
use Bitrix\Crm\Service;

const DEAL_LIMIT = 100;

if (!Loader::includeModule('crm')) {
    throw new \RuntimeException('Модуль crm не загружен');
}

global $USER;
if (!$USER || !$USER->IsAuthorized()) {
    throw new \RuntimeException('Нужен авторизованный пользователь');
}
const CRM_DEAL_TYPE_ID = \CCrmOwnerType::Deal;

function fetchDealFactory() {
    return Service\Container::getInstance()->getFactory(CRM_DEAL_TYPE_ID);
}

function fetchDealItems(int $category_id, string $stage_id): array {
    $factory = fetchDealFactory();

    if ($factory === null) {
        return [];
    }

    return $factory->getItemsFilteredByPermissions([
        'select' => [
            'ID',
            'TITLE',
            'CATEGORY_ID',
            'STAGE_ID',
            'COMPANY_ID',
            'CONTACT_ID',
            'ASSIGNED_BY_ID',
            'OPPORTUNITY',
            'CURRENCY_ID',
        ],
        'filter' => [
            '=CATEGORY_ID' => $category_id,
            '=STAGE_ID' => $stage_id,
        ],
        'order' => [
            'ID' => 'ASC',
        ],
        'limit' => DEAL_LIMIT,
    ]);
}

$deal_items = fetchDealItems($category_id, $stage_id);

foreach ($deal_items as $deal_item) {
    print_r($deal_item->toArray());
}

Добавить сделку

<?php

use Bitrix\Main\Loader;
use Bitrix\Crm\Service;


if (!Loader::includeModule('crm')) {
    throw new \RuntimeException('Модуль crm не загружен');
}

global $USER;
if (!$USER || !$USER->IsAuthorized()) {
    throw new \RuntimeException('Нужен авторизованный пользователь');
}
const CRM_DEAL_TYPE_ID = \CCrmOwnerType::Deal;

function fetchDealFactory() {
    return Service\Container::getInstance()->getFactory(CRM_DEAL_TYPE_ID);
}

function addDeal(array $deal_fields): array {
    $factory = fetchDealFactory();

    if ($factory === null) {
        return [
            'is_success' => false,
            'deal_id' => 0,
            'error_messages' => ['Фабрика сделок не найдена'],
        ];
    }

    $deal_item = $factory->createItem($deal_fields);

    $operation = $factory->getAddOperation($deal_item);
    $operation_result = $operation->launch();

    return [
        'is_success' => $operation_result->isSuccess(),
        'deal_id' => (int) $deal_item->getId(),
        'error_messages' => $operation_result->getErrorMessages(),
    ];
}

$deal_fields = [
    'TITLE' => $title,
    'CATEGORY_ID' => $category_id,
    'STAGE_ID' => $stage_id,
    'COMPANY_ID' => $company_id,
    'CONTACT_ID' => $contact_id,
    'ASSIGNED_BY_ID' => $assigned_by_id,
    'OPPORTUNITY' => $opportunity,
    'CURRENCY_ID' => 'RUB',
];

$add_result = addDeal($deal_fields);

print_r($add_result);

Изменить сделку

<?php

use Bitrix\Main\Loader;
use Bitrix\Crm\Service;


if (!Loader::includeModule('crm')) {
    throw new \RuntimeException('Модуль crm не загружен');
}

global $USER;
if (!$USER || !$USER->IsAuthorized()) {
    throw new \RuntimeException('Нужен авторизованный пользователь');
}
const CRM_DEAL_TYPE_ID = \CCrmOwnerType::Deal;

function fetchDealFactory() {
    return Service\Container::getInstance()->getFactory(CRM_DEAL_TYPE_ID);
}

function moveDealToStage(int $deal_id, string $stage_id): array {
    $factory = fetchDealFactory();

    if ($factory === null) {
        return [
            'is_success' => false,
            'error_messages' => ['Фабрика сделок не найдена'],
        ];
    }

    $deal_item = ($factory->getItemsFilteredByPermissions(['filter' => ['=ID' => $deal_id], 'limit' => 1])[0] ?? null);

    if ($deal_item === null) {
        return [
            'is_success' => false,
            'error_messages' => ['Сделка не найдена'],
        ];
    }

    $deal_item->setStageId($stage_id);

    $operation = $factory->getUpdateOperation($deal_item);
    $operation_result = $operation->launch();

    return [
        'is_success' => $operation_result->isSuccess(),
        'error_messages' => $operation_result->getErrorMessages(),
    ];
}

$update_result = moveDealToStage($deal_id, $stage_id);

print_r($update_result);

Удалить сделку

<?php

use Bitrix\Main\Loader;
use Bitrix\Crm\Service;


if (!Loader::includeModule('crm')) {
    throw new \RuntimeException('Модуль crm не загружен');
}

global $USER;
if (!$USER || !$USER->IsAuthorized()) {
    throw new \RuntimeException('Нужен авторизованный пользователь');
}
const CRM_DEAL_TYPE_ID = \CCrmOwnerType::Deal;

function fetchDealFactory() {
    return Service\Container::getInstance()->getFactory(CRM_DEAL_TYPE_ID);
}

function deleteDeal(int $deal_id): array {
    $factory = fetchDealFactory();

    if ($factory === null) {
        return [
            'is_success' => false,
            'error_messages' => ['Фабрика сделок не найдена'],
        ];
    }

    $deal_item = ($factory->getItemsFilteredByPermissions(['filter' => ['=ID' => $deal_id], 'limit' => 1])[0] ?? null);

    if ($deal_item === null) {
        return [
            'is_success' => false,
            'error_messages' => ['Сделка не найдена'],
        ];
    }

    $operation = $factory->getDeleteOperation($deal_item);
    $operation_result = $operation->launch();

    return [
        'is_success' => $operation_result->isSuccess(),
        'error_messages' => $operation_result->getErrorMessages(),
    ];
}

$delete_result = deleteDeal($deal_id);

print_r($delete_result);

Связи

У сделки может быть компания и один или несколько контактов.

Контакты сделки

<?php

use Bitrix\Main\Loader;
use Bitrix\Crm\Binding\DealContactTable;

if (!Loader::includeModule('crm')) {
    throw new \RuntimeException('Модуль crm не загружен');
}

global $USER;
if (!$USER || !$USER->IsAuthorized()) {
    throw new \RuntimeException('Нужен авторизованный пользователь');
}

function fetchDealContactIds(int $deal_id): array {
    return DealContactTable::getDealContactIDs($deal_id);
}

function bindDealContactIds(int $deal_id, array $contact_ids): void {
    DealContactTable::bindContactIDs($deal_id, $contact_ids);
}

$contact_ids = fetchDealContactIds($deal_id);

print_r($contact_ids);

Компания сделки

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

<?php

use Bitrix\Main\Loader;
use Bitrix\Crm\Service;


if (!Loader::includeModule('crm')) {
    throw new \RuntimeException('Модуль crm не загружен');
}

global $USER;
if (!$USER || !$USER->IsAuthorized()) {
    throw new \RuntimeException('Нужен авторизованный пользователь');
}
const CRM_DEAL_TYPE_ID = \CCrmOwnerType::Deal;

function updateDealCompany(int $deal_id, int $company_id): array {
    $factory = Service\Container::getInstance()->getFactory(CRM_DEAL_TYPE_ID);

    if ($factory === null) {
        return [
            'is_success' => false,
            'error_messages' => ['Фабрика сделок не найдена'],
        ];
    }

    $deal_item = ($factory->getItemsFilteredByPermissions(['filter' => ['=ID' => $deal_id], 'limit' => 1])[0] ?? null);

    if ($deal_item === null) {
        return [
            'is_success' => false,
            'error_messages' => ['Сделка не найдена'],
        ];
    }

    $deal_item->set('COMPANY_ID', $company_id);

    $operation = $factory->getUpdateOperation($deal_item);
    $operation_result = $operation->launch();

    return [
        'is_success' => $operation_result->isSuccess(),
        'error_messages' => $operation_result->getErrorMessages(),
    ];
}

$update_result = updateDealCompany($deal_id, $company_id);

print_r($update_result);

События

События сделок позволяют выполнить свой PHP-код перед добавлением, после добавления, перед изменением, после изменения, перед удалением и после удаления сделки.

Список событий

Обработчики обычно регистрируют в /local/php_interface/init.php или внутри своего модуля. Для коробочной разработки это удобный способ реагировать на изменения сделок без REST-приложения.

Событие Когда вызывается Метод
OnBeforeCrmDealAdd Перед добавлением сделки. CCrmDeal::Add
OnAfterCrmDealAdd После добавления сделки. CCrmDeal::Add
OnBeforeCrmDealUpdate Перед изменением сделки. CCrmDeal::Update
OnAfterCrmDealUpdate После изменения сделки. CCrmDeal::Update
OnBeforeCrmDealDelete Перед удалением сделки. CCrmDeal::Delete
OnAfterCrmDealDelete После удаления сделки. CCrmDeal::Delete
OnAfterExternalCrmDealAdd После добавления внешней сделки. CCrmDeal::Add
OnBeforeCrmDealProductRowsSave Перед сохранением товарных строк сделки. CCrmDeal::SaveProductRows
OnAfterCrmDealProductRowsSave После сохранения товарных строк сделки. CCrmDeal::SaveProductRows

Перед добавлением

В OnBeforeCrmDealAdd можно изменить поля до сохранения или отменить создание сделки. Например, можно заполнить валюту по умолчанию или запретить создание сделки без названия.

<?php

use Bitrix\Main\EventManager;

$event_manager = EventManager::getInstance();

function registerBeforeDealAddHandler(EventManager $event_manager): void {
    $event_manager->addEventHandlerCompatible(
        'crm',
        'OnBeforeCrmDealAdd',
        'handleBeforeDealAdd'
    );
}

function handleBeforeDealAdd(array &$deal_fields): bool {
    global $APPLICATION;

    if (empty($deal_fields['TITLE'])) {
        $APPLICATION->ThrowException('Не заполнено название сделки');

        return false;
    }

    if (empty($deal_fields['CURRENCY_ID'])) {
        $deal_fields['CURRENCY_ID'] = 'RUB';
    }

    return true;
}

registerBeforeDealAddHandler($event_manager);

В событиях до сохранения массив полей передаётся по ссылке. Если изменить значение в $deal_fields, оно попадёт в сохраняемую сделку.

После изменения

OnAfterCrmDealUpdate можно использовать для логирования, синхронизации со своими таблицами или запуска вспомогательной логики после изменения сделки.

<?php

use Bitrix\Main\EventManager;

$event_manager = EventManager::getInstance();

function registerAfterDealUpdateHandler(EventManager $event_manager): void {
    $event_manager->addEventHandlerCompatible(
        'crm',
        'OnAfterCrmDealUpdate',
        'handleAfterDealUpdate'
    );
}

function handleAfterDealUpdate(array &$deal_fields): void {
    $deal_id = isset($deal_fields['ID']) ? (int) $deal_fields['ID'] : 0;

    if ($deal_id <= 0) {
        return;
    }

    AddMessage2Log(
        [
            'message' => 'Сделка изменена',
            'deal_id' => $deal_id,
            'field_names' => array_keys($deal_fields),
        ],
        'deal_events'
    );
}

registerAfterDealUpdateHandler($event_manager);

Событие после изменения не всегда содержит полный набор полей сделки. Если нужно проверить поле, которого нет в $deal_fields, лучше отдельно получить сделку по ID.

Перед удалением

OnBeforeCrmDealDelete можно использовать, если удаление нужно проверить или запретить.

<?php

use Bitrix\Main\EventManager;

$event_manager = EventManager::getInstance();

function registerBeforeDealDeleteHandler(EventManager $event_manager): void {
    $event_manager->addEventHandlerCompatible(
        'crm',
        'OnBeforeCrmDealDelete',
        'handleBeforeDealDelete'
    );
}

function handleBeforeDealDelete(int $deal_id): bool {
    global $APPLICATION;

    $has_delete_access = true;

    if (!$has_delete_access) {
        $APPLICATION->ThrowException('Удаление сделки запрещено');

        return false;
    }

    AddMessage2Log(
        [
            'message' => 'Проверка перед удалением сделки',
            'deal_id' => $deal_id,
        ],
        'deal_events'
    );

    return true;
}

registerBeforeDealDeleteHandler($event_manager);

Товарные строки

Для товарных строк сделки есть отдельные события: OnBeforeCrmDealProductRowsSave и OnAfterCrmDealProductRowsSave. Они относятся к сохранению товаров сделки, а не к обычному изменению полей сделки.

<?php

use Bitrix\Main\EventManager;

$event_manager = EventManager::getInstance();

function registerAfterDealProductRowsSaveHandler(EventManager $event_manager): void {
    $event_manager->addEventHandlerCompatible(
        'crm',
        'OnAfterCrmDealProductRowsSave',
        'handleAfterDealProductRowsSave'
    );
}

function handleAfterDealProductRowsSave(int $deal_id, array $product_rows): void {
    AddMessage2Log(
        [
            'message' => 'Товарные строки сделки сохранены',
            'deal_id' => $deal_id,
            'product_rows' => $product_rows,
        ],
        'deal_product_rows'
    );
}

registerAfterDealProductRowsSaveHandler($event_manager);

Если обработчик не получает товарные строки в ожидаемом виде, сначала выведите все аргументы обработчика в лог через func_get_args() и проверьте фактическую структуру на своей версии коробки.

Источники