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