В процессе автоматизации бизнес-процессов, служб поддержки (Service Desk) или проектной деятельности часто возникает задача: рассчитать точную дату и время завершения работ. Казалось бы, задача тривиальная — прибавить к дате начала плановое количество часов. Однако на практике мы сталкиваемся с необходимостью учитывать график работы организации (например, пятидневка с 9:00 до 18:00), выходные дни, государственные праздники и переносы рабочих дней.
В этой статье мы подробно разберем, как решить эту задачу в среде 1С:Предприятие. Мы рассмотрим как стандартные механизмы, заложенные в типовых конфигурациях, так и алгоритм создания собственного гибкого механизма «почасового сдвига», если в вашей конфигурации нельзя создавать новые регистры или использовать стандартный производственный календарь.
Если вы работаете в конфигурации, где внедрена Библиотека стандартных подсистем (БСП) или используются тяжелые решения вроде 1С:ERP, 1С:Комплексная автоматизация или 1С:Документооборот, изобретать велосипед не нужно. В них уже заложены механизмы по работе с временными интервалами.
Проанализируем подсистему «Календарные графики» в БСП. Вопреки распространенному мнению, она умеет работать не только с целыми днями. В модуле КалендарныеГрафики существует экспортная функция РасписанияРаботыНаПериод(). Она позволяет получить детальную таблицу интервалов времени работы за указанный интервал дат:
// Пример получения интервалов работы по графику в БСП
Графики = Новый Массив;
Графики.Добавить(СсылкаНаГрафикРаботы);
ТаблицаИнтервалов = КалендарныеГрафики.РасписанияРаботыНаПериод(Графики, ДатаНачала, ДатаОкончания);
// Таблица содержит колонки: ДатаГрафика, ВремяНачала, ВремяОкончания
В 1С:Документообороте логика расчета сроков задач по графику сотрудника встроена еще глубже. В модулях бизнес-процессов (например, РаботаСБизнесПроцессамиИЗадачами) содержатся готовые функции расчета даты окончания по персональным календарям. Если у вас доступна эта конфигурация, рекомендуем изучить код этих функций.
Что делать, если в вашей конфигурации нет производственного календаря (или его структура сильно переписана и работает некорректно), а добавлять новые объекты метаданных запрещено правилами поддержки? В этом случае нам придется написать собственный алгоритм.
Простое прибавление секунд в цикле, проверяя каждый шаг на ДеньНедели(), — это крайне неоптимальный путь, который вызовет зависание системы при больших объемах работ. Мы применим более изящный математический подход. Разделим расчет на три основных этапа:
Рассмотрим пример реализации этой функции на встроенном языке 1С. Для простоты будем считать, что рабочий день длится с 9:00 до 18:00 (длительность смены — 9 часов, без учета обеда):
Функция РассчитатьПлановуюДатуОкончания(Знач ДатаСтарта, Знач ТрудозатратыЧ, МассивВыходныхИПраздников) Экспорт
// Определяем параметры рабочего дня
ВремяНачалаСмены = 9 * 3600; // 9:00 в секундах
ВремяКонцаСмены = 18 * 3600; // 18:00 в секундах
ДлительностьСмены = ВремяКонцаСмены - ВремяНачалаСмены; // 9 часов в секундах
ОстатокТрудозатратСС = ТрудозатратыЧ * 3600; // Переводим часы в секунды
ТекущаяДатаВремя = ДатаСтарта;
Пока ОстатокТрудозатратСС > 0 Цикл
// 1. Проверяем, является ли день рабочим
ТекущаяДата = НачалоДня(ТекущаяДатаВремя);
Если ДеньНедели(ТекущаяДата) > 5 Или МассивВыходныхИПраздников.Найти(ТекущаяДата) <> Неопределено Тогда
// Переходим на следующий день, устанавливаем время на начало смены
ТекущаяДатаВремя = ТекущаяДата + 86400 + ВремяНачалаСмены;
Продолжить;
КонецЕсли;
// Время начала работы в секундах внутри текущего дня
СекундСНачалаДня = ТекущаяДатаВремя - ТекущаяДата;
// Корректируем время, если мы "вылетели" за пределы смены
Если СекундСНачалаДня < ВремяНачалаСмены Тогда
СекундСНачалаДня = ВремяНачалаСмены;
ТекущаяДатаВремя = ТекущаяДата + ВремяНачалаСмены;
ИначеЕсли СекундСНачалаДня >= ВремяКонцаСмены Тогда
// Переносим старт на утро следующего дня
ТекущаяДатаВремя = ТекущаяДата + 86400 + ВремяНачалаСмены;
Продолжить;
КонецЕсли;
// Сколько секунд осталось работать в текущем дне
ДоступноСекундСегодня = ВремяКонцаСмены - СекундСНачалаДня;
Если ОстатокТрудозатратСС <= ДоступноСекундСегодня Тогда
// Задача завершится сегодня
ТекущаяДатаВремя = ТекущаяДатаВремя + ОстатокТрудозатратСС;
ОстатокТрудозатратСС = 0;
Иначе
// Задача переносится на следующие дни
ОстатокТрудозатратСС = ОстатокТрудозатратСС - ДоступноСекундСегодня;
ТекущаяДатаВремя = ТекущаяДата + 86400 + ВремяНачалаСмены;
КонецЕсли;
КонецЦикла;
Возврат ТекущаяДатаВремя;
КонецФункции
В кодовой реализации выше используется параметр МассивВыходныхИПраздников. Откуда его взять, если в базе нет производственного календаря? Чтобы не хардкодить праздничные дни и переносы вручную в модулях системы, мы можем воспользоваться бесплатным и крайне стабильным внешним API-сервисом isdayoff.ru.
Важная ошибка многих разработчиков — опрашивать веб-сервис в цикле по одному дню. Это приводит к колоссальным задержкам в работе интерфейса и может вызвать блокировку вашего IP-адреса сервисом. Правильный подход — получить календарь на весь год или на период планирования (например, на 30–60 дней вперед) одним HTTP-запросом и кэшировать результат в памяти.
Разберем, как получить данные по рабочим и праздничным дням одной строкой:
Функция ПолучитьКалендарьПраздниковНаПериод(ДатаНачала, ДатаОкончания)
МассивПраздников = Новый Массив;
// Формируем запрос к сервису isdayoff.ru
ФорматДаты = "DF=yyyyMMdd";
СтрДатаНачала = Формат(ДатаНачала, ФорматДаты);
СтрДатаОкончания = Формат(ДатаОкончания, ФорматДаты);
АдресЗапроса = СтрШаблон("/api/getdata?date1=%1&date2=%2&cc=ru", СтрДатаНачала, СтрДатаОкончания);
HTTPСоединение = Новый HTTPСоединение("isdayoff.ru",,,,, 5, Новый ЗащищенноеСоединениеOpenSSL);
HTTPЗапрос = Новый HTTPЗапрос(АдресЗапроса);
Попытка
HTTPОтвет = HTTPСоединение.Получить(HTTPЗапрос);
Если HTTPОтвет.КодСостояния = 200 Тогда
РезультатСтрокой = HTTPОтвет.ПолучитьТелоКакСтроку();
// Строка состоит из символов: "0" - рабочий, "1" - выходной, "2" - сокращенный
Для Индекс = 1 По СтрДлина(РезультатСтрокой) Цикл
Символ = Сред(РезультатСтрокой, Индекс, 1);
ТекущаяДата = НачалоДня(ДатаНачала) + (Индекс - 1) * 86400;
// Если это выходной (1) или праздничный день
Если Символ = "1" Тогда
МассивПраздников.Добавить(ТекущаяДата);
КонецЕсли;
КонецДля;
КонецЕсли;
Исключение
ЗаписьЖурналаРегистрации("ПолучениеКалендаря", УровеньЖурналаРегистрации.Предупреждение,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
Возврат МассивПраздников;
КонецФункции
Используя связку из этих двух методов, мы получаем отказоустойчивое и невероятно быстрое решение. Мы один раз обращаемся к веб-сервису при запуске расчета (или кэшируем эти данные на сервере 1С в повторно используемом модуле), после чего мгновенно рассчитываем даты окончания любых задач с точностью до секунд.