В практической разработке на платформе «1С:Предприятие 8» часто возникает задача: рассчитать точное количество рабочих дней внутри периода, заданного в документе (например, даты начала и окончания отпуска, командировки или действия договора), исключив при этом государственные праздники и выходные дни. Начинающие разработчики часто заходят в тупик, пытаясь «разложить» этот период программным циклом или выполнить некорректное соединение таблиц в запросе.
В этой статье мы подробно разберем, как решить данную задачу с помощью механизма запросов и производственного календаря, рассмотрим ключевые подводные камни (такие как учет времени в датах и региональные календари), а также покажем альтернативный быстрый способ решения через функции Библиотеки Стандартных Подсистем (БСП).
Представим, что у нас есть документ, содержащий реквизиты ДатаНачала и ДатаОкончания. Нам необходимо в рамках одного запроса (например, для отчета или динамического списка) получить список этих документов и для каждого из них вычислить количество рабочих дней.
Типичная ошибка, которую совершают многие разработчики при попытке связать документ с календарем, выглядит следующим образом:
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
ПО Документ.ДатаНачала = Календарь.Дата
Такое условие связи абсолютно неверно. Оно сопоставит документу только один день — день начала периода. Наша же цель — «развернуть» период документа во временную таблицу по дням, сопоставив его со всеми датами, которые входят в этот интервал.
Выясним причину, почему это работает по-другому. Чтобы получить все дни из интервала, связь между таблицей документов и производственным календарем должна выполняться по условию вхождения (интервальному сравнению).
---
Рассмотрим по шагам, как построить оптимальный запрос для решения нашей задачи.
Для определения рабочих дней мы будем использовать регистр сведений ДанныеПроизводственногоКалендаря (в устаревших конфигурациях на обычных формах он назывался РегламентированныйПроизводственныйКалендарь). Этот регистр содержит записи по каждому календарному дню года с указанием его типа (рабочий, предпраздничный, суббота, воскресенье, праздник).
Основная идея запроса заключается в левом соединении таблицы документов с регистром календаря по условию нахождения даты календаря между датой начала и датой окончания документа. Затем выполняется группировка по документу и подсчет количества связанных записей.
Проанализируем важнейший нюанс: учет времени в датах документов. В документах 1С дата обычно содержит время (например, 01.10.2019 14:30:00), в то время как в производственном календаре даты всегда хранятся строго на начало дня (01.10.2019 00:00:00). Если мы используем стандартное условие МЕЖДУ без предварительной обработки дат, то первый день периода может выпасть из расчета. Например, дата календаря 01.10.2019 00:00:00 не будет находиться между 01.10.2019 14:30:00 и 10.10.2019 00:00:00, так как она меньше нижней границы.
Чтобы избежать этой проблемы, в условии соединения мы обязательно должны привести даты документа к началу дня с помощью функции НАЧАЛОПЕРИОДА.
Посмотрим на готовый шаблон запроса:
ВЫБРАТЬ
СсылкаНаДокумент.Ссылка КАК Документ,
СсылкаНаДокумент.Сотрудник КАК Сотрудник,
СсылкаНаДокумент.ДатаНачала КАК ДатаНачала,
СсылкаНаДокумент.ДатаОкончания КАК ДатаОкончания,
ЕСТЬNULL(КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Календарь.Дата), 0) КАК КоличествоРабочихДней
ИЗ
Документ.ИмяВашегоДокумента КАК СсылкаНаДокумент
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
ПО (Календарь.Дата МЕЖДУ НАЧАЛОПЕРИОДА(СсылкаНаДокумент.ДатаНачала, ДЕНЬ)
И НАЧАЛОПЕРИОДА(СсылкаНаДокумент.ДатаОкончания, ДЕНЬ))
И (Календарь.ПроизводственныйКалендарь = &ПараметрКалендарь)
И (Календарь.ВидДня В (
ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий),
ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный)
))
СГРУППИРОВАТЬ ПО
СсылкаНаДокумент.Ссылка,
СсылкаНаДокумент.Сотрудник,
СсылкаНаДокумент.ДатаНачала,
СсылкаНаДокумент.ДатаОкончания
ЛЕВОЕ СОЕДИНЕНИЕ): Мы используем левое соединение, чтобы документы вывелись в любом случае, даже если в информационной базе не заполнен производственный календарь на указанный период (в таком случае количество рабочих дней вернет 0).МЕЖДУ): Связывает каждую строку документа со всеми строками дат из регистра календаря, которые укладываются в интервал. Благодаря этому происходит виртуальное «разложение» периода на дни.НАЧАЛОПЕРИОДА: Отрезает время у дат документов, гарантируя корректное сравнение с датами календаря.ВидДня): Мы выбираем только те дни, которые являются рабочими (Рабочий) или предпраздничными (Предпраздничный). Выходные и праздничные дни автоматически отсекаются и не попадают в итоговый подсчет.ПроизводственныйКалендарь: В современных конфигурациях 1С (ЗУП 3, ERP, КА, БП 3) в системе может вестись несколько производственных календарей (например, для регионов со своими праздничными днями). Если не ограничить выборку конкретным календарем (например, предопределенным календарем «РФ»), данные по дням задвоятся или затроятся.КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ...)): Мы группируем результат по документам и рассчитываем количество записей дат. Использование ключевого слова РАЗЛИЧНЫЕ внутри функции подсчета защищает от случайного дублирования строк при сложных соединениях.---
Если вам не требуется получать эти данные в рамках сложного запроса для отчета (например, в СКД), а нужно быстро рассчитать количество рабочих дней в коде модуля (при изменении дат на форме, при проведении документа или в рамках некоторого алгоритма заполнения), вы можете воспользоваться встроенными механизмами Библиотеки Стандартных Подсистем (БСП).
В БСП за работу с календарями отвечает общий модуль КалендарныеГрафики. В нем реализована готовая функция РазностьДатПоКалендарю, которая берет на себя всю рутину по работе с регистром сведений.
Рассмотрим пример программного вызова этой функции:
// Определяем параметры расчета
КалендарьСсылка = Справочники.ПроизводственныеКалендари.НайтиПоКоду("РФ");
ДатаНачала = Объект.ДатаНачала;
ДатаОкончания = Объект.ДатаОкончания;
Попытка
// Вызываем функцию БСП для получения разности рабочих дней
КоличествоРабочихДней = КалендарныеГрафики.РазностьДатПоКалендарю(КалендарьСсылка, ДатаНачала, ДатаОкончания);
Исключение
// Обработка ситуации, если календарь на указанный год не заполнен
КоличествоРабочихДней = 0;
ЗаписатьЛогСобытия("Не удалось рассчитать рабочие дни: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
Этот метод чрезвычайно удобен, так как он берет на себя все проверки заполнения календаря, автоматически обрабатывает временную составляющую дат и возвращает точный готовый результат в одну строчку кода.
---
Если вы планируете использовать первый вариант (запрос) для обработки большого массива документов, помните о следующих правилах оптимизации производительности СУБД:
ДанныеПроизводственногоКалендаря (или его аналоге) заведены индексы по полям Дата и ПроизводственныйКалендарь. В типовых конфигурациях эти поля по умолчанию являются индексируемыми, что обеспечивает мгновенный поиск данных.ЕСТЬNULL, чтобы в отчеты или алгоритмы расчетов передавались корректные числовые значения (ноль вместо значения NULL).Мы подробно разобрали, как решить проблему подсчета рабочих дней в периоде документа. Используйте запросы с интервальным соединением по условию МЕЖДУ для отчетов и готовые функции модуля КалендарныеГрафики для программного кода — это сделает ваши разработки быстрыми, надежными и легкими в поддержке.