Как правильно перенести элементы справочника с сохранением иерархии в 1С 7.7 и исправить ошибку «Элемент не выбран»

Программист 1С v7.7 1C:Бухгалтерия Бухгалтерский учет IT и автоматизация бизнеса
← На главную

Перенос данных между справочниками в платформе 1С:Предприятие 7.7 — это классическая задача, с которой часто сталкиваются разработчики при модернизации или реструктуризации учетных систем. Одним из наиболее частых сценариев является перенос элементов из справочника Материалы в справочник Номенклатура в конфигурации «Бухгалтерский учет 7.7» (начиная с релиза 4.5). Несмотря на кажущуюся простоту, при написании кода обработки можно столкнуться с неочевидными архитектурными особенностями платформы, которые приводят к ошибкам выполнения. В этой статье мы подробно разберем, почему возникает популярная ошибка «Элемент не выбран», проанализируем ошибки позиционирования объектов и рассмотрим оптимальные алгоритмы переноса данных, включая рекурсивный метод создания иерархии за один проход.

Анализ исходного кода и причин возникновения ошибки «Элемент не выбран»

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


СпрСотр = СоздатьОбъект("Справочник.Номенклатура");     
Пока Спр.ПолучитьЭлемент() = 1 Цикл
    Если Спр.ТекущийЭлемент().ЭтоГруппа()=0 Тогда
        СпрСотр.Новый();
        СпрСотр.Наименование = Спр.Наименование;
        СпрСотр.Записать(); // Первая запись нового элемента
        
        // Попытка найти родителя на том же объекте СпрСотр!
        Если СпрСотр.НайтиПоНаименованию(спр.Родитель,Спр.Родитель.Код) = 1 тогда  
            СпрСотр.Родитель = СпрСотр.ТекущийЭлемент();
        КонецЕсли;
        
        ...
        СпрСотр.Записать(); // Попытка повторной записи
    КонецЕсли;
КонецЦикла;

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

Когда мы выполняем команду СпрСотр.Новый(), платформа инициализирует в памяти буфер для создания нового элемента справочника «Номенклатура». Объект СпрСотр переходит в состояние создания новой записи. Все последующие присвоения реквизитов (таких как Наименование, ЕдиницаИзмерения) записывают значения именно в этот временный буфер.

Однако, далее в коде встречается следующая конструкция:

Если СпрСотр.НайтиПоНаименованию(спр.Родитель, Спр.Родитель.Код) = 1 Тогда

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

После этого управление переходит к строке СпрСотр.Записать(). Но так как контекст нового элемента был утерян, а поиск родителя мог завершиться неудачно (или даже в случае успеха позиционировал объект на существующую группу), платформа не понимает, какую именно запись нужно модифицировать. В результате система генерирует ошибку выполнения «Элемент не выбран».

Ошибки синтаксиса при использовании метода НайтиПоНаименованию

Помимо логической ошибки с потерей контекста, в вызове метода поиска присутствует грубое нарушение синтаксиса платформы 1С 7.7. Разберем сигнатуру метода НайтиПоНаименованию:

НайтиПоНаименованию(<Наименование>, <Режим>, <ФлагПоиска>)

Проанализируем параметры, которые передаются в исходном коде автора:

  1. Первым параметром передается спр.Родитель. Это объект типа «СправочникСсылка» (в терминах v8, в v7.7 — это просто объект справочника). Метод же ожидает строковое значение. Платформа попытается автоматически преобразовать объект в строку, получив его текстовое представление (наименование), однако это является признаком плохой практики программирования. Правильно передавать строковый реквизит напрямую: спр.Родитель.Наименование.
  2. Вторым параметром передается Спр.Родитель.Код. В 1С 7.7 второй параметр <Режим> управляет областью поиска и может принимать только числовые значения: 0 — поиск по всему справочнику, 1 — поиск только среди элементов, подчиненных установленной группе (родителю). Передача кода справочника в качестве этого параметра ломает внутреннюю логику работы метода.
  3. Третий параметр <ФлагПоиска> полностью пропущен. По умолчанию он равен 0, что означает неточный поиск (по первым символам наименования). Это крайне опасно. Например, если в справочнике есть группы «Кабель» и «Кабель силовой», то при поиске строки «Кабель» система может позиционироваться на «Кабель силовой», если он будет находиться выше по индексу. Для точного соответствия необходимо всегда передавать третьим параметром значение 1.

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

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


СпрНом = СоздатьОбъект("Справочник.Номенклатура");
СпрПоискРодителя = СоздатьОбъект("Справочник.Номенклатура");
СпрЕд = СоздатьОбъект("Справочник.ЕдиницыИзмерений");

СпрМат = СоздатьОбъект("Справочник.Материалы");
СпрМат.ВыбратьЭлементы();

Пока СпрМат.ПолучитьЭлемент() = 1 Цикл
    // В 1С 7.7 проверку на группу лучше делать напрямую через метод объекта
    Если СпрМат.ЭтоГруппа() = 0 Тогда
        
        СпрНом.Новый();
        СпрНом.Наименование = СпрМат.Наименование;
        СпрНом.ПолнНаименование = СпрМат.Наименование;
        
        // Поиск родительской группы выполняем через отдельный объект
        Если ПустоеЗначение(СпрМат.Родитель) = 0 Тогда
            Если СпрПоискРодителя.НайтиПоНаименованию(СпрМат.Родитель.Наименование, 0, 1) = 1 Тогда
                СпрНом.Родитель = СпрПоискРодителя.ТекущийЭлемент();
            КонецЕсли;
        КонецЕсли;
        
        // Поиск единицы измерения также через отдельный объект
        Если СпрЕд.НайтиПоНаименованию(СпрМат.ЕдиницаИзмерения.Наименование, 0, 1) = 1 Тогда
            СпрНом.ЕдиницаИзмерения = СпрЕд.ТекущийЭлемент();
        КонецЕсли;
        
        СпрНом.Цена = СпрМат.Цена;
        СпрНом.ТипТовара = Перечисление.ТипыТоваров.Собственный;
        СпрНом.ТипНоменклатуры = Перечисление.ТипыНоменклатуры.Товар;
        
        СпрНом.Записать();
        Сообщить("Успешно перенесен элемент: " + СпрНом.Наименование);
    КонецЕсли;
