Загрузка данных
////////////////////////////////////////////////////////////////////////////////
// Сопоставление номенклатуры чеков ЗПЧ -> БП, версия 4
// Задача Bitrix 68771 «Некорректная загрузка розничных чеков Авто-Точка, Авто-Мастер»
//
// Весь файл — валидный BSL: выводы вынесены в комментарии, его можно вставить
// в модуль формы целиком. Указания по встраиванию — в конце файла.
//
////////////////////////////////////////////////////////////////////////////////
//
// ЧАСТЬ I. ВЫВОДЫ ПО РАЗБОРУ
//
// Проверено по выгрузке конфигурации E:\1c\БП (типовой) (версия 3.0.198.15).
// Доступа к работающей информационной базе не было — где нужен запрос к данным,
// это отмечено явно.
//
//------------------------------------------------------------------------------
// 1. Расширения Автоточка и Автомастер разошлись
//------------------------------------------------------------------------------
//
// Документ «Передача_Claude_Code_загрузка_чеков.md» описывает старый приёмник.
// Это верно только для Автоточки. Автомастер уже переработан: в нём есть
// КонтекстПодбора (склад + дата остатков + накопитель Израсходовано +
// сбор ПроблемыПодбора), резервирование остатка и вывод проблемных строк.
//
// Главное отличие — время подбора. В Автомастере (Form/Module.bsl:1018):
//
// // Здесь только раскладка складов ЗПЧ по складам БП. Номенклатура подбирается
// // в СоздатьДокументыОРП: до подтверждения таблицы соответствия склад неизвестен,
// // а остатки проверяются именно по складу документа.
//
// v3 это откатывал: он брал склад через СкладБППоСкладуЗПЧ в том же проходе,
// где СоответствиеСкладовЧекиОбработка только что заполнил строку автоподбором
// ПодобратьСклад. Если пользователь потом правит склад в форме — номенклатура
// уже подобрана по остаткам чужого склада. В v4 подбор выполняется одним
// пакетным проходом в начале СоздатьДокументыОРП, то есть после подтверждения
// таблицы соответствия. Это даёт и подтверждённый склад, и пакетную группировку.
//
//------------------------------------------------------------------------------
// 2. Субконто счёта 41.01 (пункт 7.1 исходного документа)
//------------------------------------------------------------------------------
//
// Из конфигурации состав определить нельзя, и это не оговорка:
// в ChartsOfAccounts\Хозрасчетный\Ext\Predefined.xml у 41.01 стоит
// <ExtDimensionTypes/> — пусто, и непустых вхождений в файле нет вообще.
// Состав субконто в БП 3.0 — данные базы («Настройка плана счетов»).
//
// Но канонический шаблон для группы счетов «Товары» задан в коде,
// CommonModules\ОбщегоНазначенияБП\Ext\Module.bsl:3893-3923:
//
// 1 Номенклатура Параметр = Истина — всегда
// 2 СтавкиНДС Параметр = Ложь — никогда
// 3 Склады Параметр = "ВестиСкладскойУчет"
// 4 Партии Параметр = "ВестиПартионныйУчет"
//
// Отсюда:
// * Субконто1 = Номенклатура гарантированно. Допущение v3 верно.
// * Склады — Субконто2 при включённом складском учёте.
// * Партии могут быть Субконто2 (если складской выключен) или Субконто3.
//
// И вот здесь v3 ломался. Его запрос не передавал виды субконто (третий
// параметр виртуальной таблицы пустой) и не агрегировал. Виртуальная таблица
// Остатки возвращает строку на каждую комбинацию субконто, поэтому при
// включённом партионном учёте ЛЕВОЕ СОЕДИНЕНИЕ ... ПО К.Номенклатура = Ост.Субконто1
// давало по нескольку строк на одну карточку: в пул кандидатов попадали дубли
// с частичными остатками, себестоимость считалась по одной партии, Свободно
// занижалось. То же самое при выключенном отборе по складу и включённом
// складском учёте — дубли по складам.
//
// Типовой код так не делает: CommonModules\ИнформированиеОтрицательныеОстатки\
// Ext\Module.bsl:104,118 всегда передаёт массив видов субконто явно
// (&СубконтоНоменклатураСклад) и разыменовывает через ВЫРАЗИТЬ(... КАК Справочник.Склады).
//
// В v4 виды субконто передаются массивом. Побочный эффект: индекс Субконто2
// становится детерминированным, и самописный ИндексСубконтоСклады() не нужен.
//
//------------------------------------------------------------------------------
// 3. v3 был регрессом относительно того, что уже написано в Автомастере
//------------------------------------------------------------------------------
//
// Автомастер, Form/Module.bsl:1144-1203, ПолучитьОстатокНоменклатуры:
//
// признак Автомастер (как есть) v3
// -------------------- --------------------------------- ------------------
// виды субконто &ВидыСубконто массивом не передаются
// организация Организация = &Организация отбора нет
// учёт по складам БухгалтерскийУчет. самописный
// ВедетсяУчетПоСкладам(Счет) ИндексСубконтоСклады()
// счета СчетаУчетаТоваров(): 41.01, только 41.01
// 41.02, 41.11
//
// Отбор по организации — не теория: реквизит Организация на форме есть
// (Form/Module.bsl:5) и используется во всех остальных запросах формы.
// ВедетсяУчетПоСкладам — штатная функция поверх кэша
// БухгалтерскийУчетВызовСервераПовтИсп.ПолучитьСвойстваСчета()
// (CommonModules\БухгалтерскийУчет\Ext\Module.bsl:478). v3 вместо неё на каждый
// запрос кандидатов делал НайтиПоКоду("41.01") и читал табличную часть
// Счет.ВидыСубконто из базы.
//
// В легаси Автоточки, к слову, Счет4111 вычисляется и не используется
// (Form/Module.bsl:1233).
//
// Все четыре пункта в v4 исправлены.
//
//------------------------------------------------------------------------------
// 4. Дефект логики: ранговое сопоставление вырождалось при остатке > 1
//------------------------------------------------------------------------------
//
// В v3 строка брала ПЕРВОГО кандидата, у которого хватает свободного остатка.
// Это не «ранг к рангу».
//
// Эталонный пример из задачи проходил только потому, что у обеих карточек
// остаток ровно 1. Если бы у карточки с себестоимостью 1060,36 было 2 шт:
//
// строка 1730 -> карточка 1060,36 (Свободно 2 -> 1)
// строка 1610 -> первый кандидат снова подходит -> ТА ЖЕ карточка 1060,36
//
// Обе строки на одной карточке, остатка хватает, документ проводится, ошибки
// нет — себестоимость просто списалась не с той карточки. Молчаливая версия
// исходного бага. Открытый вопрос №2 в документе описывает другой случай
// (близкая себестоимость), этот не был покрыт.
//
// В v4 раздача в два прохода:
// проход 1 — каждой строке своя, ещё не занятая в этой группе карточка
// (строгий ранг к рангу);
// проход 2 — добор: строки, которым отдельной карточки не хватило, садятся
// на самую дорогую карточку со свободным остатком, повтор разрешён.
//
//------------------------------------------------------------------------------
// 5. Артикулы с обрезкой (пункт 7.3) — подтверждается, и сильнее чем ожидалось
//------------------------------------------------------------------------------
//
// Артикул в Catalogs\Номенклатура.xml:401-415 — строка(50), переменная.
// То есть обрезка до 25 не схемная, а импортная.
//
// Но Лев(КатНомер, 25) — НЕ мёртвый DBF-код. Он в живом канале реализаций,
// в обоих расширениях:
// Раширения\{Автоточка,Автомастер}\Documents\РеализацияТоваровУслуг\
// Forms\ФормаДокументаОбщая\Ext\Form\Module.bsl:443,459,470
// везде «Номенклатура.Артикул = &Артикул» с Лев(Артикул,25) в параметре.
//
// Прямое следствие для открытого вопроса №3: v4, как и v3, сравнивает артикул
// полностью, значит карточки с исторически обрезанными артикулами фаза 1
// не найдёт и строки посыплются в фазу 2. Проверять в базе:
//
// ВЫБРАТЬ Ссылка, Артикул ИЗ Справочник.Номенклатура
// ГДЕ ДлинаСтроки(Артикул) = 25 И НЕ ПометкаУдаления И НЕ ЭтоГруппа
//
// Сознательно НЕ добавляю обрезку в подбор: она превращает точное сравнение
// в префиксное и порождает ложные срабатывания. Если обрезанные карточки
// действительно есть — это задача на чистку справочника.
//
//------------------------------------------------------------------------------
// 6. Мелкие дефекты v3, исправленные в v4
//------------------------------------------------------------------------------
//
// * ФолбэкБезОстатка не пополнял накопитель Расход — выданная фолбэком
// карточка не учитывалась дальше по пакету.
// * Приоритет сортировался по-разному в основном подборе
// ("Себестоимость Убыв, Приоритет Возр") и в фолбэке
// ("Приоритет Возр, Себестоимость Убыв"). В основном подборе приоритет
// по наименованию фактически не работал — только тайбрейкер при равной
// себестоимости.
// * В пул кандидатов не отсекались группы справочника (НЕ ЭтоГруппа).
// * Трюк с латинской «o» в КоличествoОстатокДт и СтрЗаменить убран.
// * КандидатыПоКлючу делал один запрос на группу. На 88 строках это десятки
// запросов плюс столько же чтений Счет.ВидыСубконто. В v4 всё собрано
// в пакет: 1 запрос к справочнику + (число дат x число счетов) запросов
// остатков. Для типовой загрузки за один день — 4 запроса.
// * Документы, склад которых не сопоставлен, в подборе не участвуют
// (в СоздатьДокументыОРП они всё равно пропускаются) — иначе они
// съедали бы остаток у реальных документов.
//
//------------------------------------------------------------------------------
// 7. Что осталось непроверенным — нужны данные базы
//------------------------------------------------------------------------------
//
// * фактический состав субконто 41.01 (пункт 7.1);
// * дубли карточек по артикулу 252909 (пункт 7.2);
// * карточки под одним наименованием с разными артикулами (пункт 7.4);
// * 25 строк, легших на карточку с другим наименованием (пункт 7.5);
// * три карточки, использованные дважды (пункт 7.6).
//
// Всё это проверяется прогоном с ВыводитьДиагностику = Истина на эталонном
// документе ООБП-000452 от 05.06.2026.
//
//------------------------------------------------------------------------------
// 8. Что осталось нерешённым по существу
//------------------------------------------------------------------------------
//
// * Счета 41.02 и 41.11. v4 берёт их из СчетаУчетаТоваров() вслед за
// Автомастером. Для розницы это может быть существенно — подтвердить
// у владельца задачи, какие счета учёта реально используются.
// * Смешанная конфигурация счетов (на одном ведётся учёт по складам,
// на другом нет) обрабатывается так: остатки счёта без складской
// аналитики кладутся в «пустой склад» и добавляются к остатку любого
// склада. Накопитель Расход при этом ключуется по складу, поэтому
// теоретически возможна переотдача такого остатка между складами.
// Это ровно та же семантика, что уже реализована в Автомастере;
// менять её без подтверждения не стал.
// * Если у двух карточек одного артикула близкая себестоимость,
// ранжирование по цене вырождается в произвольный выбор. Расхождение
// самоустраняется при распродаже обеих позиций (открытый вопрос №2
// исходного документа остаётся открытым).
//
////////////////////////////////////////////////////////////////////////////////
//
// ЧАСТЬ II. КОД
//
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Точка входа. Вызывается один раз из СоздатьДокументыОРП, после того как
// таблица СоответствиеСкладовЧеки подтверждена пользователем.
//
// Заполняет в каждой структуре строки товара реквизит НоменклатураБП.
// Возвращает массив структур с проблемными строками — формат совпадает
// с тем, что печатает ВывестиПроблемыПодбора.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Функция ПодобратьНоменклатуруПакета(МассивДокументов, ВыводитьДиагностику = Ложь)
Проблемы = Новый Массив;
Строки = СобратьСтрокиПакета(МассивДокументов);
Если Строки.Количество() = 0 Тогда
Возврат Проблемы;
КонецЕсли;
Индексы = ЗагрузитьКарточкиКандидатов(Строки);
Остатки = ЗагрузитьОстаткиКандидатов(Строки, Индексы.Карточки);
// "Номенклатура|Склад" -> уже разобранное количество в этом пакете
Расход = Новый Соответствие;
Если ВыводитьДиагностику Тогда
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон("ДИАГНОСТИКА: строк в пакете %1, карточек-кандидатов %2",
Формат(Строки.Количество(), "ЧГ=0"),
Формат(Индексы.Карточки.Количество(), "ЧГ=0")));
КонецЕсли;
// Фаза 1 — пул строго по артикулу, только карточки со свободным остатком.
ОбработатьФазу(Строки, Индексы, Остатки, Расход, "Артикул", ВыводитьДиагностику);
// Фаза 2 — пул по наименованию и наименованию поставщика (точное равенство).
// Здесь разбирается случай «одно наименование, разные артикулы».
ОбработатьФазу(Строки, Индексы, Остатки, Расход, "Наименование", ВыводитьДиагностику);
// Фаза 3 — фолбэк без остатка, с регистрацией проблемы.
ФолбэкБезОстатка(Строки, Индексы, Остатки, Расход, Проблемы, ВыводитьДиагностику);
Возврат Проблемы;
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// Проход сбора: все строки всех документов пакета в одну плоскую таблицу.
// В ДанныеДокументов нет ссылки на документ — только ДатаДокумента, Склад
// и Товары (массив структур), поэтому документ идентифицируется порядковым
// номером в пакете.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Функция СобратьСтрокиПакета(МассивДокументов)
Строки = Новый ТаблицаЗначений;
Строки.Колонки.Добавить("Данные"); // ссылка на структуру строки товара
Строки.Колонки.Добавить("СкладБП");
Строки.Колонки.Добавить("ДатаОстатков", Новый ОписаниеТипов("Дата"));
Строки.Колонки.Добавить("Артикул", Новый ОписаниеТипов("Строка"));
Строки.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
Строки.Колонки.Добавить("НаимПоставщика", Новый ОписаниеТипов("Строка"));
Строки.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
Строки.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));
Строки.Колонки.Добавить("КлючАртикул", Новый ОписаниеТипов("Строка"));
Строки.Колонки.Добавить("КлючНаименование", Новый ОписаниеТипов("Строка"));
Строки.Колонки.Добавить("Разобрана", Новый ОписаниеТипов("Булево"));
ИндексДокумента = 0;
Для Каждого СтрокаДокумента Из МассивДокументов Цикл
ИндексДокумента = ИндексДокумента + 1;
Если НЕ СтрокаДокумента.Свойство("Товары") Тогда
Продолжить;
КонецЕсли;
СкладБП = СкладБППоСкладуЗПЧ(СтрокаДокумента.Склад);
// Документы без сопоставленного склада в СоздатьДокументыОРП пропускаются.
// Если пустить их в подбор, они съедят остаток у реальных документов.
Если НЕ ЗначениеЗаполнено(СкладБП) Тогда
Продолжить;
КонецЕсли;
ДатаДокумента = ДатаДокументаСтроки(СтрокаДокумента);
Для Каждого СтрокаТовара Из СтрокаДокумента.Товары Цикл
// сбрасываем результат прошлой загрузки, если структура переиспользуется
СтрокаТовара.Вставить("НоменклатураБП", Неопределено);
СтрокаТовара.Вставить("ХарактеристикаБП", Неопределено);
Новая = Строки.Добавить();
Новая.Данные = СтрокаТовара;
Новая.СкладБП = СкладБП;
Новая.ДатаОстатков = КонецДня(ДатаДокумента) + 1;
Новая.Артикул = ЗначениеСтроки(СтрокаТовара, "КаталожныйНомерНоменклатуры", "Артикул");
Новая.Наименование = ЗначениеСтроки(СтрокаТовара, "Номенклатура");
Новая.НаимПоставщика = ЗначениеСтроки(СтрокаТовара, "НоменклатураПоставщика");
Кол = ЧисловоеЗначение(СтрокаТовара, "Количество");
Новая.Количество = ?(Кол > 0, Кол, 1);
Новая.Цена = ЧисловоеЗначение(СтрокаТовара, "Цена");
ОсноваИмени = ?(ЗначениеЗаполнено(Новая.Наименование),
Новая.Наименование, Новая.НаимПоставщика);
Префикс = Формат(ИндексДокумента, "ЧГ=0") + "|" + КлючСклада(СкладБП) + "|";
Новая.КлючАртикул = Префикс + "А:" + ВРег(Новая.Артикул);
Новая.КлючНаименование = Префикс + "Н:" + ВРег(ОсноваИмени);
КонецЦикла;
КонецЦикла;
Возврат Строки;
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// Один запрос к справочнику на весь пакет. Возвращает структуру с индексами:
// ПоАртикулу ВРег(артикул) -> массив ссылок
// ПоНаименованию ВРег(наименование) -> массив ссылок
// Карточки массив всех найденных ссылок (для запроса остатков)
// Свойства ссылка -> Структура(Наименование, Артикул)
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Функция ЗагрузитьКарточкиКандидатов(Строки)
ПоАртикулу = Новый Соответствие;
ПоНаименованию = Новый Соответствие;
Карточки = Новый Массив;
Свойства = Новый Соответствие;
Индексы = Новый Структура("ПоАртикулу, ПоНаименованию, Карточки, Свойства",
ПоАртикулу, ПоНаименованию, Карточки, Свойства);
Артикулы = Новый Массив;
Наименования = Новый Массив;
УникА = Новый Соответствие;
УникН = Новый Соответствие;
Для Каждого С Из Строки Цикл
Если ЗначениеЗаполнено(С.Артикул) И УникА[ВРег(С.Артикул)] = Неопределено Тогда
УникА.Вставить(ВРег(С.Артикул), Истина);
Артикулы.Добавить(С.Артикул);
КонецЕсли;
Если ЗначениеЗаполнено(С.Наименование) И УникН[ВРег(С.Наименование)] = Неопределено Тогда
УникН.Вставить(ВРег(С.Наименование), Истина);
Наименования.Добавить(С.Наименование);
КонецЕсли;
Если ЗначениеЗаполнено(С.НаимПоставщика) И УникН[ВРег(С.НаимПоставщика)] = Неопределено Тогда
УникН.Вставить(ВРег(С.НаимПоставщика), Истина);
Наименования.Добавить(С.НаимПоставщика);
КонецЕсли;
КонецЦикла;
Если Артикулы.Количество() = 0 И Наименования.Количество() = 0 Тогда
Возврат Индексы;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Ном.Ссылка КАК Номенклатура,
| Ном.Наименование КАК Наименование,
| Ном.Артикул КАК Артикул
|ИЗ
| Справочник.Номенклатура КАК Ном
|ГДЕ
| НЕ Ном.ПометкаУдаления
| И НЕ Ном.ЭтоГруппа
| И (Ном.Артикул В (&Артикулы)
| ИЛИ Ном.Наименование В (&Наименования))";
Запрос.УстановитьПараметр("Артикулы", Артикулы);
Запрос.УстановитьПараметр("Наименования", Наименования);
Попытка
Выборка = Запрос.Выполнить().Выбрать();
Исключение
ОбщегоНазначения.СообщитьПользователю(
"Ошибка запроса кандидатов по справочнику номенклатуры: " + ОписаниеОшибки());
Возврат Индексы;
КонецПопытки;
Пока Выборка.Следующий() Цикл
Свойства.Вставить(Выборка.Номенклатура,
Новый Структура("Наименование, Артикул",
СокрЛП(Выборка.Наименование), СокрЛП(Выборка.Артикул)));
Карточки.Добавить(Выборка.Номенклатура);
ДобавитьВИндекс(ПоАртикулу, ВРег(СокрЛП(Выборка.Артикул)), Выборка.Номенклатура);
ДобавитьВИндекс(ПоНаименованию, ВРег(СокрЛП(Выборка.Наименование)), Выборка.Номенклатура);
КонецЦикла;
Возврат Индексы;
КонецФункции
&НаСервере
Процедура ДобавитьВИндекс(Индекс, Ключ, Ссылка)
Если ПустаяСтрока(Ключ) Тогда
Возврат;
КонецЕсли;
Список = Индекс[Ключ];
Если Список = Неопределено Тогда
Список = Новый Массив;
Индекс.Вставить(Ключ, Список);
КонецЕсли;
Список.Добавить(Ссылка);
КонецПроцедуры
////////////////////////////////////////////////////////////////////////////////
// Остатки всех кандидатов сразу: по одному запросу на (дата остатков, счёт).
//
// Виды субконто передаются массивом явно — так делает типовой код
// (ИнформированиеОтрицательныеОстатки). Это даёт две вещи:
// * агрегацию по партиям, если партионный учёт включён (иначе виртуальная
// таблица вернула бы строку на каждую партию);
// * детерминированный индекс Субконто2 = Склады, без поиска индекса вручную.
//
// Отбор по организации обязателен: Хозрасчетный — регистр с измерением
// Организация, база может быть многофирменной.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Функция ЗагрузитьОстаткиКандидатов(Строки, Карточки)
// "Номенклатура|Склад|Дата" -> Структура(Количество, Сумма)
Остатки = Новый Соответствие;
Если Карточки.Количество() = 0 Тогда
Возврат Остатки;
КонецЕсли;
Даты = Новый Массив;
УникДаты = Новый Соответствие;
Для Каждого С Из Строки Цикл
Если УникДаты[С.ДатаОстатков] = Неопределено Тогда
УникДаты.Вставить(С.ДатаОстатков, Истина);
Даты.Добавить(С.ДатаОстатков);
КонецЕсли;
КонецЦикла;
ВидНоменклатура = ПланыВидовХарактеристик.ВидыСубконтоХозрасчетные.Номенклатура;
ВидСклады = ПланыВидовХарактеристик.ВидыСубконтоХозрасчетные.Склады;
ПустойСклад = Справочники.Склады.ПустаяСсылка();
Для Каждого Счет Из СчетаУчетаТоваров() Цикл
ПоСкладам = БухгалтерскийУчет.ВедетсяУчетПоСкладам(Счет);
ВидыСубконто = Новый Массив;
ВидыСубконто.Добавить(ВидНоменклатура);
Если ПоСкладам Тогда
ВидыСубконто.Добавить(ВидСклады);
КонецЕсли;
ТекстЗапроса =
"ВЫБРАТЬ
| ВЫРАЗИТЬ(Ост.Субконто1 КАК Справочник.Номенклатура) КАК Номенклатура,
| [ПОЛЕ_СКЛАДА] КАК Склад,
| СУММА(Ост.КоличествоОстатокДт) КАК Количество,
| СУММА(Ост.СуммаОстатокДт) КАК Сумма
|ИЗ
| РегистрБухгалтерии.Хозрасчетный.Остатки(
| &ДатаОстатков,
| Счет = &Счет,
| &ВидыСубконто,
| Организация = &Организация
| И Субконто1 В (&Карточки)) КАК Ост
|СГРУППИРОВАТЬ ПО
| ВЫРАЗИТЬ(Ост.Субконто1 КАК Справочник.Номенклатура)[ГРУППИРОВКА_СКЛАДА]";
Если ПоСкладам Тогда
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "[ПОЛЕ_СКЛАДА]",
"ВЫРАЗИТЬ(Ост.Субконто2 КАК Справочник.Склады)");
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "[ГРУППИРОВКА_СКЛАДА]",
"," + Символы.ПС + " ВЫРАЗИТЬ(Ост.Субконто2 КАК Справочник.Склады)");
Иначе
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "[ПОЛЕ_СКЛАДА]",
"ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)");
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "[ГРУППИРОВКА_СКЛАДА]", "");
КонецЕсли;
Для Каждого ДатаОст Из Даты Цикл
Запрос = Новый Запрос(ТекстЗапроса);
Запрос.УстановитьПараметр("ДатаОстатков", ДатаОст);
Запрос.УстановитьПараметр("Счет", Счет);
Запрос.УстановитьПараметр("ВидыСубконто", ВидыСубконто);
Запрос.УстановитьПараметр("Организация", Организация);
Запрос.УстановитьПараметр("Карточки", Карточки);
Попытка
Выборка = Запрос.Выполнить().Выбрать();
Исключение
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон("Ошибка запроса остатков по счету %1 на %2: %3",
Строка(Счет), Формат(ДатаОст, "ДЛФ=D"), ОписаниеОшибки()));
Продолжить;
КонецПопытки;
Пока Выборка.Следующий() Цикл
СкладОстатка = ?(ПоСкладам, Выборка.Склад, ПустойСклад);
Ключ = КлючОстатка(Выборка.Номенклатура, СкладОстатка, ДатаОст);
Текущий = Остатки[Ключ];
Если Текущий = Неопределено Тогда
Текущий = Новый Структура("Количество, Сумма", 0, 0);
Остатки.Вставить(Ключ, Текущий);
КонецЕсли;
Текущий.Количество = Текущий.Количество + Выборка.Количество;
Текущий.Сумма = Текущий.Сумма + Выборка.Сумма;
КонецЦикла;
КонецЦикла;
КонецЦикла;
Возврат Остатки;
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// Остаток и себестоимость карточки на складе. Остатки счетов без складской
// аналитики лежат под «пустым складом» и добавляются к остатку любого склада.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Функция ОстатокКарточки(Остатки, Номенклатура, Склад, ДатаОстатков)
Количество = 0;
Сумма = 0;
Данные = Остатки[КлючОстатка(Номенклатура, Склад, ДатаОстатков)];
Если Данные <> Неопределено Тогда
Количество = Количество + Данные.Количество;
Сумма = Сумма + Данные.Сумма;
КонецЕсли;
Если ЗначениеЗаполнено(Склад) Тогда
Данные = Остатки[КлючОстатка(Номенклатура, Справочники.Склады.ПустаяСсылка(), ДатаОстатков)];
Если Данные <> Неопределено Тогда
Количество = Количество + Данные.Количество;
Сумма = Сумма + Данные.Сумма;
КонецЕсли;
КонецЕсли;
Возврат Новый Структура("Остаток, Себестоимость",
Количество, ?(Количество > 0, Сумма / Количество, 0));
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// Фаза подбора. Группы: (документ, склад, артикул) или (документ, склад, наименование).
// У карточки один артикул и одно наименование, поэтому пулы внутри фазы
// не пересекаются — гонки между группами за одну карточку нет, а точное
// совпадение артикула выигрывает глобально.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Процедура ОбработатьФазу(Строки, Индексы, Остатки, Расход, Режим, ВыводитьДиагностику)
ИмяКлюча = ?(Режим = "Артикул", "КлючАртикул", "КлючНаименование");
Ключи = Новый Массив;
Уник = Новый Соответствие;
Для Каждого С Из Строки Цикл
Если С.Разобрана Тогда
Продолжить;
КонецЕсли;
Если Режим = "Артикул" И НЕ ЗначениеЗаполнено(С.Артикул) Тогда
Продолжить;
КонецЕсли;
Если Режим = "Наименование"
И НЕ ЗначениеЗаполнено(С.Наименование)
И НЕ ЗначениеЗаполнено(С.НаимПоставщика) Тогда
Продолжить;
КонецЕсли;
Если Уник[С[ИмяКлюча]] = Неопределено Тогда
Уник.Вставить(С[ИмяКлюча], Истина);
Ключи.Добавить(С[ИмяКлюча]);
КонецЕсли;
КонецЦикла;
Для Каждого Ключ Из Ключи Цикл
Отбор = Новый Структура(ИмяКлюча + ", Разобрана", Ключ, Ложь);
Группа = Строки.НайтиСтроки(Отбор);
Если Группа.Количество() = 0 Тогда
Продолжить;
КонецЕсли;
РаспределитьГруппу(Группа, Индексы, Остатки, Расход, Режим, ВыводитьДиагностику);
КонецЦикла;
КонецПроцедуры
////////////////////////////////////////////////////////////////////////////////
// Раздача группы строк по кандидатам: дороже продано <-> дороже куплено.
//
// Два прохода. Первый — строгий ранг к рангу: каждой строке своя, ещё не занятая
// карточка. Именно этого не хватало в v3: там строка брала первого подходящего
// кандидата, и при остатке > 1 обе строки садились на одну карточку, себестоимость
// списывалась не с той. Второй проход — добор для строк, которым отдельной
// карточки не хватило.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Процедура РаспределитьГруппу(Группа, Индексы, Остатки, Расход, Режим, ВыводитьДиагностику)
Образец = Группа[0];
Кандидаты = ПулКандидатов(Индексы, Остатки, Расход, Режим, Образец, Истина);
Если Кандидаты.Количество() = 0 Тогда
Возврат; // строки останутся неразобранными, попадут в следующую фазу
КонецЕсли;
Кандидаты.Сортировать("Себестоимость Убыв, Приоритет Возр, Наименование Возр");
Порядок = СтрокиПоУбываниюЦены(Группа);
// Проход 1 — ранговый.
Для Каждого С Из Порядок Цикл
Выбранный = Неопределено;
Для Каждого К Из Кандидаты Цикл
Если НЕ К.Занят И К.Свободно >= С.Количество Тогда
Выбранный = К;
Прервать;
КонецЕсли;
КонецЦикла;
Если Выбранный <> Неопределено Тогда
Выбранный.Занят = Истина;
ЗакрепитьСтроку(С, Выбранный, Расход, Режим, "ранг", ВыводитьДиагностику);
КонецЕсли;
КонецЦикла;
// Проход 2 — добор, повторное использование карточки разрешено.
Для Каждого С Из Порядок Цикл
Если С.Разобрана Тогда
Продолжить;
КонецЕсли;
Выбранный = Неопределено;
Для Каждого К Из Кандидаты Цикл
Если К.Свободно >= С.Количество Тогда
Выбранный = К;
Прервать;
КонецЕсли;
КонецЦикла;
Если Выбранный = Неопределено Тогда
Продолжить; // уйдёт в следующую фазу
КонецЕсли;
ЗакрепитьСтроку(С, Выбранный, Расход, Режим, "добор", ВыводитьДиагностику);
КонецЦикла;
КонецПроцедуры
&НаСервере
Процедура ЗакрепитьСтроку(С, Кандидат, Расход, Режим, Проход, ВыводитьДиагностику)
Кандидат.Свободно = Кандидат.Свободно - С.Количество;
Ключ = КлючРасхода(Кандидат.Номенклатура, С.СкладБП);
УжеВзято = Расход[Ключ];
Расход.Вставить(Ключ, ?(УжеВзято = Неопределено, 0, УжеВзято) + С.Количество);
С.Данные.Вставить("НоменклатураБП", Кандидат.Номенклатура);
С.Разобрана = Истина;
Если ВыводитьДиагностику Тогда
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон("[%1/%2] ""%3"" (цена %4, кол-во %5) -> ""%6"" (себест. %7, остаток %8)",
Режим, Проход, С.Наименование,
Формат(С.Цена, "ЧДЦ=2"), Формат(С.Количество, "ЧДЦ=3"),
Строка(Кандидат.Номенклатура),
Формат(Кандидат.Себестоимость, "ЧДЦ=2"),
Формат(Кандидат.Остаток, "ЧДЦ=3")));
КонецЕсли;
КонецПроцедуры
////////////////////////////////////////////////////////////////////////////////
// Пул кандидатов из заранее загруженных индексов. Запросов не делает.
// ТолькоСоСвободным = Ложь используется фолбэком.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Функция ПулКандидатов(Индексы, Остатки, Расход, Режим, Образец, ТолькоСоСвободным)
Результат = Новый ТаблицаЗначений;
Результат.Колонки.Добавить("Номенклатура");
Результат.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
Результат.Колонки.Добавить("Остаток", Новый ОписаниеТипов("Число"));
Результат.Колонки.Добавить("Себестоимость", Новый ОписаниеТипов("Число"));
Результат.Колонки.Добавить("Свободно", Новый ОписаниеТипов("Число"));
Результат.Колонки.Добавить("Приоритет", Новый ОписаниеТипов("Число"));
Результат.Колонки.Добавить("Занят", Новый ОписаниеТипов("Булево"));
Ссылки = Новый Массив;
Добавленные = Новый Соответствие;
Если Режим = "Артикул" Тогда
СобратьСсылки(Ссылки, Добавленные, Индексы.ПоАртикулу[ВРег(Образец.Артикул)]);
Иначе
СобратьСсылки(Ссылки, Добавленные, Индексы.ПоНаименованию[ВРег(Образец.Наименование)]);
СобратьСсылки(Ссылки, Добавленные, Индексы.ПоНаименованию[ВРег(Образец.НаимПоставщика)]);
КонецЕсли;
Для Каждого Ссылка Из Ссылки Цикл
ДанныеОстатка = ОстатокКарточки(Остатки, Ссылка, Образец.СкладБП, Образец.ДатаОстатков);
УжеВзято = Расход[КлючРасхода(Ссылка, Образец.СкладБП)];
Свободно = ДанныеОстатка.Остаток - ?(УжеВзято = Неопределено, 0, УжеВзято);
Если ТолькоСоСвободным И Свободно <= 0 Тогда
Продолжить;
КонецЕсли;
Свойства = Индексы.Свойства[Ссылка];
НаимКарточки = ?(Свойства = Неопределено, Строка(Ссылка), Свойства.Наименование);
Стр = Результат.Добавить();
Стр.Номенклатура = Ссылка;
Стр.Наименование = НаимКарточки;
Стр.Остаток = ДанныеОстатка.Остаток;
Стр.Себестоимость = ДанныеОстатка.Себестоимость;
Стр.Свободно = Свободно;
Стр.Приоритет = ПриоритетКарточки(НаимКарточки, Образец);
КонецЦикла;
Возврат Результат;
КонецФункции
&НаСервере
Процедура СобратьСсылки(Ссылки, Добавленные, Список)
Если Список = Неопределено Тогда
Возврат;
КонецЕсли;
Для Каждого Ссылка Из Список Цикл
Если Добавленные[Ссылка] = Неопределено Тогда
Добавленные.Вставить(Ссылка, Истина);
Ссылки.Добавить(Ссылка);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
&НаСервере
Функция ПриоритетКарточки(НаименованиеКарточки, Образец)
Если ВРег(НаименованиеКарточки) = ВРег(Образец.Наименование) Тогда
Возврат 0;
ИначеЕсли ЗначениеЗаполнено(Образец.НаимПоставщика)
И ВРег(НаименованиеКарточки) = ВРег(Образец.НаимПоставщика) Тогда
Возврат 1;
КонецЕсли;
Возврат 2;
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// Фаза 3. Свободных остатков нет ни у одного кандидата. Подставляем карточку,
// чтобы строка не осталась пустой, и регистрируем проблему.
// Накопитель Расход пополняем и здесь — в v3 этого не было, и следующая строка
// пакета считала этот остаток свободным.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Процедура ФолбэкБезОстатка(Строки, Индексы, Остатки, Расход, Проблемы, ВыводитьДиагностику)
ВыданныеФолбэком = Новый Соответствие;
Для Каждого С Из Строки Цикл
Если С.Разобрана Тогда
Продолжить;
КонецЕсли;
Кандидаты = ПулКандидатов(Индексы, Остатки, Расход, "Артикул", С, Ложь);
Если Кандидаты.Количество() = 0 Тогда
Кандидаты = ПулКандидатов(Индексы, Остатки, Расход, "Наименование", С, Ложь);
КонецЕсли;
Если Кандидаты.Количество() = 0 Тогда
Проблемы.Добавить(НоваяПроблема(С, 0, "номенклатура в базе не найдена"));
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон("Артикул ""%1"" (%2): номенклатура в базе не найдена",
?(ЗначениеЗаполнено(С.Артикул), С.Артикул, "—"), С.Наименование));
Продолжить;
КонецЕсли;
Кандидаты.Сортировать("Приоритет Возр, Свободно Убыв, Себестоимость Убыв");
Выбранный = Неопределено;
Для Каждого К Из Кандидаты Цикл
Если ВыданныеФолбэком[К.Номенклатура] = Неопределено Тогда
Выбранный = К;
Прервать;
КонецЕсли;
КонецЦикла;
Если Выбранный = Неопределено Тогда
Выбранный = Кандидаты[0];
КонецЕсли;
ВыданныеФолбэком.Вставить(Выбранный.Номенклатура, Истина);
ДоступноБыло = Выбранный.Свободно;
ЗакрепитьСтроку(С, Выбранный, Расход, "Фолбэк", "без остатка", ВыводитьДиагностику);
Проблемы.Добавить(НоваяПроблема(С, ДоступноБыло,
СтрШаблон("свободных остатков нет ни у одного из %1 кандидатов, подставлена ""%2""",
Формат(Кандидаты.Количество(), "ЧГ=0"), Строка(Выбранный.Номенклатура))));
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон("ВНИМАНИЕ: ""%1"" (арт. %2, кол-во %3) — свободных остатков нет ни у одного из %4 кандидатов. Подставлена ""%5"", требуется ручная проверка",
С.Наименование,
?(ЗначениеЗаполнено(С.Артикул), С.Артикул, "—"),
Формат(С.Количество, "ЧДЦ=3"),
Формат(Кандидаты.Количество(), "ЧГ=0"),
Строка(Выбранный.Номенклатура)));
Если ВыводитьДиагностику Тогда
Для Каждого К Из Кандидаты Цикл
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон(" кандидат ""%1"": остаток %2, свободно %3, себест. %4, приоритет %5",
Строка(К.Номенклатура),
Формат(К.Остаток, "ЧДЦ=3"),
Формат(К.Свободно, "ЧДЦ=3"),
Формат(К.Себестоимость, "ЧДЦ=2"),
Формат(К.Приоритет, "ЧГ=0")));
КонецЦикла;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
&НаСервере
Функция НоваяПроблема(С, Доступно, Причина)
Проблема = Новый Структура;
Проблема.Вставить("Склад", С.СкладБП);
Проблема.Вставить("Наименование", С.Наименование);
Проблема.Вставить("Артикул", С.Артикул);
Проблема.Вставить("Требуется", С.Количество);
Проблема.Вставить("Доступно", Доступно);
Проблема.Вставить("Причина", Причина);
Возврат Проблема;
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// Вспомогательные
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Функция СтрокиПоУбываниюЦены(Группа)
Сорт = Новый ТаблицаЗначений;
Сорт.Колонки.Добавить("Строка");
Сорт.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));
Для Каждого С Из Группа Цикл
Н = Сорт.Добавить();
Н.Строка = С;
Н.Цена = С.Цена;
КонецЦикла;
Сорт.Сортировать("Цена Убыв");
Результат = Новый Массив;
Для Каждого Н Из Сорт Цикл
Результат.Добавить(Н.Строка);
КонецЦикла;
Возврат Результат;
КонецФункции
&НаСервере
Функция КлючСклада(Склад)
Возврат ?(ЗначениеЗаполнено(Склад), Строка(Склад.УникальныйИдентификатор()), "");
КонецФункции
&НаСервере
Функция КлючРасхода(Номенклатура, Склад)
Возврат Строка(Номенклатура.УникальныйИдентификатор()) + "|" + КлючСклада(Склад);
КонецФункции
&НаСервере
Функция КлючОстатка(Номенклатура, Склад, ДатаОстатков)
Возврат Строка(Номенклатура.УникальныйИдентификатор()) + "|"
+ КлючСклада(Склад) + "|"
+ Формат(ДатаОстатков, "ДФ=yyyyMMddHHmmss");
КонецФункции
&НаСервере
Функция ЗначениеСтроки(Структура, ИмяПоля, ИмяПоляАльт = "")
Значение = "";
Если Структура.Свойство(ИмяПоля) И ЗначениеЗаполнено(Структура[ИмяПоля]) Тогда
Значение = СокрЛП(Строка(Структура[ИмяПоля]));
ИначеЕсли ЗначениеЗаполнено(ИмяПоляАльт)
И Структура.Свойство(ИмяПоляАльт) И ЗначениеЗаполнено(Структура[ИмяПоляАльт]) Тогда
Значение = СокрЛП(Строка(Структура[ИмяПоляАльт]));
КонецЕсли;
Возврат Значение;
КонецФункции
&НаСервере
Функция ЧисловоеЗначение(Структура, ИмяПоля)
Если НЕ Структура.Свойство(ИмяПоля) Тогда
Возврат 0;
КонецЕсли;
Попытка
Возврат Число(Структура[ИмяПоля]);
Исключение
Возврат 0;
КонецПопытки;
КонецФункции
&НаСервере
Функция ДатаДокументаСтроки(СтрокаДокумента)
Если СтрокаДокумента.Свойство("ДатаДокумента") Тогда
Попытка
Возврат Дата(СтрокаДокумента.ДатаДокумента);
Исключение
КонецПопытки;
КонецЕсли;
Возврат ТекущаяДатаСеанса();
КонецФункции
&НаСервере
Функция СкладБППоСкладуЗПЧ(СкладЗПЧ)
СтрокиСклада = СоответствиеСкладовЧеки.НайтиСтроки(Новый Структура("СкладЗПЧ", СкладЗПЧ));
Если СтрокиСклада.Количество() > 0 Тогда
Возврат СтрокиСклада[0].СкладБП;
КонецЕсли;
Возврат Справочники.Склады.ПустаяСсылка();
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// НИЖЕ — уже есть в Автомастере. При переносе в Автомастер эти две процедуры
// НЕ добавлять (будет дубль имени). В Автоточку — добавить.
////////////////////////////////////////////////////////////////////////////////
&НаСервере
Функция СчетаУчетаТоваров()
Счета = Новый Массив;
Для Каждого КодСчета Из СтрРазделить("41.01,41.02,41.11", ",") Цикл
Счет = ПланыСчетов.Хозрасчетный.НайтиПоКоду(КодСчета);
Если ЗначениеЗаполнено(Счет) Тогда
Счета.Добавить(Счет);
КонецЕсли;
КонецЦикла;
Возврат Счета;
КонецФункции
&НаСервере
Процедура ВывестиПроблемыПодбора(Проблемы)
Если Проблемы.Количество() = 0 Тогда
Возврат;
КонецЕсли;
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон("Строк с недостаточными остатками: %1. Документы созданы, но не проведутся:",
Проблемы.Количество()));
Для Каждого Проблема Из Проблемы Цикл
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон(" %1 / %2 (арт. %3): требуется %4, доступно %5 — %6",
Строка(Проблема.Склад),
Проблема.Наименование,
?(ЗначениеЗаполнено(Проблема.Артикул), Проблема.Артикул, "—"),
Строка(Проблема.Требуется),
Строка(Проблема.Доступно),
Проблема.Причина));
КонецЦикла;
КонецПроцедуры
////////////////////////////////////////////////////////////////////////////////
//
// ЧАСТЬ III. КАК ВСТРАИВАТЬ
//
////////////////////////////////////////////////////////////////////////////////
//
// ОБЩЕЕ ДЛЯ ОБЕИХ ОБРАБОТОК
//
// Удалить (заменены кодом выше):
// СопоставитьНоменклатуруЧека
// НайтиНоменклатуруПоНаименованиюРасширенный
// НайтиНоменклатуруПоАртикулуСОстатками
// НайтиНоменклатуруПоНаименованию (мёртвый код, нигде не вызывается)
// ПолучитьОстатокНоменклатуры
//
// Не трогать: легаси-чтение DBF из C:\SERVICE1\Obmen\Выгрузки
// (ЗагрузитьЧекиНаСервере, НайтиНоменклатуру, СопоставитьСклад) — этот канал
// не используется, база ЗПЧ работает по HTTP.
//
//------------------------------------------------------------------------------
// АВТОМАСТЕР
//------------------------------------------------------------------------------
//
// Дополнительно удалить (весь механизм КонтекстПодбора заменён пакетным):
// НовыйКонтекстПодбора
// КлючРезерва
// ДоступныйОстаток
// ЗарезервироватьОстаток
// ЗарегистрироватьПроблемуПодбора
//
// НЕ добавлять из этого файла: СчетаУчетаТоваров, ВывестиПроблемыПодбора —
// они уже есть.
//
// В СоздатьДокументыОРП (около строки 1483):
//
// было:
// Израсходовано = Новый Соответствие;
// ПроблемыПодбора = Новый Массив;
//
// стало:
// ПроблемыПодбора = ПодобратьНоменклатуруПакета(МассивДокументов, Ложь);
//
// Важно: вызов должен идти ПОСЛЕ формирования ЗаполненныеСклады, то есть
// после того как таблица СоответствиеСкладовЧеки подтверждена.
//
// В теле цикла по документам удалить (около строк 1528-1529):
// КонтекстПодбора = НовыйКонтекстПодбора(
// СкладБП, ДатаДокумента, Израсходовано, ПроблемыПодбора);
//
// В цикле по товарам удалить (около строки 1561):
// СопоставитьНоменклатуруЧека(СтрокаТовара, КонтекстПодбора);
// Чтение СтрокаТовара.НоменклатураБП ниже остаётся без изменений.
//
// Вызов ВывестиПроблемыПодбора(ПроблемыПодбора) в конце процедуры остаётся.
//
//------------------------------------------------------------------------------
// АВТОТОЧКА
//------------------------------------------------------------------------------
//
// 1. Сократить СоответствиеДанныхИБЧеки до раскладки складов — как в Автомастере:
//
// &НаСервере
// Процедура СоответствиеДанныхИБЧеки(ДанныеДокументов)
// // Здесь только раскладка складов ЗПЧ по складам БП. Номенклатура
// // подбирается в СоздатьДокументыОРП: до подтверждения таблицы
// // соответствия склад неизвестен, а остатки проверяются по складу документа.
// Для Каждого Строка Из ДанныеДокументов Цикл
// СоответствиеСкладовЧекиОбработка(Строка);
// КонецЦикла;
// КонецПроцедуры
//
// 2. В начало СоздатьДокументыОРП (около строки 1476), после того как построен
// список сопоставленных складов:
//
// ПроблемыПодбора = ПодобратьНоменклатуруПакета(МассивДокументов, Ложь);
//
// 3. В цикле по товарам читать СтрокаТовара.НоменклатураБП вместо вызова
// СопоставитьНоменклатуруЧека.
//
// 4. В конец СоздатьДокументыОРП:
//
// ВывестиПроблемыПодбора(ПроблемыПодбора);
//
// 5. Добавить из этого файла СчетаУчетаТоваров и ВывестиПроблемыПодбора —
// в Автоточке их нет.
//
//------------------------------------------------------------------------------
// ПРОВЕРКА ПЕРЕД БОЕВЫМ ВКЛЮЧЕНИЕМ
//------------------------------------------------------------------------------
//
// Ничего из этого файла не проверено на данных: доступа к информационной базе
// не было, синтаксический контроль не выполнялся. Обязательный минимум:
//
// 1cv8 DESIGNER /F <база> /CheckModules
//
// Затем прогон с ВыводитьДиагностику = Истина (второй параметр
// ПодобратьНоменклатуруПакета) на эталонном документе ООБП-000452 от 05.06.2026.
// Ожидаемый результат по строкам 48/49:
//
// [Артикул/ранг] "Р/к поршня переднего суппорта 14"..." (цена 1730,00)
// -> "Р/к поршня переднего суппорта 14"..." (себест. 1060,36)
// [Артикул/ранг] "Р/к поршня переднего суппорта 14"..." (цена 1610,00)
// -> "Frenkit 252909 ремкомплект дискового тормоза с поршнем" (себест. 946,00)
//
// Пустых НоменклатураБП — 0 из 88, документ проводится.
//
// Отдельно посмотреть в диагностике:
// * сколько строк ушло в фазу «Наименование» — много означает расхождение
// артикулов справочника с тем, что шлёт ЗПЧ (см. вывод 5, обрезка до 25);
// * что получилось по группе «Колодки тормозные передние» — полтора десятка
// карточек с одним наименованием и разными артикулами;
// * строки с проходом «добор» — там ранговое соответствие не было
// однозначным, себестоимость могла лечь не на ту карточку.
//
////////////////////////////////////////////////////////////////////////////////