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


//++Варнин А.А. Задача №72300 - переработано 27.07.26
//
// Что изменено относительно исходной версии:
//  1. Доп. реквизиты (OEMНомер, АртикулМаркетплейса) забираются одним запросом в пакете,
//     а не циклом по СтрЦена.Номенклатура.ДополнительныеРеквизиты (был N+1 к БД).
//  2. Наименование и Марка приходят из запроса через ПРЕДСТАВЛЕНИЕ(),
//     а не через Строка(Ссылка) в цикле отправки (тоже был N+1).
//  3. Всем временным таблицам проставлен ИНДЕКСИРОВАТЬ ПО.
//  4. Убраны обращения через точку в финальном запросе — явное соединение со справочником.
//  5. Убрано ВЫРАЗИТЬ(... КАК ЧИСЛО(15,2)) из запроса (оно усекает, а не округляет).
//     Округление делается один раз в коде через Окр().
//  6. Отсекается служебная номенклатура ("не использовать") и помеченная на удаление.
//  7. Отправка в Kafka переведена на ОтправитьПакетСРезультатом (реальный статус доставки),
//     с фолбэком на поштучную отправку и КОРРЕКТНОЙ проверкой кода возврата
//     (0 = поставлено в очередь = успех, -1 = ошибка).
//  8. Проверяется результат ОстановитьПродюсера() — именно он подтверждает flush очереди.
//  9. Рег. задание пишет старт/финиш/итоги в журнал регистрации и падает при неуспехе.
// 10. ВыгрузитьВExcel вынесена под флаг константы, путь берётся из константы.

#Область РегламентноеЗадание_кор_ВыгрузкаЗакупочныхЦен

Процедура ВыполнитьВыгрузкуЗакупочныхЦен() Экспорт

	ИмяСобытия = "Выгрузка закупочных цен";
	НачалоРаботы = ТекущаяУниверсальнаяДатаВМиллисекундах();

	ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Информация, , , "Старт выгрузки.");

	ЦеныТЗ = СформироватьТаблицуЦен();

	Если ЦеныТЗ.Количество() = 0 Тогда
		ТекстОшибки = "Запрос не вернул ни одной строки. Выгрузка не выполнена.";
		ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , , ТекстОшибки);
		ВызватьИсключение ТекстОшибки;
	КонецЕсли;

	ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Информация, , ,
		СтрШаблон("Сформировано строк: %1. Время запроса: %2 мс.",
			ЦеныТЗ.Количество(), ТекущаяУниверсальнаяДатаВМиллисекундах() - НачалоРаботы));

	РезультатОтправки = ОтправитьВKafka(ЦеныТЗ);

	ВыгрузитьВExcel(ЦеныТЗ);

	ЗаписьЖурналаРегистрации(ИмяСобытия,
		?(РезультатОтправки.Успех, УровеньЖурналаРегистрации.Информация, УровеньЖурналаРегистрации.Ошибка),
		, , СтрШаблон("Финиш. %1 Общее время: %2 мс.",
			РезультатОтправки.Текст, ТекущаяУниверсальнаяДатаВМиллисекундах() - НачалоРаботы));

	// Регламентное задание должно завершиться с ошибкой, иначе о проблеме никто не узнает.
	Если НЕ РезультатОтправки.Успех Тогда
		ВызватьИсключение РезультатОтправки.Текст;
	КонецЕсли;

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

