Самая дорогая ошибка в моей практике стоила месяца и выглядела так: в отчете по остаткам не сходилась сумма. Не падало ничего. Обработка отрабатывала, документы записывались, пользователь работал. Просто цифра была другая.
Искали втроем. Нашли случайно: в имени переменной затесалась латинская o. КоличествoПроводки и КоличествоПроводки — на экране это одно слово, для платформы два разных. Одно получало значение, второе оставалось пустым.
С тех пор я делю ошибки не на «простые и сложные», а на две категории: те, которые падают, и те, которые возвращают правдоподобный результат. Первые находит кто угодно за пять минут. Вторые живут в базе месяцами, потому что о них никто не сообщает: код компилируется, синтаксический контроль проходит, BSL Language Server молчит, тесты зеленые. Узнаете вы о них от бухгалтера, и не сразу.
Ниже — двадцать таких мест. Разделы идут по механизму отказа, а не по темам: важно понимать, чего бояться, а не в каком разделе синтакс-помощника это лежит. Самое дорогое — первым.
Справочник без этого не стоит ничего, поэтому сразу про доверие к тексту. Все, что можно было выполнить, я выполнил в живой Бухгалтерии 3.0 и повторил на двух платформах: 8.3.27.2214 и 8.5.1.1343. Чтобы не проверять по двадцать раз одно и то же, я сложил проверки в одну обработку — она приложена к статье, можете открыть у себя и посмотреть, что ответит ваша платформа.
Пять пунктов из черновика не подтвердились. Я их не выбросил, а переписал по тому, что увидел, и в двух местах правда оказалась интереснее заготовки.
1. Молчаливая потеря данных
Худшая категория. Программа работает, данные портятся.
1.1. Минус из Excel превращается в ноль
Что пишут. Читают число из текста ячейки — так, как это делают в каждой второй загрузке из файла:
Попытка
Сумма = Число(ТекстЯчейки);
Исключение
Сумма = 0;
КонецПопытки;
Что происходит. Минус в выгрузке из Excel — это часто не дефис (-, U+002D), а типографский знак минуса (−, U+2212) или короткое тире (–, U+2013): так его расставляет сам Excel при форматировании. Число() на такой строке падает — и обертка Попытка честно превращает падение в ноль. Все отрицательные суммы в загрузке становятся нулями, ни одна строка не сообщает об ошибке, итог не сходится на сумму всех минусов.
Какой именно символ ломает преобразование, надо знать поименно, потому что интуиция здесь ошибается. Я прогнал все шесть вариантов в живой базе:
Сообщить(Число("-1500")); // -1500, обычный дефис
Сообщить(Число("W22;1500")); // исключение: знак минуса U+2212
Сообщить(Число("–1500")); // исключение: короткое тире U+2013
Сообщить(Число("-1" + Символ(160) + "500")); // -1500, неразрывный пробел проходит
Сообщить(Число("-1 500")); // исключение: обычный пробел не проходит
Сообщить(Число("-1500,25")); // -1500,25, запятая проходит
Неразрывный пробел между разрядами платформа понимает и молча выбрасывает, а точно такой же на вид обычный пробел — нет. То есть строка, скопированная из Excel, может преобразоваться, а она же после прогона через любую чистку пробелов — упасть. Одинаково на 8.3.27.2214 и 8.5.1.1343.
Как правильно. Нормализовать строку до преобразования и не глотать исключение в ноль:
Текст = СтрЗаменить(ТекстЯчейки, Символ(8722), "-"); // минус U+2212
Текст = СтрЗаменить(Текст, Символ(8211), "-"); // тире U+2013
Текст = СтрЗаменить(Текст, Символ(160), ""); // неразрывный пробел, на всякий случай
Текст = СтрЗаменить(Текст, " ", ""); // а вот этот обязателен
Сумма = Число(Текст); // без Попытки: пусть падает, если все еще не число
Чем ловится. Только тестом на файле с отрицательными значениями, сохраненном из настоящего Excel — а не набранном руками в блокноте. Если в тестовых данных минусов нет, вы не увидите этого никогда.
1.2. Реквизит с ограничением знака обнуляет минус
Что пишут.
Движение.Сумма = -1500;
Набор.Записать();
Что происходит. Если у реквизита в конфигураторе стоит галка «Неотрицательное» (в XML выгрузки это AllowedSign со значением Nonnegative), записанное отрицательное значение становится нулем. Без сообщения, без исключения, без отметки в журнале регистрации. Записали −1500 — прочитали 0.
Причем обнуляется значение не при записи, а уже при присваивании: Объект.Реквизит = -1500; Сообщить(Объект.Реквизит); печатает 0 еще до всякого Записать(). Я проверил это в типовой Бухгалтерии на реквизите справочника с таким ограничением, в транзакции с откатом: ноль в объекте появляется в ту же секунду, что и присваивание.
Как правильно. Перед тем как писать в реквизит отрицательное значение, посмотреть его свойства в конфигураторе. Если минусы нужны — снять ограничение знака; если реквизит чужой и трогать нельзя — хранить знак отдельно или использовать другой реквизит.
Чем ловится. Поиском AllowedSign в XML-выгрузке. В типовой Бухгалтерии таких реквизитов 233 в 119 справочниках — так что вероятность наткнуться выше, чем кажется. Считать надо именно реквизиты объектов: у реквизитов форм ограничение знака тоже встречается, но там оно безобидное, в базу они не пишутся.
1.3. КонецДня() делает пустую дату заполненной
Что пишут. Границу периода приводят к концу дня:
Если ЗначениеЗаполнено(ДатаПо) Тогда
ДатаПо = КонецДня(ДатаПо);
КонецЕсли;
А потом где-то раньше по коду ДатаПо уже прошла через КонецДня() или через арифметику с секундами.
Что происходит. Пустая дата в 1С — это 01.01.0001 00:00:00. После КонецДня() она становится 01.01.0001 23:59:59, а это уже заполненная дата. ЗначениеЗаполнено возвращает Истина, проверка «пользователь не указал период» перестает работать, и запрос молча отбирает пустоту.
Пример:
Пустая = '00010101';
Сообщить(ЗначениеЗаполнено(Пустая)); // Ложь
Сообщить(ЗначениеЗаполнено(КонецДня(Пустая))); // Истина
Сообщить(ЗначениеЗаполнено(Пустая + 5)); // Истина
Как правильно. Проверять заполненность до любых преобразований и запоминать результат в отдельную переменную.
Чем ловится. Сценарием с незаполненным периодом. В ручном тестировании период обычно заполняют — поэтому и не ловится.
1.4. Субконто в виртуальной таблице роняет счет из выборки
Что пишут. В виртуальную таблицу остатков добавляют субконто, рассчитывая просто получить его в выборке:
ВЫБРАТЬ
Остатки.Счет,
Остатки.Субконто1,
Остатки.СуммаОстатокДт
ИЗ
РегистрБухгалтерии.Хозрасчетный.Остатки(
&Дата,
Счет В ИЕРАРХИИ(&Счета),
&ВидыСубконто // <- вот здесь
) КАК Остатки
Что происходит. Счета, у которых такого вида субконто нет, пропадают из результата целиком. Не с нулями — их просто не будет. Отчет при этом выглядит нормальным: строки есть, суммы правдоподобные, итог меньше настоящего ровно на те счета, о которых вы не узнали.
Как правильно. Либо не указывать виды субконто в параметрах виртуальной таблицы и получать разрез отдельным соединением, либо собирать две выборки — со субконто и без — и объединять их.
Чем ловится. Только сверкой итога отчета с оборотно-сальдовой ведомостью за тот же период. По коду это не видно: запрос синтаксически безупречен.
2. Падает там, где не ждешь
Эти как раз падают — но в момент, когда вы этого не ожидаете, и с текстом, который не подсказывает причину.
2.1. XMLСтрока не берет наборы записей
Что пишут. Делают снимок набора записей, чтобы сохранить его до изменения:
Снимок = СериализаторXDTO.XMLСтрока(НаборЗаписей);
Что происходит. Исключение: «Отсутствует отображение для типа РегистрСведенийНаборЗаписей.<Имя>». Сериализатор пишет значение без явного указания типа, а для набора записей такого отображения просто нет.
Как правильно.
Снимок = ОбщегоНазначения.ЗначениеВСтрокуXML(НаборЗаписей);
Эта функция БСП делает то же самое с явным назначением типа — так БСП версионирует наборы записей у себя.
Чем ловится. Первым же запуском на наборе записей. Коварство в другом: на объектных типах XMLСтрока работает прекрасно, и переход на набор записей выглядит безобидным рефакторингом.
2.2. Обращение к члену у результата Новый не компилируется
Что пишут. Сокращают две строки в одну:
ИмяФайла = Новый Файл(Путь).ПолноеИмя;
Что происходит. Это не ошибка выполнения, это ошибка компиляции модуля: «Неопознанный оператор». Модуль не собирается целиком — обработка не откроется вовсе. И это не подсвечивает ни редактор, ни BSL Language Server: конструкция выглядит абсолютно нормальной для человека, пришедшего из любого другого языка.
Как правильно. Через переменную, всегда:
Файл = Новый Файл(Путь);
ИмяФайла = Файл.ПолноеИмя;
Чем ловится. Попыткой открыть обработку. Если конструкция попала в редко используемую ветку большого модуля — узнаете нескоро.
2.3. Имя глобального объекта в переменной
Что пишут. Называют локальную переменную удачным по смыслу словом:
ДопустимаяДлина = Метаданные.Справочники.Номенклатура.ДлинаНаименования;
Что происходит. Компилятор молчит. BSL Language Server молчит. В рантайме: «Поле объекта недоступно для записи (ДопустимаяДлина)». Имя занято системным перечислением платформы.
У меня на этом не записалось 49 элементов из 49 — цикл падал на первой же итерации, а сообщение об ошибке про длину наименования не говорило ничего.
Имена занимает не только платформа. Пока я собирал проверки к этой статье, я дважды поймал себя на том же: переменная Знач не компилируется вообще (это модификатор параметра «по значению»), а процедура с именем Открыть в модуле управляемой формы дает «Процедура или функция с указанным именем уже определена» — так называется собственный метод формы.
Как правильно. Переименовать. Имя платформенного объекта, ключевое слово или имя метода самой формы в левой части присваивания — всегда ошибка.
Чем ловится. Только запуском. Список таких имен нигде не опубликован одним куском, и построить его автоматически по коду конфигурации, как выяснилось, не получается: «имя всегда употребляется с точкой и ни разу не присваивается» — эвристика, которая на типовой дает десятки тысяч кандидатов и при этом теряет настоящие платформенные объекты.
2.4. Латинская буква внутри кириллического имени
Что пишут. Ничего особенного. Буква попадает сама — при копировании из письма, из документа, из чужого кода, при переключении раскладки.
Что происходит. КоличествoПроводки с латинской o — это другое имя, чем КоличествоПроводки. Два разных идентификатора, визуально неотличимых. Одна переменная получает значение, вторая остается пустой.
Пример — вставьте в консоль как есть:
ИтогоКоличество = 0;
Для Счетчик = 1 По 3 Цикл
ИтогoКоличество = ИтогоКоличество + Счетчик; // слева латинская o
КонецЦикла;
Сообщить(ИтогоКоличество); // 0, а не 6
Накопление уходит в переменную с латинской буквой, а печатается кириллическая, которая так и осталась нулем. Ни одна строка не падает.
Как правильно. Ловить автоматически. Важная тонкость: ругаться на любую латиницу внутри кириллического имени нельзя — СебестоимостьERP, ЗначениеВСтрокуXML, СохранитьВExcel совершенно законны, и такое правило на типовой ERP дает тысячи ложных срабатываний.
Точный признак другой: одно имя встречается в файле в двух написаниях. Нормализуйте латинские буквы-двойники в кириллические (ABCEHKMOPTXY → АВСЕНКМОРТХУ, acekmoptxy → асекмортху) и ищите нормализованные формы, у которых набралось больше одного исходного написания. Пока написание одно — код работает, имя просто выглядит странно. Вред начинается со второго.
Нормализацию делайте до понижения регистра: пары B/В и H/Н неразличимы только в верхнем регистре, а b/в и h/н — уже разные буквы.
Чем ловится. Только сравнением алфавитов. Ни глазами, ни поиском, ни линтером.
2.5. Подстановка в закомментированную строку запроса
Что пишут. Патчат текст запроса кодом, используя комментарий как якорь:
ТекстЗапроса = СтрЗаменить(ТекстЗапроса,
"//ВСТАВИТЬ_ПОЛЯ", ДополнительныеПоля);
Что происходит. Если подставляемый текст многострочный, под комментарием остается только его первая строка. Хвост выходит из-под // и становится частью запроса — в произвольном месте. Запрос либо падает с невнятной ошибкой синтаксиса, либо, что хуже, выполняется и возвращает не то.
Как правильно. Якорь ставить не в комментарии, а в самом теле запроса, и подставлять в том порядке, в котором части запроса идут: сначала группировка, потом выборка. Сам патч прогонять на симуляции — собрать итоговый текст и посмотреть на него глазами до отправки в базу.
Чем ловится. Печатью получившегося текста запроса. Отладчик показывает его уже собранным, так что достаточно один раз посмотреть.
3. Формы
3.1. Реквизит формы ХранилищеЗначения: на 8.3 не работает, на 8.5 работает
Что пишут. Добавляют на форму реквизит типа ХранилищеЗначения, чтобы положить туда картинку, таблицу или произвольные данные.
Что происходит. Зависит от версии платформы, и это самое неприятное. Я собрал одну и ту же обработку с таким реквизитом и запустил на двух.
На 8.3.27.2214 реквизит объявляется, обработка открывается, но реквизитом он только называется. ТипЗнч у него не ХранилищеЗначения, а ДанныеФормыСтруктура. Присвоить нельзя:
Ошибка при установке значения атрибута контекста (Хранилище): Нельзя изменять поле, содержащее объект данных формы
а на клиенте у него нет и метода: «Метод объекта не обнаружен (Получить)».
На 8.5.1.1343 тот же файл работает как задумано: присваивание проходит, ТипЗнч возвращает Хранилище значения, значение доезжает до клиента, Получить() отдает то, что положили на сервере.
То есть код, написанный и проверенный на 8.5, у заказчика на 8.3 упадет, и упадет не там, где вы его писали, а при первом же присваивании.
Как правильно. Не зависеть от версии: держать данные во временном хранилище, а на форме — только адрес строкой.
АдресДанных = ПоместитьВоВременноеХранилище(Данные, УникальныйИдентификатор);
// ...
Данные = ПолучитьИзВременногоХранилища(АдресДанных);
Так, судя по всему, и поступают все: в выгрузке типовой Бухгалтерии 3.0 я просмотрел 7981 управляемую форму и не нашел ни одного реквизита типа ХранилищеЗначения.
Чем ловится. Разбором Form.xml: ищите реквизиты, у которых тип оканчивается на ValueStorage. И прогоном на той платформе, которая стоит у заказчика, а не у вас.
3.2. Расширения элементов формы — не объекты
Что пишут. По аналогии с другими свойствами:
Элементы.Поле.РасширениеПоляВвода.КнопкаВыбора = Ложь;
Что происходит. Не работает, и причина не в синтаксисе: такого объекта не существует вообще. «Расширение поля ввода» — это способ, которым синтакс-помощник сгруппировал свойства, доступные элементу формы определенного вида. Группировка в документации, а не сущность в платформе.
Проверить можно, не запуская 1С: имена объектов платформы лежат в библиотеках каталога bin (см. пункт 5.4). КнопкаВыбора там есть, РасширениеПоляВвода — нет.
Как правильно. Свойство задается прямо на элементе:
Элементы.Поле.КнопкаВыбора = Ложь;
Чем ловится. Тем же поиском по бинарникам. Ошибка тут в первую очередь в голове: структура справки читается как структура объектов, а это не одно и то же.
3.3. Булева колонка рисуется текстом «Да» / «Нет»
Что пишут. Добавляют в таблицу на форме колонку булева типа.
Что происходит. Колонка выводится полем ввода, а не флажком. На экране пользователь видит слова «Да» и «Нет», читает их как вид операции или как статус и не догадывается, что по колонке можно щелкнуть. Это не ошибка — платформа ничего не нарушила, просто вид поля по умолчанию оказался не тем, которого вы ждали.
Как правильно. Явно задать вид «Поле флажка» в конфигураторе.
Чем ловится. Разбором Form.xml — и признак там абсолютно точный. Две колонки одного и того же булева типа выглядят в XML так:
<CheckBoxField name="Пометить" id="52">
<DataPath>Дубли.Пометить</DataPath>
</CheckBoxField>
<InputField name="ЕстьВЭРП" id="70">
<DataPath>Дубли.ЕстьВЭРП</DataPath>
</InputField>
Тип реквизита при этом один и тот же, и лежит он не рядом с элементом, а в разделе <Attributes>: для колонок таблиц и деревьев значений — внутри <Columns> у самого реквизита таблицы. Ищите InputField, чей путь к данным ведет на реквизит с типом xs:boolean.
Проверяя это на своих обработках, я нашел пять таких колонок — и все пять пользователь все это время читал как текст, не подозревая, что по ним можно щелкнуть.
4. Внешние обработки и отчеты
4.1. Фоновые задания внешней обработке недоступны
Что пишут. Длинную операцию заворачивают в механизм длительных операций БСП, чтобы пользователь видел прогресс и мог прервать.
Что происходит. Не работает — если обработка открыта через «Файл → Открыть». Фоновое задание может выполнить только метод, который платформа умеет найти по имени: процедуру общего модуля конфигурации или команду обработки, подключенной через подсистему БСП «Дополнительные отчеты и обработки». Файл на диске для фонового задания не существует.
Так что «оберну в ДлительныеОперации.ВыполнитьВФоне и все» работает ровно до момента, когда обработку у заказчика откроют не из справочника дополнительных обработок, а с рабочего стола.
Как правильно. Есть два пути, и первый обычно переоценивают. Фоновое задание все-таки можно запустить — если целиком спрятать обработку за процедуру общего модуля конфигурации: задание вызывает ее, она внутри поднимает обработку из файла через ВнешниеОбработки.Создать и дергает нужный метод. Так устроена подсистема «Дополнительные отчеты и обработки». Цена: свой общий модуль в конфигурации заказчика, файл, доступный серверу, и безопасный режим. Для обработки, которую открывают с рабочего стола, это обычно неподъемно.
Второй путь проще и работает всегда: резать работу на порции и гонять их с клиента: клиентская процедура берет очередную порцию, вызывает сервер, обновляет состояние и переподключает себя обработчиком ожидания:
&НаКлиенте
Процедура ОбработатьОчереднуюПорцию()
Обработано = ОбработатьПорциюНаСервере(Обработано);
Если Обработано >= Всего Тогда
Возврат;
КонецЕсли;
Состояние("Обработано: " + Обработано + " из " + Всего);
ПодключитьОбработчикОжидания("ОбработатьОчереднуюПорцию", 0.2, Истина);
КонецПроцедуры
Чем ловится. Запуском на объеме. На трех тестовых строках все выглядит прекрасно.
4.2. Состояние() в серверном коде: где молчит, а где не компилируется
Что пишут. Показывают прогресс из процедуры, собранной для обеих сторон — так делают, когда одну и ту же порцию хотят считать и на клиенте, и на сервере:
&НаКлиентеНаСервереБезКонтекста
Процедура ОбработатьВсе(Данные)
Счетчик = 0;
Для Каждого Строка Из Данные Цикл
Счетчик = Счетчик + 1;
Состояние("Обработано: " + Счетчик);
ОбработкаПрерыванияПользователя();
КонецЦикла;
КонецПроцедуры
Что происходит. Состояние() и ОбработкаПрерыванияПользователя() — клиентские методы, сервера в списке их доступности нет. А вот что из этого выходит на практике, зависит от клиента, и я развел это опытом: одна и та же обработка, две директивы, два клиента.
| Директива | Толстый клиент | Тонкий клиент |
|---|---|---|
&НаСервере |
компилируется, отрабатывает молча | ошибка компиляции |
&НаКлиентеНаСервереБезКонтекста |
компилируется, с сервера молча | ошибка компиляции |
В тонком клиенте это громко и сразу:
{ВнешняяОбработка.Проба.Форма.Форма.Форма(36,2)}:
Процедура или функция с указанным именем не определена (Состояние)
<<?>>Состояние("проба индикатора");
Модуль не собирается целиком, и вы узнаете об этом при первом же открытии. Неприятно, зато сразу.
В толстом клиенте все иначе. Все компилируется, вызов с сервера проходит без единого звука: прогресс не появляется, нажатие «Прервать» ни к чему не приводит, цикл крутится до конца. Пользователь видит зависшее окно и снимает сеанс.
Отсюда и обратная ловушка: написали и проверили в толстом клиенте — у заказчика на тонком обработка просто не откроется. Это ровно тот же разлом, что и в пункте 4.4, только повернутый другой стороной.
Как правильно. См. предыдущий пункт: прогресс и прерывание живут на клиенте, а сервер только считает порцию и возвращает результат.
Чем ловится. Разбором директив компиляции — ищите эти два вызова под &НаСервере, &НаСервереБезКонтекста и &НаКлиентеНаСервереБезКонтекста. И запуском в том клиенте, который стоит у заказчика.
4.3. Пересобрали обработку, а выполняется прежняя версия
Что пишут. Правят обработку, пересобирают файл, открывают заново — и видят старое поведение. Правят еще раз. Снова старое. Народное объяснение: платформа кэширует внешнюю обработку по имени объекта, поэтому надо версионировать имя объекта внутри файла.
Что происходит. Объяснение неверное, я его проверил и оно не подтвердилось. Никакого кэша по имени объекта нет. Две сборки одной обработки с одинаковым именем объекта и одинаковыми идентификаторами, отличается только текст модуля: ВнешниеОбработки.Создать по пути отдает новую версию сразу, в том же сеансе. Подмена файла по тому же пути, тот же файл по другому пути, другое имя объекта по тому же пути — во всех случаях выполняется то, что лежит в файле сейчас.
А вот что действительно залипает — уже открытое окно формы. В одном сеансе:
// шаг 1: подключили версию 1, открыли форму - ПриСозданииНаСервере отработала
ОткрытьФорму("ВнешняяОбработка." + Имя + ".Форма");
// шаг 2: подключили версию 2 тем же именем, открыли ту же форму
ОткрытьФорму("ВнешняяОбработка." + Имя + ".Форма");
На втором шаге модуль объекта уже отвечает «версия 2», а ПриСозданииНаСервере формы не выполняется вообще: платформа не создает вторую форму с тем же ключом, а показывает ту, что уже открыта, — со старым кодом внутри. Добавьте признак уникальности — и та же строка создаст новую форму, и новый код выполнится:
ОткрытьФорму("ВнешняяОбработка." + Имя + ".Форма", , , Истина);
Одинаково на 8.3.27.2214 и 8.5.1.1343.
Как правильно. Закрывать окно обработки перед тем, как открыть пересобранный файл. Переименовывать объект внутри файла не нужно — это лечение несуществующей болезни, которое стоит вам версионирования имен.
Чем ловится. Симптом знакомый: добавили Сообщить("сюда дошли"), перезапустили, сообщения нет. Вывод «код не вызывается» верен буквально — но не потому, что выполняется прежняя версия файла, а потому что форму никто не создавал заново.
4.4. ТекущаяДата() на клиенте, и почему замена на ТекущаяДатаСеанса() не компилируется
Что пишут.
&НаКлиенте
Процедура СформироватьИмяФайла()
Имя = "выгрузка_" + Формат(ТекущаяДата(), "ДФ='yyyyMMdd_HHmmss'");
КонецПроцедуры
Что происходит. Берется время рабочей станции. Пользователь волен перевести часы, часовой пояс может отличаться от серверного, и метки времени в ваших файлах и журналах перестают быть сопоставимыми с документами, которые датирует сервер.
BSL Language Server на эту строку ругается и предлагает замену: «Используйте "ТекущаяДатаСеанса" вместо устаревшего "ТекущаяДата"». Совет разумный и повсеместный. Только если ему последовать прямо здесь, модуль перестанет компилироваться в тонком клиенте:
{ВнешняяОбработка.Проверка.Форма.Форма.Форма(28,16)}:
Процедура или функция с указанным именем не определена (ТекущаяДатаСеанса)
ДатаСеанса = <<?>>ТекущаяДатаСеанса();
ТекущаяДатаСеанса() объявлена доступной на сервере, в толстом клиенте, во внешнем соединении и на мобильном сервере. Тонкого и веб-клиента в этом списке нет. Я поймал это на себе ровно так: обработка, собранная и проверенная запуском через 1cv8.exe — а это толстый клиент, — открылась и отработала; та же обработка в 1cv8c.exe не скомпилировалась вовсе.
Чтобы не списать это на конфигурацию, я прогнал один и тот же вызов в двух базах и двумя клиентами:
| База, режим совместимости Версия8_3_27 | 1cv8.exe, толстый |
1cv8c.exe, тонкий |
|---|---|---|
| Бухгалтерия 3.0.206.19 | доступна | недоступна |
| Управление торговлей 11.5.22.149 | доступна | недоступна |
Платформа одна, 8.5.1.1343. Дело не в конфигурации и не в режиме совместимости, а ровно в клиенте. ТекущаяДата() при этом доступна везде.
Сорт ошибки тут важнее самой ошибки. Вызов стоял внутри Попытка, и это не помогло: ошибка компиляции, а не выполнения, — она уносит весь модуль целиком, вместе с двадцатью остальными проверками. Попытка ловит только то, что уже скомпилировалось.
Как правильно. Не подставлять одну функцию вместо другой, а перенести вычисление туда, где ему место. Время берет сервер и отдает клиенту готовым значением:
&НаКлиенте
Процедура СформироватьИмяФайла()
Имя = "выгрузка_" + Формат(ДатаСеансаССервера(), "ДФ='yyyyMMdd_HHmmss'");
КонецПроцедуры
&НаСервереБезКонтекста
Функция ДатаСеансаССервера()
Возврат ТекущаяДатаСеанса();
КонецФункции
Лишний вызов сервера — цена того, что в данных не окажется времени с чужих часов. Если очень нужно проверить, доступна ли функция именно у вас, не ломая модуль, — оберните ее в Выполнить(): он компилируется в момент выполнения.
Чем ловится. Разбором директив компиляции — и обязательно запуском в том клиенте, в котором работает заказчик. Толстый клиент прощает то, на чем тонкий падает, а 1cv8.exe в файловом режиме — это именно толстый.
5. Файлы и выгрузка
5.1. Собранную обработку проверить нечем: Конфигуратор молчит
Что пишут. Сборку и проверку внешней обработки ставят на поток: собрать файл из исходников Конфигуратором, прогнать его же синтаксической проверкой, запустить в Предприятии ключом /Execute и забрать результат из файла.
1cv8 DESIGNER /LoadExternalDataProcessorOrReportFromFiles src\Обработка.xml Обработка.epf
1cv8 DESIGNER /CheckModules -ExternalDataProcessor Обработка.epf
1cv8 ENTERPRISE /Execute Обработка.epf
Что происходит. Я положил в модуль формы одну строку с ошибкой, которая не видна глазами, — присваивание переменной с именем Знач. Это зарезервированное слово, модификатор параметра «по значению», и переменной с таким именем быть не может.
Знач = 1;
Сообщить(Знач);
Сборка: «Загрузка завершена», файл создан. Проверка: «Синтаксических ошибок не обнаружено!». Модули форм внешних обработок /CheckModules не компилирует, и молчание тут ничего не значит.
Запуск: процесс не завершается, файл с результатом не появляется, код возврата взять неоткуда. Снаружи это неотличимо от зависания в вашем коде, и искать вы будете бесконечный цикл. А на экране в это время висит окно:
Ошибка инициализации модуля: ВнешняяОбработка.ПробаСломанная.Форма.Форма.Форма
по причине:
{ВнешняяОбработка.ПробаСломанная.Форма.Форма.Форма(6,2)}: Неопознанный оператор
<<?>>Знач = 1;
Беда в том, что окно это видно только тому, кто сидит перед машиной. Если сборку собирает билд-сервер или вы просто свернули лишнее, диагноза нет ни в логе, ни в коде возврата. На обеих платформах одно и то же.
Как правильно. Проверять модули форм тем, что их действительно разбирает. BSL Language Server на этом же файле дает три ParseError ровно на нужных строках. Получается наоборот привычному: сторонний линтер видит то, чего не видит родная проверка платформы, — и ставить его в сборку надо до Конфигуратора, а не после.
И не доверять тишине. Если запуск ключом /Execute не завершился за разумное время, это не «долго считает», это чаще всего окно с ошибкой, которого вы не видите. Откройте тот же файл руками и посмотрите на экран.
Чем ловится. Ничем из того, на что вы рассчитываете: ни сборкой, ни /CheckModules, ни кодом возврата. Только линтером до сборки и живым запуском после.
5.2. Табличный документ из xlsx: листы склеиваются, а Значение недоступно
Что пишут.
ТабДок = Новый ТабличныйДокумент;
ТабДок.Прочитать(ПутьКФайлу);
Значение = ТабДок.Область("R2C3").Значение;
Что происходит. Ходовое объяснение «читается только первый лист» неверно, я его проверил. Книга из двух листов читается целиком: платформа кладет листы в одну таблицу подряд, второй под первым. На моем файле вышла таблица 2 на 3, маркер первого листа в R1C1, маркер второго — в R2C1.
От этого не легче, а хуже. Ничего не потерялось — зато пропала граница между листами. Если на первом листе двести строк, а на втором своя шапка и еще сто, вы получите триста строк подряд, где двести первая внезапно окажется заголовком, и никакого признака, что здесь начался другой лист. Разбор, написанный в расчете на один лист, не упадет: он просто прочитает шапку второго листа как строку данных. На обеих платформах одинаково.
Второе верно: Значение у прочитанной ячейки недоступно для чтения, платформа отвечает «Поле объекта недоступно для чтения (Значение)». Доступен только Текст. То есть числа и даты вы разбираете из строк, и пункт 1.1 этой статьи начинается ровно здесь.
Как правильно. Если листов может быть больше одного — не полагаться на Прочитать: восстановить границы в прочитанном документе нечем. Читать через zip, это следующий пункт.
Чем ловится. Файлом из двух листов с разными шапками. На однолистовом все работает.
5.3. Многолистовой xlsx разбирается через zip
Что пишут. Ищут способ добраться до конкретного листа табличным документом. Прочитать его выйдет (см. предыдущий пункт), но вперемешку с остальными и без единого разделителя, а это обычно не то, что нужно.
Как правильно. xlsx - это zip-архив с XML внутри, и он читается штатными средствами платформы, без единой внешней компоненты. Ниже рабочая функция: на вход путь и номер листа, на выходе таблица непустых ячеек. Я прогнал ее на книге из двух листов на 8.3.27.2214 и 8.5.1.1343.
// Читает один лист книги xlsx и возвращает таблицу его непустых ячеек.
//
// Возвращаемое значение:
// ТаблицаЗначений - колонки Строка, Колонка (числа с единицы) и Значение
// (Число или Строка - так, как значение лежит в книге).
Функция ПрочитатьЛистXLSX(Знач ПутьКниги, Знач НомерЛиста) Экспорт
Каталог = ПолучитьИмяВременногоФайла();
СоздатьКаталог(Каталог);
Попытка
Архив = Новый ЧтениеZipФайла(ПутьКниги);
Архив.ИзвлечьВсе(Каталог, РежимВосстановленияПутейФайловZIP.Восстанавливать);
Архив.Закрыть();
// Строковые значения лежат общим списком, в самом листе - только номера.
Общие = ОбщиеСтроки(Каталог + "\xl\sharedStrings.xml");
Ячейки = ЯчейкиЛиста(Каталог + "\xl\worksheets\sheet"
+ Формат(НомерЛиста, "ЧГ=") + ".xml", Общие);
Исключение
УдалитьФайлы(Каталог);
ВызватьИсключение;
КонецПопытки;
УдалитьФайлы(Каталог);
Возврат Ячейки;
КонецФункции
Функция ОбщиеСтроки(Знач Путь)
Список = Новый Массив;
Файл = Новый Файл(Путь);
Если НЕ Файл.Существует() Тогда
Возврат Список; // книга без строковых значений - такое бывает
КонецЕсли;
Чтение = Новый ЧтениеXML;
Чтение.ОткрытьФайл(Путь);
Текущая = Неопределено;
Собираем = Ложь;
Пока Чтение.Прочитать() Цикл
Если Чтение.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
Если Чтение.Имя = "si" Тогда
Текущая = "";
ИначеЕсли Чтение.Имя = "t" Тогда
Собираем = Истина;
КонецЕсли;
ИначеЕсли Чтение.ТипУзла = ТипУзлаXML.КонецЭлемента Тогда
Если Чтение.Имя = "si" Тогда
Список.Добавить(Текущая);
Текущая = Неопределено;
ИначеЕсли Чтение.Имя = "t" Тогда
Собираем = Ложь;
КонецЕсли;
ИначеЕсли Собираем И Текущая <> Неопределено Тогда
Текущая = Текущая + Чтение.Значение;
КонецЕсли;
КонецЦикла;
Чтение.Закрыть();
Возврат Список;
КонецФункции
Функция ЯчейкиЛиста(Знач Путь, Знач Общие)
Ячейки = Новый ТаблицаЗначений;
Ячейки.Колонки.Добавить("Строка");
Ячейки.Колонки.Добавить("Колонка");
Ячейки.Колонки.Добавить("Значение");
Чтение = Новый ЧтениеXML;
Чтение.ОткрытьФайл(Путь);
Адрес = "";
ТипЯчейки = "";
Сырое = "";
Собираем = Ложь;
Пока Чтение.Прочитать() Цикл
Если Чтение.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
Если Чтение.Имя = "c" Тогда
Адрес = Чтение.ПолучитьАтрибут("r");
ТипЯчейки = Чтение.ПолучитьАтрибут("t");
Сырое = "";
ИначеЕсли Чтение.Имя = "v" ИЛИ Чтение.Имя = "t" Тогда
Собираем = Истина; // формулу в <f> при этом не берем
КонецЕсли;
ИначеЕсли Чтение.ТипУзла = ТипУзлаXML.КонецЭлемента Тогда
Если Чтение.Имя = "v" ИЛИ Чтение.Имя = "t" Тогда
Собираем = Ложь;
ИначеЕсли Чтение.Имя = "c" И ЗначениеЗаполнено(Сырое) Тогда
СтрокаЯчейки = Ячейки.Добавить();
СтрокаЯчейки.Строка = НомерСтрокиПоАдресу(Адрес);
СтрокаЯчейки.Колонка = НомерКолонкиПоАдресу(Адрес);
СтрокаЯчейки.Значение = ЗначениеЯчейки(Сырое, ТипЯчейки, Общие);
КонецЕсли;
ИначеЕсли Собираем Тогда
Сырое = Сырое + Чтение.Значение;
КонецЕсли;
КонецЦикла;
Чтение.Закрыть();
Возврат Ячейки;
КонецФункции
Функция ЗначениеЯчейки(Знач Сырое, Знач ТипЯчейки, Знач Общие)
Если ТипЯчейки = "s" Тогда
Индекс = Число(Сырое);
Возврат ?(Индекс < Общие.Количество(), Общие[Индекс], "");
ИначеЕсли ТипЯчейки = "inlineStr" ИЛИ ТипЯчейки = "str" Тогда
Возврат Сырое;
КонецЕсли;
// Числа в файле всегда с точкой и без разделителей разрядов.
Попытка
Возврат Число(СтрЗаменить(Сырое, ".", ","));
Исключение
Возврат Сырое;
КонецПопытки;
КонецФункции
Функция НомерКолонкиПоАдресу(Знач Адрес)
Номер = 0;
Для Поз = 1 По СтрДлина(Адрес) Цикл
Знак = ВРег(Сред(Адрес, Поз, 1));
Если Знак >= "0" И Знак <= "9" Тогда
Прервать;
КонецЕсли;
Номер = Номер * 26 + КодСимвола(Знак) - КодСимвола("A") + 1;
КонецЦикла;
Возврат Номер;
КонецФункции
Функция НомерСтрокиПоАдресу(Знач Адрес)
Цифры = "";
Для Поз = 1 По СтрДлина(Адрес) Цикл
Знак = Сред(Адрес, Поз, 1);
Если Знак >= "0" И Знак <= "9" Тогда
Цифры = Цифры + Знак;
КонецЕсли;
КонецЦикла;
Возврат ?(ПустаяСтрока(Цифры), 0, Число(Цифры));
КонецФункции
На моей книге из двух листов это дает вот что:
=== лист 1 === R1C1 = [ЛИСТ1-МАРКЕР] тип Строка R1C2 = [-1 500] тип Число R1C3 = [42,5] тип Число === лист 2 === R1C1 = [ЛИСТ2-МАРКЕР] тип Строка R1C2 = [777] тип Число
Обратите внимание на типы. Там, где табличный документ отдал бы строку и оставил вам пункт 1.1 со всеми его минусами, из zip приходит готовое отрицательное число: в самом файле оно записано как -1500, всегда с точкой в качестве разделителя и без пробелов между разрядами, независимо от того, что видел в Excel пользователь.
Две оговорки. Имя sheet с номером совпадает с порядком вкладок только в простых книгах - строго говоря, соответствие надо брать из xl/workbook.xml и xl/_rels/workbook.xml.rels. И даты в xlsx хранятся числом дней от 30.12.1899, и отличить дату от обычного числа можно только по стилю ячейки из xl/styles.xml.
Чем ловится. Требованием заказчика, у которого книга из пяти листов.
5.4. Есть ли такой метод у платформы — проверяется по бинарникам
Что пишут. Метод, который «точно был». Или которого не было никогда.
Что происходит. Модуль не компилируется, и вы теряете время, гадая, перепутали вы имя или версию платформы.
Как правильно. Спросить у самой платформы. Имена методов лежат в библиотеках в каталоге bin в кодировке UTF-16LE, и наличие имени проверяется поиском по ним:
имя = 'ОбходФайловЦиклом'.encode('utf-16-le')
for п in glob.glob(r'C:\Program Files\1cv8\<версия>\bin\*.dll'):
if имя in open(п, 'rb').read():
print('есть'); break
Файл справки .hbk для этого не годится — он сжат и поиском не берется.
Одна оговорка, без которой способ подводит. Имя в библиотеке означает, что платформа его знает, — но не означает, что оно доступно вам. Живой пример из этой же статьи: ЧасовойПоясИнформационнойБазы в бинарниках есть, а компилятор отвечает «Процедура или функция с указанным именем не определена». Отсутствие имени — приговор, наличие — только основание посмотреть справку.
Чем ловится. Этим же способом. Я проверил так шестнадцать имен перед тем, как писать код, и одно из них — как раз ОбходФайловЦиклом — в платформе отсутствует: его нет ни в одной из 279 библиотек и программ каталога bin, тогда как НайтиФайлы находится сразу в нескольких. Пять минут проверки против часа отладки.
Два мифа
Напоследок — то, что ошибками не является, хотя регулярно правится на код-ревью.
Переменная с именем Строка не мешает вызвать функцию Строка(). Раздражает — да, читается плохо — да, но кода не ломает. Переименовывать имеет смысл ради читаемости, а не ради корректности.
Совпадение номеров в Form.xml — не коллизия. Пулы идентификаторов раздельные для элементов, реквизитов и команд. Элемент с id="19" и реквизит с id="19" прекрасно уживаются: в типовой Бухгалтерии 3.0 такие совпадения есть в 3462 формах из первых четырех тысяч, то есть почти в каждой. Если вы «чините» это вручную, вы ломаете рабочую форму.
Шпаргалка
Все двадцать в одной таблице — чтобы не перечитывать статью, а пробежать глазами перед код-ревью.
| № | Признак | Что ломается | Чем ловится |
|---|---|---|---|
| 1.1 | Попытка Число(...) Исключение 0 на данных из Excel |
минусы становятся нулями | тест на файле с отрицательными из настоящего Excel |
| 1.2 | галка «Неотрицательное» у реквизита | минус записывается как ноль | поиск AllowedSign в выгрузке |
| 1.3 | КонецДня() или + секунды до проверки заполненности |
пустой период считается заданным | сценарий с незаполненной датой |
| 1.4 | виды субконто в параметрах виртуальной таблицы | счета без этого субконто выпадают | сверка итога с ОСВ |
| 2.1 | СериализаторXDTO.XMLСтрока(Набор) |
исключение на наборе записей | первый запуск на наборе |
| 2.2 | Новый Тип(...).Свойство |
модуль не компилируется | попытка открыть обработку |
| 2.3 | ИмяПлатформенногоОбъекта = ... |
падение в рантайме | только запуск |
| 2.4 | одно имя в двух написаниях | накопление уходит в другую переменную | сравнение алфавитов, поиск пар |
| 2.5 | многострочная вставка в //якорь запроса |
хвост вставки выходит из-под комментария | печать собранного текста запроса |
| 3.1 | реквизит формы ХранилищеЗначения |
на 8.3 падает при присваивании, на 8.5 работает | поиск ValueStorage в Form.xml |
| 3.2 | Элемент.РасширениеПоляВвода.Свойство |
такого объекта нет | поиск имени по бинарникам |
| 3.3 | булева колонка без явного вида | рисуется текстом «Да»/«Нет» | InputField на xs:boolean в Form.xml |
| 4.1 | длительная операция из внешней обработки | не запускается без подключения через БСП | запуск обработки через «Файл → Открыть» |
| 4.2 | Состояние() в серверном контексте |
в толстом клиенте молчит, в тонком модуль не компилируется | директивы компиляции, запуск в 1cv8c.exe |
| 4.3 | пересобрали обработку, окно не закрыли | форма не создается заново, работает старый код | Сообщить() не печатается |
| 4.4 | ТекущаяДатаСеанса() в клиентском коде |
в тонком клиенте модуль не компилируется | запуск в 1cv8c.exe, а не в 1cv8.exe |
| 5.1 | ошибка компиляции в модуле формы обработки | /CheckModules молчит, /Execute виснет без следов |
BSL LS до сборки, живой запуск после |
| 5.2 | ТабличныйДокумент.Прочитать(xlsx) |
листы склеиваются в один, Значение недоступно |
файл из двух листов с разными шапками |
| 5.3 | нужен отдельный лист xlsx | границы листов в прочитанном документе нет | ЧтениеZipФайла + ЧтениеXML |
| 5.4 | метод, который «точно был» | не компилируется | поиск имени по bin\*.dll в UTF-16LE |
Что со всем этим делать
Ни один из двадцати не подсвечивается редактором конфигуратора. Большая часть не находится никаким статическим анализом в принципе: это свойства выполнения или вопросы к смыслу кода, а не к его форме.
Но кое-что видно прямо в выгрузке: ограничение знака у реквизита, два написания одного имени, реквизит формы с хранилищем значения, булева колонка без явного вида. Под такие вещи я написал восемь проверок по XML-выгрузке и прошел ими по типовым ERP и Бухгалтерии: семнадцать гигабайт выгрузки, сорок семь тысяч модулей. Обработка с этими восемью проверками приложена к статье: открывается в любой конфигурации через «Файл → Открыть», на вход — каталог выгрузки, на выход — таблица находок и TSV. Каждое правило отключается галкой. Результат прогона оказался поучительнее самих находок.
Вторым файлом приложена «Проверка поведения платформы» — та самая обработка, которой я проверял утверждения этой статьи. Открывается так же, через «Файл → Открыть», кнопка «Проверить все» выполняет двадцать шесть проверок на вашей базе и показывает, что ответила ваша платформа, а кнопка рядом выгружает результат в TSV. Проверок двадцать шесть, а пунктов двадцать: некоторые распались на несколько отдельных замеров, один только пункт 1.1 дал семь. Два пункта осознанно трогают базу и об этом написано прямо в форме: проверка по пункту 1.2 пишет элемент справочника в транзакции и откатывает ее, проверка по пункту 4.1 запускает и сразу отменяет фоновое задание. Следов не остается.

