При разработке отчетов по посещаемости или учету рабочего времени часто возникает ситуация, когда плановые показатели (штатное расписание) и фактические данные (документы посещений) выводятся в разных строках, даже если они относятся к одному и тому же дню. В результате вместо компактной таблицы мы получаем «разъезжающийся» отчет, где в одной строке заполнен только план, а в другой — только факт. Разберем подробно, почему так происходит и как скомпоновать данные в одну строку по каждому дню.
Проблема часто кроется в механизме ПОЛНОГО СОЕДИНЕНИЯ или ЛЕВОГО СОЕДИНЕНИЯ в запросе. Если мы соединяем две таблицы по полям Сотрудник и День, но при этом в одной из таблиц есть дополнительные поля (например, СтатьяЗатрат, НоменклатурнаяГруппа или Подразделение), которых нет в другой или они имеют значение NULL, система компоновки данных (СКД) воспримет их как разные комбинации измерений и создаст отдельные строки.
Еще одна распространенная причина — несовпадение типов или точности дат. Если в одной таблице дата — это 01.07.2023 00:00:00, а в другой — 01.07.2023 14:30:15, то для СКД это разные периоды. Всегда приводите даты к началу дня с помощью функции НАЧАЛОПЕРИОДА(Дата, ДЕНЬ).
Рассмотрим правильную логику построения запроса. Чтобы данные не дублировались, нам нужно подготовить две временные таблицы и соединить их по ключевым полям. Посмотрим на пример упрощенной структуры:
// ВТ_План - получаем данные штатного расписания
ВЫБРАТЬ
ВТ.Сотрудник КАК Сотрудник,
ВТ.День КАК День,
СУММА(ВТ.Часов) КАК ЧасовПлан
ПОМЕСТИТЬ ВТ_План
ИЗ
ИсточникПлана КАК ВТ
СГРУППИРОВАТЬ ПО
ВТ.Сотрудник,
ВТ.День;
////////////////////////////////////////////////////////////////////////////////
// ВТ_Факт - получаем данные из документов посещений
ВЫБРАТЬ
Посещения.Сотрудник КАК Сотрудник,
НАЧАЛОПЕРИОДА(Посещения.Период, ДЕНЬ) КАК День,
СУММА(Посещения.Количество) КАК ЧасовФакт
ПОМЕСТИТЬ ВТ_Факт
ИЗ
Документ.Посещения КАК Посещения
ГДЕ
Посещения.Проведен
СГРУППИРОВАТЬ ПО
Посещения.Сотрудник,
НАЧАЛОПЕРИОДА(Посещения.Период, ДЕНЬ);
Теперь самое важное — финальное объединение. Чтобы не потерять данные, если сотрудника нет в одной из таблиц, используем ПОЛНОЕ СОЕДИНЕНИЕ и функцию ЕСТЬNULL для полей группировки:
ВЫБРАТЬ
ЕСТЬNULL(ВТ_План.Сотрудник, ВТ_Факт.Сотрудник) КАК Сотрудник,
ЕСТЬNULL(ВТ_План.День, ВТ_Факт.День) КАК День,
ВТ_План.ЧасовПлан КАК ЧасовПлан,
ВТ_Факт.ЧасовФакт КАК ЧасовФакт
ИЗ
ВТ_План КАК ВТ_План
ПОЛНОЕ СОЕДИНЕНИЕ ВТ_Факт КАК ВТ_Факт
ПО ВТ_План.Сотрудник = ВТ_Факт.Сотрудник
И ВТ_План.День = ВТ_Факт.День
Часто в конфигурациях 1С данные штатного расписания хранятся в «горизонтальном» виде: один документ содержит колонки Часов1, Часов2... Часов31. Для СКД такую структуру необходимо нормализовать (перевести колонки в строки). Рассмотрим, как это сделать программно в процедуре ПриКомпоновкеРезультата, чтобы затем передать данные в СКД через менеджер временных таблиц.
Проанализируем пример кода, который обходит колонки документа и формирует вертикальную таблицу:
// Создаем таблицу значений для хранения развернутых данных
тзШтатноеРасписание = Новый ТаблицаЗначений;
тзШтатноеРасписание.Колонки.Добавить("Сотрудник");
тзШтатноеРасписание.Колонки.Добавить("День", Новый ОписаниеТипов("Дата"));
тзШтатноеРасписание.Колонки.Добавить("Часов", Новый ОписаниеТипов("Число"));
// Выбираем данные из документа (например, ШтатноеРасписаниеУпр)
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ * ИЗ Документ.ШтатноеРасписаниеУпр.Сотрудники ГДЕ Ссылка.Проведен";
Результат = Запрос.Выполнить().Выбрать();
Пока Результат.Следующий() Цикл
Для ит = 1 По 31 Цикл
ИмяПоля = "Часов" + ит;
ЗначениеЧасов = Результат[ИмяПоля];
Если ЗначениеЧасов > 0 Тогда
НоваяСтрока = тзШтатноеРасписание.Добавить();
НоваяСтрока.Сотрудник = Результат.Сотрудник;
// Важно: корректно вычисляем дату дня
НоваяСтрока.День = НачалоМесяца(Результат.Ссылка.ПериодРегистрации) + (ит - 1) * 86400;
НоваяСтрока.Часов = ЗначениеЧасов;
КонецЕсли;
КонецЦикла;
КонецЦикла;
Внимание: при расчете даты через прибавление секунд (86400 * (ит-1)), как показано в примере выше, будьте осторожны с переходом месяцев (например, 31-е число в феврале). Рекомендуется добавить проверку Месяц(НоваяСтрока.День) = Месяц(НачалоМесяца), чтобы не захватить лишние дни следующего месяца.
После того как данные получены в правильном формате (одна строка — один день), необходимо настроить саму схему компоновки данных:
День установите роль Период и укажите тип периода День. Это подскажет СКД, как правильно упорядочивать и объединять данные по времени.ЧасовПлан и ЧасовФакт должны быть добавлены на закладку «Ресурсы» с функцией СУММА(). Это позволит системе «схлопнуть» записи, если они все же продублируются из-за разных аналитик.Сотруднику, а затем по Дню. Проверьте, что в выбранных полях группировки День не добавлены лишние детальные записи (например, время создания документа), которые могут «разбивать» строку.Если в фактических данных заполнена НоменклатурнаяГруппа (или «Аналитика»), а в плановых — нет, СКД выведет их в две строки: одну с заполненной аналитикой и фактом, вторую — с пустой аналитикой и планом. Выясним, как этого избежать:
Проанализируем ситуацию: если аналитика важна только для факта, попробуйте убрать её из группировок верхнего уровня. Если же нужно видеть план и факт в одной строке вместе с аналитикой, то в запросе для таблицы плана нужно либо подставить «пустое» значение аналитики по умолчанию, либо использовать функцию ЕСТЬNULL(Аналитика, "Общая"), чтобы значения совпали.
Если вы подготавливаете данные в коде (как в разделе про 31 колонку), передавайте их в СКД через МенеджерВременныхТаблиц. Это позволит основному запросу СКД видеть вашу подготовленную таблицу тзШтатноеРасписание так, будто это реальная таблица базы данных.
МВТ = Новый МенеджерВременныхТаблиц;
ЗапросПодготовки = Новый Запрос("ВЫБРАТЬ * ПОМЕСТЬИТЬ ВТШтатное ИЗ &ТЗ КАК ТЗ");
ЗапросПодготовки.УстановитьПараметр("ТЗ", тзШтатноеРасписание);
ЗапросПодготовки.МенеджерВременныхТаблиц = МВТ;
ЗапросПодготовки.Выполнить();
// При инициализации процессора компоновки передаем МВТ
ПроцессорКомпоновкиДанных.Инициализировать(МакетКомпоновки, ВнешниеНаборыДанных, ДанныеРасшифровки, Истина, , МВТ);
Такой подход гарантирует, что вы сможете выполнить сложные соединения (JOIN) между внешними данными и данными из таблиц документов прямо внутри SQL-движка, что обеспечит максимальную производительность и корректность группировки по дням.