При разработке отчетов или алгоритмов, отслеживающих движение номенклатуры, мы часто сталкиваемся с задачей определения "последнего места пребывания" товара. Казалось бы, задача тривиальная: взять последнюю запись из регистра накопления. Однако, когда дело доходит до документов ПеремещениеТоваров, возникает техническая коллизия.
Рассмотрим ситуацию подробно: документ перемещения делает две записи в регистр одновременно. Одна запись списывает товар со склада-отправителя (Расход), вторая — приходует на склад-получатель (Приход). У обеих записей поле Период абсолютно идентично (оно равно дате и времени документа). При использовании стандартной группировки через МАКСИМУМ(Период) в выборку попадают обе записи, что ломает логику отчета.
Давайте разберем, как правильно составить запрос, чтобы получить единственно верное "последнее" движение и корректно определить текущий склад.
Когда мы обращаемся к физической таблице регистра (например, РегистрНакопления.ТоварыНаСкладах), мы видим хронологию событий. Если мы попытаемся найти последнее состояние, используя только дату, мы столкнемся с дублями.
Проанализируем типичный запрос, который приводит к ошибке:
ВЫБРАТЬ
МАКСИМУМ(Товары.Период) КАК Период,
Товары.Номенклатура
ПОМЕСТИТЬ ВТ_МаксДаты
ИЗ
РегистрНакопления.ТоварыНаСкладах КАК Товары
СГРУППИРОВАТЬ ПО
Товары.Номенклатура;
// Дальнейшее соединение с таблицей регистра по Периоду и Номенклатуре
// вернет ДВЕ строки для Перемещения, так как у Прихода и Расхода период равен найденному максимуму.
В документе перемещения логика следующая: товар уходит со склада А и приходит на склад Б. Если наша цель — узнать, где товар находится в итоге, нам нужна запись именно с видом движения Приход.
Самый логичный способ отфильтровать лишнюю строку — использовать системное поле ВидДвижения. В платформе 1С это перечисление, содержащее значения Приход и Расход.
Если мы ищем "последний склад", то приоритет должен быть у Прихода. То есть, если в одну секунду товар и ушел, и пришел, значит, он сейчас "пришел".
Мы можем использовать сортировку внутри запроса. Давайте составим алгоритм, который выбирает одну запись (ПЕРВЫЕ 1) для конкретной номенклатуры, упорядочивая их так, чтобы "Приход" оказывался выше "Расхода" при прочих равных.
В запросе это можно реализовать через конструкцию ВЫБОР или просто полагаясь на порядок сортировки:
ВЫБРАТЬ ПЕРВЫЕ 1
Товары.Склад,
Товары.Период,
Товары.ВидДвижения
ИЗ
РегистрНакопления.ТоварыНаСкладах КАК Товары
ГДЕ
Товары.Номенклатура = &Номенклатура
УПОРЯДОЧИТЬ ПО
Товары.Период УБЫВ,
Товары.МоментВремени УБЫВ,
Товары.ВидДвижения УБЫВ
Обратите внимание на поле ВидДвижения. Мы сортируем его по убыванию. Это сработает, если мы хотим отдать приоритет одному из видов. Однако надежнее явно указать приоритет числом, как предлагалось в обсуждении:
ВЫБРАТЬ ПЕРВЫЕ 1
Товары.Склад
ИЗ
РегистрНакопления.ТоварыНаСкладах КАК Товары
ГДЕ
Товары.Номенклатура = &Номенклатура
УПОРЯДОЧИТЬ ПО
Товары.Период УБЫВ,
Товары.МоментВремени УБЫВ,
// Присвоим Приходу вес 1, Расходу 0. Сортируем по убыванию, чтобы Приход был первым.
ВЫБОР КОГДА Товары.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход) ТОГДА 1 ИНАЧЕ 0 КОНЕЦ УБЫВ
Иногда сортировки по виду движения недостаточно. Представим, что в одном документе есть несколько строк с одной и той же номенклатурой (например, разные характеристики или серии), и нам нужно найти технически последнюю запись, которую сделала система.
В этом случае нам поможет поле НомерСтроки. Это системное поле таблицы движений регистра. Уникальный ключ записи регистра накопления состоит из:
ПериодРегистратор (Ссылка на документ)НомерСтрокиПоле МоментВремени, которое часто рекомендуют использовать, содержит в себе Дату и Ссылку на документ. Но внутри одного документа (одного регистратора) МоментВремени у всех движений одинаковый. Поэтому единственным критерием для определения порядка записей внутри документа является НомерСтроки.
Давайте сформируем запрос, который найдет абсолютно последнюю запись для списка товаров. Для этого используем коррелированный подзапрос или соединение (для оптимизации на больших объемах лучше использовать временные таблицы, но для понимания логики рассмотрим пример с соединением).
Шаг 1. Найдем максимальный Момент Времени для каждой номенклатуры.
Почему МоментВремени, а не Период? Потому что в одну секунду (Период) может быть введено два разных документа. МоментВремени позволит их различить и выстроить в хронологическом порядке.
Шаг 2. Найдем максимальный Номер Строки.
Если мы нашли документ с максимальным моментом времени, внутри него нам нужно взять запись с максимальным номером строки. Это гарантированно вернет одну запись, даже если это перемещение.
Пример оптимизированного подхода через временные таблицы:
// 1. Получаем макс. Период (для предварительного отсечения, оптимизация индекса)
ВЫБРАТЬ
Т.Номенклатура,
МАКСИМУМ(Т.Период) КАК Период
ПОМЕСТИТЬ ВТ_МаксПериоды
ИЗ
РегистрНакопления.ТоварыНаСкладах КАК Т
СГРУППИРОВАТЬ ПО
Т.Номенклатура;
////////////////////////////////////////////////////////////////////////////////
// 2. Получаем макс. Регистратор (МоментВремени) внутри этого периода
// Это нужно, если в одну секунду было два документа
ВЫБРАТЬ
Т.Номенклатура,
Т.Период,
МАКСИМУМ(Т.Регистратор) КАК Регистратор
ПОМЕСТИТЬ ВТ_МаксРегистратор
ИЗ
ВТ_МаксПериоды КАК МаксПериоды
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах КАК Т
ПО МаксПериоды.Номенклатура = Т.Номенклатура
И МаксПериоды.Период = Т.Период
СГРУППИРОВАТЬ ПО
Т.Номенклатура,
Т.Период;
////////////////////////////////////////////////////////////////////////////////
// 3. Получаем макс. НомерСтроки внутри этого Регистратора
// Это решает проблему Прихода/Расхода в Перемещении
ВЫБРАТЬ
Т.Номенклатура,
Т.Период,
Т.Регистратор,
МАКСИМУМ(Т.НомерСтроки) КАК НомерСтроки
ПОМЕСТИТЬ ВТ_ФинальныйКлюч
ИЗ
ВТ_МаксРегистратор КАК МаксРег
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах КАК Т
ПО МаксРег.Номенклатура = Т.Номенклатура
И МаксРег.Регистратор = Т.Регистратор
СГРУППИРОВАТЬ ПО
Т.Номенклатура,
Т.Период,
Т.Регистратор;
////////////////////////////////////////////////////////////////////////////////
// 4. Выбираем конечные данные (Склад) по полному ключу
ВЫБРАТЬ
Т.Номенклатура,
Т.Склад,
Т.Период
ИЗ
ВТ_ФинальныйКлюч КАК Ключ
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах КАК Т
ПО Ключ.Номенклатура = Т.Номенклатура
И Ключ.Регистратор = Т.Регистратор
И Ключ.НомерСтроки = Т.НомерСтроки;
Если ваша задача — просто узнать, "где товар лежит сейчас", то анализ таблицы движений (оборотов) может быть излишним усложнением. Рассмотрим более простой вариант — использование виртуальной таблицы Остатки.
Если товар находится в наличии, он числится на остатках определенного склада. Запрос к таблице остатков автоматически учитывает все приходы и расходы и выдает итоговый результат.
ВЫБРАТЬ
ТоварыОстатки.Склад,
ТоварыОстатки.Номенклатура,
ТоварыОстатки.ВНаличииОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки( , Номенклатура = &Номенклатура) КАК ТоварыОстатки
Важно: Этот способ работает идеально, если товар всегда находится только на одном складе. Если же остаток "размазан" по складам, или вам нужно знать последний склад даже для товара, которого уже нет (остаток 0), тогда метод с анализом движений, описанный выше (через НомерСтроки и ВидДвижения), является единственно верным.
Подведем итог. Чтобы выбрать единственную последнюю запись из регистра накопления при наличии документов перемещения:
Период. В 1С это поле не уникально.ВидаДвижения (Приход важнее Расхода для определения местоположения) или НомерСтроки (максимальный номер строки — это последняя записанная операция).Остатки.Период -> МоментВремени (Регистратор) -> НомерСтроки.