Как в 1С 7.7 получить цену номенклатуры из последнего документа поступления?

Программист 1С v7.7 Управленческий учет Торговля и дистрибуция
← На главную

При разработке отчетов по складским остаткам в платформе 1С:Предприятие 7.7 часто возникает практическая задача: вывести не только количество остатка на складе, но и актуальную учетную цену. Если в конфигурации не ведется специализированный регистр учета цен или периодический реквизит номенклатуры, единственным источником данных о стоимости становится последний проведенный документ прихода — например, ПоступлениеМПЗПрочее или ПоступлениеТМЦ.

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

Анализ синтаксических ошибок и особенности языка запросов 1С 7.7

Рассмотрим типовую конструкцию запроса, которую часто пытаются написать разработчики, переходящие с версии 1С 8.x или только начинающие работу с 1С 7.7:


|ДатаД = Документ.ПоступлениеМПЗПрочее.ДатаДок;
|Ном = Документ.ПоступлениеМПЗПрочее.МПЗ;
|Цена = Документ.ПоступлениеМПЗПрочее.Цена;
|Группировка Цена Упорядочить по ДатаД Возр; 
|Условие(Ном = ТекНом);

При выполнении такого текста платформа немедленно выдает ошибку:

Запрос[7] : Ожидается конкретизация переменной 'Цена'

Выясним причину такого поведения синтаксического анализатора:

  1. Недопустимость упорядочивания числового ресурса: Конструкция Упорядочить по в языке запросов 1С 7.7 предназначена исключительно для ссылочных типов данных (справочники, документы). Нельзя группировать строки по числовому реквизиту Цена и требовать упорядочить эту группировку по дате документа.
  2. Автоматическая хронологическая сортировка документов: Чтобы выстроить документы по дате и времени, в запросе не нужно объявлять отдельную переменную ДатаДок. Достаточно обратиться к контексту документа через реквизит ТекущийДокумент. Платформа 1С 7.7 при использовании директивы Группировка Док; по умолчанию упорядочивает выборку строго по хронологии (позиция документа на временной оси: дата, время, номер).
  3. Ограничение периода выборки: Если в запросе указана конструкция вида Период с ВыбНачПериода по ВыбКонПериода;, то поступления, оформленные ранее начальной даты отчета, просто не попадут в выборку. Если номенклатура лежит на складе давно, цена окажется нулевой. Для получения последней цены начальную границу периода следует опускать: Период по ВыбКонПериода;.

Критическая ошибка архитектуры: запрос внутри цикла

Проанализируем структуру кода отчета. Помещение создания и выполнения объекта Запрос внутрь цикла обхода основного запроса по остаткам (так называемая проблема N+1):


Пока Запрос.Группировка(1) = 1 Цикл
    ТекНом = Запрос.МПЗ;
    Запрос2 = СоздатьОбъект("Запрос");
    // формирование текста запроса...
    Запрос2.Выполнить(ТекстЗапроса2);
КонецЦикла;

Такой подход создает колоссальную нагрузку на дисковую подсистему и процессор. При наличии в номенклатурном справочнике нескольких тысяч позиций отчет будет формироваться десятки минут или вызовет переполнение памяти. Любые сводные запросы должны выполняться ровно один раз до начала цикла вывода данных, после чего сопоставление выполняется через память (например, через объект ТаблицаЗначений).

Решение 1. Сбор последних цен одним запросом через Таблицу Значений (Оптимальный способ)

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

Посмотрим на практическую реализацию этой схемы:


// 1. Создаем и выполняем запрос для поиска последних цен
ЗапросЦен = СоздатьОбъект("Запрос");
ТекстЗапросаЦен = 
"//{{ЗАПРОС(ПолучитьЦены)
|Период по ВыбКонПериода;
|Обрабатывать НеПомеченныеНаУдаление;
|Ном = Документ.ПоступлениеМПЗПрочее.МПЗ;
|Док = Документ.ПоступлениеМПЗПрочее.ТекущийДокумент;
|Цена = Документ.ПоступлениеМПЗПрочее.Цена;
|Группировка Ном без групп;
|Группировка Док Убыв;
|Условие(Цена > 0);
|"//}}ЗАПРОС
;

ТЗЦен = СоздатьОбъект("ТаблицаЗначений");
ТЗЦен.НоваяКолонка("МПЗ");
ТЗЦен.НоваяКолонка("Цена", "Число", 15, 2);

Если ЗапросЦен.Выполнить(ТекстЗапросаЦен) = 1 Тогда
    // Обходим номенклатуру
    Пока ЗапросЦен.Группировка("Ном") = 1 Цикл
        // Берем только ПЕРВЫЙ документ в группировке (он самый свежий благодаря модификатору Убыв)
        Если ЗапросЦен.Группировка("Док") = 1 Тогда
            ТЗЦен.НоваяСтрока();
            ТЗЦен.МПЗ  = ЗапросЦен.Ном;
            ТЗЦен.Цена = ЗапросЦен.Цена;
        КонецЕсли;
    КонецЦикла;
КонецЕсли;

Теперь в таблице ТЗЦен содержатся уникальные пары номенклатуры и ее последней ненулевой цены. В основном цикле вывода отчета достаточно вызвать быстрый метод поиска строки:


ТекМПЗ = Запрос.МПЗ;
ПоследняяЦена = 0;
НомСтр = 0;

