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