При разработке обработок загрузки данных из внешних источников (например, файлов Microsoft Excel) начинающие разработчики часто сталкиваются со следующей ошибкой компиляции в тонком клиенте:
{Обработка.ЧтениеИзExcel.Форма.Форма.Форма(75,9)}: Переменная не определена (справочники)
Эта проблема возникает при попытке выполнить код выборки или поиска элементов справочника непосредственно в клиентских процедурах с директивой компиляции &НаКлиенте. В этой статье мы подробно разберем, почему возникает данная ошибка, изучим архитектурные особенности платформы 1С:Предприятие 8.3 и построим правильный, отказоустойчивый и быстрый алгоритм загрузки данных с последующим открытием и заполнением форм элементов.
Для начала проанализируем архитектуру управляемого приложения. Платформа строго разделяет выполнение кода на клиентскую часть (тонкий или веб-клиент) и серверную часть. Ключевое правило распределения ресурсов гласит: прямая работа с базой данных на клиенте запрещена.
Объект метаданных Справочники (глобальный контекст менеджера справочников) существует исключительно на сервере. На клиенте вы не можете:
Справочники.ОсновныеСредства.Выбрать() для обхода элементов.НайтиПоНаименованию() или НайтиПоРеквизиту().СоздатьЭлемент().Попытка обхода элементов справочника в цикле на клиенте (даже если бы это было технически возможно) привела бы к катастрофическому падению производительности из-за сотен микрозапросов между клиентским приложением и сервером СУБД. Наша задача — спроектировать процесс так, чтобы вся тяжелая работа выполнялась на сервере за один вызов.
Вместо того чтобы читать Excel-файл построчно на клиенте с помощью медленных COM-объектов (таких как Excel.Application), которые к тому же не работают на операционных системах Linux, мы применим современный кроссплатформенный подход 1С.
Сначала мы предложим пользователю выбрать файл на клиенте, поместим его во временное хранилище и передадим полученный адрес на сервер. Рассмотрим код клиентской процедуры:
&НаКлиенте
Процедура ЗагрузитьФайлExcel(Команда)
// Описываем параметры диалога выбора файла
ПараметрыДиалога = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
ПараметрыДиалога.Фильтр = "Файлы Excel (*.xls;*.xlsx)|*.xls;*.xlsx";
ПараметрыДиалога.МножественныйВыбор = Ложь;
ПараметрыДиалога.Заголовок = "Выберите файл для импорта";
// Ожидаем выбор файла пользователем
ОписаниеОповещения = Новый ОписаниеОповещения("ПослеВыбораФайла", ЭтотОбъект);
НачатьВыборФайла(ОписаниеОповещения, ПараметрыДиалога);
КонецПроцедуры
&НаКлиенте
Процедура ПослеВыбораФайла(ВыбранныеФайлы, ДополнительныеПараметры) Экспорт
Если ВыбранныеФайлы = Неопределено Или ВыбранныеФайлы.Количество() = 0 Тогда
Возврат;
КонецЕсли;
ПутьКФайлу = ВыбранныеФайлы[0];
// Помещаем файл во временное хранилище для передачи на сервер
ОписаниеОповещения = Новый ОписаниеОповещения("ПослеПомещенияФайла", ЭтотОбъект);
НачатьПомещениеФайлаНаСервер(ОписаниеОповещения, , , , ПутьКФайлу, УникальныйИдентификатор);
КонецПроцедуры
&НаКлиенте
Процедура ПослеПомещенияФайла(Результат, АдресХранилища, ВыбранноеИмяФайла, ДополнительныеПараметры) Экспорт
Если Не Результат Тогда
ПоказатьПредупреждение(, "Не удалось загрузить файл во временное хранилище!");
Возврат;
КонецЕсли;
// Передаем управление серверному методу, отправляя адрес во временном хранилище
ОбработатьФайлНаСервере(АдресХранилища);
КонецПроцедуры
Теперь на сервере мы извлечем бинарные данные из временного хранилища, сохраним их во временный файл на диске сервера и прочитаем с помощью встроенного объекта 1С ТабличныйДокумент. Этот метод работает невероятно быстро, не требует установленного Microsoft Office и стабилен под любыми операционными системами.
Проанализируем серверную процедуру обработки:
&НаСервере
Процедура ОбработатьФайлНаСервере(АдресХранилища)
// Получаем двоичные данные из хранилища
ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(АдресХранилища);
// Создаем имя временного файла на сервере
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("xlsx");
ДвоичныеДанныеФайла.Записать(ИмяВременногоФайла);
// Читаем файл через Табличный Документ
ТабДок = Новый ТабличныйДокумент;
Попытка
ТабДок.Прочитать(ИмяВременногоФайла, СпособЧтенияЗначенийТабличногоДокумента.Значение);
Исключение
ВызватьИсключение "Ошибка чтения файла: " + ОписаниеОшибки();
КонецПопытки;
// Удаляем временный файл после прочтения
Попытка
УдалитьФайлы(ИмяВременногоФайла);
Исключение
// Игнорируем ошибку удаления, если файл занят операционной системой
КонецПопытки;
// Создаем ТаблицуЗначений для хранения данных из Excel
ТаблицаИмпорта = Новый ТаблицаЗначений;
ТаблицаИмпорта.Колонки.Добавить("КодЕГАИС", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(50)));
ТаблицаИмпорта.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(150)));
ТаблицаИмпорта.Колонки.Добавить("НовоеЗначениеРеквизита", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
// Заполняем таблицу значениями из Табличного Документа (начиная со 2-й строки, пропуская шапку)
ВсегоСтрок = ТабДок.ВысотаТаблицы;
Для НомерСтроки = 2 По ВсегоСтрок Цикл
НоваяСтрока = ТаблицаИмпорта.Добавить();
НоваяСтрока.КодЕГАИС = ТабДок.Область(НомерСтроки, 1).Текст;
НоваяСтрока.Наименование = ТабДок.Область(НомерСтроки, 2).Текст;
НоваяСтрока.НовоеЗначениеРеквизита = Число(ТабДок.Область(НомерСтроки, 3).Текст);
КонецЦикла;
// Выполняем сопоставление с базой данных ОДНИМ запросом (Оптимальный паттерн)
СопоставитьИЗаписатьДанные(ТаблицаИмпорта);
КонецПроцедуры
Для сопоставления загруженных данных с базой данных мы не будем обходить справочник в цикле. Вместо этого передадим таблицу значений в параметры запроса. Это позволит моментально определить существующие элементы и выявить расхождения.
&НаСервере
Процедура СопоставитьИЗаписатьДанные(ТаблицаИмпорта)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТаблицаИмпорта.КодЕГАИС КАК КодЕГАИС,
| ТаблицаИмпорта.Наименование КАК Наименование,
| ТаблицаИмпорта.НовоеЗначениеРеквизита КАК НовоеЗначениеРеквизита
|ПОМЕСТИТЬ ВТ_Импорт
|ИЗ
| &ТаблицаИмпорта КАК ТаблицаИмпорта
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Импорт.КодЕГАИС КАК КодЕГАИС,
| ВТ_Импорт.Наименование КАК НаименованиеИзФайла,
| ВТ_Импорт.НовоеЗначениеРеквизита КАК НовоеЗначение,
| ОсновныеСредства.Ссылка КАК СсылкаНаОбъект,
| ОсновныеСредства.ДополнительныйРеквизит КАК СтароеЗначение
|ИЗ
| ВТ_Импорт КАК ВТ_Импорт
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.ОсновныеСредства КАК ОсновныеСредства
| ПО ВТ_Импорт.КодЕГАИС = ОсновныеСредства.КодЕГАИС";
Запрос.УстановитьПараметр("ТаблицаИмпорта", ТаблицаИмпорта);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Если ЗначениеЗаполнено(Выборка.СсылкаНаОбъект) Тогда
// Если значение реквизита изменилось, перезаписываем элемент на сервере
Если Выборка.СтароеЗначение <> Выборка.НовоеЗначение Тогда
ОбъектСпр = Выборка.СсылкаНаОбъект.ПолучитьОбъект();
ОбъектСпр.ДополнительныйРеквизит = Выборка.НовоеЗначение;
Попытка
ОбъектСпр.Записать();
Исключение
ЗаписьЖурналаРегистрации("ИмпортДанных", УровеньЖурналаРегистрации.Ошибка, , ,
"Не удалось записать элемент: " + Выборка.СсылкаНаОбъект + ". Описание: " + ОписаниеОшибки());
КонецПопытки;
КонецЕсли;
Иначе
// Обработка ситуации, когда элемент не найден (например, вывод уведомления на клиент)
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Иногда бизнес-логика требует не просто фоновой перезаписи объектов на сервере, а интерактивного открытия формы справочника с уже заполненными из Excel реквизитами, чтобы пользователь проверил изменения перед сохранением. Рассмотрим два сценария реализации этой задачи.
Для интерактивного создания и открытия нового элемента справочника с предзаполненными реквизитами мы воспользуемся встроенным параметром формы ЗначенияЗаполнения. При таком методе платформа автоматически передаст структуру в обработчик модуля объекта ОбработкаЗаполнения.
Код клиентского метода для вызова формы:
&НаКлиенте
Процедура ОткрытьИЗаполнитьНовыйЭлемент(ДанныеИзExcel)
// Структурируем данные для автозаполнения
СтруктураЗаполнения = Новый Структура;
СтруктураЗаполнения.Вставить("Наименование", ДанныеИзExcel.Наименование);
СтруктураЗаполнения.Вставить("КодЕГАИС", ДанныеИзExcel.КодЕГАИС);
СтруктураЗаполнения.Вставить("ДополнительныйРеквизит", ДанныеИзExcel.НовоеЗначениеРеквизита);
ПараметрыФормы = Новый Структура("ЗначенияЗаполнения", СтруктураЗаполнения);
// Открываем форму создания нового элемента
ОткрытьФорму("Справочник.ОсновныеСредства.ФормаОбъекта", ПараметрыФормы);
КонецПроцедуры
Если элемент справочника уже существует, но мы хотим открыть его форму на клиенте и временно подставить туда новые данные из файла Excel без немедленной физической записи в базу данных, нам понадобится передать кастомный параметр в форму.
1. Сначала инициируем открытие формы существующего элемента с клиента:
&НаКлиенте
Процедура ОткрытьИЗаменитьРеквизитСуществующего(СсылкаНаЭлемент, НовоеЗначениеИзExcel)
ПараметрыФормы = Новый Структура;
ПараметрыФормы.Вставить("Ключ", СсылкаНаЭлемент); // Параметр "Ключ" указывает форме открыть конкретную ссылку
ПараметрыФормы.Вставить("НовоеЗначениеРеквизита", НовоеЗначениеИзExcel); // Наш собственный параметр
ОткрытьФорму("Справочник.ОсновныеСредства.ФормаОбъекта", ПараметрыФормы);
КонецПроцедуры
2. Перейдем в Модуль формы справочника ОсновныеСредства и перехватим отправленный параметр в событии ПриСозданииНаСервере:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// Проверяем наличие нашего уникального параметра в структуре параметров формы
Если Параметры.Свойство("НовоеЗначениеРеквизита") Тогда
// Модифицируем реквизит основного реквизита формы "Объект"
Объект.ДополнительныйРеквизит = Параметры.НовоеЗначениеРеквизита;
// Опционально: можно вывести сообщение пользователю прямо на форме
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
"Внимание! Значение реквизита заполнено автоматически из загруженного файла Excel. Изменения не сохранены.");
КонецЕсли;
КонецПроцедуры
Используя данный метод, пользователь увидит открывшуюся форму существующего основного средства с измененным реквизитом. На экране отобразится предупреждение о несохраненных данных, и пользователь сможет осознанно нажать кнопку Записать и закрыть либо отменить изменения.
Мы спроектировали оптимальный цикл взаимодействия клиента и сервера. Разделение функционала предотвращает ошибки нарушения прав доступа и исключает зависания системы. Помещение файлов во временное хранилище и сопоставление данных через серверные пакетные запросы — это залог качественной разработки на платформе 1С:Предприятие 8.3.