Как выбрать последнюю запись из непериодического регистра сведений в 1С?

Программист 1С v8.3 (Управляемые формы) 1С:Зарплата и Управление Персоналом
← На главную

В процессе разработки в среде 1С программисты регулярно сталкиваются с необходимостью получения наиболее актуальной (последней) записи из регистров сведений. Если регистр является периодическим, задача решается тривиально — с помощью виртуальной таблицы СрезПоследних. Однако, если перед нами непериодический регистр сведений, стандартной виртуальной таблицы среза у него нет.

Классический пример такой задачи в конфигурациях «1С:Зарплата и управление персоналом 3.1», «1С:ERP Управление предприятием» и «1С:Комплексная автоматизация» — работа с регистром сведений ЛицевыеСчетаСотрудниковПоЗарплатнымПроектам. В этом непериодическом регистре хранятся сведения о банковских картах физических лиц. Поскольку у одного человека за время его работы может быть открыто несколько лицевых счетов в рамках одного или разных зарплатных проектов, нам часто требуется программно определить, какая карта является последней и актуальной на текущий момент. Рассмотрим подробно, как решить эту задачу с помощью механизмов запросов 1С.

В чем заключается сложность прямого запроса?

Основная ошибка начинающих разработчиков заключается в попытке выбрать все поля регистра (например, сотрудника, зарплатный проект и дату открытия) и наложить функцию МАКСИМУМ на дату открытия в одном простом запросе с группировкой. Проанализируем, почему такой подход не работает.

Если мы добавим в поля выборки и группировки измерение ЗарплатныйПроект, СУБД будет группировать записи по уникальным комбинациям физического лица и зарплатного проекта. В результате, если у сотрудника есть две карты по разным зарплатным проектам (например, старая карта «ВТБ» и новая «Газпромбанк»), запрос вернет обе записи, показав максимальную дату для каждой из них отдельно. Наша же цель — получить строго одну, самую последнюю запись для каждого человека.

Для корректного решения нам потребуется использовать пакетный запрос и временные таблицы.

Шаг 1. Классическое решение через пакетный запрос (Поиск по максимальной дате)

Разберем по шагам наиболее распространенный и эффективный алгоритм получения последней записи, основанный на анализе максимальной даты открытия лицевого счета. Мы построим пакетный запрос из трех последовательных шагов.

Шаг 1.1. Получение среза максимальных дат во временную таблицу

Сначала нам необходимо определить для каждого физического лица дату открытия его самого последнего лицевого счета. Для этого мы выбираем физических лиц и применяем агрегатную функцию МАКСИМУМ к полю ДатаОткрытияЛицевогоСчета. Результат мы помещаем во временную таблицу ВТЛицевыеСчета.

Шаг 1.2. Получение информации о зарплатном проекте

На втором шаге мы соединяем полученную временную таблицу ВТЛицевыеСчета с исходным регистром сведений по двум ключевым полям: ФизическоеЛицо и ДатаОткрытияЛицевогоСчета. Это позволяет нам вытащить соответствующий этой максимальной дате зарплатный проект.

Шаг 1.3. Итоговая выборка и сопоставление данных

На финальном этапе мы выводим данные для сопоставления с исходным списком сотрудников (например, переданным из документа ведомости), проверяя соответствие текущего зарплатного проекта последней открытой карте.

