В практике разработки на платформе 1С:Предприятие часто возникает задача: необходимо в рамках одного запроса вычислить дату, отстоящую от исходной на определенное количество рабочих дней. Если с календарными днями всё просто (используется функция ДОБАВИТЬКДАТЕ), то рабочие дни требуют учета праздников и выходных, информация о которых хранится в производственном календаре.
В классическом SQL (например, T-SQL) такую задачу можно решить через подзапросы в списке полей. В языке запросов 1С возможности ограничены, поэтому нам приходится прибегать к более сложным конструкциям. Разберем несколько эффективных способов решения этой задачи: от классических соединений до оптимизированных методов с предварительной индексацией.
Этот способ считается классическим и не требует изменения структуры конфигурации. Идея заключается в том, чтобы «пронумеровать» все рабочие дни в календаре, а затем найти дату, индекс которой равен индекс текущей даты + заданное смещение.
Рассмотрим реализацию этого алгоритма по шагам:
РабДни, в которой для каждой даты из регистра РегламентированныйПроизводственныйКалендарь рассчитываем ее порядковый номер среди всех рабочих дней.Проанализируем пример кода, реализующий эту логику:
// 1. Помещаем исходные данные во временную таблицу
ВЫБРАТЬ
ИсходнаяТаблица.Дата КАК ИсходнаяДата,
ИсходнаяТаблица.КоличествоРабочихДней КАК Смещение
ПОМЕСТИТЬ втИсходныеДанные
ИЗ
&Данные КАК ИсходнаяТаблица;
////////////////////////////////////////////////////////////////////////////////
// 2. Индексируем рабочие дни календаря
ВЫБРАТЬ
Календарь.ДатаКалендаря КАК ДатаКалендаря,
КОЛИЧЕСТВО(Календарь1.ДатаКалендаря) КАК НомерРабочегоДня
ПОМЕСТИТЬ втИндексированныйКалендарь
ИЗ
РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК Календарь
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК Календарь1
ПО Календарь.ДатаКалендаря >= Календарь1.ДатаКалендаря
И (Календарь1.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)
ИЛИ Календарь1.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный))
ГДЕ
(Календарь.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)
ИЛИ Календарь.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный))
СГРУППИРОВАТЬ ПО
Календарь.ДатаКалендаря;
////////////////////////////////////////////////////////////////////////////////
// 3. Получаем итоговый результат
ВЫБРАТЬ
вт.ИсходнаяДата,
вт.Смещение,
МИНИМУМ(КалендарьРез.ДатаКалендаря) КАК ИтоговаяДата
ИЗ
втИсходныеДанные КАК вт
ЛЕВОЕ СОЕДИНЕНИЕ втИндексированныйКалендарь КАК КалендарьНач
ПО вт.ИсходнаяДата = КалендарьНач.ДатаКалендаря
ЛЕВОЕ СОЕДИНЕНИЕ втИндексированныйКалендарь КАК КалендарьРез
ПО (КалендарьНач.НомерРабочегоДня + вт.Смещение = КалендарьРез.НомерРабочегоДня)
СГРУППИРОВАТЬ ПО
вт.ИсходнаяДата,
вт.Смещение
Важный нюанс: Обратите внимание на условие по ВидДня. Мы включаем не только «Рабочий», но и «Предпраздничный», так как фактически они являются рабочими. Если этого не сделать, алгоритм пропустит сокращенные рабочие дни.
Предыдущий метод имеет существенный минус: соединение таблицы календаря «саму на себя» (ПО Календарь.ДатаКалендаря >= Календарь1.ДатаКалендаря) создает квадратичную зависимость. На больших интервалах (например, календарь за 10 лет) запрос будет выполняться очень медленно.
Если в вашей системе такие расчеты выполняются часто, выясним причину медленной работы и предложим архитектурное решение. Лучший способ ускорить процесс — добавить в регистр РегламентированныйПроизводственныйКалендарь дополнительный числовой ресурс ПорядковыйНомерРабочегоДня.
В этом случае запрос упрощается до невероятно эффективной конструкции:
ВЫБРАТЬ
вт.ИсходнаяДата,
КалендарьРезультат.ДатаКалендаря КАК ДатаРезультат
ИЗ
втИсходныеДанные КАК вт
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК КалендарьНачало
ПО вт.ИсходнаяДата = КалендарьНачало.ДатаКалендаря
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК КалендарьРезультат
ПО (КалендарьНачало.ПорядковыйНомерРабочегоДня + вт.Смещение = КалендарьРезультат.ПорядковыйНомерРабочегоДня)
Такой подход работает мгновенно даже на миллионах строк, так как использует простые соединения по индексированным числовым полям.
Проанализируем ситуацию: что произойдет, если ИсходнаяДата в нашей таблице является субботой? В методе №1 ЛЕВОЕ СОЕДИНЕНИЕ с индексированным календарем вернет NULL, так как субботы нет в таблице рабочих дней. Это приведет к ошибке в расчетах.
Чтобы избежать этой проблемы, перед началом вычислений необходимо «нормализовать» исходную дату — то есть найти ближайший к ней будущий рабочий день. Разберем, как это реализовать в запросе:
ВЫБРАТЬ
вт.ИсходнаяДата КАК ДатаФактическая,
МИНИМУМ(РегКалендарь.ДатаКалендаря) КАК БлижайшаяРабочаяДата
ПОМЕСТИТЬ втНормализованныеДаты
ИЗ
втИсходныеДанные КАК вт
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК РегКалендарь
ПО РегКалендарь.ДатаКалендаря >= вт.ИсходнаяДата
И (РегКалендарь.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)
ИЛИ РегКалендарь.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный))
СГРУППИРОВАТЬ ПО
вт.ИсходнаяДата
Теперь, используя БлижайшаяРабочаяДата в качестве точки отсчета, вы гарантированно получите корректный результат смещения.
Если задача стоит в рамках построения отчета, рассмотрим альтернативный вариант. В СКД можно использовать вычисляемые поля и функции общих модулей. Это позволяет уйти от тяжелых запросов к чистому коду 1С.
Вы можете создать в общем модуле функцию ДобавитьРабочиеДни(Дата, Количество), которая использует встроенные механизмы типовых конфигураций (например, КалендарныеГрафики.ПолучитьДатуЗавершения() в БСП). В поле выражения СКД это будет выглядеть так:
МойОбщийМодуль.ДобавитьРабочиеДни(Дата, Смещение)
Плюсы этого метода:
Мы рассмотрели несколько подходов к решению задачи. Выбор конкретного метода зависит от ваших условий:
Помните, что при работе с производственным календарем крайне важно всегда проверять наличие данных в регистре на несколько лет вперед, чтобы запрос не вернул пустые значения из-за отсутствия заполненного календаря на следующий год.