При формировании печатных форм первичных документов, заявлений и служебных записок нередко возникает задача автоматического вычисления даты, отстоящей от текущей или даты документа на несколько дней назад. Типичный сценарий: дата заявления должна быть на 3 дня раньше даты создания документа, но при этом она не может приходиться на нерабочие дни — субботу или воскресенье. Если расчетная дата попадает на выходной, ее необходимо сдвинуть на предшествующую рабочую пятницу.
Разберем, как правильно реализовать такой алгоритм на платформе 1С:Предприятие, какие нюансы работы со временем необходимо учитывать и какие существуют подходы к решению этой задачи.
Прежде чем приступать к расчету дней недели, проанализируем базовые правила платформы при работе со временем:
ТекущаяДата() в коде печатной формы является частой ошибкой. Если открыть и распечатать документ полугодовой давности, функция ТекущаяДата() подставит сегодняшний день, нарушив хронологию. Рекомендуется производить расчет от реквизита самого документа, например Шапка.Дата или СсылкаНаОбъект.Дата. Если бизнес-логика строго требует текущего момента, в клиент-серверной архитектуре безопаснее использовать ТекущаяДатаСеанса(), чтобы избежать разницы часовых поясов клиента и сервера.Дата содержит как календарную дату, так и время (часы, минуты, секунды). При простом вычитании секунд (например, 86400 * 3) сохраняется время суток. Перед анализом дней недели и сравнением дат обязательно приводим значение к началу дня с помощью функции НачалоДня().86400 секунд (24 * 60 * 60), соответственно, трое суток равны 259200 секундам (72 * 60 * 60).Рассмотрим прямолинейный и наглядный алгоритм, предложенный в обсуждении. Мы вычитаем необходимое количество дней, а затем с помощью цикла проверяем, не попали ли мы на субботу (день 6) или воскресенье (день 7). Если попали — продолжаем отнимать по одному дню до тех пор, пока не получим рабочий день.
Посмотрим на пример функции:
Функция ПолучитьДатуЗаявления(ИсходнаяДата) Экспорт
// Приводим дату к началу суток
РасчетнаяДата = НачалоДня(ИсходнаяДата) - 259200; // Отнимаем 3 дня
// Проверяем день недели: 6 - суббота, 7 - воскресенье
Пока ДеньНедели(РасчетнаяДата) = 6 Или ДеньНедели(РасчетнаяДата) = 7 Цикл
РасчетнаяДата = РасчетнаяДата - 86400; // Сдвигаем еще на сутки назад
КонецЦикла;
Возврат РасчетнаяДата;
КонецФункции
Выясним, как работает этот алгоритм на практике:
Цикл для проверки субботы и воскресенья не является строго обязательным. Мы можем заранее определить, на какой день выпадает расчетная дата или день недели исходного документа, и применить точное смещение за одну операцию.
Проанализируем такой вариант функции:
Функция ПолучитьДатуЗаявленияБыстро(ИсходнаяДата) Экспорт
РасчетнаяДата = НачалоДня(ИсходнаяДата) - (3 * 86400);
НомерДня = ДеньНедели(РасчетнаяДата);
Если НомерДня = 6 Тогда
// Суббота: сдвигаем на 1 день назад (пятница)
РасчетнаяДата = РасчетнаяДата - 86400;
ИначеЕсли НомерДня = 7 Тогда
// Воскресенье: сдвигаем на 2 дня назад (пятница)
РасчетнаяДата = РасчетнаяДата - (2 * 86400);
КонецЕсли;
Возврат РасчетнаяДата;
КонецФункции
Этот способ работает мгновенно в памяти, не требует повторных проверок в цикле и легко читается в коде модуля печатной формы.
Рассмотренные выше варианты отлично справляются с субботами и воскресеньями, однако они не учитывают государственные праздничные дни и официальные переносы (например, новогодние каникулы, майские праздники, 12 июня или 4 ноября). Если дата заявления выпадет на праздничный понедельник, алгоритм с функцией ДеньНедели() сочтет его рабочим днем.
В типовых конфигурациях (таких как «Управление производственным предприятием», «ERP Управление предприятием», «Комплексная автоматизация» или «Зарплата и управление персоналом») для этих целей предусмотрен регистр сведений РегламентированныйПроизводственныйКалендарь. В нем хранится классификатор дней: перечисление ВидыДнейПроизводственногоКалендаря (значения: Рабочий, Суббота, Воскресенье, Праздник, Предпраздничный).
Разберем пример запроса к регистру, который найдет ближайшую предшествующую рабочую дату:
Функция ПолучитьПредшествующуюРабочуюДату(ИсходнаяДата) Экспорт
ЦелеваяДата = НачалоДня(ИсходнаяДата) - (3 * 86400);
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| Календарь.ДатаКалендаря КАК ДатаКалендаря
|ИЗ
| РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК Календарь
|ГДЕ
| Календарь.ДатаКалендаря <= &ЦелеваяДата
| И Календарь.ВидДня В (
| ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий),
| ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный))
|УПОРЯДОЧИТЬ ПО
| Календарь.ДатаКалендаря УБЫВ";
Запрос.УстановитьПараметр("ЦелеваяДата", ЦелеваяДата);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.ДатаКалендаря;
КонецЕсли;
// Резервный возврат, если календарь в базе не заполнен
Возврат ЦелеваяДата;
КонецФункции
Преимущества запроса к производственному календарю: алгоритм автоматически отработает любые праздничные дни и предпраздничные переносы в соответствии с законодательством без необходимости ежегодно модифицировать программный код.
Важное ограничение: данный регистр должен своевременно заполняться пользователями в начале каждого рабочего года. Поэтому в коде всегда рекомендуется предусматривать возврат расчетной даты на случай, если выборка окажется пустой.