Как правильно написать запрос для проведения документа и оптимизировать запись движений методом Загрузить?

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

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

Суть проблемы: циклы против запросов

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

Задача состоит в том, чтобы провести документ «Оказание услуги» (или аналогичный) и сформировать движения сразу по нескольким регистрам:

  1. ОстаткиМатериалов (списание материалов).
  2. СтоимостьМатериалов (списание себестоимости).
  3. Продажи (фиксация выручки).

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

Этап 1. Базовое решение через циклы

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

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


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

КонецПроцедуры

Этот код работает, но он не оптимален. При большом количестве строк в документе система будет делать множество вызовов записи. Перейдем к оптимизации.

Этап 2. Переход к методу Загрузить()

Метод Движения.ИмяРегистра.Загрузить(ТаблицаЗначений) позволяет записать набор записей одним действием, просто подставив готовую таблицу данных. Это значительно быстрее.

Однако, чтобы этот метод сработал, таблица значений (или результат запроса) должна соответствовать жестким требованиям:

  1. Имена колонок в запросе должны в точности совпадать с именами измерений, ресурсов и реквизитов регистра.
  2. Для оборотных регистров достаточно периода и измерений/ресурсов.
  3. Для регистров остатков обязательно наличие колонки ВидДвижения.

Разберем, как сформировать такой запрос. Главная ошибка новичков — попытка сделать всё одним запросом к разным регистрам сразу. Лучше разделить задачу.

Пример запроса для регистра «ОстаткиМатериалов»

Нам нужно получить данные, которые сразу «лягут» в структуру регистра. Обратите внимание на использование конструкции ЗНАЧЕНИЕ(...) для задания вида движения.


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

Запрос.УстановитьПараметр("Ссылка", Ссылка);
Запрос.УстановитьПараметр("Период", Дата);
Запрос.УстановитьПараметр("Склад", Склад);

ТЗ_Остатки = Запрос.Выполнить().Выгрузить();
Движения.ОстаткиМатериалов.Загрузить(ТЗ_Остатки);

Важные моменты:

Этап 3. Сложные запросы с контролем остатков и себестоимостью

Самая сложная часть — получить себестоимость для списания. Для этого нужно соединить табличную часть документа с виртуальной таблицей остатков. Здесь часто возникают ошибки с NULL.

Если товара нет на остатке, соединение вернет NULL. Любая математическая операция с NULL (например, умножение или сложение) даст в результате NULL. Это приведет к ошибке при записи или некорректным данным.

Рассмотрим правильный запрос для формирования движений по регистру «СтоимостьМатериалов» и «Продажи».


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

Разбор ошибок и тонкостей

Проанализируем ключевые аспекты, которые сделают ваш код профессиональным:

  1. Обработка NULL (ЕСТЬNULL / ISNULL).

    Обратите внимание на конструкцию ЕСТЬNULL(СтоимостьОстатки.СтоимостьОстаток, 0). При левом соединении, если для материала не найдена запись в регистре остатков (например, это первая поставка или ошибка учета), поле будет пустым. Функция ЕСТЬNULL заменяет пустоту на 0, что позволяет избежать падения запроса при вычислениях.

  2. Деление на ноль.

    В примере выше мы делим стоимость на количество, чтобы получить цену единицы. Если КоличествоОстаток равно 0 (или NULL), произойдет ошибка деления на ноль. В функции ЕСТЬNULL(..., 1) мы подстраховались, заменив возможный ноль/null на 1 (это условность, в реальности нужно обрабатывать такие ситуации через ВЫБОР КОГДА ... ТОГДА ... ИНАЧЕ ... КОНЕЦ).

  3. Момент времени и Граница.

    При получении остатков крайне важно понимать, на какой момент вы их берете. Если документ просто проводится текущим временем — можно использовать параметр &Период. Но если вы перепроводите старый документ, простой запрос на его дату может включить в остатки движения этого же самого документа (если он уже был проведен ранее). Чтобы этого избежать, используют объект Граница с видом Исключая.

Итоговый алгоритм действий

Чтобы переписать код с циклов на метод Загрузить:

  1. Напишите запрос, который выбирает данные из табличной части документа.
  2. Сгруппируйте одинаковые позиции номенклатуры.
  3. Добавьте в запрос поля, необходимые регистру, которых нет в документе (например, ВидДвижения, Период, Склад из шапки), используя параметры или константы запроса.
  4. Задайте псевдонимы (КАК ...) всем полям так, чтобы они совпадали с именами ресурсов и измерений регистра.
  5. Если нужны данные из других таблиц (остатки, цены), используйте ЛЕВОЕ СОЕДИНЕНИЕ и обязательно обрабатывайте NULL.
  6. Выполните запрос, выгрузите результат и передайте его в метод Загрузить.

Такой подход делает проведение документов быстрым и устойчивым к ошибкам, а код — чистым и поддерживаемым.

← На главную