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

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет
← К списку

Приветствуем вас! Сегодня мы с вами разберем одну из частых и не всегда очевидных задач в 1С — как правильно рассчитать и красиво вывести интервал времени, особенно когда речь идет о рабочем времени с учетом производственного календаря. Мы выясним, почему стандартные средства форматирования могут быть недостаточными, и рассмотрим несколько подходов к решению этой проблемы, от простых до более комплексных. Начнем с того, что данные о продолжительности в 1С часто хранятся в виде разности двух дат или числового значения (например, в секундах или минутах). Однако простое форматирование такого значения не всегда дает желаемый результат, особенно если мы хотим получить вывод вида "5 дней 3 часа 12 минут" с правильным склонением слов и учетом только рабочих периодов.

1. Базовое форматирование интервала и проблема склонения

Давайте рассмотрим стандартные возможности форматирования даты и времени в 1С. Для отображения интервала времени, если у нас есть две даты (например, `ДатаНачала` и `ДатаОкончания`), мы можем использовать функцию `Формат()` с соответствующим шаблоном.

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


ДатаИнтервала = '00010101000000' + ИнтервалВСекундах;
СтрокаИнтервала = Формат(ДатаИнтервала, "ДФ='д ""день"" ЧЧ ""час"" mm ""минута""'");

Здесь '00010101000000' представляет собой пустую дату. К ней мы прибавляем наш ИнтервалВСекундах. Символы формата д, ЧЧ, mm отвечают за дни, часы и минуты соответственно. Двойные кавычки используются для экранирования строковых литералов внутри шаблона.

Однако, здесь мы сталкиваемся с важной проблемой, о которой упоминалось в обсуждении: склонение слов. Если интервал составит "1 минута", строка будет выглядеть как " д час 1 минута" или "0 д 0 час 1 минута". Мы же хотим видеть "1 минуту", "2 минуты", "5 минут". Стандартная функция Формат() не умеет склонять слова по числу.

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

  1. Мы передаем в функцию число и три варианта слова (например, "день", "дня", "дней").
  2. Внутри функции анализируем число:
    • Если число оканчивается на 1 (и не является 11), используем первый вариант ("день").
    • Если число оканчивается на 2, 3, 4 (и не является 12, 13, 14), используем второй вариант ("дня").
    • В остальных случаях (0, 5-9, 10-20), используем третий вариант ("дней").

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

2. Расчет рабочего интервала с использованием типовых механизмов 1С

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

Эти механизмы хранят информацию о рабочих, выходных и праздничных днях в специализированных регистрах сведений, таких как РегистрСведений.КалендарныеГрафики или РегистрСведений.ПроизводственныйКалендарь. Это позволяет очень точно учитывать все особенности рабочих графиков, включая переносы выходных и предпраздничные дни.

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


// Пример получения количества рабочих дней с использованием типовой функции
// Предполагается, что есть модуль РасчетЗарплатыБазовый или аналогичный
// и доступ к объекту ПроизводственныйКалендарь.

Функция КоличествоРабочихДнейПоГрафику(НачалоПериода, КонецПериода, График) Экспорт

    // Получаем производственный календарь, связанный с графиком
    ЕвойныйКалендарь = График.ЕвойныйКалендарь(); // Пример метода получения календаря

    ДанныеКалендаря = РасчетЗарплатыБазовый.РазностьДатПроизводственныхКалендарейПоВидамДней(
        ЕвойныйКалендарь,
        НачалоПериода,
        КонецПериода
    );

    // Получаем количество рабочих дней из результата
    Возврат ДанныеКалендаря.Получить(Перечисления.ВидыДнейПроизводственногоКалендаря.Рабочий);

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

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

  1. Корректность: Они учитывают все нюансы производственного календаря и законодательства.
  2. Производительность: Запросы к регистрам обычно оптимизированы.
  3. Поддержка: Функции обновляются разработчиками 1С вместе с обновлениями платформы и законодательства.
  4. Универсальность: Работают с различными календарями и графиками.

