Приветствуем вас! Сегодня мы с вами разберем одну из частых и не всегда очевидных задач в 1С — как правильно рассчитать и красиво вывести интервал времени, особенно когда речь идет о рабочем времени с учетом производственного календаря. Мы выясним, почему стандартные средства форматирования могут быть недостаточными, и рассмотрим несколько подходов к решению этой проблемы, от простых до более комплексных. Начнем с того, что данные о продолжительности в 1С часто хранятся в виде разности двух дат или числового значения (например, в секундах или минутах). Однако простое форматирование такого значения не всегда дает желаемый результат, особенно если мы хотим получить вывод вида "5 дней 3 часа 12 минут" с правильным склонением слов и учетом только рабочих периодов.
Давайте рассмотрим стандартные возможности форматирования даты и времени в 1С. Для отображения интервала времени, если у нас есть две даты (например, `ДатаНачала` и `ДатаОкончания`), мы можем использовать функцию `Формат()` с соответствующим шаблоном.
Предположим, у нас есть интервал, выраженный в секундах, и мы хотим его представить в виде "Дней, часов, минут". Для этого мы можем создать "фиктивную" дату, прибавив к пустой дате наш интервал в секундах. Например:
ДатаИнтервала = '00010101000000' + ИнтервалВСекундах;
СтрокаИнтервала = Формат(ДатаИнтервала, "ДФ='д ""день"" ЧЧ ""час"" mm ""минута""'");
Здесь '00010101000000' представляет собой пустую дату. К ней мы прибавляем наш ИнтервалВСекундах. Символы формата д, ЧЧ, mm отвечают за дни, часы и минуты соответственно. Двойные кавычки используются для экранирования строковых литералов внутри шаблона.
Однако, здесь мы сталкиваемся с важной проблемой, о которой упоминалось в обсуждении: склонение слов. Если интервал составит "1 минута", строка будет выглядеть как " д час 1 минута" или "0 д 0 час 1 минута". Мы же хотим видеть "1 минуту", "2 минуты", "5 минут". Стандартная функция Формат() не умеет склонять слова по числу.
Для решения этой проблемы нам потребуется написать вспомогательную функцию, которая будет принимать число и возвращать слово в правильной форме. Давайте посмотрим на пример логики для такой функции:
В типовых конфигурациях 1С часто встречаются подобные функции в общих модулях, например, для работы со строками или текстом. Мы можем адаптировать их или создать свою. Для примера, можно использовать типовую функцию ПолучитьСклоненияСтрокиПоЧислу, если она доступна в вашей конфигурации.
Когда речь заходит о расчете рабочего интервала, простым вычитанием дат не обойтись, поскольку функция РазностьДат() считает календарные дни, а не рабочие. К счастью, в большинстве конфигураций 1С (Бухгалтерия Предприятия, Зарплата и Управление Персоналом, Комплексная Автоматизация, ERP) предусмотрены мощные типовые механизмы для работы с производственными календарями и графиками работы.
Эти механизмы хранят информацию о рабочих, выходных и праздничных днях в специализированных регистрах сведений, таких как РегистрСведений.КалендарныеГрафики или РегистрСведений.ПроизводственныйКалендарь. Это позволяет очень точно учитывать все особенности рабочих графиков, включая переносы выходных и предпраздничные дни.
Мы настоятельно рекомендуем использовать типовые функции, если они доступны в вашей конфигурации. Например, в конфигурациях на базе БСП (Библиотеки Стандартных Подсистем) часто присутствует функция РазностьДатПроизводственныхКалендарейПоВидамДней. Давайте посмотрим на пример использования этой функции:
// Пример получения количества рабочих дней с использованием типовой функции
// Предполагается, что есть модуль РасчетЗарплатыБазовый или аналогичный
// и доступ к объекту ПроизводственныйКалендарь.
Функция КоличествоРабочихДнейПоГрафику(НачалоПериода, КонецПериода, График) Экспорт
// Получаем производственный календарь, связанный с графиком
ЕвойныйКалендарь = График.ЕвойныйКалендарь(); // Пример метода получения календаря
ДанныеКалендаря = РасчетЗарплатыБазовый.РазностьДатПроизводственныхКалендарейПоВидамДней(
ЕвойныйКалендарь,
НачалоПериода,
КонецПериода
);
// Получаем количество рабочих дней из результата
Возврат ДанныеКалендаря.Получить(Перечисления.ВидыДнейПроизводственногоКалендаря.Рабочий);
КонецФункции
Преимущества использования типовых функций очевидны:
Если типовые функции не подходят или нам нужен более точный расчет с учетом конкретных рабочих часов в течение дня (например, с 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С может быть несколько видов календарей (производственный, индивидуальные графики). При реализации собственных функций убедитесь, что вы работаете с нужным календарем, передавая его в качестве параметра (как это сделано в функции ПолучитьРабочиеДниВИнтервале).
Мы рассмотрели различные подходы к расчету и отображению интервалов рабочего времени в 1С. Надеемся, что это подробное объяснение поможет вам эффективно решать подобные задачи в ваших проектах!
← К списку