Если ТЗЦен.НайтиЗначение(ТекМПЗ, НомСтр, "МПЗ") = 1 Тогда
    ТЗЦен.ПолучитьСтрокуПоНомеру(НомСтр);
    ПоследняяЦена = ТЗЦен.Цена;
КонецЕсли;

Решение 2. Получение себестоимости через регистр «Партии»

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

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

Цена = Регистр.Партии.Сумма / Регистр.Партии.Количество;

Однако если на предприятии присутствует пересортица или документы проводятся без указания сумм, надежнее использовать Решение 1 с явным обращением к шапке и строкам конкретного приходного документа.

Итоговый оптимизированный модуль процедуры формирования отчета

Объединим основной запрос по остаткам склада и предварительную выборку цен в единую готовую процедуру, исключив все лишние действия:


Процедура Сформировать()
    Перем Запрос, ТекстЗапроса, ЗапросЦен, ТекстЗапросаЦен, Таб, ТЗЦен;
    Перем ТекМПЗ, ПоследняяЦена, НомСтр;

    // ------------------------------------------------------------------------
    // ЭТАП 1. Получение последних цен из документов ПоступлениеМПЗПрочее
    // ------------------------------------------------------------------------
    ТЗЦен = СоздатьОбъект("ТаблицаЗначений");
    ТЗЦен.НоваяКолонка("МПЗ");
    ТЗЦен.НоваяКолонка("Цена", "Число", 15, 2);

    ЗапросЦен = СоздатьОбъект("Запрос");
    ТекстЗапросаЦен = 
    "//{{ЗАПРОС(ПолучитьЦены)
    |Период по ВыбКонПериода;
    |Обрабатывать НеПомеченныеНаУдаление;
    |Ном = Документ.ПоступлениеМПЗПрочее.МПЗ;
    |Док = Документ.ПоступлениеМПЗПрочее.ТекущийДокумент;
    |Цена = Документ.ПоступлениеМПЗПрочее.Цена;
    |Группировка Ном без групп;
    |Группировка Док Убыв;
    |Условие(Цена > 0);
    |"//}}ЗАПРОС
    ;

    Если ЗапросЦен.Выполнить(ТекстЗапросаЦен) = 1 Тогда
        Пока ЗапросЦен.Группировка(1) = 1 Цикл
            // Берем самый последний проведенный документ по хронологии
            Если ЗапросЦен.Группировка(2) = 1 Тогда
                ТЗЦен.НоваяСтрока();
                ТЗЦен.МПЗ  = ЗапросЦен.Ном;
                ТЗЦен.Цена = ЗапросЦен.Цена;
            КонецЕсли;
        КонецЦикла;
    КонецЕсли;

    // ------------------------------------------------------------------------
    // ЭТАП 2. Формирование остатков по регистру МестаХранения
    // ------------------------------------------------------------------------
    Запрос = СоздатьОбъект("Запрос");
    ТекстЗапроса = 
    "//{{ЗАПРОС(Сформировать)
    |Период с ВыбНачПериода по ВыбКонПериода;
    |Обрабатывать НеПомеченныеНаУдаление;
    |МПЗ = Регистр.МестаХранения.МПЗ;
    |ВидМПЗ = Регистр.МестаХранения.ВидМПЗ;
    |МестоХранения = Регистр.МестаХранения.МестоХранения;
    |Количество = Регистр.МестаХранения.Количество;
    |ИспользоватьВЗаказахСклад = Регистр.МестаХранения.МестоХранения.ИспользоватьВЗаказахСклад;
    |Функция КоличествоКонОст = КонОст(Количество);
    |Группировка МПЗ без упорядочивания;
    |Группировка МестоХранения;
    |Группировка ВидМПЗ;
    |Условие(ИспользоватьВЗаказахСклад = 1);
    |"//}}ЗАПРОС
    ;

    Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
        Возврат;
    КонецЕсли;

    // ------------------------------------------------------------------------
    // ЭТАП 3. Вывод данных в печатную форму таблицы
    // ------------------------------------------------------------------------
    Таб = СоздатьОбъект("Таблица");
    Таб.ИсходнаяТаблица("Сформировать");
    Таб.ВывестиСекцию("Заголовок");
    Состояние("Заполнение выходной таблицы...");
    Таб.Опции(0, 0, Таб.ВысотаТаблицы(), 0);

    Пока Запрос.Группировка(1) = 1 Цикл
        ТекМПЗ = Запрос.МПЗ;
        ПоследняяЦена = 0;

        Если ТекМПЗ.ЭтоГруппа() = 0 Тогда
            НомСтр = 0;
            // Быстрый поиск сохраненной цены
            Если ТЗЦен.НайтиЗначение(ТекМПЗ, НомСтр, "МПЗ") = 1 Тогда
                ТЗЦен.ПолучитьСтрокуПоНомеру(НомСтр);
                ПоследняяЦена = ТЗЦен.Цена;
            КонецЕсли;
            Таб.ВывестиСекцию("МПЗ");
        Иначе  
            Таб.ВывестиСекцию("ГруппаМПЗ");
        КонецЕсли;    

        Пока Запрос.Группировка(2) = 1 Цикл
            Пока Запрос.Группировка(3) = 1 Цикл
                Таб.ВывестиСекцию("МестоХранения");
            КонецЦикла;
        КонецЦикла;   
    КонецЦикла;

    Таб.ВывестиСекцию("Итого");
    Таб.ТолькоПросмотр(1);
    Таб.Показать("Сформировать", "");
КонецПроцедуры

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

← На главную