Задача: последние цены номенклатуры

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

СрезПоследних — виртуальная таблица регистра сведений, которая по каждой комбинации измерений возвращает одну, самую свежую запись на указанную дату. Идеально для вопроса «какая сейчас цена у этого товара».

Цены в 1С обычно хранятся в периодическом регистре сведений «Цены номенклатуры»: измерение — Номенклатура (иногда ещё ВидЦены), ресурс — Цена, и у каждой записи есть дата (период). За месяц у одного товара может накопиться десяток записей — цена менялась. Нам же нужна последняя действующая. Собеседующий проверяет три навыка: знаете ли вы виртуальную таблицу СрезПоследних, умеете ли соединять её со справочником номенклатуры и что делаете с товарами, у которых цены нет вообще.

Шаг 1. СрезПоследних — берём последнюю цену

Наивный подход — сгруппировать записи и взять максимум по дате — работает плохо и пишется долго. Платформа уже умеет отдавать «последнее значение на дату» через виртуальную таблицу СрезПоследних:

ВЫБРАТЬ
    Цены.Номенклатура КАК Номенклатура,
    Цены.Цена КАК Цена
ИЗ
    РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ДатаЦены, ) КАК Цены

Первый параметр СрезПоследнихПериод: дата, на которую берём актуальную цену (часто это дата документа — «цена, действовавшая в момент отгрузки»). Второй параметр — условие отбора (например, по конкретному виду цены), мы оставили его пустым. По каждой номенклатуре таблица вернёт ровно одну строку — с самой поздней записью не позже указанной даты.

Шаг 2. Соединяем с номенклатурой

Пока у нас цены есть только по тем товарам, для которых вообще заводили цену. Но задача обычно звучит как «выведи весь список номенклатуры и её текущую цену». Значит, отталкиваться надо от справочника Номенклатура, а цену «подтягивать» к нему. Это работа для левого соединения (ЛЕВОЕ СОЕДИНЕНИЕ): берём все строки левой таблицы (номенклатуру), и к ним — подходящие строки правой (цены), а где пары нет — будет пусто.

ВЫБРАТЬ
    Номенклатура.Ссылка КАК Номенклатура,
    Номенклатура.Наименование КАК Наименование,
    Цены.Цена КАК Цена
ИЗ
    Справочник.Номенклатура КАК Номенклатура
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ДатаЦены, ) КАК Цены
    ПО Цены.Номенклатура = Номенклатура.Ссылка
ГДЕ
    НЕ Номенклатура.ПометкаУдаления
    И НЕ Номенклатура.ЭтоГруппа

Порядок таблиц важен: слева — Справочник.Номенклатура (её берём целиком), справа — срез цен. Условие соединения ПО Цены.Номенклатура = Номенклатура.Ссылка связывает записи по товару. В ГДЕ мы заодно отсекли помеченные на удаление позиции и группы (папки) справочника — в отчёте по ценам они не нужны.

Шаг 3. ЕСТЬNULL — обрабатываем отсутствующие цены

Вот главный момент задачи. Для товаров, у которых цены в регистре нет, левое соединение подставит в поле Цена специальное значение NULL — «данных нет». Если оставить как есть, отчёт покажет пустую ячейку, а при попытке что-то посчитать (например, Цена * Количество) результат тоже станет NULL, и итог «поедет». Правильно — заменить отсутствующую цену на понятное значение, обычно на 0. Для этого есть функция ЕСТЬNULL(значение, замена): если первый аргумент NULL, вернётся второй.

ВЫБРАТЬ
    Номенклатура.Ссылка КАК Номенклатура,
    Номенклатура.Наименование КАК Наименование,
    ЕСТЬNULL(Цены.Цена, 0) КАК Цена
ИЗ
    Справочник.Номенклатура КАК Номенклатура
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ДатаЦены, ) КАК Цены
    ПО Цены.Номенклатура = Номенклатура.Ссылка
ГДЕ
    НЕ Номенклатура.ПометкаУдаления
    И НЕ Номенклатура.ЭтоГруппа
