Загрузка данных из Excel в 1С – частая задача, особенно когда речь идет о наполнении справочников, таких как «Номенклатура». Однако, создание иерархической структуры групп при такой загрузке может стать настоящим вызовом. Мы сталкиваемся с ситуацией, когда элементы номенклатуры создаются, группы по иерархии тоже формируются, но сами элементы попадают не в те группы, в которые должны. Давайте вместе разберемся, почему так происходит и как правильно организовать процесс, чтобы сохранить необходимую иерархию.
Основная проблема, с которой мы столкнулись в данной ситуации, заключается в некорректном управлении переменной, отвечающей за родительскую группу. Как правило, это проявляется в строке кода вида:
// ... код обработки строки Excel ...
Родитель = Справочники.Номенклатура.ПустаяСсылка(); // или Родитель = "";
// ... создание элемента номенклатуры с этим родителем ...
Если строка Родитель = ""; или Родитель = Справочники.Номенклатура.ПустаяСсылка(); находится внутри цикла обработки каждой строки файла Excel, то при каждой новой итерации цикла переменная Родитель обнуляется. Это означает, что для каждого нового элемента или подгруппы, созданной в цикле, родительская группа будет либо не определена (корень справочника), либо привязана к последней явно назначенной группе, которая вскоре снова обнулится. В результате элементы могут попадать в корень справочника или в случайные группы, а не в ту группу, которая должна быть их непосредственным родителем согласно структуре Excel.
В нашем случае, как отмечает участник дискуссии, проблема заключалась именно в том, что «родитель в цикле при выборке каждой строки обнуляется». Мы видим, что группа создается и сохраняется в переменную «родитель», но при следующей строке эта переменная обнуляется, что приводит к потере контекста иерархии.
Очевидное и наиболее простое решение, предложенное в ходе обсуждения, заключается в корректном управлении переменной Родитель. Мы должны предотвратить ее обнуление при каждой итерации цикла, если это не требуется по логике иерархии. Если строка Родитель = ""; или Родитель = Справочники.Номенклатура.ПустаяСсылка(); изначально была помещена для инициализации, мы можем вынести ее за пределы цикла обработки строк или использовать ее только в тех местах, где действительно требуется сбросить родителя на корневой уровень.
Предположим, что мы читаем данные построчно и определяем уровень вложенности. Если строка является новой группой, мы устанавливаем ее как текущего родителя для последующих элементов. Если следующая строка имеет тот же или более глубокий уровень вложенности, она должна использовать этого родителя. Только если уровень вложенности уменьшается (мы выходим из текущей подгруппы), нам нужно будет найти нового родителя более высокого уровня.
// Переменная для хранения текущей родительской группы
ТекущийРодитель = Справочники.Номенклатура.ПустаяСсылка();
// Обход строк Excel
Для Каждой Строка Из ДанныеExcel Цикл
// Определяем уровень вложенности строки или ее тип (группа/элемент)
// Например, по значению в колонке "Уровень" или по отступу
УровеньСтроки = ОпределитьУровеньПостроке(Строка);
Если СтрокаЯвляетсяГруппой(Строка) Тогда
// Создаем или находим группу
НоваяГруппа = НайтиИлиСоздатьГруппу(Строка.Наименование, ТекущийРодитель);
// Важно: если это новая группа, она становится текущим родителем для следующих элементов
ТекущийРодитель = НоваяГруппа.Ссылка;
ИначеЕсли СтрокаЯвляетсяЭлементом(Строка) Тогда
// Создаем или находим элемент номенклатуры
НовыйЭлемент = НайтиИлиСоздатьЭлемент(Строка.Наименование, ТекущийРодитель);
// Родитель для элемента - это ТекущийРодитель, установленный ранее
КонецЕсли;
// Здесь может быть дополнительная логика по изменению ТекущийРодитель
// если уровень вложенности строки изменяется (например, возвращаемся на уровень выше)
// ...
КонецЦикла;
В этом примере переменная ТекущийРодитель управляется динамически и не обнуляется при каждой новой строке, позволяя сохранять контекст иерархии.
Прежде чем мы перейдем к более сложным алгоритмам, давайте рассмотрим, как вообще может быть представлена иерархия в самом файле Excel, ведь от этого зависит и логика нашего кода в 1С.
Структура с группировкой строк (OutlineLevel): В Excel существует встроенная функция "Группировать", которая позволяет создавать иерархию строк. Программно в 1С мы можем получить уровень вложенности для каждой строки с помощью свойства OutlineLevel у объекта строки табличного документа. Чем выше значение этого свойства, тем глубже уровень вложенности строки. Например, заголовок группы может иметь OutlineLevel = 1, подгруппа – 2, а товар внутри подгруппы – 3. Это очень удобный и надежный способ.
// Предположим, что ТабличныйДокумент - это загруженный файл Excel
Для Каждого СтрокаТД Из ТабличныйДокумент.Области(1, 1, ТабличныйДокумент.ВысотаТаблицы, ТабличныйДокумент.ШиринаТаблицы).Строки Цикл
УровеньСтроки = СтрокаТД.УровеньГруппировки; // Используем УровеньГруппировки
// Дальнейшая логика обработки в зависимости от УровеньСтроки
КонецЦикла;
Полный путь в отдельной колонке: Один из наиболее однозначных способов. В файле Excel может быть колонка, где для каждого товара или группы указан полный путь с использованием разделителей, например, "Бытовая техника|Кухня|Холодильники". Такой формат позволяет однозначно определить всю цепочку родительских групп и легко воссоздать ее в 1С, последовательно создавая или находя каждую часть пути.
Иерархия по отступам или пустым колонкам: Визуально иерархия может быть представлена отступами в названии (например, пробелы перед наименованием) или расположением групп и товаров в разных колонках (например, группы в колонке A, подгруппы в B, товары в C). Этот способ требует более сложного алгоритма для парсинга, так как необходимо анализировать содержимое ячеек или их положение, чтобы определить родительские элементы.
Явное указание родителя: В файле может быть отдельная колонка, где для каждого элемента или подгруппы явно указывается код или наименование его непосредственной родительской группы. Этот подход также очень удобен, но требует, чтобы родительские группы были уже созданы или их можно было найти по указанному идентификатору.
Для создания надежной иерархии мы можем использовать несколько алгоритмов, каждый из которых имеет свои преимущества.
Этот метод является одним из наиболее надежных и часто используется при сложных иерархических структурах, когда зависимости могут быть нелинейными или когда родительские элементы могут встречаться позже дочерних в файле. Мы разбиваем процесс на два этапа:
Первый проход: Создание всех групп. На этом этапе мы в цикле обходим все строки файла Excel и создаем все элементы, которые определяются как группы номенклатуры. На этом шаге мы не пытаемся установить их иерархические связи. Все созданные группы можно временно поместить в корень справочника или в специальную временную группу. Главное – создать их как объекты в базе и получить их ссылки. При этом, мы также можем создать временную таблицу или карту, связывающую наименование группы из Excel с ее ссылкой в 1С.
Второй проход: Установка родительских связей. После того как все группы созданы, мы снова обходим строки файла Excel. На этом этапе для каждого элемента (и группы, которая должна быть подгруппой) мы находим его родительскую группу (которая уже была создана на первом шаге) и устанавливаем соответствующую связь. Для поиска родителя мы используем информацию из Excel (например, полный путь или явное указание родителя) и нашу временную карту ссылок.
Преимущества: Позволяет избежать проблем с порядком следования элементов в файле Excel, прост в отладке и очень стабилен.
// Этап 1: Создание всех групп и сохранение их ссылок
КартаГрупп = Новый Соответствие(); // НаименованиеExcel -> Ссылка1С
Для Каждой СтрокаExcel Из ДанныеExcel Цикл
Если СтрокаЯвляетсяГруппой(СтрокаExcel) Тогда
НаименованиеГруппы = СтрокаExcel.ПолучитьЗначение("НаименованиеГруппы");
// Поиск существующей группы или создание новой
// (Родитель пока не устанавливается или устанавливается в корень)
ГруппаОбъект = Справочники.Номенклатура.НайтиПоНаименованию(НаименованиеГруппы, , , ВидНоменклатуры.Группа);
Если ГруппаОбъект.Пустая() Тогда
ГруппаОбъект = Справочники.Номенклатура.СоздатьГруппу();
ГруппаОбъект.Наименование = НаименованиеГруппы;
// Здесь может быть Родитель = Справочники.Номенклатура.ПустаяСсылка();
ГруппаОбъект.Записать();
КонецЕсли;
КартаГрупп.Вставить(НаименованиеГруппы, ГруппаОбъект.Ссылка);
КонецЕсли;
КонецЦикла;
// Этап 2: Установка родительских связей для групп и создание элементов
Для Каждой СтрокаExcel Из ДанныеExcel Цикл
Если СтрокаЯвляетсяГруппой(СтрокаExcel) Тогда
НаименованиеГруппы = СтрокаExcel.ПолучитьЗначение("НаименованиеГруппы");
ПолныйПуть = СтрокаExcel.ПолучитьЗначение("ПолныйПуть"); // Например: "Бытовая техника|Кухня"
// Получаем ссылку на текущую группу
ТекущаяГруппаСсылка = КартаГрупп.Получить(НаименованиеГруппы);
Если Не ТекущаяГруппаСсылка.Пустая() Тогда
// Определяем родителя по полному пути
РодительСсылка = ОпределитьРодителяПоПути(ПолныйПуть, КартаГрупп);
ГруппаОбъект = ТекущаяГруппаСсылка.ПолучитьОбъект();
Если ГруппаОбъект.Родитель <> РодительСсылка Тогда
ГруппаОбъект.Родитель = РодительСсылка;
ГруппаОбъект.Записать();
КонецЕсли;
КонецЕсли;
ИначеЕсли СтрокаЯвляетсяЭлементом(СтрокаExcel) Тогда
НаименованиеЭлемента = СтрокаExcel.ПолучитьЗначение("Наименование");
ПолныйПутьРодителя = СтрокаExcel.ПолучитьЗначение("ПолныйПутьРодителя"); // Например: "Бытовая техника|Кухня|Холодильники"
РодительЭлементаСсылка = ОпределитьРодителяПоПути(ПолныйПутьРодителя, КартаГрупп);
ЭлементОбъект = Справочники.Номенклатура.НайтиПоНаименованию(НаименованиеЭлемента, , РодительЭлементаСсылка);
Если ЭлементОбъект.Пустая() Тогда
ЭлементОбъект = Справочники.Номенклатура.СоздатьЭлемент();
ЭлементОбъект.Наименование = НаименованиеЭлемента;
ЭлементОбъект.Родитель = РодительЭлементаСсылка;
ЭлементОбъект.Записать();
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Функция-помощник для определения родителя по полному пути
Функция ОпределитьРодителяПоПути(ПолныйПуть, КартаГрупп)
Если ПолныйПуть = "" Тогда
Возврат Справочники.Номенклатура.ПустаяСсылка();
КонецЕсли;
МассивПути = СтрРазделить(ПолныйПуть, "|", Ложь);
Если МассивПути.Количество() > 1 Тогда
// Родитель - это предпоследний элемент пути
НаименованиеРодителя = МассивПути[МассивПути.Количество() - 2];
Возврат КартаГрупп.Получить(НаименованиеРодителя);
Иначе
Возврат Справочники.Номенклатура.ПустаяСсылка(); // Если путь состоит только из самой группы
КонецЕсли;
КонецФункции
Этот подход хорошо подходит для иерархии с неизвестным или динамическим уровнем вложенности, особенно если иерархия в Excel определяется по свойству OutlineLevel или отступам. Мы создаем функцию, которая при обработке строки, являющейся группой, вызывает саму себя для обработки дочерних элементов этой группы.
Принцип работы: Мы обходим строки Excel. Если находим группу, создаем ее и запоминаем как текущего родителя. Затем рекурсивно вызываем ту же функцию для обработки следующих строк, которые являются дочерними по отношению к текущей группе. Когда дочерние элементы заканчиваются (уровень вложенности возвращается на прежний или становится выше текущей группы), мы возвращаемся из рекурсивного вызова. При этом важно отслеживать текущий уровень вложенности, чтобы понимать, когда мы заходим в подгруппу, а когда выходим из нее.
Преимущества: Элегантность и естественность для древовидных структур. Подходит для работы с Excel, где иерархия задана через OutlineLevel.
// Основная функция для запуска рекурсивной загрузки
Процедура ЗагрузитьНоменклатуруРекурсивно(ДанныеExcel)
ТекущаяСтрокаИндекс = 0; // Глобальная или передаваемая переменная для отслеживания текущей строки
ЗагрузитьГруппуИЭлементы(ДанныеExcel, Справочники.Номенклатура.ПустаяСсылка(), ТекущаяСтрокаИндекс, 0);
КонецПроцедуры
// Рекурсивная функция
Функция ЗагрузитьГруппуИЭлементы(ДанныеExcel, РодительскаяСсылка, ТекущаяСтрокаИндекс, ТекущийУровеньВложенности)
Пока ТекущаяСтрокаИндекс < ДанныеExcel.Количество() Цикл
СтрокаExcel = ДанныеExcel[ТекущаяСтрокаИндекс];
УровеньСтроки = СтрокаExcel.ПолучитьЗначение("OutlineLevel"); // Или другой способ определения уровня
Если УровеньСтроки > ТекущийУровеньВложенности Тогда
// Это новая подгруппа или элемент на более глубоком уровне
Если СтрокаЯвляетсяГруппой(СтрокаExcel) Тогда
НоваяГруппа = Справочники.Номенклатура.СоздатьГруппу();
НоваяГруппа.Наименование = СтрокаExcel.ПолучитьЗначение("Наименование");
НоваяГруппа.Родитель = РодительскаяСсылка;
НоваяГруппа.Записать();
ТекущаяСтрокаИндекс = ТекущаяСтрокаИндекс + 1; // Переходим к следующей строке для рекурсивного вызова
// Рекурсивный вызов для дочерних элементов
ЗагрузитьГруппуИЭлементы(ДанныеExcel, НоваяГруппа.Ссылка, ТекущаяСтрокаИндекс, УровеньСтроки);
// После возврата из рекурсии, ТекущаяСтрокаИндекс уже будет обновлен
ИначеЕсли СтрокаЯвляетсяЭлементом(СтрокаExcel) Тогда
НовыйЭлемент = Справочники.Номенклатура.СоздатьЭлемент();
НовыйЭлемент.Наименование = СтрокаExcel.ПолучитьЗначение("Наименование");
НовыйЭлемент.Родитель = РодительскаяСсылка;
НовыйЭлемент.Записать();
ТекущаяСтрокаИндекс = ТекущаяСтрокаИндекс + 1;
КонецЕсли;
ИначеЕсли УровеньСтроки < ТекущийУровеньВложенности Тогда
// Мы вышли из текущей группы, возвращаемся из рекурсии
Возврат;
Иначе // УровеньСтроки = ТекущийУровеньВложенности
// Это братский элемент или группа на том же уровне
Если СтрокаЯвляетсяГруппой(СтрокаExcel) Тогда
НоваяГруппа = Справочники.Номенклатура.СоздатьГруппу();
НоваяГруппа.Наименование = СтрокаExcel.ПолучитьЗначение("Наименование");
НоваяГруппа.Родитель = РодительскаяСсылка;
НоваяГруппа.Записать();
ТекущаяСтрокаИндекс = ТекущаяСтрокаИндекс + 1;
// Рекурсивный вызов для дочерних элементов этой братской группы
ЗагрузитьГруппуИЭлементы(ДанныеExcel, НоваяГруппа.Ссылка, ТекущаяСтрокаИндекс, УровеньСтроки);
ИначеЕсли СтрокаЯвляетсяЭлементом(СтрокаExcel) Тогда
НовыйЭлемент = Справочники.Номенклатура.СоздатьЭлемент();
НовыйЭлемент.Наименование = СтрокаExcel.ПолучитьЗначение("Наименование");
НовыйЭлемент.Родитель = РодительскаяСсылка;
НовыйЭлемент.Записать();
ТекущаяСтрокаИндекс = ТекущаяСтрокаИндекс + 1;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецФункции
Этот подход предполагает, что данные из Excel сначала полностью загружаются во внутреннюю структуру 1С, такую как ДеревоЗначений или ТаблицаЗначений. Это позволяет нам сначала построить полную иерархию в памяти программы, а затем, обходя это дерево или таблицу, последовательно создавать соответствующие группы и элементы в справочнике «Номенклатура».
Принцип работы:
ДеревоЗначений. Каждая строка Excel становится строкой дерева, а колонки Excel – колонками дерева. На этом этапе мы также можем добавить колонки для хранения ссылок на уже созданные группы 1С.OutlineLevel), мы можем в цикле заполнить ДеревоЗначений, создавая дочерние узлы в соответствии с уровнями вложенности.ДеревоЗначений полностью сформировано и отражает желаемую иерархию, мы обходим это дерево. Для каждого узла дерева (который может быть группой или элементом) мы создаем соответствующий объект справочника «Номенклатура», присваиваем ему родителя (который является ссылкой на объект 1С, соответствующий родительскому узлу в ДеревеЗначений) и записываем его.Преимущества: Полный контроль над данными до их записи в базу, возможность предварительной валидации и манипуляций, удобство в работе с уже структурированными данными.
// 1. Загрузка данных из Excel в ТаблицуЗначений
ТаблицаДанных = Новый ТаблицаЗначений;
ТаблицаДанных.Колонки.Добавить("Наименование");
ТаблицаДанных.Колонки.Добавить("Тип"); // "Группа" или "Элемент"
ТаблицаДанных.Колонки.Добавить("Уровень");
ТаблицаДанных.Колонки.Добавить("РодительНаименование"); // Для явного указания родителя
ТаблицаДанных.Колонки.Добавить("Ссылка1С", Новый ОписаниеТипов("СправочникСсылка.Номенклатура")); // Для временного хранения ссылок
// ... Заполнение ТаблицыДанных из Excel ...
// 2. Построение ДереваЗначений и создание иерархии в памяти
// Этот шаг может быть опциональным, если иерархия уже есть в ТаблицеДанных
ДеревоНоменклатуры = Новый ДеревоЗначений;
ДеревоНоменклатуры.Колонки.Добавить("Наименование");
ДеревоНоменклатуры.Колонки.Добавить("Тип");
ДеревоНоменклатуры.Колонки.Добавить("РодительСсылка1С", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ДеревоНоменклатуры.Колонки.Добавить("Ссылка1С", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
// Функция для рекурсивного заполнения дерева и создания групп
Функция ЗаполнитьДеревоИСоздатьГруппы(ТаблицаДанных, ТекущийРодительУзелДерева = Неопределено, ТекущийУровеньВложенности = 0)
// Итерации по ТаблицеДанных, определяем уровни, создаем узлы
// и рекурсивно вызываем ЗаполнитьДеревоИСоздатьГруппы для дочерних узлов
// При создании групп, записываем их в 1С и сохраняем ссылку в Ссылка1С узла дерева
// ... сложная логика парсинга и создания узлов ...
КонецФункции;
// 3. Обход ДереваЗначений и создание элементов в 1С
Процедура ОбходДереваИСозданиеЭлементов(УзлыДерева, РодительскаяСсылка1С)
Для Каждого Узел Из УзлыДерева Цикл
// Создаем группу или элемент в 1С
Если Узел.Тип = "Группа" Тогда
НоваяГруппа = Справочники.Номенклатура.СоздатьГруппу();
НоваяГруппа.Наименование = Узел.Наименование;
НоваяГруппа.Родитель = РодительскаяСсылка1С;
НоваяГруппа.Записать();
Узел.Ссылка1С = НоваяГруппа.Ссылка;
// Рекурсивный вызов для дочерних элементов этой группы
ОбходДереваИСозданиеЭлементов(Узел.Строки, НоваяГруппа.Ссылка);
ИначеЕсли Узел.Тип = "Элемент" Тогда
НовыйЭлемент = Справочники.Номенклатура.СоздатьЭлемент();
НовыйЭлемент.Наименование = Узел.Наименование;
НовыйЭлемент.Родитель = РодительскаяСсылка1С;
НовыйЭлемент.Записать();
Узел.Ссылка1С = НовыйЭлемент.Ссылка;
КонецЕсли;
КонецЦикла;
КонецПроцедуры;
// Запуск процесса
// Заполняем ДеревоНоменклатуры из ТаблицыДанных
// ...
// После заполнения
ОбходДереваИСозданиеЭлементов(ДеревоНоменклатуры.Строки, Справочники.Номенклатура.ПустаяСсылка());
Помимо выбора алгоритма, мы должны учитывать несколько критически важных аспектов для успешной и стабильной загрузки.
Управление переменной "Родитель": Как мы уже выяснили, это ключевой момент. Мы должны разработать логику, которая будет отслеживать текущего родителя при переходе от строки к строке. Мы должны динамически устанавливать родителя, основываясь на уровне вложенности текущей строки относительно предыдущей. Если уровень вложенности увеличивается, текущий элемент или группа становится родителем. Если уменьшается, нам нужно вернуться на несколько уровней выше, чтобы найти правильного родителя.
Поиск существующих групп и элементов: Перед созданием новой группы или элемента всегда следует проверять, не были ли они созданы ранее. Это поможет избежать дублирования данных. Поиск можно осуществлять по наименованию и, если это возможно, по родителю. Например, Справочники.Номенклатура.НайтиПоНаименованию(Наименование, Истина, Родитель).
// Пример поиска группы перед созданием
ГруппаСуществующая = Справочники.Номенклатура.НайтиПоНаименованию(НаименованиеГруппы, Истина, РодительСсылка, ВидНоменклатуры.Группа);
Если ГруппаСуществующая.Пустая() Тогда
// Создаем новую группу
ОбъектГруппы = Справочники.Номенклатура.СоздатьГруппу();
ОбъектГруппы.Наименование = НаименованиеГруппы;
ОбъектГруппы.Родитель = РодительСсылка;
ОбъектГруппы.Записать();
Возврат ОбъектГруппы.Ссылка;
Иначе
Возврат ГруппаСуществующая;
КонецЕсли;
Фоновая загрузка: При работе с большими объемами данных (тысячи строк в Excel) загрузку целесообразно выполнять в фоновом режиме. Это позволяет не блокировать интерфейс пользователя и избежать таймаутов. Для этого мы можем использовать фоновые задания 1С. При передаче файла в фоновое задание есть свои особенности, связанные с временным хранилищем. Обычно файл сначала загружается во временное хранилище 1С, а затем его адрес передается в качестве параметра фоновому заданию.
// Пример вызова фонового задания
Если ФоноваяЗагрузка Тогда
ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ПутьКФайлу);
АдресВременногоХранилища = ПоместитьВоВременноеХранилище(ДвоичныеДанныеФайла, УникальныйИдентификатор);
ПараметрыЗадания = Новый Структура;
ПараметрыЗадания.Вставить("АдресФайла", АдресВременногоХранилища);
// Дополнительные параметры
ФоновыеЗадания.Выполнить("МояОбработка.МодульМенеджера.ВыполнитьЗагрузку", ПараметрыЗадания, "Загрузка номенклатуры из Excel");
Иначе
// Выполнить загрузку в текущем сеансе
МояОбработка.МодульМенеджера.ВыполнитьЗагрузку(ПараметрыЗагрузки);
КонецЕсли;
Обработка ошибок и транзакции: Весь процесс загрузки рекомендуется оборачивать в транзакцию. В случае возникновения ошибки, это позволит нам откатить все изменения, сделанные в процессе загрузки, и избежать частичной или некорректной загрузки данных, которая может нарушить целостность справочника. Мы используем методы НачатьТранзакцию(), ЗафиксироватьТранзакцию() и ОтменитьТранзакцию().
Попытка
НачатьТранзакцию();
// ... Ваш код загрузки номенклатуры ...
ЗафиксироватьТранзакцию();
Сообщить("Загрузка успешно завершена!");
Исключение
ОтменитьТранзакцию();
Сообщить("Ошибка при загрузке: " + ОписаниеОшибки(), СтатусСообщения.Важное);
ВызватьИсключение; // Перевыбросить исключение, если это необходимо
КонецПопытки;
Правильное управление переменной Родитель, выбор подходящего алгоритма и учет всех нюансов, таких как поиск существующих элементов и обработка ошибок, помогут нам создать стабильную и надежную систему загрузки номенклатуры из Excel в 1С с сохранением иерархической структуры. Даже если прямое решение не сработало, понимание принципов, как показал опыт из дискуссии, приводит к успешному результату.