Скрипт для копирования воронки Смарт процесс
php
/**
* Скрипт: копирование воронки (категории) и стадий тикетинга (Smart Process).
*
* Что копируем:
* - Категория (воронка): NAME, SORT (по умолчанию +5 от исходной)
* - Стадии (статусы b_crm_status): NAME, NAME_INIT, SORT, COLOR, SYSTEM, SEMANTICS
*
* Важно про ID:
* - Строковый ID стадии (STATUS_ID) пересобирается под новую категорию, но код после ":" сохраняется.
* Пример: DT130_130430:NEW -> DT130_<newCategoryId>:NEW
* - Числовой ID записи статуса (ID) по умолчанию НЕ сохраняем (чтобы не ловить коллизии).
* Можно задать $statusIdStart чтобы создать новые статусы с предсказуемыми ID по порядку.
*
* СПОСОБ 1: Задать параметры в коде (вверху файла)
* ================================================
* Задайте параметры ниже и запустите скрипт через браузер или CLI
*/
// ============================================
// ПАРАМЕТРЫ (задайте здесь значения)
// ============================================
$sourceCategoryId = 130430; // ОБЯЗАТЕЛЬНО: ID исходной воронки для копирования
$newName = "Еще одна воронка"; // ОБЯЗАТЕЛЬНО: название новой воронки
$entityTypeId = 0; // (опционально) если 0, определяется автоматически из категории
$targetCategoryId = 0; // (опционально) задать ID новой воронки (0 = авто)
$categorySort = null; // (опционально) SORT новой категории (null = авто)
$statusIdStart = 0; // (опционально) стартовый числовой ID для новых статусов (0 = авто)
$dryRun = false; // true = показать что будет сделано без записи в БД, false = выполнить
// ============================================
/**
* СПОСОБ 2: Через CLI с параметрами командной строки
* ===================================================
* php local/php_interface/console_copy_ticketing_funnel.php \
* --source-category-id=130430 \
* --new-name="Новая воронка"
*
* СПОСОБ 3: Через браузер с GET параметрами
* ==========================================
* /local/php_interface/console_copy_ticketing_funnel.php?source_category_id=130430&new_name=Новая+воронка
*/
// Флаги для обхода проверки авторизации
define("NOT_CHECK_PERMISSIONS", true);
define("STOP_STATISTICS", true);
define("NO_KEEP_STATISTIC", "Y");
define("NO_AGENT_STATISTIC", "Y");
define("DisableEventsCheck", true);
// Подключаем Битрикс
if (!isset($_SERVER['DOCUMENT_ROOT']) || empty($_SERVER['DOCUMENT_ROOT'])) {
$_SERVER['DOCUMENT_ROOT'] = dirname(dirname(__DIR__));
}
require($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');
use Bitrix\Crm\Model\ItemCategoryTable;
use Bitrix\Crm\StatusTable;
use Bitrix\Main\Loader;
use Bitrix\Main\Type\DateTime;
if (!Loader::includeModule('crm')) {
die("Модуль crm должен быть подключен
");
}
$isCli = php_sapi_name() === 'cli';
$isWeb = !$isCli;
// Получаем параметры из разных источников (приоритет: CLI > GET/POST > переменные в коде)
if ($isCli) {
// CLI: парсим аргументы командной строки
$cliOptions = getopt('', [
'entity-type-id:',
'source-category-id:',
'new-name:',
'target-category-id::',
'sort::',
'status-id-start::',
'dry-run',
'help',
]);
if (isset($cliOptions['help'])) {
echo "Использование:
";
echo " php local/php_interface/console_copy_ticketing_funnel.php --source-category-id=130430 --new-name=\"Новая воронка\"
";
echo "
Обязательные опции:
";
echo " --source-category-id=NNN ID исходной воронки для копирования
";
echo " --new-name=\"...\" Название новой воронки
";
echo "
Опциональные опции:
";
echo " --entity-type-id=NNN (если не указан, определяется автоматически)
";
echo " --target-category-id=NNN задать ID новой воронки
";
echo " --sort=NNN SORT новой воронки
";
echo " --status-id-start=NNN стартовый числовой ID для новых стадий
";
echo " --dry-run показать, что будет сделано, без записи в БД
";
exit(0);
}
// Переопределяем параметры из CLI, если они указаны
if (!empty($cliOptions['source-category-id'])) {
$sourceCategoryId = (int)$cliOptions['source-category-id'];
}
if (!empty($cliOptions['new-name'])) {
$newName = trim((string)$cliOptions['new-name']);
}
if (!empty($cliOptions['entity-type-id'])) {
$entityTypeId = (int)$cliOptions['entity-type-id'];
}
if (isset($cliOptions['target-category-id'])) {
$targetCategoryId = (int)$cliOptions['target-category-id'];
}
if (isset($cliOptions['sort'])) {
$categorySort = (int)$cliOptions['sort'];
}
if (isset($cliOptions['status-id-start'])) {
$statusIdStart = (int)$cliOptions['status-id-start'];
}
if (isset($cliOptions['dry-run'])) {
$dryRun = true;
}
} else {
// Веб: получаем параметры из GET/POST
if (isset($_REQUEST['source_category_id']) && $_REQUEST['source_category_id'] > 0) {
$sourceCategoryId = (int)$_REQUEST['source_category_id'];
}
if (isset($_REQUEST['new_name']) && !empty($_REQUEST['new_name'])) {
$newName = trim((string)$_REQUEST['new_name']);
}
if (isset($_REQUEST['entity_type_id']) && $_REQUEST['entity_type_id'] > 0) {
$entityTypeId = (int)$_REQUEST['entity_type_id'];
}
if (isset($_REQUEST['target_category_id']) && $_REQUEST['target_category_id'] > 0) {
$targetCategoryId = (int)$_REQUEST['target_category_id'];
}
if (isset($_REQUEST['sort']) && $_REQUEST['sort'] > 0) {
$categorySort = (int)$_REQUEST['sort'];
}
if (isset($_REQUEST['status_id_start']) && $_REQUEST['status_id_start'] > 0) {
$statusIdStart = (int)$_REQUEST['status_id_start'];
}
if (isset($_REQUEST['dry_run']) && $_REQUEST['dry_run'] == '1') {
$dryRun = true;
}
}
// Нормализуем параметры
$sourceCategoryId = (int)$sourceCategoryId;
$newName = trim((string)$newName);
$targetCategoryId = (int)$targetCategoryId;
$statusIdStart = (int)$statusIdStart;
if ($sourceCategoryId <= 0) {
die("Ошибка: source-category-id должен быть > 0
");
}
if (empty($newName)) {
die("Ошибка: new-name не может быть пустым
");
}
// Сначала получаем категорию без фильтра по entity-type-id, чтобы определить его автоматически
$sourceCategory = ItemCategoryTable::getRow([
'filter' => [
'=ID' => $sourceCategoryId,
],
]);
if (!$sourceCategory) {
die("Ошибка: исходная воронка не найдена (CATEGORY_ID={$sourceCategoryId})
");
}
// Определяем entity-type-id автоматически, если не указан
if ($entityTypeId <= 0) {
$entityTypeId = (int)$sourceCategory['ENTITY_TYPE_ID'];
}
if ($entityTypeId <= 0) {
die("Ошибка: не удалось определить ENTITY_TYPE_ID из категории {$sourceCategoryId}
");
}
// Проверяем, что категория действительно принадлежит указанному entity-type-id (если был указан)
if ($entityTypeId > 0 && (int)$sourceCategory['ENTITY_TYPE_ID'] !== $entityTypeId) {
die("Ошибка: категория {$sourceCategoryId} принадлежит ENTITY_TYPE_ID={$sourceCategory['ENTITY_TYPE_ID']}, а не {$entityTypeId}
");
}
if ($categorySort === null) {
$categorySort = (int)($sourceCategory['SORT'] ?? 500) + 5;
}
// Функция для вывода (поддержка HTML для веб и текста для CLI)
function output($text, $isHtml = false) {
global $isWeb;
if ($isWeb && $isHtml) {
echo $text;
} else {
echo strip_tags($text) . "
";
}
}
if ($isWeb) {
echo "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>Копирование воронки</title>";
echo "<style>body{font-family:monospace;padding:20px;} .success{color:green;} .error{color:red;} .info{color:blue;} pre{background:#f5f5f5;padding:10px;border:1px solid #ddd;}</style>";
echo "</head><body><h1>Копирование воронки тикетинга</h1><pre>";
}
echo "========================================
";
echo "Копирование воронки тикетинга
";
echo "========================================
";
echo "ENTITY_TYPE_ID: {$entityTypeId}
";
echo "SOURCE CATEGORY: #{$sourceCategoryId} \"{$sourceCategory['NAME']}\"
";
echo "NEW NAME: {$newName}
";
echo "NEW SORT: {$categorySort}
";
if ($targetCategoryId > 0) {
echo "TARGET CATEGORY ID: {$targetCategoryId}
";
}
if ($statusIdStart > 0) {
echo "STATUS ID START: {$statusIdStart}
";
}
echo "DRY RUN: " . ($dryRun ? 'YES' : 'NO') . "
";
echo "========================================
";
$sourceStatuses = StatusTable::getList([
'filter' => [
'=CATEGORY_ID' => $sourceCategoryId,
],
'order' => [
'SORT' => 'ASC',
'ID' => 'ASC',
],
])->fetchAll();
if (empty($sourceStatuses)) {
die("Ошибка: у исходной воронки нет стадий (CATEGORY_ID={$sourceCategoryId})
");
}
echo "Найдено стадий в исходной воронке: " . count($sourceStatuses) . "
";
$newCategoryFields = [
'ENTITY_TYPE_ID' => $entityTypeId,
'IS_DEFAULT' => 'N',
'NAME' => $newName,
'SORT' => $categorySort,
'CREATED_DATE' => new DateTime(),
];
if ($targetCategoryId > 0) {
$newCategoryFields['ID'] = $targetCategoryId;
}
if ($dryRun) {
$newCategoryId = $targetCategoryId > 0 ? $targetCategoryId : -1;
echo "[DRY-RUN] Будет создана новая воронка: " . json_encode($newCategoryFields, JSON_UNESCAPED_UNICODE) . "
";
} else {
/** @var \Bitrix\Main\ORM\Data\AddResult $addCategoryResult */
$addCategoryResult = call_user_func([ItemCategoryTable::class, 'add'], $newCategoryFields);
if (!$addCategoryResult->isSuccess()) {
die("Ошибка создания воронки: " . implode('; ', $addCategoryResult->getErrorMessages()) . "
");
}
$newCategoryId = (int)$addCategoryResult->getId();
echo "✓ Создана новая воронка: #{$newCategoryId}
";
}
// ENTITY_ID для стадий смарт-процесса
$newEntityId = 'DYNAMIC_' . $entityTypeId . '_STAGE_' . $newCategoryId;
// Удаляем автоматически созданные стандартные стадии (Битрикс создает их при создании категории)
if (!$dryRun) {
$autoCreatedStatuses = StatusTable::getList([
'filter' => ['ENTITY_ID' => $newEntityId]
])->fetchAll();
if (!empty($autoCreatedStatuses)) {
echo "Удаление автоматически созданных стадий (" . count($autoCreatedStatuses) . ")...
";
foreach ($autoCreatedStatuses as $autoStatus) {
StatusTable::delete($autoStatus['ID']);
}
echo "✓ Удалено автоматически созданных стадий: " . count($autoCreatedStatuses) . "
";
}
}
echo "
Копирование стадий...
";
echo "Новый ENTITY_ID стадий: {$newEntityId}
";
$statusIdCounter = $statusIdStart > 0 ? $statusIdStart : 0;
// Разделяем стадии на промежуточные и финальные (с SEMANTICS='S' или 'F')
$intermediateStages = [];
$finalStages = [];
foreach ($sourceStatuses as $src) {
$semantics = trim((string)($src['SEMANTICS'] ?? ''));
if ($semantics === 'S' || $semantics === 'F') {
$finalStages[] = $src;
} else {
$intermediateStages[] = $src;
}
}
// Сначала создаем промежуточные стадии, потом финальные
$stagesToCreate = array_merge($intermediateStages, $finalStages);
$created = 0;
$map = [];
foreach ($stagesToCreate as $src) {
$srcStatusId = (string)($src['STATUS_ID'] ?? '');
$stageCode = $srcStatusId;
if (strpos($srcStatusId, ':') !== false) {
[$_prefix, $stageCode] = explode(':', $srcStatusId, 2);
}
// Новый строковый статус-id под новую категорию, но тот же stageCode
$newPrefix = \CCrmStatus::getDynamicEntityStatusPrefix($entityTypeId, $newCategoryId);
$newStatusId = $newPrefix . ':' . $stageCode;
$dst = [
'ENTITY_ID' => $newEntityId,
'STATUS_ID' => $newStatusId,
'NAME' => (string)($src['NAME'] ?? ''),
'NAME_INIT' => (string)($src['NAME_INIT'] ?? ''),
'SORT' => (int)($src['SORT'] ?? 10),
'SYSTEM' => (string)($src['SYSTEM'] ?? 'N'),
'COLOR' => (string)($src['COLOR'] ?? ''),
'CATEGORY_ID' => $newCategoryId,
];
// SEMANTICS добавляем только если он не пустой (пустая строка вызывает ошибку)
$semantics = trim((string)($src['SEMANTICS'] ?? ''));
if ($semantics !== '') {
$dst['SEMANTICS'] = $semantics;
}
if ($statusIdCounter > 0) {
$dst['ID'] = $statusIdCounter++;
}
if ($dryRun) {
$newNumericId = $dst['ID'] ?? 0;
echo "[DRY-RUN] + стадия: {$src['ID']} / {$srcStatusId} -> {$newNumericId} / {$newStatusId} | {$dst['NAME']}
";
$map[] = [
'old_id' => (int)($src['ID'] ?? 0),
'old_status_id' => $srcStatusId,
'new_id' => $newNumericId,
'new_status_id' => $newStatusId,
'name' => $dst['NAME'],
];
$created++;
continue;
}
/** @var \Bitrix\Main\ORM\Data\AddResult $addStatusResult */
$addStatusResult = call_user_func([StatusTable::class, 'add'], $dst);
if (!$addStatusResult->isSuccess()) {
$errors = implode('; ', $addStatusResult->getErrorMessages());
die("Ошибка создания стадии '{$dst['NAME']}' ({$srcStatusId} -> {$newStatusId}): {$errors}
");
}
$newNumericId = (int)$addStatusResult->getId();
// Для финальных стадий (SUCCESS, FAIL) Битрикс автоматически определяет SEMANTICS
// Не нужно обновлять вручную - Битрикс сам установит правильную семантику
echo "✓ {$src['ID']} / {$srcStatusId} -> {$newNumericId} / {$newStatusId} | {$dst['NAME']}
";
$map[] = [
'old_id' => (int)($src['ID'] ?? 0),
'old_status_id' => $srcStatusId,
'new_id' => $newNumericId,
'new_status_id' => $newStatusId,
'name' => $dst['NAME'],
];
$created++;
}
echo "
========================================
";
echo "Готово!
";
echo "Новая воронка: #{$newCategoryId}
";
echo "Создано стадий: {$created}
";
echo "========================================
";
if ($isWeb) {
echo "</pre>";
if (!$dryRun) {
echo "<p class='success'><strong>✓ Воронка успешно создана!</strong></p>";
echo "<p>ID новой воронки: <strong>#{$newCategoryId}</strong></p>";
echo "<p>Создано стадий: <strong>{$created}</strong></p>";
}
echo "<hr><p><a href='?'>← Вернуться</a></p>";
echo "</body></html>";
}