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

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

Задача вывода списка сотрудников-именинников при старте программы или по расписанию — одна из классических учебных и практических задач в разработке на платформе 1С:Предприятие. На первый взгляд алгоритм кажется простым: получить текущую дату, прибавить несколько дней и выбрать сотрудников из справочника. Однако на практике разработчики часто сталкиваются с логическими ошибками, из-за которых запрос возвращает пустой результат или работает некорректно при смене месяца и года.

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

Рассмотрим исходный фрагмент кода, который вызвал затруднения:


Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
               | Сотрудники.Фамилия,
               | Сотрудники.Имя,
               | Сотрудники.Отчество,
               | Сотрудники.ДатаРождения
               |ИЗ
               | Справочник.Сотрудники КАК Сотрудники
               |ГДЕ
               | Сотрудники.ДатаРождения МЕЖДУ &ДатаНачала И &ДатаКонец";

Запрос.УстановитьПараметр("ДатаКонец", НачалоДня(ТекущаяДата));
Запрос.УстановитьПараметр("ДатаНачала", КонецДня(ТекущаяДата+3*86400));
РезультатЗапроса = Запрос.Выполнить;
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать;
Пока ВыборкаДетальныеЗаписи.Следующий Цикл 
КонецЦикла;
Сообщить("Скоро День Рождения " + ВыборкаДетальныеЗаписи.Фамилия + ВыборкаДетальныеЗаписи.Имя+ВыборкаДетальныеЗаписи.ДатаРождения);

Анализ допущенных ошибок

Разберем по шагам, почему данный код не работает и возвращает пустой результат:

  1. Прямое сравнение полной даты вместе с годом: Конструкция Сотрудники.ДатаРождения МЕЖДУ &ДатаНачала И &ДатаКонец сравнивает дату рождения целиком, включая год. Если сотрудник родился в 1985 году, а параметры передают даты текущего года (например, 2024), условие никогда не выполнится. Исключением будут только младенцы, родившиеся буквально на днях.
  2. Перепутаны границы периода в операторе МЕЖДУ: В языке запросов 1С конструкция Поле МЕЖДУ Значение1 И Значение2 требует, чтобы Значение1 было строго меньше или равно Значение2. В примере параметр &ДатаНачала был установлен на 3 дня вперед, а &ДатаКонец — на сегодня. При таком порядке оператор всегда возвращает Ложь.
  3. Вывод сообщения вне тела цикла: Вызов Сообщить(...) расположен после ключевого слова КонецЦикла. Цикл Пока Выборка.Следующий() отрабатывает вхолостую, курсор доходит до конца выборки, и обращение к полям после цикла приводит либо к пустой строке, либо к ошибке выполнения.
  4. Пропущенные скобки при вызове функций: В платформе 1С обращение к системным функциям требует обязательных скобок (ТекущаяДата(), Запрос.Выполнить(), Результат.Выбрать()).
  5. Ошибочная логика раздельного сравнения дня и месяца: Попытка исправить запрос условием ДЕНЬ(Дата) МЕЖДУ &ДеньНачала И &ДеньКонец И МЕСЯЦ(Дата) МЕЖДУ &МесяцНачала И &МесяцКонец полностью ломается при переходе между месяцами. Например, если сегодня 30 января, а мы смотрим на 3 дня вперед (до 2 февраля), день начала (30) больше дня окончания (2), и условие 30 МЕЖДУ 30 И 2 вернет Ложь. Аналогично ситуация сломается на стыке декабря и января при смене года.
  6. Прибавление чисел к дате: В 1С дата хранится с точностью до секунды. Чтобы прибавить 3 дня, необходимо прибавить количество секунд: ТекущаяДата() + 3 * 86400. Выражение ТекущаяДата() + 3 прибавит всего 3 секунды.

Теперь рассмотрим, как правильно решить данную задачу несколькими способами.

Способ 1. Передача списка целевых дат через временную таблицу (Рекомендуемый)

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

