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


///////////////////////////////////////////////////////////////////////////////
// Подбор номенклатуры БП для строк чеков ЗПЧ.
//
// Ключевые отличия от предыдущей версии:
//  - кандидат выбирается по СВОЕМУ свободному остатку, а не по общему итогу;
//  - накопитель свободных остатков реально участвует в расчёте и хранится
//    в разрезе склад + номенклатура;
//  - если одной карточкой строка не закрывается, она разбивается на части,
//    количество/сумма пересчитываются, части возвращаются в СтрокаДокумента.Товары;
//  - НоменклатураБП заполняется всегда (пустой ссылкой, если кандидатов нет);
//  - остаток считается на ту же дату, которая будет у создаваемого ОРП.
//
// ВНИМАНИЕ: после разбиения строк в СтрокаДокумента.Товары может стать больше,
// чем пришло из ЗПЧ. Код, который строит ОРП, должен ходить по Товары уже
// после этой процедуры и не опираться на исходное число строк.
////////////////////////////////////////////////////////////////////////////////

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

	// Ключ: "GUID склада|GUID номенклатуры", значение: свободный остаток
	// с учётом уже разобранных строк текущего сеанса загрузки.
	СвободныеОстатки = Новый Соответствие;

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

		// Сначала заполняем таблицу соответствия, потом ей пользуемся.
		СоответствиеСкладовЧекиОбработка(СтрокаДокумента);
		СкладБП = СкладБППоСкладуЗПЧ(СтрокаДокумента.Склад);

		// ОРП создаётся концом дня — остаток должен считаться на тот же момент,
		// иначе подбор и проверка при проведении разойдутся.
		ДатаОстатков = КонецДня(Дата(СтрокаДокумента.ДатаДокумента));

		НовыеТовары = Новый Массив;

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

			Требуется = СтрокаТовара.Количество;

			Кандидаты = КандидатыНоменклатуры(СтрокаТовара, СкладБП, ДатаОстатков);
			ЗаполнитьПриоритетыКандидатов(Кандидаты, СтрокаТовара);
			ПодставитьСвободныеОстатки(Кандидаты, СвободныеОстатки, СкладБП);

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

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

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

			// 1) Пытаемся закрыть строку одной карточкой — приоритетной из тех,
			//    у кого хватает собственного свободного остатка.
			ПодходящийКандидат = Неопределено;
			Для Каждого Кандидат Из Кандидаты Цикл
				Если Кандидат.Остаток >= Требуется Тогда
					ПодходящийКандидат = Кандидат;
					Прервать;
				КонецЕсли;
			КонецЦикла;

			Если ПодходящийКандидат <> Неопределено Тогда
				СписатьОстаток(СвободныеОстатки, СкладБП, ПодходящийКандидат.Номенклатура, Требуется);
				ЧастьСтроки = КопияСтрокиТовара(СтрокаТовара, Требуется);
				ЧастьСтроки.Вставить("НоменклатураБП", ПодходящийКандидат.Номенклатура);
				НовыеТовары.Добавить(ЧастьСтроки);
				Продолжить;
			КонецЕсли;

			// 2) Одной карточкой не закрывается — разбиваем строку по кандидатам.
			ОсталосьРаспределить = Требуется;

			Для Каждого Кандидат Из Кандидаты Цикл

				Если ОсталосьРаспределить <= 0 Тогда
					Прервать;
				КонецЕсли;

				Если Кандидат.Остаток <= 0 Тогда
					Продолжить;
				КонецЕсли;

				Взято = ?(Кандидат.Остаток < ОсталосьРаспределить, Кандидат.Остаток, ОсталосьРаспределить);

				ЧастьСтроки = КопияСтрокиТовара(СтрокаТовара, Взято);
				ЧастьСтроки.Вставить("НоменклатураБП", Кандидат.Номенклатура);
				НовыеТовары.Добавить(ЧастьСтроки);

				СписатьОстаток(СвободныеОстатки, СкладБП, Кандидат.Номенклатура, Взято);
				ОсталосьРаспределить = ОсталосьРаспределить - Взято;

			КонецЦикла;

			// 3) Хвост, который не покрыт ничем: вешаем на самого приоритетного
			//    кандидата, чтобы не потерять сумму чека. Строка при проведении
			//    даст ошибку нехватки — это осознанно, дефицит виден пользователю.
			Если ОсталосьРаспределить > 0 Тогда
				ЧастьСтроки = КопияСтрокиТовара(СтрокаТовара, ОсталосьРаспределить);
				ЧастьСтроки.Вставить("НоменклатураБП", Кандидаты[0].Номенклатура);
				НовыеТовары.Добавить(ЧастьСтроки);
				СписатьОстаток(СвободныеОстатки, СкладБП, Кандидаты[0].Номенклатура, ОсталосьРаспределить);
			КонецЕсли;

		КонецЦикла;

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

	КонецЦикла;

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

