Как рассчитать плановую дату и время окончания работ по графику в 1С

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

В процессе автоматизации бизнес-процессов, служб поддержки (Service Desk) или проектной деятельности часто возникает задача: рассчитать точную дату и время завершения работ. Казалось бы, задача тривиальная — прибавить к дате начала плановое количество часов. Однако на практике мы сталкиваемся с необходимостью учитывать график работы организации (например, пятидневка с 9:00 до 18:00), выходные дни, государственные праздники и переносы рабочих дней.

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

Сценарий 1: Использование типовых механизмов БСП и ERP

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

Проанализируем подсистему «Календарные графики» в БСП. Вопреки распространенному мнению, она умеет работать не только с целыми днями. В модуле КалендарныеГрафики существует экспортная функция РасписанияРаботыНаПериод(). Она позволяет получить детальную таблицу интервалов времени работы за указанный интервал дат:


// Пример получения интервалов работы по графику в БСП
Графики = Новый Массив;
Графики.Добавить(СсылкаНаГрафикРаботы);

ТаблицаИнтервалов = КалендарныеГрафики.РасписанияРаботыНаПериод(Графики, ДатаНачала, ДатаОкончания);
// Таблица содержит колонки: ДатаГрафика, ВремяНачала, ВремяОкончания

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

Сценарий 2: Реализация собственного алгоритма «почасового сдвига»

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

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

  1. Корректировка точки старта: Если время начала работы попадает на нерабочее время (ночь, выходной), мы принудительно сдвигаем время старта на 9:00 ближайшего рабочего дня.
  2. Обработка первого (неполного) рабочего дня: Мы определяем, сколько рабочих часов осталось до конца текущей смены (до 18:00). Если задача укладывается в этот остаток, мы просто прибавляем время к старту. Если нет — вычитаем этот остаток из общего пула трудозатрат и переносим точку старта на 9:00 следующего рабочего дня.
  3. Циклический сдвиг по целым дням: Мы движемся по дням вперед, вычитая из оставшегося времени задачи полную норму рабочего дня (в нашем случае — 9 часов, если считать с 9:00 до 18:00, или 8 часов, если мы вычитаем обеденный перерыв). Как только остаток работы становится меньше нормы дня, мы прибавляем эти «хвосты» к 9:00 последнего расчетного дня.

Рассмотрим пример реализации этой функции на встроенном языке 1С. Для простоты будем считать, что рабочий день длится с 9:00 до 18:00 (длительность смены — 9 часов, без учета обеда):


Функция РассчитатьПлановуюДатуОкончания(Знач ДатаСтарта, Знач ТрудозатратыЧ, МассивВыходныхИПраздников) Экспорт
    
    // Определяем параметры рабочего дня
    ВремяНачалаСмены  = 9 * 3600;  // 9:00 в секундах
    ВремяКонцаСмены   = 18 * 3600; // 18:00 в секундах
    ДлительностьСмены = ВремяКонцаСмены - ВремяНачалаСмены; // 9 часов в секундах
    
    ОстатокТрудозатратСС = ТрудозатратыЧ * 3600; // Переводим часы в секунды
    ТекущаяДатаВремя = ДатаСтарта;
    
    Пока ОстатокТрудозатратСС > 0 Цикл
        
        // 1. Проверяем, является ли день рабочим
        ТекущаяДата = НачалоДня(ТекущаяДатаВремя);
        Если ДеньНедели(ТекущаяДата) > 5 Или МассивВыходныхИПраздников.Найти(ТекущаяДата) <> Неопределено Тогда
            // Переходим на следующий день, устанавливаем время на начало смены
            ТекущаяДатаВремя = ТекущаяДата + 86400 + ВремяНачалаСмены;
            Продолжить;
        КонецЕсли;
        
        // Время начала работы в секундах внутри текущего дня
        СекундСНачалаДня = ТекущаяДатаВремя - ТекущаяДата;
        
        // Корректируем время, если мы "вылетели" за пределы смены
        Если СекундСНачалаДня < ВремяНачалаСмены Тогда
            СекундСНачалаДня = ВремяНачалаСмены;
            ТекущаяДатаВремя = ТекущаяДата + ВремяНачалаСмены;
        ИначеЕсли СекундСНачалаДня >= ВремяКонцаСмены Тогда
            // Переносим старт на утро следующего дня
            ТекущаяДатаВремя = ТекущаяДата + 86400 + ВремяНачалаСмены;
            Продолжить;
        КонецЕсли;
        
        // Сколько секунд осталось работать в текущем дне
        ДоступноСекундСегодня = ВремяКонцаСмены - СекундСНачалаДня;
        
        Если ОстатокТрудозатратСС <= ДоступноСекундСегодня Тогда
            // Задача завершится сегодня
            ТекущаяДатаВремя = ТекущаяДатаВремя + ОстатокТрудозатратСС;
            ОстатокТрудозатратСС = 0;
        Иначе
            // Задача переносится на следующие дни
            ОстатокТрудозатратСС = ОстатокТрудозатратСС - ДоступноСекундСегодня;
            ТекущаяДатаВремя = ТекущаяДата + 86400 + ВремяНачалаСмены;
        КонецЕсли;
        
    КонецЦикла;
    
    Возврат ТекущаяДатаВремя;
    
КонецФункции

Сценарий 3: Получение праздников и выходных через внешнее API

В кодовой реализации выше используется параметр МассивВыходныхИПраздников. Откуда его взять, если в базе нет производственного календаря? Чтобы не хардкодить праздничные дни и переносы вручную в модулях системы, мы можем воспользоваться бесплатным и крайне стабильным внешним 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С в повторно используемом модуле), после чего мгновенно рассчитываем даты окончания любых задач с точностью до секунд.

← На главную