Посмотрим на практический пример реализации этого алгоритма в коде:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ЛицевыеСчета.ФизическоеЛицо КАК ФизическоеЛицо,
    |	МАКСИМУМ(ЛицевыеСчета.ДатаОткрытияЛицевогоСчета) КАК ДатаОткрытияЛицевогоСчета
    |ПОМЕСТИТЬ ВТ_МаксимальныеДаты
    |ИЗ
    |	РегистрСведений.ЛицевыеСчетаСотрудниковПоЗарплатнымПроектам КАК ЛицевыеСчета
    |ГДЕ
    |	ЛицевыеСчета.ФизическоеЛицо В(&ФизическиеЛица)
    |
    |СГРУППИРОВАТЬ ПО
    |	ЛицевыеСчета.ФизическоеЛицо
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ЛицевыеСчета.ФизическоеЛицо КАК ФизическоеЛицо,
    |	ЛицевыеСчета.ЗарплатныйПроект КАК ЗарплатныйПроект,
    |	ЛицевыеСчета.ДатаОткрытияЛицевогоСчета КАК ДатаОткрытияЛицевогоСчета,
    |	ЛицевыеСчета.НомерЛицевогоСчета КАК НомерЛицевогоСчета
    |ПОМЕСТИТЬ ВТ_ПоследниеЛС
    |ИЗ
    |	ВТ_МаксимальныеДаты КАК МаксДаты
    |		ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЛицевыеСчетаСотрудниковПоЗарплатнымПроектам КАК ЛицевыеСчета
    |		ПО МаксДаты.ФизическоеЛицо = ЛицевыеСчета.ФизическоеЛицо
    |			И МаксДаты.ДатаОткрытияЛицевогоСчета = ЛицевыеСчета.ДатаОткрытияЛицевогоСчета
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ВТ_ПоследниеЛС.ФизическоеЛицо КАК ФизическоеЛицо,
    |	ВТ_ПоследниеЛС.ЗарплатныйПроект КАК ЗарплатныйПроект,
    |	ВТ_ПоследниеЛС.НомерЛицевогоСчета КАК НомерЛицевогоСчета
    |ИЗ
    |	ВТ_ПоследниеЛС КАК ВТ_ПоследниеЛС";

Запрос.УстановитьПараметр("ФизическиеЛица", СписокФизическихЛиц);
РезультатЗапроса = Запрос.Выполнить();

Каверзный нюанс: Проблема дублирования дат

Предложенный выше классический метод отлично работает в большинстве стандартных ситуаций. Однако в реальной практике регулярно случаются исключения, которые могут полностью нарушить логику работы этого запроса и привести к повторному удвоению строк в выборке. Выясним причину этой проблемы.

Существует две распространенные ситуации, когда у одного физического лица может быть зарегистрировано несколько лицевых счетов с абсолютно одинаковой датой открытия:

  1. Массовый переход на новый зарплатный проект (миграция): При переходе предприятия в другой банк лицевые счета сотен сотрудников создаются единовременно путем автоматического импорта банковского реестра. В этом случае у сотрудников создаются новые записи с той же датой открытия, что и у старых действующих карт.
  2. Человеческий фактор: При ручном переносе исторических данных из старых баз или при отсутствии точных сведений бухгалтеры нередко оставляют поле даты открытия незаполненным (пустая дата) либо указывают одинаковую "техническую" дату для всех карт сотрудника (например, первый день текущего года).

Если у сотрудника окажется два разных лицевых счета с одинаковой максимальной датой, внутреннее соединение во втором запросе пакета вернет две строки вместо одной. Чтобы гарантированно избежать этого, мы должны применить дополнительное искусственное поле группировки.

