Признание
В первой статье про невидимый символ GS я закончил так:
«Восстановить потерянный GS в общем случае нельзя. Можно перебрать варианты и надеяться, что уцелеет ровно один. На кодах с длинным криптохвостом — а 91, 92, 93 это до 90 символов каждый — надеяться особо не на что.»
Это неверно. Причём неверно не в мелочи, а в том самом выводе, ради которого статья писалась.
В обсуждении подсказали другой подход: расставлять GS не перебором вариантов, а по данным уже состоявшегося разбора. Направление верное — и именно оно заставило меня вернуться в модуль. Там выяснилось, что ровно по этой логике типовая уже умеет.
В том же общем модуле РазборКодаМаркировкиИССлужебныйКлиентСервер, из которого я цитировал РазделительGS(), лежит готовая сборка кода маркировки с расстановкой разделителей. В той же самой выгрузке. Четырьмя сотнями строк выше того места, куда я смотрел.
Дальше — что это за механизм, чем он управляется и где у него граница. Всё проверено прогоном на реальных кодах, протокол в конце.
Функция, которую я не заметил
Строка 791:
Функция ПолныйКодМаркировкиИзЗначенийЭлементов(ЗначенияЭлементовКМ,
ОписаниеЭлементовКодаМаркировки,
ИдентификаторыПримененияСРазделителемGS = Неопределено) Экспорт
Она собирает полный код маркировки из разобранных элементов. Внутри — три решения, и каждое закрывает край, о который я в первой статье споткнулся.
Первое. Формат определяется не догадкой, а по первому элементу:
Если КоличествоЭлементов > 0 Тогда
ВключаетИдентификаторыПрименения = ЗначениеЗаполнено(ОписаниеЭлементовКодаМаркировки[0].Код);
Иначе
ВключаетИдентификаторыПрименения = Ложь;
КонецЕсли;
Если идентификаторов нет — а у табачной пачки они заданы пустыми строками, разбор там чисто позиционный — функция уходит в другую ветку и просто склеивает значения, ничего между ними не вставляя. Мою пачку сигарет, на которой ручное правило «ставить разделитель перед всем, кроме 01 и 21» сбоило, она бы не испортила.
Второе. Разделитель ставится не по списку исключений, а по набору:
Если ИдентификаторыПримененияСРазделителемGS[КодЭлемента] <> Неопределено Тогда
Если (ИндексЭлемента + 1) < КоличествоЭлементов Тогда
СтрокиПолногоКодаМаркировки.Добавить(РазделительGS);
КонецЕсли;
КонецЕсли;
Набор берётся из справочника кодов GS1 по признаку ЕстьРазделитель — том самом, который я разбирал в первой статье:
Функция ИдентификаторыПримененияСРазделителемGS() Экспорт
Идентификаторы = Новый Соответствие;
КодыGS1 = РазборКодаМаркировкиИССлужебныйКлиентСерверПовтИсп.КодыGS1();
Для Каждого ДанныеКода Из КодыGS1 Цикл
Если ДанныеКода.Значение.ЕстьРазделитель Тогда
Идентификаторы.Вставить(ДанныеКода.Ключ, Истина);
КонецЕсли;
КонецЦикла;
Возврат Идентификаторы;
КонецФункции
На стенде я его распечатал: 88 идентификаторов. Там и ожидаемые 21, 91, 92, 93, и 10 (номер партии), и 8005, у которого длина фиксированная, а разделитель всё равно нужен. Ровно тот случай, про который я в первой статье написал упрощённо «переменная длина — разделитель нужен, фиксированная — нет», а потом сам же в комментариях уточнял, что упрощение неточное.
Третье. После последнего элемента разделитель не ставится — разница в один символ, но без неё собранный код уже не совпадёт с оригиналом побайтово.
Развилка, ради которой стоило лезть дальше
Функция сборки сама по себе ничего не решает — её надо кому-то вызвать. Публичная точка входа лежит на пятьдесят строк выше, на строке 734:
Функция ПолныйКодМаркировкиПоДаннымРазбораСРасширеннойДетализацией(
РезультатРазбора,
ВосстанавливатьСтруктуруКодаМаркировки = Ложь,
КодМаркировки = Неопределено) Экспорт
И вот её сердцевина:
ИначеЕсли Детализация.ВключаетИдентификаторыПрименения Тогда
Если Детализация.СодержитРазделительGS Тогда
Возврат КодМаркировки;
ИначеЕсли ВосстанавливатьСтруктуруКодаМаркировки Тогда
Возврат ПолныйКодМаркировкиИзЗначенийЭлементов(
Детализация.ЗначенияЭлементовКодаМаркировки,
Детализация.ОписаниеЭлементовКодаМаркировки);
КонецЕсли;
Возврат Неопределено;
КонецЕсли;
Три исхода. Разделители на месте — вернуть как есть. Разделителей нет, но флаг включён — собрать заново. Разделителей нет и флаг выключен — вернуть Неопределено, то есть «полного кода нет».
А Неопределено дальше по стеку превращается вот в это:
Формат кода маркировки некорректный:
В структуре кода маркировки отсутствуют разделители GS
Ту самую строчку, которую гуглят тысячами.