УПОРЯДОЧИТЬ ПО
    Наименование

Теперь у товаров без цены будет честный 0, а не пустота, и любые дальнейшие расчёты отработают корректно.

Результат:

Гвозди 3мм       45,00
Доска 40х150     820,00
Новый товар      0,00
Саморез 4х50     3,50

Как это работает

Разберём по частям. СрезПоследних(&ДатаЦены, ) внутри себя для каждой номенклатуры находит запись с максимальной датой, не превышающей переданный Период, и возвращает её — то есть отдаёт готовую «последнюю цену на дату». ЛЕВОЕ СОЕДИНЕНИЕ гарантирует, что в результат попадут все товары из справочника, даже те, для кого среза нет: недостающие поля правой таблицы заполнятся NULL. А ЕСТЬNULL перехватывает эти NULL и подменяет их нулём.

Почему важно именно левое соединение, а не внутреннее (ВНУТРЕННЕЕ СОЕДИНЕНИЕ)? Внутреннее оставило бы только те товары, у которых цена есть — новинки без цены просто выпали бы из отчёта, и никто бы не заметил, что их забыли оценить. Левое соединение показывает всех и подсвечивает пробелы нулём.

Частые ошибки

Сравнивают с NULL через знак равенства. Условие ГДЕ Цены.Цена = NULL не работает: NULL не равен ничему, даже самому себе. Для проверки «пусто ли значение» есть отдельная конструкция ЕСТЬ NULL (например, ГДЕ Цены.Цена ЕСТЬ NULL), а для подстановки замены — функция ЕСТЬNULL(...). Не путайте эти две вещи.

Берут внутреннее соединение вместо левого. Тогда товары без цены молча исчезают из отчёта. Классическая ошибка, из-за которой «пропадает часть номенклатуры», а причину ищут часами. Если нужен весь список — только ЛЕВОЕ СОЕДИНЕНИЕ.

Ставят справочник справа, а срез слева. Тогда «главной» становится таблица цен, и товары без записей всё равно выпадают. При левом соединении целиком берётся левая таблица — значит, слева должен быть тот источник, который нужен полностью (здесь — Справочник.Номенклатура).

Забывают Период у СрезПоследних. Если нужна цена на дату документа, а Период не передали, платформа вернёт самую свежую цену вообще — это может быть цена «из будущего» относительно документа. Для исторических расчётов всегда задавайте Период явно.

Итоги

  • Актуальную цену на дату даёт виртуальная таблица СрезПоследних регистра сведений; первый параметр — Период.
  • Чтобы в отчёт попал весь список номенклатуры, отталкиваемся от справочника и подтягиваем цену через ЛЕВОЕ СОЕДИНЕНИЕ.
  • Отсутствующую цену (NULL) заменяем на 0 функцией ЕСТЬNULL(Цены.Цена, 0).
  • NULL нельзя сравнивать через =; проверка «пусто» — это ЕСТЬ NULL, подстановка — ЕСТЬNULL(...).
  • Левое соединение показывает все товары; внутреннее молча теряет позиции без цены.
Проверьте себя
1. Какая виртуальная таблица регистра сведений вернёт самую свежую цену каждой номенклатуры на указанную дату?
AОстатки
BОбороты
CСрезПоследних
DСрезПервых
2. После ЛЕВОГО СОЕДИНЕНИЯ со срезом цен у товаров без цены поле Цена содержит NULL. Как показать у них 0?
AГДЕ Цены.Цена = 0
BЕСТЬNULL(Цены.Цена, 0)
CСУММА(Цены.Цена)
DГДЕ Цены.Цена = NULL
3. Почему для отчёта «весь список номенклатуры и её цена» берут ЛЕВОЕ, а не ВНУТРЕННЕЕ соединение справочника со срезом цен?
AЛевое соединение работает быстрее внутреннего
BВнутреннее соединение оставит только товары, у которых цена есть, и потеряет остальные
CВнутреннее соединение не поддерживает виртуальные таблицы
DРазницы нет, это одно и то же