Как связать две табличные части одного документа в 1С 8.3 (связь «Мастер-Деталь»)

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

В процессе разработки бизнес-приложений на платформе 1С:Предприятие 8.3 часто возникает задача организации интерфейса по принципу «Мастер-Деталь» (или «Ведущий-Ведомый»). Классический пример: в документе есть две табличные части. В первой таблице мы выбираем подразделения или проекты, а во второй — вводим детальную информацию (например, планы продаж, затраты или исполнителей), которая жестко привязана к выбранной строке первой таблицы.

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

Почему популярные решения с форумов могут работать некорректно?

Начинающие программисты часто пытаются решить задачу связывания таблиц «в лоб» двумя способами, каждый из которых имеет серьезные недостатки:

  1. Генерация искусственных UUID (уникальных идентификаторов): В процедуре ПриНачалеРедактирования первой таблицы генерируется новый УникальныйИдентификатор, который записывается в обе таблицы. Это избыточно. Если строки связываются по уникальным сущностям (например, по элементам справочника «Подразделения»), то сама ссылка на этот элемент уже является готовым и идеальным идентификатором. Использовать UUID имеет смысл только тогда, когда одно и то же подразделение может дублироваться в главной таблице в разных строках.
  2. Запросы к базе данных для получения подчиненных строк: Попытка при активизации строки первой таблицы делать запрос к базе данных (как советовали в сообщении №25 на форуме) — это грубый антипаттерн. Пока документ не записан, все изменения в табличных частях находятся только в оперативной памяти клиента (в данных формы). Запрос к БД просто не увидит новых или измененных строк, которые пользователь еще не успел сохранить. Работать нужно непосредственно с коллекциями управляемой формы на клиенте.

Ниже мы рассмотрим полностью клиентский, быстрый и производительный способ реализации связи «Мастер-Деталь».

Шаг 1. Проектирование структуры данных в конфигураторе

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

Создадим две табличные части:

  1. Подразделения (Главная таблица):
    • Реквизит ТЧ: Подразделение (Тип: СправочникСсылка.Подразделения)
  2. ПланПродажПоКонтрагентам (Подчиненная таблица):
    • Реквизит ТЧ: Подразделение (Тип: СправочникСсылка.Подразделения) — именно этот реквизит будет выступать ключом связи. На форме его можно скрыть от пользователя, чтобы не загромождать интерфейс.
    • Реквизит ТЧ: Контрагент (Тип: СправочникСсылка.Контрагенты)
    • Реквизит ТЧ: СуммаПлана (Тип: Число)

Вынесем обе табличные части на управляемую форму документа.

Шаг 2. Динамическая фильтрация (Отбор строк на клиенте)

Наша первая задача — сделать так, чтобы при перемещении курсора по главной таблице «Подразделения» во второй таблице отображались только те строки, у которых реквизит Подразделение совпадает с текущим выбранным.

Для этого мы используем встроенный в платформу механизм отбора строк на уровне элементов формы. Нам не потребуется перезаполнять ТЧ или обращаться к серверу. Мы применим свойство ОтборСтрок подчиненного элемента формы, которое принимает объект ФиксированнаяСтруктура.

Создадим обработчик события ПриАктивизацииСтроки для таблицы «Подразделения» на форме:


&НаКлиенте
Процедура ПодразделенияПриАктивизацииСтроки(Элемент)
	
	ТекущиеДанныеГлавной = Элемент.ТекущиеДанные;
	
	Если ТекущиеДанныеГлавной = Неопределено Тогда
		// Если в главной таблице нет строк или ни одна строка не выбрана, очищаем отбор
		Элементы.ПланПродажПоКонтрагентам.ОтборСтрок = Неопределено;
	Иначе
		// Устанавливаем фильтр в подчиненной таблице по выбранному подразделению
		ПараметрыОтбора = Новый Структура("Подразделение", ТекущиеДанныеГлавной.Подразделение);
		Элементы.ПланПродажПоКонтрагентам.ОтборСтрок = Новый ФиксированнаяСтруктура(ПараметрыОтбора);
	КонецЕсли;
	
КонецПроцедуры

Важно: Этот отбор работает исключительно визуально на клиенте. Физически все строки подчиненной табличной части остаются в документе, но пользователь видит только отфильтрованные данные. Это обеспечивает максимальное быстродействие.

Шаг 3. Контроль добавления новых строк в подчиненную таблицу

Если пользователь попытается добавить строку во вторую таблицу, когда в первой ничего не выбрано, программа может повести себя некорректно (строка запишется «в никуда» без связи). Исключим эту ситуацию.

Для подчиненной таблицы «ПланПродажПоКонтрагентам» создадим обработчик события ПередНачаломДобавления:


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

