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


Заменяет целиком #Область API_Берга в общем модуле кор_HTTPКоннектор.
// Область РегламентноеЗадание_кор_ЗагрузкаОстатковБерга отсюда удалена: загрузка в регистр
// и подмена остатков для БИТ.УМ - в модуле кор_ОстаткиПоставщиков.

#Область API_Берга

// Клиент API Берга (https://api.berg.ru, версия 1.0): остатки и цены.

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

// Параметры подключения по умолчанию.
//
// Возвращаемое значение:
//  Структура:
//   * Ключ        - Строка - API-ключ (заголовок X-Berg-API-Key).
//   * Сервер      - Строка
//   * Версия      - Строка - версия API в пути запроса.
//   * Таймаут     - Число  - секунды.
//   * ТипыСкладов - Массив из Число - 1 - склад филиала, 2 - ЦС Берга, 3 - дополнительные;
//                   пустой массив - все типы складов.
//
Функция НовыеПараметрыПодключения() Экспорт

	Параметры = Новый Структура;
	Параметры.Вставить("Ключ", "");
	Параметры.Вставить("Сервер", "api.berg.ru");
	Параметры.Вставить("Версия", "v1.0");
	Параметры.Вставить("Таймаут", 60);
	Параметры.Вставить("ТипыСкладов", Новый Массив);

	Возврат Параметры;

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

Функция СохраненныйКлючAPI() Экспорт

	УстановитьПривилегированныйРежим(Истина);
	Возврат СокрЛП(Константы.кор_БергКлючAPI.Получить());

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

Функция ИмяСобытияЖурнала() Экспорт

	Возврат "Берг.API";

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

// Запрашивает у Берга остатки по позициям пачками по 50.
//
// Параметры:
//  ПараметрыПодключения - см. НовыеПараметрыПодключения
//  Позиции - Массив из Структура:
//   * Индекс     - Число  - номер позиции у вызывающего; возвращается в Предложения.ИндексПозиции.
//   * ResourceID - Число  - id товара Берга; 0 - искать по артикулу и бренду.
//   * Артикул    - Строка
//   * BrandID    - Число  - 0, если неизвестен.
//   * Бренд      - Строка
//
// Возвращаемое значение:
//  Структура:
//   * Предложения     - ТаблицаЗначений - см. НоваяТаблицаПредложений.
//                       Товар Берга без предложений даёт одну строку с СкладID = 0 и нулями:
//                       так вызывающий узнаёт его id и понимает, что остатка нет.
//   * Ошибки          - Массив из Строка
//   * ИндексыСОшибкой - Массив из Число - Индекс позиций из пачек, которые не загрузились.
//   * КоличествоПачек - Число
//   * ОтветыСервера   - Массив из Строка - сырые ответы для отладки.
//
Функция ПолучитьОстатки(ПараметрыПодключения, Позиции) Экспорт

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

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

	Для Каждого Позиция Из Позиции Цикл
		Пачка.Добавить(Позиция);
		Если Пачка.Количество() = РазмерПачки Тогда
			Результат.КоличествоПачек = Результат.КоличествоПачек + 1;
			ЗагрузитьПачку(ПараметрыПодключения, Пачка, Результат.КоличествоПачек, Результат);
			Пачка = Новый Массив;
		КонецЕсли;
	КонецЦикла;

	Если Пачка.Количество() > 0 Тогда
		Результат.КоличествоПачек = Результат.КоличествоПачек + 1;
		ЗагрузитьПачку(ПараметрыПодключения, Пачка, Результат.КоличествоПачек, Результат);
	КонецЕсли;

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

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

