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].Псевдоним = "Страна";
    
    Список.ТекстЗапроса = Схема.ПолучитьТекстЗапроса();

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

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

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

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

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

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