Рис. 1 — три исхода на одной развилке
Чем управляется флаг
Дальше я пошёл искать, кто выставляет ВосстанавливатьСтруктуруКодаМаркировки. Нашёл в модуле ИнтеграцияИСМПСлужебный — четыре раза, строки 10644, 11343, 11702 и 13816, и везде одинаково:
ПараметрыРазбора.Вставить("ВосстанавливатьСтруктуруКодаМаркировки",
Не ПараметрыСканирования.ПроверятьСтруктуруКодовМаркировки);
Прочитайте ещё раз, там одно слово решает всё. Не.
Восстановление разделителей включается тогда, когда выключена настройка «Проверять структуру кодов маркировки».
Теперь вспомните, что советуют на всех форумах, когда вылезает ошибка про отсутствующие разделители. Снять эту галку. Совет работает — люди подтверждают. И никто не объясняет почему, потому что объяснение выглядит как «ну, отключили проверку, вот она и не ругается».
Объяснение другое. Галка не отключает контроль. Она переключает режим: с «отвергать код без разделителей» на «восстановить их по описанию элементов». Человек думает, что затыкает проверку, а на самом деле включает функциональность.
Дальше я пошёл искать саму галку в интерфейсе — и нашёл три вещи подряд, каждая интереснее предыдущей.
Первая: галка есть. Обработка ПанельАдминистрированияИСМП, форма НастройкиИСМП, поле ПроверятьСтруктуруКодовМаркировки. Хранится всё в константе НастройкиСканированияКодовМаркировкиИСМП, значение по умолчанию — Истина. То есть по умолчанию восстановление выключено.
Вторая: галка скрыта. Вот её описание в форме:
<CheckBoxField name="ПроверятьСтруктуруКодовМаркировки" id="442">
<DataPath>ПроверятьСтруктуруКодовМаркировки</DataPath>
<UserVisible>
<xr:Common>false</xr:Common>
</UserVisible>
UserVisible = false. Поле на форме есть, но пользователю по умолчанию не показывается — чтобы его увидеть, надо лезть в «Изменить форму». Одинаково в «Управлении торговлей» 11.5.27.79 и в «Рознице» 2.3.25.23, я сверил обе выгрузки.
Это, кстати, объясняет половину форумных мучений: людям советуют снять галку, а они её не находят.
Третья: имя группы. Галка лежит в группе, у которой заголовок такой:
Группа восстанавливать структуру кода маркировки
Группа называется «восстанавливать», галка внутри неё — «проверять», и между ними в коде стоит Не. Разработчик, который это писал, прекрасно понимал, что переключатель делает. Просто наружу вынес обратную формулировку.
Добивает подсказка самой галки:
«Проверять структуру кодов при передаче отчета производственной линии и сканировании: код должен полностью соответствовать требованиям ЦРПТ (содержать криптохвост, разделители GS, и т.д.).»
Про то, что снятие включает восстановление, — ни слова.
И есть ещё четвёртое место, где это имя всплывает наружу. В модуле НастройкаПараметровСканированияСлужебныйКлиентСерверПовтИсп лежит таблица человекочитаемых названий параметров — она используется при логировании запросов к ИС МП. Строка оттуда:
ВозвращаемоеЗначение.Вставить(
"ПроверятьСтруктуруКодовМаркировки",
НСтр("ru = 'Восстанавливать структуру кода маркировки'"));
В логе тот же параметр показывается под именем «Восстанавливать структуру кода маркировки». То есть в одном месте конфигурации он «проверять», в другом «восстанавливать», и это один и тот же флаг.
Это, кстати, второй раз, когда я упираюсь в галку с неочевидным смыслом. Про предыдущую — «снимите и поставьте обратно, помогает» — у меня была отдельная статья. Начинаю подозревать, что это жанр.