Рассмотрим, как усовершенствовать наш пакетный запрос для полной защиты от дублей при совпадении дат открытия:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ЛицевыеСчета.ФизическоеЛицо КАК ФизическоеЛицо,
    |	МАКСИМУМ(ЛицевыеСчета.ДатаОткрытияЛицевогоСчета) КАК ДатаОткрытияЛицевогоСчета
    |ПОМЕСТИТЬ ВТ_МаксДаты
    |ИЗ
    |	РегистрСведений.ЛицевыеСчетаСотрудниковПоЗарплатнымПроектам КАК ЛицевыеСчета
    |ГДЕ
    |	ЛицевыеСчета.ФизическоеЛицо В(&ФизическиеЛица)
    |
    |СГРУППИРОВАТЬ ПО
    |	ЛицевыеСчета.ФизическоеЛицо
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ЛицевыеСчета.ФизическоеЛицо КАК ФизическоеЛицо,
    |	ЛицевыеСчета.ДатаОткрытияЛицевогоСчета КАК ДатаОткрытияЛицевогоСчета,
    |	МАКСИМУМ(ЛицевыеСчета.ЗарплатныйПроект) КАК ЗарплатныйПроект
    |ПОМЕСТИТЬ ВТ_УникальныйПроект
    |ИЗ
    |	ВТ_МаксДаты КАК МаксДаты
    |		ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЛицевыеСчетаСотрудниковПоЗарплатнымПроектам КАК ЛицевыеСчета
    |		ПО МаксДаты.ФизическоеЛицо = ЛицевыеСчета.ФизическоеЛицо
    |			И МаксДаты.ДатаОткрытияЛицевогоСчета = ЛицевыеСчета.ДатаОткрытияЛицевогоСчета
    |
    |СГРУППИРОВАТЬ ПО
    |	ЛицевыеСчета.ФизическоеЛицо,
    |	ЛицевыеСчета.ДатаОткрытияЛицевогоСчета
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ЛицевыеСчета.ФизическоеЛицо КАК ФизическоеЛицо,
    |	ЛицевыеСчета.ЗарплатныйПроект КАК ЗарплатныйПроект,
    |	ЛицевыеСчета.НомерЛицевогоСчета КАК НомерЛицевогоСчета
    |ИЗ
    |	ВТ_УникальныйПроект КАК УникальныйПроект
    |		ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЛицевыеСчетаСотрудниковПоЗарплатнымПроектам КАК ЛицевыеСчета
    |		ПО УникальныйПроект.ФизическоеЛицо = ЛицевыеСчета.ФизическоеЛицо
    |			И УникальныйПроект.ДатаОткрытияЛицевогоСчета = ЛицевыеСчета.ДатаОткрытияЛицевогоСчета
    |			И УникальныйПроект.ЗарплатныйПроект = ЛицевыеСчета.ЗарплатныйПроект";

Запрос.УстановитьПараметр("ФизическиеЛица", СписокФизическихЛиц);
РезультатЗапроса = Запрос.Выполнить();

В этом улучшенном варианте во второй временной таблице ВТ_УникальныйПроект мы применили группировку по физлицу и дате, выбрав максимальный зарплатный проект с помощью МАКСИМУМ(ЗарплатныйПроект). Поскольку ссылки на справочники сравниваются на уровне СУБД по их уникальным идентификаторам (UUID), это гарантирует выбор ровно одного проекта, исключая любое возможное дублирование строк.

Шаг 2. Бизнес-ориентированный подход 1С (Рекомендуемый метод)

Несмотря на техническую корректность манипуляций с датами открытия, в типовых конфигурациях 1С (ЗУП 3, ERP, КА) бизнес-логика определения активного лицевого счета устроена несколько иначе. Понятие «самая последняя по дате ввода карта» далеко не всегда эквивалентно понятию «карта, на которую в данный момент бухгалтерия должна перечислять заработную плату».

У сотрудника может быть открыто несколько действующих карт, но активным местом выплаты является только одно. Чтобы узнать, куда действительно необходимо отправлять средства, система использует специализированные регистры мест выплаты зарплаты.

Проанализируем архитектурно правильный способ связывания данных:

  1. Определение места выплаты сотрудника: Первым делом мы обращаемся к непериодическому регистру сведений МестаВыплатыЗарплатыСотрудников. Значение ресурса МестоВыплаты содержит ссылку на активный зарплатный проект сотрудника.
  2. Учет механизма наследования («Как всем»): Если индивидуальное место выплаты для сотрудника не заполнено, система 1С проверяет настройки более высокого уровня: сначала регистр МестаВыплатыЗарплатыПодразделений (проект для конкретного отдела), а затем МестаВыплатыЗарплатыОрганизаций (общий зарплатный проект компании).
  3. Получение лицевого счета по активному проекту: Получив актуальный для сотрудника зарплатный проект, мы соединяем его с регистром ЛицевыеСчетаСотрудниковПоЗарплатнымПроектам по двум полям: ФизическоеЛицо и полученному ЗарплатномуПроекту.

Этот подход на 100% страхует разработчика от ошибок с дублированием дат открытия лицевых счетов и полностью совпадает со стандартными алгоритмами заполнения банковских ведомостей в типовых конфигурациях 1С.

← На главную