При разработке обработок загрузки данных в клиент-серверной архитектуре 1С разработчики часто сталкиваются с типовой проблемой: диалог выбора файла и локальный файл доступны только на клиенте, в то время как создание и запись документов должны выполняться на стороне сервера. Кроме того, классический подход с использованием COMОбъект("Excel.Application") требует наличия установленного Microsoft Excel на сервере, не работает в среде Linux и существенно замедляет загрузку больших объемов данных.
Рассмотрим по шагам, как правильно организовать передачу файла с клиентской машины на сервер и прочитать данные из файла таблицы без прямого использования COM-объектов Excel.
В современных версиях платформы 1С:Предприятие 8.3 для работы с файлами применяется следующий стандартный алгоритм:
НачатьПомещениеФайла или НачатьПомещениеФайлов).ПолучитьИмяВременногоФайла.ТабличныйДокумент).Начиная с версии платформы 8.3.6, объект ТабличныйДокумент поддерживает метод Прочитать, который умеет напрямую открывать файлы форматов .xls и .xlsx без установки сторонних программ и библиотек.
Разберем реализацию этого способа. На форме создадим команду и опишем клиентские процедуры:
&НаКлиенте
Процедура ЗагрузитьИзExcel(Команда)
// Инициализируем диалог выбора файла
ДиалогВыбора = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
ДиалогВыбора.МножественныйВыбор = Ложь;
ДиалогВыбора.Фильтр = "Файлы Excel (*.xlsx;*.xls)|*.xlsx;*.xls";
ДиалогВыбора.Заголовок = "Выберите файл для загрузки";
// Описываем оповещение завершения выбора и помещения
ОповещениеЗавершения = Новый ОписаниеОповещения("ВыбратьИПоместитьФайлЗавершение", ЭтотОбъект);
// Асинхронно отправляем файл во временное хранилище
НачатьПомещениеФайла(ОповещениеЗавершения, "", ДиалогВыбора, Истина, УникальныйИдентификатор);
КонецПроцедуры
&НаКлиенте
Процедура ВыбратьИПоместитьФайлЗавершение(Результат, АдресХранилища, ВыбранноеИмяФайла, ДополнительныеПараметры) Экспорт
Если НЕ Результат Тогда
Возврат;
КонецЕсли;
// Передаем адрес временного хранилища на сервер для обработки
ОбработатьФайлНаСервере(АдресХранилища);
ПоказатьОповещениеПользователя("Загрузка данных", , "Обработка файла успешно завершена!");
КонецПроцедуры
Теперь напишем серверную часть, в которой прочитаем файл и создадим документ:
&НаСервере
Процедура ОбработатьФайлНаСервере(АдресХранилища)
// 1. Получаем двоичные данные из временного хранилища
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресХранилища);
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("xlsx");
ДвоичныеДанные.Записать(ИмяВременногоФайла);
// 2. Читаем файл в ТабличныйДокумент
ТабДок = Новый ТабличныйДокумент;
Попытка
ТабДок.Прочитать(ИмяВременногоФайла);
Исключение
УдалитьФайлы(ИмяВременногоФайла);
ВызватьИсключение "Не удалось прочитать файл: " + ОписаниеОшибки();
КонецПопытки;
// 3. Создаем документ и заполняем строки
НовыйДок = Документы.НачислениеЗарплаты.СоздатьДокумент();
НовыйДок.Дата = ТекущаяДата();
НовыйДок.Организация = Справочники.Организации.НайтиПоКоду("000000003");
ВидУдержания = ПланыВидовРасчета.Удержания.НайтиПоКоду("11112");
ВсегоСтрок = ТабДок.ВысотаТаблицы;
// Обходим строки со 2-й (предполагая, что 1-я строка — заголовок)
Для НомерСтроки = 2 По ВсегоСтрок Цикл
ТабНомерСтр = СокрЛП(ТабДок.Область(НомерСтроки, 2).Текст);
Если ПустаяСтрока(ТабНомерСтр) Тогда
Продолжить;
КонецЕсли;
// Преобразуем строковое представление суммы в число
ТекстСуммы = ТабДок.Область(НомерСтроки, 5).Текст;
ТекстСуммы = СтрЗаменить(ТекстСуммы, Символы.НПП, ""); // Удаляем неразрывные пробелы
ТекстСуммы = СтрЗаменить(ТекстСуммы, " ", ""); // Удаляем обычные пробелы
ТекстСуммы = СтрЗаменить(ТекстСуммы, ",", "."); // Приводим разделитель к точке
СуммаОбеда = Число(ТекстСуммы);
// Поиск физического лица
СотрудникСсылка = Справочники.ФизическиеЛица.НайтиПоКоду(ТабНомерСтр);
Если НЕ ЗначениеЗаполнено(СотрудникСсылка) Тогда
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Сотрудник с табельным номером " + ТабНомерСтр + " не найден в строке " + НомерСтроки;
Сообщение.Сообщить();
Продолжить;
КонецЕсли;
СтрокаТЧ = НовыйДок.Удержания.Добавить();
СтрокаТЧ.ФизическоеЛицо = СотрудникСсылка;
СтрокаТЧ.Удержание = ВидУдержания;
СтрокаТЧ.Результат = СуммаОбеда;
СтрокаТЧ.ДатаНачала = НовыйДок.Дата;
СтрокаТЧ.ДатаОкончания = НовыйДок.Дата;
КонецЦикла;
НовыйДок.Записать(РежимЗаписиДокумента.Запись);
// 4. Обязательно удаляем временный файл
УдалитьФайлы(ИмяВременногоФайла);
КонецПроцедуры
Если на сервере установлена ОС Windows и настроен OLE DB-провайдер Microsoft.ACE.OLEDB, для чтения больших таблиц можно применить подключение через ADODB. Это позволяет обращаться к листу Excel с помощью привычных SQL-запросов.
Посмотрим на пример функции чтения данных через ADO:
&НаСервере
Функция ПрочитатьExcelЧерезADO(ПутьКФайлу)
ТаблицаРезультат = Новый ТаблицаЗначений;
ТаблицаРезультат.Колонки.Добавить("ТабНомер", Новый ОписаниеТипов("Строка"));
ТаблицаРезультат.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
Соединение = Новый COMОбъект("ADODB.Connection");
СтрокаПодключения = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + ПутьКФайлу + ";Extended Properties=""Excel 12.0;HDR=YES;IMEX=1;"";";
Попытка
Соединение.Open(СтрокаПодключения);
Исключение
ВызватьИсключение "Ошибка подключения к Excel через OLEDB: " + ОписаниеОшибки();
КонецПопытки;
Каталог = Новый COMОбъект("ADOX.Catalog");
Каталог.ActiveConnection = Соединение;
ИмяПервогоЛиста = Каталог.Tables.Item(0).Name;
НаборЗаписей = Новый COMОбъект("ADODB.Recordset");
ТекстSQL = "SELECT * FROM [" + ИмяПервогоЛиста + "]";
НаборЗаписей.Open(ТекстSQL, Соединение, 3, 1);
Пока НЕ НаборЗаписей.EOF Цикл
ТабНомер = СокрЛП(Строка(НаборЗаписей.Fields.Item(1).Value));
Сумма = Число(НаборЗаписей.Fields.Item(2).Value);
Если ЗначениеЗаполнено(ТабНомер) Тогда
НовСтр = ТаблицаРезультат.Добавить();
НовСтр.ТабНомер = ТабНомер;
НовСтр.Сумма = Сумма;
КонецЕсли;
НаборЗаписей.MoveNext();
КонецЦикла;
НаборЗаписей.Close();
Соединение.Close();
Возврат ТаблицаРезультат;
КонецФункции
ТабличныйДокумент.Область().Текст разделителями разрядов часто выступают неразрывные пробелы (Символы.НПП). Всегда очищайте строку перед вызовом функции Число().USR1CV8) отсутствуют права на чтение/запись в каталоге временных файлов. Всегда оборачивайте работу с файлами в блок Попытка ... Исключение и очищайте временные данные через УдалитьФайлы().НайтиПоКоду или НайтиПоНаименованию на каждой итерации цикла создает множество отдельных обращений к СУБД. При обработке больших файлов заранее соберите все коды в массив и выполните один пакетный запрос к базе с условием ГДЕ Код В (&МассивКодов), выгрузив результат в объект Соответствие.