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


///////////////////////////////////////////////////////////////////////////////
// Сопоставление номенклатуры чеков ЗПЧ -> БП, версия 3
//
// Изменения относительно v2:
//   * подбор разбит на три фазы вместо одного прохода:
//       фаза 1 — пул строго по артикулу, только со свободным остатком;
//       фаза 2 — пул по наименованию / наименованию поставщика, только со свободным остатком;
//       фаза 3 — фолбэк без остатка, с предупреждением.
//     У карточки один артикул и одно наименование, поэтому пулы внутри фазы
//     не пересекаются — гонки между группами за одну карточку нет,
//     а точное совпадение артикула выигрывает глобально.
//   * случай "одно наименование, разные артикулы" разбирается в фазе 2:
//     такие строки попадают в одну группу и делятся по цене.
//   * исправлено: в v2 фолбэк срабатывал внутри группы и помечал строку
//     разобранной, из-за чего поиск по наименованию до неё не доходил.
//
// ВыводитьДиагностику — на время отладки Истина, потом Ложь.
////////////////////////////////////////////////////////////////////////////////

&НаСервере
Процедура ЗаполнитьНоменклатуруЧеков(ДанныеДокументов)

	ВыводитьДиагностику = Истина;

	// "Номенклатура|Склад" -> уже разобранное количество в этом пакете
	Расход = Новый Соответствие;

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

	// --- Проход 1: плоская таблица всех строк пакета -------------------------
	// В ДанныеДокументов нет ссылки на документ — только ДатаДокумента, Склад и
	// Товары (массив). Документ в пакете идентифицируем порядковым номером.
	ИндексДокумента = 0;

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

		ИндексДокумента = ИндексДокумента + 1;

		СоответствиеСкладовЧекиОбработка(СтрокаДокумента);

		ДатаДокумента = ДатаДокументаСтроки(СтрокаДокумента);
		СкладБП       = СкладБППоСкладуЗПЧ(СтрокаДокумента.Склад);

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

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

			// сбрасываем результат прошлой загрузки, если структура переиспользуется
			СтрокаТовара.Вставить("НоменклатураБП", Неопределено);
			СтрокаТовара.Вставить("ХарактеристикаБП", Неопределено);

			Новая = Строки.Добавить();
			Новая.Данные         = СтрокаТовара;
			Новая.СкладБП        = СкладБП;
			Новая.ДатаОстатков   = КонецДня(ДатаДокумента) + 1;
			Новая.КатНомер       = ЗначениеСтроки(СтрокаТовара, "КаталожныйНомерНоменклатуры", "Артикул");
			Новая.Наименование   = ЗначениеСтроки(СтрокаТовара, "Номенклатура");
			Новая.НаимПоставщика = ЗначениеСтроки(СтрокаТовара, "НоменклатураПоставщика");

			Кол = 0;
			Если СтрокаТовара.Свойство("Количество") Тогда
				Кол = СтрокаТовара.Количество;
			КонецЕсли;
			Новая.Количество = ?(Кол > 0, Кол, 1);

			Если СтрокаТовара.Свойство("Цена") Тогда
				Новая.Цена = СтрокаТовара.Цена;
			КонецЕсли;

			Префикс = Формат(ИндексДокумента, "ЧГ=0") + "|" + Строка(СкладБП) + "|";
			Новая.КлючАртикул     = Префикс + "А:" + Новая.КатНомер;
			Новая.КлючНаименование = Префикс + "Н:" + Новая.Наименование;

		КонецЦикла;
	КонецЦикла;

	Если ВыводитьДиагностику Тогда
		ИндексСклада = ИндексСубконтоСклады();
		ОбщегоНазначения.СообщитьПользователю(
			?(ИндексСклада > 0,
				СтрШаблон("ДИАГНОСТИКА: субконто ""Склады"" на 41.01 найдено (Субконто%1), отбор по складу применяется", ИндексСклада),
				"ДИАГНОСТИКА: субконто ""Склады"" на 41.01 НЕТ — остатки считаются без отбора по складу"));
	КонецЕсли;

	// --- Фаза 1: пул по артикулу --------------------------------------------
	ОбработатьФазу(Строки, Расход, "Артикул", ВыводитьДиагностику);

	// --- Фаза 2: пул по наименованию ----------------------------------------
	ОбработатьФазу(Строки, Расход, "Наименование", ВыводитьДиагностику);

	// --- Фаза 3: фолбэк без остатка -----------------------------------------
	ФолбэкБезОстатка(Строки, ВыводитьДиагностику);

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

////////////////////////////////////////////////////////////////////////////////

