Как загрузить данные из Excel в 1С и создать документы с группировкой по контрагентам?

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

При автоматизации складского или бухгалтерского учета разработчики часто сталкиваются с задачей импорта данных из внешних файлов. Типичный сценарий: заказчик предоставляет файл Excel, содержащий сотни строк с товарными позициями. Наша задача — загрузить эти данные в 1С и создать на их основе документы, например, ПоступлениеТоваровУслуг.

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

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

Анализ критических ошибок в исходном коде

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

  1. Конфликт переменных в циклах. Автор использует одну и ту же переменную СтрокаОтсчета в трех вложенных циклах Для. В 1С (как и во многих других языках программирования) изменение значения переменной-счетчика во внутреннем цикле ломает логику внешнего цикла. Это приводит к непредсказуемому поведению: бесконечным циклам или преждевременному выходу из них.
  2. Запись документа внутри цикла перебора строк. Конструкция НовыйДокумент.Записать(...) ошибочно обернута в цикл по количеству строк файла. Из-за этого один и тот же документ записывается десятки раз подряд без необходимости, создавая колоссальную нагрузку на СУБД.
  3. Попытка присвоить строковое значение ссылочному реквизиту. Строка кода НовыйДокумент.Контрагент = ТекущееЗначение пытается напрямую записать текстовое наименование контрагента из ячейки Excel в реквизит с типом СправочникСсылка.Контрагенты. Это вызовет ошибку заполнения или оставит поле пустым. Перед записью текстовое значение необходимо сопоставить со справочником в базе данных.
  4. Использование COM-соединения в глубоких циклах. Обращение к ячейкам Excel через Excel.Cells(Row, Col).Value внутри нескольких вложенных циклов работает крайне медленно. Каждое такое обращение инициирует внешнее обращение к процессу Windows, что парализует работу системы при обработке файлов даже средней величины.

Решением этих проблем является полный отказ от обработки данных «на лету» напрямую из COM-объекта Excel. Правильный подход состоит из трех этапов:

  1. Считывание файла Excel целиком во временную таблицу значений (ТЗ) в памяти 1С.
  2. Поиск и сопоставление текстовых данных с элементами справочников 1С (номенклатура, контрагенты, склады).
  3. Группировка таблицы и создание документов в транзакции.

Подготовка данных: Быстрое чтение Excel без COM-объекта

В современных конфигурациях 1С на управляемых формах использование COMОбъект("Excel.Application") считается плохой практикой. Этот метод требует установленного Microsoft Office на сервере приложений, работает медленно и часто зависает в памяти. Вместо этого мы используем встроенный объект платформы ТабличныйДокумент, который умеет читать файлы .xlsx напрямую и невероятно быстро.

Рассмотрим процедуру загрузки данных из файла в коллекцию ТаблицаЗначений:


Функция ПрочитатьФайлExcelВТаблицу(ПутьКФайлу) Экспорт
    
    ТабДок = Новый ТабличныйДокумент;
    Попытка
        ТабДок.Прочитать(ПутьКФайлу, СпособЧтенияЗначенийТабличногоДокумента.Значение);
    Исключение
        ВызватьИсключение "Не удалось прочитать файл! Ошибка: " + ОписаниеОшибки();
    КонецПопытки;
    
    // Создаем таблицу значений для хранения сырых данных
    ТЗ_Данные = Новый ТаблицаЗначений;
    ТЗ_Данные.Колонки.Добавить("КонтрагентИмя", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(150)));
    ТЗ_Данные.Колонки.Добавить("НоменклатураИмя", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(150)));
    ТЗ_Данные.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисел(15, 3)));
    ТЗ_Данные.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисел(15, 2)));
    ТЗ_Данные.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисел(15, 2)));
    
    КоличествоСтрок = ТабДок.ВысотаТаблицы;
    
    // Предполагаем, что 1-я строка — это шапка таблицы, данные начинаются со 2-й строки
    Для НомерСтроки = 2 По КоличествоСтрок Цикл
        
        // Получаем значения ячеек напрямую из ТабДок
        КонтрагентТекст = СокрЛП(ТабДок.Область(НомерСтроки, 1).Текст); // 1-я колонка
        ТоварТекст      = СокрЛП(ТабДок.Область(НомерСтроки, 2).Текст); // 2-я колонка
        КоличествоЧисло = Число(ТабДок.Область(НомерСтроки, 3).Значение); // 3-я колонка
        ЦенаЧисло       = Число(ТабДок.Область(НомерСтроки, 4).Значение); // 4-я колонка
        
        // Пропускаем пустые строки
        Если ПустаяСтрока(КонтрагентТекст) И ПустаяСтрока(ТоварТекст) Тогда
            Продолжить;
        КонецЕсли;
        
        НоваяСтрока = ТЗ_Данные.Добавить();
        НоваяСтрока.КонтрагентИмя  = КонтрагентТекст;
        НоваяСтрока.НоменклатураИмя = ТоварТекст;
        НоваяСтрока.Количество      = КоличествоЧисло;
        НоваяСтрока.Цена            = ЦенаЧисло;
        НоваяСтрока.Сумма           = КоличествоЧисло * ЦенаЧисло;
        
    КонецЦикла;
    
    Возврат ТЗ_Данные;
    