Функция НоваяТаблицаПредложений() Экспорт

	ТипЧисло  = Новый ОписаниеТипов("Число");
	ТипСтрока = Новый ОписаниеТипов("Строка");
	ТипБулево = Новый ОписаниеТипов("Булево");

	Таблица = Новый ТаблицаЗначений;
	Таблица.Колонки.Добавить("ИндексПозиции", ТипЧисло);
	Таблица.Колонки.Добавить("ResourceID", ТипЧисло);
	Таблица.Колонки.Добавить("Артикул", ТипСтрока);
	Таблица.Колонки.Добавить("Бренд", ТипСтрока);
	Таблица.Колонки.Добавить("Наименование", ТипСтрока);
	Таблица.Колонки.Добавить("СкладID", ТипЧисло);
	Таблица.Колонки.Добавить("Количество", ТипЧисло);
	Таблица.Колонки.Добавить("Цена", ТипЧисло);
	Таблица.Колонки.Добавить("БольшеДоступно", ТипБулево);

	Возврат Таблица;

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

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

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

Процедура ЗагрузитьПачку(ПараметрыПодключения, Пачка, НомерПачки, Результат)

	// Берг не возвращает порядковый номер позиции запроса, поэтому ответ сопоставляется
	// обратно: по id товара, если он был в запросе, иначе по нормализованному артикулу.
	ИндексыПоID = Новый Соответствие;
	ИндексыПоАртикулу = Новый Соответствие;

	Items = Новый Массив;
	Для Каждого Позиция Из Пачка Цикл
		Элемент = Новый Соответствие;
		Если Позиция.ResourceID <> 0 Тогда
			Элемент.Вставить("resource_id", Позиция.ResourceID);
			ДобавитьИндекс(ИндексыПоID, Позиция.ResourceID, Позиция.Индекс);
		Иначе
			Элемент.Вставить("resource_article", СокрЛП(Позиция.Артикул));
			Если Позиция.BrandID <> 0 Тогда
				Элемент.Вставить("brand_id", Позиция.BrandID);
			ИначеЕсли ЗначениеЗаполнено(Позиция.Бренд) Тогда
				Элемент.Вставить("brand_name", СокрЛП(Позиция.Бренд));
			КонецЕсли;
			ДобавитьИндекс(ИндексыПоАртикулу, КлючАртикула(Позиция.Артикул), Позиция.Индекс);
		КонецЕсли;
		Items.Добавить(Элемент);
	КонецЦикла;

	Тело = Новый Соответствие;
	Тело.Вставить("analogs", 0);
	Тело.Вставить("items", Items);
	Если ПараметрыПодключения.ТипыСкладов.Количество() > 0 Тогда
		Тело.Вставить("warehouse_types", ПараметрыПодключения.ТипыСкладов);
	КонецЕсли;

	Ответ = ВыполнитьЗапросБерг(ПараметрыПодключения, "POST", "ordering/get_stock", Тело);
	Результат.ОтветыСервера.Добавить(СтрШаблон("Пачка %1, HTTP %2" + Символы.ПС + "%3",
		НомерПачки, Ответ.КодСостояния, Ответ.Текст));

	Если ЗначениеЗаполнено(Ответ.Ошибка) Тогда
		Результат.Ошибки.Добавить(СтрШаблон(НСтр("ru = 'Пачка %1: %2'"), НомерПачки, Ответ.Ошибка));
		Для Каждого Позиция Из Пачка Цикл
			Результат.ИндексыСОшибкой.Добавить(Позиция.Индекс);
		КонецЦикла;
		Возврат;
	КонецЕсли;

	Для Каждого Товар Из МассивИзОтвета(Ответ.Данные, "resources") Цикл

		ResourceID = ЧислоИзОтвета(Товар.Получить("id"));
		Артикул = СтрокаИзОтвета(Товар.Получить("article"));

		Индексы = ИндексыПоID.Получить(ResourceID);
		Если Индексы = Неопределено Тогда
			Индексы = ИндексыПоАртикулу.Получить(КлючАртикула(Артикул));
		КонецЕсли;
		Если Индексы = Неопределено Тогда
			Продолжить;
		КонецЕсли;

		Бренд = "";
		ДанныеБренда = Товар.Получить("brand");
		Если ТипЗнч(ДанныеБренда) = Тип("Соответствие") Тогда
			Бренд = СтрокаИзОтвета(ДанныеБренда.Получить("name"));
		КонецЕсли;

		ОписаниеТовара = Новый Структура("ResourceID, Артикул, Бренд, Наименование",
			ResourceID, Артикул, Бренд, СтрокаИзОтвета(Товар.Получить("name")));

		Предложения = МассивИзОтвета(Товар, "offers");
		Для Каждого Индекс Из Индексы Цикл

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

			Для Каждого Предложение Из Предложения Цикл
				Строка = Результат.Предложения.Добавить();
				ЗаполнитьЗначенияСвойств(Строка, ОписаниеТовара);
				Строка.ИндексПозиции = Индекс;
				Строка.Количество = ЧислоИзОтвета(Предложение.Получить("quantity"));
				Строка.Цена = ЧислоИзОтвета(Предложение.Получить("price"));
				Строка.БольшеДоступно = (Предложение.Получить("available_more") = Истина);
				Склад = Предложение.Получить("warehouse");
				Если ТипЗнч(Склад) = Тип("Соответствие") Тогда
					Строка.СкладID = ЧислоИзОтвета(Склад.Получить("id"));
				КонецЕсли;
			КонецЦикла;

		КонецЦикла;

	КонецЦикла;

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

