О чём это
Я больше года веду расширения к боевым базам УТ 11.5 в розничной сети: бонусы, скидки, интеграция касс с внешним бэкендом, обмен с сайтом. Ошибки здесь стоят дорого — на другом конце стоит кассир с очередью.
Ниже — одиннадцать вещей, каждая из которых съела у меня от часа до вечера. Все проверены на боевых базах, ни одна не является теорией. Часть касается языка и компиляции, часть — запросов, часть — типовой модели данных.
Если хотя бы три из них вам незнакомы, статья окупится.
1. Расширение не видит общие модули другого расширения
Ситуация: в базе два расширения, во втором нужен вызов функции из общего модуля первого. Пишете как обычно:
Результат = МоеРасширение_Продажи.СобратьСтруктуру(Ссылка);
Гейт компиляции отвечает:
Переменная не определена (МоеРасширение_Продажи)
Это не опечатка и не проблема прав. Общие модули одного расширения не видны напрямую из другого расширения — они не попадают в глобальный контекст соседа.
Обход — получить модуль по имени через типовой механизм:
Модуль = ОбщегоНазначения.ОбщийМодуль("МоеРасширение_Продажи");
Результат = Модуль.СобратьСтруктуру(Ссылка);
Работает, но обратите внимание: связь стала строковой. Компилятор больше не проверит имя, ошибка вылезет только в рантайме. Если имя модуля изменится, вы узнаете об этом от пользователя.
2. Зарезервированные слова, о которых не думаешь
Есть слова, которые нельзя использовать как имена переменных и как псевдонимы полей в запросах. Часть из них выглядит совершенно безобидно:
ЗначНовыйВ
Последнее особенно коварно: В — оператор запроса (ГДЕ Поле В (&Список)), и попытка назвать так псевдоним колонки ломает разбор.
Диагностика хуже самой ошибки. Компилятор выдаёт «Неопознанный оператор» и следом каскад ошибок в строках ниже — потому что после сбоя разбора он теряет структуру. Вы начинаете чинить последнюю ошибку из списка, хотя реальная причина в первой.
Правило: при каскаде ошибок в модуле всегда чините самую верхнюю, остальные с высокой вероятностью её эхо.
3. Тело модуля объекта должно идти последним
В модуле объекта или менеджера всё, что не обёрнуто в процедуру или функцию, считается телом модуля и выполняется при инициализации.
Тело модуля обязано идти после всех объявлений. Если случайно оставить исполняемую строку между двумя процедурами — например, при копипасте — компилятор сообщит об ошибке в месте, которое к причине отношения не имеет.
Формально это описано, но при правке большого модуля через дамп в файлах, когда вы не видите структуру целиком в конфигураторе, нарушить порядок очень легко.
4. У ряда справочников длина кода равна нулю
Самая дорогая для меня грабля из всего списка, потому что она тихая.
У справочников Склады и КассыККМ в УТ 11.5 CodeLength = 0. Это значит, что реквизита Код у них не существует физически.
В запросе это проявляется честно:
Поле не найдено "СЧ.Ссылка.Склад.Код"
А вот в объектной модели — нет. Обращение Склад.Код возвращает пустую строку без всякой ошибки. Код компилируется, выполняется, ничего не падает.
Последствия у меня были такие: расширение годами отправляло во внешний бэкенд поле shopcode, собранное из Склад.Код. Оно всегда было пустым. Никто этого не замечал, потому что приёмник просто клал пустую строку в базу, а магазин определялся другим способом.
Вывод: магазин и кассу нужно матчить по наименованию или по GUID ссылки, а не по коду. И шире — если поле в интеграции всегда пустое, проверьте, существует ли оно вообще у источника.
5. Продавец лежит в табличной части, а не в шапке
У документа ЧекККМ в шапке есть Кассир. Логично предположить, что Продавец там же.
Его там нет. Продавец — реквизит табличной части «Товары». Каждая строка чека может иметь своего продавца.
Отдельная засада в том, как это обнаруживается. Если вы ищете реквизит грепом по выгруженному XML документа, то <Name>Продавец</Name> найдётся — но по виду фрагмента невозможно понять, шапка это или табличная часть, разметка одинаковая. Нужно смотреть контекст на несколько уровней вверх.
Я потратил на это заметное время, будучи уверенным, что реквизит есть в шапке, и получая пустое значение.
6. ВЫБРАТЬ ПЕРВЫЕ не принимает параметр
Естественная попытка ограничить выборку параметром:
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ &Лимит Ссылка ИЗ Справочник.Номенклатура";
Запрос.УстановитьПараметр("Лимит", 100);
Не работает. Число в ВЫБРАТЬ ПЕРВЫЕ должно быть литералом, параметризации там нет.
Единственный способ — собрать текст запроса конкатенацией:
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ " + Формат(Лимит, "ЧГ=0") + "
| Ссылка
|ИЗ Справочник.Номенклатура";
Формат(..., "ЧГ=0") здесь обязателен: без него число с разделителем групп разрядов превратится в 1 000 и сломает запрос. И, разумеется, Лимит должен быть числом, а не пришедшей снаружи строкой — иначе вы только что открыли инъекцию в текст запроса.
7. Создание объекта кодом не заполняет то, что заполняет форма
Грабля, которая обошлась дороже всех остальных, потому что мой собственный контроль показывал, что всё в порядке.
Задача была завести виды карт лояльности программно:
Элемент = Справочники.ВидыКартЛояльности.СоздатьЭлемент();
Элемент.Наименование = "Промокод 15%";
Элемент.Записать();
Объект создаётся. НайтиПоНаименованию его находит. Метод проверки честно отвечает «созданы, существуют». Всё выглядит рабочим.
А на форме выбора этих элементов нет.
Причина: форма при интерактивном создании заполняет обязательные реквизиты сама, и код этого не делает. У меня пустыми остались Статус, ТипКарты и — главное — ДатаНачалаДействия, по которой идёт отбор «действует на дату». Элемент есть, но не действует ни в одну дату, поэтому формы его отфильтровывают.
Обнаружилось это только по скриншоту от владельца бизнеса, который просто открыл список и не увидел записей. Ни один мой программный чек проблему не видел, потому что все они проверяли существование, а не пригодность.
Вывод, который я вынес: после программного создания типового объекта обязательно сравните заполненность реквизитов с элементом, созданным руками через форму. Это две разные записи, даже если наименование одинаковое.
8. Подчинённые справочники: связь через Владельца, а не реквизит
Справочник.КартыЛояльности подчинён виду карты. Попытка обратиться к виду как к реквизиту:
Карта.ВидКарты // Поле объекта не обнаружено (ВидКарты)
Правильно — через Владелец, а отбор при поиске выглядит так:
Справочники.КартыЛояльности.Выбрать(, ВидКарты)
Второй параметр Выбрать() — это как раз владелец.
Общий приём, который меня выручает: посмотреть, как с объектом работает типовой код. Я нашёл ответ, прочитав типовую процедуру расчёта скидки — там видно, каким способом платформа сама ищет карту. Это надёжнее, чем гадать по списку реквизитов.
9. Значения перечислений подбираются по синониму, а не по имени
При программном заполнении реквизита типа перечисления вам нужно внутреннее имя значения. Проблема в том, что вы его не видите: на форме показан синоним, а имя лежит в метаданных.
Когда бизнес говорит «поставь способ предоставления “Скидка (наценка) процентом”», это синоним. Внутреннее имя может отличаться как угодно.
Я сделал хелпер, который перебирает типы реквизита, находит среди них перечисление и ищет в нём значение с нужным синонимом:
Функция ЗначениеПоСиноним(ОписаниеТипов, ИскомыйСиноним)
Для Каждого Тип Из ОписаниеТипов.Типы() Цикл
МетаТип = Метаданные.НайтиПоТипу(Тип);
Если МетаТип = Неопределено
ИЛИ НЕ Метаданные.Перечисления.Содержит(МетаТип) Тогда
Продолжить;
КонецЕсли;
Для Каждого ЗначениеМД Из МетаТип.ЗначенияПеречисления Цикл
Если ЗначениеМД.Синоним = ИскомыйСиноним Тогда
Возврат Перечисления[МетаТип.Имя][ЗначениеМД.Имя];
КонецЕсли;
КонецЦикла;
КонецЦикла;
Возврат Неопределено;
КонецФункции
Позволяет разговаривать с бизнесом на языке форм, а не метаданных.
10. Гейт компиляции ловит модули форм — проверьте это у себя
Долгое время я считал, что /CheckModules проверяет только общие модули и модули объектов, а модули управляемых форм проходят мимо. Из этого следовал дорогой вывод: чтобы проверить правку формы РМК, надо поднимать клиент под виртуальным X-сервером и кликать вслепую.
Я решил проверить экспериментом. Подложил в модуль формы документа заведомую синтаксическую ошибку и прогнал гейт:
{МоеРасширение Документ.ЧекККМ.Форма.ФормаДокументаРМК.Форма(1047,6)}:
Неопознанный оператор
Модули форм проверяются, с точным адресом до строки и колонки. Прогон интерфейса под Xvfb не нужен.
Оговорка: у меня есть запись в журнале, что на другой базе гейт формы не ловил. Разбираться, от чего это зависит — версия платформы, состав ключей, тип формы, — я не стал. Поэтому рекомендация: проверьте на своей базе тем же способом. Подложить заведомую ошибку и прогнать гейт занимает пять минут, а знание о том, покрывает он формы или нет, экономит часы на каждой правке.
11. Расширения применяются динамически, но условие надо проверять
Распространённое убеждение: чтобы применить изменения расширения, надо выгнать пользователей.
На практике загрузка расширения и применение к базе проходят динамически, никого не выкидывая. У меня был случай: в базе четырнадцать сеансов, из них четверо активно работали, применение заняло полторы минуты, ни один пользователь не пострадал.
Но условие есть, и оно ровно одно: конфигурацию не должен держать открытый конфигуратор. Проверяется до начала работ:
rac session list --infobase=<uuid> --cluster=<uuid> localhost:1545 | grep -c Designer
Ноль — можно применять. Не ноль — сначала разбираться, чей это сеанс.
И отдельное наблюдение, которое стоит держать в голове: зависший конфигуратор чаще всего ваш собственный. У меня был случай, когда выгрузка висела тридцать одну минуту с пустым логом, я успел мысленно обвинить подрядчика, а держал базу мой же сеанс, стартовавший раньше и не завершившийся корректно.
Сводка
| # | Грабля | Симптом |
|---|---|---|
| 1 | Общий модуль чужого расширения | «Переменная не определена» |
| 2 | Знач, Новый, В как имена |
«Неопознанный оператор» + каскад |
| 3 | Тело модуля не последним | Ошибка не там, где причина |
| 4 | CodeLength = 0 у складов и касс |
Поле молча пустое в объектной модели |
| 5 | Продавец в табличной части |
Пустое значение, грепом не отличить |
| 6 | ВЫБРАТЬ ПЕРВЫЕ &Параметр |
Не работает, только конкатенация |
| 7 | Программное создание объекта | Пустые обязательные реквизиты, объект «невидим» |
| 8 | Подчинённый справочник | «Поле объекта не обнаружено» |
| 9 | Перечисления по синониму | Внутреннего имени не видно на форме |
| 10 | Гейт и модули форм | Может покрывать — проверьте у себя |
| 11 | Динамическое применение | Работает, если нет открытого конфигуратора |
Общий вывод
Если попытаться выделить одно правило из всех одиннадцати случаев, оно будет такое: не верьте отсутствию ошибки.
Половина списка — это ситуации, где код компилируется, выполняется и не падает, но делает не то: пустой код склада, объект без даты начала действия, поле продавца не из той таблицы. Ни один автоматический контроль их не поймал, все были найдены либо по жалобе человека, либо целенаправленным экспериментом.
Отсюда практика, которую я держу: после любой нетривиальной правки — сверка с образцом, созданным руками через интерфейс, и проверка результата глазами того, кто с этим работает. В 1С это надёжнее любого зелёного кода возврата.
Платформа 8.3.27, УТ 11.5, сервер на Linux. Все случаи — с боевых баз розничной сети.
Вступайте в нашу телеграмм-группу Инфостарт