КонецФункции

Использование этого метода позволяет прочитать тысячи строк из Excel за доли секунды, предоставив нам структурированную таблицу значений ТЗ_Данные для дальнейшей обработки.


Вариант группировки А: Через копирование и свертку таблицы значений (Рекомендуемый)

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

  1. Создаем копию нашей таблицы данных.
  2. Удаляем из копии все лишние колонки, оставив только уникальные ключевые поля (в нашем случае — КонтрагентИмя).
  3. Сворачиваем копию таблицы по колонке КонтрагентИмя. Таким образом мы получаем список уникальных контрагентов, по которым нужно создать документы.
  4. Запускаем цикл по полученному списку уникальных контрагентов.
  5. Внутри цикла для текущего контрагента создаем один документ 1С, заполняем реквизиты шапки.
  6. С помощью метода таблицы значений НайтиСтроки() отбираем из исходной таблицы только те строки, которые относятся к текущему контрагенту, и переносим их в табличную часть документа.
  7. Записываем готовый документ один раз в конце итерации цикла.

Рассмотрим программную реализацию этого алгоритма с сопоставлением типов данных:


Процедура СоздатьДокументыСГруппировкой_ВариантА(ТаблицаДанных, ВыбОрганизация, ВыбСклад)
    
    // Шаг 1. Получаем список уникальных контрагентов из таблицы данных
    ТаблицаКонтрагентов = ТаблицаДанных.Скопировать();
    ТаблицаКонтрагентов.Свернуть("КонтрагентИмя");
    
    НачатьТранзакцию();
    
    Попытка
        
        // Шаг 2. Цикл по уникальным контрагентам
        Для Каждого СтрокаКонтрагента Из ТаблицаКонтрагентов Цикл
            
            ТекущийКонтрагентТекст = СтрокаКонтрагента.КонтрагентИмя;
            
            // Находим ссылку на Контрагента в базе 1С. 
            // Поиск по наименованию может быть неточным, но в рамках примера используем его.
            СсылкаКонтрагент = Справочники.Контрагенты.НайтиПоНаименованию(ТекущийКонтрагентТекст, Истина);
            
            Если СсылкаКонтрагент.Пустая() Тогда
                // Здесь можно автоматически создать контрагента или зафиксировать ошибку
                Сообщить("Предупреждение: Контрагент '" + ТекущийКонтрагентТекст + "' не найден в базе данных! Пропускаем.");
                Продолжить;
            КонецЕсли;
            
            // Шаг 3. Создаем новый документ "Поступление товаров и услуг"
            НовыйДокумент = Документы.ПоступлениеТоваровУслуг.СоздатьДокумент();
            НовыйДокумент.Дата = ТекущаяДата();
            НовыйДокумент.Организация = ВыбОрганизация;
            НовыйДокумент.Склад = ВыбСклад;
            НовыйДокумент.Контрагент = СсылкаКонтрагент;
            
            // Заполнение договора по умолчанию для выбранного контрагента
            НовыйДокумент.ДоговорКонтрагента = СсылкаКонтрагент.ОсновнойДоговорКонтрагента;
            
            // Шаг 4. Находим все строки из исходной таблицы по текущему контрагенту
            СтруктураОтбора = Новый Структура("КонтрагентИмя", ТекущийКонтрагентТекст);
            СтрокиТоваровКонтрагента = ТаблицаДанных.НайтиСтроки(СтруктураОтбора);
            
            // Шаг 5. Заполняем табличную часть "Товары"
            Для Каждого СтрокаТовара Из СтрокиТоваровКонтрагента Цикл
                
                // Сопоставляем номенклатуру
                СсылкаНоменклатура = Справочники.Номенклатура.НайтиПоНаименованию(СтрокаТовара.НоменклатураИмя, Истина);
                
                Если СсылкаНоменклатура.Пустая() Тогда
                    Сообщить("Товар '" + СтрокаТовара.НоменклатураИмя + "' не найден в справочнике Номенклатура!");
                    Продолжить;
                КонецЕсли;
                
                НоваяСтрокаТЧ = НовыйДокумент.Товары.Добавить();
                НоваяСтрокаТЧ.Номенклатура = СсылкаНоменклатура;
                НоваяСтрокаТЧ.Количество   = СтрокаТовара.Количество;
                НоваяСтрокаТЧ.Цена         = СтрокаТовара.Цена;
                НоваяСтрокаТЧ.Сумма        = СтрокаТовара.Сумма;
                
                // Проставляем счета учета (для Бухгалтерии предприятия)
                НоваяСтрокаТЧ.СчетУчета = ПланыСчетов.Хозрасчетный.ТоварыНаСкладах;
                
            КонецЦикла;
            
            // Если в документ не добавилось ни одной строки товаров, не записываем его
            Если НовыйДокумент.Товары.Количество() > 0 Тогда
                НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
                Сообщить("Успешно создан документ: " + Строка(НовыйДокумент) + " для контрагента: " + ТекущийКонтрагентТекст);
            КонецЕсли;
            
        КонецЦикла;
        
        ЗафиксироватьТранзакцию();
        
    Исключение
        ОтменитьТранзакцию();
        Сообщить("Произошла ошибка при создании документов. Все изменения отменены! Описание: " + ОписаниеОшибки());
    КонецПопытки;
    
