Как рассчитывать и отображать даты родительских задач в иерархическом справочнике 1С

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

При разработке подсистем управления проектами и проектными задачами в 1С часто возникает задача агрегации данных: для родительских задач (групп или элементов-узлов) дата начала должна определяться как минимальная дата среди всех подчиненных элементов, а дата окончания — как максимальная. Разберем возможные архитектурные подходы реализации этой задачи для обычных форм (например, конфигурация «Управление производственным предприятием 1.2»), их плюсы, минусы и особенности реализации.

Вариант 1. Динамический расчет и вывод в списке через событие ПриПолученииДанных

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

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

Разберем по шагам реализацию в форме списка:

  1. Добавим в табличное поле списка две новые колонки (например, ДатаНачалаРасчетная и ДатаОкончанияРасчетная) без привязки к данным реквизитов.
  2. Создадим обработчик события ПриПолученииДанных(Элемент, ОформленияСтрок). В отличие от ПриВыводеСтроки, данный обработчик получает сразу массив строк, видимых на экране, что позволяет выполнить один пакетный запрос вместо сотен единичных запросов.
  3. В обработчике отберем ссылки только на родительские элементы и выполним расчет границ дат запросом.

Посмотрим на пример кода обработчика в модуле формы списка:


Процедура СписокПриПолученииДанных(Элемент, ОформленияСтрок)
	
	МассивРодителей = Новый Массив;
	
	Для Каждого Оформление Из ОформленияСтрок Цикл
		// Проверяем, является ли строка родителем (группой или узлом иерархии)
		Если Оформление.ДанныеСтроки.ЭтоГруппа Или Оформление.ДанныеСтроки.Ссылка.ЭтоГруппа Тогда
			МассивРодителей.Добавить(Оформление.ДанныеСтроки.Ссылка);
		КонецЕсли;
	КонецЦикла;
	
	Если МассивРодителей.Количество() = 0 Тогда
		Возврат;
	КонецЕсли;
	
	Запрос = Новый Запрос;
	Запрос.Текст = 
		"ВЫБРАТЬ
		|	ПроектныеЗадачи.Родитель КАК Родитель,
		|	МИНИМУМ(ПроектныеЗадачи.ДатаНачала) КАК ДатаНачала,
		|	МАКСИМУМ(ПроектныеЗадачи.ДатаОкончания) КАК ДатаОкончания
		|ИЗ
		|	Справочник.ПроектныеЗадачи КАК ПроектныеЗадачи
		|ГДЕ
		|	ПроектныеЗадачи.Родитель В(&МассивРодителей)
		|	И НЕ ПроектныеЗадачи.ПометкаУдаления
		|СГРУППИРОВАТЬ ПО
		|	ПроектныеЗадачи.Родитель";
	
	Запрос.УстановитьПараметр("МассивРодителей", МассивРодителей);
	Результат = Запрос.Выполнить().Выгрузить();
	
	Для Каждого Оформление Из ОформленияСтрок Цикл
		СтрокаИтога = Результат.Найти(Оформление.ДанныеСтроки.Ссылка, "Родитель");
		Если СтрокаИтога <> Неопределено Тогда
			Оформление.Ячейки.ДатаНачалаРасчетная.УстановитьТекст(Формат(СтрокаИтога.ДатаНачала, "ДФ=dd.MM.yyyy"));
			Оформление.Ячейки.ДатаОкончанияРасчетная.УстановитьТекст(Формат(СтрокаИтога.ДатаОкончания, "ДФ=dd.MM.yyyy"));
		КонецЕсли;
	КонецЦикла;
	
КонецПроцедуры

Важное ограничение: при динамическом выводе стандартные отборы и сортировки по этим колонкам работать не будут.

Вариант 2. Хранение расчетных дат в реквизитах справочника и расчет в модуле объекта

Если по расчетным датам родителей необходимо строить отчеты, производить сортировку в списке или контролировать сроки в бизнес-логике, их следует хранить в реквизитах родителя. В таком случае расчет выполняется на сервере в модуле объекта при записи подчиненных элементов.

Выясним причину, почему расчет не следует делать в модуле формы: запись элемента может производиться программно (из обработок, обменов, регламентных заданий), минуя форму.

