В процессе разработки в среде 1С:Предприятие 8 (в частности, в конфигурациях на обычных формах, таких как «Управление торговым предприятием» или «Управление производственным предприятием») часто возникает задача связать дочерний документ с родительским. Например, нам необходимо, находясь в контексте документа Расходный ордер на товары, узнать значение флага ТоварВывезен (булево), который расположен в родительском документе Реализация товаров и услуг.
В этой статье мы подробно разберем, как правильно реализовать эту задачу, проанализируем архитектурные особенности связей документов в УТП, рассмотрим способы оптимизации запросов и разберем популярные ошибки разработчиков.
Перед тем как писать код, важно понять, как именно документы связаны между собой в вашей конфигурации. В типовой УТП связь между финансовым документом (накладной) и складским (ордером) строится следующим образом:
Расходный ордер на товары связь с накладной чаще всего хранится не в стандартном реквизите ДокументОснование, а в специализированном реквизите ДокументПередачи (который имеет составной тип данных, включающий реализации, перемещения и т.д.).ДокументПередачи будет напрямую записана ссылка на РеализацияТоваровУслуг.ЗаказПокупателя. Прямой ссылки между ордером и реализацией в базе данных может не быть, и искать связь придется через родительский заказ.Распространенная ошибка начинающих разработчиков — получение всего объекта для чтения одного-единственного реквизита. Например, конструкция СсылкаНаДокумент.ПолучитьОбъект().ТоварВывезен является крайне неоптимальной. При вызове метода ПолучитьОбъект() платформа считывает из базы данных абсолютно все данные документа, включая тяжелые табличные части, дополнительные реквизиты и движения. Это создает избыточную нагрузку на СУБД и сеть.
Для быстрого и легкого получения отдельного реквизита в конфигурациях на базе БСП (Библиотеки стандартных подсистем) следует использовать функцию:
ОбщегоНазначения.ЗначениеРеквизитаОбъекта(СсылкаНаДокумент, "ТоварВывезен")
Под капотом эта функция формирует легкий SQL-запрос, который выбирает только одно необходимое поле, минимизируя нагрузку на сервер.
Если нам достоверно известно, что в документе Расходный ордер на товары заполнен реквизит ДокументПередачи, содержащий ссылку на реализацию, мы можем написать быстрый и безопасный запрос. Поскольку ДокументПередачи имеет составной тип данных, в запросе крайне важно использовать оператор ВЫРАЗИТЬ. Это исключит лишние соединения (LEFT JOIN) со всеми возможными таблицами документов составного типа в СУБД.
Рассмотрим пример функции, реализующей этот подход:
Функция ПолучитьСтатусВывозаТовара(СсылкаНаОрдер) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВЫРАЗИТЬ(Ордер.ДокументПередачи КАК Документ.РеализацияТоваровУслуг).ТоварВывезен КАК ТоварВывезен
|ИЗ
| Документ.РасходныйОрдерНаТовары КАК Ордер
|ГДЕ
| Ордер.Ссылка = &СсылкаНаОрдер";
Запрос.УстановитьПараметр("СсылкаНаОрдер", СсылкаНаОрдер);
РезультатЗапроса = Запрос.Выполнить();
Если НЕ РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
// Возвращаем булево значение реквизита (или Ложь, если реквизит не заполнен)
Возврат Истина = Выборка.ТоварВывезен;
КонецЕсли;
Возврат Ложь;
КонецФункции
Если цепочка документов в вашей системе может быть произвольной длины (например: Ордер -> Расходная накладная -> Заказ -> ...), потребуется универсальный алгоритм поиска, который будет последовательно подниматься «вверх» по цепочке подчиненности, пока не обнаружит документ нужного типа.
Для реализации такого обхода напишем рекурсивную функцию, которая анализирует метаданные документов на каждом шаге:
Функция НайтиРодительскуюРеализацию(ИсходныйДокумент) Экспорт
// Если на входе уже ссылка на Реализацию, возвращаем её
Если ТипЗнч(ИсходныйДокумент) = Тип("ДокументСсылка.РеализацияТоваровУслуг") Тогда
Возврат ИсходныйДокумент;
КонецЕсли;
МетаданныеДокумента = ИсходныйДокумент.Метаданные();
РодительскийДокумент = Неопределено;
// Сначала пытаемся найти специализированный реквизит связи УТП
Если МетаданныеДокумента.Реквизиты.Найти("ДокументПередачи") <> Неопределено Тогда
РодительскийДокумент = ИсходныйДокумент.ДокументПередачи;
// Если его нет, проверяем стандартный реквизит ДокументОснование
ИначеЕсли МетаданныеДокумента.Реквизиты.Найти("ДокументОснование") <> Неопределено Тогда
РодительскийДокумент = ИсходныйДокумент.ДокументОснование;
КонецЕсли;
// Если родитель найден и он заполнен, рекурсивно проверяем его
Если ЗначениеЗаполнено(РодительскийДокумент) Тогда
Возврат НайтиРодительскуюРеализацию(РодительскийДокумент);
КонецЕсли;
// Если поднялись до самого верха и реализацию не нашли
Возврат Документы.РеализацияТоваровУслуг.ПустаяСсылка();
КонецФункции
Имея эту функцию, получить статус вывоза товара становится предельно просто:
СсылкаНаРеализацию = НайтиРодительскуюРеализацию(СсылкаНаОрдер);
Если ЗначениеЗаполнено(СсылкаНаРеализацию) Тогда
ТоварВывезен = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(СсылкаНаРеализацию, "ТоварВывезен");
Иначе
ТоварВывезен = Ложь; // Реализация в цепочке не найдена
КонецЕсли;
Разберем ситуацию, когда ордер и реализация не ссылаются друг на друга напрямую, но оба подчинены одному документу ЗаказПокупателя. В таком случае нам нужно пойти «вверх» до Заказа, а затем «вниз» до связанной реализации. Сделать это эффективнее всего с помощью пакетного запроса:
Функция ПолучитьВывозПоСвязанномуЗаказу(СсылкаНаОрдер) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗРЕШЕННЫЕ
| Реализация.ТоварВывезен КАК ТоварВывезен
|ИЗ
| Документ.РасходныйОрдерНаТовары КАК Ордер
| // Соединяем ордер с реализацией по общему документу сделки (Заказу)
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Реализация
| ПО (Ордер.ДокументПередачи = Реализация.Сделка ИЛИ Ордер.ДокументПередачи = Реализация.ДокументОснование)
|ГДЕ
| Ордер.Ссылка = &СсылкаНаОрдер
| И НЕ Реализация.ПометкаУдаления";
Запрос.УстановитьПараметр("СсылкаНаОрдер", СсылкаНаОрдер);
РезультатЗапроса = Запрос.Выполнить();
Если НЕ РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Возврат Выборка.ТоварВывезен;
КонецЕсли;
Возврат Ложь;
КонецФункции
Часто на форумах советуют использовать типовой критерий отбора СвязанныеДокументы для получения родителей. Это грубая ошибка.
Критерии отбора в платформе 1С работают исключительно по принципу «сверху вниз». Они ищут документы, которые ссылаются на переданный объект. Например, если вы передадите в критерий отбора ссылку на Реализацию товаров и услуг, он успешно найдет подчиненный Расходный ордер на товары, так как внутри структуры ордера записана ссылка на реализацию.
Но если вы передадите в критерий отбора ссылку на ордер, он вернет пустой результат, потому что сама реализация ничего не знает об ордере и не хранит на него ссылку в своих реквизитах. Попытка использовать критерий отбора для движения «вверх» к родительским документам заведомо обречена на провал.
Для надежного и быстрого решения задачи всегда анализируйте схему документооборота на конкретном внедрении. Если цепочки документов жесткие и регламентированные — используйте прямые запросы с оператором ВЫРАЗИТЬ (Вариант 1) или метод ЗначениеРеквизитаОбъекта. Если схемы гибкие и могут содержать промежуточные звенья — применяйте рекурсивную функцию поиска по метаданным (Вариант 2).