Как получить все значения из табличной части документа при копировании в 1С без дублирования

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

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

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

Разбор исходной ошибки: почему дублируются документы и строки

Рассмотрим фрагмент кода, который изначально использовался для копирования документов МАВ_БюджетнаяОперация:


Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    ДокументКопия = Выборка.Ссылка.Скопировать(); 
    Для Каждого ТЧ ИЗ ДокументКопия.ТЧОсновная Цикл
        ТЧ.Сумма = Выборка.Сумма;
    КонецЦикла;
    ДокументКопия.Записать(РежимЗаписиДокумента.Проведение);
КонецЦикла;

Выясним причину, по которой этот код работает некорректно. Запрос обращается непосредственно к табличной части документа: ИЗ Документ.МАВ_БюджетнаяОперация.ТЧОсновная КАК ТЧ. Такая выборка возвращает плоскую таблицу, где каждая запись — это отдельная строка табличной части конкретного документа.

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

Проследим за пошаговым выполнением цикла Пока Выборка.Следующий():

  1. Итерация 1: Мы позиционируемся на первой строке выборки (где сумма равна 100). Вызывается метод Выборка.Ссылка.Скопировать(). Создается первый новый документ-копия, в который автоматически переносятся все три строки исходного документа. Далее запускается внутренний цикл Для Каждого ТЧ ИЗ ДокументКопия.ТЧОсновная. Этот цикл обходит все три строки нового документа и записывает в них сумму из текущей выборки — то есть 100 рублей в каждую строку. Документ записывается и проводится.
  2. Итерация 2: Мы переходим ко второй строке выборки (где сумма равна 200). Снова вызывается метод Выборка.Ссылка.Скопировать(). Создается второй документ-копия (опять со всеми тремя исходными строками). Внутренний цикл записывает во все три строки сумму 200 рублей. Второй документ записывается и проводится.
  3. Итерация 3: Повторяется то же самое для третьей строки. Создается третий документ-копия, во всех строках которого устанавливается сумма 300 рублей.

В результате вместо одного нового документа с суммами 100, 200 и 300 мы получили три разных документа, в каждом из которых суммы затерты одинаковыми значениями. Это классическая логическая ошибка, связанная с непониманием структуры данных, возвращаемой запросом по табличным частям.

Решение №1. Самый простой способ: отказ от запроса к табличной части

Если задача состоит в том, чтобы просто скопировать документы и пересчитать показатели их табличной части (например, умножить количество и сумму на определенный коэффициент), нам вообще не нужно делать запрос к табличной части. Намного проще и быстрее выбрать запросом только уникальные ссылки на сами документы-источники, а все расчеты произвести непосредственно в оперативной памяти.

Посмотрим на пример правильной реализации этого подхода:


// Шаг 1. Формируем запрос только по шапке документов
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ДатаНач", ДатаНач);
Запрос.УстановитьПараметр("ДатаКон", ДатаКон);
Запрос.УстановитьПараметр("Сценарий", Сценарий);
Запрос.УстановитьПараметр("СтатьяОборотов", СтатьяОборотов);
Запрос.УстановитьПараметр("Подразделение", Подразделение);

Запрос.Текст = 
    "ВЫБРАТЬ
    |	МАВ_БюджетнаяОперация.Ссылка КАК Ссылка
    |ИЗ
    |	Документ.МАВ_БюджетнаяОперация КАК МАВ_БюджетнаяОперация
    |ГДЕ
    |	МАВ_БюджетнаяОперация.ПериодПланирования МЕЖДУ &ДатаНач И &ДатаКон
    |	И МАВ_БюджетнаяОперация.Проведен = ИСТИНА
    |	И МАВ_БюджетнаяОперация.Сценарий = &Сценарий
    |	И МАВ_БюджетнаяОперация.СтатьяОборотовБюджета = &СтатьяОборотов
    |	И МАВ_БюджетнаяОперация.Подразделение = &Подразделение";

Выборка = Запрос.Formulir().Выполнить().Выбрать();