&НаСервере
Процедура ОбработатьФазу(Строки, Расход, Режим, ВыводитьДиагностику)

	ИмяКлюча = ?(Режим = "Артикул", "КлючАртикул", "КлючНаименование");

	Ключи = Новый Соответствие;
	Для Каждого С Из Строки Цикл

		Если С.Разобрана Тогда
			Продолжить;
		КонецЕсли;

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

		Ключи.Вставить(С[ИмяКлюча], Истина);

	КонецЦикла;

	Для Каждого КлючЗначение Из Ключи Цикл

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

		РаспределитьГруппу(НайденныеСтроки, Расход, Режим, ВыводитьДиагностику);

	КонецЦикла;

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

////////////////////////////////////////////////////////////////////////////////

&НаСервере
Процедура РаспределитьГруппу(НайденныеСтроки, Расход, Режим, ВыводитьДиагностику)

	Образец = НайденныеСтроки[0];

	Кандидаты = КандидатыПоКлючу(Режим, Образец.КатНомер, Образец.Наименование,
		Образец.НаимПоставщика, Образец.СкладБП, Образец.ДатаОстатков);

	Если Кандидаты.Количество() = 0 Тогда
		Возврат; // строки останутся неразобранными, попадут в следующую фазу
	КонецЕсли;

	// свободный остаток и приоритет по наименованию
	Для Каждого К Из Кандидаты Цикл

		Ключ     = КлючРасхода(К.Номенклатура, Образец.СкладБП);
		УжеВзято = Расход[Ключ];
		К.Свободно = К.Остаток - ?(УжеВзято = Неопределено, 0, УжеВзято);

		Если К.Наименование = Образец.Наименование Тогда
			К.Приоритет = 0;
		ИначеЕсли ЗначениеЗаполнено(Образец.НаимПоставщика)
			И НРег(К.Наименование) = НРег(Образец.НаимПоставщика) Тогда
			К.Приоритет = 1;
		Иначе
			К.Приоритет = 2;
		КонецЕсли;

	КонецЦикла;

	Кандидаты.Сортировать("Себестоимость Убыв, Приоритет Возр");

	// ранговое сопоставление: дороже продано <-> дороже куплено.
	// НайтиСтроки возвращает массив, сортируем копией таблицы.
	Порядок = СтрокиПоУбываниюЦены(НайденныеСтроки);

	Для Каждого С Из Порядок Цикл

		Выбранный = Неопределено;

		Для Каждого К Из Кандидаты Цикл
			Если К.Свободно >= С.Количество Тогда
				Выбранный = К;
				Прервать;
			КонецЕсли;
		КонецЦикла;

		Если Выбранный = Неопределено Тогда
			Продолжить; // уйдёт в следующую фазу
		КонецЕсли;

		Выбранный.Свободно = Выбранный.Свободно - С.Количество;
		Ключ = КлючРасхода(Выбранный.Номенклатура, С.СкладБП);
		УжеВзято = Расход[Ключ];
		Расход.Вставить(Ключ, ?(УжеВзято = Неопределено, 0, УжеВзято) + С.Количество);

		С.Данные.Вставить("НоменклатураБП", Выбранный.Номенклатура);
		С.Разобрана = Истина;

		Если ВыводитьДиагностику И (Режим = "Наименование" ИЛИ Выбранный.Приоритет > 0) Тогда
			ОбщегоНазначения.СообщитьПользователю(
				СтрШаблон("[%1] ""%2"" (цена %3) -> ""%4"" (себест. %5, остаток %6)",
					Режим, С.Наименование, Формат(С.Цена, "ЧДЦ=2"),
					Строка(Выбранный.Номенклатура),
					Формат(Выбранный.Себестоимость, "ЧДЦ=2"),
					Формат(Выбранный.Остаток, "ЧДЦ=3")));
		КонецЕсли;

	КонецЦикла;

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

////////////////////////////////////////////////////////////////////////////////

