Загрузка данных


/ Форма «Загрузка артикулов» внешнего отчета «Остатки товаров по артикулам».
//
// Открывается из основной формы отчета, возвращает владельцу Массив из Строка —
// введенные артикулы. При отказе возвращает Неопределено.
//
// Артикулы вводятся в поле табличного документа, а не в таблицу формы: только так
// работает вставка колонки из буфера обмена (Ctrl+V) — таблица управляемой формы
// многострочную вставку не принимает. Строка 1 — шапка «Артикулы», данные с 2-й.
// Так же устроены типовые формы загрузки из внешних файлов, например
// ОбщаяФорма.ЗагрузкаИдентификаторовИзВнешнихФайлов.

#Область ОбработчикиСобытийФормы

&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)

	ИнициализироватьТабличныйДокумент();

КонецПроцедуры

#КонецОбласти

#Область ОбработчикиКомандФормы

&НаКлиенте
Процедура Готово(Команда)

	СписокАртикулов = ВведенныеАртикулы();

	Если СписокАртикулов.Количество() = 0 Тогда
		ПоказатьПредупреждение(, НСтр("ru = 'Не введено ни одного артикула.'"));
		Возврат;
	КонецЕсли;

	Закрыть(СписокАртикулов);

КонецПроцедуры

&НаКлиенте
Процедура ЗакрытьФорму(Команда)

	Закрыть(Неопределено);

КонецПроцедуры

&НаКлиенте
Процедура ОчиститьСписок(Команда)

	ИнициализироватьТабличныйДокумент();

КонецПроцедуры

&НаКлиенте
Процедура ЗаполнитьИзФайла(Команда)

	Оповещение = Новый ОписаниеОповещения("ЗаполнитьИзФайлаЗавершение", ЭтотОбъект);

	ПараметрыЗагрузки = ФайловаяСистемаКлиент.ПараметрыЗагрузкиФайла();
	ПараметрыЗагрузки.ИдентификаторФормы = УникальныйИдентификатор;
	ПараметрыЗагрузки.Диалог.Заголовок = НСтр("ru = 'Выберите файл с артикулами'");
	ПараметрыЗагрузки.Диалог.Фильтр = НСтр(
		"ru = 'Все поддерживаемые форматы (*.xlsx;*.xls;*.ods;*.mxl;*.csv;*.txt)|*.xlsx;*.xls;*.ods;*.mxl;*.csv;*.txt'");

	ФайловаяСистемаКлиент.ЗагрузитьФайл(Оповещение, ПараметрыЗагрузки);

КонецПроцедуры

#КонецОбласти

#Область СлужебныеПроцедурыИФункции

// Обработчик выбора файла: дописывает артикулы из первой колонки файла в таблицу.
//
// Параметры:
//   ПомещенныйФайл - Структура - см. ФайловаяСистемаКлиент.ЗагрузитьФайл
//                  - Неопределено - пользователь отказался от выбора.
//   ДополнительныеПараметры - Произвольный - не используется.
//
&НаКлиенте
Процедура ЗаполнитьИзФайлаЗавершение(ПомещенныйФайл, ДополнительныеПараметры) Экспорт

	Если ПомещенныйФайл = Неопределено Тогда
		Возврат;
	КонецЕсли;

	ОписаниеОшибки = "";
	Добавлено = ДобавитьАртикулыИзФайла(ПомещенныйФайл.Хранение, ПомещенныйФайл.Имя, ОписаниеОшибки);

	Если Не ПустаяСтрока(ОписаниеОшибки) Тогда
		ПоказатьПредупреждение(, ОписаниеОшибки);
	ИначеЕсли Добавлено = 0 Тогда
		ПоказатьПредупреждение(, НСтр("ru = 'В первой колонке файла нет ни одного значения.'"));
	КонецЕсли;

КонецПроцедуры

// Готовит пустой бланк: шапка «Артикулы» в первой строке, ниже — место для ввода.
//
&НаСервере
Процедура ИнициализироватьТабличныйДокумент()

	ТаблицаАртикулов.Очистить();

	Линия = Новый Линия(ТипЛинииЯчейкиТабличногоДокумента.Сплошная);

	Шапка = ТаблицаАртикулов.Область("R1C1");
	Шапка.Текст = НСтр("ru = 'Артикулы'");
	Шапка.Шрифт = Новый Шрифт(, , Истина);
	Шапка.ЦветФона = Новый Цвет(240, 240, 240);
	Шапка.Обвести(Линия, Линия, Линия, Линия);
	Шапка.ШиринаКолонки = 40;

	ТаблицаАртикулов.ФиксацияСверху = 1;