Рис. 2 — что на самом деле переключает галка
Прогон
По коду всё выглядело однозначно, но публиковать вывод по чтению исходников после того, как я уже один раз ошибся, было бы странно. Поэтому — стенд.
База: «Управление торговлей» 11.5.27.79, платформа 8.5.1.1302. Коды — реальные, снятые с упаковок, тот же корпус, что я собирал для первой статьи. Методика простая: взять код с разделителями, удалить их, разобрать получившуюся строку, вызвать функцию сборки дважды — с флагом Ложь и с флагом Истина — и сравнить результат с исходной строкой целиком, а не по длине.
Вот что получилось:
|
Код |
Разбор без GS |
Флаг ВЫКЛ |
Флаг ВКЛ |
Совпало с оригиналом |
|
БАД, 01+21+91+92, 85 знаков |
успех |
Неопределено |
восстановлен |
ДА |
|
Вода, 01+21+93, 38 знаков |
успех |
Неопределено |
восстановлен |
ДА |
|
Вода вторая, 01+21+93, 38 знаков |
успех |
Неопределено |
восстановлен |
ДА |
|
Молоко, 01+21+93, 31 знак |
успех |
Неопределено |
восстановлен |
ДА |
Четыре из четырёх. Побайтово.
В том же прогоне участвовали ещё два кода, у которых разделителей нет по формату — снимать и восстанавливать там нечего, но они показывают границу разбора. Обувная марка 0104610331124908215dnTVyUII>ZI! — 01+21 без криптохвоста, 31 знак — разобралась штатно. А табачная пачка ESSE — нет. С ней вышла отдельная история, к ней ниже.
Вот кусок протокола по коду БАД — тому самому, про который я писал «с длинным криптохвостом надеяться особо не на что»:
БАД, 01+21+91+92
исходный: 0104670001387927215+sUe3d;g?vmy<GS>91EE11<GS>92Kbmc6NzwH/C/XwJmt9WWb4mPEon42u9ow2wbfTdUJiM= (85, разделителей: 2)
разбор ИСХОДНОГО: успех
разбор БЕЗ GS: успех
флаг ВЫКЛ: Неопределено
флаг ВКЛ: 0104670001387927215+sUe3d;g?vmy<GS>91EE11<GS>92Kbmc6NzwH/C/XwJmt9WWb4mPEon42u9ow2wbfTdUJiM=
СОВПАЛО С ОРИГИНАЛОМ: ДА
Криптохвост в 44 символа, два разделителя, оба восстановлены на своих местах.