Процедура ДобавитьИндекс(Соответствие, Ключ, Индекс)

	Индексы = Соответствие.Получить(Ключ);
	Если Индексы = Неопределено Тогда
		Индексы = Новый Массив;
		Соответствие.Вставить(Ключ, Индексы);
	КонецЕсли;
	Индексы.Добавить(Индекс);

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

// Артикул без регистра, пробелов и разделителей: "ctr-0123 45" и "CTR012345" совпадут.
Функция КлючАртикула(Артикул)

	Исходный = ВРег(СокрЛП(Артикул));
	Результат = "";
	Для Номер = 1 По СтрДлина(Исходный) Цикл
		Символ = Сред(Исходный, Номер, 1);
		Если СтрНайти(" -./\_", Символ) = 0 Тогда
			Результат = Результат + Символ;
		КонецЕсли;
	КонецЦикла;

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

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

Функция ЧислоИзОтвета(Значение)

	Если ТипЗнч(Значение) = Тип("Число") Тогда
		Возврат Значение;
	ИначеЕсли ТипЗнч(Значение) = Тип("Строка") Тогда
		Попытка
			Возврат Число(СтрЗаменить(СокрЛП(Значение), ",", "."));
		Исключение
			Возврат 0;
		КонецПопытки;
	КонецЕсли;

	Возврат 0;

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

Функция СтрокаИзОтвета(Значение)

	Возврат ?(Значение = Неопределено, "", СокрЛП(Строка(Значение)));

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

