При автоматизации складского или бухгалтерского учета разработчики часто сталкиваются с задачей импорта данных из внешних файлов. Типичный сценарий: заказчик предоставляет файл Excel, содержащий сотни строк с товарными позициями. Наша задача — загрузить эти данные в 1С и создать на их основе документы, например, ПоступлениеТоваровУслуг.
Сложность заключается в том, что файл представляет собой «плоскую» таблицу, где строки могут относиться к разным поставщикам (контрагентам). Если просто перебирать строки файлы в цикле, мы рискуем создать отдельный документ на каждую строчку, что приведет к замусориванию базы данных. Нам же необходимо сгруппировать строки: если у одного контрагента в файле 15 строк товаров, программа должна создать ровно один документ с этого контрагента, в табличную часть которого будут добавлены все 15 позиций.
В этой статье мы подробно разберем типичные ошибки начинающих разработчиков при решении этой задачи, рассмотрим оптимальные алгоритмы группировки данных и напишем отказоустойчивый код для платформы 1С v8.3.
Прежде чем перейти к правильным алгоритмам, давайте выясним причины, по которым код автора из первого сообщения темы не работал или работал некорректно. Проанализируем ключевые ошибки в его логике:
СтрокаОтсчета в трех вложенных циклах Для. В 1С (как и во многих других языках программирования) изменение значения переменной-счетчика во внутреннем цикле ломает логику внешнего цикла. Это приводит к непредсказуемому поведению: бесконечным циклам или преждевременному выходу из них.НовыйДокумент.Записать(...) ошибочно обернута в цикл по количеству строк файла. Из-за этого один и тот же документ записывается десятки раз подряд без необходимости, создавая колоссальную нагрузку на СУБД.НовыйДокумент.Контрагент = ТекущееЗначение пытается напрямую записать текстовое наименование контрагента из ячейки Excel в реквизит с типом СправочникСсылка.Контрагенты. Это вызовет ошибку заполнения или оставит поле пустым. Перед записью текстовое значение необходимо сопоставить со справочником в базе данных.Excel.Cells(Row, Col).Value внутри нескольких вложенных циклов работает крайне медленно. Каждое такое обращение инициирует внешнее обращение к процессу Windows, что парализует работу системы при обработке файлов даже средней величины.Решением этих проблем является полный отказ от обработки данных «на лету» напрямую из COM-объекта Excel. Правильный подход состоит из трех этапов:
В современных конфигурациях 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. Цикл по уникальным контрагентам
Для Каждого СтрокаКонтрагента Из ТаблицаКонтрагентов Цикл
ТекущийКонтрагентТекст = СтрокаКонтрагента.КонтрагентИмя;
// Находим ссылку на Контрагента в базе 1С.
// Поиск по наименованию может быть неточным, но в рамках примера используем его.
СсылкаКонтрагент = Справочники.Контрагенты.НайтиПоНаименованию(ТекущийКонтрагентТекст, Истина);
Если СсылкаКонтрагент.Пустая() Тогда
// Здесь можно автоматически создать контрагента или зафиксировать ошибку
Сообщить("Предупреждение: Контрагент '" + ТекущийКонтрагентТекст + "' не найден в базе данных! Пропускаем.");
Продолжить;
КонецЕсли;
// Шаг 3. Создаем новый документ "Поступление товаров и услуг"
НовыйДокумент = Документы.ПоступлениеТоваровУслуг.СоздатьДокумент();
НовыйДокумент.Дата = ТекущаяДата();
НовыйДокумент.Организация = ВыбОрганизация;
НовыйДокумент.Склад = ВыбСклад;
НовыйДокумент.Контрагент = СсылкаКонтрагент;
// Заполнение договора по умолчанию для выбранного контрагента
НовыйДокумент.ДоговорКонтрагента = СсылкаКонтрагент.ОсновнойДоговорКонтрагента;
// Шаг 4. Находим все строки из исходной таблицы по текущему контрагенту
СтруктураОтбора = Новый Структура("КонтрагентИмя", ТекущийКонтрагентТекст);
СтрокиТоваровКонтрагента = ТаблицаДанных.НайтиСтроки(СтруктураОтбора);
// Шаг 5. Заполняем табличную часть "Товары"
Для Каждого СтрокаТовара Из СтрокиТоваровКонтрагента Цикл
// Сопоставляем номенклатуру
СсылкаНоменклатура = Справочники.Номенклатура.НайтиПоНаименованию(СтрокаТовара.НоменклатураИмя, Истина);
Если СсылкаНоменклатура.Пустая() Тогда
Сообщить("Товар '" + СтрокаТовара.НоменклатураИмя + "' не найден в справочнике Номенклатура!");
Продолжить;
КонецЕсли;
НоваяСтрокаТЧ = НовыйДокумент.Товары.Добавить();
НоваяСтрокаТЧ.Номенклатура = СсылкаНоменклатура;
НоваяСтрокаТЧ.Количество = СтрокаТовара.Количество;
НоваяСтрокаТЧ.Цена = СтрокаТовара.Цена;
НоваяСтрокаТЧ.Сумма = СтрокаТовара.Сумма;
// Проставляем счета учета (для Бухгалтерии предприятия)
НоваяСтрокаТЧ.СчетУчета = ПланыСчетов.Хозрасчетный.ТоварыНаСкладах;
КонецЦикла;
// Если в документ не добавилось ни одной строки товаров, не записываем его
Если НовыйДокумент.Товары.Количество() > 0 Тогда
НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
Сообщить("Успешно создан документ: " + Строка(НовыйДокумент) + " для контрагента: " + ТекущийКонтрагентТекст);
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Сообщить("Произошла ошибка при создании документов. Все изменения отменены! Описание: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Преимущества данного метода: Данный код легко читать и поддерживать. Метод НайтиСтроки() работает в оперативной памяти чрезвычайно быстро, что гарантирует высокую производительность алгоритма.
Этот метод не требует создания копий таблиц значений и отлично подходит для экономии оперативной памяти при обработке сверхбольших объемов данных. Идея метода заключается в следующем:
ТаблицаДанных.Сортировать("КонтрагентИмя"). После этого все одинаковые контрагенты выстраиваются друг за другом.Проанализируем программную реализацию линейного обхода:
Процедура СоздатьДокументыСГруппировкой_ВариантБ(ТаблицаДанных, ВыбОрганизация, ВыбСклад)
// Сортируем таблицу, чтобы одинаковые контрагенты шли подряд
ТаблицаДанных.Сортировать("КонтрагентИмя");
НачатьТранзакцию();
Попытка
НовыйДокумент = Неопределено;
ТекущийКонтрагентТекст = "";
Для Каждого СтрокаТаблицы Из ТаблицаДанных Цикл
// Если контрагент сменился (или это первая строка цикла)
Если СтрокаТаблицы.КонтрагентИмя <> ТекущийКонтрагентТекст Тогда
// Сначала запишем предыдущий документ, если он был создан и заполнен
Если НовыйДокумент <> Неопределено Тогда
Если НовыйДокумент.Товары.Количество() > 0 Тогда
НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
Сообщить("Создан документ: " + Строка(НовыйДокумент));
КонецЕсли;
КонецЕсли;
// Переключаемся на нового контрагента
ТекущийКонтрагентТекст = СтрокаТаблицы.КонтрагентИмя;
СсылкаКонтрагент = Справочники.Contragents.НайтиПоНаименованию(ТекущийКонтрагентТекст, Истина);
Если СсылкаКонтрагент.Пустая() Тогда
НовыйДокумент = Неопределено; // Сбрасываем ссылку на документ
Сообщить("Контрагент не найден: " + ТекущийКонтрагентТекст);
Продолжить;
КонецЕсли;
// Инициализируем новый документ
НовыйДокумент = Документы.ПоступлениеТоваровУслуг.СоздатьДокумент();
НовыйДокумент.Дата = ТекущаяДата();
НовыйДокумент.Организация = ВыбОрганизация;
НовыйДокумент.Склад = ВыбСклад;
НовыйДокумент.Контрагент = СсылкаКонтрагент;
НовыйДокумент.ДоговорКонтрагента = СсылкаКонтрагент.ОсновнойДоговорКонтрагента;
КонецЕсли;
// Добавляем строки в ТЧ текущего активного документа
Если НовыйДокумент <> Неопределено Тогда
СсылкаНоменклатура = Справочники.Номенклатура.НайтиПоНаименованию(СтрокаТаблицы.НоменклатураИмя, Истина);
Если СсылкаНоменклатура.Пустая() Тогда
Сообщить("Товар не найден: " + СтрокаТаблицы.НоменклатураИмя);
Продолжить;
КонецЕсли;
НоваяСтрока = НовыйДокумент.Товары.Добавить();
НоваяСтрока.Номенклатура = СсылкаНоменклатура;
НоваяСтрока.Количество = СтрокаТаблицы.Количество;
НоваяСтрока.Цена = СтрокаТаблицы.Цена;
НоваяСтрока.Сумма = СтрокаТаблицы.Сумма;
НоваяСтрока.СчетУчета = ПланыСчетов.Хозрасчетный.ТоварыНаСкладах;
КонецЕсли;
КонецЦикла;
// КРИТИЧЕСКИ ВАЖНО: Записываем самый последний документ после окончания цикла!
Если НовыйДокумент <> Неопределено И НовыйДокумент.Товары.Количество() > 0 Тогда
НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
Сообщить("Создан документ: " + Строка(НовыйДокумент));
КонецЕсли;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Сообщить("Произошла ошибка! Транзакция отменена. Подробности: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Преимущества данного метода: Минимальное потребление памяти, так как обход происходит «поточно» за один проход по исходной отсортированной таблице.
Чтобы ваша обработка работала стабильно в реальных условиях эксплуатации, обязательно обратите внимание на следующие архитектурные аспекты:
НачатьТранзакцию / ЗафиксироватьТранзакцию). При загрузке большого массива документов всегда заворачивайте весь процесс в единую транзакцию. Если в середине процесса произойдет сбой (например, отключится электроэнергия, или один из реквизитов окажется незаполненным), система автоматически откатит все изменения назад. Это предотвратит появление в базе данных "полузагруженных" пакетов документов.НайтиПоНаименованию) ненадежен, так как в базе могут находиться дубли с пробелами или знаками кавычек (например, «ООО "Ромашка"» и «ООО Ромашка»). По возможности всегда требуйте от составителей файлов указывать уникальный идентификатор контрагента — ИНН (и КПП для юридических лиц). Поиск по ИНН осуществляется следующим образом:
НайденнаяСсылка = Справочники.Контрагенты.НайтиПоРеквизиту("ИНН", ИННИзФайла);
НайденныйТовар = Справочники.Номенклатура.НайтиПоРеквизиту("Артикул", АртикулИзФайла);
НовыйДокумент.ПроверитьЗаполнение(). Это позволит вовремя обнаружить пропущенные обязательные поля конфигурации (такие как валюта договора, ставка НДС, подразделение организации и т.д.) до попытки записи в базу данных.Применяя предложенные алгоритмы группировки данных через промежуточные таблицы значений, вы сможете создавать надежные, быстродействующие и легко читаемые импортные обработки для любых конфигураций на платформе 1С.