При разработке обработок загрузки данных из внешних файлов (чаще всего это прайс-листы, накладные или спецификации в формате Excel) программисты регулярно сталкиваются с проблемой изменчивости структуры файлов. Сегодня колонка «Артикул» идет первой, завтра — пятой, а послезавтра поставщик добавляет новые колонки, сдвигая все данные вправо. Предоставление пользователю возможности вручную указывать номера колонок для каждого реквизита — стандартное и наиболее надежное решение. Однако если реквизитов много (номенклатура, артикул, количество, цена, ставка НДС, сумма НДС, единица измерения, штрихкод и т.д.), создание статических реквизитов под каждый номер колонки в метаданных обработки перегружает интерфейс и усложняет поддержку кода. Разберем, как спроектировать эту систему элегантно, эффективно и в соответствии со стандартами разработки на платформе 1С:Предприятие 8.3.
Создание жестких реквизитов объекта или формы вроде НомерКолонкиНоменклатура, НомерКолонкиКоличество и т.д. ведет к ряду архитектурных проблем. Во-первых, форма обработки превращается в «простыню» из десятков полей ввода чисел, в которых пользователю легко запутаться. Во-вторых, любое добавление нового реквизита загрузки требует изменения структуры метаданных, перерисовки интерфейса формы и доработки кода чтения данных. В-третьих, хранение таких настроек в разрезе разных поставщиков или разных типов файлов становится нетривиальной задачей, требующей написания громоздких механизмов сериализации.
Рассмотрим более гибкие, профессиональные альтернативы, которые успешно применяются в современной практике.
Вместо создания десятков отдельных реквизитов мы можем использовать одну табличную часть (или таблицу значений на форме), которая будет хранить пары «Имя реквизита 1С — Номер колонки в файле». Этот подход позволяет легко масштабировать список загружаемых полей без изменения структуры метаданных формы.
Создадим на форме таблицу значений (или табличную часть в объекте обработки) с именем НастройкиКолонок со следующей структурой:
ИмяРеквизита (тип: Строка) — внутреннее имя реквизита (например, "Номенклатура", "Артикул", "Количество").СинонимРеквизита (тип: Строка) — понятное пользователю представление.НомерКолонки (тип: Число) — номер колонки в файле Excel, сопоставленный данному реквизиту.Обязательное (тип: Булево) — флаг, указывающий, может ли загрузка выполняться без этого поля.Проанализируем программный код, который инициализирует эту таблицу при создании формы и запрещает пользователю нарушать ее целостность (добавлять или удалять строки):
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
ИнициализироватьТаблицуНастроек();
КонецПроцедуры
&НаСервере
Процедура ИнициализироватьТаблицуНастроек()
// Очищаем таблицу перед заполнением
Объект.НастройкиКолонок.Очистить();
// Задаем жесткий список поддерживаемых полей для загрузки
ДобавитьСтрокуНастройки("Номенклатура", "Наименование номенклатуры", Истинно, 1);
ДобавитьСтрокуНастройки("Артикул", "Артикул (код)", Ложь, 2);
ДобавитьСтрокуНастройки("Количество", "Количество", Истинно, 3);
ДобавитьСтрокуНастройки("Цена", "Цена без НДС", Истинно, 4);
ДобавитьСтрокуНастройки("СуммаНДС", "Сумма НДС", Ложь, 5);
ДобавитьСтрокуНастройки("Штрихкод", "Штрихкод товара", Ложь, 0); // 0 означает, что поле не считывается
КонецПроцедуры
&НаСервере
Процедура ДобавитьСтрокуНастройки(ИмяРеквизита, Синоним, Обязательное, НомерПоУмолчанию)
НоваяСтрока = Объект.НастройкиКолонок.Добавить();
НоваяСтрока.ИмяРеквизита = ИмяРеквизита;
НоваяСтрока.СинонимРеквизита = Синоним;
НоваяСтрока.Обязательное = Обязательное;
НоваяСтрока.НомерКолонки = НомерПоУмолчанию;
КонецПроцедуры
Чтобы пользователь не мог случайно удалить строки или добавить новые, для элемента управления «Таблица» на форме необходимо установить свойства: РазрешитьДобавление = Ложь, РазрешитьУдаление = Ложь. Колонки ИмяРеквизита и Обязательное следует сделать недоступными для редактирования, оставив открытой только колонку НомерКолонки.
Если мы хотим сохранить классический интерфейс с отдельными полями ввода, но при этом избежать изменения метаданных самой обработки, мы можем применить механизм программного создания реквизитов формы. Платформа 1С:Предприятие 8.3 позволяет «на лету» модифицировать состав реквизитов формы с помощью метода ИзменитьРеквизиты().
Рассмотрим по шагам реализацию этого механизма:
&НаСервере
Процедура СоздатьДинамическиеРеквизитыНаСервере()
// 1. Формируем массив описываемых полей
ПоляДляЗагрузки = ПолучитьОписаниеЗагружаемыхПолей();
МассивДобавляемыхРеквизитов = Новый Массив;
Для Каждого Поле Из ПоляДляЗагрузки Цикл
ИмяРеквизитаФормы = "Колонка_" + Поле.Имя;
// Описываем числовой тип для номера колонки
ОписаниеТипаЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(2, 0, ДопустимыйЗнак.Неотрицательный));
Реквизит = Новый РеквизитФормы(ИмяРеквизитаФормы, ОписаниеТипаЧисло, , Поле.Синоним, Истинно);
МассивДобавляемыхРеквизитов.Добавить(Реквизит);
КонецЦикла;
// Добавляем реквизиты на форму
ЭтаФорма.ИзменитьРеквизиты(МассивДобавляемыхРеквизитов);
// 2. Создаем элементы управления (поля ввода) на форме программно
ГруппаФормы = Элементы.ГруппаНастройкиКолонок; // Предварительно созданная группа на форме
Для Каждого Поле Из ПоляДляЗагрузки Цикл
ИмяРеквизитаФормы = "Колонка_" + Поле.Имя;
ИмяЭлемента = "Поле_" + Поле.Имя;
НовыйЭлемент = Элементы.Добавить(ИмяЭлемента, Тип("ПолеФормы"), ГруппаФормы);
НовыйЭлемент.Вид = ВидПоляФормы.ПолеВвода;
НовыйЭлемент.ПутьКДанным = ИмяРеквизитаФормы;
НовыйЭлемент.КнопкаОчистки = Истинно;
НовыйЭлемент.РежимВыбораИзСписка = Истинно;
// Наполняем выпадающий список номерами для удобства выбора
Для НомерКолонки = 1 По 30 Цикл
НовыйЭлемент.СписокВыбора.Добавить(НомерКолонки, "Колонка " + Строка(НомерКолонки));
КонецЦикла;
КонецЦикла;
КонецПроцедуры
&НаСервере
Функция ПолучитьОписаниеЗагружаемыхПолей()
Результат = Новый Массив;
Результат.Добавить(Новый Структура("Имя, Синоним", "Номенклатура", "Номенклатура"));
Результат.Добавить(Новый Структура("Имя, Синоним", "Артикул", "Артикул"));
Результат.Добавить(Новый Структура("Имя, Синоним", "Количество", "Количество"));
Результат.Добавить(Новый Структура("Имя, Синоним", "Цена", "Цена"));
Возврат Результат;
КонецФункции
Важным преимуществом этого метода является визуальное удобство: вместо ручного ввода цифр пользователь выбирает значение из удобного раскрывающегося списка («Колонка 1», «Колонка 2»). Для передачи выбранных настроек в модуль объекта при выполнении загрузки достаточно обойти динамические реквизиты формы и упаковать их в обычную структуру:
&НаКлиенте
Процедура ВыполнитьЗагрузку(Команда)
ПараметрыНастройки = Новый Структура;
ПоляДляЗагрузки = ПолучитьОписаниеЗагружаемыхПолейНаСервере();
Для Каждого Поле Из ПоляДляЗагрузки Цикл
ИмяРеквизитаФормы = "Колонка_" + Поле.Имя;
ПараметрыНастройки.Вставить(Поле.Имя, ЭтаФорма[ИмяРеквизитаФормы]);
КонецЦикла;
ВыполнитьЗагрузкуНаСервере(ПараметрыНастройки);
КонецПроцедуры
Очевидно, что заставлять пользователя сопоставлять колонки при каждом запуске обработки — это плохой тон в проектировании интерфейсов. Настройки должны сохраняться.
Если мы пошли по пути создания динамических реквизитов на форме (Решение 2), то включить сохранение настроек можно с помощью стандартного свойства формы СохраняемыеРеквизиты. Однако при работе с динамически созданными реквизитами стандартное сохранение через свойства формы может работать нестабильно. Более профессиональным решением является явное использование ХранилищаОбщихНастроек платформы.
Разберем, как программно сохранять и восстанавливать структуру сопоставления колонок для конкретного пользователя в разрезе различных контрагентов (поставщиков):
&НаСервере
Процедура СохранитьНастройкиСопоставления(ИмяПрофиля)
НастройкиДляСохранения = Новый Соответствие;
ПоляДляЗагрузки = ПолучитьОписаниеЗагружаемыхПолей();
Для Каждого Поле Из ПоляДляЗагрузки Цикл
ИмяРеквизитаФормы = "Колонка_" + Поле.Имя;
НастройкиДляСохранения.Вставить(Поле.Имя, ЭтаФорма[ИмяРеквизитаФормы]);
КонецЦикла;
// Сохраняем настройки в системное хранилище
КлючОбъекта = "ЗагрузкаИзExcel_СопоставлениеКолонок";
ХранилищеОбщихНастроек.Сохранить(КлючОбъекта, ИмяПрофиля, НастройкиДляСохранения);
КонецПроцедуры
&НаСервере
Процедура ВосстановитьНастройкиСопоставления(ИмяПрофиля)
КлючОбъекта = "ЗагрузкаИзExcel_СопоставлениеКолонок";
Настройки = ХранилищеОбщихНастроек.Загрузить(КлючОбъекта, ИмяПрофиля);
Если Настройки <> Неопределено Тогда
Для Каждого КлючИЗначение Из Настройки Цикл
ИмяРеквизитаФормы = "Колонка_" + КлючИЗначение.Ключ;
// Проверяем, существует ли динамический реквизит на форме
Если ПолучитьСвойстваСтруктурыФормы(ИмяРеквизитаФормы) Тогда
ЭтаФорма[ИмяРеквизитаФормы] = КлючИЗначение.Значение;
КонецЕсли;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
&НаСервере
Функция ПолучитьСвойстваСтруктурыФормы(ИмяРеквизита)
Попытка
Значение = ЭтаФорма[ИмяРеквизита];
Возврат Истинно;
Исключение
Возврат Ложь;
КонецПопытки;
КонецФункции
Такой подход позволяет организовать на форме полноценную работу с профилями настроек (например, «Прайс-лист Дистрибьюция», «Прайс-лист Импорт» и т. д.), переключение между которыми мгновенно перезаполняет номера колонок.
Чтобы максимально облегчить жизнь пользователю, мы можем применить алгоритм автоматического распознавания колонок при первом открытии файла Excel. Проанализируем первую строку файла (которая обычно содержит заголовки) и сопоставим ее со словарем известных синонимов.
Ниже представлен пример функции, которая анализирует массив прочитанных заголовков колонок файла и возвращает сопоставленные номера:
&НаСервере
Функция АвтоматическиОпределитьКолонки(МассивЗаголовковФайла) Экспорт
РезультатСопоставления = Новый Соответствие; // Ключ: ИмяРеквизита1С, Значение: НомерКолонкиФайла
// Инициализируем словарь синонимов для поиска совпадений
СловарьСинонимов = Новый Соответствие;
СловарьСинонимов.Вставить("номенклатура", "Номенклатура");
СловарьСинонимов.Вставить("товар", "Номенклатура");
СловарьСинонимов.Вставить("наименование", "Номенклатура");
СловарьСинонимов.Вставить("артикул", "Артикул");
СловарьСинонимов.Вставить("код", "Артикул");
СловарьСинонимов.Вставить("кол-во", "Количество");
СловарьСинонимов.Вставить("количество", "Количество");
СловарьСинонимов.Вставить("цена", "Цена");
СловарьСинонимов.Вставить("прайс", "Цена");
НомерКолонки = 1;
Для Каждого Заголовок Из МассивЗаголовковФайла Цикл
// Очищаем заголовок от лишних пробелов и приводим к нижнему регистру
ОчищенныйЗаголовок = НРег(СокрЛП(Заголовок));
ИмяРеквизита1С = СловарьСинонимов.Получить(ОчищенныйЗаголовок);
Если ИмяРеквизита1С <> Неопределено Тогда
// Если данное поле еще не было сопоставлено ранее
Если РезультатСопоставления.Получить(ИмяРеквизита1С) = Неопределено Тогда
РезультатСопоставления.Вставить(ИмяРеквизита1С, НомерКолонки);
КонецЕсли;
КонецЕсли;
НомерКолонки = НомерКолонки + 1;
КонецДляЦикла;
Возврат РезультатСопоставления;
КонецФункции
Для более продвинутого поиска, когда точного совпадения нет (например, опечатки или незначительные изменения в окончаниях слов), рекомендуется использовать алгоритмы нечеткого сравнения строк (например, вычисление расстояния Левенштейна). Если совпадение превышает порог в 80%, мы можем автоматически подставлять номер колонки.
Ранее классическим подходом к чтению Excel было использование объекта COMОбъект("Excel.Application"). Однако этот подход категорически непригоден для современных реалий работы платформы «1С:Предприятие 8.3»:
Для надежного, быстрого и кроссплатформенного чтения файлов Excel необходимо использовать штатный встроенный объект платформы — ТабличныйДокумент.
Рассмотрим, как прочитать файл Excel на сервере без привлечения сторонних COM-библиотек:
&НаСервере
Функция ПрочитатьExcelВТаблицуЗначений(ПутьКФайлу, СтрокаНачалаДанных = 2) Экспорт
ТабДок = Новый ТабличныйДокумент;
Попытка
// Метод поддерживает чтение форматов .xls, .xlsx, .ods
ТабДок.Прочитать(ПутьКФайлу, СпособЧтенияТабличногоДокумента.XLSX);
Исключение
ВызватьИсключение "Не удалось открыть файл. Возможно, формат не поддерживается: " + ОписаниеОшибки();
КонецПопытки;
КоличествоСтрок = ТабДок.ВысотаТаблицы;
КоличествоКолонок = ТабДок.ШиринаТаблицы;
// Конструируем результирующую Таблицу Значений
ТЗ = Новый ТаблицаЗначений;
Для НомерКолонки = 1 По КоличествоКолонок Цикл
ТЗ.Колонки.Добавить("Колонка_" + Строка(НомерКолонки));
КонецЦикла;
// Наполняем таблицу данными из табличного документа
Для НомерСтроки = СтрокаНачалаДанных По КоличествоСтрок Цикл
НоваяСтрока = ТЗ.Добавить();
Для НомерКолонки = 1 По КоличествоКолонок Цикл
ОбластьЯчейки = ТабДок.Область(НомерСтроки, НомерКолонки);
НоваяСтрока[НомерКолонки - 1] = ОбластьЯчейки.Текст;
КонецЦикла;
КонецДляЦикла;
Возврат ТЗ;
КонецФункции
Данный метод абсолютно кроссплатформен и работает на серверах под управлением Windows, Linux, а также в веб-клиентах и macOS. Скорость чтения через ТабличныйДокумент превосходит COM-соединение в несколько раз.
Подводя итоги, сформулируем практический план действий при разработке гибкого импорта из Excel с большим количеством параметров: