При разработке отчетов по движению кандидатов или бизнес-процессам часто возникает классическая проблема: отчет выводит записи, которые уже сменили свой статус, хотя пользователь установил фильтр на конкретное состояние. Рассмотрим ситуацию на примере регистра сведений ТекущаяРаботаПоЗаявкамКандидатов. Основная сложность здесь заключается в том, что обычный фильтр в секции ГДЕ по полю Состояние ищет наличие этого состояния во всей истории изменений, а нам нужно анализировать только последнее актуальное значение.
В этой статье мы разберем, как правильно построить запрос в условиях платформы 1С 8.2, где отсутствует современная функция АВТОНОМЕРЗАПИСИ(), и как корректно рассчитать время нахождения в каждом статусе.
Разберем ситуацию, когда кандидат Бирюкова Анна сначала имела статус "На рассмотрении", а затем перешла в статус "Принята". Если мы просто пишем условие ГДЕ Состояние = &Статус, система находит в истории первую запись Бирюковой и включает ее в отчет. Нам же необходимо сначала определить текущее состояние кандидата и только потом применять фильтр.
Проанализируем типичную ошибку в соединениях. Если вы используете ЛЕВОЕ СОЕДИНЕНИЕ с таблицей статусов, но в секции ГДЕ ставите условие на поле из этой правой таблицы (например, НЕ ТаблицаСтатусов.Кандидат ЕСТЬ NULL), то ваше соединение фактически превращается во внутреннее (Inner Join). Это отсекает данные и часто приводит к неверным результатам при расчете временных интервалов.
Для того чтобы отчет учитывал только тех кандидатов, которые сейчас находятся в нужном статусе, первым делом необходимо получить данные из виртуальной таблицы СрезПоследних. Рассмотрим, как подготовить временную таблицу с текущими статусами:
ВЫБРАТЬ
Рег.ЗаявкаКандидата КАК Кандидат,
Рег.Состояние КАК ТекущееСостояние,
Рег.Период КАК ДатаПоследнегоИзменения
ПОМЕСТИТЬ ВТ_АктуальныеСтатусы
ИЗ
РегистрСведений.ТекущаяРаботаПоЗаявкамКандидатов.СрезПоследних(&ДатаКонец, ) КАК Рег
ГДЕ
НЕ Рег.ЗаявкаКандидата.ПометкаУдаления
;
Используя эту таблицу как базовый фильтр, мы гарантируем, что в итоговый результат попадут только нужные нам объекты.
Поскольку в версии 8.2 нет встроенной нумерации строк в запросе, для расчета разницы во времени между «текущим» и «предыдущим» статусом нам придется использовать метод самосоединения таблиц. Выясним причину, по которой это необходимо: для каждой записи нам нужно найти в базе запись, предшествующую ей по времени для того же самого кандидата.
Проанализируем алгоритм эмуляции нумерации строк:
Таблица1.Период >= Таблица2.Период.Рассмотрим пример кода для подготовки пронумерованной истории:
ВЫБРАТЬ
Рег.Период КАК Период,
Рег.ЗаявкаКандидата КАК Кандидат,
Рег.Состояние КАК Состояние
ПОМЕСТИТЬ ВТ_История
ИЗ
РегистрСведений.ТекущаяРаботаПоЗаявкамКандидатов КАК Рег
ГДЕ
Рег.Период МЕЖДУ &ДатаНачало И &ДатаКонец
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ТаблицаДляСвязи.Период) КАК НомерСтроки,
Таблица.Период КАК Период,
Таблица.Кандидат КАК Кандидат,
Таблица.Состояние КАК Состояние
ПОМЕСТИТЬ ВТ_ИсторияСНомерами
ИЗ
ВТ_История КАК Таблица
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_История КАК ТаблицаДляСвязи
ПО Таблица.Кандидат = ТаблицаДляСвязи.Кандидат
И Таблица.Период >= ТаблицаДляСвязи.Период
СГРУППИРОВАТЬ ПО
Таблица.Кандидат,
Таблица.Период,
Таблица.Состояние
;
Теперь, когда у нас есть порядковые номера для каждого изменения статуса, мы можем легко найти "предыдущую" запись, просто вычтя единицу из номера строки. Разберем этот финальный этап запроса:
ВЫБРАТЬ
ИсторияТекущая.Кандидат КАК Кандидат,
ИсторияТекущая.Состояние КАК Состояние,
ИсторияТекущая.Период КАК ДатаНачала,
ЕСТЬNULL(ИсторияПредыдущая.Период, ИсторияТекущая.Период) КАК ДатаПредыдущая,
РАЗНОСТЬДАТ(ЕСТЬNULL(ИсторияПредыдущая.Период, ИсторияТекущая.Период), ИсторияТекущая.Период, СЕКУНДА) КАК СекундВСтатусе
ИЗ
ВТ_ИсторияСНомерами КАК ИсторияТекущая
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИсторияСНомерами КАК ИсторияПредыдущая
ПО ИсторияТекущая.Кандидат = ИсторияПредыдущая.Кандидат
И ИсторияТекущая.НомерСтроки = (ИсторияПредыдущая.НомерСтроки + 1)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_АктуальныеСтатусы КАК Актуальные
ПО ИсторияТекущая.Кандидат = Актуальные.Кандидат
ГДЕ
(Актуальные.ТекущееСостояние = &СтатусФильтр ИЛИ &СтатусФильтр = ЗНАЧЕНИЕ(Перечисление.СостоянияКандидатов.ПустаяСсылка))
;
Важный момент: использование функции РАЗНОСТЬДАТ с параметром СЕКУНДА позволяет получить максимально точный интервал. Если вам нужно вывести результат в формате "Дни:Часы:Минуты", рекомендуется выполнять деление остатка от секунд в итоговой выборке, а не полагаться на функции ДЕНЬ() или МЕСЯЦ() от пустой даты, так как они могут давать погрешность из-за особенностей календаря.
Метод с использованием КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ...) является ресурсоемким (сложность $O(n^2)$). Если в вашем регистре десятки тысяч записей по одному кандидату, запрос может работать медленно. В этом случае рассмотрим альтернативу: вместо нумерации строк можно соединять записи по условию "Максимальный период, который меньше текущего".
Посмотрим на пример такого соединения:
ВЫБРАТЬ
Т1.Кандидат,
Т1.Период КАК ПериодТекущий,
МАКСИМУМ(Т2.Период) КАК ПериодПредыдущий
ПОМЕСТИТЬ ВТ_СвязиПериодов
ИЗ
ВТ_История КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_История КАК Т2
ПО Т1.Кандидат = Т2.Кандидат
И Т1.Период > Т2.Период
СГРУППИРОВАТЬ ПО
Т1.Кандидат,
Т1.Период
;
Этот способ часто работает быстрее на платформе 8.2, так как он более эффективно использует индексы временных таблиц. Не забудьте проиндексировать временные таблицы по полям Кандидат и Период для ускорения работы соединений.
Подводя итог, для корректной работы отчета мы реализовали следующую логику:
во-первых, выделили текущие состояния кандидатов через СрезПоследних для фильтрации;
во-вторых, подготовили историю изменений с вычислением предыдущих дат;
в-третьих, соединили эти данные для получения итоговой длительности этапов работы.
Такой подход позволяет избежать ситуации, когда "Бирюкова Анна" попадает в отчет по фильтру "На рассмотрении", хотя ее текущий статус уже изменился. Мы обеспечили точность данных и корректность отображения времени обработки заявок.