При разработке отчетов по сотрудникам (например, списка именинников месяца) программисты часто сталкиваются с двумя классическими проблемами. Первая — как правильно составить запрос, чтобы отфильтровать сотрудников по месяцу рождения, игнорируя год и день. Вторая — почему при правильной работе запроса в консоли, программный вывод отчета возвращает пользователю пустой экран. В этой статье мы подробно разберем обе проблемы, найдем логические ошибки в коде и напишем красивое, оптимизированное решение.
Рассмотрим первоначальный вариант запроса, который предложил автор темы:
ГДЕ
НАЧАЛОПЕРИОДА(Сотрудники.ФизическоеЛицо.ДатаРождения, МЕСЯЦ) >= &ВыберитеМесяцРождения
Выясним причину, почему эта конструкция работает некорректно. Функция НАЧАЛОПЕРИОДА предназначена для приведения даты к началу указанного периода (секунды, минуты, часа, дня, месяца, квартала, года). На выходе она возвращает полноценный тип Дата, в котором сохраняются и год, и месяц, и день (всегда первое число).
Например, если у сотрудника дата рождения 15.07.1990, то функция НАЧАЛОПЕРИОДА(..., МЕСЯЦ) вернет дату 01.07.1990 00:00:00. Если пользователь хочет выбрать сотрудников, родившихся в июле, и вводит число или дату, сравнение по оператору >= вернет совершенно непредсказуемый результат, так как система будет сравнивать года рождения. Сотрудники, родившиеся в другие года, либо полностью выпадут из выборки, либо попадут туда некорректно.
Для того чтобы решить эту задачу, нам нужно извлечь только числовую составляющую месяца (от 1 до 12) из даты рождения. Для этого в языке запросов 1С существует специальная функция МЕСЯЦ().
Правильный вариант отбора в секции ГДЕ будет выглядеть следующим образом:
ГДЕ
МЕСЯЦ(Сотрудники.ФизическоеЛицо.ДатаРождения) = &ВыберитеМесяцРождения
В этом случае функция МЕСЯЦ() вернет число (для июля это будет 7), которое мы легко сможем сопоставить с переданным параметром.
Как справедливо заметили участники обсуждения, автору требуется не просто отбор, но и правильная сортировка. Если мы выводим список именинников месяца, они должны идти по порядку — от 1-го до 31-го числа. Если мы оставим запрос без сортировки, СУБД вернет записи в хаотичном порядке (например, по порядку создания карточек сотрудников в базе или по алфавиту).
Обычная сортировка по полю ДатаРождения нам не подойдет, так как система в первую очередь отсортирует людей по году рождения (сначала выведутся те, кто старше). Чтобы упорядочить сотрудников исключительно по числу месяца, нам необходимо использовать функцию ДЕНЬ() в секции УПОРЯДОЧИТЬ ПО:
УПОРЯДОЧИТЬ ПО
ДЕНЬ(Сотрудники.ФизическоеЛицо.ДатаРождения) ВОЗР
Теперь именинники будут выстроены строго в хронологическом порядке внутри выбранного месяца.
Перейдем ко второй проблеме. Автор темы жалуется, что в консоли запросов всё работает отлично, но при программном формировании отчета на форме отображается пустое поле табличного документа. Компилятор при этом не выдает никаких ошибок.
Проанализируем функцию получения макета, представленную автором:
&НаСервере
Функция ПолучитьМакетНаСервере()
ТабДок = Новый ТабличныйДокумент;
...
ОбработкаОбъект = РеквизитФормыВ Значение("Отчет");
Макет = ОбработкаОбъект.ПолучитьМакет("Отчет_Выгрузка");
ОбластьШапка = Макет.ПолучитьОбласть("Шапка");
...
ТабДок.Вывести(ОбластьШапка);
...
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
ЗаполнитьЗначенияСвойств(ОбластьСтрока.Параметры, ВыборкаДетальныеЗаписи);
ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;
Возврат Макет; // <--- Внимание на эту строку!
КонецФункции
Разберем детально, что происходит в этой функции:
ТабличныйДокумент с именем ТабДок. Именно в него мы собираемся выводить наши строки.Макет.ТабДок. На данном этапе ТабДок заполнен данными и полностью готов к показу пользователю.Возврат Макет;. Вместо заполненного результатами документа ТабДок функция возвращает исходный, абсолютно пустой макет-шаблон!Для исправления этой логической ошибки нам необходимо возвращать именно сформированный табличный документ:
Возврат ТабДок;
Также важно убедиться, что на клиенте, в процедуре, вызывающей эту серверную функцию, результат правильно присваивается реквизиту формы, связанному с элементом управления "Поле табличного документа". Например:
&НаКлиенте
Процедура СформироватьОтчет(Команда)
// Результат работы функции (наш заполненный ТабДок) записывается в реквизит формы
РезультатТабличныйДокумент = ПолучитьМакетНаСервере();
КонецПроцедуры
Обратим внимание на конструкцию Сотрудники.ФизическоеЛицо.ДатаРождения. В платформе 1С обращение к реквизитам через точку от ссылочного поля (в данном случае от поля ФизическоеЛицо, которое является ссылкой на другой справочник) приводит к созданию неявных левых соединений (LEFT JOIN) на уровне СУБД.
Если база данных большая, такие обращения могут заметно замедлить выполнение запроса. Гораздо более профессиональным подходом считается использование явного соединения таблиц. Давайте перепишем запрос, сделав его производительным и чистым:
ВЫБРАТЬ
ФизЛица.ФИО КАК ФизическоеЛицоФИО,
ФизЛица.ДатаРождения КАК ФизическоеЛицоДатаРождения,
Сотрудники.Ссылка КАК Ссылка
ИЗ
Справочник.Сотрудники КАК Сотрудники
// Используем явное внутреннее соединение, так как у каждого сотрудника должно быть ФизЛицо
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.ФизическиеЛица КАК ФизЛица
ПО Сотрудники.ФизическоеЛицо = ФизЛица.Ссылка
ГДЕ
МЕСЯЦ(ФизЛица.ДатаРождения) = &ВыберитеМесяцРождения
УПОРЯДОЧИТЬ ПО
ДЕНЬ(ФизЛица.ДатаРождения) ВОЗР
Объединим все наши исправления воедино. Также добавим важную деталь: если пользователь вводит месяц в виде строки (например, "07"), перед передачей в параметр запроса мы преобразуем его в число с помощью функции Число(). Это застрахует код от ошибок несоответствия типов данных SQL.
Вот как будет выглядеть эталонный программный код:
&НаСервере
Функция ПолучитьЗаполненныйОтчетНаСервере()
// Инициализируем результирующий табличный документ
ТабДок = Новый ТабличныйДокумент;
ТабДок.ТолькоПросмотр = Истина;
ТабДок.АвтоМасштаб = Истина;
ТабДок.ОриентацияСтраницы = ОриентацияСтраницы.Портрет;
// Определяем дату формирования отчета
ДатаОтчета = ?(ЗначениеЗаполнено(ДатаФормирования), ДатаФормирования, ТекущаяДата());
// Получаем макет из объекта отчета
ОбработкаОбъект = РеквизитФормыВЗначение("Отчет");
Макет = ОбработкаОбъект.ПолучитьМакет("Отчет_Выгрузка");
// Выводим шапку отчета
ОбластьШапка = Макет.ПолучитьОбласть("Шапка");
ОбластьШапка.Параметры.ДатаФормирования = Формат(ДатаОтчета, "ДЛФ=DD");
ТабДок.Вывести(ОбластьШапка);
// Подготавливаем запрос с оптимизированным соединением таблиц и сортировкой
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ФизЛица.ФИО КАК ФизическоеЛицоФИО,
| ФизЛица.ДатаРождения КАК ФизическоеЛицоДатаРождения,
| Сотрудники.Ссылка КАК Ссылка
|ИЗ
| Справочник.Сотрудники КАК Сотрудники
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.ФизическиеЛица КАК ФизЛица
| ПО Сотрудники.ФизическоеЛицо = ФизЛица.Ссылка
|ГДЕ
| МЕСЯЦ(ФизЛица.ДатаРождения) = &ВыберитеМесяцРождения
|УПОРЯДОЧИТЬ ПО
| ДЕНЬ(ФизЛица.ДатаРождения) ВОЗР";
// Преобразуем параметр "МесяцРождения" в число для корректного сравнения в СУБД
ЧисловойМесяц = 0;
Попытка
ЧисловойМесяц = Число(МесяцРождения);
Исключение
ВызватьИсключение "Некорректно указан месяц рождения! Введите число от 1 до 12.";
КонецПопытки;
// Проверяем корректность диапазона месяца
Если ЧисловойМесяц < 1 Или ЧисловойМесяц > 12 Тогда
ВызватьИсключение "Номер месяца должен находиться в диапазоне от 1 до 12.";
КонецЕсли;
Запрос.УстановитьПараметр("ВыберитеМесяцРождения", ЧисловойМесяц);
// Выполнение запроса и обход результатов
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
// Заполняем параметры области макета свойствами из выборки
ЗаполнитьЗначенияСвойств(ОбластьСтрока.Параметры, ВыборкаДетальныеЗаписи);
// Выводим заполненную область в наш табличный документ
ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;
// Возвращаем именно заполненный ТАБЛИЧНЫЙ ДОКУМЕНТ, а не макет!
Возврат ТабДок;
КонецФункции
&НаКлиенте
Процедура СформироватьОтчет(Команда)
// Очищаем реквизит на форме перед формированием (связанный с Полем табличного документа)
РезультатОтчета.Очистить();
// Получаем сформированный табличный документ с сервера
ТабличныйДокументССервера = ПолучитьЗаполненныйОтчетНаСервере();
// Отображаем результат на форме
РезультатОтчета = ТабличныйДокументССервера;
КонецПроцедуры
Хотя классический вывод табличного документа через выборку запроса в коде всё ещё часто встречается в старых конфигурациях, современным стандартом разработки в 1С является использование Системы компоновки данных (СКД).
Для реализации этой же задачи через СКД нам не потребовалось бы писать простыни кода с получением областей и выводом их в цикле. Достаточно выполнить несколько простых шагов:
Сотрудники и ФизическиеЛица.МесяцРождения с выражением МЕСЯЦ(ДатаРождения) и ДеньРождения с выражением ДЕНЬ(ДатаРождения).МесяцРождения, сделав его доступным пользователю на форме отчета.ДеньРождения по возрастанию.Платформа 1С самостоятельно возьмет на себя построение формы настроек, передачу параметров, выполнение оптимального запроса и отрисовку табличного документа на клиенте, избавив разработчика от рутинной работы и риска допустить досадные логические ошибки.