Функция СформироватьТаблицуЦен()

	ВидЦены = Справочники.ВидыЦен.НайтиПоРеквизиту("Идентификатор", "ЗакупочнаяЦена");
	Если НЕ ЗначениеЗаполнено(ВидЦены) Тогда
		ВызватьИсключение "Не найден вид цены с идентификатором ""ЗакупочнаяЦена"".";
	КонецЕсли;

	СвойствоOEM = НайтиДополнительныйРеквизит("OEMНомер");
	СвойствоАртикулМП = НайтиДополнительныйРеквизит("АртикулМаркетплейсов");

	Запрос = Новый Запрос(ТекстЗапросаБезНДС());
	Запрос.УстановитьПараметр("ВидЦены", ВидЦены);
	Запрос.УстановитьПараметр("КонецПериода", ТекущаяДатаСеанса());
	Запрос.УстановитьПараметр("СвойствоOEM", СвойствоOEM);
	Запрос.УстановитьПараметр("СвойствоАртикулМП", СвойствоАртикулМП);
	Запрос.УстановитьПараметр("МаскаСлужебных", "%не использовать%");

	ЦеныТЗ = Запрос.Выполнить().Выгрузить();

	// Единственное округление за весь цикл — здесь.
	Для Каждого СтрЦена Из ЦеныТЗ Цикл
		СтрЦена.СредняяЦена = Окр(СтрЦена.СредняяЦена, 2, РежимОкругления.Окр15как20);
		СтрЦена.Цена        = Окр(СтрЦена.Цена, 2, РежимОкругления.Окр15как20);
	КонецЦикла;

	Возврат ЦеныТЗ;

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

