Как в 1С отфильтровать список выбора одного реквизита по значению другого?

Программист 1С v8.3 (Управляемые формы)
← На главную

Рассмотрим частую задачу в разработке на 1С: необходимо ограничить варианты выбора в одном поле в зависимости от того, что указано в другом. Классический пример — форма договора контрагента. У нас есть владелец договора (контрагент) и вид договора. Задача — сделать так, чтобы при выборе контрагента с типом "Клиент" пользователю были доступны для выбора только договоры с видом "С клиентом", а другие варианты были скрыты.

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

Проанализируем ошибки в первоначальном коде и разберем правильные, стандартные способы решения этой задачи.

Анализ неверного подхода и типичных ошибок

В теме на форуме автор попытался использовать событие ПриИзменении для реквизита ВидДоговора. Давайте посмотрим на его код и разберем, почему он не работает и почему сам подход неоптимален.

Первая попытка на клиенте:


&НаКлиенте
Процедура ВидДоговораПриИзменении(Элемент)
         
   Если Объект.Владелец = ПредопределенноеЗначение("Перечисление.ТипыКонтрагентов.Клиент") Тогда   
     Объект.ВидДоговора = ("Перечисление.ВидыДоговоров.СКлиентом");
   КонецЕсли;
   
КонецПроцедуры

Здесь сразу несколько ошибок:

  1. Неверное сравнение. Реквизит Объект.Владелец — это ссылка на элемент справочника "Контрагенты". Мы не можем напрямую сравнивать ссылку на справочник со значением перечисления ТипыКонтрагентов. Чтобы получить тип контрагента, нужно обратиться к его реквизиту, например, Объект.Владелец.ТипКонтрагента.
  2. Неверное присваивание. Конструкция ("Перечисление.ВидыДоговоров.СКлиентом") присваивает реквизиту строку, а не значение перечисления. Правильно было бы использовать ПредопределенноеЗначение().
  3. Логика UX. Как уже говорилось, изменять выбор пользователя после того, как он его сделал, — плохая практика.

Вторая попытка на сервере:


&НаСервере
Процедура ПриИзмененииВидДоговораНаСервере()
     Если Объект.Владелец.ТипКонтрагента = ПредопределенноеЗначение("Перечисление.ТипыКонтрагентов.Клиент") Тогда   
        Объект.ВидДоговора = ПредопределенноеЗначение("Перечисление.ВидыДоговоров.СКлиентом");
    КонецЕсли;
КонецПроцедуры

Здесь тоже есть ошибки, на которые справедливо указали на форуме:

  1. Использование клиентской функции на сервере. Функция ПредопределенноеЗначение() работает только на клиенте. На сервере для обращения к предопределенным значениям перечислений используется прямой синтаксис: Перечисления.ИмяПеречисления.ИмяЗначения. В нашем случае это было бы Перечисления.ТипыКонтрагентов.Клиент и Перечисления.ВидыДоговоров.СКлиентом.
  2. Вызов процедуры. Скорее всего, эта серверная процедура просто ниоткуда не вызывается. Чтобы она сработала, ее нужно было бы вызвать из клиентского обработчика события ПриИзменении.

Теперь, когда мы разобрали ошибки, перейдем к правильным методам решения.

Способ 1: Использование "Связи параметров выбора" (Декларативный)

Это самый простой, быстрый и предпочтительный способ, если структура метаданных это позволяет. Он не требует написания кода и настраивается прямо в конфигураторе. Идея в том, чтобы "связать" поле для выбора с другим полем на форме.

Когда этот способ подойдет? Он идеально работает, если у значения, которое мы выбираем (в нашем случае это ВидДоговора, являющийся перечислением), есть реквизит, который можно сопоставить с полем-источником (Владелец или его реквизиты). В нашем конкретном случае вид договора — это перечисление, у которого нет реквизитов, поэтому этот способ напрямую не применим. Но если бы виды договоров хранились в справочнике, и у этого справочника был бы реквизит "ТипКонтрагента", этот метод был бы идеален.

Разберем по шагам, как это настраивается для подходящих случаев:

  1. Откройте в конфигураторе форму элемента справочника "ДоговорыКонтрагентов".
  2. В дереве элементов формы выделите поле ввода для реквизита "ВидДоговора".
  3. В палитре свойств найдите свойство Связи параметров выбора (не путать с "Параметры связи"!).
  4. Нажмите кнопку с многоточием. Откроется окно настройки.
  5. В этом окне добавьте новую связь. Укажите, что отбор будет производиться по значению поля Объект.Владелец.ТипКонтрагента. Платформа автоматически установит фильтр для списка выбора по этому значению.

Этот способ элегантен и не требует программирования, но его применимость ограничена структурой данных. Для нашей задачи с перечислением он не подходит, поэтому рассмотрим более универсальный программный метод.

