Как рассчитать рабочие часы между двумя датами в 1С, исключая выходные и праздники?

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

В повседневных задачах автоматизации часто возникает необходимость вычислить точную длительность выполнения какого-либо процесса в рабочих часах. Стандартный вычет одной даты из другой дает календарное время, что не подходит для бизнес-задач, где учитывается только рабочее время сотрудников. При разработке систем учета времени или тайм-трекеров нам нужно получить количество часов между датой начала и датой окончания, но с учетом графика работы, например, с 9:00 до 18:00, и с исключением суббот, воскресений и официальных праздников.

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

Подход 1: Простой перебор дней по производственному календарю

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

Логика этого метода выглядит так:

  1. Запускаем цикл, перебирающий все дни от НачалоДня(ДатаНачала) до НачалоДня(ДатаОкончания).
  2. Внутри цикла для каждой даты делаем запрос к регистру сведений РегламентированныйПроизводственныйКалендарь, чтобы определить тип дня (рабочий, суббота, воскресенье, праздник).
  3. Если день рабочий, обрабатываем его:
    • Если это и день начала, и день окончания периода, то рассчитываем разницу во времени в пределах этого дня.
    • Если это только день начала, вычисляем, сколько рабочих часов осталось с момента ДатаНачала до конца рабочего дня.
    • Если это только день окончания, вычисляем, сколько рабочих часов прошло с начала рабочего дня до момента ДатаОкончания.
    • Если это "полный" рабочий день внутри периода, просто добавляем 8 часов.
  4. Суммируем все часы и получаем результат.

Посмотрим на пример реализации такой функции. Обратите внимание, что этот код имеет серьезные допущения и не является универсальным.


Функция РассчитатьРабочиеЧасыПоКалендарю(ДатаНачала, ДатаОкончания)

    ИтогоСекунд = 0;
    ТекущаяДата = НачалоДня(ДатаНачала);

    Пока ТекущаяДата <= ДатаОкончания Цикл

        Запрос = Новый Запрос;
        Запрос.Текст =
            "ВЫБРАТЬ
            |    РегламентированныйПроизводственныйКалендарь.ВидДня
            |ИЗ
            |    РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК РегламентированныйПроизводственныйКалендарь
            |ГДЕ
            |    РегламентированныйПроизводственныйКалендарь.ДатаКалендаря = &ДатаКалендаря";

        Запрос.УстановитьПараметр("ДатаКалендаря", ТекущаяДата);
        Выборка = Запрос.Выполнить().Выбрать();

        Если Выборка.Следующий() Тогда
            Если Выборка.ВидДня = Перечисления.ВидыДнейПроизводственногоКалендаря.Рабочий Тогда
                
                // Определяем границы рабочего дня (здесь жестко заданы 9:00 и 18:00)
                НачалоРабочегоДня = ТекущаяДата + 9*3600;
                КонецРабочегоДня  = ТекущаяДата + 18*3600;

                // Определяем фактический интервал для расчета
                НачалоИнтервала = Макс(ДатаНачала, НачалоРабочегоДня);
                КонецИнтервала  = Мин(ДатаОкончания, КонецРабочегоДня);

                Если НачалоИнтервала < КонецИнтервала Тогда
                   ИтогоСекунд = ИтогоСекунд + (КонецИнтервала - НачалоИнтервала);
                КонецЕсли;

            КонецЕсли;
        КонецЕсли;

        ТекущаяДата = НачалоДня(ТекущаяДата) + 86400; // Переходим к следующему дню

    КонецЦикла;

    Возврат ИтогоСекунд / 3600;

КонецФункции

Недостатки этого подхода:

Этот метод можно использовать только в очень простых случаях, когда условия работы строго фиксированы и не меняются. Для более сложных и реальных сценариев он не подходит.

Подход 2: Корректное решение с использованием Графиков работы

В большинстве типовых конфигураций 1С (ERP, ЗУП, КА, УТ) существует специальный объект для хранения информации о рабочих часах — справочник ГрафикиРаботыСотрудников (или аналогичный, например, ГрафикиРаботы). Этот подход является наиболее правильным и гибким, так как он использует стандартные механизмы платформы.

Основная идея:

Мы получаем данные непосредственно из графика работы, который уже содержит всю информацию о рабочих интервалах, выходных, праздниках и сокращенных днях. Нам не нужно делать запросы к производственному календарю и вручную определять границы рабочего дня.

Разберем по шагам, как реализовать функцию с использованием графиков.

  1. Получаем объект графика. Нам нужно найти нужный график работы, например, по наименованию или коду.
  2. Определяем основной цикл. Как и в предыдущем методе, организуем цикл по дням от даты начала до даты окончания.
  3. Получаем интервалы рабочего времени. Для каждого дня внутри цикла мы будем получать из графика таблицу его рабочих интервалов. В типовых конфигурациях для этого есть специальные функции. Например, в конфигурациях с Библиотекой стандартных подсистем (БСП) это можно сделать через общие модули, такие как УправлениеПерсоналом или УправлениеВременем.
  4. Суммируем секунды. Для каждого рабочего интервала мы рассчитываем пересечение с нашим общим периодом (от ДатаНачала до ДатаОкончания) и суммируем полученное время в секундах.