&НаСервере
Процедура ФолбэкБезОстатка(Строки, ВыводитьДиагностику)

	// карточки, уже отданные фолбэком, чтобы он не плодил дубли
	ВыданныеФолбэком = Новый Соответствие;

	Для Каждого С Из Строки Цикл

		Если С.Разобрана Тогда
			Продолжить;
		КонецЕсли;

		Кандидаты = КандидатыПоКлючу("Артикул", С.КатНомер, С.Наименование,
			С.НаимПоставщика, С.СкладБП, С.ДатаОстатков);

		Если Кандидаты.Количество() = 0 Тогда
			Кандидаты = КандидатыПоКлючу("Наименование", С.КатНомер, С.Наименование,
				С.НаимПоставщика, С.СкладБП, С.ДатаОстатков);
		КонецЕсли;

		Если Кандидаты.Количество() = 0 Тогда
			ОбщегоНазначения.СообщитьПользователю(
				СтрШаблон("Артикул ""%1"" (%2): номенклатура в базе не найдена",
					С.КатНомер, С.Наименование));
			Продолжить;
		КонецЕсли;

		Для Каждого К Из Кандидаты Цикл
			Если К.Наименование = С.Наименование Тогда
				К.Приоритет = 0;
			ИначеЕсли ЗначениеЗаполнено(С.НаимПоставщика)
				И НРег(К.Наименование) = НРег(С.НаимПоставщика) Тогда
				К.Приоритет = 1;
			Иначе
				К.Приоритет = 2;
			КонецЕсли;
		КонецЦикла;
		Кандидаты.Сортировать("Приоритет Возр, Себестоимость Убыв");

		Выбранный = Неопределено;
		Для Каждого К Из Кандидаты Цикл
			Если ВыданныеФолбэком[К.Номенклатура] = Неопределено Тогда
				Выбранный = К;
				Прервать;
			КонецЕсли;
		КонецЦикла;
		Если Выбранный = Неопределено Тогда
			Выбранный = Кандидаты[0];
		КонецЕсли;

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

		ОбщегоНазначения.СообщитьПользователю(
			СтрШаблон("ВНИМАНИЕ: ""%1"" (арт. %2, кол-во %3) — свободных остатков нет ни у одного из %4 кандидатов. Подставлена ""%5"", требуется ручная проверка",
				С.Наименование, С.КатНомер, Формат(С.Количество, "ЧДЦ=0"),
				Кандидаты.Количество(), Строка(Выбранный.Номенклатура)));

		Если ВыводитьДиагностику Тогда
			Для Каждого К Из Кандидаты Цикл
				ОбщегоНазначения.СообщитьПользователю(
					СтрШаблон("    кандидат ""%1"": остаток %2, себест. %3, приоритет %4",
						Строка(К.Номенклатура),
						Формат(К.Остаток, "ЧДЦ=3"),
						Формат(К.Себестоимость, "ЧДЦ=2"),
						Формат(К.Приоритет, "ЧДЦ=0")));
			КонецЦикла;
		КонецЕсли;

	КонецЦикла;

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

////////////////////////////////////////////////////////////////////////////////
// Пул кандидатов. Режим "Артикул" — строго по артикулу.
// Режим "Наименование" — по наименованию и наименованию поставщика.
// Пулы одного режима между собой не пересекаются: у карточки один артикул
// и одно наименование.
////////////////////////////////////////////////////////////////////////////////

&НаСервере
Функция КандидатыПоКлючу(Режим, КатНомер, Наименование, НаимПоставщика, Склад, ДатаОстатков)

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

	Если Режим = "Артикул" Тогда
		Если НЕ ЗначениеЗаполнено(КатНомер) Тогда
			Возврат Результат;
		КонецЕсли;
		УсловиеОтбора = "Ном.Артикул = &КатНомер";
	Иначе
		Если НЕ ЗначениеЗаполнено(Наименование) И НЕ ЗначениеЗаполнено(НаимПоставщика) Тогда
			Возврат Результат;
		КонецЕсли;
		УсловиеОтбора = "";
		Если ЗначениеЗаполнено(Наименование) Тогда
			УсловиеОтбора = "Ном.Наименование = &Наименование";
		КонецЕсли;
		Если ЗначениеЗаполнено(НаимПоставщика) Тогда
			УсловиеОтбора = УсловиеОтбора
				+ ?(ПустаяСтрока(УсловиеОтбора), "", " ИЛИ ")
				+ "Ном.Наименование = &НаимПоставщика";
		КонецЕсли;
		УсловиеОтбора = "(" + УсловиеОтбора + ")";
	КонецЕсли;

	ИндексСклада   = ИндексСубконтоСклады();
	ПрименятьСклад = (ИндексСклада > 0) И ЗначениеЗаполнено(Склад);

	УсловиеСклада = ?(ПрименятьСклад,
		СтрШаблон("
		|				И Субконто%1 = &Склад", Формат(ИндексСклада, "ЧГ=0")),
		"");

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

	// опечаткозащита: имя поля пишем один раз
	ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "КоличествoОстатокДт", "КоличествоОстатокДт");
	ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "[УСЛОВИЕ_ОТБОРА]", УсловиеОтбора);
	ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "[УСЛОВИЕ_СКЛАДА]", УсловиеСклада);

	Запрос = Новый Запрос(ТекстЗапроса);
	Запрос.УстановитьПараметр("ДатаОстатков", ДатаОстатков);
	Запрос.УстановитьПараметр("Счет",         ПланыСчетов.Хозрасчетный.НайтиПоКоду("41.01"));
	Если Режим = "Артикул" Тогда
		Запрос.УстановитьПараметр("КатНомер", СокрЛП(КатНомер));
	Иначе
		Если ЗначениеЗаполнено(Наименование) Тогда
			Запрос.УстановитьПараметр("Наименование", СокрЛП(Наименование));
		КонецЕсли;
		Если ЗначениеЗаполнено(НаимПоставщика) Тогда
			Запрос.УстановитьПараметр("НаимПоставщика", СокрЛП(НаимПоставщика));
		КонецЕсли;
	КонецЕсли;
	Если ПрименятьСклад Тогда
		Запрос.УстановитьПараметр("Склад", Склад);
	КонецЕсли;

	Попытка
		Выборка = Запрос.Выполнить().Выбрать();
	Исключение
		ОбщегоНазначения.СообщитьПользователю(
			СтрШаблон("Ошибка запроса кандидатов (%1, ""%2""): %3",
				Режим, ?(Режим = "Артикул", КатНомер, Наименование), ОписаниеОшибки()));
		Возврат Результат;
	КонецПопытки;

	Пока Выборка.Следующий() Цикл
		Стр = Результат.Добавить();
		Стр.Номенклатура  = Выборка.Номенклатура;
		Стр.Наименование  = Выборка.Наименование;
		Стр.Остаток       = Выборка.Остаток;
		Стр.Себестоимость = ?(Выборка.Остаток > 0, Выборка.СуммаОстатка / Выборка.Остаток, 0);
	КонецЦикла;

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

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

