Разработка и Конфигурация · 12 мин
Как получить предпоследнюю цену в 1С
Как в 1С получить предпоследнюю цену товара через запрос к регистру цен: сортировка по периоду, выбор второй записи, примеры кода, типичные ошибки и проверка результата.
В типовых конфигурациях 1С цены номенклатуры хранятся в периодическом регистре сведений «Цены номенклатуры» (в УТ 11, ERP, КА 2) или «Цены номенклатуры контрагентов». Получить последнюю цену просто — для этого есть виртуальная таблица «СрезПоследних». А вот предпоследнюю цену штатным срезом не достать: потребуется либо запрос с сортировкой по периоду, либо программный обход записей регистра.
Задача получения предпоследней цены возникает при анализе динамики ценообразования: нужно сравнить текущую цену с предыдущей, вычислить процент изменения, найти товары с резким скачком цены или восстановить цену, действовавшую до последнего изменения. Ниже разобраны рабочие способы для запроса и встроенного языка, с ограничениями каждого подхода.
Важный нюанс: «предпоследняя» цена — это не цена на какую-то дату, а вторая по счёту запись при сортировке истории цен от новых к старым. Если цена менялась дважды в один день, порядок внутри дня определяется моментом времени записи (документом-регистратором), и это нужно учитывать при сортировке.
Где хранятся цены и почему срез последних не подходит
В конфигурациях УТ 11, ERP 2, КА 2 цены фиксируются в периодическом регистре сведений «ЦеныНоменклатуры» с измерениями «Номенклатура», «ВидЦены» (и «Характеристика» при использовании характеристик). Регистратором выступает документ «Установка цен номенклатуры». В УТ 10.3 и УПП 1.3 структура похожая, но регистр подчинён регистратору напрямую.
Виртуальная таблица «СрезПоследних» возвращает ровно одну запись на комбинацию измерений — самую позднюю на указанную дату. Никаких параметров «вторая по счёту» у неё нет. Поэтому для предпоследней цены нужно обращаться к физической таблице регистра (РегистрСведений.ЦеныНоменклатуры без уточнения виртуальной таблицы) и работать с полем «Период».
Способ 1: запрос с двойным «ВЫБРАТЬ ПЕРВЫЕ»
Классический приём: сначала выбрать две последние записи по убыванию периода, затем из этих двух взять одну, отсортировав по возрастанию. Это самый компактный вариант для одного товара и одного вида цен.
ВЫБРАТЬ ПЕРВЫЕ 1
Т.Цена КАК ПредпоследняяЦена,
Т.Период КАК ДатаУстановки
ИЗ
(ВЫБРАТЬ ПЕРВЫЕ 2
Цены.Период КАК Период,
Цены.Цена КАК Цена
ИЗ
РегистрСведений.ЦеныНоменклатуры КАК Цены
ГДЕ
Цены.Номенклатура = &Номенклатура
И Цены.ВидЦены = &ВидЦены
УПОРЯДОЧИТЬ ПО
Цены.Период УБЫВ) КАК Т
УПОРЯДОЧИТЬ ПО
Т.Период
Внутренний запрос берёт две самые свежие записи, внешний переворачивает их и оставляет более раннюю из пары — это и есть предпоследняя цена. Если в истории всего одна запись, результат будет пустым, и это следует обрабатывать в коде.
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 Т.Цена ИЗ (ВЫБРАТЬ ПЕРВЫЕ 2 Цены.Период, Цены.Цена ИЗ РегистрСведений.ЦеныНоменклатуры КАК Цены ГДЕ Цены.Номенклатура = &Номенклатура И Цены.ВидЦены = &ВидЦены УПОРЯДОЧИТЬ ПО Цены.Период УБЫВ) КАК Т УПОРЯДОЧИТЬ ПО Т.Период";
Запрос.УстановитьПараметр("Номенклатура", Товар);
Запрос.УстановитьПараметр("ВидЦены", ВидЦены);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
ПредпоследняяЦена = Выборка.Цена;
Иначе
ПредпоследняяЦена = Неопределено; // истории изменений нет
КонецЕсли;Способ 2: предпоследние цены сразу для списка товаров
В отчётах обычно нужна предпоследняя цена не для одного товара, а для всей номенклатуры. Построчный обход с запросом в цикле — плохое решение по производительности. Правильный подход — один запрос с нумерацией записей через соединение таблицы самой с собой или через пакет с временными таблицами.
Практичный вариант: сначала найти две максимальные даты установки цены по каждому товару, затем соединиться с регистром по второй дате. Для поиска второй максимальной даты используют группировку по максимальному периоду среди записей, которые меньше общего максимума.
ВЫБРАТЬ
Цены.Номенклатура КАК Номенклатура,
Цены.ВидЦены КАК ВидЦены,
МАКСИМУМ(Цены.Период) КАК ПредпоследнийПериод
ПОМЕСТИТЬ ВТ_Периоды
ИЗ
РегистрСведений.ЦеныНоменклатуры КАК Цены
ВНУТРЕННЕЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
Т.Номенклатура КАК Номенклатура,
Т.ВидЦены КАК ВидЦены,
МАКСИМУМ(Т.Период) КАК МаксПериод
ИЗ
РегистрСведений.ЦеныНоменклатуры КАК Т
СГРУППИРОВАТЬ ПО
Т.Номенклатура, Т.ВидЦены) КАК Последние
ПО Цены.Номенклатура = Последние.Номенклатура
И Цены.ВидЦены = Последние.ВидЦены
И Цены.Период < Последние.МаксПериод
СГРУППИРОВАТЬ ПО
Цены.Номенклатура,
Цены.ВидЦены
;
ВЫБРАТЬ
П.Номенклатура,
П.ВидЦены,
Цены.Цена КАК ПредпоследняяЦена,
П.ПредпоследнийПериод КАК ДатаУстановки
ИЗ
ВТ_Периоды КАК П
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК Цены
ПО Цены.Номенклатура = П.Номенклатура
И Цены.ВидЦены = П.ВидЦены
И Цены.Период = П.ПредпоследнийПериод
Такой запрос выполняется одним обращением к базе и подходит для отчётов на СКД. Минус — при очень больших объёмах регистра соединение с самим собой может быть тяжёлым; в этом случае ограничивайте выборку отбором по нужной номенклатуре или периоду.
Способ 3: программный обход выборки
Когда логика сложнее, чем «вторая запись» — например, нужно пропустить нулевые цены или записи конкретного автора — удобнее обойти выборку в коде. Запрос возвращает историю по убыванию периода, а в цикле считаем записи и берём вторую подходящую.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Цены.Период, Цены.Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры КАК Цены
|ГДЕ
| Цены.Номенклатура = &Номенклатура
| И Цены.ВидЦены = &ВидЦены
|УПОРЯДОЧИТЬ ПО
| Цены.Период УБЫВ";
Запрос.УстановитьПараметр("Номенклатура", Товар);
Запрос.УстановитьПараметр("ВидЦены", ВидЦены);
Выборка = Запрос.Выполнить().Выбрать();
НомерЗаписи = 0;
ПредпоследняяЦена = Неопределено;
Пока Выборка.Следующий() Цикл
НомерЗаписи = НомерЗаписи + 1;
Если НомерЗаписи = 2 Тогда
ПредпоследняяЦена = Выборка.Цена;
Прервать;
КонецЕсли;
КонецЦикла;
Этот вариант проще отлаживать и легко модифицировать: можно взять не вторую, а третью запись, отфильтровать записи по условию или заодно собрать всю историю изменений для анализа. Для одиночных обращений по производительности он сопоставим с запросным способом.
- 1Определите регистр, в котором хранятся цены в вашей конфигурации (ЦеныНоменклатуры или ЦеныНоменклатурыКонтрагентов)
- 2Напишите запрос к физической таблице регистра с отбором по номенклатуре и виду цен
- 3Добавьте упорядочивание по периоду по убыванию и ограничение ВЫБРАТЬ ПЕРВЫЕ 2
- 4Оберните во внешний запрос с ВЫБРАТЬ ПЕРВЫЕ 1 и обратной сортировкой или обработайте выборку в коде
- 5Проверьте результат на товаре с известной историей изменения цен
Сравнение подходов и выбор варианта
Выбор способа зависит от контекста: разовая проверка, отчёт по всей номенклатуре или сложная бизнес-логика. Все три варианта корректны, различаются производительностью и гибкостью.
Сравнение вариантов
Лучший вариант для одной пары товар/вид цены. Компактно и быстро, но не масштабируется на списки товаров без цикла
Подходит для отчётов и СКД по всей номенклатуре одним запросом. Тяжелее для СУБД на больших регистрах
Максимальная гибкость: фильтры, пропуски записей, сбор истории. Неудобен для массовой обработки в цикле
Проверка результата и контрольные точки
Проверить правильность проще всего вручную: откройте карточку товара, перейдите к истории цен (в УТ 11 — панель навигации «Цены номенклатуры») и сравните вторую сверху запись с результатом запроса. Даты и значения должны совпасть.
Как проверить результат
Предпоследняя цена из запроса равна второй строке в истории цен карточки товара
Запрос вернул текущую цену — вероятно, сортировка идёт по возрастанию или взят срез последних
Запрос возвращает пустой результат, код присваивает Неопределено
Возвращается единственная цена как предпоследняя — логическая ошибка, сравнение динамики будет неверным
Порядок определяется регистратором, выбрана корректная запись
Результат нестабилен при повторных выполнениях — не хватает поля сортировки
Типичные ошибки и ограничения
Ещё одно ограничение касается прав доступа: если у пользователя нет права чтения регистра цен, запрос выполнится с ошибкой или вернёт пустой результат при RLS. В отчётах для менеджеров без доступа к ценам закупки это частая причина «не работающего» запроса.
Если предпоследняя цена нужна регулярно — например, для ежедневного контроля изменений прайса — удобно оформить запрос как функцию в общем модуле с параметрами «Номенклатура» и «ВидЦены», возвращающую структуру с ценой и датой. Это избавит от дублирования текста запроса в отчётах и обработках.
Практический сценарий: процент изменения цены
Частая прикладная задача — показать в отчёте текущую цену, предпоследнюю и процент изменения. Для этого в запрос соединяются срез последних (текущая цена) и описанный выше механизм получения предпоследней. Процент считается как (Текущая − Предпоследняя) / Предпоследняя × 100, с защитой от деления на ноль через ВЫБОР.
При этом товары без истории изменений (одна запись в регистре) корректно показывать с пустой колонкой изменения, а не с нулевым процентом — иначе новые позиции будут смешаны с товарами, цена которых действительно не изменилась.
Что важно запомнить
Предпоследнюю цену получают из физической таблицы регистра цен с сортировкой по периоду по убыванию: для одного товара — двойным ВЫБРАТЬ ПЕРВЫЕ, для списка — запросом с группировкой по второй максимальной дате, для сложной логики — обходом выборки. Всегда обрабатывайте случай единственной записи в истории и добавляйте регистратор в сортировку при изменениях внутри одного дня.
Частые вопросы
Можно ли получить предпоследнюю цену через СрезПоследних?
Напрямую нет — виртуальная таблица всегда возвращает одну самую позднюю запись. Обходной приём: взять срез последних на дату, предшествующую последнему изменению, но для этого дату изменения всё равно нужно сначала найти запросом к физической таблице, поэтому проще сразу работать с ней.
Что вернёт запрос, если цена товара устанавливалась только один раз?
Пустой результат: внутренний запрос найдёт одну запись, и «второй» записи не существует. В коде это нужно обрабатывать — присваивать Неопределено или 0 и отдельно помечать такие товары в отчётах, чтобы не смешивать их с реальными изменениями цены.
Как учесть характеристики номенклатуры в запросе?
Добавьте в отбор и группировку поле «Характеристика» — в УТ 11 и ERP оно является измерением регистра цен. Если характеристики не используются, поле можно опустить, но при включённой настройке без отбора по характеристике вы рискуете получить запись по другой характеристике того же товара.
Почему запрос возвращает текущую цену вместо предпоследней?
Чаще всего перепутано направление сортировки: внутренний запрос должен сортировать по убыванию периода (сначала новые), а внешний — по возрастанию. Также проверьте, что вы не обращаетесь случайно к виртуальной таблице СрезПоследних вместо физической таблицы регистра.
Как получить не вторую, а третью или четвёртую запись истории?
В запросе с двойным «ВЫБРАТЬ ПЕРВЫЕ» измените внутреннее ограничение на нужное число (например, ПЕРВЫЕ 3), а внешний запрос оставьте с ПЕРВЫЕ 1 и обратной сортировкой — получите самую раннюю из трёх последних, то есть третью по новизне запись. При программном обходе просто измените номер искомой записи в условии цикла.
Работает ли этот подход в 1С:Бухгалтерии 8?
В БП 3.0 нет полноценного регистра цен с историей как в УТ — цена номенклатуры хранится иначе (в типовых механизмах установки цен через документ «Установка цен номенклатуры» с регистром сведений). Если регистр цен присутствует, подход с физической таблицей и сортировкой по периоду применим без изменений; уточните имя регистра в конфигураторе вашей базы.