diamond АШ Tlg

Как расширять запрос в динамических списках на управляемой форме в 1C ЗУП 3.1

Разбор ситуации в ЗУП, когда необходимость формирования текста запроса через программный конструктор запросов (!) сделали нормой, а не извращением.

Постановка задачи

Задача чисто фантазийная, с целью подвести обучаемого к источнику проблемы и показать способ её решения!

Дано: при доработке в ЗУП документа "Командировка" решили сохранять в отдельном регистре сведений страну, если сотрудник направляется в зарубежную командировку. Вся разработка ведётся в расширении, этот регистр в упрощенном виде выглядит примерно так:

Новый регистр

Задача: добавить поле "Страна" в список доступных полей в форме журнала документов командировок.

Простое решение "в лоб"

Если мы откроем форму журнала документов "Командировки", то увидим, что источником таблицы формы является динамический список с произвольным запросом:

Произвольный запрос

Копируем текст запроса, вносим в него лёгкую модификацию, добавив левое соединение и меняем программно в расширении:

&НаСервере
Процедура Расш1_ПриСозданииНаСервереПосле(Отказ, СтандартнаяОбработка)
        
    Расш1_ДинамическийСписокМодификация();
    Расш1_ПрограммноеСозданиеЭлементовФормы();
        
КонецПроцедуры
    
&НаСервере
Процедура Расш1_ДинамическийСписокМодификация()
        
    Список.ТекстЗапроса = "ВЫБРАТЬ
        | ЖурналДокументовКомандировки.Ссылка КАК Ссылка,
        | ЖурналДокументовКомандировки.Дата КАК Дата,
        | НАЧАЛОПЕРИОДА(ЖурналДокументовКомандировки.Дата, ДЕНЬ) КАК ДатаДокумента,
        | ЖурналДокументовКомандировки.ПометкаУдаления КАК ПометкаУдаления,
        | ЖурналДокументовКомандировки.Номер КАК Номер,
        | ЖурналДокументовКомандировки.Проведен КАК Проведен,
        | ЖурналДокументовКомандировки.Месяц КАК Месяц,
        | ЖурналДокументовКомандировки.Сотрудник КАК Сотрудник,
        | ЖурналДокументовКомандировки.Организация КАК Организация,
        | ЖурналДокументовКомандировки.Начало КАК Начало,
        | ЖурналДокументовКомандировки.Окончание КАК Окончание,
        | ЖурналДокументовКомандировки.Утвержден КАК Утвержден,
        | ЖурналДокументовКомандировки.Ответственный КАК Ответственный,
        | ЖурналДокументовКомандировки.Комментарий КАК Комментарий,
        | ВЫБОР
        |     КОГДА НЕ ЖурналДокументовКомандировки.ПометкаУдаления
        |             И НЕ ЖурналДокументовКомандировки.Утвержден
        |         ТОГДА ИСТИНА
        |     ИНАЧЕ ЛОЖЬ
        | КОНЕЦ КАК ТребуетОбработки,
        | ЖурналДокументовКомандировки.Тип КАК Тип,
        | ВЫБОР
        |     КОГДА НаличиеФайлов.ЕстьФайлы ЕСТЬ NULL
        |         ТОГДА 0
        |     КОГДА НаличиеФайлов.ЕстьФайлы
        |         ТОГДА 1
        |     ИНАЧЕ 0
        | КОНЕЦ КАК ЕстьФайлы,
        | NULL КАК СостояниеОригиналаПервичногоДокумента,
        | ИСТИНА КАК ОбщееСостояние,
        | 0 КАК СостояниеОригиналПолучен,
        | Расш1_НаправленияКомандировок.Страна КАК Страна
        |ИЗ
        | ЖурналДокументов.Командировки КАК ЖурналДокументовКомандировки
        |     ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.НаличиеФайлов КАК НаличиеФайлов
        |     ПО ЖурналДокументовКомандировки.Ссылка = НаличиеФайлов.ОбъектСФайлами
        |     ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Расш1_НаправленияКомандировок КАК Расш1_НаправленияКомандировок
        |     ПО ЖурналДокументовКомандировки.Ссылка = Расш1_НаправленияКомандировок.Командировка
        |{ГДЕ
        | (ИСТИНА В
        |         (ВЫБРАТЬ ПЕРВЫЕ 1
        |             ИСТИНА
        |         ИЗ
        |             РегистрСведений.СоставДокументовЗарплатаКадры КАК СоставДокументовЗарплатаКадры
        |         ГДЕ
        |             СоставДокументовЗарплатаКадры.ФизическоеЛицо = &ФизическоеЛицо
        |             И ЖурналДокументовКомандировки.Ссылка = СоставДокументовЗарплатаКадры.ДокументФизическогоЛица)) КАК Поле2}";
        
