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