При разработке собственных конфигураций на платформе «1С:Предприятие 8» начинающие специалисты часто сталкиваются со следующей проблемой: документ списания или расхода товаров успешно проводится, однако при анализе движений по регистру накопления выясняется, что система зафиксировала операцию «Приход» вместо «Расход». При этом в коде проведения вроде бы явно указывается намерение сделать расходное движение.
В этой статье мы подробно разберем, почему возникает такое поведение платформы, проанализируем ошибки в коде и запросах, а также рассмотрим несколько вариантов архитектурно правильного решения этой задачи.
Для понимания сути проблемы обратим внимание на то, как происходит заполнение набора записей регистра накопления в процедуре проведения. Автор темы использует метод Загрузить():
ДвижениеРасход = Движения.ОстаткиНоменклатуры;
ДвижениеРасход.Записывать = Истина;
ДвижениеРасход.Загрузить(НаборЗаписейРасход);
Метод Загрузить() предназначен для быстрого переноса данных из внешней таблицы значений (в данном случае это результат работы функции ИнициализироватьТаблицыДляПроведенияТовары()) в набор записей регистра. При этом платформа производит сопоставление колонок таблицы значений и полей регистра по их именам.
Если в загружаемой таблице значений отсутствует колонка с именем ВидДвижения, платформа не сообщает об ошибке. Вместо этого для каждой добавляемой строки поле «Вид движения» инициализируется значением по умолчанию для системного перечисления ВидДвиженияНакопления.
Перечисление ВидДвиженияНакопления состоит из двух элементов:
Приход (индекс 0)Расход (индекс 1)Поскольку значением по умолчанию для любого перечисления всегда является его первый элемент (с нулевым индексом), платформа автоматически проставляет значение «Приход» для всех загружаемых строк. Именно поэтому документ расхода увеличивал остатки на складе вместо их уменьшения.
В исходном коде автора первая строчка процедуры проведения выглядит так:
ЭтотОбъект.ДополнительныеСвойства.Вставить("ВидДвижения", ВидДвиженияНакопления.Расход);
Многие начинающие разработчики полагают, что запись значения в свойство ДополнительныеСвойства автоматически сигнализирует платформе о том, какой вид движения нужно использовать. Это глубокое заблуждение.
ДополнительныеСвойства — это обычная структура (коллекция типа «Ключ и Значение»), которая принадлежит объекту документа. Она предназначена исключительно для временного хранения произвольных данных в оперативной памяти на время выполнения транзакции (например, для передачи параметров из модуля документа в подписки на события или в модуль набора записей регистра). Сама по себе платформа никак не анализирует содержимое этой структуры и не выполняет на её основе никаких автоматических действий по формированию движений, пока вы явно не напишете соответствующий код обработки.
Проанализируем функцию получения данных для проведения, которую написал автор:
Функция ИнициализироватьТаблицыДляПроведенияТовары(Ссылка, ДополнительныеСвойства) Экспорт
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст = "ВЫБРАТЬ
| РасходТовара.Склад КАК Склад,
| РасходТовараТовары.Номенклатура КАК Номенклатура,
| СУММА(РасходТовараТовары.Сумма) КАК Сумма,
| СУММА(РасходТовараТовары.Количество * РасходТовараТовары.ЕдиницаИзмерения.Коэффициент) КАК КоличествоВБазовыхЕдиницах,
| РасходТовара.Date КАК Период
|ПОМЕСТИТЬ Товары
|ИЗ
| Документ.РасходТовара.Товары КАК РасходТовараТовары,
| Документ.РасходТовара КАК РасходТовара
|ГДЕ
| РасходТовара.Ссылка = &Ссылка
| И РасходТовараТовары.Ссылка = &СсылкаТовары
|
|СГРУППИРОВАТЬ ПО
| РасходТовара.Склад,
| РасходТовараТовары.Номенклатура,
| РасходТовара.Дата
|
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Товары.Склад КАК Склад,
| Товары.Номенклатура КАК Номенклатура,
| Товары.Сумма КАК Сумма,
| Товары.КоличествоВБазовыхЕдиницах КАК КоличествоВБазовыхЕдиницах,
| Товары.Период КАК Период
|ИЗ
| Товары КАК Товары";
Запрос.УстановитьПараметр("СсылкаТовары", Ссылка);
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Результат = Запрос.Выполнить().Выгрузить();
Возврат Результат;
КонецФункции
Помимо отсутствия колонки вида движения, в этом коде есть несколько критических ошибок и неоптимальных решений (антипаттернов):
ИЗ через запятую указаны две таблицы: табличная часть Документ.РасходТовара.Товары и сам документ-шапка Документ.РасходТовара. Это приводит к созданию декартова произведения таблиц перед фильтрацией в секции ГДЕ. Проектирование таких запросов крайне негативно сказывается на производительности СУБД. Табличная часть документа уже содержит реквизит Ссылка, через который можно легко получить любые реквизиты шапки (например, Склад или Дату) без лишних соединений.ИНДЕКСИРОВАТЬ ПО Номенклатура. Однако во втором запросе пакета временная таблица просто выбирается целиком, без соединений с другими таблицами или условий фильтрации. Создание индекса в СУБД — ресурсоемкая операция, которая в данном случае выполняется вхолостую и замедляет работу системы.РасходТовараТовары.ЕдиницаИзмерения.Коэффициент внутри агрегатной функции СУММА() заставляет СУБД выполнять неявное левое соединение (LEFT JOIN) со справочником единиц измерения. На больших базах данных это может привести к снижению скорости выполнения запроса. Лучше сначала получить базовые данные, а затем соединить их с коэффициентами.ДвижениеРасход.Записать; без круглых скобок. В 1С Записать — это метод (процедура), поэтому вызов обязательно должен содержать скобки: ДвижениеРасход.Записать(). Более того, явный вызов этого метода внутри транзакции проведения при установленном флаге Записывать = Истина является избыточным и может приводить к излишним блокировкам таблиц базы данных.Для исправления неверного вида движения нам необходимо сделать так, чтобы в результирующей таблице значений присутствовала колонка ВидДвижения со значением ВидДвиженияНакопления.Расход. Рассмотрим несколько способов реализации этой задачи.
Если данная функция инициализации таблиц используется исключительно для проведения расходного документа, мы можем жестко прописать вид движения прямо в тексте SQL-запроса с помощью системной конструкции ЗНАЧЕНИЕ(). Это самый простой и быстрый способ решения.
Для этого во втором запросе пакета добавим поле вида движения:
|ВЫБРАТЬ
| Товары.Склад КАК Склад,
| Товары.Номенклатура КАК Номенклатура,
| Товары.Сумма КАК Сумма,
| Товары.КоличествоВБазовыхЕдиницах КАК КоличествоВБазовыхЕдиницах,
| ЗНАЧЕНИЕ(ВидДвиженияНакопления.Расход) КАК ВидДвижения,
| Товары.Период КАК Период
|ИЗ
| Товары КАК Товары
Если мы хотим сделать функцию инициализации более универсальной (например, использовать один и тот же код для проведения приходных и расходных накладных), вид движения лучше передавать в запрос в качестве параметра.
В тексте запроса это будет выглядеть следующим образом:
|ВЫБРАТЬ
| Товары.Склад КАК Склад,
| Товары.Номенклатура КАК Номенклатура,
| Товары.Сумма КАК Сумма,
| Товары.КоличествоВБазовыхЕдиницах КАК КоличествоВБазовыхЕдиницах,
| &ВидДвижения КАК ВидДвижения,
| Товары.Период КАК Период
|ИЗ
| Товары КАК Товары
Перед выполнением запроса нам потребуется передать значение параметра, прочитав его из структуры ДополнительныеСвойства:
ВидДвижения = ВидДвиженияНакопления.Расход; // Значение по умолчанию
Если ДополнительныеСвойства.Свойство("ВидДвижения") Тогда
ВидДвижения = ДополнительныеСвойства.ВидДвижения;
КонецЕсли;
Запрос.УстановитьПараметр("ВидДвижения", ВидДвижения);
Если по каким-то причинам изменять текст запроса нежелательно, мы можем дозаполнить таблицу значений уже после того, как она была выгружена из результата запроса. Для этого используется метод обхода строк:
РезультатТаблица = Запрос.Выполнить().Выгрузить();
// Добавляем колонку с нужным типом данных
РезультатТаблица.Колонки.Добавить("ВидДвижения", Новый ОписаниеТипов("ВидДвиженияНакопления"));
// Заполняем её значением "Расход" для всех строк
РезультатТаблица.ЗаполнитьЗначения(ВидДвиженияНакопления.Расход, "ВидДвижения");
Возврат РезультатТаблица;
Использование ДополнительныеСвойства для передачи параметров внутрь собственных экспортных функций одного и того же объекта — это избыточное усложнение архитектуры кода (оверхед). Чище и надежнее передавать требуемый вид движения напрямую в качестве явного параметра функции.
Давайте объединим все исправления, оптимизируем запрос (избавимся от Cross Join и лишнего индексирования) и напишем красивый, производительный код.
Код модуля документа в процедуре ОбработкаПроведения будет выглядеть следующим образом:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Очищаем движения перед проведением
Движения.ОстаткиНоменклатуры.Записывать = Истина;
Движения.ОстаткиНоменклатуры.Очистить();
// Получаем таблицу данных для проведения, передавая ссылку и нужный вид движения напрямую
ТаблицаДвижений = Документы.РасходТовара.ИнициализироватьТаблицыДляПроведенияТовары(Ссылка, ВидДвиженияНакопления.Расход);
// Загружаем подготовленные данные в движения
Движения.ОстаткиНоменклатуры.Загрузить(ТаблицаДвижений);
// Обратите внимание: метод .Записать() явно вызывать не нужно!
// Платформа автоматически запишет набор при успешном завершении транзакции,
// так как свойство "Записывать" установлено в значении Истина.
КонецПроцедуры
Теперь рассмотрим оптимизированный код экспортной функции в модуле менеджера или модуле объекта документа РасходТовара:
Функция ИнициализироватьТаблицыДляПроведенияТовары(СсылкаДокумента, ВидДвижения) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТабличнаяЧасть.Ссылка.Склад КАК Склад,
| ТабличнаяЧасть.Номенклатура КАК Номенклатура,
| СУММА(ТабличнаяЧасть.Сумма) КАК Сумма,
| СУММА(ТабличнаяЧасть.Количество * ТабличнаяЧасть.ЕдиницаИзмерения.Коэффициент) КАК КоличествоВБазовыхЕдиницах,
| &ВидДвижения КАК ВидДвижения,
| ТабличнаяЧасть.Ссылка.Дата КАК Период
|ИЗ
| Документ.РасходТовара.Товары КАК ТабличнаяЧасть
|ГДЕ
| ТабличнаяЧасть.Ссылка = &СсылкаДокумента
|ГРУППИРОВАТЬ ПО
| ТабличнаяЧасть.Ссылка.Склад,
| ТабличнаяЧасть.Номенклатура,
| ТабличнаяЧасть.Ссылка.Дата";
Запрос.УстановитьПараметр("СсылкаДокумента", СсылкаДокумента);
Запрос.УстановитьПараметр("ВидДвижения", ВидДвижения);
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
Что изменилось в оптимизированном запросе:
Документ.РасходТовара.Товары. Поля Склад и Дата (Период) извлекаются через точку от реквизита Ссылка. Платформа 1С самостоятельно преобразует это в оптимальное соединение на уровне СУБД.Товары. Сгруппированные данные сразу возвращаются пользователю без лишних операций записи и чтения в базе данных tempdb.ВидДвижения передается явно. Это позволяет использовать данную функцию в любых сценариях проведения, гибко управляя видом формируемых движений.Используя предложенные рекомендации, вы избавитесь от логических ошибок при проведении документов, а ваш код станет понятным, быстрым и соответствующим стандартам разработки фирмы «1С».