Способ 2: Программная установка отбора в событии "НачалоВыбора"

Это самый гибкий и мощный способ, который подойдет для большинства ситуаций. Мы перехватываем момент, когда пользователь нажимает на кнопку выбора, и перед открытием формы выбора динамически формируем для нее нужный фильтр.

Для поля "ВидДоговора" в форме элемента справочника "ДоговорыКонтрагентов" создадим обработчик события НачалоВыбора.

Алгоритм действий будет следующим:

  1. Отменяем стандартное поведение. Первой строкой в обработчике пишем СтандартнаяОбработка = Ложь;. Это говорит платформе, что мы сами решим, как и какую форму выбора открывать.
  2. Получаем данные для фильтра. Проверяем, заполнен ли владелец договора. Если да, то нам нужно узнать его тип. Так как реквизит ТипКонтрагента хранится в базе данных, для его получения нужно сделать вызов на сервер, чтобы избежать лишних запросов и оптимизировать производительность.
  3. Формируем структуру отбора. На основе полученного типа контрагента создаем структуру, которая описывает фильтр. Например, если тип "Клиент", то мы хотим видеть только вид договора "СКлиентом".
  4. Открываем форму выбора с параметрами. Используем метод ОткрытьФорму(), передавая в него имя формы выбора и созданную нами структуру с параметрами отбора.

Посмотрим на пример реализации этого алгоритма.


&НаКлиенте
Процедура ВидДоговораНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
    
    // 1. Отменяем стандартное открытие формы выбора.
    СтандартнаяОбработка = Ложь;
    
    // Проверяем, выбран ли владелец. Если нет, то фильтровать не по чему.
    Если НЕ ЗначениеЗаполнено(Объект.Владелец) Тогда
        // Открываем форму выбора без всяких отборов.
        ОткрытьФорму("Перечисление.ВидыДоговоров.ФормаВыбора",, Элемент);
        Возврат;
    КонецЕсли;
    
    // 2. Получаем тип контрагента-владельца на сервере.
    ТипКонтрагентаВладельца = ПолучитьТипКонтрагентаНаСервере(Объект.Владелец);
    
    // 3. Создаем структуру для отбора.
    // В нашем случае мы хотим отобрать конкретные значения, а не по реквизиту.
    // Поэтому будем формировать не структуру отбора, а массив допустимых значений.
    СписокВыбора = Новый Массив;
    
    // В зависимости от типа контрагента наполняем массив допустимыми видами договоров.
    Если ТипКонтрагентаВладельца = Перечисления.ТипыКонтрагентов.Клиент Тогда
        СписокВыбора.Добавить(Перечисления.ВидыДоговоров.СКлиентом);
        // Можно добавить и другие виды, если они допустимы для клиента.
        // СписокВыбора.Добавить(Перечисления.ВидыДоговоров.Прочее); 
        
    ИначеЕсли ТипКонтрагентаВладельца = Перечисления.ТипыКонтрагентов.Поставщик Тогда
        СписокВыбора.Добавить(Перечисления.ВидыДоговоров.СПоставщиком);
        
    Иначе
        // Для всех остальных типов разрешаем, например, "Прочее"
        СписокВыбора.Добавить(Перечисления.ВидыДоговоров.Прочее);
    КонецЕсли;
    
    // 4. Открываем форму выбора, передавая ей наш список.
    // Стандартная форма выбора перечисления умеет работать со списком выбора.
    ПараметрыФормы = Новый Структура("ВыборЗначений", СписокВыбора);
    ОткрытьФорму("Перечисление.ВидыДоговоров.ФормаВыбора", ПараметрыФормы, Элемент);

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

&НаСервереБезКонтекста
Функция ПолучитьТипКонтрагентаНаСервере(СсылкаНаКонтрагента)
    
    // Это оптимальный способ получить один реквизит объекта, не считывая весь объект.
    // Для простых случаев можно и просто "разыменовать" ссылку:
    // Возврат СсылкаНаКонтрагента.ТипКонтрагента;
    // Но для высокой производительности лучше использовать ЗначениеРеквизитаОбъекта.
    Возврат ОбщегоНазначения.ЗначениеРеквизитаОбъекта(СсылкаНаКонтрагента, "ТипКонтрагента");
    
    // Если нет модуля ОбщегоНазначения, используем простой вариант:
    // Возврат СсылкаНаКонтрагента.ТипКонтрагента;

КонецФункции

Важные моменты в этом коде:

В заключение, как верно подметили на форуме, отличный способ научиться правильным техникам — это изучать код типовых конфигураций (например, "Бухгалтерия предприятия" или "Управление торговлей"). Подобные механизмы фильтрации списков там используются повсеместно и реализованы в соответствии с лучшими практиками разработки на платформе 1С.

← На главную