3. Расчет рабочего интервала с учетом конкретных часов и оптимизацией запросов (универсальное решение)

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

Давайте разберем по шагам, как можно реализовать такую функцию, опираясь на опыт коллег с форума:

Шаг 1: Получение всех рабочих дней в заданном интервале

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


// Функция ПолучитьРабочиеДниВИнтервале
// Возвращает Соответствие, где ключ - дата дня, значение - Истина (если день рабочий)
&НаСервере
Функция ПолучитьРабочиеДниВИнтервале(ДатаНачала, ДатаОкончания, Календарь = Неопределено)

    Запрос = Новый Запрос;
    Запрос.Текст =
        "ВЫБРАТЬ
        |	т1.ДатаГрафика КАК Дата
        |ИЗ
        |	РегистрСведений.КалендарныеГрафики КАК т1
        |ГДЕ
        |	т1.ДатаГрафика >= &ДатаНачала
        |	И т1.ДатаГрафика <= &ДатаОкончания
        |	И т1.ДеньВключенВГрафик = Истина";

    Если Календарь <> Неопределено Тогда
        Запрос.Текст = Запрос.Текст + " И т1.Календарь = &Календарь";
        Запрос.УстановитьПараметр("Календарь", Календарь);
    КонецЕсли;

    Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаНачала));
    Запрос.УстановитьПараметр("ДатаОкончания", НачалоДня(ДатаОкончания));

    РезультатЗапроса = Запрос.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();

    Результат = Новый Соответствие;
    Пока Выборка.Следующий() Цикл
        Результат.Вставить(Выборка.Дата, Истина);
    КонецЦикла;

    Возврат Результат;

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

В этой функции мы получаем все даты из регистра КалендарныеГрафики, которые попадают в наш интервал и помечены как рабочие дни. Результат помещаем в объект Соответствие для быстрого доступа по дате.

Шаг 2: Расчет минут внутри одного рабочего дня

Теперь нам нужна функция, которая будет рассчитывать количество рабочих минут в пределах одного дня, учитывая заданные часы начала и окончания рабочего дня (например, с 9 до 18).


// Функция РасчетМинутВДень
// Рассчитывает количество рабочих минут в заданном интервале
// внутри одного дня, с учетом рабочих часов дня (ЧасН, ЧасО)
&НаСервере
Функция РасчетМинутВДень(ДатаНачала, ДатаОкончания, ЧасН, ЧасО)

    НачалоРабочегоДня = НачалоДня(ДатаНачала) + ЧасН * 3600;
    КонецРабочегоДня = НачалоДня(ДатаНачала) + ЧасО * 3600;

    // Ограничиваем расчет рабочим временем дня
    Если ДатаНачала < НачалоРабочегоДня Тогда
        ДатаНачала = НачалоРабочегоДня;
    КонецЕсли;

    Если ДатаОкончания > КонецРабочегоДня Тогда
        ДатаОкончания = КонецРабочегоДня;
    КонецЕсли;

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

    Возврат (ДатаОкончания - ДатаНачала) / 60; // Разница в секундах, делим на 60 для минут

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

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

Шаг 3: Основная функция расчета рабочих минут между датами

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