КонецПроцедуры
    
&НаСервере
Процедура Расш1_ПрограммноеСозданиеЭлементовФормы()
        
    Расш1_Страна = Элементы.Добавить("расш1_Страна", Тип("ПолеФормы"), Элементы.Список);
    Расш1_Страна.ПутьКДанным = "Список.Страна";
    Расш1_Страна.Заголовок = "Страна";
        
КонецПроцедуры

Цель достигнута, получаем вполне рабочее решение (в данный момент времени).

Что же может пойти не так?

Спустя какое-то время HR подключил к ЗУП сервис "Кабинет Сотрудника" и отправил туда на ознакомление подписанный приказ:

Подписанный приказ Т9

И тут при попытке открыть список командировок начинаются довольно странные проблемы. Казалось бы, причём тут печатная форма, мы к ней вообще не прикасались? Первым делом конечно не глядя выносят вердикт что накосячили в типовой в обновлении:

Причем тут печатная форма?

Когда выясняется, что виновата всё-таки доработка, то отладчиком обнаруживается, что текст запроса изменился после вызова этого фрагмента кода, который, к слову, обнаруживается глобально почти во всех списочных формах кадровых документов ЗУП в процедуре ПриСозданииНаСервере():

// КадровыйЭДО
КадровыйЭДО.ПриСозданииНаСервереФормыСписка(ЭтотОбъект, Список);
// Конец КадровыйЭДО

После этого в запросе и появляются эти загадочные поля с именами печатных форм. Заглянем в процедуру, и обнаружим там прекрасный пример работы с "программным интерфейсом" любимого всеми конструктора запросов:

Схема = Новый СхемаЗапроса();
Схема.РежимКомпоновкиДанных = Истина;
Схема.УстановитьТекстЗапроса(Список.ТекстЗапроса);
...

Исправленное и улучшенное решение

Мы бы конечно могли перехватить в отладчике уже модифицированный КЭДО запрос, скопировать и дополнить в конфигураторе уже его, и получить вполне рабочее решение. Но, вероятно, это будет снова короткоживущая доработка - до первого существенного обновления функционала ЗУП.

Для качественного решения нам не остается другого пути, кроме как пойти стопами разработчиков ЗУПа. Скопируем из вышеуказанной процедуры общего модуля КЭДО куски кода и добавим источник с левым соединением, по образу и подобию:

&НаСервере
Процедура Расш1_ДинамическийСписокМодификация()

    Схема = Новый СхемаЗапроса();
    Схема.РежимКомпоновкиДанных = Истина;
    Схема.УстановитьТекстЗапроса(Список.ТекстЗапроса);
    Пакет = Схема.ПакетЗапросов[Схема.ПакетЗапросов.Количество() - 1];
    Оператор = Пакет.Операторы[0];
    
    Оператор.Источники.Добавить("РегистрСведений.Расш1_НаправленияКомандировок", "Расш1_НаправленияКомандировок");
    ИсточникЖурналДокументовКомандировки = Оператор.Источники[0];
    ИсточникЖурналДокументовКомандировки.Соединения.Добавить(
        "Расш1_НаправленияКомандировок", 
        "ЖурналДокументовКомандировки.Ссылка = Расш1_НаправленияКомандировок.Командировка");
    НовСоединение = ИсточникЖурналДокументовКомандировки.Соединения.НайтиПоПсевдониму("Расш1_НаправленияКомандировок");
    НовСоединение.ОбязательноеСоединение = Истина;
    НовСоединение.ТипСоединения = ТипСоединенияСхемыЗапроса.ЛевоеВнешнее;
        
    Оператор.ВыбираемыеПоля.Добавить("Расш1_НаправленияКомандировок.Страна");
    Пакет.Колонки[Оператор.ВыбираемыеПоля.Количество() - 1].Псевдоним = "Страна";
    
    Список.ТекстЗапроса = Схема.ПолучитьТекстЗапроса();

КонецПроцедуры

К сожалению, качественной документации с полными примерами для интерфейса СхемаЗапроса я не нашёл, хотя честнее будет сказать что просто поленился искать. Как у меня получилось? Просто раскрыл отладчиком объект СхемаЗапроса и заглянул в каждый его уголок. Если вам нужно программно соорудить более сложный запрос, чем с левым соединением, то рекомендую загрузить в объект пример готового целевого запроса и заскринить подробный состав схемы в отладчике. Думаю, это самый быстрый и лёгкий способ изучения.

Частное мнение

Надо ли таким способом создавать новые запросы? НЕТ, кроме случаев разработки собственной консоли запросов, или какого-то генератора мега-запросов на все случаи жизни.

Можно ли таким способом модифицировать готовые запросы? ДА, если иного выхода нет.