При автоматизации бизнес-процессов в 1С, особенно в системах учета персонала, таких как 1С:Зарплата и Управление Персоналом (ЗУП), часто возникает необходимость программно определять рабочие даты. Например, при назначении аттестаций или других событий, которые должны приходиться строго на рабочий день, мы сталкиваемся с ситуацией, когда рассчитанная дата может выпасть на выходной или праздничный день. В таких случаях нам необходимо найти ближайшую следующую рабочую дату. Рассмотрим, как это можно реализовать, используя возможности платформы 1С и данные производственного календаря.
Основой для определения рабочих и нерабочих дней в 1С является Производственный календарь. В конфигурациях 1С, таких как ЗУП, эта информация хранится в регистрах сведений, в частности в РегистрСведений.РегламентированныйПроизводственныйКалендарь или РегистрСведений.ДанныеПроизводственногоКалендаря. Эти регистры содержат данные о типе каждого дня (рабочий, выходной, предпраздничный) в соответствии с государственными праздниками и переносами выходных дней.
Мы можем разработать универсальную функцию, которая будет возвращать дату, отстоящую на заданное количество рабочих дней от начальной, учитывая регламентированный производственный календарь. Эта функция позволит нам найти как ближайший рабочий день в будущем, так и в прошлом.
Рассмотрим параметры, которые нам потребуются:
ДатаНач – начальная дата, от которой мы начинаем отсчет.ЧислоДней – количество рабочих дней, на которое искомая дата должна отстоять от начальной. Положительное значение будет искать дни вперед, отрицательное – назад.РабочийДень и ПредпраздничныйДень – ссылки на перечисления ВидыДнейПроизводственногоКалендаря.Рабочий и ВидыДнейПроизводственногоКалендаря.Предпраздничный соответственно. Важно учитывать предпраздничные дни как рабочие, поскольку они часто являются сокращенными, но все же рабочими днями.Давайте посмотрим на пример такой функции:
// Функция возвращает дату отстоящую на заданное количество рабочих дней от начальной в соответствии с
// регламентированным производственным календарем
//
// Параметры:
// ДатаНач - начальная дата
// ЧислоДней - количество рабочих дней, на которое искомая дата должна отстоять от начальной
//
Функция ПолучитьРабочийДеньНедели(ДатаНач, ЧислоДней) Экспорт
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ДатаНач", ДатаНач);
Запрос.УстановитьПараметр("ЧислоДней", ЧислоДней);
Запрос.УстановитьПараметр("РабочийДень", Перечисления.ВидыДнейПроизводственногоКалендаря.Рабочий);
Запрос.УстановитьПараметр("ПредпраздничныйДень", Перечисления.ВидыДнейПроизводственногоКалендаря.Предпраздничный);
Если ЧислоДней > 0 Тогда
Запрос.Текст = "
|ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ " + ЧислоДней + "
| РегламентированныйПроизводственныйКалендарь.ДатаКалендаря
|ИЗ
| РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК РегламентированныйПроизводственныйКалендарь
|ГДЕ
| РегламентированныйПроизводственныйКалендарь.ДатаКалендаря > &ДатаНач
| И (РегламентированныйПроизводственныйКалендарь.ВидДня = &РабочийДень
| ИЛИ РегламентированныйПроизводственныйКалендарь.ВидДня = &ПредпраздничныйДень)
|УПОРЯДОЧИТЬ ПО
| РегламентированныйПроизводственныйКалендарь.ДатаКалендаря
|";
Иначе
ЧислоДней = -ЧислоДней;
Запрос.Текст = "
|ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ " + ЧислоДней + "
| РегламентированныйПроизводственныйКалендарь.ДатаКалендаря
|ИЗ
| РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК РегламентированныйПроизводственныйКалендарь
|ГДЕ
| РегламентированныйПроизводственныйКалендарь.ДатаКалендаря < &ДатаНач
| И (РегламентированныйПроизводственныйКалендарь.ВидДня = &РабочийДень
| ИЛИ РегламентированныйПроизводственныйКалендарь.ВидДня = &ПредпраздничныйДень)
|УПОРЯДОЧИТЬ ПО
| РегламентированныйПроизводственныйКалендарь.ДатаКалендаря УБЫВ
|";
КонецЕсли;
Выборка = Запрос.Выполнить().Выбрать();
ТекДата = Неопределено;
Если ЧислоДней > 0 Тогда
Пока Выборка.Следующий() Цикл
ТекДата = Выборка.ДатаКалендаря;
КонецЦикла;
Возврат ТекДата;
КонецЕсли;
Возврат Неопределено;
КонецФункции
Данная функция выполняет запрос к регистру сведений, выбирая рабочие и предпраздничные дни. Если ЧислоДней положительно, запрос ищет дни после ДатаНач и упорядочивает их по возрастанию. Если ЧислоДней отрицательно, запрос ищет дни до ДатаНач и упорядочивает их по убыванию. В обоих случаях, цикл Пока Выборка.Следующий() помогает нам получить последнюю дату из выборки, которая и будет искомым рабочим днем, отстоящим на ЧислоДней.
Если нам нужно просто найти первую рабочую дату строго после заданной даты (например, после выходного или праздничного дня), мы можем использовать более простой прямой запрос. Этот подход особенно удобен, если логика работы с датами ограничивается одним конкретным сценарием.
Разберем шаги выполнения такого запроса:
РегистрСведений.ДанныеПроизводственногоКалендаря, поскольку он содержит необходимую информацию о видах дней.&ПроизводственныйКалендарь) и по дате (Дата > &Дата), чтобы рассматривать только дни после нашей исходной даты.ВидДня не равно 0.Вот пример такого запроса, который был доработан для учета предпраздничных дней:
ВЫБРАТЬ
ДанныеПроизводственногоКалендаря.ПроизводственныйКалендарь КАК ПроизводственныйКалендарь,
ДанныеПроизводственногоКалендаря.Год КАК Год,
ВЫБОР
КОГДА ДанныеПроизводственногоКалендаря.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)
ТОГДА ДанныеПроизводственногоКалендаря.ВидДня
ИНАЧЕ
ВЫБОР
КОГДА ДанныеПроизводственногоКалендаря.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный)
ТОГДА ДанныеПроизводственногоКалендаря.ВидДня
ИНАЧЕ 0
КОНЕЦ
КОНЕЦ КАК ВидДня,
ДанныеПроизводственногоКалендаря.ДатаПереноса КАК ДатаПереноса,
ДанныеПроизводственногоКалендаря.Дата КАК Дата
ПОМЕСТИТЬ ВТ
ИЗ
РегистрСведений.ДанныеПроизводственногоКалендаря КАК ДанныеПроизводственногоКалендаря
ГДЕ
ДанныеПроизводственногоКалендаря.ПроизводственныйКалендарь = &ПроизводственныйКалендарь
И ДанныеПроизводственногоКалендаря.Дата > &Дата
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ 1
ВТ.Дата КАК Дата
ИЗ
ВТ КАК ВТ
ГДЕ
ВТ.ВидДня <> 0
УПОРЯДОЧИТЬ ПО
ВТ.Дата
В этом запросе мы сначала создаем временную таблицу ВТ, которая содержит все дни после заданной даты для указанного производственного календаря, при этом виды дней, не являющиеся рабочими или предпраздничными, помечаются нулем. Затем из этой временной таблицы мы выбираем первую дату, у которой ВидДня не равен нулю, что гарантирует получение ближайшего рабочего или предпраздничного дня.
Иногда возникает обратная задача: необходимо найти последний рабочий день, который предшествует определенной дате. Например, для расчета сроков подачи документов или завершения какого-либо процесса. Мы можем адаптировать предыдущий подход для решения этой задачи.
Основные отличия будут заключаться в следующем:
Дата > &Дата мы будем использовать диапазон дат Дата МЕЖДУ ДОБАВИТЬКДАТЕ(&Дата, День, -15) И ДОБАВИТЬКДАТЕ(&Дата, ДЕНЬ, -1). Это позволяет нам искать дни в прошлом, ограничивая период, например, последними 15 днями перед исходной датой.ПЕРВЫЕ 1, чтобы получить только один, самый близкий день.Приведем пример запроса для получения последнего рабочего дня, предшествующего переданному в параметр:
ВЫБРАТЬ
ВЫБОР
КОГДА ДанныеПроизводственногоКалендаря.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)
ТОГДА ДанныеПроизводственногоКалендаря.ВидДня
ИНАЧЕ
ВЫБОР
КОГДА ДанныеПроизводственногоКалендаря.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный)
ТОГДА ДанныеПроизводственногоКалендаря.ВидДня
ИНАЧЕ 0
КОНЕЦ
КОНЕЦ КАК ВидДня,
ДанныеПроизводственногоКалендаря.Дата КАК Дата
ПОМЕСТИТЬ ВТ
ИЗ
РегистрСведений.ДанныеПроизводственногоКалендаря КАК ДанныеПроизводственногоКалендаря
ГДЕ
ДанныеПроизводственногоКалендаря.ПроизводственныйКалендарь = &ПроизводственныйКалендарь
И ДанныеПроизводственногоКалендаря.Дата МЕЖДУ ДОБАВИТЬКДАТЕ(&Дата, День, -15) И ДОБАВИТЬКДАТЕ(&Дата, ДЕНЬ, -1)
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ 1
ВТ.Дата КАК Дата
ИЗ
ВТ КАК ВТ
ГДЕ
ВТ.ВидДня <> 0
УПОРЯДОЧИТЬ ПО
ВТ.Дата УБЫВ
В этом запросе мы используем функцию ДОБАВИТЬКДАТЕ, чтобы динамически определить диапазон поиска. Например, ДОБАВИТЬКДАТЕ(&Дата, День, -1) даст нам вчерашний день, а ДОБАВИТЬКДАТЕ(&Дата, День, -15) - дату за 15 дней до текущей. Сортировка по ВТ.Дата УБЫВ позволяет нам гарантированно получить ближайшую к исходной дате рабочую дату из прошлого.
Важный аспект, который необходимо учитывать при работе с производственным календарем, это его актуальность. Производственный календарь в 1С заполняется ежегодно. Если мы пытаемся получить рабочий день, который приходится на следующий год, а календарь на этот год еще не заполнен, наши запросы могут вернуть некорректный результат или НЕОПРЕДЕЛЕНО. Это не проблема самой логики запроса, а особенность данных.
Для корректной работы системы нам необходимо убедиться, что производственный календарь заполнен на все необходимые периоды. Рассмотрим, как это можно сделать:
Таким образом, своевременное заполнение производственного календаря является ключевым условием для корректной работы всех механизмов, основанных на определении рабочих дней.
Иногда пользователи могут перепутать запрос на получение первого рабочего дня после конкретной даты с запросом на получение первого рабочего дня месяца. Разберем этот нюанс.
Запрос на получение первого рабочего дня месяца может выглядеть следующим образом:
ВЫБРАТЬ МИНИМУМ(ДанныеПроизводственногоКалендаря.Дата) КАК Дата
ИЗ РегистрСведений.ДанныеПроизводственногоКалендаря КАК ДанныеПроизводственногоКалендаря
ГДЕ ДанныеПроизводственногоКалендаря.Дата МЕЖДУ НАЧАЛОПЕРИОДА(&Дата, МЕСЯЦ) И КОНЕЦПЕРИОДА(&Дата, МЕСЯЦ)
И ДанныеПроизводственногоКалендаря.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)
Если мы передадим в этот запрос &Дата = 24.02.2021, он вернет 01.02.2021 (первый рабочий день февраля). Если передадим &Дата = 15.03.2021, он вернет 01.03.2021 (первый рабочий день марта). Этот запрос всегда возвращает первый рабочий день *начала* того месяца, в который входит переданная дата.
Напротив, в нашей задаче (например, для &Дата = 24.02.2021), нам может потребоваться найти ближайший рабочий день после 24.02.2021. Если 24.02 был выходным, а 25.02 – рабочим, мы хотим получить 25.02. Если 24.02 был рабочим, но нам нужно найти следующий рабочий день после него, и 25.02 тоже рабочий, то мы получим 25.02. Это фундаментальное различие. Предложенные выше решения (как функция, так и прямой запрос) ориентированы именно на поиск рабочего дня после конкретной даты, а не в начале месяца.
Понимание этой разницы критически важно для корректного применения запросов и функций, связанных с производственным календарем, в различных сценариях учета.