Как получить дату, отстоящую от заданной на определенное количество рабочих дней в 1С?

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

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

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

Метод 1. Программный расчет с использованием механизмов БСП

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

Для решения нашей задачи идеально подходит функция КалендарныеГрафики.ДатаПоКалендарю(). Давайте разберем её использование на конкретном примере.


// Пример получения даты через 10 рабочих дней от текущей даты
ИсходнаяДата = ТекущаяДатаСеанса();
КоличествоДней = 10;

// В качестве календаря передаем ссылку на нужный производственный календарь
СсылкаНаКалендарь = Справочники.ПроизводственныеКалендари.НайтиПоНаименованию("РФ");

Попытка
    НоваяДата = КалендарныеГрафики.ДатаПоКалендарю(СсылкаНаКалендарь, ИсходнаяДата, КоличествоДней, Истина);
    Сообщить("Искомая дата: " + НоваяДата);
Исключение
    Сообщить("Не удалось рассчитать дату: возможно, не заполнен производственный календарь!");
КонецПопытки;

Давайте проанализируем параметры функции ДатаПоКалендарю:

  1. Календарь — ссылка на элемент справочника производственных календарей или графиков работы.
  2. ДатаОт — исходная дата, от которой мы начинаем отсчет.
  3. КоличествоДней — число рабочих дней, которые необходимо прибавить.
  4. ВызыватьИсключение — булево значение. Если установлено в Истина, то при незаполненном календаре на требуемый период система вызовет стандартное исключение, которое мы можем перехватить в блоке Попытка...Исключение.

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

Метод 2. Расчет даты внутри запроса (для отчетов и динамических списков)

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

Вариант А. Простой запрос для одной даты (метод ПЕРВЫЕ N)

Если нам нужно рассчитать дату относительно одного конкретного значения (например, параметра запроса), мы можем использовать следующий простой и производительный алгоритм. Мы выбираем первые $N$ рабочих дней, идущих после исходной даты, сортируем их по возрастанию и берем максимальное значение.


ВЫБРАТЬ ПЕРВЫЕ 10
    Календарь.Дата КАК ДатаКалендаря
ПОМЕСТИТЬ ВТ_РабочиеДни
ИЗ
    РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
ГДЕ
    Календарь.Дата > &ИсходнаяДата
    И Календарь.ВидДня В (ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий), ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный))
    И Календарь.ПроизводственныйКалендарь = &ПроизводственныйКалендарь
УПОРЯДОЧИТЬ ПО
    ДатаКалендаря АСЦ
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    МАКСИМУМ(ВТ_РабочиеДни.ДатаКалендаря) КАК ИскомаяДата
ИЗ
    ВТ_РабочиеДни КАК ВТ_РабочиеДни

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

Вариант Б. Сложный запрос с левым соединением (метод нарастающего итога)

Что делать, если нам нужно рассчитать дату окончания срока одновременно для множества строк таблицы (например, для списка документов отгрузки с разными датами)? Метод ПЕРВЫЕ N здесь не сработает, так как он ограничивает всю выборку целиком. В этом случае мы применяем математический подход с левым соединением таблицы самой с собой для вычисления нарастающего итога (порядкового номера каждого рабочего дня).

Давайте разберем, как устроен этот алгоритм шаг за шагом:

  1. Выбираем все рабочие и предпраздничные дни за интересующий нас период и помещаем их во временную таблицу. Каждому такому дню сопоставляем число рабочих дней, равное 1.
  2. Соединяем эту таблицу саму с собой по условию: ПО (ТЗ1.ДатаКалендаря <= ТЗ.ДатаКалендаря).
  3. Сгруппируем результат по дате из основной таблицы и просуммируем числа рабочих дней из присоединенной таблицы.

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

Исходная таблица рабочие дней (ТЗ):
01.03.2019 (1 день)
02.03.2019 (1 день)
03.03.2019 (1 день)

После левого соединения по условию "меньше или равно" мы получаем промежуточную структуру:

Для даты 01.03.2019 присоединится только:
- 01.03.2019 (1 день). Сумма = 1.

Для даты 02.03.2019 присоединятся:
- 01.03.2019 (1 день)
- 02.03.2019 (1 день). Сумма = 2.

Для даты 03.03.2019 присоединятся:
- 01.03.2019 (1 день)
- 02.03.2019 (1 день)
- 03.03.2019 (1 день). Сумма = 3.

Таким образом, группировка и сумма дают нам непрерывную сквозную индексацию рабочих дней. Имея такой проиндексированный календарь, мы можем легко связать его с таблицей документов по формуле: ИндексДатыДокумента + КоличествоДнейОтсрочки = ИндексЦелевойДаты. Ниже представлен пример реализации такого запроса в 1С:


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

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

Используя временную таблицу ВТ_ИндексированныйКалендарь, мы можем быстро и эффективно рассчитывать смещения рабочих дней для неограниченного числа строк в рамках одного запроса.

Метод 3. Программный обход в цикле (без использования календарей)

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

Давайте напишем функцию, которая прибавляет к дате заданное число рабочих дней, пропуская субботы и воскресенья:


Функция ПрибавитьРабочиеДниБезКалендаря(ИсходнаяДата, КоличествоДней) Экспорт
    ТекущаяДата = ИсходнаяДата;
    ПрибавленоДней = 0;
    
    Пока ПрибавленоДней < КоличествоДней Цикл
        ТекущаяДата = ТекущаяДата + 86400; // Прибавляем 1 сутки (в секундах)
        ДеньНедели = ДеньНедели(ТекущаяДата);
        
        // Если день не суббота (6) и не воскресенье (7), то засчитываем его
        Если ДеньНедели <> 6 И ДеньНедели <> 7 Тогда
            ПрибавленоДней = ПрибавленоДней + 1;
        КонецЕсли;
    КонецЦикла;
    
    Возврат ТекущаяДата;
КонецФункции

Этот способ работает очень быстро и надежно, так как не зависит от наполненности базы данных. Однако помните, что в периоды длинных праздников (например, новогодних каникул или майских праздников) этот метод даст существенную погрешность.

Важные нюансы и подводные камни

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

  1. Переход через границу года. Если вы производите расчет в конце декабря, итоговая дата неизбежно попадет на январь следующего года. Если в вашей базе данных еще не заполнен производственный календарь на следующий год, типовые функции БСП вернут ошибку, а запросы к регистру сведений выдадут пустой результат. Всегда проверяйте заполненность календаря на целевой период перед расчетами.
  2. Предпраздничные дни. Никогда не забывайте включать в запросы условие по предпраздничным дням (ВидДня = Предпраздничный). Они являются рабочими днями.
  3. Региональные особенности. В некоторых регионах действуют свои праздничные дни (например, Курбан-байрам в Татарстане или Башкортостане). Если ваша компания ведет деятельность в нескольких регионах, используйте региональные производственные календари вместо единого календаря "РФ".

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

← На главную