КонецПроцедуры

Преимущества данного метода: Данный код легко читать и поддерживать. Метод НайтиСтроки() работает в оперативной памяти чрезвычайно быстро, что гарантирует высокую производительность алгоритма.


Вариант группировки Б: Через предварительную сортировку и линейный обход

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

  1. Сортируем исходную таблицу по ключевому полю: ТаблицаДанных.Сортировать("КонтрагентИмя"). После этого все одинаковые контрагенты выстраиваются друг за другом.
  2. Перебираем таблицу в одном-единственном цикле.
  3. Следим за моментом, когда значение контрагента в текущей строке меняется по сравнению с предыдущей.
  4. Если контрагент изменился, мы записываем текущий созданный документ, обнуляем переменную и открываем новый документ для нового контрагента.

Проанализируем программную реализацию линейного обхода:


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

Преимущества данного метода: Минимальное потребление памяти, так как обход происходит «поточно» за один проход по исходной отсортированной таблице.


Важные рекомендации по повышению надежности загрузки

Чтобы ваша обработка работала стабильно в реальных условиях эксплуатации, обязательно обратите внимание на следующие архитектурные аспекты:

  1. Использование транзакций (НачатьТранзакцию / ЗафиксироватьТранзакцию). При загрузке большого массива документов всегда заворачивайте весь процесс в единую транзакцию. Если в середине процесса произойдет сбой (например, отключится электроэнергия, или один из реквизитов окажется незаполненным), система автоматически откатит все изменения назад. Это предотвратит появление в базе данных "полузагруженных" пакетов документов.
  2. Поиск по уникальным реквизитам. Поиск контрагентов по наименованию (НайтиПоНаименованию) ненадежен, так как в базе могут находиться дубли с пробелами или знаками кавычек (например, «ООО "Ромашка"» и «ООО Ромашка»). По возможности всегда требуйте от составителей файлов указывать уникальный идентификатор контрагента — ИНН (и КПП для юридических лиц). Поиск по ИНН осуществляется следующим образом: НайденнаяСсылка = Справочники.Контрагенты.НайтиПоРеквизиту("ИНН", ИННИзФайла);
  3. Поиск номенклатуры по коду или артикулу. Аналогично контрагентам, товары в файле могут иметь орфографические ошибки в названиях. Самым надежным способом сопоставления номенклатуры является поиск по Артикулу, Коду или Штрихкоду. Для поиска по артикулу используйте метод:
    
    НайденныйТовар = Справочники.Номенклатура.НайтиПоРеквизиту("Артикул", АртикулИзФайла);
    
  4. Проверка заполнения обязательных реквизитов. Перед записью нового документа программно проверяйте его готовность методом НовыйДокумент.ПроверитьЗаполнение(). Это позволит вовремя обнаружить пропущенные обязательные поля конфигурации (такие как валюта договора, ставка НДС, подразделение организации и т.д.) до попытки записи в базу данных.

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

← На главную