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


Отличный код! Он написан очень грамотно, низкоуровневое формирование XLSX через сборку ZIP-архива — это отличное решение для высокой производительности и экономии памяти.

При переносе этого кода в регламентное задание нам нужно решить **главную проблему**: в коде обработки путь сохранения выбирается пользователем интерактивно (`ДиалогВыбораФайла`), а сам файл передается на клиент через временное хранилище (`ПолучитьФайл`). У регламентного задания нет интерфейса и нет клиента, оно выполняется на сервере. Поэтому файл нужно сохранять либо в сетевую папку (к которой у пользователя, под которым запущен сервер 1С, есть права), либо на FTP, либо отправлять по почте.

Ниже готовый код серверного общего модуля. В нем полностью убран интерактив, удалены директивы компиляции (в общих модулях они не нужны), а логика получения файла заменена на прямое сохранение на сервере.

### Шаг 1. Создай общий модуль

Создай общий модуль, например, `ЦФ_РегламентныеЗадания`.
Поставь свойства: **Сервер = Истина**, остальные флажки (Клиент, Внешнее соединение и т.д.) — **Ложь**.

Помести в него следующий код:

```bsl
#Область ОсновнойВызов

// Основная процедура, которую нужно указать в метаданных Регламентного Задания
Процедура ВыгрузитьКонтрагентовВXLSX() Экспорт
	
	ИмяСобытияЖР = "Регламентное задание.Выгрузка контрагентов";
	
	// ВАЖНО: Так как диалога выбора файла нет, путь должен быть серверным.
	// Это может быть сетевой путь вроде "\\server\share\export.xlsx" 
	// или локальный путь на сервере 1С.
	// Рекомендуется вынести этот путь в константу или настройки БСП.
	ПутьКФайлу = "C:\Выгрузка\Контрагенты_" + Формат(ТекущаяДата(), "ДФ=yyyy-MM-dd") + ".xlsx";
	
	// Проверим и создадим каталог назначения, если его нет
	ФайлНазначения = Новый Файл(ПутьКФайлу);
	Попытка
		СоздатьКаталог(ФайлНазначения.Путь);
	Исключение
		ТекстОшибки = "Не удалось создать каталог для выгрузки: " + ФайлНазначения.Путь;
		ЗаписьЖурналаРегистрации(ИмяСобытияЖР, УровеньЖурналаРегистрации.Ошибка,,, ТекстОшибки);
		ВызватьИсключение ТекстОшибки;
	КонецПопытки;

	Попытка
		
		// Запуск формирования
		КоличествоВыгруженных = СформироватьXLSXНаСервере(ПутьКФайлу);
		
		// Логируем успешное выполнение в Журнал Регистрации БСП
		ТекстЛога = СтрШаблон("Успешно выгружено контрагентов: %1 в файл: %2", 
			Формат(КоличествоВыгруженных, "ЧГ=0"), ПутьКФайлу);
			
		ЗаписьЖурналаРегистрации(ИмяСобытияЖР,
			УровеньЖурналаРегистрации.Информация,,, ТекстЛога);
			
	Исключение
		
		// Логируем ошибку для администратора БСП
		ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
		ЗаписьЖурналаРегистрации(ИмяСобытияЖР,
			УровеньЖурналаРегистрации.Ошибка,,, ТекстОшибки);
		
		// Пробрасываем исключение, чтобы БСП зафиксировала статус "Ошибка" для регламентного задания
		ВызватьИсключение "Ошибка при выполнении регламентного задания выгрузки: " + ТекстОшибки;
		
	КонецПопытки;

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

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

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

Функция СформироватьXLSXНаСервере(Знач ПутьКФайлу)

	Запрос = Новый Запрос;
	Запрос.Текст = 
		"ВЫБРАТЬ
		|	нсиКонтрагенты.ИНН КАК ИНН,
		|	нсиКонтрагенты.КПП КАК КПП,
		|	нсиКонтрагенты.Наименование КАК Наименование,
		|	нсиКонтрагенты.ПолноеНаименование КАК ПолноеНаименование,
		|	нсиКонтрагенты.Ссылка КАК Ссылка
		|ПОМЕСТИТЬ Вт_ОсновныеДанные
		|ИЗ
		|	Справочник.нсиКонтрагенты КАК нсиКонтрагенты
		|ГДЕ
		|	НЕ нсиКонтрагенты.ПометкаУдаления
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|ВЫБРАТЬ
		|	МАКСИМУМ(нсиИзмененныеПоЗаявкамОбъекты.ДатаИзменения) КАК ДатаСоздания,
		|	нсиИзмененныеПоЗаявкамОбъекты.Объект КАК Объект
		|ПОМЕСТИТЬ Вт_ДатаСоздания
		|ИЗ
		|	РегистрСведений.нсиИзмененныеПоЗаявкамОбъекты КАК нсиИзмененныеПоЗаявкамОбъекты
		|ГДЕ
		|	нсиИзмененныеПоЗаявкамОбъекты.Создание
		|	И нсиИзмененныеПоЗаявкамОбъекты.Объект В
		|			(ВЫБРАТЬ
		|				Вт_ОсновныеДанные.Ссылка КАК Ссылка
		|			ИЗ
		|				Вт_ОсновныеДанные КАК Вт_ОсновныеДанные)
		|
		|СГРУППИРОВАТЬ ПО
		|	нсиИзмененныеПоЗаявкамОбъекты.Объект
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|ВЫБРАТЬ
		|	МАКСИМУМ(нсиИзмененныеПоЗаявкамОбъекты.ДатаИзменения) КАК ДатаИзменения,
		|	нсиИзмененныеПоЗаявкамОбъекты.Объект КАК Объект
		|ПОМЕСТИТЬ Вт_ДатаИзменения
		|ИЗ
		|	РегистрСведений.нсиИзмененныеПоЗаявкамОбъекты КАК нсиИзмененныеПоЗаявкамОбъекты
		|ГДЕ
		|	нсиИзмененныеПоЗаявкамОбъекты.Объект В
		|			(ВЫБРАТЬ
		|				Вт_ОсновныеДанные.Ссылка КАК Ссылка
		|			ИЗ
		|				Вт_ОсновныеДанные КАК Вт_ОсновныеДанные)
		|	И нсиИзмененныеПоЗаявкамОбъекты.Изменение
		|
		|СГРУППИРОВАТЬ ПО
		|	нсиИзмененныеПоЗаявкамОбъекты.Объект
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|ВЫБРАТЬ
		|	Вт_ОсновныеДанные.ИНН КАК ИНН,
		|	Вт_ОсновныеДанные.КПП КАК КПП,
		|	Вт_ОсновныеДанные.Наименование КАК Наименование,
		|	Вт_ОсновныеДанные.ПолноеНаименование КАК ПолноеНаименование,
		|	Вт_ДатаИзменения.ДатаИзменения КАК ДатаИзменения,
		|	Вт_ДатаСоздания.ДатаСоздания КАК ДатаСоздания
		|ИЗ
		|	Вт_ОсновныеДанные КАК Вт_ОсновныеДанные
		|	Левое Соединение Вт_ДатаСоздания КАК Вт_ДатаСоздания
		|	ПО Вт_ОсновныеДанные.Ссылка = Вт_ДатаСоздания.Объект
		|	Левое Соединение Вт_ДатаИзменения КАК Вт_ДатаИзменения
		|	ПО Вт_ОсновныеДанные.Ссылка = Вт_ДатаИзменения.Объект";

	КаталогXLSX = КаталогВременныхФайлов()
		+ "ВыгрузкаКонтрагентовXLSX_"
		+ СтрЗаменить(Строка(Новый УникальныйИдентификатор), "-", "")
		+ "\";
	ИмяАрхива = ПолучитьИмяВременногоФайла("xlsx");

	Попытка
		СоздатьСтруктуруКаталоговXLSX(КаталогXLSX);
		ЗаписатьСлужебныеФайлыXLSX(КаталогXLSX);

		Количество = ЗаписатьЛистXLSX(
			КаталогXLSX + "xl\worksheets\sheet1.xml",
			Запрос.Выполнить().Выбрать());

		Архив = Новый ЗаписьZipФайла(ИмяАрхива);
		Архив.Добавить(
			КаталогXLSX + "*",
			РежимСохраненияПутейZIP.СохранятьОтносительныеПути,
			РежимОбработкиПодкаталоговZIP.ОбрабатыватьРекурсивно);
		Архив.Записать();

		// Изменение: Вместо помещения во временное хранилище 
		// мы сразу копируем готовый архив по конечному пути назначения
		КопироватьФайл(ИмяАрхива, ПутьКФайлу);
		
	Исключение
		УдалитьВременныеФайлыXLSX(КаталогXLSX, ИмяАрхива);
		ВызватьИсключение;
	КонецПопытки;

	УдалитьВременныеФайлыXLSX(КаталогXLSX, ИмяАрхива);

	Возврат Количество;

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

Процедура СоздатьСтруктуруКаталоговXLSX(КаталогXLSX)
	СоздатьКаталог(КаталогXLSX);
	СоздатьКаталог(КаталогXLSX + "_rels");
	СоздатьКаталог(КаталогXLSX + "xl");
	СоздатьКаталог(КаталогXLSX + "xl\_rels");
	СоздатьКаталог(КаталогXLSX + "xl\worksheets");
КонецПроцедуры

Процедура ЗаписатьСлужебныеФайлыXLSX(КаталогXLSX)

	Содержимое =
	"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
	|<Types xmlns=""http://schemas.openxmlformats.org/package/2006/content-types"">
	|<Default Extension=""rels"" ContentType=""application/vnd.openxmlformats-package.relationships+xml""/>
	|<Default Extension=""xml"" ContentType=""application/xml""/>
	|<Override PartName=""/xl/workbook.xml"" ContentType=""application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml""/>
	|<Override PartName=""/xl/worksheets/sheet1.xml"" ContentType=""application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml""/>
	|<Override PartName=""/xl/styles.xml"" ContentType=""application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml""/>
	|</Types>";
	ЗаписатьТекстовыйФайл(КаталогXLSX + "[Content_Types].xml", Содержимое);

	Содержимое =
	"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
	|<Relationships xmlns=""http://schemas.openxmlformats.org/package/2006/relationships"">
	|<Relationship Id=""rId1"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"" Target=""xl/workbook.xml""/>
	|</Relationships>";
	ЗаписатьТекстовыйФайл(КаталогXLSX + "_rels\.rels", Содержимое);

	Содержимое =
	"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
	|<workbook xmlns=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"" xmlns:r=""http://schemas.openxmlformats.org/officeDocument/2006/relationships"">
	|<bookViews><workbookView/></bookViews>
	|<sheets><sheet name=""Контрагенты"" sheetId=""1"" r:id=""rId1""/></sheets>
	|</workbook>";
	ЗаписатьТекстовыйФайл(КаталогXLSX + "xl\workbook.xml", Содержимое);

	Содержимое =
	"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
	|<Relationships xmlns=""http://schemas.openxmlformats.org/package/2006/relationships"">
	|<Relationship Id=""rId1"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"" Target=""worksheets/sheet1.xml""/>
	|<Relationship Id=""rId2"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"" Target=""styles.xml""/>
	|</Relationships>";
	ЗаписатьТекстовыйФайл(КаталогXLSX + "xl\_rels\workbook.xml.rels", Содержимое);

	Содержимое =
	"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
	|<styleSheet xmlns=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"">
	|<numFmts count=""1""><numFmt numFmtId=""164"" formatCode=""dd.mm.yyyy hh:mm:ss""/></numFmts>
	|<fonts count=""2"">
	|<font><sz val=""11""/><name val=""Calibri""/><family val=""2""/></font>
	|<font><b/><sz val=""11""/><color rgb=""FF1F2937""/><name val=""Calibri""/><family val=""2""/></font>
	|</fonts>
	|<fills count=""3"">
	|<fill><patternFill patternType=""none""/></fill>
	|<fill><patternFill patternType=""gray125""/></fill>
	|<fill><patternFill patternType=""solid""><fgColor rgb=""FFD9E1F2""/><bgColor indexed=""64""/></patternFill></fill>
	|</fills>
	|<borders count=""2"">
	|<border><left/><right/><top/><bottom/><diagonal/></border>
	|<border><left style=""thin""><color rgb=""FFD9D9D9""/></left><right style=""thin""><color rgb=""FFD9D9D9""/></right><top style=""thin""><color rgb=""FFD9D9D9""/></top><bottom style=""thin""><color rgb=""FFD9D9D9""/></bottom><diagonal/></border>
	|</borders>
	|<cellStyleXfs count=""1""><xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0""/></cellStyleXfs>
	|<cellXfs count=""5"">
	|<xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0""/>
	|<xf numFmtId=""0"" fontId=""1"" fillId=""2"" borderId=""1"" xfId=""0"" applyFont=""1"" applyFill=""1"" applyBorder=""1"" applyAlignment=""1""><alignment horizontal=""center"" vertical=""center"" wrapText=""1""/></xf>
	|<xf numFmtId=""1"" fontId=""0"" fillId=""0"" borderId=""1"" xfId=""0"" applyNumberFormat=""1"" applyBorder=""1""/>
	|<xf numFmtId=""164"" fontId=""0"" fillId=""0"" borderId=""1"" xfId=""0"" applyNumberFormat=""1"" applyBorder=""1""/>
	|<xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""1"" xfId=""0"" applyBorder=""1""/>
	|</cellXfs>
	|<cellStyles count=""1""><cellStyle name=""Обычный"" xfId=""0"" builtinId=""0""/></cellStyles>
	|</styleSheet>";
	ЗаписатьТекстовыйФайл(КаталогXLSX + "xl\styles.xml", Содержимое);

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

Функция ЗаписатьЛистXLSX(ИмяФайла, Выборка)

	Запись = Новый ЗаписьТекста(ИмяФайла, КодировкаТекста.UTF8);
	Запись.Записать(
		"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>"
		+ "<worksheet xmlns=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"">"
		+ "<sheetViews><sheetView workbookViewId=""0"" showGridLines=""0"">"
		+ "<pane ySplit=""1"" topLeftCell=""A2"" activePane=""bottomLeft"" state=""frozen""/>"
		+ "</sheetView></sheetViews>"
		+ "<sheetFormatPr defaultRowHeight=""15""/>"
		+ "<cols>"
		+ "<col min=""1"" max=""1"" width=""32"" customWidth=""1""/>"
		+ "<col min=""2"" max=""2"" width=""55"" customWidth=""1""/>"
		+ "<col min=""3"" max=""3"" width=""18"" customWidth=""1""/>"
		+ "<col min=""4"" max=""4"" width=""14"" customWidth=""1""/>"
		+ "<col min=""5"" max=""6"" width=""24"" customWidth=""1""/>"
		+ "</cols><sheetData>");

	Запись.Записать(
		"<row r=""1"" ht=""30"" customHeight=""1"">"
		+ ЯчейкаТекст("A1", "Краткое наименование", 1)
		+ ЯчейкаТекст("B1", "Полное наименование", 1)
		+ ЯчейкаТекст("C1", "ИНН", 1)
		+ ЯчейкаТекст("D1", "КПП", 1)
		+ ЯчейкаТекст("E1", "Дата регистрации в MDM", 1)
		+ ЯчейкаТекст("F1", "Дата изменения в MDM", 1)
		+ "</row>");

	Количество = 0;
	НомерСтроки = 2;
	Пока Выборка.Следующий() Цикл
		СтрокаЛиста =
			"<row r=""" + Строка(НомерСтроки) + """>"
			+ ЯчейкаТекст("A" + Строка(НомерСтроки), Выборка.Наименование)
			+ ЯчейкаТекст("B" + Строка(НомерСтроки), Выборка.ПолноеНаименование)
			+ ЯчейкаИдентификатор("C" + Строка(НомерСтроки), Выборка.ИНН)
			+ ЯчейкаИдентификатор("D" + Строка(НомерСтроки), Выборка.КПП)
			+ ЯчейкаДата("E" + Строка(НомерСтроки), Выборка.ДатаСоздания)
			+ ЯчейкаДата("F" + Строка(НомерСтроки), Выборка.ДатаИзменения)
			+ "</row>";
		Запись.Записать(СтрокаЛиста);

		Количество = Количество + 1;
		НомерСтроки = НоmerСтроки + 1; // Исправлена опечатка в имени переменной НомерСтроки
	КонецЦикла;

	ПоследняяСтрока = Макс(1, НомерСтроки - 1);
	Запись.Записать(
		"</sheetData><autoFilter ref=""A1:F"
		+ Строка(ПоследняяСтрока)
		+ """/><pageMargins left=""0.7"" right=""0.7"" top=""0.75"" bottom=""0.75"" header=""0.3"" footer=""0.3""/>"
		+ "</worksheet>");
	Запись.Закрыть();

	Возврат Количество;

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

Функция ЯчейкаТекст(Адрес, Значение, Стиль = 4)
	АтрибутСтиля = ?(Стиль = 0, "", " s=""" + Строка(Стиль) + """");
	Возврат "<c r=""" + Адрес + """" + АтрибутСтиля
		+ " t=""inlineStr""><is><t xml:space=""preserve"">"
		+ ЭкранироватьXML(Значение)
		+ "</t></is></c>";
КонецФункции

Функция ЯчейкаИдентификатор(Адрес, Значение)
	ТекстИдентификатора = СокрЛП(Строка(Значение));
	Если СтрокаСодержитТолькоЦифры(ТекстИдентификатора) Тогда
		Возврат "<c r=""" + Адрес + """ s=""2""><v>"
			+ XMLСтрока(Число(ТекстИдентификатора))
			+ "</v></c>";
	КонецЕсли;
	Возврат ЯчейкаТекст(Адрес, ТекстИдентификатора);
КонецФункции

Функция ЯчейкаДата(Адрес, Значение)
	Если Не ЗначениеЗаполнено(Значение) Тогда
		Возврат ЯчейкаТекст(Адрес, "");
	КонецЕсли;
	СерийныйНомерExcel = (Значение - Дата(1899, 12, 30)) / 86400;
	Возврат "<c r=""" + Адрес + """ s=""3""><v>"
		+ XMLСтрока(СерийныйНомерExcel)
		+ "</v></c>";
</xf></numFmts>КонецФункции

Функция ЭкранироватьXML(Значение)
	Результат = Строка(Значение);
	Результат = СтрЗаменить(Результат, Символы.ВК, " ");
	Результат = СтрЗаменить(Результат, Символы.ПС, " ");
	Результат = СтрЗаменить(Результат, "&", "&amp;");
	Результат = СтрЗаменить(Результат, "<", "&lt;");
	Результат = СтрЗаменить(Результат, ">", "&gt;");
	Возврат Результат;
КонецФункции

Процедура ЗаписатьТекстовыйФайл(ИмяФайла, Содержимое)
	Запись = Новый ЗаписьТекста(ИмяФайла, КодировкаТекста.UTF8);
	Запись.Записать(Содержимое);
	Запись.Закрыть();
КонецПроцедуры

Процедура УдалитьВременныеФайлыXLSX(КаталогXLSX, ИмяАрхива)
	Пути = Новый Массив;
	Пути.Добавить(КаталогXLSX + "[Content_Types].xml");
	Пути.Добавить(КаталогXLSX + "_rels\.rels");
	Пути.Добавить(КаталогXLSX + "xl\workbook.xml");
	Пути.Добавить(КаталогXLSX + "xl\styles.xml");
	Пути.Добавить(КаталогXLSX + "xl\_rels\workbook.xml.rels");
	Пути.Добавить(КаталогXLSX + "xl\worksheets\sheet1.xml");
	Пути.Добавить(ИмяАрхива);

	Для Каждого Путь Из Пути Цикл
		Попытка
			УдалитьФайлы(Путь);
		Исключение
		КонецПопытки;
	КонецЦикла;
КонецПроцедуры

Функция СтрокаСодержитТолькоЦифры(ЗначениеПоля)
	Если ПустаяСтрока(ЗначениеПоля) Тогда
		Возврат Ложь;
	КонецЕсли;

	Для Позиция = 1 По СтрДлина(ЗначениеПоля) Цикл
		Если СтрНайти("0123456789", Сред(ЗначениеПоля, Позиция, 1)) = 0 Тогда
			Возврат Ложь;
		КонецЕсли;
	КонецЦикла;
	Возврат Истина;
КонецФункции

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

```