Рассмотрим пример функции, которая реализует эту логику. Она будет более сложной, но и гораздо более точной и универсальной.


Функция РассчитатьРабочиеЧасыПоГрафику(ДатаНачала, ДатаОкончания, ГрафикРаботы)

    Если ДатаНачала >= ДатаОкончания Тогда
        Возврат 0;
    КонецЕсли;

    ИтогоСекунд = 0;
    
    // Получаем данные графика за весь нужный период
    // Это более производительно, чем получать в цикле на каждый день
    ПараметрыПериода = УправлениеВременем.ПараметрыДляПолученияДанныхГрафика(ГрафикРаботы, ДатаНачала, ДатаОкончания);
    ТаблицаДанныхГрафика = УправлениеВременем.ДанныеГрафикаРаботы(ПараметрыПериода);

    Для Каждого СтрокаДня Из ТаблицаДанныхГрафика Цикл
        
        Если НЕ СтрокаДня.ЯвляетсяРабочимДнем Тогда
            Продолжить;
        КонецЕсли;
        
        ДатаДня = СтрокаДня.Дата;

        // Для каждого рабочего дня получаем его интервалы
        Для Каждого Интервал Из СтрокаДня.Интервалы Цикл
            
            // Начало и конец рабочего интервала в формате даты
            НачалоРабочегоИнтервала = ДатаДня + Интервал.Начало; // Начало - секунды от начала дня
            КонецРабочегоИнтервала  = ДатаДня + Интервал.Окончание; // Окончание - секунды от начала дня

            // Рассчитываем пересечение нашего периода с рабочим интервалом
            НачалоПересечения = Макс(ДатаНачала, НачалоРабочегоИнтервала);
            КонецПересечения  = Мин(ДатаОкончания, КонецРабочегоИнтервала);

            Если НачалоПересечения < КонецПересечения Тогда
                ИтогоСекунд = ИтогоСекунд + (КонецПересечения - НачалоПересечения);
            КонецЕсли;
            
        КонецЦикла;
        
    КонецЦикла;

    Возврат ИтогоСекунд / 3600;

КонецФункции

// Пример вызова
ПроверяемыйГрафик = Справочники.ГрафикиРаботыСотрудников.НайтиПоНаименованию("Основной (пятидневка)");
Начало = '20231120163000'; // 20 ноября, 16:30
Конец = '20231122110000';   // 22 ноября, 11:00
РезультатВЧасах = РассчитатьРабочиеЧасыПоГрафику(Начало, Конец, ПроверяемыйГрафик);
Сообщить("Количество рабочих часов: " + РезультатВЧасах);

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

Подход 3: Оптимальный метод с использованием готовых функций БСП

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

Ключевая функция: УправлениеВременем.РазностьДатВСекундахПоГрафику()

Эта функция делает все то же самое, что и наш код из второго подхода, но она уже написана, протестирована и поддерживается фирмой "1С" в рамках БСП. Использовать ее — самый простой и надежный путь.

Как ее использовать?

Функция принимает три параметра:

Посмотрим на пример кода. Он получается максимально лаконичным.


// Находим нужный график работы
График = Справочники.ГрафикиРаботыСотрудников.НайтиПоНаименованию("Пятидневка (с 9 до 18)");

Если НЕ ЗначениеЗаполнено(График) Тогда
    Сообщить("График работы не найден!");
    Возврат;
КонецЕсли;

// Задаем период
ДатаНачала    = ТекущаяДата() - 5 * 86400; // 5 дней назад
ДатаОкончания = ТекущаяДата();

// Вызываем готовую функцию БСП
ДлительностьВСЕкундах = УправлениеВременем.РазностьДатВСекундахПоГрафику(ДатаНачала, ДатаОкончания, График);

// Переводим результат в часы
ДлительностьВЧасах = ДлительностьВСЕкундах / 3600;

Сообщить(СтрокаФормат("Количество рабочих часов в периоде с %1 по %2 по графику ""%3"" составило: %4 ч.",
    Формат(ДатаНачала, "ДФ=dd.MM.yyyy HH:mm:ss"),
    Формат(ДатаОкончания, "ДФ=dd.MM.yyyy HH:mm:ss"),
    График,
    ДлительностьВЧасах));

Итог:

Для решения задачи расчета рабочего времени всегда рекомендуется использовать стандартные механизмы. Если ваша конфигурация содержит БСП, используйте готовую функцию УправлениеВременем.РазностьДатВСекундахПоГрафику — это наиболее эффективный вариант, особенно когда вы создаете универсальный отчёт для ЗУП или другие сложные аналитические инструменты.

← На главную