Функция ВыполнитьЗапросБерг(ПараметрыПодключения, Метод, Ресурс, Тело = Неопределено)

	Результат = Новый Структура("КодСостояния, Текст, Данные, Ошибка", 0, "", Неопределено, "");

	Если ПустаяСтрока(ПараметрыПодключения.Ключ) Тогда
		Результат.Ошибка = НСтр("ru = 'Не указан API-ключ Берга.'");
		Возврат Результат;
	КонецЕсли;

	Адрес = СтрШаблон("/%1/%2.json", ПараметрыПодключения.Версия, Ресурс);

	HTTPЗапрос = Новый HTTPЗапрос(Адрес);
	HTTPЗапрос.Заголовки.Вставить("X-Berg-API-Key", ПараметрыПодключения.Ключ);
	HTTPЗапрос.Заголовки.Вставить("Accept", "application/json");
	Если Тело <> Неопределено Тогда
		HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
		HTTPЗапрос.УстановитьТелоИзСтроки(ОбщегоНазначения.ЗначениеВJSON(Тело), КодировкаТекста.UTF8,
			ИспользованиеByteOrderMark.НеИспользовать);
	КонецЕсли;

	Попытка
		Соединение = Новый HTTPСоединение(ПараметрыПодключения.Сервер, 443,,,
			ПолучениеФайловИзИнтернета.ПолучитьПрокси("https"),
			ПараметрыПодключения.Таймаут,
			ОбщегоНазначенияКлиентСервер.НовоеЗащищенноеСоединение());
		Если Метод = "POST" Тогда
			HTTPОтвет = Соединение.ОтправитьДляОбработки(HTTPЗапрос);
		Иначе
			HTTPОтвет = Соединение.Получить(HTTPЗапрос);
		КонецЕсли;
	Исключение
		Результат.Ошибка = СтрШаблон(НСтр("ru = 'Не удалось выполнить запрос к %1: %2'"),
			ПараметрыПодключения.Сервер, КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
		ЗаписьЖурналаРегистрации(ИмяСобытияЖурнала(), УровеньЖурналаРегистрации.Ошибка,,,
			Адрес + Символы.ПС + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
		Возврат Результат;
	КонецПопытки;

	Результат.КодСостояния = HTTPОтвет.КодСостояния;
	Результат.Текст = HTTPОтвет.ПолучитьТелоКакСтроку(КодировкаТекста.UTF8);

	Попытка
		Результат.Данные = ОбщегоНазначения.JSONВЗначение(Результат.Текст);
	Исключение
		Результат.Данные = Неопределено;
	КонецПопытки;

	ОписаниеОшибок = ОписаниеОшибокAPI(Результат.Данные);
	Если Результат.КодСостояния <> 200 Или ЗначениеЗаполнено(ОписаниеОшибок) Тогда
		Результат.Ошибка = СтрШаблон(НСтр("ru = 'HTTP %1. %2'"), Результат.КодСостояния,
			?(ЗначениеЗаполнено(ОписаниеОшибок), ОписаниеОшибок, Лев(Результат.Текст, 500)));
		ЗаписьЖурналаРегистрации(ИмяСобытияЖурнала(), УровеньЖурналаРегистрации.Ошибка,,,
			Адрес + Символы.ПС + Результат.Ошибка);
	ИначеЕсли Результат.Данные = Неопределено Тогда
		Результат.Ошибка = НСтр("ru = 'Ответ сервера не является JSON.'");
	КонецЕсли;

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

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

Функция ОписаниеОшибокAPI(Данные)

	Тексты = Новый Массив;
	Для Каждого Ошибка Из МассивИзОтвета(Данные, "errors") Цикл
		Тексты.Добавить(СтрШаблон("%1: %2", ЗначениеПоля(Ошибка, "code", "сode"), Ошибка.Получить("text")));
	КонецЦикла;

	Возврат СтрСоединить(Тексты, "; ");

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

Функция МассивИзОтвета(Данные, ИмяПоля)

	Результат = Новый Массив;
	Если ТипЗнч(Данные) <> Тип("Соответствие") Тогда
		Возврат Результат;
	КонецЕсли;

	Значение = Данные.Получить(ИмяПоля);
	Если ТипЗнч(Значение) <> Тип("Массив") Тогда
		Возврат Результат;
	КонецЕсли;

	Для Каждого Элемент Из Значение Цикл
		Если ТипЗнч(Элемент) = Тип("Соответствие") Тогда
			Результат.Добавить(Элемент);
		КонецЕсли;
	КонецЦикла;

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

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

Функция ЗначениеПоля(Данные, ИмяПоля, ЗапасноеИмяПоля)

	Значение = Данные.Получить(ИмяПоля);
	Если Значение = Неопределено Тогда
		Значение = Данные.Получить(ЗапасноеИмяПоля);
	КонецЕсли;

	Возврат Значение;

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

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

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