КонецПроцедуры

// Считывает непустые значения первой колонки, начиная со строки под шапкой.
//
// Возвращаемое значение:
//   Массив из Строка
//
&НаСервере
Функция ВведенныеАртикулы()

	Артикулы = Новый Массив;

	Для НомерСтроки = 2 По ТаблицаАртикулов.ВысотаТаблицы Цикл
		ДобавитьАртикул(Артикулы, ТаблицаАртикулов.Область("R" + НомерСтроки + "C1").Текст);
	КонецЦикла;

	Возврат Артикулы;

КонецФункции

// Дописывает в таблицу артикулы из файла.
//
// Параметры:
//   АдресФайла     - Строка - адрес двоичных данных файла во временном хранилище.
//   ИмяФайла       - Строка - имя файла с расширением.
//   ОписаниеОшибки - Строка - возвращаемый параметр: текст ошибки разбора файла,
//                             пустая строка при успешном чтении.
//
// Возвращаемое значение:
//   Число - сколько значений добавлено.
//
&НаСервере
Функция ДобавитьАртикулыИзФайла(АдресФайла, ИмяФайла, ОписаниеОшибки)

	ИзФайла = АртикулыИзФайла(АдресФайла, ИмяФайла, ОписаниеОшибки);

	Если Не ПустаяСтрока(ОписаниеОшибки) Тогда
		Возврат 0;
	КонецЕсли;

	НомерСтроки = Макс(ТаблицаАртикулов.ВысотаТаблицы, 1);

	Для Каждого Артикул Из ИзФайла Цикл
		НомерСтроки = НомерСтроки + 1;
		ТаблицаАртикулов.Область("R" + НомерСтроки + "C1").Текст = Артикул;
	КонецЦикла;

	Возврат ИзФайла.Количество();

КонецФункции

// Разбирает файл и возвращает артикулы из первой колонки.
//
// Текстовые форматы читаются потоком из временного хранилища. Табличные форматы
// читаются через временный файл: вариант ТабличныйДокумент.Прочитать(<Поток>)
// поддерживает только MXL, MXL7 и ODS, а xls/xlsx доступны исключительно варианту
// «из файла», где формат определяется по расширению. Работа с каталогом временных
// файлов в безопасном режиме разрешена — проверить на конкретной конфигурации;
// если запрещена, из фильтра выбора файла нужно убрать xls и xlsx, а ODS читать
// потоком с явным указанием ТипФайлаТабличногоДокумента.ODS.
//
// Параметры:
//   АдресФайла     - Строка - адрес двоичных данных файла во временном хранилище.
//   ИмяФайла       - Строка - имя файла с расширением, по нему выбирается способ чтения.
//   ОписаниеОшибки - Строка - возвращаемый параметр: текст ошибки разбора файла.
//
// Возвращаемое значение:
//   Массив из Строка - непустые значения первой колонки, по одному на строку файла.
//
&НаСервереБезКонтекста
Функция АртикулыИзФайла(АдресФайла, ИмяФайла, ОписаниеОшибки)

	Артикулы = Новый Массив;
	ОписаниеОшибки = "";

	Точка = СтрНайти(ИмяФайла, ".", НаправлениеПоиска.СКонца);
	Расширение = ?(Точка = 0, "", НРег(Сред(ИмяФайла, Точка + 1)));

	ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресФайла);

	Попытка

		Если Расширение = "txt" ИЛИ Расширение = "csv" Тогда
			ЗаполнитьИзТекстовогоФайла(Артикулы, ДвоичныеДанные, Расширение = "csv");
		Иначе
			ЗаполнитьИзТабличногоФайла(Артикулы, ДвоичныеДанные, Расширение);
		КонецЕсли;

	Исключение

		Артикулы.Очистить();
		ОписаниеОшибки = НСтр("ru = 'Не удалось прочитать файл: неподдерживаемый формат или файл поврежден.'");
		ЗаписьЖурналаРегистрации(
			НСтр("ru = 'Загрузка артикулов'", ОбщегоНазначения.КодОсновногоЯзыка()),
			УровеньЖурналаРегистрации.Ошибка, , ,
			ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));

	КонецПопытки;

	Возврат Артикулы;

КонецФункции

