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


////////////////////////////////////////////////////////////////////////////////
// Сопоставление номенклатуры чеков ЗПЧ -> БП
//
// Заменяет: ЗаполнитьНоменклатуруЧеков, СопоставитьНоменклатуруЧека,
//           НайтиНоменклатуруПоНаименованиюРасширенный,
//           НайтиНоменклатуруПоАртикулуСОстатками,
//           ПолучитьОстатокНоменклатуры, НайтиНоменклатуруПоНаименованию (мёртвая)
//
// Ключевые отличия от прежней логики:
//   1. Пул кандидатов строится ПО АРТИКУЛУ, а не по наименованию.
//      Наименование — только приоритет внутри пула.
//   2. Строки одного чека с одним артикулом распределяются ГРУППОЙ:
//      дороже продано -> дороже куплено (ранговое сопоставление по себестоимости).
//   3. Остаток считается по складу чека (Субконто2) и на НАЧАЛО дня.
//   4. Разобранное количество копится в пределах пакета — одна карточка
//      не может уйти в две строки, если на ней 1 шт.
////////////////////////////////////////////////////////////////////////////////

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

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

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

	// --- Проход 1: плоская таблица всех строк пакета -------------------------
	Для Каждого СтрокаДокумента Из ДанныеДокументов Цикл

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

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

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

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

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

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

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

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

			// группа = один чек + один склад + один артикул
			Новая.КлючГруппы = Строка(СтрокаДокумента.Ссылка) + "|"
				+ Строка(СкладБП) + "|" + Новая.КатНомер;

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

	// --- Проход 2: раздача номенклатуры по группам ---------------------------
	Ключи = Новый Соответствие;
	Для Каждого С Из Строки Цикл
		Ключи.Вставить(С.КлючГруппы, Истина);
	КонецЦикла;

	Для Каждого КлючЗначение Из Ключи Цикл
		Отбор  = Новый Структура("КлючГруппы", КлючЗначение.Ключ);
		Группа = Строки.Скопировать(Строки.НайтиСтроки(Отбор));
		РаспределитьГруппу(Группа, Расход);
	КонецЦикла;

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

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

&НаСервере
Процедура РаспределитьГруппу(Группа, Расход)

	Образец = Группа[0];

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

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

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

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

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

	КонецЦикла;

	// ранговое сопоставление: дороже продано <-> дороже куплено
	Группа.Сортировать("Цена Убыв");
	Кандидаты.Сортировать("Себестоимость Убыв, Приоритет Возр");

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

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

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

		Если Выбранный <> Неопределено Тогда

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

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

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

		Иначе

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

		КонецЕсли;

	КонецЦикла;

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

&НаСервере
Функция ЛучшийБезОстатка(Кандидаты)

	Копия = Кандидаты.Скопировать();
	Копия.Сортировать("Приоритет Возр, Себестоимость Убыв");
	Возврат ?(Копия.Количество() > 0, Копия[0], Неопределено);

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

////////////////////////////////////////////////////////////////////////////////
// Один запрос отдаёт и пул, и остатки, и себестоимость.
// ВНИМАНИЕ: проверьте порядок субконто счёта 41.01 в вашей базе —
// предполагается Субконто1 = Номенклатура, Субконто2 = Склад.
////////////////////////////////////////////////////////////////////////////////

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

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

	Если НЕ ЗначениеЗаполнено(КатНомер) ИЛИ НЕ ЗначениеЗаполнено(Склад) Тогда
		Возврат Результат;
	КонецЕсли;

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

	Запрос.УстановитьПараметр("КатНомер",     СокрЛП(КатНомер));
	Запрос.УстановитьПараметр("Склад",        Склад);
	Запрос.УстановитьПараметр("ДатаОстатков", ДатаОстатков);
	Запрос.УстановитьПараметр("Счет",         ПланыСчетов.Хозрасчетный.НайтиПоКоду("41.01"));

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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