Посмотрим на пример полной реализации:


// 1. Формируем таблицу значений с целевыми датами
ТаблицаДат = Новый ТаблицаЗначений;
ТаблицаДат.Колонки.Добавить("День", Новый ОписаниеТипов("Число"));
ТаблицаДат.Колонки.Добавить("Месяц", Новый ОписаниеТипов("Число"));

ТекДата = НачалоДня(ТекущаяДата());
ИнтервалДней = 3;

Для Счетчик = 0 По ИнтервалДней Цикл
    АнализируемаяДата = ТекДата + Счетчик * 86400;
    
    НоваяСтрока = ТаблицаДат.Добавить();
    НоваяСтрока.День = День(АнализируемаяДата);
    НоваяСтрока.Месяц = Месяц(АнализируемаяДата);
    
    // Обработка 29 февраля в невисокосный год
    Если НоваяСтрока.День = 28 И НоваяСтрока.Месяц = 2 И День(КонецМесяца(АнализируемаяДата)) = 28 Тогда
        СтрокаВисокосная = ТаблицаДат.Добавить();
        СтрокаВисокосная.День = 29;
        СтрокаВисокосная.Месяц = 2;
    КонецЕсли;
КонецЦикла;

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

Запрос.УстановитьПараметр("ТаблицаДат", ТаблицаДат);

РезультатЗапроса = Запрос.Выполнить();

Если НЕ РезультатЗапроса.Пустой() Тогда
    Выборка = РезультатЗапроса.Выбрать();
    Пока Выборка.Следующий() Цикл
        ТекстСообщения = СтрШаблон("Скоро день рождения: %1 %2 %3 (Дата рождения: %4)",
            Выборка.Фамилия,
            Выборка.Имя,
            Выборка.Отчество,
            Формат(Выборка.ДатаРождения, "ДФ=dd.MM.yyyy"));
        Сообщить(ТекстСообщения);
    КонецЦикла;
Иначе
    Сообщить("В ближайшие 3 дня именинников нет.");
КонецЕсли;

Преимущества данного подхода:

  1. Корректно работает при переходе через границы месяцев (например, с 30 января по 2 февраля).
  2. Корректно отрабатывает переход через границу года (с 30 декабря по 2 января).
  3. Позволяет гибко учесть людей, родившихся 29 февраля, поздравляя их 28 февраля в невисокосные годы.

Способ 2. Виртуальное приведение даты рождения к текущему году в запросе

Если требуется решить задачу исключительно средствами языка запросов без подготовки внешних таблиц, можно использовать функции ДОБАВИТЬКДАТЕ и РАЗНОСТЬДАТ. Суть метода заключается в том, чтобы вычислить разницу в годах между датой рождения и текущим годом, а затем сместить дату рождения на это количество лет вперед.

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


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|   Сотрудники.Фамилия КАК Фамилия,
|   Сотрудники.Имя КАК Имя,
|   Сотрудники.Отчество КАК Отчество,
|   Сотрудники.ДатаРождения КАК ДатаРождения
|ИЗ
|   Справочник.Сотрудники КАК Сотрудники
|ГДЕ
|   НЕ Сотрудники.ПометкаУдаления
|   И (ДОБАВИТЬКДАТЕ(Сотрудники.ДатаРождения, ГОД, РАЗНОСТЬДАТ(Сотрудники.ДатаРождения, &ТекущаяДата, ГОД)) 
|           МЕЖДУ &ДатаНачала И &ДатаКонец
|       ИЛИ ДОБАВИТЬКДАТЕ(Сотрудники.ДатаРождения, ГОД, РАЗНОСТЬДАТ(Сотрудники.ДатаРождения, &ТекущаяДата, ГОД) + 1) 
|           МЕЖДУ &ДатаНачала И &ДатаКонец)";