// Читает первую колонку текстового файла. Для CSV значение берется до первого
// разделителя. Кодировка определяется платформой по BOM; при его отсутствии
// используется ANSI — так открываются файлы, выгруженные из Excel и 1С.
//
// Параметры:
//   Артикулы       - Массив из Строка - приемник.
//   ДвоичныеДанные - ДвоичныеДанные - содержимое файла.
//   ЭтоCSV         - Булево - резать строку по разделителю «;».
//
&НаСервереБезКонтекста
Процедура ЗаполнитьИзТекстовогоФайла(Артикулы, ДвоичныеДанные, ЭтоCSV)

	Кодировка = ?(ЕстьМеткаUTF8(ДвоичныеДанные), КодировкаТекста.UTF8, КодировкаТекста.ANSI);

	Чтение = Новый ЧтениеТекста(ДвоичныеДанные.ОткрытьПотокДляЧтения(), Кодировка);

	Попытка

		СтрокаФайла = Чтение.ПрочитатьСтроку();

		Пока СтрокаФайла <> Неопределено Цикл

			Если ЭтоCSV Тогда
				Колонки = СтрРазделить(СтрокаФайла, ";");
				Значение = Колонки[0];
			Иначе
				Значение = СтрокаФайла;
			КонецЕсли;

			ДобавитьАртикул(Артикулы, Значение);

			СтрокаФайла = Чтение.ПрочитатьСтроку();

		КонецЦикла;

	Исключение
		Чтение.Закрыть();
		ВызватьИсключение;
	КонецПопытки;

	Чтение.Закрыть();

КонецПроцедуры

// Читает первую колонку табличного файла (xlsx, xls, ods, mxl) через временный файл.
//
// Параметры:
//   Артикулы       - Массив из Строка - приемник.
//   ДвоичныеДанные - ДвоичныеДанные - содержимое файла.
//   Расширение     - Строка - расширение без точки, задает формат чтения.
//
&НаСервереБезКонтекста
Процедура ЗаполнитьИзТабличногоФайла(Артикулы, ДвоичныеДанные, Расширение)

	ИмяВременногоФайла = ПолучитьИмяВременногоФайла(?(ПустаяСтрока(Расширение), "mxl", Расширение));
	ДвоичныеДанные.Записать(ИмяВременногоФайла);

	ТабличныйДокумент = Новый ТабличныйДокумент;

	Попытка
		ТабличныйДокумент.Прочитать(ИмяВременногоФайла, СпособЧтенияЗначенийТабличногоДокумента.Значение);
	Исключение
		УдалитьФайлы(ИмяВременногоФайла);
		ВызватьИсключение;
	КонецПопытки;

	УдалитьФайлы(ИмяВременногоФайла);

	Для НомерСтроки = 1 По ТабличныйДокумент.ВысотаТаблицы Цикл

		Ячейка = ТабличныйДокумент.Область(НомерСтроки, 1, НомерСтроки, 1);
		ДобавитьАртикул(Артикулы, ?(Ячейка.СодержитЗначение, Ячейка.Значение, Ячейка.Текст));

	КонецЦикла;

КонецПроцедуры

// Добавляет в массив очищенное значение артикула, если оно непустое.
//
// Числовые значения приводятся к строке без разделителей групп: иначе артикул
// 12345678 превратился бы в «12 345 678».
//
// Параметры:
//   Артикулы - Массив из Строка - приемник.
//   Значение - Произвольный - исходное значение ячейки или строки файла.
//
&НаСервереБезКонтекста
Процедура ДобавитьАртикул(Артикулы, Значение)

	Если ТипЗнч(Значение) = Тип("Число") Тогда
		Артикул = СокрЛП(Формат(Значение, "ЧГ=0; ЧН="));
	Иначе
		Артикул = СокрЛП(Строка(Значение));
	КонецЕсли;

	Если Не ПустаяСтрока(Артикул) Тогда
		Артикулы.Добавить(Артикул);
	КонецЕсли;

КонецПроцедуры

// Проверяет наличие сигнатуры UTF-8 (BOM) в начале данных.
//
// Параметры:
//   ДвоичныеДанные - ДвоичныеДанные - содержимое файла.
//
// Возвращаемое значение:
//   Булево
//
&НаСервереБезКонтекста
Функция ЕстьМеткаUTF8(ДвоичныеДанные)

	Если ДвоичныеДанные.Размер() < 3 Тогда
		Возврат Ложь;
	КонецЕсли;

	Буфер = ДвоичныеДанные.ОткрытьПотокДляЧтения().Прочитать(3).ЗакрытьИПолучитьБуферДвоичныхДанных();

	Возврат Буфер[0] = 239 И Буфер[1] = 187 И Буфер[2] = 191;

КонецФункции

#КонецОбласти