Как правильно перенести дату с выходного дня на ближайший рабочий в 1С?

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

В разработке на платформе 1С часто возникает задача автоматического переноса даты. Например, если срок оплаты, сдачи отчета или отгрузки товаров выпадает на субботу, воскресенье или праздничный день, система должна автоматически сдвинуть эту дату вперед — на первый последующий рабочий день. Начинающие разработчики часто пытаются решить эту задачу «в лоб» с помощью циклов или рекурсивных функций. Однако такие подходы могут таить в себе как логические ошибки, так и серьезные проблемы с производительностью системы.

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

Разбор ошибки в рекурсивной функции

Рассмотрим ситуацию, с которой столкнулся автор темы. Он написал рекурсивную функцию, которая проверяет вид дня в регистре сведений РегламентированныйПроизводственныйКалендарь. Если день оказывается выходным, функция вызывает сама себя, передавая в качестве параметра дату, увеличенную на одни сутки (Дата + 24 * 60 * 60).

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

Выясним причину такого поведения. Проанализируем исходный фрагмент кода:


Если ВидДня <> Перечисления.ВидыДнейПроизводственногоКалендаря.Рабочий Тогда
    Сообщить("Дата " + Дата + " Выпадает на выходной/праздничный день! Ищем следующий рабочий");
    ВычислитьРабочийДень(Дата + (24*60*60)) // Ошибка здесь!
Иначе 
    Возврат Дата;
КонецЕсли;

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

Правильное исправление рекурсивного вызова требует обязательного использования ключевого слова Возврат перед вызовом функции на более глубоком уровне:


Функция ВычислитьРабочийДень(Дата) Экспорт
    // ... (Код формирования запроса) ...
    РезультатЗапроса = Запрос.Выполнить().Выгрузить();
    Если РезультатЗапроса.Количество() = 0 Тогда
        Возврат Дата; // Предохранитель на случай незаполненного календаря
    КонецЕсли;
    
    ВидДня = РезультатЗапроса[0].ВидДня;
    
    Если ВидДня <> Перечисления.ВидыДнейПроизводственногоКалендаря.Рабочий Тогда
        // Обязательно возвращаем результат вложенного вызова!
        Возврат ВычислитьРабочийДень(Дата + (24*60*60)); 
    Иначе 
        Возврат Дата;
    КонецЕсли;
КонецФункции

Хотя мы исправили логику рекурсии, радоваться рано. Такой подход является крайне неоптимальным. Мы имеем дело с типичным антипаттерном — запросом в цикле (или в рекурсии). Если пользователь укажет дату в начале новогодних праздников, система сделает до 8–10 последовательных запросов к базе данных, что существенно замедлит работу системы и создаст избыточную нагрузку на СУБД.

Оптимальное решение: Один запрос к базе данных

Как совершенно справедливо заметили участники обсуждения в сообщениях 3 и 5, эту задачу нужно решать в один запрос. Вместо того чтобы перебирать дни по одному и каждый раз обращаться к базе данных, мы можем сразу запросить из регистра сведений ближайший день с видом «Рабочий», который больше или равен нашей исходной дате.

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

Напишем быструю и эффективную функцию без использования рекурсии:


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

Разберем по шагам, как работает этот запрос:

  1. Конструкция ВЫБРАТЬ ПЕРВЫЕ 1 позволяет нам мгновенно получить только одну, самую первую подходящую запись, не нагружая память СУБД.
  2. Условие Календарь.ДатаКалендаря >= &ИсходнаяДата гарантирует, что если переданный день уже является рабочим, запрос вернет его же. Если день выходной — запрос начнет искать даты в будущем.
  3. Фильтрация по видам дней отсекает выходные и праздники, оставляя только рабочие и предпраздничные даты.
  4. Инструкция УПОРЯДОЧИТЬ ПО Календарь.ДатаКалендаря ВОЗР гарантирует, что записи будут отсортированы хронологически, и благодаря ПЕРВЫЕ 1 мы получим именно самую близкую к исходной дате рабочую смену.

Использование механизмов БСП (Библиотеки Стандартных Подсистем)

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

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

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


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

// Заполняем массив проверяемых дат
МассивДат = Новый Массив;
МассивДат.Добавить(ИсходнаяДата);

// Вызываем метод БСП для поиска ближайших рабочих дней.
// Третий параметр "Ложь" указывает на то, что мы ищем будущие (последующие) дни.
// Если передать "Истина" - метод найдет предшествующие рабочие дни.
РезультатТаблица = КалендарныеГрафики.ДатыБлижайшихРабочихДней(КалендарьРФ, МассивДат, Ложь);

Если РезультатТаблица.Количество() > 0 Тогда
    БлижайшийРабочийДень = РезультатТаблица[0].Дата;
КонецЕсли;

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

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

← На главную