Рассмотрим нюансы, которые необходимо учесть при реализации пересчета в модуле объекта СправочникОбъект.ПроектныеЗадачи:

  1. Многоуровневая иерархия: изменение даты на нижнем уровне должно вызывать рекурсивный пересчет по всей цепочке родителей вверх до корня.
  2. Смена родителя: при перемещении задачи из одной ветки в другую границы дат необходимо пересчитывать как у нового родителя, так и у старого (значение Ссылка.Родитель).
  3. Пометка на удаление: при установке или снятии пометки удаления у подчиненного элемента требуется обновить агрегаты родителя.
  4. Предотвращение бесконечной рекурсии: при программной записи родителя внутри обработчика необходимо устанавливать свойство ОбменДанными.Загрузка = Истина, чтобы избежать повторного срабатывания логики.
  5. Блокировки транзакций: при параллельной записи подчиненных задач разными пользователями родительский узел становится узким местом. В модуле объекта перед пересчетом следует учитывать возможные конфликты блокировок.

Посмотрим на пример реализации логики пересчета в модуле объекта:


Процедура ПриЗаписи(Отказ)
	
	Если ОбменДанными.Загрузка Тогда
		Возврат;
	КонецЕсли;
	
	// Проверяем наличие родителя для пересчета
	Если ЗначениеЗаполнено(Родитель) Тогда
		ПересчитатьДатыРодителя(Родитель);
	КонецЕсли;
	
	// Если родитель изменился, обновляем также старого родителя
	Если ЗначениеЗаполнено(Ссылка) И Ссылка.Родитель <> Родитель И ЗначениеЗаполнено(Ссылка.Родитель) Тогда
		ПересчитатьДатыРодителя(Ссылка.Родитель);
	КонецЕсли;
	
КонецПроцедуры

Процедура ПересчитатьДатыРодителя(СсылкаРодителя)
	
	Запрос = Новый Запрос;
	Запрос.Текст = 
		"ВЫБРАТЬ
		|	МИНИМУМ(ПроектныеЗадачи.ДатаНачала) КАК МинДата,
		|	МАКСИМУМ(ПроектныеЗадачи.ДатаОкончания) КАК МаксДата
		|ИЗ
		|	Справочник.ПроектныеЗадачи КАК ПроектныеЗадачи
		|ГДЕ
		|	ПроектныеЗадачи.Родитель = &Родитель
		|	И НЕ ПроектныеЗадачи.ПометкаУдаления";
	
	Запрос.УстановитьПараметр("Родитель", СсылкаРодителя);
	Выборка = Запрос.Выполнить().Выбрать();
	
	Если Выборка.Следующий() Тогда
		ОбъектРодитель = СсылкаРодителя.ПолучитьОбъект();
		Если ОбъектРодитель <> Неопределено Тогда
			ОбъектРодитель.ОбменДанными.Загрузка = Истина;
			ОбъектРодитель.ДатаНачала = Выборка.МинДата;
			ОбъектРодитель.ДатаОкончания = Выборка.МаксДата;
			Попытка
				ОбъектРодитель.Записать();
			Исключение
				// Обработка коллизий блокировок
			КонецПопытки;
		КонецЕсли;
	КонецЕсли;
	
КонецПроцедуры

Вариант 3. Использование независимого регистра сведений

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

Разберем структуру регистра:

  1. Измерение: ПроектнаяЗадача (тип СправочникСсылка.ПроектныеЗадачи).
  2. Ресурсы: ДатаНачала (тип Дата), ДатаОкончания (тип Дата).

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

Сравнение и выбор подхода

Проанализируем критерии выбора между предложенными вариантами:

  1. Динамический вывод в форме (Вариант 1): подходит, если даты нужны исключительно для визуального восприятия оператором списка задач, а база не перегружена сложными отчетами по срокам родителей.
  2. Запись в реквизиты родителя (Вариант 2): стандартное решение, если по датам групп задач необходимо строить отчетность и использовать стандартные отборы платформы 1С.
  3. Регистр сведений (Вариант 3): рекомендуется при высокой интенсивности параллельной работы пользователей, когда прямая перезапись родительских элементов вызывает частые тайм-ауты блокировок.
← На главную