В процессе разработки бизнес-логики в среде 1С:Предприятие 8 регулярно возникает задача вычисления сроков, привязанных к рабочим дням. Например, нам может потребоваться рассчитать дату окончания действия договора через 10 рабочих дней, определить крайний срок оплаты накладной или запланировать дату отгрузки товара. Поскольку обычное сложение дат в секундах учитывает выходные и праздничные дни, для решения этой задачи требуются специализированные алгоритмы.
В этой статье мы подробно разберем все возможные способы решения данной задачи: от использования готовых механизмов Библиотеки Стандартных Подсистем (БСП) до построения сложных запросов с нарастающим итогом и написания простых вспомогательных функций.
Если в вашей конфигурации внедрена Библиотека Стандартных Подсистем (БСП), то изобретать велосипед не нужно. В составе БСП есть специализированный общий модуль КалендарныеГрафики, который содержит весь необходимый инструментарий для работы с производственными календарями.
Для решения нашей задачи идеально подходит функция КалендарныеГрафики.ДатаПоКалендарю(). Давайте разберем её использование на конкретном примере.
// Пример получения даты через 10 рабочих дней от текущей даты
ИсходнаяДата = ТекущаяДатаСеанса();
КоличествоДней = 10;
// В качестве календаря передаем ссылку на нужный производственный календарь
СсылкаНаКалендарь = Справочники.ПроизводственныеКалендари.НайтиПоНаименованию("РФ");
Попытка
НоваяДата = КалендарныеГрафики.ДатаПоКалендарю(СсылкаНаКалендарь, ИсходнаяДата, КоличествоДней, Истина);
Сообщить("Искомая дата: " + НоваяДата);
Исключение
Сообщить("Не удалось рассчитать дату: возможно, не заполнен производственный календарь!");
КонецПопытки;
Давайте проанализируем параметры функции ДатаПоКалендарю:
Истина, то при незаполненном календаре на требуемый период система вызовет стандартное исключение, которое мы можем перехватить в блоке Попытка...Исключение.Также в модуле КалендарныеГрафики полезно знать о существовании функции ДатыПоКалендарю(), которая позволяет рассчитать массив дат, если вам требуется сделать сдвиг на разное количество дней сразу для нескольких вычислений.
Иногда нам требуется выполнить этот расчет непосредственно в запросе (например, при разработке отчетов на СКД или для динамического списка документов). Внутри запроса мы не можем вызвать функции общего модуля БСП напрямую, поэтому приходится использовать данные регистра сведений ДанныеПроизводственногоКалендаря.
Если нам нужно рассчитать дату относительно одного конкретного значения (например, параметра запроса), мы можем использовать следующий простой и производительный алгоритм. Мы выбираем первые $N$ рабочих дней, идущих после исходной даты, сортируем их по возрастанию и берем максимальное значение.
ВЫБРАТЬ ПЕРВЫЕ 10
Календарь.Дата КАК ДатаКалендаря
ПОМЕСТИТЬ ВТ_РабочиеДни
ИЗ
РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
ГДЕ
Календарь.Дата > &ИсходнаяДата
И Календарь.ВидДня В (ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий), ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный))
И Календарь.ПроизводственныйКалендарь = &ПроизводственныйКалендарь
УПОРЯДОЧИТЬ ПО
ДатаКалендаря АСЦ
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
МАКСИМУМ(ВТ_РабочиеДни.ДатаКалендаря) КАК ИскомаяДата
ИЗ
ВТ_РабочиеДни КАК ВТ_РабочиеДни
В этом запросе мы обязательно учитываем не только вид дня Рабочий, но и Предпраздничный. Это критически важно, так как предпраздничные дни также являются полноценными рабочими днями, хоть и сокращенными.
Что делать, если нам нужно рассчитать дату окончания срока одновременно для множества строк таблицы (например, для списка документов отгрузки с разными датами)? Метод ПЕРВЫЕ N здесь не сработает, так как он ограничивает всю выборку целиком. В этом случае мы применяем математический подход с левым соединением таблицы самой с собой для вычисления нарастающего итога (порядкового номера каждого рабочего дня).
Давайте разберем, как устроен этот алгоритм шаг за шагом:
ПО (ТЗ1.ДатаКалендаря <= ТЗ.ДатаКалендаря).Давайте посмотрим наглядный пример того, как преобразуются данные при таком соединении. Пусть у нас есть три рабочих дня подряд:
Исходная таблица рабочие дней (ТЗ):
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.ДатаКалендаря <= ТЗ.ДатаКалендаря
СГРУППИРОВАТЬ ПО
ТЗ.ДатаКалендаря
;
Используя временную таблицу ВТ_ИндексированныйКалендарь, мы можем быстро и эффективно рассчитывать смещения рабочих дней для неограниченного числа строк в рамках одного запроса.
Если перед вами стоит задача быстро написать алгоритм в конфигурации, где вообще нет производственных календарей (или они не заполняются пользователями), и при этом допустима небольшая погрешность (не учитываются общегосударственные праздники, а только стандартные выходные — суббота и воскресенье), можно воспользоваться простым циклом.
Давайте напишем функцию, которая прибавляет к дате заданное число рабочих дней, пропуская субботы и воскресенья:
Функция ПрибавитьРабочиеДниБезКалендаря(ИсходнаяДата, КоличествоДней) Экспорт
ТекущаяДата = ИсходнаяДата;
ПрибавленоДней = 0;
Пока ПрибавленоДней < КоличествоДней Цикл
ТекущаяДата = ТекущаяДата + 86400; // Прибавляем 1 сутки (в секундах)
ДеньНедели = ДеньНедели(ТекущаяДата);
// Если день не суббота (6) и не воскресенье (7), то засчитываем его
Если ДеньНедели <> 6 И ДеньНедели <> 7 Тогда
ПрибавленоДней = ПрибавленоДней + 1;
КонецЕсли;
КонецЦикла;
Возврат ТекущаяДата;
КонецФункции
Этот способ работает очень быстро и надежно, так как не зависит от наполненности базы данных. Однако помните, что в периоды длинных праздников (например, новогодних каникул или майских праздников) этот метод даст существенную погрешность.
При реализации механизмов расчета рабочих дней на практике программисты часто сталкиваются с неочевидными проблемами. Рекомендуем обратить внимание на следующие моменты:
ВидДня = Предпраздничный). Они являются рабочими днями.Мы детально разобрали особенности вычисления рабочих дней в 1С. Выбирайте наиболее подходящий под ваши задачи метод и применяйте его на практике!