////////////////////////////////////////////////////////////////////////////////
// Вспомогательные
////////////////////////////////////////////////////////////////////////////////

&НаСервере
Функция СтрокиПоУбываниюЦены(НайденныеСтроки)

	Сорт = Новый ТаблицаЗначений;
	Сорт.Колонки.Добавить("Строка");
	Сорт.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));

	Для Каждого С Из НайденныеСтроки Цикл
		Н = Сорт.Добавить();
		Н.Строка = С;
		Н.Цена   = С.Цена;
	КонецЦикла;
	Сорт.Сортировать("Цена Убыв");

	Результат = Новый Массив;
	Для Каждого Н Из Сорт Цикл
		Результат.Добавить(Н.Строка);
	КонецЦикла;
	Возврат Результат;

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

&НаСервере
Функция ИндексСубконтоСклады()

	Счет = ПланыСчетов.Хозрасчетный.НайтиПоКоду("41.01");
	Если НЕ ЗначениеЗаполнено(Счет) Тогда
		Возврат 0;
	КонецЕсли;

	ВидСклады = ПланыВидовХарактеристик.ВидыСубконтоХозрасчетные.Склады;

	Индекс = 0;
	Для Каждого СтрокаСубконто Из Счет.ВидыСубконто Цикл
		Индекс = Индекс + 1;
		Если СтрокаСубконто.ВидСубконто = ВидСклады Тогда
			Возврат Индекс;
		КонецЕсли;
	КонецЦикла;

	Возврат 0;

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

&НаСервере
Функция КлючРасхода(Номенклатура, Склад)
	Возврат Строка(Номенклатура) + "|" + Строка(Склад);
КонецФункции

&НаСервере
Функция ЗначениеСтроки(Структура, ИмяПоля, ИмяПоляАльт = "")

	Значение = "";
	Если Структура.Свойство(ИмяПоля) И ЗначениеЗаполнено(Структура[ИмяПоля]) Тогда
		Значение = СокрЛП(Строка(Структура[ИмяПоля]));
	ИначеЕсли ЗначениеЗаполнено(ИмяПоляАльт)
		И Структура.Свойство(ИмяПоляАльт) И ЗначениеЗаполнено(Структура[ИмяПоляАльт]) Тогда
		Значение = СокрЛП(Строка(Структура[ИмяПоляАльт]));
	КонецЕсли;
	Возврат Значение;

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

&НаСервере
Функция ДатаДокументаСтроки(СтрокаДокумента)

	Если СтрокаДокумента.Свойство("ДатаДокумента") Тогда
		Попытка
			Возврат Дата(СтрокаДокумента.ДатаДокумента);
		Исключение
		КонецПопытки;
	КонецЕсли;
	Возврат ТекущаяДатаСеанса();

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

&НаСервере
Функция СкладБППоСкладуЗПЧ(СкладЗПЧ)

	СтрокиСклада = СоответствиеСкладовЧеки.НайтиСтроки(Новый Структура("СкладЗПЧ", СкладЗПЧ));
	Если СтрокиСклада.Количество() > 0 Тогда
		Возврат СтрокиСклада[0].СкладБП;
	КонецЕсли;
	Возврат Справочники.Склады.ПустаяСсылка();

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