Рис. 3 — путь кода: потеря, разбор, сборка
Где граница
Теперь честно про то, чего механизм не делает — иначе получится, что я поменял одну неверную крайность на другую.
Восстановление работает поверх успешного разбора. Функция сборки берёт на вход ЗначенияЭлементовКодаМаркировки и ОписаниеЭлементовКодаМаркировки — то есть результат уже состоявшегося разбора. Если разбор не прошёл, восстанавливать нечего: элементов нет.
Значит верным в первой статье осталось вот что: информация о границе поля действительно утеряна вместе с символом. Просто типовая восстанавливает её не гаданием, а по описанию элементов конкретного шаблона товарной группы. Граница проходит не по длине криптохвоста, как я писал, а по тому, сумел ли разбор опознать шаблон.
Позиционным табачным кодам нужен явный вид продукции. Здесь я сначала написал неправду и хочу разобрать, как именно.
Прогнал код с пачки ESSE — 04640030090525Cf3qCq7AD!o9OLZ, 29 знаков, разделителей ноль по формату. Разбор не удался. Передал вид продукции — Перечисления.ВидыПродукцииИС.Табак — снова не удался. Вывод напрашивался: у табака отдельная ветка МОТП, общий разбор его не берёт. Я так и записал.
Потом полез в настройки разбора и получил исключение:
Фильтр по виду продукции в настройках разбора кода маркировки задан не верно.
Смотрю в код проверки:
Если ЗначениеЗаполнено(ВидыПродукции) И ТипЗнч(ВидыПродукции) = Тип("СправочникСсылка.ВидыПродукцииИС") Тогда
`ВидыПродукцииИС` — справочник, а не перечисление. А я передавал ПеречислениеСсылка. Фильтр молча отбрасывал значение как неподходящее по типу, оставался пустым, и разбор шёл так, будто вида продукции я не указывал вовсе.
Перепрогнал правильно — взял ссылку из справочника, элемент «Табачная продукция». И взял не один код, а четыре реальные табачные марки — включая ту же пачку ESSE:
04601653041066o,Y?E*OACuU7+hy все виды: не разобран фильтр «Табачная продукция»: РАЗОБРАН
04670404340741C46yxOnADVUB9hf все виды: не разобран фильтр «Табачная продукция»: РАЗОБРАН
04650190052691RxE&ZCSAAAArEL1 все виды: не разобран фильтр «Табачная продукция»: РАЗОБРАН
04640030090525Cf3qCq7AD!o9OLZ все виды: не разобран фильтр «Табачная продукция»: РАЗОБРАН
Четыре из четырёх. Общая функция разбирает табак прекрасно — просто ей надо сказать, что это табак.
И это логично. Позиционный код — это 29 цифробуквенных знаков без единой метки. Отличить его от чего угодно другого можно только по шаблону товарной группы, а шаблон берётся из вида продукции. Без него алгоритму не за что зацепиться, и он честно отвечает «не соответствует ни одному шаблону» — та самая ошибка, которой я посвятил отдельную статью, и там первой причиной как раз стояло «учёт маркируемой продукции не ведётся».
Так что про МОТП я написал зря. Пятнадцать модулей с этим суффиксом в конфигурации действительно есть, и своя специфика у табака есть — серая зона, МРЦ, контроль статусов. Но разбор кода идёт общий.
Коды с датами и номером партии я не проверял. В моём корпусе нет ни 10 (BATCH_LOT), ни 11–17. По коду они закрываются тем же набором из 88 идентификаторов, но на реальных кодах я это не гонял и утверждать не буду.
Как я чуть не написал вторую неправду
Отдельно расскажу про свою же методическую ошибку, потому что она поучительнее самого результата.
Первые два прогона я сделал на кодах, которые собрал руками: взял из корпуса структуру, а GTIN и хвосты подставил произвольные — 93dGVz, 93Zjk3, выглядит же как настоящий. Вода и молоко на них не разобрались вообще. Я уже почти написал вывод: «коды вида 01+21+93 без разделителей не восстанавливаются, механизм работает только на длинных».
Потом полез сверить с корпусом и увидел, что реальные коды другие. Перегнал на настоящих — все четыре восстановились.
То есть я едва не опубликовал второе ошибочное утверждение, ровно в статье, которая исправляет первое. И выглядело оно убедительно: два кода из трёх не прошли, картина складывалась.
Вывод такой: тестовые данные для проверки формата придумывать нельзя. Формат — это и есть данные. Синтетический код проверяет не механизм, а вашу фантазию о механизме, и отрицательный результат на нём ничего не значит.
И вторая ошибка того же рода — с типом параметра. Я передал ПеречислениеСсылка.ВидыПродукцииИС туда, где ждут СправочникСсылка.ВидыПродукцииИС. В конфигурации есть оба объекта с одинаковым именем, а фильтр отбрасывает значение неподходящего типа молча, без исключения. Получился отрицательный результат, который выглядел как факт про табак, а был фактом про мой вызов.
У обеих ошибок общее свойство: отрицательный результат ничего не доказывает, пока не доказано, что инструмент исправен. Если бы я в тот же прогон добавил заведомо рабочий контрольный случай, обе поймались бы сразу. В финальных прогонах я так и сделал — разбирал сначала исходный код с разделителями и только потом обкромсанный.
Что с этим делать на практике
Если у вас вылезает «В структуре кода маркировки отсутствуют разделители GS» — теперь понятно, что происходит и какой ценой лечится.
Снятая галка «Проверять структуру кодов маркировки» действительно уберёт ошибку, и уберёт не заметанием под ковёр: коды будут собираться обратно с разделителями на своих местах. Но вместе с этим вы теряете отсечку по структуре на всём потоке — включая те коды, где повреждено что-то другое.
Разумнее сначала понять, откуда приходят коды без разделителей. Если это выгрузка через xlsx — там символ теряется не по капризу, а потому что XML запрещает коды 14–31, и лечится это форматом передачи, а не настройкой в 1С. Если промежуточная система чистит управляющие символы — тем более.
Галка — рабочий обходной путь, а не решение. Разница в том, что теперь вы знаете, что именно она включает.
Осадок
Первую статью я закончил мыслью, что надо лезть в типовую первым делом, а не последним, потому что там обычно уже кто-то ходил. Мысль правильная. Только я в неё залез и всё равно не увидел то, что лежало в том же модуле.
Причина, по-моему, простая. Я искал подтверждение красивой находке — перебору вариантов, — и нашёл его. Функция ВариантыРазбораШтрихкодаGS1БезРазделителей реально существует, реально перебирает все длины и реально возвращает массив. Я на неё посмотрел и решил, что вопрос закрыт: раз в типовой перебор, значит однозначно не восстановить. А рядом лежала детерминированная сборка для случая, когда шаблон известен, и её я не искал, потому что уже был уверен в ответе.
Читать исходники — недостаточно. Надо ещё не решать заранее, что ты в них ищешь.
И спасибо тем, кто разбирал это в комментариях к первой статье: без этого обсуждения я в модуль не вернулся бы вовсе.
Проверено 30 августа 2026 года. Код — по выгрузкам конфигураций «Управление торговлей» 11.5.27.79, «Розница» 2.3.25.23 и «Бухгалтерия предприятия» 3.0.111.25: перечисленные функции есть во всех трёх, в БП — на строке 784. Прогоны выполнены в базе «Управление торговлей» 11.5.27.79 на платформе 8.5.1.1302, в режиме только чтения, протоколы приложены. Всего в прогонах участвовало девять реальных кодов с упаковок: БАД, две воды, молоко, обувь и четыре табачные пачки.
Вступайте в нашу телеграмм-группу Инфостарт