// Шаг 2. Обходим документы и производим копирование и пересчет
Пока Выборка.Следующий() Цикл
    
    // Копируем документ ОДИН раз
    ДокументКопия = Выборка.Ссылка.Скопировать();
    
    // Перебираем строки ТЧ скопированного документа прямо в памяти
    Для Каждого СтрокаТЧ Из ДокументКопия.ТЧОсновная Цикл
        СтрокаТЧ.Количество = СтрокаТЧ.Количество * Коэффициент;
        СтрокаТЧ.Сумма = СтрокаТЧ.Сумма * Коэффициент;
    КонецЦикла;
    
    // Заполняем новые реквизиты шапки
    ДокументКопия.Дата = ТекущаяДата();
    Если ЗначениеЗаполнено(ДатаНовая) Тогда
        ДокументКопия.ПериодПланирования = ДатаНовая;
    КонецЕсли;
    ДокументКопия.Сценарий = СценарийНовый;
    
    // Безопасная запись с обработкой исключений
    Попытка
        ДокументКопия.Записать(РежимЗаписиДокумента.Проведение);
        Сообщить("Успешно создан и проведен документ: " + ДокументКопия.Ссылка);
    Исключение
        Сообщить("Не удалось провести документ " + ДокументКопия.Ссылка + ". Ошибка: " + ОписаниеОшибки());
    КонецПопытки;
    
КонецЦикла;

Этот способ является наиболее предпочтительным с точки зрения производительности и чистоты кода. Мы совершаем минимальное количество чтений из базы данных и не нагружаем процессор СУБД избыточными соединениями таблиц.

Решение №2. Использование иерархического обхода (конструкция ИТОГИ ПО)

Бывают ситуации, когда расчет новых сумм или других показателей табличной части невозможно выполнить простым умножением на коэффициент в памяти. Например, если новые значения рассчитываются сложным запросом, объединяющим данные из различных регистров сведений или накопления. В таком случае нам действительно необходимо получать данные табличной части из запроса.

Чтобы избежать дублирования документов, нам потребуется использовать обход выборки по группировкам с помощью конструкции ИТОГИ ПО Ссылка. Это позволит сгруппировать строки по документам-владельцам.

Разберем этот метод по шагам:


Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ДатаНач", ДатаНач);
Запрос.УстановитьПараметр("ДатаКон", ДатаКон);
Запрос.УстановитьПараметр("Сценарий", Сценарий);
Запрос.УстановитьПараметр("СтатьяОборотов", СтатьяОборотов);
Запрос.УстановитьПараметр("Подразделение", Подразделение);
Запрос.УстановитьПараметр("Коэффициент", Коэффициент);

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

// Обходим результат запроса по группировкам (сначала только документы)
ВыборкаДокументов = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаДокументов.Следующий() Цикл
    
    // Копируем документ ОДИН раз для всей группы строк
    ДокументКопия = ВыборкаДокументов.Ссылка.Скопировать();
    
    // Получаем подчиненную выборку по детальным записям (строкам ТЧ конкретного документа)
    ВыборкаСтрок = ВыборкаДокументов.Выбрать();
    Пока ВыборкаСтрок.Следующий() Цикл
        
        // Индексация строк в ТЧ на сервере начинается с 0, а НомерСтроки в БД — с 1.
        ИндексСтроки = ВыборкаСтрок.НомерСтроки - 1;
        
        // Проверяем, существует ли строка с таким индексом в скопированном документе
        Если ИндексСтроки < ДокументКопия.ТЧОсновная.Количество() Тогда
            СтрокаТЧ = ДокументКопия.ТЧОсновная[ИндексСтроки];
            СтрокаТЧ.Количество = ВыборкаСтрок.Количество;
            СтрокаТЧ.Сумма = ВыборкаСтрок.Сумма;
        КонецЕсли;
        
    КонецЦикла;
    
    // Заполнение реквизитов шапки
    ДокументКопия.Дата = ТекущаяДата();
    Если ЗначениеЗаполнено(ДатаНовая) Тогда
        ДокументКопия.ПериодПланирования = ДатаНовая;
    КонецЕсли;
    ДокументКопия.Сценарий = СценарийНовый;
    
    Попытка
        ДокументКопия.Записать(РежимЗаписиДокумента.Проведение);
        Сообщить("Успешно перенесен документ: " + ДокументКопия.Ссылка);
    Исключение
        Сообщить("Ошибка проведения документа: " + ОписаниеОшибки());
    КонецПопытки;
    
КонецЦикла;

Использование свойства НомерСтроки и вычисление ИндексСтроки = ВыборкаСтрок.НомерСтроки - 1 гарантирует, что измененные данные запишутся строго в те строки скопированного документа, из которых они были получены в исходном документе.

Разбор второстепенного вопроса: как правильно реализовать необязательные фильтры

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

Участники предложили следующее решение: если параметр не заполнен, передавать в него пустую ссылку:


