Отчет собрался, конфигуратор молчит, пользователь присылает скриншот
Знакомая последовательность. Внешний отчет на СКД: модуль объекта на две тысячи строк, девять текстов запросов, схема с тремя наборами данных и связями между ними. Переделываю его под другую базу, собираю ERF, прогоняю пакетную проверку конфигуратором, чисто. Отдаю.
Через двадцать минут приходит скриншот:
Ошибка инициализации модуля: ВнешнийОтчет.ВаловаяПрибыль.МодульОбъекта
{МодульОбъекта(201,23)}: Переменная не определена (РасширениеДоступа_Сервер)
Правлю, пересобираю, отдаю. Через двадцать минут приходит второй скриншот:
{(19, 41)}: Не задано значение параметра "СтатьиРасходовВыделенные"
КОГДА ПрочиеРасходы.СтатьяРасходов В (<<?>>&СтатьиРасходовВыделенные)
Два круга через живого человека ради двух ошибок, каждая из которых воспроизводится на той же машине за три секунды. Если знать, чем ее воспроизводить.
Воспроизводит ее COM-коннектор, и дальше вся статья про то, как из него собрать проверку, которая умеет падать. Прогоны шли 26.09.2026 и 27.09.2026 на двух версиях платформы сразу, 8.3.27.2214 и 8.5.1.1343. Основная база — Управление торговлей 11.5, часть проб на Бухгалтерии 3.0 и на демобазе ERP, и где какая, сказано по месту.
Отдельно про то, чего в статьях обычно не пишут. Текст прошел независимую ревизию, и она нашла два места, где я не просто был неправ, а расписывал читателю рецепт, который не работает. Оба разобраны ниже без купюр: один в первой же секции, второй в разделе про листинги. Плюс шесть отдельных утверждений прошлой редакции прогон не пережил: про коннектор 8.3, про открытие ERF через COM, про PowerShell 7, про версию формата выгрузки, про порядок элементов в схеме и про несовместимые типы в соединении. Каждое разобрано на своем месте. Мне кажется, статья от этого стала полезнее, чем была.
Все числа ниже сняты с одной замороженной пары файлов: Template.xml на 77 825 байт и собранный из тех же исходников ERF, оба от 27.09.2026. Исходники живого проекта за сутки поменялись дважды, и числа поехали вместе с ними, так что без такой заморозки сверять было бы нечего.
Почему пакетная проверка тут не помогает
Первое, что хочется сделать перед отправкой файла, прогнать пакетную проверку. В рецептах по сети ходит вот такой вызов, и сам им пользовался не один месяц:
1cv8.exe DESIGNER /F"C:\Bases\Работа" /N"Администратор" ^ /CheckModules -ExternalReportsOrDataProcessors "C:\build\Отчет.erf" ^ /Out "C:\build\check.txt"
Код возврата 0, в логе «Синтаксических ошибок не обнаружено!». Выглядит как проверка.
Дальше начинается интересное. Меняем путь на заведомо несуществующий, получаем тот же зеленый ответ. В первой редакции этой статьи отсюда делался вывод, что ключ молча игнорируется, и обоснование было такое: несуществующая база роняет вызов с кодом 1, а несуществующий файл нет, значит база проверяется, а файл нет.
Обоснование оказалось негодным, и поймала это ревизия. Проба с базой доказывает только то, что база открывается. Она не отличает «ключ есть, но платформа не читает его значение» от «ключа нет вовсе». Различающая проба выглядит по-другому:
Что подсунуто /CheckModules |
Код | Лог |
|---|---|---|
-ExternalReportsOrDataProcessors + несуществующий файл |
0 | Синтаксических ошибок не обнаружено! |
-ExternalReportsOrDataProcessors + ERF с заведомо битым модулем |
0 | то же |
-ЯвнаяЧушьКлюч777 + несуществующий файл |
0 | то же |
-ЯвнаяЧушьКлюч777 + ERF с заведомо битым модулем |
0 | то же |
/CheckModules вообще без под-ключа |
0 | то же |
| путь к базе не существует | 1 | Информационная база не обнаружена! |
Выдуманный ключ ведет себя точно так же, как «настоящий». И полное отсутствие ключа тоже.
Тогда мы полезли искать ключ в самой платформе. Побайтный поиск ASCII и UTF-16 по всем файлам каталога bin, две тысячи с небольшим файлов в каждой версии:
| Строка | 8.3.27 | 8.5.1 |
|---|---|---|
ExternalReportsOrDataProcessors |
нет | нет |
CheckModules |
есть | есть |
ExtendedModulesCheck |
есть | есть |
LoadExternalDataProcessorOrReportFromFiles |
есть | есть |
DumpExternalDataProcessorOrReportToFiles |
есть | есть |
Контрольные строки находятся, игла не находится ни в одной из версий. Такого ключа в платформе нет. Конфигуратор проглатывает любой неизвестный под-ключ /CheckModules молча, без предупреждения и без ненулевого кода, и проверяет конфигурацию базы, как если бы вы ничего лишнего не писали.
Сюжет от этого не ослаб, а окреп. Была история про ключ, который врет. Стала история получше: четыре зеленых ответа подряд на параметр, которого не существует, и пятый вообще без параметра. Если вы, как я, гоняли этот вызов в скрипте приемки, то приемка у вас все это время проверяла пустоту. Всего таких зеленых запусков за два дня набралось тринадцать, ни один не покраснел.
Ключи, которые у /CheckModules есть, лежат в 1cv8.exe одним списком, и это ровно клиенты и режимы проверки: -ThinClient, -WebClient, -Server, -ExternalConnection, -ExtendedModulesCheck и их родня. Для внешнего файла там нет ничего. Внешний отчет пакетным синтаксическим контролем не проверяется, и обходного ключа тут нет.
Проверка, которая не умеет падать
Прежде чем поверить зеленому результату, скормите проверке заведомо сломанный вход и убедитесь, что она покраснела.
Одна минута работы, а отсекает целый класс самообмана. И, как видно из предыдущей секции, сломанный вход надо подбирать так, чтобы он различал ваши гипотезы, а не просто был сломанным. Мой не различал.
Забегая вперед: на том же самом попался и мой собственный стенд, причем трижды подряд, и ниже про это есть отдельный разбор.
Что именно ломается во внешнем отчете и чем это ловится
Про столбцы. «Конфигуратор» — это обычная проверка модуля, когда файл открыт руками, а не пакетный вызов: пакетного в таблице нет вовсе, ему тут нечего делать.
| Что ломается | Конфигуратор с открытым файлом | Запрос через COM | Компоновка из Template.xml | Открытие всего ERF через COM |
|---|---|---|---|---|
| Опечатка в имени переменной | ловит | нет | нет | ловит |
| Обращение к общему модулю, которого нет в базе | ловит | нет | нет | ловит |
| «Поле не найдено» в тексте запроса | нет | ловит | ловит | ловит |
| Параметр СКД без значения | нет | нет | ловит | ловит |
| Поле схемы, которого нет в наборе данных | нет | нет | ловит | ловит |
| Битая связь наборов данных | нет | нет | ловит | ловит |
| Верстка и расшифровка | нет | нет | нет | нет |
| Модуль формы, поведение формы | ловит | нет | нет | не проверял, форм не было |
Где в этой таблице прогон, а где мой опыт. Прогонами закрыты: «поле не найдено», «параметр СКД без значения», «обращение к общему модулю» и весь правый столбец, кроме помеченной ячейки про формы. Столбец про конфигуратор не проверялся отдельно ни разу, он стоит целиком на опыте. Строка «верстка и расшифровка» тоже: четыре «нет» там от общих соображений, и ревизия совершенно справедливо за это ткнула, потому что данные расшифровки листинг ниже передает.
А одна строка из этой таблицы убрана совсем. Раньше в ней стояло, что запрос через COM ловит несовместимые типы в соединении. Не ловит. Четыре пробы на условие соединения, все зеленые, плюс контрольная на арифметике:
| Условие соединения | Результат |
|---|---|
ПО О.Ссылка = К.Наименование (ссылка со строкой) |
OK, 5 строк |
ПО О.Наименование = ДАТАВРЕМЯ(2020,1,1) (строка с датой) |
OK, 5 строк |
ПО О.Ссылка = 5 (ссылка с числом) |
OK, 5 строк |
ОБЪЕДИНИТЬ ВСЕ строки и даты в одной колонке |
OK, 3 строки |
О.Наименование + 5 (арифметика) |
FAIL: {(1, 33)}: Неверные параметры "+" |
Платформа молча сравнивает что угодно с чем угодно, а спотыкается только на арифметике, и там у нее другой текст ошибки.
Штатное сообщение про несовместимые типы существует, и ниже в статье оно даже будет: его дает не условие соединения, а неверный тип значения параметра. Пятой пробы, которая вытащила бы его из условия соединения, мы не нашли.
Стенд: COM-коннектор и живая база
Дальше по тексту мы будем гонять один и тот же отчет по разным базам, и первое правило тут простое: база нужна именно та, на которой отчет будет работать. На соседней базе с похожей конфигурацией вы получите чужие ошибки и, что хуже, чужое отсутствие ошибок. Отдельным прогоном этого не проверял, но ниже есть случай, где отчет под УТ на базе Бухгалтерии падает совсем не там, где вы ждете.
Сначала про точку, иначе все дальнейшее не заработает
Это второе место, где ревизия поймала за руку, и поймала болезненно. В прошлой редакции листинги были написаны вот так:
$схема = $conn.СериализаторXDTO.ПрочитатьXML($чтение)
foreach ($вариант in $схема.ВариантыНастроек) { ... }
Красиво, читаемо и не работает. Чтение свойства COM-объекта 1С через точку не отдает значения. Не «часть членов», как было написано раньше, а все восемь проверенных: объекты приходят $null, строка приходит пустой.
| Обращение | Через точку | Инлайн InvokeMember с культурой ru-RU |
|---|---|---|
$conn.СериализаторXDTO |
$null | __ComObject |
$conn.ВнешниеОтчеты |
$null | __ComObject |
$conn.Метаданные |
$null | __ComObject |
$conn.ФабрикаXDTO |
$null | __ComObject |
$conn.Справочники |
$null | __ComObject |
$conn.ПользователиИнформационнойБазы |
$null | __ComObject |
$тз.Колонки |
$null | __ComObject, Количество() = 2 |
$си.ВерсияПриложения |
пустая строка | 8.5.1.1343 |
При этом вызов метода через точку работает ($conn.NewObject(...), $q.Выполнить(), $тз.Количество()), и запись свойства через точку тоже работает ($q.Текст = ...). Не работает именно чтение. Одинаково в Windows PowerShell 5.1 и в PowerShell 7.
Теперь чем это было плохо. Листинг из прошлой редакции, прогнанный дословно, дает $схема = $null, цикл делает ноль итераций, не печатается ни одной строки, и скрипт выходит с кодом 0. Читатель, скопировавший мой рецепт, получил бы проверку, которая молчит и рапортует успех. Ровно то, против чего эта статья написана. При этом сам же в разделе про грабли писал, что работаю через обертки, а в листингах их не ставил.
Поэтому обертки теперь идут первыми, и весь остальной код в статье написан через них:
$ci = [System.Globalization.CultureInfo]::GetCultureInfo('ru-RU')
$BF = [System.Reflection.BindingFlags]
function Чит($о, $имя) { , $о.GetType().InvokeMember($имя, $BF::GetProperty, $null, $о, @(), $ci) }
function Зап($о, $имя, $знач) { [void]$о.GetType().InvokeMember($имя, $BF::SetProperty, $null, $о, @($знач), $ci) }
function Выз($о, $имя, $арг=@()) { , $о.GetType().InvokeMember($имя, $BF::InvokeMethod, $null, $о, $арг, $ci) }
Запятая перед $о.GetType() в Чит и Выз не опечатка и не украшение. Без нее функция возвращает коллекцию 1С, PowerShell разворачивает ее на возврате в System.Object[], и следующий же Количество() падает с «Method 'System.Object[].Количество' not found». Запятая заворачивает результат в одноэлементный массив, PowerShell разворачивает его, и наружу выходит живой объект. Один символ в двух функциях, а без него весь стенд сыплется в самом неочевидном месте.
Это и есть настоящая причина той ошибки про Object[], в которой раньше был обвинен InvokeMember. Не виноват: инлайн он отдает живую коллекцию, Количество() на ней работает. Виноват возврат из функции. А значит и прежний совет «считать колонки через ВыгрузитьКолонку в try/catch» лечил не ту болезнь ))
Подключение
$connector = New-Object -ComObject 'V85.COMConnector'
$conn = $connector.Connect('File="C:\Bases\Работа";Usr="Администратор";')
Write-Output ('движок: ' + (Чит ($conn.NewObject('СистемнаяИнформация')) 'ВерсияПриложения'))
Про выбор коннектора: одно утверждение из двух не пережило прогон
В первой редакции тут стояли две вещи: что для 8.3 идентификатор другой, V83.COMConnector, и что перепутать версии нельзя, потому что коннектор 8.3 к базе от 8.5 не подключится. Первое верно. Второе взято из документации и общих соображений, а когда дошло до проверки, оно рассыпалось.
Начиналось все не с этого. V83.COMConnector просто не работал:
CONNECT FAIL: Library not registered. (0x8002801D TYPE_E_LIBNOTREGISTERED)
При этом объект создается, падает именно Connect. В реестре все на месте: ProgID ведет на свой CLSID, CLSID на comcntr.dll от 8.3. А вот библиотеки типов, на которую этот CLSID ссылается, в HKLM\SOFTWARE\Classes\TypeLib просто нет. Зарегистрирована там только библиотека от 8.5. Похоже, установщик старшей версии перерегистрировал comcntr.dll на себя, но за причину не поручусь: видел результат, а не процесс. Живут два коннектора, работает один, и тот, что не работает, никак об этом не предупреждает, пока дело не дойдет до подключения.
Штатное лечение, regsvr32 нужной dll, требует прав администратора и переключает всю машину на одну версию. Нужны были обе сразу, поэтому пришлось дописать ветку руками в HKCU\Software\Classes\TypeLib (она читается наравне с машинной и прав не требует): GUID библиотеки берется из ветки TypeLib того самого CLSID, подключ 1.0\0\win64 получает путь к его comcntr.dll, плюс пустые FLAGS и HELPDIR. После этого заработали оба.
И вот тут выяснилось, что мое второе утверждение было выдумкой:
| Проба | Что получилось |
|---|---|
| коннектор 8.3 к базе Бухгалтерии | подключился на движке 8.3, запрос вернул данные |
| коннектор 8.5 к той же базе | подключился на движке 8.5, те же данные |
| коннектор 8.3 к демобазе ERP, которая до сих пор открывалась только на 8.5 | подключился, движок 8.3, данные на месте |
| коннектор 8.5 к ней же | подключился, движок 8.5 |
Ограничение есть, но оно не про формат, а про время: пока в файловой базе жив хоть какой-нибудь сеанс другой версии, вы получаете отказ.
Существуют активные сеансы работы с данной информационной базой, использующие Платформу 1С:Предприятия другой версии. Используйте для подключения платформу версии 8.3.27.2214.
Причем «хоть какой-нибудь» тут не фигура речи. Первый прогон исправленных листингов упал именно с этим текстом, и виноват был не скрипт: на базе висел брошенный тонкий клиент 8.3.27, запущенный совсем другой задачей 27.09.2026 в 15:48 и так и не завершившийся. Снял процесс, те же листинги прошли без единой ошибки. Если у вас этот отказ приходит на пустом месте, ищите не у себя в коде, а в списке процессов. Если два соединения разных версий делать из одного процесса, но честно отпускать первое, они уживаются.
Ту ветку реестра потом пришлось убрать: проверено, что V83.COMConnector снова падает с Library not registered, а V85 работает. Машина в том же состоянии, в каком была.
Уровень первый: тексты запросов
Текст запроса в модуле лежит в виде склейки строковых литералов. Вытащить его скриптом несложно, но есть две тонкости, на которых самописные экстракторы спотыкаются:
- Комментарии внутри литерала. Платформа разрешает вставлять закомментированные строки между строками-продолжениями. Для компилятора их нет, для наивного парсера они часть текста. Такие строки надо выбрасывать до склейки.
- Ветвления. Если текст собирается через
Если ... Тогда ... Иначе, мой экстрактор берет веткуТогдаи помечает, что вторая не проверена. Способа выбрать правильную автоматически не нашел, и до сих пор гоняю вторую ветку руками, когда вспомню. Чаще не вспоминаю.
Дальше все просто:
$q = $conn.NewObject('Запрос')
Зап $q 'Текст' ([System.IO.File]::ReadAllText($файлЗапроса, [System.Text.Encoding]::UTF8))
Выз $q 'УстановитьПараметр' @('НачалоПериода', [datetime]'2022-01-01') | Out-Null
Выз $q 'УстановитьПараметр' @('ОкончаниеПериода', [datetime]'2026-12-01') | Out-Null
Выз $q 'УстановитьПараметр' @('Процент', 99) | Out-Null
$вт = Выз (Выз $q 'Выполнить') 'Выгрузить'
Write-Output ('строк: ' + (Выз $вт 'Количество'))
foreach ($к in @('СуммаОтгрузкиПоЗаказу', 'Себестоимость', 'СуммаДоставки')) {
Write-Output (' итог ' + $к + ': ' + (Выз $вт 'Итог' @($к)))
}
Write-Output ('колонок: ' + (Выз (Чит $вт 'Колонки') 'Количество'))
Обратите внимание на [datetime] вместо привычного Get-Date. Это не вкусовщина: Get-Date через InvokeMember проходит, параметр устанавливается без ошибки, а потом запрос падает с «Нельзя сравнивать поля неограниченной длины и поля несовместимых типов» на сравнении даты. Похоже, обертка PSObject до платформы не разворачивается и дата приезжает строкой, но за объяснение не поручусь, а вот за наблюдение да: [datetime]'2022-01-01' работает, (Get-Date '2022-01-01') нет. Через точку, кстати, работают оба, так что при переводе листингов на обертки это вылезает внезапно.
Так мы ловим «Поле не найдено» и битые соединения:
Поле не найдено "ЗК.АналитикаУчетаПоПартнерам"
И, что не менее полезно, видно не только падает или нет, но и сколько строк и какие итоги. Отдельный вид беды — запрос, который отрабатывает без ошибок и возвращает ноль строк. На глаз в коде он не виден.
Отдельный прием для параметров-таблиц. Строить таблицу значений руками в PowerShell мучительно и легко промахнуться типом колонки. Проще попросить саму 1С:
$шаблон = $conn.NewObject('Запрос')
Зап $шаблон 'Текст' 'ВЫБРАТЬ ПЕРВЫЕ 0 Т.Номенклатура, Т.Количество ИЗ Документ.ЗаказКлиента.Товары КАК Т'
$тз = Выз (Выз $шаблон 'Выполнить') 'Выгрузить'
Уровень второй: компоновка СКД прямо из Template.xml
Схему компоновки не обязательно доставать из собранного ERF. В выгрузке отчета она лежит отдельным файлом Templates\ОсновнаяСхемаКомпоновкиДанных\Ext\Template.xml, и платформа читает его как есть:
$чтение = $conn.NewObject('ЧтениеXML')
Выз $чтение 'ОткрытьФайл' @($путьКTemplateXml) | Out-Null
$схема = Выз (Чит $conn 'СериализаторXDTO') 'ПрочитатьXML' @($чтение)
Выз $чтение 'Закрыть' | Out-Null
Тип XDTO вторым параметром указывать не надо. С явным типом, полученным через ФабрикаXDTO.Тип(...), вызов падает с «Несоответствие типов (параметр номер '2')», а без него сериализатор определяет тип сам.
Дальше собирается настоящая цепочка компоновки, по одному проходу на каждый вариант настроек:
$варианты = Чит $схема 'ВариантыНастроек'
$всего = Выз $варианты 'Количество'
Write-Output ('вариантов настроек: ' + $всего)
for ($i = 0; $i -lt $всего; $i++) {
$вариант = Выз $варианты 'Получить' @($i)
$источник = $conn.NewObject('ИсточникДоступныхНастроекКомпоновкиДанных', $схема)
$кн = $conn.NewObject('КомпоновщикНастроекКомпоновкиДанных')
Выз $кн 'Инициализировать' @($источник) | Out-Null
Выз $кн 'ЗагрузитьНастройки' @((Чит $вариант 'Настройки')) | Out-Null
$настройки = Выз $кн 'ПолучитьНастройки'
$км = $conn.NewObject('КомпоновщикМакетаКомпоновкиДанных')
$макет = Выз $км 'Выполнить' @($схема, $настройки)
$пкд = $conn.NewObject('ПроцессорКомпоновкиДанных')
Выз $пкд 'Инициализировать' @($макет) | Out-Null
$табДок = $conn.NewObject('ТабличныйДокумент')
$пв = $conn.NewObject('ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент')
Выз $пв 'УстановитьДокумент' @($табДок) | Out-Null
Выз $пв 'Вывести' @($пкд) | Out-Null
Write-Output (' OK "' + (Чит $вариант 'Имя') + '" строк в табдоке: ' + (Чит $табДок 'ВысотаТаблицы'))
}
Этот листинг прогнан в том виде, в каком он тут напечатан, на 5.1 и на 7.6.5, и печатает:
вариантов настроек: 3 OK "Основной" строк в табдоке: 11 OK "СписокЗаказов" строк в табдоке: 10 OK "Незакрытые" строк в табдоке: 10
Теперь главное, и оно выяснено не чтением документации, а сравнением трех схем. КомпоновщикМакета.Выполнить не жалуется на незаданный параметр. Берем схему, делаем копию, выкидываем из одного параметра единственную строку <use>Always</use> и прогоняем:
| Схема | Вариант | Макет | Вывод |
|---|---|---|---|
| целая, контрольная | все три | OK | OK, высоты 11 / 10 / 10 |
без use |
Основной | OK | FAIL: Не задано значение параметра "СтатьиРасходовВыделенные" {(228, 42)} |
без use |
СписокЗаказов | OK | FAIL, {(233, 42)} |
без use |
Незакрытые | OK | OK, высота 9 |
переставленный порядок элементов внутри <parameter> |
все три | OK | OK, высоты 11 / 10 / 10 |
Из этой таблицы следуют три разных вещи, и каждая из своих строк.
Макет строится всегда, на неподставленный параметр никто не жалуется, ошибка приходит только на Вывести. Значит останавливаться на макете нельзя: это зеленый ответ, который ничего не стоит.
Вариант «Незакрытые» прошел зеленым на той же битой схеме, потому что в его запросе этого параметра нет. То есть гонять надо все варианты настроек, а не первый. Проверь мы только его, битая схема ушла бы к заказчику как исправная.
Одна строка в табличном документе при этом все-таки потерялась, 9 против 10, и это стоит разобрать, потому что иначе объяснение выглядит подозрительно. Выгрузил текст обеих печаток по строкам и сравнил: пропала шапочная строка «Статьи прочих расходов, выводимые отдельной колонкой:». То есть в запросе параметра действительно нет, а в шапке он печатался, и снятие use убрало его оттуда. Заодно это живая проверка того, что параметр с Always лезет в шапку отчета.
И последняя строка: правило про порядок элементов внутри <parameter>, которое тут раньше стояло, не подтвердилось. Перестановка use вперед остальных не сломала ничего, все три высоты на месте. Правило снято.
Про внешние наборы данных и полтора потерянных часа
Если отчет получает данные из внешнего набора (модуль объекта готовит таблицу значений и отдает ее в ВнешниеНаборыДанных), то на стенде эту таблицу надо чем-то подменить. Первая мысль подсунуть пустую таблицу правильной структуры: ВЫБРАТЬ ПЕРВЫЕ 0, колонки на месте, данные не нужны, проверяем же схему.
Полтора часа у меня ушло на то, чтобы понять следующее: с пустым внешним набором СКД не выполняет связанные наборы данных. Связь не срабатывает, запрос второго набора не выполняется, ошибка в нем не всплывает. Проверка проходит успешно на заведомо сломанной схеме.
Заметно это стало только потому, что по привычке ушел контрольный запуск на старой версии файла, той самой, на которой у пользователя падало. Она прошла зеленым.
Вот тут и закрывается обещание из начала статьи про три холостых варианта стенда. Все три уже прошли перед вами:
- останавливался на
КомпоновщикМакета.Выполнитьи не доходил до вывода; - гонял только первый вариант настроек;
- подсовывал внешнему набору пустую таблицу.
Каждый был ровно таким же самообманом, как вызов из первой секции: зеленый ответ на заведомо сломанном входе.
Вылечилось одним символом: ВЫБРАТЬ ПЕРВЫЕ 1 вместо ВЫБРАТЬ ПЕРВЫЕ 0. После этого старая схема упала с текстом ошибки, слово в слово совпавшим со скриншотом пользователя, а новая дала данные.
Это наблюдение долго держалось в тексте с оговоркой, потому что на схеме из этой статьи повторить его не удавалось: внешних наборов в ней нет. Оговорку можно снять. Ревизор собрал отдельную схему специально под это: внешний набор, связанный с ним набор-запрос и в нем параметр без значения и без use. С ВЫБРАТЬ ПЕРВЫЕ 0 вывод прошел, высота 2, ошибка связанного набора не всплыла. С ВЫБРАТЬ ПЕРВЫЕ 1 получил Не задано значение параметра "НеЗаданныйПараметр". Другая схема, другой человек, тот же эффект.
Правило из этого получается рабочее: держать при себе последнюю сломанную версию файла и прогонять стенд на ней. Пока не увидел красное, стенд не построен, а нарисован.
Уровень третий: открыть весь ERF целиком
А вот тут придется публично сдать назад.
В первой редакции статьи стоял уверенный абзац о том, что открыть собранный ERF через COM не получается, потому что платформа выдает предупреждение безопасности, диалога в COM-соединении нет, и вызов падает. Напрашивающийся ход выглядит так:
$отчет = Выз (Чит $conn 'ВнешниеОтчеты') 'Создать' @($путьКErf)
$табДок = $conn.NewObject('ТабличныйДокумент')
$данныеРасшифровки = $conn.NewObject('ДанныеРасшифровкиКомпоновкиДанных')
Выз $отчет 'СкомпоноватьРезультат' @($табДок, $данныеРасшифровки) | Out-Null
Write-Output ('высота табдока: ' + (Чит $табДок 'ВысотаТаблицы'))
Когда сел закрывать это утверждение прогоном, оно отработало с первого раза. Высота 11, та же, что дала ручная цепочка на основном варианте настроек. Никакого предупреждения.
Разница оказалась в галочке. У пользователя информационной базы есть свойство ЗащитаОтОпасныхДействий.ПредупреждатьОбОпасныхДействиях, и в моих базах оно было снято. Пять шагов, каждый в своем процессе:
| Шаг | Флаг при подключении | Что сделал в этом же сеансе | Создать |
|---|---|---|---|
| 1 | снят | ничего | OK |
| 2 | снят | установил | OK |
| 3 | установлен | ничего | FAIL, «Предупреждение безопасности… Разрешить открывать данный файл?» |
| 4 | установлен | снял | FAIL, тот же текст |
| 5 | снят | ничего | OK |
Шаги 2 и 4 дали не то, чего ждешь: результат определяется не текущим значением флага, а тем, каким он был в момент подключения. Внутри живого соединения флаг переключать бесполезно, надо переподключаться. Этого я бы не угадал.
Других факторов не варьировал: одна машина, один пользователь, свои базы. Так что правильная формулировка — «в моих пробах на результат влиял только флаг», а не «больше ничего на него не влияет».
Дальше стало интереснее. Собрал заведомо сломанную копию своего отчета, вписав в модуль объекта обращение к общему модулю, которого нет ни в одной базе:
Ошибка инициализации модуля: ВнешнийОтчет.ВаловаяПоЗакрытымЗаказам.МодульОбъекта
{МодульОбъекта(4,10)}: Переменная не определена (РасширениеДоступа_Сервер)
Имя отчета и координаты тут свои, отчет-то другой, но ошибка ровно та же, с которой начиналась статья. Только теперь она приходит не от пользователя через двадцать минут, а от скрипта через три секунды. Целая копия того же отчета тем же вызовом открылась нормально, высота 11.
И третье. Если подсунуть отчет, написанный под другую конфигурацию, Создать падает еще на схеме, до всякого модуля:
Ошибка в схеме компоновки данных / Ошибка получения информации набора данных
/ Ошибка в запросе набора данных / {(104, 51)}: Неверные параметры
"Перечисление.ОперацииВзаиморасчетов.Оплата"
Итого этот путь делает три дела: сверяет схему с метаданными базы, компилирует модуль объекта и доводит компоновку до табличного документа. Вызовов при этом два, и это важно: схема без use спокойно проходит Создать и падает уже на СкомпоноватьРезультат. Останавливаться на открытии файла так же бесполезно, как останавливаться на макете.
Из этих трех дел ручная цепочка из «Уровня второго» не умеет второго: модуль она не трогает вовсе.
А вот чужую конфигурацию, к моему удивлению, ловит и она. Прогнал тот же Template.xml ручной цепочкой на базе Бухгалтерии, и все варианты упали. Правда, вот с чем:
Object reference not set to an instance of an object
С таким сообщением можно сидеть долго. ВнешниеОтчеты.Создать на той же базе называет и набор данных, и строку, и имя перечисления. Разница не в том, ловит или нет, а в том, что написано в ошибке, и это иногда важнее.
Зачем тогда ручная цепочка вообще нужна? Ей все еще есть место. Она работает по исходникам, без сборки ERF, показывает высоту по каждому варианту настроек отдельно, и в нее можно вмешаться в середине: подменить внешние наборы, посмотреть на макет. Но начинать надо с целого файла, и да, я понимаю, что пронумеровал уровни ровно наоборот тому, как ими стоит пользоваться. Так они и строились: сначала запросы, потом схема, потом уже догадка, что можно было открыть весь файл.
Три ошибки, которые так нашлись
«Не задано значение параметра»: параметру без значения нужен use=Always
Ошибка из начала статьи. В запросе набора данных отбор по списку:
КОГДА ПрочиеРасходы.СтатьяРасходов В (&СтатьиРасходовВыделенные)
Параметр в схеме есть, тип задан, список значений разрешен. Схема валидна, редактор компоновки претензий не имеет. А при выполнении «Не задано значение параметра».
Причина: у параметра нет значения и не выставлено использование. СКД такой параметр в запрос не подставляет, а текст запроса на него ссылается. Лечится одной строкой в XML схемы:
<use>Always</use>
Раньше в заголовке этого раздела стояло «параметру-списку», и это была случайная деталь моего случая. Ревизия сняла use у соседнего параметра-числа, у которого значение на месте, и ничего не сломалось. Значит дело не в том, что параметр списочный, а в том, что у него нет ни значения, ни использования.
Пустой список при этом дает В (пустой список), то есть ЛОЖЬ. И параметр с Always печатается в шапке отчета, так что служебный придется убирать из вывода отдельно. Оба наблюдения из практики, прогоном их не закрывал. Образец, с которым удобно сверяться, есть в типовой: отчет АнализЖурналаРегистрации, параметр ПользователиИГруппы.
Левое соединение, которое вело себя как внутреннее
Классика, которую трудно поймать чем-либо статическим, потому что синтаксически все верно:
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ПрочиеРасходы КАК ПрочиеРасходы
ПО ПрочиеРасходы.ЗаказКлиента = Заказы.Ссылка
ГДЕ
ПрочиеРасходы.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
Условие на правую таблицу стоит в ГДЕ, а не в условии соединения. Для заказов без прочих расходов правая часть дает NULL, сравнение с NULL не истина, строка отсекается. Левое соединение превратилось во внутреннее, и половина заказов пропала из отчета.
Прежде чем писать, что этого не ловит никто, стоило попробовать: запрос лег в модуль и уехал в BSL Language Server. Он выдал два замечания: обращение к метаданному, которого нет в моей рабочей папке, и устаревший Сообщить. Про соединение ни слова. Это один анализатор, а не все, поэтому формулировка осторожная: инструмента, который поймал бы такое статически, я не знаю, и буду рад ошибиться в комментариях.
COM-стенд тоже не сообщает об этом как об ошибке, тут нечему падать. Он ловит это иначе, количеством строк. Прогон до правки и после дает разные числа, и если вы ожидали, что число не изменится, значит нашли.
«Переменная не определена»: общий модуль из чужого расширения
Отчет обращался к общему модулю из расширения, которое стоит у заказчика. У него, с подключенным расширением, файл открывался нормально. В базе без расширения модуль объекта не компилируется вовсе, ошибка инициализации при открытии файла.
Ловушка в том, что защита времени выполнения тут не работает:
// ТАК НЕ РАБОТАЕТ
Если ЕстьМодульДоступа() Тогда
Менеджеры = РасширениеДоступа_Сервер.ДоступныеЗначения("Менеджер");
КонецЕсли;
Голое имя несуществующего модуля — это ошибка компиляции, а не выполнения, и до проверки условия дело не доходит. Проверка тут пошла уже после того, как абзац был написан, и правильно: своему слову в таких местах верить опасно. Два ERF, отличающихся только модулем объекта, открыты одним вызовом:
| Вариант модуля | Что сказал ВнешниеОтчеты.Создать |
|---|---|
вызов под Если ЕстьМодульДоступа() Тогда |
FAIL, {МодульОбъекта(9,11)}: Переменная не определена (РасширениеДоступа_Сервер) |
то же самое через Вычислить |
OK, отчет открылся и скомпоновался, высота 11 |
Условие не спасает: строка 9 — это как раз тело Если, и платформа спотыкается о нее до того, как условие вообще будет вычислено. Модуль должен исчезнуть из текста как идентификатор и остаться только строкой:
Функция ЕстьМодульДоступа()
Возврат Метаданные.ОбщиеМодули.Найти(ИмяМодуляДоступа()) <> Неопределено;
КонецФункции
// Модуль живет в расширении, поэтому вызывается через Вычислить():
// прямая ссылка на него не компилируется в базе без расширения.
Функция ВызватьМодульДоступа(ИмяМетода)
Возврат Вычислить(ИмяМодуляДоступа() + "." + ИмяМетода + "(""Менеджер"")");
КонецФункции
Функция ИмяМодуляДоступа()
Возврат "РасширениеДоступа_Сервер";
КонецФункции
У решения есть цена, и ее надо понимать: содержимое строки внутри Вычислить не видит ни конфигуратор, ни анализатор, переименование метода в расширении никто не подсветит, а опечатка всплывет только при вызове. Поэтому таких мест должно быть ровно одно, и вокруг него понятный запасной сценарий на случай, когда расширения нет.
Остальные грабли PowerShell
Главная, про точку, вынесена наверх, без нее ничего не работает. Здесь то, что осталось.
Грабель за эту работу набралось пять: чтение свойства через точку, запятая в обертке, дата через Get-Date, кодировка без BOM и имя обертки, перекрытое алиасом. Первые три разобраны выше, потому что без них не работает код.
Раньше тут стояло, что все это про Windows PowerShell 5.1, а в семерке часть не воспроизводится. Прогнали 27.09.2026 один и тот же скрипт двумя хостами, 5.1 и 7.6.5, и оказалось наоборот: из пяти семерка чинит ровно одну, кодировку, а остальные четыре живут в ней слово в слово.
Скрипт с кириллицей сохранять только в UTF-8 с BOM. Вот это как раз та единственная, которую семерка чинит. В 5.1 файл без BOM читается в системной кодировке, и строка $тз = 'строка' приезжает в парсер как $тз = 'строка'. Дальше сыплются ошибки про неожиданный токен и незакрытую скобку, и ни одна из них на кодировку не намекает. Тот же файл под PowerShell 7 отрабатывает как ни в чем не бывало.
Осторожнее с именами функций-оберток. Моя двухбуквенная обертка GP тихо перехватила алиас Get-ItemProperty и сломала половину скрипта: в PowerShell алиас старше функции. Называйте обертки длиннее, а лучше по-русски, как выше.
И про $Error. Стенд, который молчит, выглядит как стенд, который все проверил. Поэтому каждый прогон теперь заканчивается строкой Write-Output ('записей в $Error: ' + $Error.Count), и состоявшимся считается только прогон с нулем. Именно она показала бы историю с точкой на год раньше.
Чего этот способ не делает
Самое очевидное: вывод остается непроверенным. Табличный документ получается, его высота видна, а верстка, условное оформление, расшифровка и поведение формы отчета остаются на совести живого запуска.
Про модуль объекта выше сказано: открытие ERF его компилирует. С формами проверить было не на чем, у подопытного отчета их нет. Так что это не «не проверяет», а «я не проверил». Если у вас отчет с формой, прогоните и напишите в комментариях: компилирует или нет?
Вторая ветка ветвящегося запроса тоже мимо, про это было выше. Стенд видит ровно ту ветку, которую вы ему дали, и автоматики тут нет. Может, и не надо: проще прогнать оба текста отдельно и не умничать.
Предупреждение безопасности никуда не делось, просто про него ошибался в другую сторону. Если у пользователя ИБ галочка стоит, открыть файл через COM не выйдет, и это правильно. Мне известны два обхода, оба не бесплатные: либо владелец базы один раз открывает файл в клиенте и соглашается, либо галочка снимается, что уже вопрос политики, а не техники. Про согласие ходит правило, что оно привязано к файлу и после пересборки его надо давать заново; сам этого не прогонял, так что проверяйте. На своей отладочной базе галочка снята. На чужой боевой лезть бы не стал.
И последнее, про версии платформы: прежнее утверждение просто снято. Раньше здесь стоял абзац о том, что выгрузка отчета в новом формате читается только старшей линейкой. К Template.xml это не относится: в моей схеме компоновки атрибута версии нет вовсе, и обе версии платформы прочитали один и тот же файл и дали одинаковый результат, высоты 11, 10 и 10 по трем вариантам настроек. Про полную выгрузку отчета с формами вопрос открыт, там номер версии в Form.xml действительно есть, но это уже другая история и другая статья.
Чек-лист
- Любую проверку сначала проверить на сломанном входе, и вход подобрать так, чтобы он различал ваши гипотезы. Не покраснела, выбросить.
- Пакетный
/CheckModulesна внешний файл не тратить вообще: ключа для внешних файлов в платформе нет, а неизвестный под-ключ Конфигуратор проглатывает молча. - Начинать с открытия целого ERF через
ВнешниеОтчеты.СоздатьплюсСкомпоноватьРезультат. Это два вызова, и они ловят больше всего. Перед этим посмотреть у пользователя ИБ флагЗащитаОтОпасныхДействий.ПредупреждатьОбОпасныхДействиях: пока он установлен, файл не откроется, и переключать его внутри живого соединения бесполезно. - Свойства COM-объектов читать только через
InvokeMemberс культурой ru-RU, и в обертке ставить запятую перед вызовом. Через точку чтение молча не отдает значения. Даты в параметры передавать явным[datetime], а неGet-Date. - Если нужна работа по исходникам, без сборки, гонять тексты запросов и схему из
Template.xml. Смотреть не только на отсутствие ошибки, но и на число строк и итоги. - Компоновку прогонять до вывода в табличный документ, а не до макета, и по всем вариантам настроек: вариант, который не использует сломанный параметр, пройдет зеленым.
- Внешние наборы данных подменять непустой таблицей.
- Держать последнюю сломанную версию файла как контрольный образец.
- Скрипты с кириллицей сохранять в UTF-8 с BOM, обертки называть длинно.
- Каждое соединение отпускать, две версии платформы из одного процесса одновременно не держать, и помнить, что живой сеанс чужой версии может прийти со стороны.
- В конце прогона печатать
$Error.Countи не верить тишине.
А у вас какой из этих уровней уже стоит в сборке? Судя по тому, сколько раз я тут сдал назад, самое интересное в комментариях еще впереди.
Полный прогон в Предприятии этот стенд не отменяет. Он убирает из приемки тот класс ошибок, который стыдно показывать заказчику, и переносит два круга переписки в три секунды локального запуска.
Вступайте в нашу телеграмм-группу Инфостарт