Как правильно удалить строку из табличной части документа и почему возникает ошибка "Колонка не принадлежит коллекции"?

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

При программной работе с документами в 1С, особенно при изменении их табличных частей, можно столкнуться с ошибкой «Недопустимое значение параметра (параметр номер '1') (Колонка не принадлежит коллекции)». На первый взгляд, сообщение об ошибке может показаться нелогичным, ведь мы пытаемся удалить целую строку, а не обращаемся к какой-то конкретной колонке. Давайте разберемся, почему возникает эта ошибка и как ее избежать.

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

Разбираем причину ошибки на примере неверного кода

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


// Списание - это ссылка на документ СписаниеТоваров
ОтборВТЧ = Новый Структура("Номенклатура, Количество", СтрокаНайти.Номенклатура, СтрокаНайти.КоличествоСписание);
УдаляемыеСтроки = Списание.ПолучитьОбъект().Товары.НайтиСтроки(ОтборВТЧ);

// Цикл, в котором происходит ошибка
Для каждого УдаляемаяСтрока из УдаляемыеСтроки цикл
    // На каждой итерации цикла заново получаем объект из базы данных
    Списание.ПолучитьОбъект().Товары.Удалить(УдаляемаяСтрока); 
КонецЦикла;

Ключевая проблема этого кода скрыта в строке Списание.ПолучитьОбъект().Товары.Удалить(УдаляемаяСтрока) внутри цикла. Давайте проследим логику выполнения:

  1. Сначала мы один раз получаем объект документа с помощью Списание.ПолучитьОбъект() и находим в его табличной части нужные строки. Результат (массив строк) сохраняется в переменной УдаляемыеСтроки. На этом этапе у нас есть Объект №1 и найденные в нем строки.
  2. Далее начинается цикл Для каждого. На первой же итерации снова вызывается метод Списание.ПолучитьОбъект(). Этот вызов создает в памяти новый, совершенно независимый экземпляр объекта документа (назовем его Объект №2), который считывается напрямую из базы данных.
  3. После этого код пытается в Объекте №2 удалить строку УдаляемаяСтрока, которая была найдена ранее в Объекте №1.
  4. С точки зрения платформы 1С, строка из Объекта №1 не имеет никакого отношения к табличной части Объекта №2. Они существуют в разных контекстах, в разных областях памяти. Поэтому система и выдает ошибку: «Колонка не принадлежит коллекции», сообщая, что передаваемая для удаления строка является "чужой" для той табличной части, из которой ее пытаются удалить.

Таким образом, главная ошибка — это многократное получение объекта в цикле. Каждое такое действие создает новый, "чистый" экземпляр объекта из базы данных, который ничего не знает о тех строках, что вы нашли в предыдущем экземпляре.

Правильный подход: работа с одним экземпляром объекта

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

Давайте исправим исходный код. Объект документа нужно получить один раз, до начала цикла.

Посмотрим на правильный пример:


// Списание - это по-прежнему ссылка на документ
ОтборВТЧ = Новый Структура("Номенклатура, Количество", СтрокаНайти.Номенклатура, СтрокаНайти.КоличествоСписание);

// 1. Получаем объект ОДИН РАЗ и сохраняем его в переменную
ОбъектСписания = Списание.ПолучитьОбъект();

// 2. Ищем строки в табличной части именно этого объекта
УдаляемыеСтроки = ОбъектСписания.Товары.НайтиСтроки(ОтборВТЧ);

// 3. Проверяем, нашлись ли строки, чтобы не выполнять лишних действий
Если УдаляемыеСтроки.Количество() > 0 Тогда
    
    // 4. В цикле удаляем найденные строки из ТОГО ЖЕ объекта
    Для каждого УдаляемаяСтрока из УдаляемыеСтроки цикл
        ОбъектСписания.Товары.Удалить(УдаляемаяСтрока);
    КонецЦикла;

    // 5. Записываем измененный объект в базу данных ПОСЛЕ всех манипуляций
    ОбъектСписания.Записать();
    
КонецЕсли;

В этом коде все операции происходят в контексте одного объекта — ОбъектСписания. Мы находим строки в нем, а затем удаляем их из него же. Платформа без проблем находит соответствие, и удаление проходит успешно.

Альтернативный способ удаления строк: обход с конца

Стоит отметить, что удаление элементов из коллекции во время ее перебора циклом Для каждого в некоторых случаях может приводить к ошибкам или пропуску элементов. Коллекция изменяется "на лету", что нарушает работу итератора.

Более надежным способом является обход коллекции с конца с помощью цикла Для...По или использование цикла Пока.

Рассмотрим пример с обходом с конца. Этот метод гарантирует, что удаление элемента не повлияет на перебор оставшихся.


ОбъектСписания = Списание.ПолучитьОбъект();
ТабЧастьТовары = ОбъектСписания.Товары;

// Обходим коллекцию с последней строки к первой
Для Индекс = ТабЧастьТовары.Количество() - 1 По 0 Цикл
    
    ТекущаяСтрока = ТабЧастьТовары[Индекс];
    
    // Проверяем условие для удаления
    Если ТекущаяСтрока.Номенклатура = НужнаяНоменклатура Тогда
        // Удаляем по индексу или по самой строке - в данном случае оба варианта сработают
        ТабЧастьТовары.Удалить(Индекс); 
    КонецЕсли;
    
КонецЦикла;

ОбъектСписания.Записать();

Анализ итогового решения и дополнительные рекомендации

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

Рассмотрим фрагмент этого кода:


// ... код изменения количества в строке ...
Если КоррСтрока.Количество <= 0 Тогда
    КоррСтрокиСписания.Удалить(КоррСтрока);
КонецЕсли;
// ...
// После цикла по строкам
Если Списание.Товары.Количество() > 0 Тогда
    Списание.Записать(РежимЗаписиДокумента.Проведение);
Иначе
    Списание.Комментарий = "Помечен на удаление автоматически...";
    // Здесь есть избыточное действие
    Списание.Записать(РежимЗаписиДокумента.Запись);
    Списание.УстановитьПометкуУдаления(Истина);
КонецЕсли;

Здесь есть важный нюанс. Метод УстановитьПометкуУдаления(Истина) не просто устанавливает флаг, но и выполняет запись объекта в базу данных. Поэтому вызов Списание.Записать() непосредственно перед ним является избыточным. Объект будет записан дважды, что некритично, но снижает производительность.

Оптимизированный вариант:


// ...
Иначе
    Списание.Комментарий = "Помечен на удаление автоматически...";
    // Достаточно только этого метода, он сам запишет объект
    Списание.УстановитьПометкуУдаления(Истина);
КонецЕсли;

Подведем итог: ошибка «Колонка не принадлежит коллекции» при удалении строк почти всегда сигнализирует о логической ошибке в коде. Вы пытаетесь манипулировать данными одного объекта, используя контекст другого. Всегда придерживайтесь правила: получили объект -> выполнили все изменения -> записали объект. Это обеспечит корректную и предсказуемую работу вашего кода.

← На главную