Если ЗначениеЗаполнено(СтатьяОборотов) Тогда
    Запрос.УстановитьПараметр("СтатьяОборотов", СтатьяОборотов);
Иначе
    Запрос.УстановитьПараметр("СтатьяОборотов", Справочники.СтатьиОборотовБюджета.ПустаяСсылка());
КонецЕсли;

Внимание! Это грубейшая логическая ошибка. Если вы передадите пустую ссылку в качестве параметра, условие запроса МАВ_БюджетнаяОперация.СтатьяОборотовБюджета = &СтатьяОборотов преобразуется СУБД в проверку на равенство пустой ссылке. Запрос вернет только те документы, у которых статья оборотов действительно не заполнена в базе данных. Документы с заполненными статьями в выборку не попадут, хотя пользователь ожидал получить все документы без фильтрации.

Рассмотрим два правильных способа реализации необязательных фильтров в 1С.

Способ А. Динамическое построение текста запроса (Рекомендуется)

Самый производительный способ — добавлять строчку фильтра в текст запроса только тогда, когда параметр реально заполнен. В этом случае СУБД строит оптимальный план запроса и не выполняет лишних проверок.


ТекстУсловия = "";
Если ЗначениеЗаполнено(СтатьяОборотов) Тогда
    ТекстУсловия = " И МАВ_БюджетнаяОперация.СтатьяОборотовБюджета = &СтатьяОборотов";
КонецЕсли;

Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	МАВ_БюджетнаяОперация.Ссылка КАК Ссылка
    |ИЗ
    |	Документ.МАВ_БюджетнаяОперация КАК МАВ_БюджетнаяОперация
    |ГДЕ
    |	МАВ_БюджетнаяОперация.ПериодПланирования МЕЖДУ &ДатаНач И &ДатаКон
    |	И МАВ_БюджетнаяОперация.Проведен = ИСТИНА" + ТекстУсловия;

Запрос.УстановитьПараметр("ДатаНач", ДатаНач);
Запрос.УстановитьПараметр("ДатаКон", ДатаКон);

Если ЗначениеЗаполнено(СтатьяОборотов) Тогда
    Запрос.УстановитьПараметр("СтатьяОборотов", СтатьяОборотов);
КонецЕсли;

Способ Б. Универсальное условие в самом тексте запроса

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


ГДЕ
    МАВ_БюджетнаяОперация.ПериодПланирования МЕЖДУ &ДатаНач И &ДатаКон
    И МАВ_БюджетнаяОперация.Проведен = ИСТИНА
    И (&ОтключитьФильтрСтатьи ИЛИ МАВ_БюджетнаяОперация.СтатьяОборотовБюджета = &СтатьяОборотов)

При заполнении параметров мы передаем булево значение в &ОтключитьФильтрСтатьи:


Запрос.УстановитьПараметр("СтатьяОборотов", СтатьяОборотов);
Запрос.УстановитьПараметр("ОтключитьФильтрСтатьи", Не ЗначениеЗаполнено(СтатьяОборотов));

В этом сценарии, если статья не заполнена, параметр &ОтключитьФильтрСтатьи принимает значение ИСТИНА. Первое условие в скобках СУБД выполняет моментально, и проверка равенства конкретной статье опускается, что позволяет выбрать абсолютно все записи.

Оптимизация производительности: Транзакции и пакетная обработка

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

Рассмотрим, как правильно обернуть процедуру копирования в транзакцию:


НачатьТранзакцию();

Попытка
    Выборка = Запрос.Выполнить().Выбрать();
    Пока Выборка.Следующий() Цикл
        
        ДокументКопия = Выборка.Ссылка.Скопировать();
        
        Для Каждого СтрокаТЧ Из ДокументКопия.ТЧОсновная Цикл
            СтрокаТЧ.Сумма = СтрокаТЧ.Сумма * Коэффициент;
        КонецЦикла;
        
        ДокументКопия.Дата = ТекущаяДата();
        ДокументКопия.Записать(РежимЗаписиДокумента.Проведение);
        
    КонецЦикла;
    
    // Если все документы обработаны без ошибок, фиксируем транзакцию
    ЗафиксироватьТранзакцию();
    Сообщить("Все документы успешно скопированы и проведены!");
    
Исключение
    // В случае любой ошибки откатываем базу к исходному состоянию
    ОтменитьТранзакцию();
    Сообщить("Произошла критическая ошибка! Изменения отменены. Описание: " + ОписаниеОшибки());
КонецПопытки;

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

← На главную