bxshef / leadfinish
Битрикс24 (коробка): завершение обработки лида привязкой уже существующей сделки вместо создания новой
Requires
- php: >=8.1
- composer/installers: ^1.0 || ^2.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Модуль для Битрикс24. Позволяет завершить обработку лида, привязав к нему уже существующую сделку, — вместо штатного создания новой.
Разработчик — ИП Шевчик И.С., bx-shef.by.
Зачем это нужно
Штатное поведение Битрикс24: менеджер жмёт «Завершить обработку лида», и портал предлагает создать новую сделку на основании лида.
На практике сделка часто уже создана — заказ пришёл из другой системы и лежит в CRM под номером вида «Заказ покупателя 00КА-66901». Менеджеру нужно не создавать дубль, а связать лид с этой сделкой и закрыть лид.
Модуль подменяет зелёную кнопку в попапе завершения на свою — «Подобрать сделку».
Как это выглядит для менеджера
- Менеджер жмёт «Завершить обработку лида» — в карточке лида либо прямо в строке списка лидов.
- Открывается штатный попап «Выберите результат…». Вместо зелёной «Создать на основании: Сделку» в нём стоит «Подобрать сделку». Красная «Забраковать» остаётся на месте и работает как обычно.
- По нажатию открывается окно подбора с одним полем.
- Менеджер вводит номер заказа — например
66901. Список обновляется на лету. - По каждой найденной сделке видно: название, сумму с валютой, стадию, воронку и есть ли уже связь с лидом. Рядом — ссылка «Открыть ↗»: сделка откроется в новой вкладке, подбор при этом не закроется.
- Менеджер выбирает строку и жмёт «Выбрать». Окно блокируется на время сохранения.
- Оба окна закрываются, появляется уведомление, открывается слайдер привязанной сделки. Если подбор открывали из списка, таблица сразу перечитывается — строка показывает новую стадию, перезагружать страницу не нужно.
Кнопка «Отмена» просто закрывает подбор — с лидом ничего не происходит.
Где работает
| Место | Как выглядит |
|---|---|
| Карточка лида, в том числе открытая в слайдере | вместо зелёной кнопки — «Подобрать сделку» |
| Список лидов | то же, прямо в строке |
| Канбан лидов | в окне «Создать на основании лида:» — пункт «Подобрать сделку» |
В канбане окно открывается не кнопкой, а перетаскиванием карточки лида в колонку «Сделка» (или в нижнюю зону «Сделка»). Ядро показывает там список вариантов конвертации — вместо них остаётся один наш пункт.
После привязки доска перечитывается: лид закрыт и уходит с неё сам. Если закрыть окно, ничего не выбрав, карточка возвращается в исходную колонку — как и при штатной отмене.
Что происходит внутри при нажатии «Выбрать»
| Шаг | Действие |
|---|---|
| 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-помощник, своя логика вместо стандартной — собирается под задачу.
Дальше оценка, фиксированная цена за этап или почасовая ставка и демо каждые 1–2 недели. Останавливаете проект — платите только за сделанное.
Лицензия
MIT.