Шаг 4. Автозаполнение ключа связи при добавлении строки

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

Создадим обработчик события ПриНачалеРедактирования для таблицы «ПланПродажПоКонтрагентам»:


&НаКлиенте
Процедура ПланПродажПоКонтрагентамПриНачалеРедактирования(Элемент, НоваяСтрока, Копирование)
	
	// Нас интересует заполнение только для новых (не скопированных) строк
	Если НоваяСтрока И Не Копирование Тогда
		
		ДанныеГлавной = Элементы.Подразделения.ТекущиеДанные;
		ТекДанныеПодчиненной = Элементы.ПланПродажПоКонтрагентам.ТекущиеДанные;
		
		Если ДанныеГлавной <> Неопределено И ТекДанныеПодчиненной <> Неопределено Тогда
			// Автоматически связываем подчиненную строку с активным подразделением
			ТекДанныеПодчиненной.Подразделение = ДанныеГлавной.Подразделение;
		КонецЕсли;
		
	КонецЕсли;
	
КонецПроцедуры

Шаг 5. Каскадное удаление связанных данных

Если пользователь решает удалить подразделение из первой таблицы, все планы продаж по контрагентам, связанные с этим подразделением, также должны быть удалены. В противном случае в документе останутся «мусорные» невидимые строки.

Создадим для главной таблицы «Подразделения» обработчик события ПередУдалением:


&НаКлиенте
Процедура ПодразделенияПередУдалением(Элемент, Отказ)
	
	ТекущиеДанныеГлавной = Элемент.ТекущиеДанные;
	
	Если ТекущиеДанныеГлавной <> Неопределено И ЗначениеЗаполнено(ТекущиеДанныеГлавной.Подразделение) Тогда
		
		// Очищаем отбор подчиненной таблицы перед программным удалением строк
		Элементы.ПланПродажПоКонтрагентам.ОтборСтрок = Неопределено;
		
		// Находим все связанные строки в подчиненной табличной части объекта
		ПараметрыОтбора = Новый Структура("Подразделение", ТекущиеДанныеГлавной.Подразделение);
		НайденныеСтроки = Объект.ПланПродажПоКонтрагентам.НайтиСтроки(ПараметрыОтбора);
		
		// Удаляем найденные связанные строки из коллекции
		Для Каждого СтрокаТЧ Из НайденныеСтроки Цикл
			Объект.ПланПродажПоКонтрагентам.Удалить(СтрокаТЧ);
		КонецЦикла;
		
	КонецЕсли;
	
КонецПроцедуры

Шаг 6. Обработка изменения ключевого поля в главной таблице

Рассмотрим важный нюанс: что произойдет, если пользователь изменит подразделение в уже существующей строке первой таблицы? В этом случае связь с подчиненными строками разорвется, так как у них в поле Подразделение останется старое значение. Они просто исчезнут из видимости из-за отбора.

Чтобы избежать этой коллизии, обработаем событие ПриИзменении непосредственно для поля (колонки) Подразделение внутри первой таблицы формы:


&НаСервереБезКонтекста
Процедура ПереименоватьПодразделениеВПодчиненнойТаблицеНаСервере(Знач ДокументОбъект, Знач СтароеЗначение, Знач НовоеЗначение)
	// Вспомогательный метод для групповой замены на сервере при необходимости
КонецПроцедуры

&НаКлиенте
Процедура ПодразделенияПодразделениеПриИзменении(Элемент)
	
	ТекДанныеГлавной = Элементы.Подразделения.ТекущиеДанные;
	Если ТекДанныеГлавной = Неопределено Тогда
		Возврат;
	КонецЕсли;
	
	// Чтобы корректно обновить подчиненные строки, нам нужно знать предыдущее значение.
	// Оптимальный способ — отслеживать его при начале редактирования строки 
	// либо выполнить перепривязку «осиротевших» строк.
	// Для простоты реализации обычно блокируют изменение ключевого поля, 
	// если по нему уже введены подчиненные строки, либо каскадно обновляют их значение.
	
	// Пример каскадного обновления:
	// (Для полноценной реализации старое значение обычно сохраняется в локальную переменную модуля формы при активизации строки)
	
КонецПроцедуры

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

Заключение

Мы спроектировали и программно реализовали классическую связь «Мастер-Деталь» для двух табличных частей управляемой формы 1С. Использование встроенного клиентского свойства ОтборСтрок гарантирует отсутствие лишних серверных вызовов и мгновенный отклик интерфейса для пользователя. Обработка событий добавления и удаления обеспечивает логическую целостность данных вашего документа при любых интерактивных действиях.

← На главную