При создании отчетов в 1С:Предприятии на базе Системы Компоновки Данных (СКД) часто возникает задача расчета показателей, которые зависят от значений из предыдущих строк. Одним из таких кейсов является вычисление времени отсутствия между смежными интервалами работы или присутствия сотрудника, когда необходимо получить разницу между текущим временем прихода и предыдущим временем ухода. В этой статье мы подробно разберем, как реализовать такую логику, преодолевая ограничения стандартных функций СКД для детальных записей.
Предположим, у нас есть данные о времени прихода и ухода сотрудника, хранящиеся в регистре сведений. Мы хотим получить отчет следующего вида:
Пример желаемого результата:
Период Приход Уход ВремяОтсутствия 21.01.21 8.45 10.00 0 21.01.21 10.05 13.00 5 (10.05 - 10.00) 21.01.21 14.00 18.00 60 (14.00 - 13.00)
Автор исходной темы на форуме столкнулся с проблемой, пытаясь использовать функцию ВычислитьВыражение в СКД для получения предыдущего значения, но получил "охинею" (некорректный результат) в группировках. Мы выясним причину такого поведения и предложим более надежный подход.
ВычислитьВыражение не всегда подходит для детальных записейФункция ВычислитьВыражение в СКД предназначена для агрегирования данных и обращения к значениям в контексте группировок. Если вы пытаетесь использовать ее для получения значения "предыдущей" строки в детальных записях без явных группировок или при сложной логике определения "предыдущей" записи, она может давать непредсказуемые результаты. Для корректной работы часто требуется строго определенный порядок записей и контекст группировки, чтобы СКД могла однозначно определить, что является "предыдущим" значением. В нашем случае, "предыдущий уход" должен быть самым поздним уходом, который был раньше текущего прихода в рамках одного дня для одного сотрудника, что сложно выразить через простые параметры ВычислитьВыражение.
Самым надежным способом получить значение из "предыдущей" записи для дальнейших вычислений является модификация запроса, который формирует данные для СКД. Наша цель – сделать так, чтобы значение "УходПредыдущий" присутствовало в той же строке результата запроса, что и "ПриходТекущий". Это позволит нам затем легко вычислить разницу между этими двумя полями.
Рассмотрим по шагам, как это можно сделать, используя механизм самосоединения (ЛЕВОЕ СОЕДИНЕНИЕ) таблиц в запросе 1С.
Предположим, наш регистр сведений называется РегистрСведений.РегистрацииВремени и имеет следующие поля:
Период (Дата, дата без времени) - День, к которому относится регистрация.Сотрудник (Справочник.Сотрудники) - Измерение, по которому ведутся регистрации.ДатаПрихода (ДатаВремя) - Ресурс, фиксирующий время прихода.ДатаУхода (ДатаВремя) - Ресурс, фиксирующий время ухода.В нашем запросе мы сначала выберем все необходимые поля из основного источника данных.
ВЫБРАТЬ
РегистрацииВремени.Период КАК ДатаДня,
РегистрацииВремени.Сотрудник КАК Сотрудник,
РегистрацииВремени.ДатаПрихода КАК ДатаПриходаТекущая,
РегистрацииВремени.ДатаУхода КАК ДатаУходаТекущая
ИЗ
РегистрСведений.РегистрацииВремени КАК РегистрацииВремени
Теперь нам нужно получить для каждой текущей записи время ухода, которое было непосредственно перед текущим приходом. Мы сделаем это, соединив регистр сведений "сам с собой". Для этого мы добавим в запрос вторую ссылку на тот же регистр, но с другим псевдонимом, например, ПредыдущиеРегистрации.
Условия соединения будут следующие:
Сотруднику и Периоду (дню), чтобы найти предыдущие регистрации для того же сотрудника в тот же день.ПредыдущиеРегистрации.ДатаУхода < РегистрацииВремени.ДатаПриходаТекущая гарантирует, что мы рассматриваем только те уходы, которые произошли до текущего прихода.МАКСИМУМ(ПредыдущиеРегистрации.ДатаУхода), чтобы из всех подходящих предыдущих уходов выбрать самый последний, который и будет нашим "УходомПредыдущим".Обратите внимание на использование ЛЕВОЕ СОЕДИНЕНИЕ. Это важно, потому что для первой записи дня или для первой записи в целом не будет "предыдущего ухода". В этом случае МАКСИМУМ(ПредыдущиеРегистрации.ДатаУхода) вернет NULL, что корректно для нашего расчета.
Рассмотрим полный запрос:
ВЫБРАТЬ
РегистрацииВремени.Период КАК ДатаДня,
РегистрацииВремени.Сотрудник КАК Сотрудник,
РегистрацииВремени.ДатаПрихода КАК ДатаПриходаТекущая,
РегистрацииВремени.ДатаУхода КАК ДатаУходаТекущая,
МАКСИМУМ(ПредыдущиеРегистрации.ДатаУхода) КАК ДатаУходаПредыдущая
ИЗ
РегистрСведений.РегистрацииВремени КАК РегистрацииВремени
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РегистрацииВремени КАК ПредыдущиеРегистрации
ПО (ПредыдущиеРегистрации.Период = РегистрацииВремени.Период)
И (ПредыдущиеРегистрации.Сотрудник = РегистрацииВремени.Сотрудник)
И (ПредыдущиеРегистрации.ДатаУхода < РегистрацииВремени.ДатаПрихода)
ГДЕ
// Здесь можно добавить дополнительные условия отбора, например, по периоду отчета
РегистрацииВремени.Период МЕЖДУ &НачалоПериода И &КонецПериода
СГРУППИРОВАТЬ ПО
РегистрацииВремени.Период,
РегистрацииВремени.Сотрудник,
РегистрацииВремени.ДатаПрихода,
РегистрацииВремени.ДатаУхода
УПОРЯДОЧИТЬ ПО
РегистрацииВремени.Период,
РегистрацииВремени.Сотрудник,
РегистрацииВремени.ДатаПрихода
Важные моменты:
СГРУППИРОВАТЬ ПО: Мы группируем по всем полям из основной таблицы РегистрацииВремени, а также по Сотруднику и Периоду. Это необходимо, потому что мы используем агрегатную функцию МАКСИМУМ().УПОРЯДОЧИТЬ ПО: Крайне важно установить правильный порядок для получения корректных результатов. Упорядочивание по Период, Сотрудник и ДатаПрихода гарантирует, что данные обрабатываются в хронологическом порядке для каждого сотрудника в течение дня.После того как наш запрос возвращает поля ДатаПриходаТекущая и ДатаУходаПредыдущая в одной строке, мы можем легко вычислить "ВремяОтсутствия" в СКД.
ВремяОтсутствия.
МАКСИМУМ(0, РазностьДат(ДатаУходаПредыдущая, ДатаПриходаТекущая, Минута))
Разберем эту формулу:
РазностьДат(<ДатаНачала>, <ДатаОкончания>, <ТипРазности>): Эта функция 1С вычисляет разницу между двумя значениями типа Дата/Время.ДатаУходаПредыдущая: Это наше "ДатаНачала".ДатаПриходаТекущая: Это наше "ДатаОкончания".Минута: Это ТипРазности, указывающий, что мы хотим получить разницу в минутах.Функция МАКСИМУМ(0, ...) используется для следующих целей:
ДатаУходаПредыдущая отсутствует (NULL, для первой записи дня), то РазностьДат также вернет NULL, и МАКСИМУМ(0, NULL) даст 0. Это соответствует нашим требованиям.ДатаПриходаТекущая окажется раньше ДатаУходаПредыдущая (что может указывать на наложение интервалов, или некорректные данные), то РазностьДат вернет отрицательное значение. МАКСИМУМ(0, отрицательное_число) также вернет 0, что логично, так как отсутствия в данном случае нет.После добавления вычисляемого поля, вы можете использовать его в макете отчета:
ВремяОтсутствия в выбранные поля.Период, Приход (или ДатаПриходаТекущая) и Уход (или ДатаУходаТекущая), чтобы получить требуемую структуру отчета.ДатаДня, Сотрудник, ДатаПриходаТекущая, чтобы детальные записи выводились в хронологическом порядке, что делает логику "предыдущий" визуально понятной.Давайте проанализируем, как наш подход сработает на приведенном ранее примере:
Исходные данные (упрощенно):
Сотрудник1 | 21.01.21 | 08:45:00 | 10:00:00
Сотрудник1 | 21.01.21 | 10:05:00 | 13:00:00
Сотрудник1 | 21.01.21 | 14:00:00 | 18:00:00
Результат запроса (после самосоединения):
Текущая запись: 21.01.21 | 08:45:00 | 10:00:00
Для этой записи нет ПредыдущиеРегистрации.ДатаУхода < 08:45:00. Следовательно, ДатаУходаПредыдущая будет NULL.
Расчет: МАКСИМУМ(0, РазностьДат(NULL, 08:45:00, Минута)) = МАКСИМУМ(0, NULL) = 0.
Текущая запись: 21.01.21 | 10:05:00 | 13:00:00
Запрос найдет МАКСИМУМ(ПредыдущиеРегистрации.ДатаУхода), который меньше 10:05:00, для того же сотрудника и дня. Это будет 10:00:00.
ДатаУходаПредыдущая = 10:00:00.
Расчет: МАКСИМУМ(0, РазностьДат(10:00:00, 10:05:00, Минута)) = МАКСИМУМ(0, 5) = 5.
Текущая запись: 21.01.21 | 14:00:00 | 18:00:00
Запрос найдет МАКСИМУМ(ПредыдущиеРегистрации.ДатаУхода), который меньше 14:00:00. Это будет 13:00:00.
ДатаУходаПредыдущая = 13:00:00.
Расчет: МАКСИМУМ(0, РазностьДат(13:00:00, 14:00:00, Минута)) = МАКСИМУМ(0, 60) = 60.
Как видим, этот подход позволяет точно реализовать требуемую логику, обходя сложности с обработкой "предыдущих" значений непосредственно в СКД для детальных записей.
Вычисление значений на основе предыдущих строк в СКД, особенно для детальных записей, часто требует непрямых методов. Использование самосоединения таблиц в запросе к источнику данных является мощным и гибким инструментом, который позволяет принести необходимое "предыдущее" значение в текущую строку. После этого стандартные функции СКД, такие как РазностьДат, легко справляются с финальным расчетом. Этот метод гарантирует корректность и предсказуемость результатов даже в сложных отчетах с детальными данными.