КонецЦикла;

В этом примере для поиска родительской группы мы используем отдельную переменную СпрПоискРодителя. Контекст объекта СпрНом, в котором создается новый элемент номенклатуры, остается нетронутым, что полностью решает проблему возникновения ошибки «Элемент не выбран».

Оптимальный рекурсивный алгоритм переноса иерархии за один проход

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

Мы рекомендуем использовать более элегантное решение — рекурсивный метод создания родительских групп «на лету». Суть метода заключается в том, что при переносе очередного элемента мы проверяем наличие его родительской группы в справочнике-приемнике. Если группа не найдена, мы рекурсивно создаем ее (включая всю цепочку вышестоящих групп), после чего привязываем наш элемент к созданной структуре. Это позволяет перенести всю иерархию идеально за один проход выборки.

Рассмотрим детальный пример реализации такой обработки:


// Функция ищет группу в Номенклатуре по коду группы из Материалов. 
// Если группы нет — создает её рекурсивно, сохраняя всю цепочку родителей.
Функция ПолучитьРодителяНоменклатуры(РодительМатериала)
    Если ПустоеЗначение(РодительМатериала) = 1 Тогда
        Возврат ПолучитьПустоеЗначение("Справочник.Номенклатура");
    КонецЕсли;
    
    СпрНом = СоздатьОбъект("Справочник.Номенклатура");
    // Ищем группу по коду (поиск по коду надежнее, чем по наименованию)
    Если СпрНом.НайтиПоКоду(РодительМатериала.Код, 0) = 1 Тогда
        Возврат СпрНом.ТекущийЭлемент();
    КонецЕсли;
    
    // Если группа не найдена в Номенклатуре, создаем её
    СпрНом.НоваяГруппа();
    СпрНом.Код = РодительМатериала.Код;
    СпрНом.Наименование = РодительМатериала.Наименование;
    
    // Рекурсивный вызов для создания вышестоящего родителя
    СпрНом.Родитель = ПолучитьРодителяНоменклатуры(РодительМатериала.Родитель);
    СпрНом.Записать();
    
    Возврат СпрНом.ТекущийЭлемент();
КонецФункции

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

Давайте детально проанализируем, как работает этот алгоритм. В процедуре ВыполнитьПеренос мы открываем выборку элементов справочника «Материалы» без учета иерархии (параметр 0 в методе ВыбратьЭлементы). Это позволяет нам перебирать все элементы подряд. Если текущий объект выборки является группой (ЭтоГруппа() = 1), мы просто пропускаем его, так как все группы будут созданы автоматически по мере необходимости.

Когда мы встречаем элемент, мы передаем ссылку на его родителя в функцию ПолучитьРодителяНоменклатуры. Эта функция проверяет, не пустой ли родитель. Если родитель существует, мы пытаемся найти аналогичную группу в справочнике «Номенклатура» по уникальному коду группы источника. Если группа найдена — функция мгновенно возвращает ее ссылку. Если группа не найдена, функция создает новую группу в «Номенклатуре», присваивает ей код и наименование из источника, а затем рекурсивно вызывает саму себя для определения родителя этой новой группы. Таким образом, автоматически воссоздается вся цепочка вложенности любой глубины.

Важные нюансы переноса кодов и автонумерации в 1С 7.7

При программном переносе кодов элементов «один в один» (СпрНом.Код = СпрМат.Код) необходимо учитывать критические особенности платформы 1С 7.7, несоблюдение которых может привести к фатальным ошибкам при последующей работе пользователей в базе данных:

  1. Длина кода: Длина кода в справочнике-приемнике («Номенклатура») обязательно должна быть не меньше, чем в справочнике-источнике («Материалы»). Если в «Материалах» длина кода составляет 10 символов, а в «Номенклатуре» настроена длина 8 символов, то при записи платформа автоматически обрежет коды до 8 символов. Это неизбежно приведет к возникновению ошибок дублирования и неуникальности кодов при записи последующих элементов. Перед переносом обязательно проверьте эти настройки в Конфигураторе.
  2. Тип кода: Обратите внимание на тип кода справочников (Числовой или Текстовый). Если типы различаются (например, в «Материалах» код числовой, а в «Номенклатуре» текстовый), то при переносе коды могут получить лишние ведущие нули или пробелы, что усложнит последующий поиск. Рекомендуется приводить коды к единому формату с помощью функций форматирования строк.
  3. Сбой автонумерации: Если в «Номенклатуре» включена автоматическая нумерация элементов (что является стандартным поведением для большинства конфигураций 1С 7.7), ручная запись кодов во время переноса может сбить внутренний счетчик платформы. После завершения импорта пользователи при попытке создать новый элемент вручную могут начать получать ошибку «Код не уникальный!».

Для решения проблемы сбившейся автонумерации после завершения переноса данных настоятельно рекомендуется выполнить процедуру перенумерации справочника. Это можно сделать штатными средствами через режим «Тестирование и исправление ИБ» (установив флаг «Перенумерация справочников») либо воспользовавшись специализированными административными обработками (например, с диска ИТС).

Использование готовых универсальных инструментов ИТС

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

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

← На главную