Search by

bxshef / leadfinish

IgorShevchik

Битрикс24 (коробка): завершение обработки лида привязкой уже существующей сделки вместо создания новой

Package info

github.com/bx-shef/leadfinish

Type:bitrix-module

pkg:composer/bxshef/leadfinish

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

v1.7.0 2026-09-18 06:08 UTC

This package is auto-updated.

Last update: 2026-09-18 06:16:44 UTC


README

CI Packagist Bitrix24 self-hosted

Модуль для Битрикс24. Позволяет завершить обработку лида, привязав к нему уже существующую сделку, — вместо штатного создания новой.

Разработчик — ИП Шевчик И.С., bx-shef.by.

Зачем это нужно

Штатное поведение Битрикс24: менеджер жмёт «Завершить обработку лида», и портал предлагает создать новую сделку на основании лида.

На практике сделка часто уже создана — заказ пришёл из другой системы и лежит в CRM под номером вида «Заказ покупателя 00КА-66901». Менеджеру нужно не создавать дубль, а связать лид с этой сделкой и закрыть лид.

Модуль подменяет зелёную кнопку в попапе завершения на свою — «Подобрать сделку».

Как это выглядит для менеджера

Завершение лида: вместо «Создать сделку» — «Подобрать сделку», окно подбора, поиск по номеру заказа

  1. Менеджер жмёт «Завершить обработку лида» — в карточке лида либо прямо в строке списка лидов.
  2. Открывается штатный попап «Выберите результат…». Вместо зелёной «Создать на основании: Сделку» в нём стоит «Подобрать сделку». Красная «Забраковать» остаётся на месте и работает как обычно.
  3. По нажатию открывается окно подбора с одним полем.
  4. Менеджер вводит номер заказа — например 66901. Список обновляется на лету.
  5. По каждой найденной сделке видно: название, сумму с валютой, стадию, воронку и есть ли уже связь с лидом. Рядом — ссылка «Открыть ↗»: сделка откроется в новой вкладке, подбор при этом не закроется.
  6. Менеджер выбирает строку и жмёт «Выбрать». Окно блокируется на время сохранения.
  7. Оба окна закрываются, появляется уведомление, открывается слайдер привязанной сделки. Если подбор открывали из списка, таблица сразу перечитывается — строка показывает новую стадию, перезагружать страницу не нужно.

Кнопка «Отмена» просто закрывает подбор — с лидом ничего не происходит.

Где работает

Место Как выглядит
Карточка лида, в том числе открытая в слайдере вместо зелёной кнопки — «Подобрать сделку»
Список лидов то же, прямо в строке
Канбан лидов в окне «Создать на основании лида:» — пункт «Подобрать сделку»

В канбане окно открывается не кнопкой, а перетаскиванием карточки лида в колонку «Сделка» (или в нижнюю зону «Сделка»). Ядро показывает там список вариантов конвертации — вместо них остаётся один наш пункт.

После привязки доска перечитывается: лид закрыт и уходит с неё сам. Если закрыть окно, ничего не выбрав, карточка возвращается в исходную колонку — как и при штатной отмене.

Что происходит внутри при нажатии «Выбрать»

