Разработка механизмов учета рабочего времени — это классическая задача, с которой сталкивается практически каждый программист 1С. На первый взгляд кажется, что достаточно просто вычесть время начала из времени окончания, но на практике мы сталкиваемся с целым рядом подводных камней: переходы смены через полночь, хранение данных в некорректных форматах и сложности с точностью вычислений. В этой статье мы подробно разберем, как построить надежный алгоритм расчета времени, учитывающий все нюансы.
Рассмотрим ситуацию, описанную в теме. Основная сложность заключается в том, что сотрудники могут начинать работу в один день, а заканчивать — на следующий. Если наш запрос жестко ограничен одной датой, такие записи просто не попадут в выборку или будут рассчитаны неверно. Проанализируем типичную ошибку в тексте запроса:
ВЫБРАТЬ
РАЗНОСТЬДАТ(ВремяНачалаРаботы.ВремяНачалаРаботы, ВремяОкончанияРаботы.ВремяОкончанияРаботы, СЕКУНДА) КАК Время
ИЗ
РегистрСведений.ВремяНачалаРаботы КАК ВремяНачала,
РегистрСведений.ВремяОкончанияРаботы КАК ВремяОкончанияРаботы
ГДЕ
ВремяНачалаРаботы.Дата = &Дата И ВремяОкончанияРаботы.Дата = &Дата
В данном примере мы видим две критические ошибки. Во-первых, использование условия Дата = &Дата полностью отсекает случаи, когда работа закончилась после 00:00. Во-вторых, отсутствие связи между таблицами (соединения) порождает так называемое «декартово произведение», когда каждая запись о начале работы соединяется с каждой записью об окончании, что приводит к геометрическому росту количества строк в результате.
Прежде чем приступать к коду, выясним одну важную деталь. В обсуждении упоминалось, что даты хранятся в виде строк. Это критическая архитектурная ошибка. В 1С тип данных Дата — это сложный объект, содержащий и дату, и время. Если вы храните их как строки, вы лишаетесь возможности использовать встроенные функции СУБД для расчета разницы и эффективной индексации.
Если изменить структуру регистра невозможно, нам придется преобразовывать строки в даты внутри запроса или в коде, но правильным решением будет использование стандартного типа Дата. Это позволит нам использовать функцию РАЗНОСТЬДАТ максимально эффективно.
Давайте разберем, как составить запрос так, чтобы он «видел» переход через полночь. Вместо жесткого равенства дат мы будем использовать интервал. Проанализируем следующий подход:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РАЗНОСТЬДАТ(Начала.Период, Окончания.Период, СЕКУНДА) КАК СекундВсего
|ИЗ
| РегистрСведений.РабочееВремя КАК Начала
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.РабочееВремя КАК Окончания
| ПО Начала.Сотрудник = Окончания.Сотрудник
| И Начала.НомерСмены = Окончания.НомерСмены
|ГДЕ
| Начала.ВидСобытия = &НачалоРаботы
| И Окончания.ВидСобытия = &ОкончаниеРаботы
| И Начала.Период МЕЖДУ &НачалоПериода И &КонецПериода";
Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(ТекущаяДата));
// Увеличиваем период до конца следующего дня, чтобы захватить ночные смены
Запрос.УстановитьПараметр("КонецПериода", КонецДня(ТекущаяДата + 86400));
Обратите внимание, что мы добавили ВНУТРЕННЕЕ СОЕДИНЕНИЕ по сотруднику. Это гарантирует, что мы считаем время конкретного человека, а не среднюю температуру по больнице. Параметры &НачалоПериода и &КонецПериода теперь позволяют захватить события, произошедшие в течение двух суток.
Функция РАЗНОСТЬДАТ(..., СЕКУНДА) возвращает целое число секунд. Самая частая ошибка новичков — просто разделить секунды на 3600. В результате получаются числа с бесконечной дробной частью (например, 9.8333333), что неудобно для пользователя и чревато ошибками округления. Рассмотрим, как сделать это правильно, используя целочисленное деление и остаток от деления.
Разберем алгоритм пересчета в коде 1С:
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
ОбщееКоличествоСекунд = Выборка.СекундВсего;
// Получаем полное количество минут
ВсегоМинут = Цел(ОбщееКоличествоСекунд / 60);
// Вычисляем количество полных часов
КолЧасов = Цел(ВсегоМинут / 60);
// Получаем остаток минут (то, что не вошло в полные часы)
КолМинут = ВсегоМинут % 60;
// Выводим результат пользователю в формате ЧЧ:ММ
ТекстРезультат = Формат(КолЧасов, "ЧЦ=2; ЧВН=") + ":" + Формат(КолМинут, "ЧЦ=2; ЧВН=");
Сообщить("Сотрудник отработал: " + ТекстРезультат);
КонецЦикла;
Использование операторов Цел() (целая часть) и % (остаток от деления) позволяет нам получить точные значения без «хвостов» после запятой. Применение функции Формат с параметром ЧВН= гарантирует, что время будет выглядеть красиво (например, "08:05" вместо "8:5").
В реальных задачах часто требуется не просто общее время, а разделение на дневные и ночные часы (с 22:00 до 06:00), так как они оплачиваются иначе. Посмотрим, как можно реализовать такую проверку программно.
Проанализируем ситуацию: если смена началась в 20:00 и закончилась в 04:00, нам нужно разделить 8 часов на два интервала: 2 часа дневных (20:00 - 22:00) и 6 часов ночных (22:00 - 04:00).
Для этого в цикле обработки выборки мы можем использовать условие:
ГраницаНочи = Дата(Год(Выборка.ДатаНачала), Месяц(Выборка.ДатаНачала), День(Выборка.ДатаНачала), 22, 0, 0);
Если Выборка.ДатаОкончания > ГраницаНочи Тогда
// Здесь логика разделения интервала
НочныеСекунды = Выборка.ДатаОкончания - ГраницаНочи;
ДневныеСекунды = ГраницаНочи - Выборка.ДатаНачала;
КонецЕсли;
Также не забываем про обеденный перерыв. Если в вашей компании принято вычитать час обеда при смене более 8 часов, просто добавьте условие:
Если ВсегоМинут > 480 Тогда ВсегоМинут = ВсегоМинут - 60; КонецЕсли;
Для качественного расчета отработанного времени в 1С необходимо придерживаться следующих правил:
float.Формат для приведения к стандартному виду ЧЧ:ММ.Выполнив эти шаги, вы получите надежный инструмент учета времени, который не «сломается» при наступлении полуночи и будет выдавать понятные пользователю цифры.