// Основная функция ПолучитьРабочихМинутМеждуДатами
&НаСервере
Функция ПолучитьРабочихМинутМеждуДатами(ДатаНачала, ДатаОкончания, ЧасН, ЧасО, Календарь = Неопределено)

    // Проверяем входные параметры на корректность
    Если ДатаНачала >= ДатаОкончания ИЛИ ЧасН < 0 ИЛИ ЧасО > 24 ИЛИ ЧасН >= ЧасО Тогда
        Возврат 0;
    КонецЕсли;

    // Рассчитываем продолжительность полного рабочего дня в минутах
    ПродолжительностьРабДня = (ЧасО - ЧасН) * 60;
    НачалоДняН = НачалоДня(ДатаНачала);
    НачалоДняК = НачалоДня(ДатаОкончания);

    // Получаем все рабочие дни в интервале одним запросом
    РабочиеДниМножество = ПолучитьРабочиеДниВИнтервале(НачалоДняН, НачалоДняК, Календарь);

    // Обработка случая, когда обе даты находятся в одном дне
    Если НачалоДняН = НачалоДняК Тогда
        Если РабочиеДниМножество.Получить(НачалоДняН) <> Неопределено Тогда
            Возврат РасчетМинутВДень(ДатаНачала, ДатаОкончания, ЧасН, ЧасО);
        Иначе
            Возврат 0;
        КонецЕсли;
    КонецЕсли;

    Общееминуты = 0;
    КоличествоПолныхРабочихДней = РабочиеДниМножество.Количество();

    // Обрабатываем начальный день
    // Если начальный день рабочий, рассчитываем минуты от ДатаНачала до конца рабочего дня
    Если РабочиеДниМножество.Получить(НачалоДняН) <> Неопределено Тогда
        Общееминуты = Общееминуты + РасчетМинутВДень(ДатаНачала, КонецДня(НачалоДняН), ЧасН, ЧасО);
        КоличествоПолныхРабочихДней = КоличествоПолныхРабочихДней - 1; // Уменьшаем счетчик полных дней
    КонецЕсли;

    // Обрабатываем конечный день
    // Если конечный день рабочий, рассчитываем минуты от начала рабочего дня до ДатаОкончания
    Если РабочиеДниМножество.Получить(НачалоДняК) <> Неопределено Тогда
        Общееминуты = Общееминуты + РасчетМинутВДень(НачалоДня(НачалоДняК), ДатаОкончания, ЧасН, ЧасО);
        КоличествоПолныхРабочихДней = КоличествоПолныхРабочихДней - 1; // Уменьшаем счетчик полных дней
    КонецЕсли;

    // Обрабатываем полные дни между начальным и конечным
    // Убедимся, что количество дней не отрицательное
    КоличествоПолныхРабочихДней = Макс(КоличествоПолныхРабочихДней, 0);

    Общееминуты = Общееминуты + КоличествоПолныхРабочихДней * ПродолжительностьРабДня;

    Возврат Общееминуты;

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

Проанализируем логику этой функции:

  1. Сначала проверяем, что переданные даты и часы корректны.
  2. Определяем НачалоДня для обеих граничных дат.
  3. С помощью функции ПолучитьРабочиеДниВИнтервале получаем все рабочие дни. Это ключевая оптимизация, позволяющая избежать множественных запросов к базе данных.
  4. Если обе даты находятся в одном рабочем дне, используем РасчетМинутВДень напрямую.
  5. Далее отдельно обрабатываем начальный и конечный дни. Если они рабочие, рассчитываем минуты, приходящиеся на рабочий интервал дня, используя РасчетМинутВДень. Мы вычитаем эти дни из общего количества рабочих дней, чтобы затем посчитать только полные рабочие дни между ними.
  6. Для всех полных рабочих дней между начальным и конечным днями (если таковые имеются) просто прибавляем полную продолжительность рабочего дня.

Такой подход обеспечивает высокую точность и гибкость, позволяя учитывать любые рабочие часы и графики.

4. Дополнительные рекомендации

Поле для сортировки и отбора:

Если вам часто приходится сортировать или отбирать данные по рассчитанному интервалу, коллеги на форуме рекомендуют создать дополнительное поле (например, в регистре или документе) типа "Число", где будет храниться этот интервал в секундах или минутах. Например, РазностьВСекундах. Это позволит выполнять быстрые запросы и сортировку без необходимости каждый раз пересчитывать сложный интервал.

Выбор типа календаря:

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

Мы рассмотрели различные подходы к расчету и отображению интервалов рабочего времени в 1С. Надеемся, что это подробное объяснение поможет вам эффективно решать подобные задачи в ваших проектах!

← К списку