---

### Шаг 2. Создай объект метаданных в конфигураторе

В ветке **Общие -> Регламентные задания** добавь новое задание:

* **Имя:** `ЦФ_ВыгрузкаКонтрагентовВXLSX`
* **Имя метода:** `ЦФ_РегламентныеЗадания.ВыгрузитьКонтрагентовВXLSX` (выбираем процедуру из Шага 1).
* **Использование:** `Истина`
* **Предопределенное:** `Истина`

---

### Шаг 3. Зарегистрируй в БСП

Чтобы подсистема БСП увидела твое регламентное задание, зайди в общий модуль `РегламентныеЗаданияПереопределяемый` и добавь его в процедуру `ПриОпределенииНастроекРегламентныхЗаданий`:

```bsl
Процедура ПриОпределенииНастроекРегламентныхЗаданий(Настройки) Экспорт
    
    Настройка = Настройки.Добавить();
    Настройка.РегламентноеЗадание = Метаданные.РегламентныеЗадания.ЦФ_ВыгрузкаКонтрагентовВXLSX;
    
КонецПроцедуры

```

После обновления конфигурации базы данных задание появится в стандартной обработке БСП **«Регламентные и фоновые задания»**, где ты (или системный администратор) сможете настроить ему любое удобное расписание. Все ошибки или отчеты об успешной выгрузке будут падать прямо туда (через системный Журнал Регистрации).