// Приоритет: 0 — точное совпадение с наименованием чека,
//            1 — пересечение с наименованием поставщика, 2 — прочее.
&НаСервере
Процедура ЗаполнитьПриоритетыКандидатов(Кандидаты, СтрокаТовара)

	НаимЧека = ВРег(СокрЛП(Строка(СтрокаТовара.Номенклатура)));

	НаимПоставщика = "";
	Если СтрокаТовара.Свойство("НоменклатураПоставщика")
		И ЗначениеЗаполнено(СтрокаТовара.НоменклатураПоставщика) Тогда
		НаимПоставщика = ВРег(СокрЛП(Строка(СтрокаТовара.НоменклатураПоставщика)));
	КонецЕсли;

	Для Каждого Кандидат Из Кандидаты Цикл

		НаимКандидата = ВРег(СокрЛП(Кандидат.Наименование));

		Если НаимКандидата = НаимЧека Тогда
			Кандидат.Приоритет = 0;
		ИначеЕсли НЕ ПустаяСтрока(НаимПоставщика)
			И (СтрНайти(НаимПоставщика, НаимКандидата) > 0
				ИЛИ СтрНайти(НаимКандидата, НаимПоставщика) > 0) Тогда
			Кандидат.Приоритет = 1;
		Иначе
			Кандидат.Приоритет = 2;
		КонецЕсли;

	КонецЦикла;

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

// Заменяет остаток из запроса на остаток из накопителя,
// если эта карточка уже участвовала в подборе.
&НаСервере
Процедура ПодставитьСвободныеОстатки(Кандидаты, СвободныеОстатки, Склад)

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

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

&НаСервере
Процедура СписатьОстаток(СвободныеОстатки, Склад, Номенклатура, Количество)

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

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

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

	Возврат Строка(Склад.УникальныйИдентификатор()) + "|"
		+ Строка(Номенклатура.УникальныйИдентификатор());

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

// Копия структуры строки товара с пересчётом количества и зависимых сумм.
&НаСервере
Функция КопияСтрокиТовара(СтрокаТовара, Количество)

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

	ИсходноеКоличество = СтрокаТовара.Количество;
	Копия.Вставить("Количество", Количество);

	Если ИсходноеКоличество = 0 Тогда
		Возврат Копия;
	КонецЕсли;

	Доля = Количество / ИсходноеКоличество;

	Если Копия.Свойство("Сумма") Тогда
		Копия.Вставить("Сумма", Окр(СтрокаТовара.Сумма * Доля, 2));
	КонецЕсли;
	Если Копия.Свойство("СуммаНДС") Тогда
		Копия.Вставить("СуммаНДС", Окр(СтрокаТовара.СуммаНДС * Доля, 2));
	КонецЕсли;

	Возврат Копия;

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

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

	КатНомер     = Лев(СокрЛП(Строка(СтрокаТовара.КаталожныйНомерНоменклатуры)), 25);
	Наименование = СокрЛП(Строка(СтрокаТовара.Номенклатура));

	НаимПоставщика = "";
	Если СтрокаТовара.Свойство("НоменклатураПоставщика")
		И ЗначениеЗаполнено(СтрокаТовара.НоменклатураПоставщика) Тогда
		НаимПоставщика = СокрЛП(Строка(СтрокаТовара.НоменклатураПоставщика));
	КонецЕсли;

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

	ИндексСклада   = ИндексСубконтоСклады();
	ПрименятьСклад = (ИндексСклада > 0) И ЗначениеЗаполнено(Склад);
	УсловиеСклада  = ?(ПрименятьСклад,
		СтрШаблон(" И Субконто%1 = &Склад", Строка(ИндексСклада)), "");

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

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

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

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

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

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

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

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

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

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

	Возврат 0;

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

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

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

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