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