Как в запросе 1С прибавить к дате заданное количество рабочих дней

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

В практике разработки на платформе 1С:Предприятие часто возникает задача: необходимо в рамках одного запроса вычислить дату, отстоящую от исходной на определенное количество рабочих дней. Если с календарными днями всё просто (используется функция ДОБАВИТЬКДАТЕ), то рабочие дни требуют учета праздников и выходных, информация о которых хранится в производственном календаре.

В классическом SQL (например, T-SQL) такую задачу можно решить через подзапросы в списке полей. В языке запросов 1С возможности ограничены, поэтому нам приходится прибегать к более сложным конструкциям. Разберем несколько эффективных способов решения этой задачи: от классических соединений до оптимизированных методов с предварительной индексацией.

Метод 1: Использование порядковой нумерации рабочих дней внутри запроса

Этот способ считается классическим и не требует изменения структуры конфигурации. Идея заключается в том, чтобы «пронумеровать» все рабочие дни в календаре, а затем найти дату, индекс которой равен индекс текущей даты + заданное смещение.

Рассмотрим реализацию этого алгоритма по шагам:

  1. Подготовка исходных данных: Помещаем нашу таблицу с исходными датами и количеством дней смещения во временную таблицу.
  2. Индексация календаря: Создаем временную таблицу РабДни, в которой для каждой даты из регистра РегламентированныйПроизводственныйКалендарь рассчитываем ее порядковый номер среди всех рабочих дней.
  3. Соединение: Сопоставляем исходную дату с ее порядковым номером, прибавляем смещение и находим дату, соответствующую новому номеру.

Проанализируем пример кода, реализующий эту логику:


// 1. Помещаем исходные данные во временную таблицу
ВЫБРАТЬ
    ИсходнаяТаблица.Дата КАК ИсходнаяДата,
    ИсходнаяТаблица.КоличествоРабочихДней КАК Смещение
ПОМЕСТИТЬ втИсходныеДанные
ИЗ
    &Данные КАК ИсходнаяТаблица;

////////////////////////////////////////////////////////////////////////////////
// 2. Индексируем рабочие дни календаря
ВЫБРАТЬ
    Календарь.ДатаКалендаря КАК ДатаКалендаря,
    КОЛИЧЕСТВО(Календарь1.ДатаКалендаря) КАК НомерРабочегоДня
ПОМЕСТИТЬ втИндексированныйКалендарь
ИЗ
    РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК Календарь
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК Календарь1
        ПО Календарь.ДатаКалендаря >= Календарь1.ДатаКалендаря
            И (Календарь1.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)
                ИЛИ Календарь1.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный))
ГДЕ
    (Календарь.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)
        ИЛИ Календарь.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный))
СГРУППИРОВАТЬ ПО
    Календарь.ДатаКалендаря;

////////////////////////////////////////////////////////////////////////////////
// 3. Получаем итоговый результат
ВЫБРАТЬ
    вт.ИсходнаяДата,
    вт.Смещение,
    МИНИМУМ(КалендарьРез.ДатаКалендаря) КАК ИтоговаяДата
ИЗ
    втИсходныеДанные КАК вт
        ЛЕВОЕ СОЕДИНЕНИЕ втИндексированныйКалендарь КАК КалендарьНач
        ПО вт.ИсходнаяДата = КалендарьНач.ДатаКалендаря
        ЛЕВОЕ СОЕДИНЕНИЕ втИндексированныйКалендарь КАК КалендарьРез
        ПО (КалендарьНач.НомерРабочегоДня + вт.Смещение = КалендарьРез.НомерРабочегоДня)
СГРУППИРОВАТЬ ПО
    вт.ИсходнаяДата,
    вт.Смещение

Важный нюанс: Обратите внимание на условие по ВидДня. Мы включаем не только «Рабочий», но и «Предпраздничный», так как фактически они являются рабочими. Если этого не сделать, алгоритм пропустит сокращенные рабочие дни.

