<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Статьи - Денис Елисеев</title>
    <link>https://eliseevdenis.ru/blog/</link>
    <description>Статьи о Битрикс24: бизнес-процессы, интеграции, REST API.</description>
    <language>ru</language>
    <atom:link href="https://eliseevdenis.ru/blog/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Бэкап Битрикс24 на 115 ГБ, в котором нет ни одной сделки</title>
      <link>https://eliseevdenis.ru/blog/backup-without-data/</link>
      <guid isPermaLink="true">https://eliseevdenis.ru/blog/backup-without-data/</guid>
      <pubDate>Sat, 24 Oct 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[<p>Резервная копия базы весила 115 ГБ, лежала в списке копий и выглядела готовой. Когда ее попробовали развернуть на тестовом сервере, оказалось, что в ней 288 таблиц из примерно тысячи. Ни пользователей, ни сделок, ни задач, ни переписки.</p>
<p>Об этом никто не знал. Копия считалась рабочей до первой попытки ее восстановить.</p>
<h2 id="s1">Что было внутри</h2>
<p>Штатное резервное копирование Битрикс24 делало файлы на 115–120 ГБ. Размер солидный, дата свежая, отметка о готовности на месте. Внутри копии не было таблиц <code>b_user</code>, <code>b_crm_deal</code>, <code>b_im_message</code>, <code>b_tasks_*</code>, <code>b_disk_*</code>, то есть всего, ради чего бэкап и делают.</p>
<p>Дамп обрывался на таблице <code>b_crm_access_attr_dynamic_1154</code>. Объяснение нашлось только в журнале событий, с фильтром <code>BACKUP_ERROR</code>:</p>
<pre><code>Таблица b_crm_access_attr_dynamic_1160 разрушена в результате внутренней ошибки MySQL.
</code></pre>
<h2 id="s2">Таблица не разрушена, ее нет</h2>
<p>Формулировка Битрикса ведет по ложному следу. Таблица <code>b_crm_access_attr_dynamic_1160</code> не повреждена: ее просто не существует. <code>SHOW CREATE TABLE</code> отвечает ошибкой 1146, «нет такой таблицы», а в словаре данных MySQL от нее не осталось даже записи.</p>
<p>Механизм оказался такой:</p>
<ol>
<li>В начале прогона Битрикс составляет список таблиц и дальше идет по нему.</li>
<li>На базе такого размера полный проход занимал около недели. Это видно по самому файлу: в имени дата 28 июля, а дописан он 4 августа.</li>
<li>За эту неделю на портале удалили смарт-процесс. Рядовая операция, ее делает любой администратор CRM. Вместе со смарт-процессом исчезла его служебная таблица прав.</li>
<li>Через несколько дней выгрузка дошла до этой таблицы по списку, не нашла ее и остановилась.</li>
<li>Таблицы выгружаются по алфавиту, а обрыв пришелся на начало. Все содержательное в копию не попало.</li>
</ol>
<p>Это не разовое невезение. В нумерации смарт-процессов на этом портале больше пятнадцати пропусков: их удаляют регулярно, и любой такой пропуск во время недельного прогона снова ломает копию.</p>
<h2 id="s3">Почему этого никто не видел</h2>
<p>Сбой копирования виден только как запись в журнале событий. Журнал чистится по сроку хранения, оповещений о сбоях нет. Администратор видит в списке файл нужного размера с нужной датой, и у него нет причин сомневаться.</p>
<p>Собственник тем более не видит ничего. В плане работ строка «резервное копирование настроено» стоит закрытой, и вспоминают о ней в день, когда копия понадобилась по-настоящему. В этот день и выясняется, что восстанавливать нечего.</p>
<h2 id="s4">Полная копия тоже может не развернуться</h2>
<p>Следующая копия прошла целиком: 2230 таблиц. При развертывании она упала на середине:</p>
<pre><code>ERROR 1062 (23000): Duplicate entry '7920399-1141386-2' for key 'b_crm_timeline_bind.PRIMARY'
</code></pre>
<p>Дубль первичного ключа значит, что одна и та же строка попала в выгрузку дважды. Битрикс читает таблицы порциями без транзакции, а портал в это время работает. У таблиц без числового ключа чтение идет через смещение (<code>LIMIT смещение, N</code>). Новая строка в середине таблицы сдвигает смещения, и часть строк выгружается повторно. Выгрузка идет часами, так что шанс на такой дубль высокий.</p>
<p>При заливке вручную это обходится: <code>INSERT</code> заменяется на <code>INSERT IGNORE</code>, и повторная строка отбрасывается. Потерь нет, это та же самая строка. Но сам факт важен: копия может содержать все таблицы и все равно не развернуться без ручной работы.</p>
<h2 id="s5">Что с этим делать</h2>
<p>Чинить таблицу бессмысленно, чинить нечего. Варианты такие:</p>
<ul>
<li><strong>Перезапустить копирование.</strong> Список таблиц соберется заново и будет верным. Но это рулетка на неделю: следующее удаление смарт-процесса или воронки снова все оборвет.</li>
<li><strong>На время прогона не удалять смарт-процессы, воронки и инфоблоки.</strong> Костыль на одну попытку, на портале с сотнями сотрудников его не удержать.</li>
<li><strong>Сократить длительность прогона.</strong> Это настоящее решение. На этом портале 182 ГБ базы занимал журнал бизнес-процессов <code>b_bp_tracking</code>, а рабочие данные (сделки, задачи, переписка, пользователи) весили около 15 ГБ. Без журнала копирование превращается из недельного в часовое, и окно для такого сбоя почти закрывается.</li>
</ul>
<p>И одно правило, которое работает всегда: <strong>бэкап проверен только тогда, когда из него восстановились.</strong> Размер файла, дата и зеленая отметка не значат ничего.</p>
<h2 id="s6">Как проверить свою копию</h2>
<p>Быстрая проверка: есть ли в дампе главные таблицы. Если пользователей в копии нет, дальше можно не смотреть:</p>
<pre><code class="language-bash">grep -c '^DROP TABLE IF EXISTS `b_user`' dump.sql
</code></pre>
<p>Сколько всего таблиц в дампе, сравнить с числом таблиц в живой базе:</p>
<pre><code class="language-bash">LC_ALL=C grep -ohE '^DROP TABLE IF EXISTS `[^`]+`' dump.sql | sort -u | wc -l
</code></pre>
<p>Совпадают ли служебные таблицы прав со смарт-процессами, которые реально существуют. Если таблиц больше или меньше, чем живых типов, следующий прогон может споткнуться:</p>
<pre><code class="language-sql">SELECT ENTITY_TYPE_ID, TITLE FROM b_crm_dynamic_type ORDER BY ENTITY_TYPE_ID;

SELECT TABLE_NAME FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE 'b_crm_access_attr_dynamic_%';
</code></pre>
<p>И раз в месяц или раз в квартал разворачивать копию на отдельном сервере. Это единственная проверка, после которой можно сказать, что бэкап есть.</p>]]></description>
    </item>
    <item>
      <title>Копейка между Битрикс24 и 1С: почему суммы счетов не сходятся</title>
      <link>https://eliseevdenis.ru/blog/bitrix24-1c-kopeyka/</link>
      <guid isPermaLink="true">https://eliseevdenis.ru/blog/bitrix24-1c-kopeyka/</guid>
      <pubDate>Sat, 24 Oct 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[<p>Суммы счетов в Битрикс24 и в 1С расходятся на копейки. Не везде, но регулярно, и виноватой по привычке считают 1С: Битрикс же показывает «правильную» сумму.</p>
<p>Разбор 7753 товарных строк показал обратное. 1С считает правильно. Копейки рождаются в Битрикс24, и в документации об этом нет ни слова.</p>
<h2 id="s1">Откуда берутся копейки</h2>
<p>Смарт-счета Битрикс24 не округляют цену до копейки, когда сохраняют товарную строку. В базе лежат цены вида <code>805.23809523</code>, <code>232.66285715</code>, <code>3784.725</code>. Из 7753 строк на этом портале цена без налога длиннее копейки у 6996, это 90%.</p>
<p>Как такая цена появляется:</p>
<ul>
<li><strong>Менеджер вводит сумму строки, а не цену.</strong> Битрикс24 делит сумму на количество и хранит частное как есть: <code>4 × 1625.00000001 = 6500.00</code>.</li>
<li><strong>Процентная скидка.</strong> Она была в 7737 строках из 7753, и почти любая скидка в процентах дает цену с хвостом.</li>
<li><strong>Ставка 5%.</strong> Цену с налогом делят на 1,05, а это почти всегда бесконечная дробь.</li>
</ul>
<p>1С так хранить не может: в документе цена в рублях и копейках. При обмене она усекает цену до копейки, а ошибка умножается на количество. При количестве 214 одна строка дает расхождение до 43 копеек. Худший счет на портале разошелся на 94 копейки.</p>
<h2 id="s2">Битрикс24 считает один документ двумя способами</h2>
<p>Вторая причина тоньше. Внутри одного счета Битрикс24 округляет по-разному:</p>
<ul>
<li><strong>сумму счета</strong> считает от итога: сложил все строки, округлил один раз в конце;</li>
<li><strong>налог</strong> считает построчно: округлил НДС каждой строки, потом сложил.</li>
</ul>
<p>На 405 счетах, где эти способы дают разный результат, налог совпал с построчным расчетом в 404. Формулы пришлось восстанавливать по коду ядра (<code>CCrmProductRow::preparePrices</code>) и проверять на всех 7753 строках: в документации REST поля цены описаны без формул.</p>
<h2 id="s3">Почему подгонять 1С нельзя</h2>
<p>Первая мысль обычно такая: настроить округление в 1С, чтобы сходилось с Битриксом. Но 1С здесь права по закону.</p>
<ul>
<li>Налоговый кодекс (п. 2 ст. 168) требует считать НДС по каждой позиции. Значит, верна построчная сумма налога.</li>
<li>Правила заполнения счета-фактуры (постановление № 1137) требуют рубли и копейки. Округлять до рублей нельзя.</li>
</ul>
<p>Получается, что «чинить» 1С значит сломать ей правильный расчет ради совпадения с неправильным.</p>
<h2 id="s4">Что не помогает</h2>
<p>Это проверено, чтобы не тратить время:</p>
<ul>
<li><strong>«Правила округления цен» в Битрикс24.</strong> Это механизм торгового каталога, к товарным строкам CRM-счета он не применяется.</li>
<li><strong>Настройки точности и правка типа поля в базе.</strong> Работает только на коробке, а здесь облако: доступа к файлам и базе нет.</li>
<li><strong>Округление в 1С.</strong> Противоречит норме, см. выше.</li>
</ul>
<h2 id="s5">Что передавать в 1С</h2>
<p>Решение на стороне обмена:</p>
<ol>
<li><strong>Передавать сумму строки, а не цену.</strong> Сумма строки в Битрикс24 правильная, кривая только цена, полученная делением.</li>
<li><strong>Итог документа считать как сумму округленных строк.</strong> Это не поле «Сумма» (<code>opportunity</code>) из карточки счета: оно посчитано от итога.</li>
<li><strong>НДС считать построчно</strong>, как требует закон.</li>
<li><strong>Если итог задан жестко, копейку разницы отнести на одну строку,</strong> с самой большой суммой. Не размазывать по всем строкам. В 1С:Управление торговлей 11 для этого есть специальный тип скидки, «СкидкаНаценкаОкругление».</li>
</ol>
<h2 id="s6">Две ловушки по дороге</h2>
<p><strong>Метод <code>crm.item.productrow.list</code> отдает не больше 50 строк.</strong> Он постраничный, и без обхода страниц строки счета молча обрезаются на пятидесятой. На этом портале так обрезались 4 счета, до 58 строк в каждом. В первом проходе они дали «необъяснимый» НДС, а после дозагрузки строк сошлись. Если ваша интеграция с 1С забирает строки этим методом, проверьте, обходит ли она страницы. Иначе в 1С уходят неполные счета.</p>
<p><strong>Флаг «налог включен в цену» не всегда правдив.</strong> Нашелся счет на 51 строку, где флаг снят, а цена уже с налогом: 441 при цене без налога 420, ровно 420 × 1,05. Принимающая система начислит НДС второй раз, и расхождение будет уже не копеечным, а в 5%.</p>]]></description>
    </item>
    <item>
      <title>Открытые линии Битрикс24 не закрываются: сообщения из Wazzup приходят из 58520 года</title>
      <link>https://eliseevdenis.ru/blog/wazzup-open-lines-58520/</link>
      <guid isPermaLink="true">https://eliseevdenis.ru/blog/wazzup-open-lines-58520/</guid>
      <pubDate>Sat, 24 Oct 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[<p>Сессии открытых линий перестали закрываться сами. Автозакрытие настроено, таймауты стоят, а диалоги висят открытыми неделями. У операторов растут счетчики непрочитанного, база данных пухнет.</p>
<p>Причина нашлась в логе: почти все входящие сообщения из WhatsApp приходили на портал с датой в 58520 году.</p>
<h2 id="s1">Что нашлось в логе</h2>
<p>Для разбора взял <code>imopenlines.log</code> за пять суток: 57 МБ, около 30 тысяч событий. 96% трафика открытых линий на этом портале шло через Wazzup (WhatsApp).</p>
<p>Во входящих событиях <code>CONNECTOR - ENTITY ADD</code> у сообщения есть дата. В 7 111 случаях из 7 224 (98,4%) она выглядела так:</p>
<pre><code>[date] =&gt; 58520-02-06 18:00:01.000000
</code></pre>
<p>Рядом в том же логе лежали нормальные даты: <code>1784705238</code> и <code>2026-07-20T06:45:25-06:00</code>. Значит, портал умеет принимать время правильно, а ломает его один источник.</p>
<h2 id="s2">Откуда 58520 год</h2>
<p>При тех настройках интеграции, что стояли на портале, Wazzup отдавал время сообщения в миллисекундах: <code>1784705238000</code>, тринадцать цифр. Коннектор на стороне Битрикс24 читает это число как секунды. 1 784 705 238 000 секунд от 1970 года как раз дают 58 520 год.</p>
<p>Правильное значение в секундах, <code>1784705238</code>, это 20 июля 2026. Ошибка в три нуля.</p>
<h2 id="s3">Почему это не косметика</h2>
<p>Неверная дата в карточке сообщения выглядит как мелочь. На деле она выключает уборку.</p>
<p>Агенты открытых линий закрывают и чистят сессии по простому условию: последнее изменение было раньше, чем «сейчас минус таймаут». Сессия, у которой дата в 58520 году, под это условие не попадет никогда. Она остается открытой навсегда, и вместе с ней остается все, что к ней привязано:</p>
<ul>
<li>чат и его участники;</li>
<li>счетчики непрочитанного у операторов;</li>
<li>служебные записи сессии, индексы, статистика по ответам;</li>
<li>место в списке недавних диалогов у каждого оператора: портал перебирает их при каждом открытии чатов.</li>
</ul>
<p>Индексы по дате теряют смысл, когда в таблице есть записи «из будущего», и выборки по истории начинают читать больше, чем нужно. В том же логе 81 ошибка <code>NO LAST SESSION</code>: портал не может найти последнюю сессию клиента. Скорее всего, это тоже следствие битых дат.</p>
<p>Масштаб такой: открытые линии на этом портале создавали 83% всех сообщений, около 4 тысяч в сутки, полтора миллиона в год. Каждая сессия, которая не закрылась, оставляет след в базе навсегда.</p>
<h2 id="s4">Почему этого не видно при подключении</h2>
<p>Интеграцию проверяют так: написали в WhatsApp, сообщение пришло в Битрикс24, оператор ответил, клиент получил ответ. Все работает, задачу закрывают.</p>
<p>Дату сообщения в этой проверке никто не смотрит. В интерфейсе она почти не видна, а последствия копятся в базе месяцами. Когда портал начинает тормозить, интеграцию уже давно считают рабочей, и искать начинают в другом месте: железо, кеш, «Битрикс тяжелый».</p>
<p>Это типичная история с интеграциями. Проверяют, что данные дошли. Не проверяют, в каком виде они легли и что с ними будет через полгода.</p>
<h2 id="s5">Что сделали</h2>
<p>Чинили в три шага, и порядок тут важен. Потом поставили контроль на будущее.</p>
<p><strong>Сначала остановили поток битых дат.</strong> Пока новые сообщения приходят из 58520 года, любая уборка бесполезна: через час появятся новые вечные сессии. Чинить стали там, где ошибка рождается, на стороне Wazzup: настроили отправку даты в том формате, который ждет Битрикс24. Код портала при этом не трогали, так что обновление Битрикс24 правку не откатит.</p>
<p>Проверить результат просто. В <code>imopenlines.log</code> у новых событий <code>CONNECTOR - ENTITY ADD</code> дата снова сегодняшняя, а число сообщений из будущего (первый запрос в конце статьи) перестает расти. После смены настройки дата начала приходить в нужном формате.</p>
<p><strong>Потом закрыли то, что уже зависло.</strong> Новые сообщения стали нормальными, а старые сессии так и остались с датой в будущем. Автозакрытие их не возьмет никогда, поэтому написали скрипт, который закрыл все текущие открытые чаты.</p>
<p>Закрывать нужно именно закрытием, а не удалением строк из базы. Сессия открытой линии связана с чатом, его участниками, счетчиками непрочитанного у операторов и статистикой. Если стереть сессию, а остальное оставить, получатся сироты: чат без сессии висит у оператора в списке, счетчик не обнуляется, а портал при обращении к такому диалогу пишет в лог ошибки вроде <code>NOT_LOAD_SESSION_OR_CHAT</code>. Пара таких ошибок в логе уже была. Штатное закрытие освобождает чат, снимает с него операторов, обнуляет их счетчики и записывает итог в статистику, то есть делает ту работу, которую должно было сделать автозакрытие.</p>
<p><strong>И только после этого чистили раздутую базу.</strong> Аккуратно: таблицы открытых линий и сообщений большие, и неосторожное удаление на живом портале обходится дорого. Перед такой чисткой нужна свежая копия базы: откатить удаление без нее нечем.</p>
<p><strong>Напоследок поставили сторожа.</strong> Настройку интеграции могут случайно поменять, а Wazzup может обновить свою сторону, и тогда даты снова поедут. Узнавать об этом через полгода по тормозам портала не хочется, поэтому добавили простой контроль через списки Битрикс24. Каждое входящее сообщение создает элемент списка, записи живут один день. Если год в дате сообщения отличается от текущего, уведомление сразу уходит администратору Битрикс24 и директору IT-департамента. Теперь о такой аварии узнают сразу и успевают отреагировать до того, как база снова разрастется.</p>
<p>Попутно в том же логе нашлась вторая причина висящих сессий, уже не техническая. Сотрудники подхватывали чужие диалоги, но у их роли не было права «закрывать чужие диалоги». Попытка закрыть падала с ошибкой <code>ERROR_USER_NOT_OPERATOR</code>, 60–90 раз в сутки, и диалог оставался открытым. Это лечится одной галочкой в правах линии.</p>
<h2 id="s6">Как проверить у себя</h2>
<p>Если на коробочном портале открытые линии работают через Wazzup или другой коннектор мессенджеров, эти запросы покажут, есть ли проблема:</p>
<pre><code class="language-sql">-- сообщения и сессии с датой больше чем на год вперед
SELECT COUNT(*) FROM b_im_message          WHERE DATE_CREATE &gt; NOW() + INTERVAL 1 YEAR;
SELECT COUNT(*) FROM b_imopenlines_session WHERE DATE_CREATE &gt; NOW() + INTERVAL 1 YEAR;

-- сколько сессий сейчас открыто
SELECT COUNT(*) FROM b_imopenlines_session WHERE CLOSED = 'N';
</code></pre>
<p>Если MySQL работает не в строгом режиме, вместо даты из будущего может лежать <code>0000-00-00</code>. Ищите и ее.</p>
<p>Если открытых сессий тысячи, а операторы ведут десятки диалогов, стоит посмотреть в <code>imopenlines.log</code> поле <code>[message][date]</code> в событиях <code>CONNECTOR - ENTITY ADD</code>. И заодно проверить в настройках линии, могут ли сотрудники закрывать диалоги, которые взяли у коллег.</p>]]></description>
    </item>
  </channel>
</rss>