// Ищет вид дополнительного реквизита по идентификатору для формул.
// Возвращает пустую ссылку, если не найден (тогда колонка просто останется пустой).
//
Функция НайтиДополнительныйРеквизит(ИдентификаторДляФормул)

	Свойство = ПланыВидовХарактеристик.ДополнительныеРеквизитыИСведения.НайтиПоРеквизиту(
		"ИдентификаторДляФормул", ИдентификаторДляФормул);

	Если НЕ ЗначениеЗаполнено(Свойство) Тогда
		ЗаписьЖурналаРегистрации("Выгрузка закупочных цен",
			УровеньЖурналаРегистрации.Предупреждение, , ,
			"Не найден дополнительный реквизит с идентификатором """ + ИдентификаторДляФормул + """.");
	КонецЕсли;

	Возврат Свойство;

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

Функция ТекстЗапросаБезНДС()

	Возврат
	
		"ВЫБРАТЬ
		|	ОстаткиТоваров.Номенклатура КАК Номенклатура
		|ПОМЕСТИТЬ ВТ_Остатки
		|ИЗ
		|	РегистрНакопления.ТоварыНаСкладах.Остатки(&КонецПериода, ) КАК ОстаткиТоваров
		|ГДЕ
		|	ОстаткиТоваров.ВНаличииОстаток > 0
		|
		|ИНДЕКСИРОВАТЬ ПО
		|	Номенклатура
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|// Пары ""документ-основание + номенклатура"", по которым цена реально установилась.
		|ВЫБРАТЬ
		|	УстановкаЦен.ДокументОснование КАК Основание,
		|	ЦеныНоменклатуры25.Номенклатура КАК Номенклатура,
		|	МАКСИМУМ(ЦеныНоменклатуры25.Период) КАК ПериодУстановки
		|ПОМЕСТИТЬ ВТ_ДокУстановкаЦен
		|ИЗ
		|	РегистрСведений.ЦеныНоменклатуры25 КАК ЦеныНоменклатуры25
		|		ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.УстановкаЦенНоменклатуры КАК УстановкаЦен
		|		ПО ЦеныНоменклатуры25.Регистратор = УстановкаЦен.Ссылка
		|		ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Остатки КАК ВТ_Остатки
		|		ПО ЦеныНоменклатуры25.Номенклатура = ВТ_Остатки.Номенклатура
		|ГДЕ
		|	УстановкаЦен.Проведен
		|	И УстановкаЦен.ДокументОснование ССЫЛКА Документ.ПриобретениеТоваровУслуг
		|	И ЦеныНоменклатуры25.ВидЦены = &ВидЦены
		|	И ЦеныНоменклатуры25.Цена > 0
		|
		|СГРУППИРОВАТЬ ПО
		|	УстановкаЦен.ДокументОснование,
		|	ЦеныНоменклатуры25.Номенклатура
		|
		|ИНДЕКСИРОВАТЬ ПО
		|	Номенклатура,
		|	Основание
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|// Суммы и количества по документу поступления в разрезе номенклатуры.
		|ВЫБРАТЬ
		|	ДокТовары.Номенклатура КАК Номенклатура,
		|	Док.Ссылка КАК Ссылка,
		|	ВТ_ДокУстановкаЦен.ПериодУстановки КАК ПериодУстановки,
		|	СУММА(ВЫБОР
		|			КОГДА Док.ЦенаВключаетНДС
		|				ТОГДА ДокТовары.Сумма - ДокТовары.СуммаНДС
		|			ИНАЧЕ ДокТовары.Сумма
		|		КОНЕЦ) КАК СуммаБезНДС,
		|	СУММА(ДокТовары.Количество) КАК Количество
		|ПОМЕСТИТЬ ВТ_ПоДокументам
		|ИЗ
		|	Документ.ПриобретениеТоваровУслуг.Товары КАК ДокТовары
		|		ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ПриобретениеТоваровУслуг КАК Док
		|		ПО ДокТовары.Ссылка = Док.Ссылка
		|		ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ДокУстановкаЦен КАК ВТ_ДокУстановкаЦен
		|		ПО Док.Ссылка = ВТ_ДокУстановкаЦен.Основание
		|			И ДокТовары.Номенклатура = ВТ_ДокУстановкаЦен.Номенклатура
		|ГДЕ
		|	Док.Проведен
		|	И ДокТовары.Количество > 0
		|	И ДокТовары.Сумма > 0
		|
		|СГРУППИРОВАТЬ ПО
		|	ДокТовары.Номенклатура,
		|	Док.Ссылка,
		|	ВТ_ДокУстановкаЦен.ПериодУстановки
		|
		|ИНДЕКСИРОВАТЬ ПО
		|	Номенклатура
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|// Цена за единицу по каждому документу. Округление НЕ делаем — оно в коде.
		|ВЫБРАТЬ
		|	ВТ_ПоДокументам.Номенклатура КАК Номенклатура,
		|	ВТ_ПоДокументам.Ссылка КАК Ссылка,
		|	ВТ_ПоДокументам.ПериодУстановки КАК ПериодУстановки,
		|	ВТ_ПоДокументам.СуммаБезНДС / ВТ_ПоДокументам.Количество КАК ЦенаДокумента
		|ПОМЕСТИТЬ ВТ_ЦеныДокументов
		|ИЗ
		|	ВТ_ПоДокументам КАК ВТ_ПоДокументам
		|
		|ИНДЕКСИРОВАТЬ ПО
		|	Номенклатура,
		|	ПериодУстановки
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|// Средняя закупочная: простое среднее цен документов (по постановке Шарова).
		|ВЫБРАТЬ
		|	ВТ_ЦеныДокументов.Номенклатура КАК Номенклатура,
		|	СРЕДНЕЕ(ВТ_ЦеныДокументов.ЦенаДокумента) КАК СредняяЦена,
		|	КОЛИЧЕСТВО(ВТ_ЦеныДокументов.Ссылка) КАК КоличествоДокументов,
		|	МАКСИМУМ(ВТ_ЦеныДокументов.ПериодУстановки) КАК ПериодУстановки
		|ПОМЕСТИТЬ ВТ_СредняяЦена
		|ИЗ
		|	ВТ_ЦеныДокументов КАК ВТ_ЦеныДокументов
		|
		|СГРУППИРОВАТЬ ПО
		|	ВТ_ЦеныДокументов.Номенклатура
		|
		|ИНДЕКСИРОВАТЬ ПО
		|	Номенклатура,
		|	ПериодУстановки
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|// Последняя цена — среднее по документам с максимальным периодом установки.
		|ВЫБРАТЬ
		|	ВТ_ЦеныДокументов.Номенклатура КАК Номенклатура,
		|	СРЕДНЕЕ(ВТ_ЦеныДокументов.ЦенаДокумента) КАК ТекущаяЦенаБезНДС
		|ПОМЕСТИТЬ ВТ_ПоследняяЦена
		|ИЗ
		|	ВТ_ЦеныДокументов КАК ВТ_ЦеныДокументов
		|		ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_СредняяЦена КАК ВТ_СредняяЦена
		|		ПО ВТ_ЦеныДокументов.Номенклатура = ВТ_СредняяЦена.Номенклатура
		|			И ВТ_ЦеныДокументов.ПериодУстановки = ВТ_СредняяЦена.ПериодУстановки
		|
		|СГРУППИРОВАТЬ ПО
		|	ВТ_ЦеныДокументов.Номенклатура
		|
		|ИНДЕКСИРОВАТЬ ПО
		|	Номенклатура
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|// Доп. реквизиты одним запросом вместо цикла по объектам.
		|// ВЫРАЗИТЬ КАК СТРОКА(150) нужен, чтобы МАКСИМУМ() отработал по составному типу
		|// поля Значение. Если реальная длина реквизита больше — увеличить число.
		|ВЫБРАТЬ
		|	ДопРеквизиты.Ссылка КАК Номенклатура,
		|	МАКСИМУМ(ВЫБОР
		|			КОГДА ДопРеквизиты.Свойство = &СвойствоOEM
		|				ТОГДА ВЫРАЗИТЬ(ДопРеквизиты.Значение КАК СТРОКА(150))
		|			ИНАЧЕ """"
		|		КОНЕЦ) КАК OEMНомер,
		|	МАКСИМУМ(ВЫБОР
		|			КОГДА ДопРеквизиты.Свойство = &СвойствоАртикулМП
		|				ТОГДА ВЫРАЗИТЬ(ДопРеквизиты.Значение КАК СТРОКА(150))
		|			ИНАЧЕ """"
		|		КОНЕЦ) КАК АртикулМаркетплейса
		|ПОМЕСТИТЬ ВТ_ДопРеквизиты
		|ИЗ
		|	Справочник.Номенклатура.ДополнительныеРеквизиты КАК ДопРеквизиты
		|		ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Остатки КАК ВТ_Остатки
		|		ПО ДопРеквизиты.Ссылка = ВТ_Остатки.Номенклатура
		|ГДЕ
		|	ДопРеквизиты.Свойство В (&СвойствоOEM, &СвойствоАртикулМП)
		|
		|СГРУППИРОВАТЬ ПО
		|	ДопРеквизиты.Ссылка
		|
		|ИНДЕКСИРОВАТЬ ПО
		|	Номенклатура
		|;
		|
		|////////////////////////////////////////////////////////////////////////////////
		|ВЫБРАТЬ
		|	Ном.Ссылка КАК Номенклатура,
		|	ПРЕДСТАВЛЕНИЕ(Ном.Ссылка) КАК Наименование,
		|	Ном.Код КАК Код,
		|	Ном.Артикул КАК Артикул,
		|	ПРЕДСТАВЛЕНИЕ(Ном.Марка) КАК Марка,
		|	ВТ_СредняяЦена.СредняяЦена КАК СредняяЦена,
		|	ВТ_СредняяЦена.КоличествоДокументов КАК КоличествоДокументов,
		|	ВТ_ПоследняяЦена.ТекущаяЦенаБезНДС КАК Цена,
		|	ЕСТЬNULL(ВТ_ДопРеквизиты.OEMНомер, """") КАК OEMНомер,
		|	ЕСТЬNULL(ВТ_ДопРеквизиты.АртикулМаркетплейса, """") КАК АртикулМаркетплейса
		|ИЗ
		|	ВТ_СредняяЦена КАК ВТ_СредняяЦена
		|		ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПоследняяЦена КАК ВТ_ПоследняяЦена
		|		ПО ВТ_СредняяЦена.Номенклатура = ВТ_ПоследняяЦена.Номенклатура
		|		ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК Ном
		|		ПО ВТ_СредняяЦена.Номенклатура = Ном.Ссылка
		|		ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДопРеквизиты КАК ВТ_ДопРеквизиты
		|		ПО ВТ_СредняяЦена.Номенклатура = ВТ_ДопРеквизиты.Номенклатура
		|ГДЕ
		|	НЕ Ном.ПометкаУдаления
		|	И ВТ_СредняяЦена.СредняяЦена > 0
		|	И НЕ Ном.Наименование ПОДОБНО &МаскаСлужебных
		|	// Если решите отсекать неторговые позиции (сканеры, ТСД, светильники) —
		|	// раскомментировать и передать параметр:
		|	// И Ном.ВидНоменклатуры В (&ВидыНоменклатурыКВыгрузке)
		|
		|УПОРЯДОЧИТЬ ПО
		|	Код";

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

// Формирует и отправляет пакет сообщений в Kafka.
//
// Возвращаемое значение:
//   Структура - Отправлено, Ошибок, Успех, Текст
//
Функция ОтправитьВKafka(ЦеныТЗ)

	Сообщения = Новый Массив;

	Для Каждого СтрЦена Из ЦеныТЗ Цикл

		Элемент = Новый Структура;
		Элемент.Вставить("name",       СокрЛП(СтрЦена.Наименование));
		Элемент.Вставить("code",       СокрЛП(СтрЦена.Код));
		Элемент.Вставить("brand",      СокрЛП(СтрЦена.Марка));
		Элемент.Вставить("article",    СокрЛП(СтрЦена.Артикул));
		Элемент.Вставить("article_mp", СокрЛП(СтрЦена.АртикулМаркетплейса));
		Элемент.Вставить("oem",        СокрЛП(СтрЦена.OEMНомер));
		Элемент.Вставить("last_price", СтрЦена.Цена);
		Элемент.Вставить("avg_price",  СтрЦена.СредняяЦена);
		// Раскомментировать, если сторона калькулятора готова принимать доп. поля:
		// Элемент.Вставить("docs_count",    СтрЦена.КоличествоДокументов);
		// Элемент.Вставить("calculated_at", Формат(ТекущаяДатаСеанса(), "ДФ=yyyy-MM-ddTHH:mm:ss"));

		Сообщение = Новый Структура("Ключ, Тело",
			Строка(СтрЦена.Номенклатура.УникальныйИдентификатор()),
			ОбщегоНазначения.ЗначениеВJSON(Элемент));

		Сообщения.Добавить(Сообщение);

	КонецЦикла;

	Возврат кор_HTTPКоннектор.ОтправитьВКафку(Сообщения);

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

Процедура ВыгрузитьВExcel(ЦеныТЗ)

	// Отладочная выгрузка. Включается константой, путь тоже из константы,
	// чтобы не хардкодить каталог конкретного сервера.
	Если НЕ Константы.кор_ВыгружатьЦеныВФайл.Получить() Тогда
		Возврат;
	КонецЕсли;

	Каталог = СокрЛП(Константы.кор_КаталогВыгрузкиЦен.Получить());
	Если НЕ ЗначениеЗаполнено(Каталог) Тогда
		ЗаписьЖурналаРегистрации("Выгрузка закупочных цен",
			УровеньЖурналаРегистрации.Предупреждение, , ,
			"Не заполнена константа кор_КаталогВыгрузкиЦен, файл не сохранён.");
		Возврат;
	КонецЕсли;

	Попытка

		ТабДок = Новый ТабличныйДокумент;
		Построитель = Новый ПостроительОтчета;
		Построитель.ИсточникДанных = Новый ОписаниеИсточникаДанных(ЦеныТЗ);
		Построитель.Выполнить();
		Построитель.Вывести(ТабДок);

		ИмяФайла = Каталог + ?(Прав(Каталог, 1) = ПолучитьРазделительПути(), "", ПолучитьРазделительПути())
			+ "цены_" + Формат(ТекущаяДатаСеанса(), "ДФ=yyyyMMdd_HHmmss") + ".xlsx";

		ТабДок.Записать(ИмяФайла, ТипФайлаТабличногоДокумента.XLSX);

	Исключение
		// Отладочная выгрузка не должна ронять регламент, но и молчать не должна.
		ЗаписьЖурналаРегистрации("Выгрузка закупочных цен",
			УровеньЖурналаРегистрации.Предупреждение, , ,
			"Не удалось сохранить файл: " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
	КонецПопытки;

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

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


// https://github.com/NuclearAPK/Simple-Kafka_Adapter
// Общий модуль: кор_HTTPКоннектор
//
// Настройки хранятся в константах:
//   кор_KafkaБрокеры      (Строка) - "host:port,host:port,..."
//   кор_KafkaТопик        (Строка) - имя топика
//   кор_KafkaКаталогЛогов (Строка) - каталог логов компоненты
//
#Область Кафка

#Область ПрограммныйИнтерфейс

Функция ПолучитьСтруктуруПодключения() Экспорт

	СтруктураПодключения = Новый Структура;
	СтруктураПодключения.Вставить("Брокеры",      СокрЛП(Константы.кор_KafkaБрокеры.Получить()));
	СтруктураПодключения.Вставить("ИмяТопика",    СокрЛП(Константы.кор_KafkaТопик.Получить()));
	СтруктураПодключения.Вставить("КаталогЛогов", СокрЛП(Константы.кор_KafkaКаталогЛогов.Получить()));

	Возврат СтруктураПодключения;

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

// Отправляет поток сообщений в Kafka.
//
// Параметры:
//   Сообщения - Массив структур - каждый элемент: Ключ (Строка), Тело (Строка JSON)
//
// Возвращаемое значение:
//   Структура - Отправлено (Число), Ошибок (Число), Успех (Булево), Текст (Строка)
//
Функция ОтправитьВКафку(Сообщения) Экспорт

	ИмяСобытия = "Kafka. Отправка цен";
	Результат = Новый Структура("Отправлено, Ошибок, Успех, Текст", 0, 0, Ложь, "");

	Если ТипЗнч(Сообщения) <> Тип("Массив") ИЛИ Сообщения.Количество() = 0 Тогда
		Результат.Текст = "Нет данных для отправки.";
		Возврат Результат;
	КонецЕсли;

	Подключение = ПолучитьСтруктуруПодключения();

	Если НЕ ЗначениеЗаполнено(Подключение.Брокеры) Тогда
		Результат.Текст = "Не заполнена константа кор_KafkaБрокеры.";
		ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , , Результат.Текст);
		Возврат Результат;
	КонецЕсли;

	Если НЕ ЗначениеЗаполнено(Подключение.ИмяТопика) Тогда
		Результат.Текст = "Не заполнена константа кор_KafkaТопик.";
		ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , , Результат.Текст);
		Возврат Результат;
	КонецЕсли;

	Компонента = СоздатьКомпонентуКафкаКлиент();
	Если Компонента = Неопределено Тогда
		Результат.Текст = "Не удалось создать компоненту Kafka.";
		Возврат Результат;
	КонецЕсли;

	Если ЗначениеЗаполнено(Подключение.КаталогЛогов) Тогда
		Попытка
			Компонента.КаталогЛогов = Подключение.КаталогЛогов;
		Исключение
			ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Предупреждение, , ,
				"Не удалось установить КаталогЛогов: " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
		КонецПопытки;
	КонецЕсли;

	// Продюсер при остановке ждёт доставки всех сообщений из очереди.
	// Дефолт 20 сек — на нашем объёме хватает, но запас лишним не будет.
	Попытка
		Компонента.УстановитьТаймаутОчисткиПродюсера(60000);
	Исключение
		// Метода нет в старых сборках — не критично.
	КонецПопытки;

	Если НЕ Компонента.ИнициализироватьПродюсера(Подключение.Брокеры) Тогда
		Результат.Текст = "Не удалось инициализировать продюсера. Брокеры: " + Подключение.Брокеры
			+ ". " + Компонента.ПолучитьСообщениеОбОшибке();
		ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , , Результат.Текст);
		Компонента = Неопределено;
		Возврат Результат;
	КонецЕсли;

	Всего = Сообщения.Количество();

	Попытка

		ОтправитьПакетами(Компонента, Сообщения, Подключение.ИмяТопика, Результат, ИмяСобытия);

	Исключение
		Результат.Текст = "Прервано на " + (Результат.Отправлено + Результат.Ошибок) + " из " + Всего
			+ ". " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
		ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , , Результат.Текст);

		Попытка Компонента.ОстановитьПродюсера(); Исключение КонецПопытки;
		Компонента = Неопределено;
		Возврат Результат;
	КонецПопытки;

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

	Компонента = Неопределено;

	Результат.Успех = (Результат.Ошибок = 0) И ОчередьОчищена;
	Результат.Текст = СтрШаблон("Отправлено: %1, ошибок: %2, всего: %3.",
		Результат.Отправлено, Результат.Ошибок, Всего);

	ЗаписьЖурналаРегистрации(ИмяСобытия,
		?(Результат.Успех, УровеньЖурналаРегистрации.Информация, УровеньЖурналаРегистрации.Предупреждение),
		, , Результат.Текст);

	Возврат Результат;

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

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

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

// Отправляет сообщения пачками через ОтправитьПакетСРезультатом.
// Если метода нет (старая сборка компоненты) — уходит на поштучную отправку.
//
Процедура ОтправитьПакетами(Компонента, Сообщения, Топик, Результат, ИмяСобытия)

	РазмерПачки = 1000;
	Пачка = Новый Массив;

	Для Каждого ЭлементСообщения Из Сообщения Цикл

		Ключ = ?(ЭлементСообщения.Свойство("Ключ"), Строка(ЭлементСообщения.Ключ), "");
		Тело = ?(ЭлементСообщения.Свойство("Тело"), Строка(ЭлементСообщения.Тело), "");

		Если НЕ ЗначениеЗаполнено(Тело) Тогда
			Результат.Ошибок = Результат.Ошибок + 1;
			Продолжить;
		КонецЕсли;

		СообщениеКомпоненты = Новый Структура;
		СообщениеКомпоненты.Вставить("message", Тело);
		Если ЗначениеЗаполнено(Ключ) Тогда
			СообщениеКомпоненты.Вставить("key", Ключ);
		КонецЕсли;

		Пачка.Добавить(СообщениеКомпоненты);

		Если Пачка.Количество() >= РазмерПачки Тогда
			ОтправитьОднуПачку(Компонента, Пачка, Топик, Результат, ИмяСобытия);
			Пачка = Новый Массив;
		КонецЕсли;

	КонецЦикла;

	Если Пачка.Количество() > 0 Тогда
		ОтправитьОднуПачку(Компонента, Пачка, Топик, Результат, ИмяСобытия);
	КонецЕсли;

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

Процедура ОтправитьОднуПачку(Компонента, Пачка, Топик, Результат, ИмяСобытия)

	ЗаписьJSON = Новый ЗаписьJSON;
	ЗаписьJSON.УстановитьСтроку();
	ЗаписатьJSON(ЗаписьJSON, Пачка);
	ПачкаJSON = ЗаписьJSON.Закрыть();

	ОтветJSON = "";
	ЕстьПакетныйМетод = Истина;

	Попытка
		ОтветJSON = Компонента.ОтправитьПакетСРезультатом(ПачкаJSON, Топик, 60000);
	Исключение
		ЕстьПакетныйМетод = Ложь;
	КонецПопытки;

	Если НЕ ЕстьПакетныйМетод Тогда
		ОтправитьПоштучно(Компонента, Пачка, Топик, Результат, ИмяСобытия);
		Возврат;
	КонецЕсли;

	Если ПустаяСтрока(ОтветJSON) Тогда
		Результат.Ошибок = Результат.Ошибок + Пачка.Количество();
		ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , ,
			"Пакет не отправлен: " + Компонента.ПолучитьСообщениеОбОшибке());
		Возврат;
	КонецЕсли;

	ЧтениеJSON = Новый ЧтениеJSON;
	ЧтениеJSON.УстановитьСтроку(ОтветJSON);
	ОтветПакета = ПрочитатьJSON(ЧтениеJSON);
	ЧтениеJSON.Закрыть();

	// Компонента возвращает счётчики строками, поэтому явное приведение.
	Доставлено = Число(ОтветПакета["success_count"]);
	НеДоставлено = Число(ОтветПакета["failed_count"]);

	Результат.Отправлено = Результат.Отправлено + Доставлено;
	Результат.Ошибок = Результат.Ошибок + НеДоставлено;

	Если НеДоставлено > 0 Тогда
		ЗаписатьОшибкиПакета(ОтветПакета, ИмяСобытия);
	КонецЕсли;

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

Процедура ЗаписатьОшибкиПакета(ОтветПакета, ИмяСобытия)

	СтрокиОшибок = Новый Массив;

	Для Каждого СтрокаРезультата Из ОтветПакета["results"] Цикл
		Если СтрокаРезультата["delivered"] = "true" Тогда
			Продолжить;
		КонецЕсли;
		СтрокиОшибок.Добавить(СтрШаблон("Ключ: %1. %2",
			СтрокаРезультата["key"], СтрокаРезультата["error"]));
		// Журнал не резиновый — 50 примеров достаточно, остальное в логе компоненты.
		Если СтрокиОшибок.Количество() >= 50 Тогда
			Прервать;
		КонецЕсли;
	КонецЦикла;

	ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , ,
		"Недоставленные сообщения:" + Символы.ПС + СтрСоединить(СтрокиОшибок, Символы.ПС));

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

// Фолбэк на поштучную асинхронную отправку.
// ВАЖНО: у свежих версий компоненты ОтправитьСообщение возвращает ЧИСЛО:
//   0  - сообщение помещено в очередь (это УСПЕХ),
//   -1 - ошибка помещения в очередь.
// Старая проверка "Если РезультатОтправки Тогда" считала все нули ошибками.
//
Процедура ОтправитьПоштучно(Компонента, Пачка, Топик, Результат, ИмяСобытия)

	Для Каждого СообщениеКомпоненты Из Пачка Цикл

		Ключ = "";
		СообщениеКомпоненты.Свойство("key", Ключ);
		Тело = СообщениеКомпоненты["message"];

		РезультатОтправки = Компонента.ОтправитьСообщение(Тело, Топик, -1, Ключ);

		Если РезультатОтправки = -1 Тогда
			Результат.Ошибок = Результат.Ошибок + 1;
			ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , ,
				"Не поставлено в очередь. Ключ: " + Ключ + ". " + Компонента.ПолучитьСообщениеОбОшибке());
		Иначе
			Результат.Отправлено = Результат.Отправлено + 1;
		КонецЕсли;

	КонецЦикла;

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

// Создаёт объект внешней компоненты Kafka из общего макета.
//
Функция СоздатьКомпонентуКафкаКлиент()

	ИмяСобытия = "Kafka. Подключение компоненты";

	Попытка
		Подключено = ПодключитьВнешнююКомпоненту(
			"ОбщийМакет.SimpleKafkaAdapter64",
			"KafkaClient",
			ТипВнешнейКомпоненты.Native,
			ТипПодключенияВнешнейКомпоненты.Изолированно);
	Исключение
		ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , ,
			ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
		Возврат Неопределено;
	КонецПопытки;

	Если НЕ Подключено Тогда
		ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , ,
			"Не удалось подключить компоненту из общего макета SimpleKafkaAdapter64.");
		Возврат Неопределено;
	КонецЕсли;

	Попытка
		Компонента = Новый("AddIn.KafkaClient.simpleKafka1C");
	Исключение
		ЗаписьЖурналаРегистрации(ИмяСобытия, УровеньЖурналаРегистрации.Ошибка, , ,
			"Компонента подключилась, но объект не создался: "
				+ ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
		Возврат Неопределено;
	КонецПопытки;

	Возврат Компонента;

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

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

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