Метод 2: Оптимизация производительности (предварительная индексация)

Предыдущий метод имеет существенный минус: соединение таблицы календаря «саму на себя» (ПО Календарь.ДатаКалендаря >= Календарь1.ДатаКалендаря) создает квадратичную зависимость. На больших интервалах (например, календарь за 10 лет) запрос будет выполняться очень медленно.

Если в вашей системе такие расчеты выполняются часто, выясним причину медленной работы и предложим архитектурное решение. Лучший способ ускорить процесс — добавить в регистр РегламентированныйПроизводственныйКалендарь дополнительный числовой ресурс ПорядковыйНомерРабочегоДня.

В этом случае запрос упрощается до невероятно эффективной конструкции:


ВЫБРАТЬ
    вт.ИсходнаяДата,
    КалендарьРезультат.ДатаКалендаря КАК ДатаРезультат
ИЗ
    втИсходныеДанные КАК вт
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК КалендарьНачало
        ПО вт.ИсходнаяДата = КалендарьНачало.ДатаКалендаря
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК КалендарьРезультат
        ПО (КалендарьНачало.ПорядковыйНомерРабочегоДня + вт.Смещение = КалендарьРезультат.ПорядковыйНомерРабочегоДня)

Такой подход работает мгновенно даже на миллионах строк, так как использует простые соединения по индексированным числовым полям.

Метод 3: Обработка ситуации, когда исходная дата — выходной

Проанализируем ситуацию: что произойдет, если ИсходнаяДата в нашей таблице является субботой? В методе №1 ЛЕВОЕ СОЕДИНЕНИЕ с индексированным календарем вернет NULL, так как субботы нет в таблице рабочих дней. Это приведет к ошибке в расчетах.

Чтобы избежать этой проблемы, перед началом вычислений необходимо «нормализовать» исходную дату — то есть найти ближайший к ней будущий рабочий день. Разберем, как это реализовать в запросе:


ВЫБРАТЬ
    вт.ИсходнаяДата КАК ДатаФактическая,
    МИНИМУМ(РегКалендарь.ДатаКалендаря) КАК БлижайшаяРабочаяДата
ПОМЕСТИТЬ втНормализованныеДаты
ИЗ
    втИсходныеДанные КАК вт
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК РегКалендарь
        ПО РегКалендарь.ДатаКалендаря >= вт.ИсходнаяДата
        И (РегКалендарь.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий)
            ИЛИ РегКалендарь.ВидДня = ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный))
СГРУППИРОВАТЬ ПО
    вт.ИсходнаяДата

Теперь, используя БлижайшаяРабочаяДата в качестве точки отсчета, вы гарантированно получите корректный результат смещения.

Метод 4: Использование Системы Компоновки Данных (СКД)

Если задача стоит в рамках построения отчета, рассмотрим альтернативный вариант. В СКД можно использовать вычисляемые поля и функции общих модулей. Это позволяет уйти от тяжелых запросов к чистому коду 1С.

Вы можете создать в общем модуле функцию ДобавитьРабочиеДни(Дата, Количество), которая использует встроенные механизмы типовых конфигураций (например, КалендарныеГрафики.ПолучитьДатуЗавершения() в БСП). В поле выражения СКД это будет выглядеть так:

МойОбщийМодуль.ДобавитьРабочиеДни(Дата, Смещение)

Плюсы этого метода:

  1. Код гораздо проще читать и поддерживать.
  2. Учитываются все нюансы производственных календарей (региональные особенности, переносы).
  3. Автоматическая обработка выходных дней в начале периода.

Подведение итогов

Мы рассмотрели несколько подходов к решению задачи. Выбор конкретного метода зависит от ваших условий:

Помните, что при работе с производственным календарем крайне важно всегда проверять наличие данных в регистре на несколько лет вперед, чтобы запрос не вернул пустые значения из-за отсутствия заполненного календаря на следующий год.

← На главную