Шаг Действие
1 В сделку записывается LEAD_ID — связь с нашим лидом
2 Лид переводится в статус CONVERTED («обработан успешно»)
3 В таймлайн лида: Лид привязан к сделке «…» (#id)
4 В таймлайн сделки: К сделке привязан лид «…» (#id)
5 Ответ уходит на клиент, окна закрываются, открывается слайдер сделки

Перевод лида в CONVERTED — штатная операция CRM, поэтому ядро само регистрирует статистику конверсии и историю стадий.

Порядок шагов намеренный: сначала связь, потом закрытие лида. Если второй шаг упадёт, останется привязанная сделка при открытом лиде — это заметно и чинится повторным нажатием. Обратный порядок дал бы закрытый лид без связи, что заметить куда труднее.

Запись в таймлайн — вспомогательная: её ошибка не отменяет уже выполненную привязку, а уходит в журнал (AddMessage2Log).

Правила подбора

  • Поиск по подстроке в названии сделки: 66901 находит «Заказ покупателя 00КА-66901».
  • Только сделки, созданные за последние 7 дней. Период показан прямо под полем ввода и выделен — это самая частая причина «сделка есть, а не находится».
  • Любая стадия, любая воронка.
  • Права учитываются: выборка идёт через Factory::getItemsFilteredByPermissions(), чужие сделки в подбор не попадут.
  • Минимум 3 символа, до 20 результатов, свежие сверху.

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

Установка

Порядок здесь важен: шаг 3 правит файл, который раскладка файлов на шаге 2 перетирает — и архивом, и Composer'ом. Сделать наоборот — список вернётся к поставочному, и кнопку никто не увидит.

1. Узнать ID тех, кому нужна кнопка

Открыть профиль сотрудника — ID стоит прямо в адресе:

/company/personal/user/44/

Число в адресе (44) и есть ID. То же самое видно в админке: Настройки → Пользователи → Список пользователей, столбец ID.

Выписать ID всех, кому кнопка нужна: менеджеров, которые завершают лиды, и свой собственный — чтобы было под кем проверять.

2. Положить файлы в портал

Через Composer. Настраивать пути не нужно — пакет приезжает туда, куда надо, сам:

composer require bxshef/leadfinish

Единственное, что требуется в composer.json проекта, — разрешить плагин раскладки. Это требование самого Composer, обойти его нельзя:

{
    "config": {
        "allow-plugins": { "composer/installers": true }
    }
}

Забыли — Composer остановится с ошибкой и не поставит ничего. Тихо не туда не уедет.

Архивом из релиза. Скачать shef.leadfinish.zip со страницы релизов:

cd /home/bitrix/www/bitrix/modules/
unzip -o shef.leadfinish.zip
chown -R bitrix:bitrix shef.leadfinish

local/modules/ тоже работает — фронт модуля от его расположения не зависит, см. ниже.

3. Вписать ID в .settings.php

Открыть /home/bitrix/www/bitrix/modules/shef.leadfinish/.settings.php и заменить список в allowed_users на тот, что выписали в шаге 1:

'allowed_users' => [
    'value' => [44, 562],   // <- сюда ID из шага 1
    'readonly' => false,
],

В поставке там стоит [1] — это администратор портала, и только он. Пока список не поправлен, менеджер кнопки не увидит, а выглядеть это будет как «модуль не работает».

Очистить список, чтобы «пока выключить», нельзя — пустой список значит ровно обратное: кнопка появится у всех авторизованных. Разбор всех случаев — в разделе «Кому показывается» ниже.

4. Установить модуль

Админка → Marketplace → Установленные решения → «[SH-local] Своя кнопка завершения лида» → Установить. Либо из консоли:

\Bitrix\Main\ModuleManager::registerModule('shef.leadfinish');

Composer только раскладывает файлы — этот шаг он не заменяет.

Установка регистрирует обработчик main::OnEpilog и копирует фронт в /bitrix/js/shef.leadfinish/. Настроек в базе модуль не создаёт: весь список доступа — это файл из шага 3.

⚠ Без этого шага кнопки не будет, даже если файлы разложены: каталог модуля браузеру недоступен. В поставке nginx стоит location ~* ^/bitrix/(modules|local_cache|…) { deny all; }, и запрос к /bitrix/modules/shef.leadfinish/js/... отдаёт 403. Поэтому JS и CSS переезжают туда, откуда отдаются, — так же делают штатные модули Битрикса.

Побочный итог: путь к фронту не зависит от того, куда положен модуль, и bitrix/modules/ с local/modules/ работают одинаково.

5. Сбросить кеш JS/CSS

Битрикс отдаёт склеенные файлы из /bitrix/cache/js/s1/..., и без сброса до браузера доедет прежний набор. Админка → Настройки продукта → Автокеширование → сбросить. Либо Ctrl+F5 в браузере.

6. Проверить под тем, кому кнопка нужна

Зайти под менеджером из шага 1, а не под собой, открыть лид и нажать «Завершить обработку лида». Вместо зелёной «Создать на основании: Сделку» должна стоять «Подобрать сделку».

Не появилась — раздел «Отладка» в конце.

Обновление

unzip -o shef.leadfinish.zip      # архивом
composer update bxshef/leadfinish # либо Composer'ом

И то и другое перетирает .settings.php вместе со списком allowed_users и расписанием приостановки.

Надёжный способ один: держать нужные значения в самой поставке — в репозитории, откуда собираются и архив, и Composer-пакет. Тогда обновление приезжает уже настроенным, и возвращать нечего. Правите только на сервере — выписывайте перед обновлением (cat .settings.php) и возвращайте после.

Переустановка через админку нужна, только если менялись события установки. Менялись lib/, js/, css/ — хватит раскладки файлов и сброса кеша.

Удаление

Там же кнопкой Удалить. Снимается обработчик события; заодно чистятся настройки в базе, оставшиеся от версий до 1.4.0. Файлы каталога остаются — их удалять вручную.

Привязки, сделанные модулем, при удалении не откатываются: LEAD_ID и статусы лидов остаются как есть. Это обычные данные CRM, а не служебные записи модуля.

Кому показывается

Список живёт в .settings.php модуля, ключ allowed_users — это шаг 3 установки. Здесь разобрано, что в нём можно написать.

В поставке стоит один администратор портала:

'allowed_users' => [
    'value' => [1],
    'readonly' => false,
],

Пустой список = кастомизация работает для всех авторизованных. Так задумано: после обкатки на нескольких людях включить её на всех можно, просто очистив список, без правки кода. Обратная сторона — очисткой список не «выключить»: получится не «никому», а «всем».

Тот же список проверяется и в ajax-действиях, не только при показе кнопки — иначе действие осталось бы вызываемым по прямому адресу.

Страницы настроек в админке у модуля нет намеренно: два источника одной правды (файл и база) рано или поздно разойдутся, и тогда непонятно, какой из них действует.

Случаи, которые легко перепутать:

В .settings.php Кому доступно
'value' => [44, 562] только этим ID
'value' => [] — список пуст всем авторизованным
ключа allowed_users нет вовсе никому: доработка не настроена
'value' => ['abc'], [[44]], '4 4' — нечитаемо никому: опечатка не должна ни раскатывать кнопку, ни выдавать её постороннему

Последние два разведены нарочно. Отсутствие ключа — это обычно неполное обновление: распаковали lib/, а .settings.php на сервере остался старый. Считать это за «пусто» значило бы молча включить кнопку всему порталу.

Файл перетирается при обновлении модуля (распаковка архива с -o), в отличие от прежней настройки в базе. Правите список на сервере — поправьте его и в репозитории, иначе следующая поставка вернёт прежний.

Приостановка доработки

Пока приём выполненных работ не оформлен, доработка сначала напоминает об этом, а потом перестаёт открываться. Две ступени:

Ступень Что происходит
soft При нажатии «Подобрать сделку» показывается экран с отсчётом на 20 секунд, затем подбор открывается как обычно
hard Подбор не открывается. Экран объясняет причину

На жёсткой ступени штатная зелёная кнопка ядра остаётся видимой и рабочей. Приостанавливается наша доработка, а не CRM заказчика: спрятав кнопку ядра и заблокировав свою, мы отняли бы у менеджера саму возможность завершить лид.

Жёсткая ступень запрещается и на сервере, в ajax-действиях, — иначе приостановка снималась бы правкой JS в консоли браузера.

Анимация (экскаватор, который пытается поднять стрелу и не может) отключается по prefers-reduced-motion. Без движения рисунок остаётся осмысленным.

Настройка

Расписание живёт в .settings.php модуля, раздел lock:

'lock' => [
    'value' => [
        'soft_from' => '2026-09-17',   // с этого дня — напоминание с отсчётом
        'hard_from' => '2026-09-21',   // с этого дня — подбор не открывается
        'off' => false,                // рубильник «оформлено»
        'users' => [],                 // кого касается; пусто — всех
    ],
    'readonly' => false,
],

Почему в файле, а не в базе и не в коде: снимать приостановку нужно быстро — правкой одного файла на сервере, без пересборки и переустановки модуля.

Любая ошибка в настройках уводит в сторону работающей доработки: негодная дата отбрасывается, мусор в off выключателем не считается, нечитаемый список users не означает «приостановить всем». Приостановить не того из-за своей же опечатки хуже, чем не показать напоминание.

Каждая дата работает и в одиночку: только soft_from — напоминание без последующей блокировки, только hard_from — блокировка без предупреждения. Жёсткая дата проверяется первой, поэтому перепутанные местами даты дают «жёстко с более ранней», а не тихую бессмыслицу.

Посмотреть экран, не дожидаясь даты, — ?lock=soft или ?lock=hard в адресе карточки лида. Обход умеет только повышать ступень: ?lock=none не существует, иначе такая ссылка разошлась бы по переписке за минуту.

Рубильник off гасит и обход. После того как приостановку сняли, старая ссылка с ?lock=hard из переписки ничего не покажет — иначе менеджер видел бы экран приостановки, которой уже нет, а сервер в этот момент действие разрешает.

Как это устроено технически

Ядро Битрикса не изменяется

Попап завершения — обычный BX.PopupWindow. Класс PopupWindow объявляет namespace BX.Main.Popup и эмитит onAfterShow, поэтому попап ловится штатным событием:

BX.Event.EventEmitter.subscribe('BX.Main.Popup:onAfterShow', (event) => {
    const popup = event.getTarget();
    // id попапа завершения лида = <controlId>_TERMINATION
});

Опорные точки в ядре (bitrix/js/crm/progress_control.js):

Что Где формируется
id попапа <controlId>_TERMINATION BX.CrmProgressControl
id обёртки кнопки <controlId>_success_btn_wrapper BX.CrmLeadTerminationControl.prepareDialogControls

Обе завязки — на суффикс id, так что переименование самого контрола ничего не ломает.

Оригинальная кнопка не удаляется, а скрывается. На открытии попапа ядро вешает на её внутренности селектор схем конверсии (BX.CrmLeadConversionSchemeSelector), а на закрытии дёргает его release(). Удаление узла оставило бы селектор со ссылками на несуществующий DOM.

Оба окна закрываются через объекты попапов, а не скрытием узлов стилями: спрятанный через display:none попап продолжает считать себя открытым и ломает следующее открытие.

Серверная часть

Всё на универсальном API CRM (Service\Container, Factory, Operation с enableCheckAccess()), а не прямыми запросами к базе: операции сами делают нормализацию данных, историю, индексацию и проверку прав.

Ajax-действия:

  • shef:leadfinish.DealBinder.search — подбор;
  • shef:leadfinish.DealBinder.bind — привязка и закрытие лида.

Оба защищены штатными фильтрами: авторизация, только POST, проверка csrf-токена.

Состав

Файл Назначение
install/index.php установка/удаление, регистрация обработчика
.settings.php контроллеры, список доступа, расписание приостановки
lib/access.php кому доступна кастомизация (общий источник правды)
lib/userlist.php разбор списка ID из настроек, общий для доступа и приостановки
lib/lock.php ступень приостановки: расписание, календарь, кого касается
lib/eventhandler.php подключение JS/CSS на страницах лида
lib/dealsearch.php подбор сделок с учётом прав
lib/binder.php привязка сделки, закрытие лида, таймлайны
lib/controller/dealbinder.php ajax-действия search и bind
js/lead-finish-button.js подмена кнопки и окно подбора
css/lead-finish.css оформление подбора
composer.json имя и тип пакета для установки через Composer
CLAUDE.md памятка для AI-агента: инварианты, ловушки, что не переигрывать

JS и CSS подключаются только там, где попап завершения существует, и только пользователям из списка: скрипт вешает глобальный слушатель попапов, и на остальных страницах портала он не нужен.

Файлы модуля лежат в корне репозитория — этого требует Composer, который разворачивает в целевой каталог корень пакета целиком. Всё, что на портал не едет (tests/, docs/, build.sh, CONTRIBUTING.md, .github/), отсечено через export-ignore в .gitattributes и исключено из сборки архива.

Отладка

В js/lead-finish-button.js вверху var DEBUG — по умолчанию false. Поставь true, и в консоли будет видно, что слушатель встал и что кнопка подменена.

Кнопка не появилась:

  • в консоли нет строки «слушатель попапа установлен» → скрипт не подключился: проверить allowed_users в .settings.php (есть ли там ваш ID и на месте ли сам ключ) и что открыта страница лида — карточка, список или канбан;
  • есть «обёртка зелёной кнопки не найдена» → ядро изменило разметку попапа, сверить id в progress_control.js;
  • ничего нет вовсе → кеш JS: Ctrl+F5 или сброс автокеширования в админке.

Подбор ничего не находит:

  • сделка старше 7 дней;
  • у пользователя нет прав на неё;
  • введено меньше 3 символов.

Ошибка при нажатии «Выбрать» — текст приходит от ядра как есть. Чаще всего это недостаток прав на изменение сделки или лида.

Совместимость

Проверено на Битрикс24 с модулем crm. Опорные точки — публичные события ядра и универсальное API CRM; при обновлении платформы отдельно стоит проверить, что попап завершения лида по-прежнему BX.PopupWindow с id <controlId>_TERMINATION.

Разработка

Ветки, коммиты, PR и чек-лист перед мержем — в CONTRIBUTING.md. Коротко: в main не пушим, всё через PR, мержит владелец.

Проверки и сборка — из корня репозитория:

./build.sh           # проверки + shef.leadfinish.zip
./build.sh --check   # только проверки, без архива

Скрипт делает php -l и node --check, гоняет тесты из tests/, ловит заглавные буквы в именах файлов lib/ (ломают автозагрузку только на Linux), показывает версию и проверяет, что внутри архива первым уровнем лежит shef.leadfinish/. Ровно то же гоняется в CI на каждый PR — собранный архив прикладывается к прогону, его можно скачать и поставить, не собирая руками.

Подробности: docs/build-and-install.md — сборка и установка, docs/module-structure.md — как устроен локальный модуль Битрикс24.

Релиз

Поставка на постоянной ссылке. Два способа, результат одинаковый:

КнопкойActions → Release → Run workflow от main. Тег выводится из VERSION в install/version.php и ставится сам, поэтому разойтись им негде.

Тегом с рабочей машины:

git tag v1.6.1        # ровно та версия, что лежит в install/version.php
git push origin v1.6.1

Дальше всё делает CI: сверяет тег с VERSION (не совпали — релиза не будет), собирает архив и выкладывает релиз с приложенным shef.leadfinish.zip и списком изменений с прошлого тега.

Зачем отдельно от артефакта прогона: артефакт живёт 14 дней, релиз — всегда. Через полгода на вопрос «что именно стоит на портале» отвечает страница релиза, а не память.

Нужна своя доработка Битрикс24?

Этот модуль сделан под конкретную задачу: процесс не ложился на коробочное поведение, а в маркетплейсе такого не было. Интеграция, AI-помощник, своя логика вместо стандартной — собирается под задачу.

Описать задачу за 5 минут →

Дальше оценка, фиксированная цена за этап или почасовая ставка и демо каждые 1–2 недели. Останавливаете проект — платите только за сделанное.

Лицензия

MIT.