Загрузка данных
///////////////////////////////////////////////////////////////////////////////
// Сопоставление номенклатуры чеков ЗПЧ -> БП, версия 3
//
// Изменения относительно v2:
// * подбор разбит на три фазы вместо одного прохода:
// фаза 1 — пул строго по артикулу, только со свободным остатком;
// фаза 2 — пул по наименованию / наименованию поставщика, только со свободным остатком;
// фаза 3 — фолбэк без остатка, с предупреждением.
// У карточки один артикул и одно наименование, поэтому пулы внутри фазы
// не пересекаются — гонки между группами за одну карточку нет,
// а точное совпадение артикула выигрывает глобально.
// * случай "одно наименование, разные артикулы" разбирается в фазе 2:
// такие строки попадают в одну группу и делятся по цене.
// * исправлено: в v2 фолбэк срабатывал внутри группы и помечал строку
// разобранной, из-за чего поиск по наименованию до неё не доходил.
//
// ВыводитьДиагностику — на время отладки Истина, потом Ложь.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Процедура ЗаполнитьНоменклатуруЧеков(ДанныеДокументов)
ВыводитьДиагностику = Истина;
// "Номенклатура|Склад" -> уже разобранное количество в этом пакете
Расход = Новый Соответствие;
Строки = Новый ТаблицаЗначений;
Строки.Колонки.Добавить("Данные"); // ссылка на структуру строки товара
Строки.Колонки.Добавить("СкладБП");
Строки.Колонки.Добавить("ДатаОстатков");
Строки.Колонки.Добавить("КатНомер");
Строки.Колонки.Добавить("Наименование");
Строки.Колонки.Добавить("НаимПоставщика");
Строки.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
Строки.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));
Строки.Колонки.Добавить("КлючАртикул");
Строки.Колонки.Добавить("КлючНаименование");
Строки.Колонки.Добавить("Разобрана", Новый ОписаниеТипов("Булево"));
// --- Проход 1: плоская таблица всех строк пакета -------------------------
// В ДанныеДокументов нет ссылки на документ — только ДатаДокумента, Склад и
// Товары (массив). Документ в пакете идентифицируем порядковым номером.
ИндексДокумента = 0;
Для Каждого СтрокаДокумента Из ДанныеДокументов Цикл
ИндексДокумента = ИндексДокумента + 1;
СоответствиеСкладовЧекиОбработка(СтрокаДокумента);
ДатаДокумента = ДатаДокументаСтроки(СтрокаДокумента);
СкладБП = СкладБППоСкладуЗПЧ(СтрокаДокумента.Склад);
Если НЕ СтрокаДокумента.Свойство("Товары") Тогда
Продолжить;
КонецЕсли;
Для Каждого СтрокаТовара Из СтрокаДокумента.Товары Цикл
// сбрасываем результат прошлой загрузки, если структура переиспользуется
СтрокаТовара.Вставить("НоменклатураБП", Неопределено);
СтрокаТовара.Вставить("ХарактеристикаБП", Неопределено);
Новая = Строки.Добавить();
Новая.Данные = СтрокаТовара;
Новая.СкладБП = СкладБП;
Новая.ДатаОстатков = КонецДня(ДатаДокумента) + 1;
Новая.КатНомер = ЗначениеСтроки(СтрокаТовара, "КаталожныйНомерНоменклатуры", "Артикул");
Новая.Наименование = ЗначениеСтроки(СтрокаТовара, "Номенклатура");
Новая.НаимПоставщика = ЗначениеСтроки(СтрокаТовара, "НоменклатураПоставщика");
Кол = 0;
Если СтрокаТовара.Свойство("Количество") Тогда
Кол = СтрокаТовара.Количество;
КонецЕсли;
Новая.Количество = ?(Кол > 0, Кол, 1);
Если СтрокаТовара.Свойство("Цена") Тогда
Новая.Цена = СтрокаТовара.Цена;
КонецЕсли;
Префикс = Формат(ИндексДокумента, "ЧГ=0") + "|" + Строка(СкладБП) + "|";
Новая.КлючАртикул = Префикс + "А:" + Новая.КатНомер;
Новая.КлючНаименование = Префикс + "Н:" + Новая.Наименование;
КонецЦикла;
КонецЦикла;
Если ВыводитьДиагностику Тогда
ИндексСклада = ИндексСубконтоСклады();
ОбщегоНазначения.СообщитьПользователю(
?(ИндексСклада > 0,
СтрШаблон("ДИАГНОСТИКА: субконто ""Склады"" на 41.01 найдено (Субконто%1), отбор по складу применяется", ИндексСклада),
"ДИАГНОСТИКА: субконто ""Склады"" на 41.01 НЕТ — остатки считаются без отбора по складу"));
КонецЕсли;
// --- Фаза 1: пул по артикулу --------------------------------------------
ОбработатьФазу(Строки, Расход, "Артикул", ВыводитьДиагностику);
// --- Фаза 2: пул по наименованию ----------------------------------------
ОбработатьФазу(Строки, Расход, "Наименование", ВыводитьДиагностику);
// --- Фаза 3: фолбэк без остатка -----------------------------------------
ФолбэкБезОстатка(Строки, ВыводитьДиагностику);
КонецПроцедуры
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Процедура ОбработатьФазу(Строки, Расход, Режим, ВыводитьДиагностику)
ИмяКлюча = ?(Режим = "Артикул", "КлючАртикул", "КлючНаименование");
Ключи = Новый Соответствие;
Для Каждого С Из Строки Цикл
Если С.Разобрана Тогда
Продолжить;
КонецЕсли;
// в фазе по артикулу участвуют только строки с заполненным артикулом
Если Режим = "Артикул" И НЕ ЗначениеЗаполнено(С.КатНомер) Тогда
Продолжить;
КонецЕсли;
Если Режим = "Наименование"
И НЕ ЗначениеЗаполнено(С.Наименование)
И НЕ ЗначениеЗаполнено(С.НаимПоставщика) Тогда
Продолжить;
КонецЕсли;
Ключи.Вставить(С[ИмяКлюча], Истина);
КонецЦикла;
Для Каждого КлючЗначение Из Ключи Цикл
Отбор = Новый Структура(ИмяКлюча + ", Разобрана", КлючЗначение.Ключ, Ложь);
НайденныеСтроки = Строки.НайтиСтроки(Отбор);
Если НайденныеСтроки.Количество() = 0 Тогда
Продолжить;
КонецЕсли;
РаспределитьГруппу(НайденныеСтроки, Расход, Режим, ВыводитьДиагностику);
КонецЦикла;
КонецПроцедуры
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Процедура РаспределитьГруппу(НайденныеСтроки, Расход, Режим, ВыводитьДиагностику)
Образец = НайденныеСтроки[0];
Кандидаты = КандидатыПоКлючу(Режим, Образец.КатНомер, Образец.Наименование,
Образец.НаимПоставщика, Образец.СкладБП, Образец.ДатаОстатков);
Если Кандидаты.Количество() = 0 Тогда
Возврат; // строки останутся неразобранными, попадут в следующую фазу
КонецЕсли;
// свободный остаток и приоритет по наименованию
Для Каждого К Из Кандидаты Цикл
Ключ = КлючРасхода(К.Номенклатура, Образец.СкладБП);
УжеВзято = Расход[Ключ];
К.Свободно = К.Остаток - ?(УжеВзято = Неопределено, 0, УжеВзято);
Если К.Наименование = Образец.Наименование Тогда
К.Приоритет = 0;
ИначеЕсли ЗначениеЗаполнено(Образец.НаимПоставщика)
И НРег(К.Наименование) = НРег(Образец.НаимПоставщика) Тогда
К.Приоритет = 1;
Иначе
К.Приоритет = 2;
КонецЕсли;
КонецЦикла;
Кандидаты.Сортировать("Себестоимость Убыв, Приоритет Возр");
// ранговое сопоставление: дороже продано <-> дороже куплено.
// НайтиСтроки возвращает массив, сортируем копией таблицы.
Порядок = СтрокиПоУбываниюЦены(НайденныеСтроки);
Для Каждого С Из Порядок Цикл
Выбранный = Неопределено;
Для Каждого К Из Кандидаты Цикл
Если К.Свободно >= С.Количество Тогда
Выбранный = К;
Прервать;
КонецЕсли;
КонецЦикла;
Если Выбранный = Неопределено Тогда
Продолжить; // уйдёт в следующую фазу
КонецЕсли;
Выбранный.Свободно = Выбранный.Свободно - С.Количество;
Ключ = КлючРасхода(Выбранный.Номенклатура, С.СкладБП);
УжеВзято = Расход[Ключ];
Расход.Вставить(Ключ, ?(УжеВзято = Неопределено, 0, УжеВзято) + С.Количество);
С.Данные.Вставить("НоменклатураБП", Выбранный.Номенклатура);
С.Разобрана = Истина;
Если ВыводитьДиагностику И (Режим = "Наименование" ИЛИ Выбранный.Приоритет > 0) Тогда
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон("[%1] ""%2"" (цена %3) -> ""%4"" (себест. %5, остаток %6)",
Режим, С.Наименование, Формат(С.Цена, "ЧДЦ=2"),
Строка(Выбранный.Номенклатура),
Формат(Выбранный.Себестоимость, "ЧДЦ=2"),
Формат(Выбранный.Остаток, "ЧДЦ=3")));
КонецЕсли;
КонецЦикла;
КонецПроцедуры
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Процедура ФолбэкБезОстатка(Строки, ВыводитьДиагностику)
// карточки, уже отданные фолбэком, чтобы он не плодил дубли
ВыданныеФолбэком = Новый Соответствие;
Для Каждого С Из Строки Цикл
Если С.Разобрана Тогда
Продолжить;
КонецЕсли;
Кандидаты = КандидатыПоКлючу("Артикул", С.КатНомер, С.Наименование,
С.НаимПоставщика, С.СкладБП, С.ДатаОстатков);
Если Кандидаты.Количество() = 0 Тогда
Кандидаты = КандидатыПоКлючу("Наименование", С.КатНомер, С.Наименование,
С.НаимПоставщика, С.СкладБП, С.ДатаОстатков);
КонецЕсли;
Если Кандидаты.Количество() = 0 Тогда
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон("Артикул ""%1"" (%2): номенклатура в базе не найдена",
С.КатНомер, С.Наименование));
Продолжить;
КонецЕсли;
Для Каждого К Из Кандидаты Цикл
Если К.Наименование = С.Наименование Тогда
К.Приоритет = 0;
ИначеЕсли ЗначениеЗаполнено(С.НаимПоставщика)
И НРег(К.Наименование) = НРег(С.НаимПоставщика) Тогда
К.Приоритет = 1;
Иначе
К.Приоритет = 2;
КонецЕсли;
КонецЦикла;
Кандидаты.Сортировать("Приоритет Возр, Себестоимость Убыв");
Выбранный = Неопределено;
Для Каждого К Из Кандидаты Цикл
Если ВыданныеФолбэком[К.Номенклатура] = Неопределено Тогда
Выбранный = К;
Прервать;
КонецЕсли;
КонецЦикла;
Если Выбранный = Неопределено Тогда
Выбранный = Кандидаты[0];
КонецЕсли;
ВыданныеФолбэком.Вставить(Выбранный.Номенклатура, Истина);
С.Данные.Вставить("НоменклатураБП", Выбранный.Номенклатура);
С.Разобрана = Истина;
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон("ВНИМАНИЕ: ""%1"" (арт. %2, кол-во %3) — свободных остатков нет ни у одного из %4 кандидатов. Подставлена ""%5"", требуется ручная проверка",
С.Наименование, С.КатНомер, Формат(С.Количество, "ЧДЦ=0"),
Кандидаты.Количество(), Строка(Выбранный.Номенклатура)));
Если ВыводитьДиагностику Тогда
Для Каждого К Из Кандидаты Цикл
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон(" кандидат ""%1"": остаток %2, себест. %3, приоритет %4",
Строка(К.Номенклатура),
Формат(К.Остаток, "ЧДЦ=3"),
Формат(К.Себестоимость, "ЧДЦ=2"),
Формат(К.Приоритет, "ЧДЦ=0")));
КонецЦикла;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
////////////////////////////////////////////////////////////////////////////////
// Пул кандидатов. Режим "Артикул" — строго по артикулу.
// Режим "Наименование" — по наименованию и наименованию поставщика.
// Пулы одного режима между собой не пересекаются: у карточки один артикул
// и одно наименование.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Функция КандидатыПоКлючу(Режим, КатНомер, Наименование, НаимПоставщика, Склад, ДатаОстатков)
Результат = Новый ТаблицаЗначений;
Результат.Колонки.Добавить("Номенклатура");
Результат.Колонки.Добавить("Наименование");
Результат.Колонки.Добавить("Остаток", Новый ОписаниеТипов("Число"));
Результат.Колонки.Добавить("Себестоимость", Новый ОписаниеТипов("Число"));
Результат.Колонки.Добавить("Свободно", Новый ОписаниеТипов("Число"));
Результат.Колонки.Добавить("Приоритет", Новый ОписаниеТипов("Число"));
Если Режим = "Артикул" Тогда
Если НЕ ЗначениеЗаполнено(КатНомер) Тогда
Возврат Результат;
КонецЕсли;
УсловиеОтбора = "Ном.Артикул = &КатНомер";
Иначе
Если НЕ ЗначениеЗаполнено(Наименование) И НЕ ЗначениеЗаполнено(НаимПоставщика) Тогда
Возврат Результат;
КонецЕсли;
УсловиеОтбора = "";
Если ЗначениеЗаполнено(Наименование) Тогда
УсловиеОтбора = "Ном.Наименование = &Наименование";
КонецЕсли;
Если ЗначениеЗаполнено(НаимПоставщика) Тогда
УсловиеОтбора = УсловиеОтбора
+ ?(ПустаяСтрока(УсловиеОтбора), "", " ИЛИ ")
+ "Ном.Наименование = &НаимПоставщика";
КонецЕсли;
УсловиеОтбора = "(" + УсловиеОтбора + ")";
КонецЕсли;
ИндексСклада = ИндексСубконтоСклады();
ПрименятьСклад = (ИндексСклада > 0) И ЗначениеЗаполнено(Склад);
УсловиеСклада = ?(ПрименятьСклад,
СтрШаблон("
| И Субконто%1 = &Склад", Формат(ИндексСклада, "ЧГ=0")),
"");
ТекстЗапроса =
"ВЫБРАТЬ
| Ном.Ссылка КАК Номенклатура,
| Ном.Наименование КАК Наименование
|ПОМЕСТИТЬ ВТКандидаты
|ИЗ
| Справочник.Номенклатура КАК Ном
|ГДЕ
| [УСЛОВИЕ_ОТБОРА]
| И НЕ Ном.ПометкаУдаления
|;
|////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| К.Номенклатура КАК Номенклатура,
| К.Наименование КАК Наименование,
| ЕСТЬNULL(Ост.КоличествoОстатокДт, 0) КАК Остаток,
| ЕСТЬNULL(Ост.СуммаОстатокДт, 0) КАК СуммаОстатка
|ИЗ
| ВТКандидаты КАК К
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрБухгалтерии.Хозрасчетный.Остатки(
| &ДатаОстатков,
| Счет = &Счет,
| ,
| Субконто1 В (ВЫБРАТЬ Т.Номенклатура ИЗ ВТКандидаты КАК Т)[УСЛОВИЕ_СКЛАДА]) КАК Ост
| ПО К.Номенклатура = Ост.Субконто1";
// опечаткозащита: имя поля пишем один раз
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "КоличествoОстатокДт", "КоличествоОстатокДт");
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "[УСЛОВИЕ_ОТБОРА]", УсловиеОтбора);
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "[УСЛОВИЕ_СКЛАДА]", УсловиеСклада);
Запрос = Новый Запрос(ТекстЗапроса);
Запрос.УстановитьПараметр("ДатаОстатков", ДатаОстатков);
Запрос.УстановитьПараметр("Счет", ПланыСчетов.Хозрасчетный.НайтиПоКоду("41.01"));
Если Режим = "Артикул" Тогда
Запрос.УстановитьПараметр("КатНомер", СокрЛП(КатНомер));
Иначе
Если ЗначениеЗаполнено(Наименование) Тогда
Запрос.УстановитьПараметр("Наименование", СокрЛП(Наименование));
КонецЕсли;
Если ЗначениеЗаполнено(НаимПоставщика) Тогда
Запрос.УстановитьПараметр("НаимПоставщика", СокрЛП(НаимПоставщика));
КонецЕсли;
КонецЕсли;
Если ПрименятьСклад Тогда
Запрос.УстановитьПараметр("Склад", Склад);
КонецЕсли;
Попытка
Выборка = Запрос.Выполнить().Выбрать();
Исключение
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон("Ошибка запроса кандидатов (%1, ""%2""): %3",
Режим, ?(Режим = "Артикул", КатНомер, Наименование), ОписаниеОшибки()));
Возврат Результат;
КонецПопытки;
Пока Выборка.Следующий() Цикл
Стр = Результат.Добавить();
Стр.Номенклатура = Выборка.Номенклатура;
Стр.Наименование = Выборка.Наименование;
Стр.Остаток = Выборка.Остаток;
Стр.Себестоимость = ?(Выборка.Остаток > 0, Выборка.СуммаОстатка / Выборка.Остаток, 0);
КонецЦикла;
Возврат Результат;
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// Вспомогательные
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Функция СтрокиПоУбываниюЦены(НайденныеСтроки)
Сорт = Новый ТаблицаЗначений;
Сорт.Колонки.Добавить("Строка");
Сорт.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));
Для Каждого С Из НайденныеСтроки Цикл
Н = Сорт.Добавить();
Н.Строка = С;
Н.Цена = С.Цена;
КонецЦикла;
Сорт.Сортировать("Цена Убыв");
Результат = Новый Массив;
Для Каждого Н Из Сорт Цикл
Результат.Добавить(Н.Строка);
КонецЦикла;
Возврат Результат;
КонецФункции
&НаСервере
Функция ИндексСубконтоСклады()
Счет = ПланыСчетов.Хозрасчетный.НайтиПоКоду("41.01");
Если НЕ ЗначениеЗаполнено(Счет) Тогда
Возврат 0;
КонецЕсли;
ВидСклады = ПланыВидовХарактеристик.ВидыСубконтоХозрасчетные.Склады;
Индекс = 0;
Для Каждого СтрокаСубконто Из Счет.ВидыСубконто Цикл
Индекс = Индекс + 1;
Если СтрокаСубконто.ВидСубконто = ВидСклады Тогда
Возврат Индекс;
КонецЕсли;
КонецЦикла;
Возврат 0;
КонецФункции
&НаСервере
Функция КлючРасхода(Номенклатура, Склад)
Возврат Строка(Номенклатура) + "|" + Строка(Склад);
КонецФункции
&НаСервере
Функция ЗначениеСтроки(Структура, ИмяПоля, ИмяПоляАльт = "")
Значение = "";
Если Структура.Свойство(ИмяПоля) И ЗначениеЗаполнено(Структура[ИмяПоля]) Тогда
Значение = СокрЛП(Строка(Структура[ИмяПоля]));
ИначеЕсли ЗначениеЗаполнено(ИмяПоляАльт)
И Структура.Свойство(ИмяПоляАльт) И ЗначениеЗаполнено(Структура[ИмяПоляАльт]) Тогда
Значение = СокрЛП(Строка(Структура[ИмяПоляАльт]));
КонецЕсли;
Возврат Значение;
КонецФункции
&НаСервере
Функция ДатаДокументаСтроки(СтрокаДокумента)
Если СтрокаДокумента.Свойство("ДатаДокумента") Тогда
Попытка
Возврат Дата(СтрокаДокумента.ДатаДокумента);
Исключение
КонецПопытки;
КонецЕсли;
Возврат ТекущаяДатаСеанса();
КонецФункции
&НаСервере
Функция СкладБППоСкладуЗПЧ(СкладЗПЧ)
СтрокиСклада = СоответствиеСкладовЧеки.НайтиСтроки(Новый Структура("СкладЗПЧ", СкладЗПЧ));
Если СтрокиСклада.Количество() > 0 Тогда
Возврат СтрокиСклада[0].СкладБП;
КонецЕсли;
Возврат Справочники.Склады.ПустаяСсылка();
КонецФункции