Как программно обновить сделку в Битрикс24 с сохранением истории изменений

При разработке интеграций с Битрикс24 часто возникает задача программного обновления сделок через ядро CRM. Однако простое использование CCrmDeal::Update не всегда приводит к появлению изменений в истории сущности. В этой заметке я расскажу, как правильно обновлять сделки, чтобы все изменения фиксировались в таймлайне.

Проблема

При стандартном обновлении сделки:

 

1.jpg

 

изменения могут не отобразиться в истории, хотя сами поля успешно обновляются.

Актуальное решение

use Bitrix\Crm\Service\Container;

use Bitrix\Main\Loader;
public static function updateItemFields(int|string $id, int|string $typeId, array $fields): ?\Bitrix\Main\Result
{
$logger = LoggerFactory::create('updateItemFields', 'smart_element_update');
$logger->info('updateItemFields params', [
'$id' => $id,
'$typeId' => $typeId,
'$fields' => $fields,
]);
$factory = Container::getInstance()->getFactory($typeId);
$item = $factory->getItem((int)$id);
if (empty($item)) {
$logger->info('updateItemFields() item is empty', [
'$id' => $id,
'$typeId' => $typeId,
'$fields' => $fields,
]);
return null;
}
// Сохраняем старые значения
$oldValues = [];
foreach ($fields as $fieldName => $fieldValue) {
$oldValues[$fieldName] = $item->get($fieldName);
}
foreach ($fields as $fieldName => $fieldValue) {
$item->set($fieldName, $fieldValue);
}
$context = new \Bitrix\Crm\Service\Context();
$userId = CurrentUser::get()->getId() ?? ADMIN;
$context->setUserId($userId);

$saveOperation = $factory->getUpdateOperation($item, $context);
$saveOperation
->disableCheckFields()
->disableCheckAccess();
$operationResult = $saveOperation->launch();
// ПОСЛЕ ОБНОВЛЕНИЯ - добавляем запись в историю
if ($operationResult->isSuccess()) {
foreach ($fields as $fieldName => $newValue) {
$oldValue = $oldValues[$fieldName];

// Если значение изменилось
if ($oldValue != $newValue) {
// Получаем название поля
$fieldLabel = self::getFieldLabelSimple($fieldName);

// Добавляем запись в историю
self::addToHistory($typeId, (int)$id, $fieldLabel, $oldValue, $newValue, $userId);

$logger->info("Добавлено в историю: {$fieldLabel}");
}
}
}
if (!$operationResult->isSuccess()) {
$logger->error('updateItemFields() result', [
'isSuccess' => $operationResult->isSuccess(),
'getErrorMessages' => $operationResult->getErrorMessages(),
'getErrors' => $operationResult->getErrors(),
]);
}
return $operationResult;
} /**
* Простое получение названия поля
*/
private static function getFieldLabelSimple(string $fieldName): string
{
// Карта системных полей
$labels = [
'TITLE' => 'Название',
'STAGE_ID' => 'Стадия',
'ASSIGNED_BY_ID' => 'Ответственный',
'BEGINDATE' => 'Дата начала',
'CLOSEDATE' => 'Дата завершения',
'OPPORTUNITY' => 'Сумма',
'CURRENCY_ID' => 'Валюта',
'COMPANY_ID' => 'Компания',
'CONTACT_ID' => 'Контакт',
'COMMENTS' => 'Комментарий',
];

// Если это пользовательское поле
if (strpos($fieldName, 'UF_') === 0) {
return 'Поле: ' . $fieldName;
}

return $labels[$fieldName] ?? $fieldName;
} /**
* Добавление записи в историю
*/
private static function addToHistory(int $typeId, int $itemId, string $fieldName, $oldValue, $newValue, int $userId): void
{
try {
Loader::includeModule('crm');

// Определяем тип сущности для истории
$entityType = self::getEntityTypeForHistory($typeId);
if (!$entityType) {
return;
}

// Формируем текст события
$oldText = $oldValue !== null && $oldValue !== '' ? (string)$oldValue : '(не указано)';
$newText = $newValue !== null && $newValue !== '' ? (string)$newValue : '(не указано)';

// Создаем событие
$event = new \CCrmEvent();
$eventId = $event->Add(
[
'ENTITY_TYPE' => $entityType,
'ENTITY_ID' => $itemId,
'ENTITY_FIELD' => $fieldName,
'EVENT_TYPE' => 1, // 1 - изменение
'EVENT_NAME' => 'Изменение поля: ' . $fieldName,
'EVENT_TEXT_1' => 'Было: ' . $oldText,
'EVENT_TEXT_2' => 'Стало: ' . $newText,
'USER_ID' => $userId,
'DATE_CREATE' => ConvertTimeStamp(time(), 'FULL'),
],
false // не проверять права
);

// Логируем результат
$logger = LoggerFactory::create('addToHistory', 'history');
if ($eventId) {
$logger->info("Событие добавлено", [
'eventId' => $eventId,
'entityType' => $entityType,
'entityId' => $itemId,
'fieldName' => $fieldName
]);
} else {
$logger->error("Не удалось добавить событие", [
'entityType' => $entityType,
'entityId' => $itemId,
'fieldName' => $fieldName
]);
}

} catch (\Exception $e) {
$logger = LoggerFactory::create('addToHistory', 'history');
$logger->error('Ошибка добавления в историю', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
} /**
* Получение типа сущности для истории
*/
private static function getEntityTypeForHistory(int $typeId): ?string
{
// Стандартные типы
if ($typeId === \CCrmOwnerType::Deal) {
return 'DEAL';
}
if ($typeId === \CCrmOwnerType::Lead) {
return 'LEAD';
}
if ($typeId === \CCrmOwnerType::Contact) {
return 'CONTACT';
}
if ($typeId === \CCrmOwnerType::Company) {
return 'COMPANY';
}
if ($typeId === \CCrmOwnerType::Order) {
return 'ORDER';
}

// Для смарт-процессов - получаем имя динамической сущности
$factory = Container::getInstance()->getFactory($typeId);
if ($factory) {
$entityName = $factory->getEntityName();
if ($entityName) {
return strtoupper($entityName);
}
}

return null;
}

Устаревшее решение

Нужно использовать метод с дополнительными параметрами:

 

2.jpg

 

Ключевые параметры

 

4.jpg

 

Альтернативный вариант

Если нужно добавить комментарий в историю или создать кастомное событие:

 

3.jpg

 

Важные замечания

  1. Права доступа — убедитесь, что пользователь имеет права на изменение сделки и запись в историю
  2. Пользовательские поля — для них обязательно используйте префикс UF_CRM_
  3. Проверка результата — всегда проверяйте результат выполнения метода

Правильное использование этих параметров гарантирует, что все программные изменения сделок будут отображаться в истории, что критически важно для аудита и отслеживания работы с CRM.

Категория: Битрикс
Дата создания: 26.03.2026 07:33:37