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


////////////////////////////////////////////////////////////////////////////////
// Сопоставление номенклатуры чеков ЗПЧ -> БП, версия 2
//
// Изменения относительно v1 (по результатам тестового прогона, где все 88 строк
// вернули нулевой остаток):
//   * отбор по складу применяется ТОЛЬКО если у счёта 41.01 действительно есть
//     субконто "Склады"; индекс субконто определяется динамически;
//   * ДатаОстатков возвращена к КонецДня(ДатаДокумента) + 1 — приход часто
//     оформляется тем же днём, что и продажа (напр. поступление 12:53, чек 13:59);
//   * фолбэк "нет свободных остатков" больше не выдаёт карточку, уже занятую
//     в этой же группе — иначе дубль воспроизводится;
//   * добавлена диагностика по кандидатам (остаток / себестоимость / свободно).
//
// Заменяет: ЗаполнитьНоменклатуруЧеков, СопоставитьНоменклатуруЧека,
//           НайтиНоменклатуруПоНаименованиюРасширенный,
//           НайтиНоменклатуруПоАртикулуСОстатками,
//           ПолучитьОстатокНоменклатуры, НайтиНоменклатуруПоНаименованию
//
// ВыводитьДиагностику — на время отладки Истина, потом Ложь.
////////////////////////////////////////////////////////////////////////////////

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	КонецЦикла;

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

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

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

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

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

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

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

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

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

		Иначе

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

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

		КонецЕсли;

	КонецЦикла;

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

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

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

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

	Возврат ?(Копия.Количество() > 0, Копия[0], Неопределено);

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

////////////////////////////////////////////////////////////////////////////////
// Один запрос отдаёт пул, остатки и себестоимость.
// Отбор по складу подставляется в текст запроса только если субконто есть.
////////////////////////////////////////////////////////////////////////////////

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	Возврат 0;

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

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

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

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

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

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

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

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

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

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

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

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