При разработке отчетов или печатных форм в системе 1С:Предприятие программисты часто сталкиваются с необходимостью дополнить данные табличной части документа информацией из других источников. Например, нужно вывести закупочные цены из регистра сведений в документ инвентаризации, где по умолчанию указаны только розничные цены. Типичная ошибка новичков заключается в попытке использовать "Левое соединение" напрямую к вложенной таблице запроса. Рассмотрим подробнее, почему этот подход не работает и как правильно структурировать запрос для получения нужного результата.
Разберем ситуацию, представленную в обсуждении. Разработчик пытается выбрать данные документа ИнвентаризацияТоваровНаСкладе и внутри этого же запроса обращается к табличной части Товары через конструкцию в скобках. Это создает так называемую "вложенную таблицу" в результате запроса. Когда возникает задача добавить к каждой строке этой вложенной таблицы данные (например, Цена из виртуальной таблицы СрезПоследних), платформа 1С не позволяет выполнить соединение ПО полю, находящемуся внутри этой вложенной выборки.
Проанализируем фрагмент ошибочного кода:
|ВЫБРАТЬ
| Инвентаризация.Номер,
| Инвентаризация.Товары.(
| Номенклатура,
| Количество
| )
|ИЗ
| Документ.ИнвентаризацияТоваровНаСкладе КАК Инвентаризация
|ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Цены.СрезПоследних КАК Цены
|ПО Инвентаризация.Товары.Номенклатура = Цены.Номенклатура // ОШИБКА!
В данном случае поле Инвентаризация.Товары.Номенклатура не является доступным для условия связи, так как оно находится внутри коллекции. Платформа просто "не видит" строки табличной части в контексте основного соединения документа.
Самый эффективный и правильный способ решить эту задачу — сменить объект метаданных, к которому мы обращаемся в секции ИЗ. Вместо того чтобы выбирать документ и "раскрывать" его таблицу, мы выберем саму таблицу как самостоятельный плоский список строк. Выясним, как это меняет структуру данных: каждая строка результата теперь будет соответствовать одной строке документа, а реквизиты шапки (номер, дата, склад) мы сможем получить "через точку" от реквизита Ссылка.
Рассмотрим правильный пример построения запроса:
Запрос.Текст =
"ВЫБРАТЬ
| ИнвТовары.Ссылка.Номер КАК Номер,
| ИнвТовары.Ссылка.Дата КАК Дата,
| ИнвТовары.Ссылка.Склад КАК Склад,
| ИнвТовары.Номенклатура КАК Номенклатура,
| ИнвТовары.Количество КАК Количество,
| ЕСТЬNULL(ЦеныСрезПоследних.Цена, 0) КАК ЦенаЗакупочная
|ИЗ
| Документ.ИнвентаризацияТоваровНаСкладе.Товары КАК ИнвТовары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ТипЦен = &ТипЦен) КАК ЦеныСрезПоследних
| ПО ИнвТовары.Номенклатура = ЦеныСрезПоследних.Номенклатура
|ГДЕ
| ИнвТовары.Ссылка = &ТекущийДокумент";
В этом примере левое соединение работает корректно, так как ИнвТовары.Номенклатура — это обычное поле в плоской таблице. Использование функции ЕСТЬNULL позволяет нам избежать проблем при отсутствии цены в регистре, подставляя ноль вместо пустого значения.
Если нам необходимо программно обрабатывать результат так, чтобы сначала шла шапка документа, а потом цикл по строкам (как это было в исходном вопросе автора через ВыборкаСтрокТовары = Шапка.Товары.Выбрать()), нам следует использовать конструкцию ИТОГИ. Это позволит платформе сгруппировать плоские строки обратно в удобную структуру.
Разберем по шагам, как модифицировать запрос и программный код:
ИТОГИ ПО Ссылка (или по всем реквизитам шапки).Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам).Пример реализации:
Запрос.Текст =
"ВЫБРАТЬ
| Ссылка КАК ДокументСсылка,
| Ссылка.Номер,
| Номенклатура,
| Количество,
| Цены.Цена КАК ЦенаЗак
|ИЗ
| Документ.ИнвентаризацияТоваровНаСкладе.Товары КАК ИнвТовары
|ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Дата, ) КАК Цены
|ПО ИнвТовары.Номенклатура = Цены.Номенклатура
|ГДЕ Ссылка = &ТекущийДокумент
|ИТОГИ ПО ДокументСсылка";
Результат = Запрос.Выполнить();
ВыборкаШапка = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаШапка.Следующий() Цикл
// Здесь работаем с данными шапки
Сообщить("Документ № " + ВыборкаШапка.Номер);
ВыборкаСтроки = ВыборкаШапка.Выбрать(); // Выбираем подчиненные записи (строки ТЧ)
Пока ВыборкаСтроки.Следующий() Цикл
// Здесь работаем со строками, где уже есть цена из соединения
Сообщить("Товар: " + ВыборкаСтроки.Номенклатура + " Цена: " + ВыборкаСтроки.ЦенаЗак);
КонецЦикла;
КонецЦикла;
Посмотрим на ситуацию со стороны производительности. Если документ содержит сотни строк, а регистр цен — миллионы, простое левое соединение может работать медленно. Проанализируем запрос из сообщения №11 форума. Автор пытался использовать подзапрос в параметрах виртуальной таблицы. Это правильное направление мысли, но синтаксически это лучше оформить через временные таблицы.
Рекомендация: Сначала выгрузите уникальные товары документа во временную таблицу, проиндексируйте их, а затем соединяйте с регистром. Это заставит сервер базы данных работать только с нужным подмножеством цен.
Пример оптимизированного подхода:
Номенклатура из ТЧ документа во временную таблицу ВТ_Товары.Номенклатура в этой временной таблице.ВТ_Товары с РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Дата, Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_Товары)).При выполнении соединения важно учитывать следующие моменты:
Номенклатуре выдаст неверный результат (или среднюю цену, если повезет). Обязательно добавляйте ПО ТЧ.Номенклатура = Цены.Номенклатура И ТЧ.Характеристика = Цены.Характеристика.ТипЦен в параметры виртуальной таблицы СрезПоследних. Это критически важно для корректности данных.СрезПоследних без указания даты вернет цены на текущий момент (на текущую секунду), а не на момент создания документа. Для исторических данных всегда используйте &ДатаДокумента.Таким образом, мы выяснили, что для добавления данных к строкам табличной части необходимо "развернуть" запрос, сделав саму табличную часть основным источником данных. Это открывает возможности для полноценного использования механизмов ЛЕВОЕ СОЕДИНЕНИЕ, индексации и оптимизации запросов, обеспечивая высокую скорость работы и точность получаемых данных.