ТекущийМомент = НачалоДня(ТекущаяДата());
Запрос.УстановитьПараметр("ТекущаяДата", ТекущийМомент);
Запрос.УстановитьПараметр("ДатаНачала", ТекущийМомент);
Запрос.УстановитьПараметр("ДатаКонец", КонецДня(ТекущийМомент + 3 * 86400));

РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();

Пока Выборка.Следующий() Цикл
    Сообщить("Именинник: " + Выборка.Фамилия + " " + Выборка.Имя + " (" + Формат(Выборка.ДатаРождения, "ДФ=dd.MM.yyyy") + ")");
КонецЦикла;

Обратите внимание: второе условие с прибавлением + 1 года необходимо для корректной обработки дней рождения в начале января, когда проверка выполняется в последних числах декабря.

Способ 3. Числовое представление дня и месяца (Формула Месяц * 100 + День)

Еще один популярный алгоритмический прием — перевод даты в число вида MMDD (номер месяца умножается на 100 и к нему прибавляется день). Например, 5 мая превращается в 505, а 25 декабря — в 1225.

Проанализируем логику условий в запросе:


ДатаС = НачалоДня(ТекущаяДата());
ДатаПо = НачалоДня(ТекущаяДата() + 3 * 86400);

КодС = Месяц(ДатаС) * 100 + День(ДатаС);
КодПо = Месяц(ДатаПо) * 100 + День(ДатаПо);

Запрос = Новый Запрос;

Если КодС <= КодПо Тогда
    // Интервал в рамках одного календарного года
    Запрос.Текст = 
    "ВЫБРАТЬ
    |   Сотрудники.Фамилия,
    |   Сотрудники.Имя,
    |   Сотрудники.ДатаРождения
    |ИЗ
    |   Справочник.Сотрудники КАК Сотрудники
    |ГДЕ
    |   НЕ Сотрудники.ПометкаУдаления
    |   И (МЕСЯЦ(Сотрудники.ДатаРождения) * 100 + ДЕНЬ(Сотрудники.ДатаРождения)) МЕЖДУ &КодС И &КодПо";
Иначе
    // Интервал переходит через Новый год (например, с 30 декабря по 2 января)
    Запрос.Текст = 
    "ВЫБРАТЬ
    |   Сотрудники.Фамилия,
    |   Сотрудники.Имя,
    |   Сотрудники.ДатаРождения
    |ИЗ
    |   Справочник.Сотрудники КАК Сотрудники
    |ГДЕ
    |   НЕ Сотрудники.ПометкаУдаления
    |   И ((МЕСЯЦ(Сотрудники.ДатаРождения) * 100 + ДЕНЬ(Сотрудники.ДатаРождения)) >= &КодС
    |       ИЛИ (МЕСЯЦ(Сотрудники.ДатаРождения) * 100 + ДЕНЬ(Сотрудники.ДатаРождения)) <= &КодПо)";
КонецЕсли;

Запрос.УстановитьПараметр("КодС", КодС);
Запрос.УстановитьПараметр("КодПо", КодПо);

РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();

Пока Выборка.Следующий() Цикл
    Сообщить("Скоро день рождения: " + Выборка.Фамилия + " " + Выборка.Имя);
КонецЦикла;

Важные практические рекомендации

  1. Проверка на активность сотрудников: В реальных базах (1С:ЗУП, 1С:ERP, 1С:КА) сотрудники могут быть уволены. Чтобы не выводить напоминания об уволенных работниках, обязательно добавляйте отбор по кадровому состоянию или проверяйте флаг ПометкаУдаления.
  2. Форматирование вывода: Для вывода читаемых дат используйте функцию Формат(Выборка.ДатаРождения, "ДФ=dd.MM.yyyy") или СтрШаблон() для объединения ФИО со строками.
  3. Точки вызова: При старте системы оповещение обычно размещают в модуле управляемого приложения в процедуре ПриНачалеРаботыСистемы() или на начальной странице (рабочем столе) пользователя.
← На главную