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