При разработке кадровых отчетов в 1С программисты часто сталкиваются с задачей «склеивания» смежных периодов работы сотрудников. Типичная ситуация: сотрудника увольняют и принимают обратно на следующий день (или через день). В отчете необходимо объединить такие записи в одну строку, показав дату самого первого приема и дату окончательного увольнения.
Однако стандартная группировка с использованием агрегатных функций МИНИМУМ(ДатаПриема) и МАКСИМУМ(ДатаУвольнения) дает сбой, если последний период работы остается открытым. В этом случае дата увольнения в базе данных является пустой (то есть ДАТАВРЕМЯ(1, 1, 1)). Поскольку пустая дата в СУБД считается самой ранней из возможных, функция МАКСИМУМ вернет заполненную дату предыдущего увольнения, что в корне неверно. Вместо открытого периода («работает по настоящее время») мы получим старую дату увольнения.
Проанализируем ситуацию и разберем по шагам три профессиональных способа решения этой задачи: от простого обхода через подмену даты до изящных алгоритмов на чистом языке запросов 1С.
Прежде чем перейти к решениям, выясним причину такого поведения запросов. В платформе 1С пустая дата — это значение 01.01.0001 00:00:00. При использовании группировки:
ДатаУвольнения = 31.01.2019.ДатаУвольнения = 01.01.0001.МАКСИМУМ(ДатаУвольнения) система выбирает наибольшее значение между 31.01.2019 и 01.01.0001. Результатом становится 31.01.2019. В итоге отчет показывает, что сотрудник уволен в январе, хотя он продолжает работать!Рассмотрим подробнее варианты решения этой проблемы.
Самый простой и популярный на форумах способ обойти ограничение функции МАКСИМУМ — временно подменить пустую дату увольнения на заведомо большую дату, которую невозможно встретить в реальных кадровых данных. Традиционно для этого используют конец четвертого тысячелетия — 31.12.3999.
Посмотрим на пример реализации. Сначала мы подготавливаем таблицу значений в коде, заменяя пустые даты увольнения на фиктивную дату. Затем передаем таблицу в запрос в качестве параметра:
// Подготовка данных в таблице значений (ТЗ) перед передачей в запрос
Для Каждого СтрокаДанных Из ДанныеПоСотрудникам Цикл
Если Не ЗначениеЗаполнено(СтрокаДанных.ДатаУвольнения) Тогда
СтрокаДанных.ДатаУвольнения = Дата("39991231235959"); // 31.12.3999 23:59:59
КонецЕсли;
КонецЦикла;
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ТЗсотрудники", ДанныеПоСотрудникам);
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ.Сотрудник КАК Сотрудник,
| ТЗ.ДатаПриема КАК ДатаПриема,
| ТЗ.ДатаУвольнения КАК ДатаУвольнения
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ТЗсотрудники КАК ТЗ
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.Сотрудник КАК Сотрудник,
| МИНИМУМ(ВТ.ДатаПриема) КАК ДатаПриема,
| ВЫБОР
| КОГДА МАКСИМУМ(ВТ.ДатаУвольнения) = ДАТАВРЕМЯ(3999, 12, 31, 23, 59, 59)
| ТОГДА ДАТАВРЕМЯ(1, 1, 1)
| ИНАЧЕ МАКСИМУМ(ВТ.ДатаУвольнения)
| КОНЕЦ КАК ДатаУвольнения
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ
|ГРУППИРОВАТЬ ПО
| ВТ.Сотрудник";
Как это работает: в процессе группировки СУБД выбирает максимальное значение. Так как 3999 год гарантированно больше любой реальной даты увольнения, функция МАКСИМУМ вернет именно его. На выходе из запроса конструкция ВЫБОР производит обратную конвертацию: если дата равна 3999 году, мы возвращаем пустую дату ДАТАВРЕМЯ(1, 1, 1) обратно в результат.
Если вы не хотите модифицировать исходную таблицу значений в коде и использовать фиктивные даты, можно применить красивый логический прием на уровне языка запросов 1С. Он базируется на том, что пустая дата является минимальной.
Если в группе интервалов работы сотрудника присутствует хотя бы один незакрытый период, то значение МИНИМУМ(ДатаУвольнения) по всей группе вернет именно пустую дату ДАТАВРЕМЯ(1, 1, 1). Если же все периоды закрыты, минимум вернет реальную дату увольнения первого периода.
Рассмотрим, как написать такое условие в запросе:
ВЫБРАТЬ
ВТ.Сотрудник КАК Сотрудник,
МИНИМУМ(ВТ.ДатаПриема) КАК ДатаПриема,
ВЫБОР
КОГДА МИНИМУМ(ВТ.ДатаУвольнения) = ДАТАВРЕМЯ(1, 1, 1)
ТОГДА ДАТАВРЕМЯ(1, 1, 1)
ИНАЧЕ МАКСИМУМ(ВТ.ДатаУвольнения)
КОНЕЦ КАК ДатаУвольнения
ИЗ
ВТ_Данные КАК ВТ
ГРУППИРОВАТЬ ПО
ВТ.Сотрудник
Преимущества метода: этот способ не требует предварительной обработки таблицы значений во встроенном языке 1С. Логика полностью переносится на сторону СУБД, что делает выполнение кода более быстрым и чистым.
Часто программисты определяют смежность периодов программно — перебирая строки в цикле и проставляя флаг «Новый период». Однако задачу объединения интервалов с разницей в 1 день (так называемая задача об «островах и промежутках») можно решить полностью в одном запросе без использования циклов.
Разберем по шагам алгоритм слияния периодов, где разница между увольнением и приемом меньше или равна 1 дню:
// 1. Помещаем исходные данные во временную таблицу и обрабатываем пустые даты увольнения
ВЫБРАТЬ
ТЗ.Сотрудник КАК Сотрудник,
ТЗ.ДатаПриема КАК ДатаПриема,
ВЫБОР
КОГДА ТЗ.ДатаУвольнения = ДАТАВРЕМЯ(1, 1, 1)
ТОГДА ДАТАВРЕМЯ(3999, 12, 31)
ИНАЧЕ ТЗ.ДатаУвольнения
КОНЕЦ КАК ДатаУвольнения
ПОМЕСТИТЬ ВТ_Исходная
ИЗ
&ТЗсотрудники КАК ТЗ
;
// 2. Находим начала уникальных (несмежных) периодов.
// Это те приемы на работу, перед которыми нет увольнений со смещением в 0 или 1 день.
ВЫБРАТЬ
Текущий.Сотрудник КАК Сотрудник,
Текущий.ДатаПриема КАК ДатаПриема
ПОМЕСТИТЬ ВТ_НачалаПериодов
ИЗ
ВТ_Исходная КАК Текущий
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Исходная КАК Предыдущий
ПО Текущий.Сотрудник = Предыдущий.Сотрудник
И РАЗНОСТЬДАТ(Предыдущий.ДатаУвольнения, Текущий.ДатаПриема, ДЕНЬ) МЕЖДУ 0 И 1
ГДЕ
Предыдущий.Сотрудник ЕСТЬ NULL
;
// 3. Сопоставляем каждому началу периода его дату окончания.
// Конечной датой будет минимальная дата увольнения, которая больше или равна дате приема начала периода,
// при этом сама не имеет продолжения.
ВЫБРАТЬ
Начала.Сотрудник КАК Сотрудник,
Начала.ДатаПриема КАК ДатаПриема,
ВЫБОР
КОГДА МИНИМУМ(Окончания.ДатаУвольнения) = ДАТАВРЕМЯ(3999, 12, 31)
ТОГДА ДАТАВРЕМЯ(1, 1, 1)
ИНАЧЕ МИНИМУМ(Окончания.ДатаУвольнения)
КОНЕЦ КАК ДатаУвольнения
ИЗ
ВТ_НачалаПериодов КАК Начала
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Исходная КАК Окончания
ПО Начала.Сотрудник = Окончания.Сотрудник
И Окончания.ДатаУвольнения >= Начала.ДатаПриема
// Исключаем даты увольнений, у которых есть продолжение (смежность)
И НЕ Окончания.ДатаУвольнения В (
ВЫБРАТЬ
След.ДатаУвольнения
ИЗ
ВТ_Исходная КАК След
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Исходная КАК Пред
ПО След.Сотрудник = Пред.Сотрудник
И РАЗНОСТЬДАТ(След.ДатаУвольнения, Пред.ДатаПриема, ДЕНЬ) МЕЖДУ 0 И 1
)
ГРУППИРОВАТЬ ПО
Начала.Сотрудник,
Начала.ДатаПриема
Этот подход позволяет исключить тяжелые циклы обхода таблиц значений на сервере приложений. Все вычисления производятся силами СУБД, что критически важно при формировании отчетов по тысячам сотрудников.
После того как мы получили корректные сгруппированные периоды, нужно красиво вывести результат пользователю. Если сотрудник продолжает работать, пустая дата увольнения (01.01.0001) в отчете СКД по умолчанию отобразится пустой строкой. Чтобы сделать отчет более презентабельным, выведем вместо пустой даты текст «работает» или «по н.в.».
Для этого выполним условное оформление в СКД:
ДатаУвольнения.работает.ДатаУвольнения Равно "01.01.0001 00:00:00" (пустая дата).Теперь при формировании отчета СКД автоматически заменит некрасивые пустые значения понятным для кадровиков и руководителей текстом.