Обработка после нажатия «Проверить все». 1С:Бухгалтерия 3.0, платформа 8.5.1.1343, толстый клиент
Сверьте свой результат с моим: полностью он совпасть и не должен. В толстом клиенте у меня 23 подтверждено и 3 опровергнуто, в тонком перевернется пункт 4.4б, а на конфигурации без подходящего реквизита и без бухгалтерских итогов пункты 1.2 и 1.4 честно скажут «не проверено». Именно поэтому первой строкой обработка пишет, где она выполнялась.
Если у вас получится другой результат — это интереснее моего, напишите в комментариях, какая платформа и какой пункт.
Из восьми правил четыре были неверными. Ни одно не упало на тестах — все юнит-тесты были зелеными.
Три ошибки вскрылись только на объеме чужого кода. Правило про омоглифы ругалось на любую латинскую букву среди кириллицы и выдало четыре тысячи срабатываний, почти сплошь законные технические суффиксы. Правило про концы строк насчитало пятнадцать тысяч испорченных файлов, которые на поверку были в полном порядке: голый перевод строки сидел внутри текстов запросов. С ним заодно рассыпалось и само утверждение, ради которого правило писалось. Я собрал обработку из исходников, где снял BOM и заменил CRLF на голый перевод строки, — Конфигуратор загрузил ее на обеих платформах и не испортил ни одной кириллической буквы. Пункт про BOM из списка пришлось убрать, а правило в обработке я оставил: пусть считает, но верить ему на слово теперь не надо. Правило про глобальные имена работало по списку, собранному на другой конфигурации, и триста семьдесят девять раз обругало обычные переменные.
Четвертую я нашел, запустив проверки на них же самих. Инструмент отыскал в себе две ошибки, которых не было: он принял собственные образцы поиска, лежащие строковыми литералами, за настоящие вызовы.
Это, собственно, и есть главная мысль, только уже про инструменты, а не про код. Молчаливая ошибка не выдает себя ничем — включая зеленые тесты и включая тесты той проверки, которая должна ее ловить. Единственное, что ее вытаскивает, — прогон на данных, которых вы не писали.
Если у вас есть пункты в эту коллекцию — пишите в комментариях, дополню статью с указанием автора.
Вступайте в нашу телеграмм-группу Инфостарт