В разработке на 1С часто возникают задачи, связанные со списанием товаров, имеющих сложный состав или спецификацию. Классический пример — продажа готового блюда в ресторане или сборка комплекта на производстве. В этом случае необходимо списать не сам товар, а его составляющие (ингредиенты, комплектующие) со склада. Давайте разберем типичную ситуацию, с которой столкнулся автор на форуме, и рассмотрим правильные и оптимальные подходы к ее решению.
Исходная задача: есть документ «Продажи», в табличной части которого указывается проданная продукция (например, пицца). Каждая пицца — это элемент справочника «Номенклатура» (или, как в примере автора, «Пицца»), у которого, в свою очередь, есть своя табличная часть с перечнем ингредиентов и их количеством. При проведении документа продажи необходимо создать движения расхода в регистре накопления «Остатки» для каждого ингредиента.
Первоначальный код автора выглядел так:
Процедура ОбработкаПроведения(Отказ, Режим)
Для Каждого ТекСтрокаЗаказ Из Заказ Цикл
Для Каждого Ингредиент Из ТекСтрокаЗаказ.Товар.ИнгредиентыИСоусы Цикл
Движение = Движения.Остатки.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Номенклатура = Ингредиент.Ингредиент;
Движение.Контрагент = Клиент;
Движение.Количество = Ингредиент.Количество * ТекСтрокаЗаказ.Количество;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
При выполнении этого кода система выдавала ошибку: «Поле объекта не обнаружено (ИнгредиентыИСоусы)». После ее исправления возник второй, более важный вопрос: как избежать дублирования записей в регистре, если в одном документе продается несколько пицц с одинаковыми ингредиентами?
Проанализируем обе проблемы и найдем для них оптимальные решения.
Эта ошибка — одна из самых распространенных у начинающих разработчиков. Она означает, что мы пытаемся обратиться к реквизиту или табличной части объекта по имени, которого не существует в его метаданных.
Выясним причину. В строке ТекСтрокаЗаказ.Товар.ИнгредиентыИСоусы мы последовательно обращаемся к объектам:
ТекСтрокаЗаказ — это строка табличной части документа.ТекСтрокаЗаказ.Товар — это реквизит строки, который хранит ссылку на элемент справочника «Пицца».ТекСтрокаЗаказ.Товар.ИнгредиентыИСоусы — здесь мы пытаемся через ссылку обратиться к табличной части справочника.Ошибка может возникнуть по двум основным причинам:
«СоставИнгредиентов», а в коде мы написали «ИнгредиентыИСоусы». Как выяснилось из обсуждения на форуме, именно это и было причиной у автора. Решение: всегда проверяйте точное имя реквизитов и табличных частей в дереве метаданных конфигурации.«Товар» может быть составного типа и ссылаться не только на справочник с нужной табличной частью, но и на другие объекты, у которых ее нет. В таких случаях перед обращением к данным нужно проверять тип ссылки.После исправления опечатки код заработает, но будет работать неправильно и неэффективно. Представим, что в документе продали:
Приведенный выше код создаст в регистре отдельные записи для каждого ингредиента каждой пиццы. В итоге мы получим примерно такой набор движений:
Это приводит к «засорению» регистра множеством мелких записей, что затрудняет анализ и снижает производительность отчетов. Правильный подход — сгруппировать все одинаковые ингредиенты и сделать одну запись на итоговое количество. Для «томатного соуса» должна быть одна запись на общее количество, требуемое для двух «Маргарит» и одной «Пепперони».
Кроме того, первоначальный код содержит критическую ошибку производительности, известную как «проблема N+1 запроса». Обращение к данным через точку внутри цикла (ТекСтрокаЗаказ.Товар.ИнгредиентыИСоусы) заставляет платформу 1С выполнять отдельный запрос к базе данных на каждой итерации цикла, чтобы получить состав пиццы. Если в документе 100 позиций, будет выполнен 101 запрос (1 на получение табличной части документа и 100 на получение состава каждой пиццы). Это крайне расточительно и медленно.
Самый правильный, быстрый и профессиональный способ решить обе проблемы — использовать объект Запрос. Он позволяет получить все необходимые данные одним пакетом, а также произвести их группировку и суммирование на стороне сервера СУБД, что является наиболее эффективным подходом.
Разберем по шагам, как должен выглядеть такой запрос:
«Заказ» нашего документа «Продажи». К ней мы присоединяем (через точку в тексте запроса) табличную часть «ИнгредиентыИСоусы» справочника «Пицца».ГДЕ Ссылка = &Ссылка.СГРУППИРОВАТЬ ПО полю с ингредиентом.СУММА().Теперь посмотрим на готовый код для процедуры ОбработкаПроведения:
Процедура ОбработкаПроведения(Отказ, Режим)
// Создаем и настраиваем запрос для получения сгруппированных данных
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПродажиЗаказ.Товар.ИнгредиентыИСоусы.Ингредиент КАК Номенклатура,
| СУММА(ПродажиЗаказ.Количество * ПродажиЗаказ.Товар.ИнгредиентыИСоусы.Количество) КАК Количество
|ИЗ
| Документ.Продажи.Заказ КАК ПродажиЗаказ
|ГДЕ
| ПродажиЗаказ.Ссылка = &СсылкаДокумента
|
|СГРУППИРОВАТЬ ПО
| ПродажиЗаказ.Товар.ИнгредиентыИСоусы.Ингредиент";
// Устанавливаем параметр запроса - ссылку на текущий документ
Запрос.УстановитьПараметр("СсылкаДокумента", Ссылка);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
// Устанавливаем флаг, чтобы платформа автоматически очистила старые движения
// этого документа перед записью новых.
Движения.Остатки.Записывать = Истина;
// Обходим результат запроса и формируем движения
Пока Выборка.Следующий() Цикл
Движение = Движения.Остатки.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Номенклатура = Выборка.Номенклатура;
Движение.Контрагент = Клиент; // Контрагент берется из шапки документа
Движение.Количество = Выборка.Количество;
КонецЦикла;
КонецПроцедуры
Преимущества этого подхода:
Если по какой-то причине использование запроса невозможно (что бывает крайне редко), можно использовать промежуточную ТаблицуЗначений и ее метод Свернуть(). Этот метод группирует одинаковые строки, суммируя значения в указанных числовых колонках.
Важно: этот способ решает только проблему дублирования записей, но не решает проблему производительности «N+1», так как для наполнения таблицы мы все равно будем использовать неэффективный вложенный цикл.
Алгоритм действий будет следующим:
ТаблицуЗначений с колонками для измерений и ресурсов регистра (Номенклатура, Количество).Свернуть(), указав колонки для группировки и колонку для суммирования.
Процедура ОбработкаПроведения(Отказ, Режим)
// Создаем временную таблицу для хранения данных
ТаблицаДвижений = Новый ТаблицаЗначений;
ТаблицаДвижений.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаДвижений.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
// Шаг 1: Заполняем таблицу "сырыми" данными (неэффективный способ)
Для Каждого ТекСтрокаЗаказ Из Заказ Цикл
// Обращение через точку в цикле - причина низкой производительности
Для Каждого Ингредиент Из ТекСтрокаЗаказ.Товар.ИнгредиентыИСоусы Цикл
НоваяСтрока = ТаблицаДвижений.Добавить();
НоваяСтрока.Номенклатура = Ингредиент.Ингредиент;
НоваяСтрока.Количество = Ингредиент.Количество * ТекСтрокаЗаказ.Количество;
КонецЦикла;
КонецЦикла;
// Шаг 2: Сворачиваем таблицу, группируя по номенклатуре и суммируя количество
ТаблицаДвижений.Свернуть("Номенклатура", "Количество");
Движения.Остатки.Записывать = Истина;
// Шаг 3: Записываем движения из свернутой таблицы
Для Каждого СтрокаДвижения Из ТаблицаДвижений Цикл
Движение = Движения.Остатки.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Номенклатура = СтрокаДвижения.Номенклатура;
Движение.Контрагент = Клиент;
Движение.Количество = СтрокаДвижения.Количество;
КонецЦикла;
КонецПроцедуры
Этот метод стоит рассматривать как запасной вариант. Решение с использованием запроса всегда предпочтительнее.
При работе с регистрами остатков нельзя забывать о двух вещах:
1. Контроль остатков. Перед тем как списать товар (создать движение Расход), хорошей практикой является проверка его наличия на складе. Если ингредиентов не хватает, проведение документа следует отменить, выдав пользователю соответствующее сообщение. Контроль остатков также лучше всего выполнять с помощью запроса.
2. Блокировки. В многопользовательской среде может возникнуть ситуация, когда два менеджера одновременно пытаются продать последнюю порцию сыра. Чтобы избежать гонки данных и продажи в минус, необходимо использовать механизм блокировок (объект БлокировкаДанных). Блокировка "захватывает" записи в регистре на время проведения документа, не давая другим транзакциям их изменить.
В заключение, правильная и оптимальная работа с данными — ключ к созданию быстрых и надежных систем на платформе 1С. Использование запросов вместо циклов для извлечения и обработки данных является фундаментальным принципом эффективной разработки.