Загрузка прайса поставила цены и фото на чужие карточки: что делает «найти по артикулу», когда артикул в базе не один

23.10.26

Интеграция - Внешние источники данных

Я написал клиенту загрузку прайса поставщика. Через два дня он прислал снимок: в подборе у блокнота стоит цена настольной лампы, а в карточке блокнота ещё и её фотография. Загрузка искала товар по коду и брала первую найденную карточку, а в базе один код был у двух товаров. Рассказываю, как это нашли, почему я пропустил три прямых сигнала и как откатывали цены. Отдельно проверил прогоном, что возвращают НайтиПоКоду, НайтиПоНаименованию и НайтиПоРеквизиту, когда значение в базе не одно, и что делает с ними пометка удаления. В конце два запроса, которыми можно проверить свою базу.

Что увидел заказчик

Заказ был обычный: раз в месяц поставщик присылает прайс на 35 тысяч строк, из него нужно обновить закупочные цены и цены сайта, завести новые товары и подгрузить фотографии. База на «Управлении торговлей» 11.5, в справочнике 41 322 карточки номенклатуры.

Загрузку я написал, проверил на копии, заказчик запустил её у себя. Через два дня он прислал снимок подбора. По коду 236671 там две строки: блокнот и настольная лампа. У блокнота стоит цена лампы. Чуть позже он прислал карточку блокнота, и в ней была фотография лампы.

Заметил это заказчик, а не я.

 

Причина

Загрузка искала товар по двум ключам по очереди: код для поиска, затем артикул. Чтобы не делать запрос на каждую строку прайса, я заранее строил индекс: читал справочник и складывал в соответствие пары «ключ, ссылка на карточку». Выглядело это так:

Если Индексы.ПоКоду[Код] = Неопределено Тогда
    Индексы.ПоКоду.Вставить(Код, Выборка.Ссылка);
КонецЕсли;

Если ключ встретился второй раз, вторая карточка в индекс не попадала. Оставалась первая, которую вернул запрос. В запросе не было упорядочивания, так что какая из двух окажется первой, заранее сказать нельзя.

У лампы и блокнота совпал код для поиска. Индекс вернул блокнот, и на него легла вся строка прайса лампы: закупочная цена, цена сайта и фотография.

Я посмотрел, насколько это частый случай в той базе. Для статьи перемерил на копии:

Поле Значений, которые повторяются Карточек с ними
Артикул 329 880
Код для поиска 79 158

 

Из 41 тысячи карточек это немного. Но загрузка идёт по всему прайсу, и каждое такое значение, встретившееся в файле, означало бросок монеты.

 

Три сигнала, которые я прочитал неправильно

Самое неприятное в этой истории то, что все данные были у меня на руках до сдачи.

Первый сигнал. Перед загрузкой я сам снял отчёт по дублям кодов в базе. В нём было написано: кодов, встречающихся больше одного раза, 79. Я прочитал это как факт о данных заказчика. Вопрос «а что сделает моя загрузка, когда встретит такой код» я себе не задал.

Второй сигнал. В коде загрузки была защита от повторяющихся артикулов. Только ловила она повторы в прайсе: две строки файла с одним артикулом. Обратную сторону, два товара в базе с одним артикулом, никто не проверял.

Третий сигнал. Журнал загрузки был полон предупреждений «штрихкод уже закреплён за другим товаром». Это и был симптом: по коду найден один товар, а штрихкод из той же строки прайса принадлежит другому. Я записал эти сообщения как информационные.

И тесты. Они проверяли счётчики: сколько создано, сколько обновлено, сколько ошибок. Ни один тест не проверял, на ту ли карточку встала цена.

 

Как чинили загрузку

Правило я поменял на такое: если по ключу найдено несколько карточек, спор решается штрихкодом из той же строки прайса. Штрихкод у товара свой, и в первом поиске он не участвовал. Если и штрихкод не помог, ключ не используется вовсе, а строка уходит в список спорных, которые решает человек.

Такая строка требует минуты внимания человека, зато цена на чужой товар не попадает.

 

Как откатывали цены

Разбор после инцидента нашёл 77 пострадавших карточек и 154 записи цен. Все записи сделал один документ установки цен, который создала загрузка.

Первая мысль была выпустить встречный документ с прежними ценами. От неё пришлось отказаться: у 49 записей прежней цены не было вовсе. Товар раньше стоял без цены, а встречный документ поставил бы ему ноль. Ноль и отсутствие цены в учёте разные вещи.

Поэтому чинили иначе: удалили неверные строки из самого документа загрузки и перепровели его. Регистр цен периодический и подчинён документу, так что после удаления строки последней записью по товару снова становится та, что была до загрузки. Если до загрузки цены не было, её и не остаётся. В итоге 105 записей вернулись к прежней цене, 49 остались без цены.

С фотографиями поступили так. Там, где до загрузки у карточки была своя фотография, её вернули из копии базы. Там, где фотографии не было, чужую просто удалили.

 

Что возвращают методы поиска, когда значение не одно

Загрузка у клиента строила свой индекс, но тот же вопрос стоит перед любым кодом, который ищет карточку методами менеджера справочника. По описанию НайтиПоКоду, НайтиПоНаименованию и НайтиПоРеквизиту ищут элемент и возвращают ссылку на него. Что будет, если подходящих элементов два, я проверил прогоном.

Стенд: демонстрационная база «Комплексной автоматизации» 2.5.27.75, файловый вариант, платформа 8.5.1.1302. Тестовые карточки номенклатуры создавались в транзакции, которая в конце отменялась: до прогона в справочнике было 480 элементов, после отмены тоже 480. Чтобы не пересечься с данными базы, к наименованиям, артикулам и кодам добавлялась метка времени, в примерах ниже я её опускаю.

Я завёл две пары карточек. Внутри пары у карточек одинаковые наименование, артикул и код. Ссылки задавал сам: в паре А первой создавалась карточка со старшей ссылкой, в паре Б первой создавалась карточка с младшей. Запись шла так:

Об = Справочники.Номенклатура.СоздатьЭлемент();
Об.УстановитьСсылкуНового(Справочники.Номенклатура.ПолучитьСсылку(Новый УникальныйИдентификатор(Уид)));
Об.Наименование = Наименование;
Об.Артикул = Артикул;
Об.Код = Код;
Об.ОбменДанными.Загрузка = Истина;
Об.Записать();

Каждый метод я вызвал трижды по каждой паре:

Вызов Пара А: раньше создана старшая ссылка Пара Б: раньше создана младшая ссылка
НайтиПоРеквизиту("Артикул", Артикул) вторая по созданию, младшая ссылка первая по созданию, младшая ссылка
НайтиПоКоду(Код) вторая по созданию, младшая ссылка первая по созданию, младшая ссылка
НайтиПоНаименованию(Наименование, Истина) вторая по созданию, младшая ссылка первая по созданию, младшая ссылка

 

Все три метода вернули одну карточку и ничем не показали, что была вторая. Какую именно, решила ссылка: в обеих парах вернулась карточка с младшей ссылкой, хотя в одной паре она создана раньше, а в другой позже. Три вызова подряд дали один и тот же результат.

Для загрузки отсюда два вывода. Карточку выбирает не смысл и не порядок заведения товара, а значение ссылки, которого в рабочей базе никто не видит. И выбор устойчивый: каждая следующая загрузка положит строку на ту же самую чужую карточку. У клиента блокнот получал бы цену лампы каждый месяц, и сверка «после прошлой загрузки ничего не сломалось» этого бы не показала.

Запрос с условием Артикул = &Артикул по тем же ключам вернул обе карточки в каждой паре.

Ещё одно наблюдение из того же прогона. У номенклатуры в этой базе включён контроль уникальности кода, но карточки с одинаковым кодом записались: запись шла в режиме обмена данными, с ОбменДанными.Загрузка = Истина. Значит, одинаковый код в справочнике возможен и при включённом контроле.

 

НайтиПоНаименованию без второго параметра ищет по началу строки

В том же прогоне была карточка «Ручка синяя», а карточки с наименованием ровно «Ручка» не было. Поиск дал такие результаты:

Справочники.Номенклатура.НайтиПоНаименованию("Ручка");          // нашлась «Ручка синяя»
Справочники.Номенклатура.НайтиПоНаименованию("Ручка", Истина);  // не нашлось ничего

Без второго параметра метод нашёл карточку, наименование которой только начинается с искомой строки. С параметром Истина не нашёл ничего, как и должен. Загрузка, которая ищет по наименованию и не передаёт Истина, при отсутствии точного совпадения обновит карточку, чьё наименование только начинается с искомого: по строке «Лампа» это может оказаться «Лампа настольная». Как метод выбирает, если в базе есть и точное совпадение, и более длинные наименования, я не проверял.

Пометка удаления не убирает карточку из поиска

Частый совет при дублях: пометить лишнюю карточку на удаление. Я проверил, увидит ли её после этого НайтиПоРеквизиту. Ещё две пары с одинаковым артикулом, в каждой одна карточка помечена на удаление:

Пара Какая карточка помечена Что вернул НайтиПоРеквизиту
Е с младшей ссылкой помеченную
Ж со старшей ссылкой непомеченную

 

Пометка на выбор не повлияла: метод снова вернул карточку с младшей ссылкой, и в паре Е это оказалась помеченная на удаление. Пометка лишней карточки помогает только тому коду, который сам отбрасывает помеченные, например запросом с условием НЕ ПометкаУдаления. Код с НайтиПоРеквизиту она не спасает.

 

Пробел в начале артикула

Пока я разбирался с дублями, нашёлся способ промахнуться в другую сторону. Загрузка берёт значение из файла, обрезает пробелы по краям и ищет в базе. Если в базе артикул записан с пробелом в начале, карточка не находится, и загрузка создаёт новую, с тем же артикулом без пробела. В справочнике становится две карточки, и следующая загрузка уже выбирает между ними.

В прогоне карточка с артикулом « СТП» по ключу «СТП» не нашлась ни через НайтиПоРеквизиту, ни запросом. Карточка с артикулом «ЛИН» и двумя пробелами в конце по ключу без пробелов нашлась обоими способами. Пробел в конце безвреден, пробел в начале превращает артикул в другой.

На копии рабочей базы клиента карточек с пробелом в начале артикула четыре.

 

Как искать, чтобы видеть все совпадения

Индекс, который видит все совпадения, отличается от исходного немногим. Под ключом лежит не одна ссылка, а массив всех карточек. Помеченные на удаление в индекс не попадают. Ключ из базы обрезается так же, как значение из файла:

Запрос = Новый Запрос(
"ВЫБРАТЬ
|	Т.Ссылка КАК Ссылка,
|	Т.Артикул КАК Артикул
|ИЗ
|	Справочник.Номенклатура КАК Т
|ГДЕ
|	НЕ Т.ПометкаУдаления
|	И НЕ Т.ЭтоГруппа
|	И Т.Артикул <> """"");
Выборка = Запрос.Выполнить().Выбрать();
ПоАртикулу = Новый Соответствие;
Пока Выборка.Следующий() Цикл
	Ключ = СокрЛП(Выборка.Артикул);
	Найденные = ПоАртикулу[Ключ];
	Если Найденные = Неопределено Тогда
		Найденные = Новый Массив;
		ПоАртикулу.Вставить(Ключ, Найденные);
	КонецЕсли;
	Найденные.Добавить(Выборка.Ссылка);
КонецЦикла;

При загрузке строки решение принимается по числу карточек под ключом:

Найденные = ПоАртикулу[СокрЛП(АртикулИзФайла)];
Если Найденные = Неопределено Тогда
	// карточки нет: создать новую
ИначеЕсли Найденные.Количество() = 1 Тогда
	// обновить Найденные[0]
Иначе
	// ключ неоднозначен: строку не записывать, в журнал перечислить все карточки
КонецЕсли;

Этот индекс я прогнал на тех же тестовых карточках. По артикулу пары А он ответил «ключ неоднозначен» и перечислил обе карточки. В паре Е взял непомеченную. Карточку с пробелом в начале артикула нашёл, потому что ключ из базы тоже обрезается. По артикулу, которого в базе нет, ответил «создать новую».

 

Как я теперь проверяю загрузки

Раньше тест загрузки заканчивался сверкой счётчиков: сколько создано, сколько обновлено. Теперь в нём есть карточки-ловушки: две карточки с одним артикулом, карточка, помеченная на удаление, рядом с обычной, карточка с пробелом в начале артикула. После загрузки тест читает каждую из них по ссылке и сверяет реквизиты. Условие звучит так: обе карточки с повторяющимся артикулом остались прежними, обновилась ровно та карточка, которая должна была, и никакая другая.

Всё идёт в транзакции, которая в конце отменяется, и тест сверяет число карточек до начала и после отмены. Так устроен и прогон из этой статьи: 480 карточек до, 480 после.

 

Что делать с повторами в базе

Остановленная строка это сообщение о том, что в справочнике два товара с одним ключом. Варианты такие.

Выбрать для поиска другое поле, значения которого не повторяются. На копии базы клиента артикул повторяется 329 раз, код для поиска 79 раз, а код справочника один раз, и то у двух карточек.

Пометить лишнюю карточку на удаление, если поиск сам отбрасывает помеченные, как индекс выше. С НайтиПоРеквизиту это не работает: метод может вернуть именно помеченную.

Объединить дубли. В «Управлении торговлей» для этого есть штатная обработка «Поиск и удаление дублей». После объединения ключ снова принадлежит одной карточке.

Чего делать не стоит: возвращать «первый найденный», чтобы строки перестали останавливаться. С этого история с лампой и началась.

 

Как проверить свою базу

Повторы артикула среди непомеченных карточек:

ВЫБРАТЬ
	Т.Артикул КАК Артикул,
	КОЛИЧЕСТВО(*) КАК Карточек
ИЗ
	Справочник.Номенклатура КАК Т
ГДЕ
	НЕ Т.ПометкаУдаления
	И НЕ Т.ЭтоГруппа
	И Т.Артикул <> ""
СГРУППИРОВАТЬ ПО
	Т.Артикул
ИМЕЮЩИЕ
	КОЛИЧЕСТВО(*) > 1
УПОРЯДОЧИТЬ ПО
	Карточек УБЫВ

На копии базы клиента запрос вернул 329 строк, в сумме 880 карточек. Вместо артикула подставьте то поле, по которому ваши загрузки и обмены ищут товар.

Артикулы, записанные с пробелом в начале:

ВЫБРАТЬ
	Т.Ссылка КАК Ссылка,
	Т.Артикул КАК Артикул
ИЗ
	Справочник.Номенклатура КАК Т
ГДЕ
	Т.Артикул ПОДОБНО " %"
	И НЕ Т.ПометкаУдаления
	И НЕ Т.ЭтоГруппа

На копии базы клиента он вернул 4 строки.

Если первый запрос что-то вернул, посмотрите, как ваша загрузка или обмен ищет товар. НайтиПоКоду, НайтиПоНаименованию и НайтиПоРеквизиту при повторе вернут одну карточку и ничего не скажут о второй. Так же ведёт себя соответствие, заполненное в цикле без проверки на повтор.

 

Чего я не проверял

  • Методы поиска проверены на файловой базе. На клиент-серверной базе с MS SQL или PostgreSQL я этот прогон не делал: какая карточка из пары вернётся там, не знаю.
  • То, что вернулась карточка с младшей ссылкой, видно на двух парах и одной версии платформы. Это наблюдение, а не правило из документации, и строить на нём выбор карточки нельзя.
  • Цифры про 77 карточек, 154 записи цен и 49 записей без прежней цены взяты из разбора в день инцидента. Перемерить их сегодня нельзя: копию базы тогда же и починили.

Вступайте в нашу телеграмм-группу Инфостарт

загрузка из Excel загрузка прайса номенклатура артикул дубли НайтиПоРеквизиту НайтиПоКоду НайтиПоНаименованию сопоставление откат загрузки Управление торговлей 11.5

Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».

  • 0% комиссии — оплата напрямую исполнителю;
  • Исполнители любого масштаба — от отдельных специалистов до команд под проект;
  • Прямой обмен контактами между заказчиком и исполнителем;
  • Безопасная сделка — при необходимости;
  • Рейтинги, кейсы и прозрачная система откликов.

См. также

Внешние источники данных Разработчик Аналитик Пользователь 1С:Предприятие 8 1C:Бухгалтерия Узбекистан Беларусь Кыргызстан Молдова Россия Казахстан Платные (руб)

Готовое решение для автоматической выгрузки данных из 1С 8.3 в базу данных ClickHouse, PostgreSQL или Microsoft SQL для работы с данными 1С в BI-системах. «Экстрактор данных 1С в BI» работает со всеми типовыми и нестандартными конфигурациями 1С 8.3 и упрощает работу бизнес-аналитиков. Благодаря этому решению, специалистам не требуется быть программистами, чтобы легко получать данные из 1С в вашей BI-системе.

35000 руб.

15.11.2022    33841    53    49    

51

Внешние источники данных Кадровый учет Файловый обмен (TXT, XML, DBF), FTP Перенос данных 1C Разработчик 1С:Предприятие 8 1С:Зарплата и кадры государственного учреждения 3 Государственные, бюджетные структуры Россия Бухгалтерский учет Бюджетный учет Платные (руб)

Обработка позволяет перенести кадровую информацию и данные по заработной плате, фактическим удержаниям, НДФЛ, вычетам, страховым взносам из базы Парус 10 учреждений (далее Парус) в конфигурацию 1С:Зарплата и кадры государственного учреждения ред. 3 (далее 1С) и начать с ней работать с любого месяца года.

85400 руб.

05.10.2022    14578    17    8    

17

Зарплата Внешние источники данных Бюджетный учет Перенос данных 1C Системный администратор Разработчик 1С:Предприятие 8 1С:Зарплата и кадры государственного учреждения 3 Государственные, бюджетные структуры Россия Бухгалтерский учет Бюджетный учет Платные (руб)

Обработка позволяет перенести кадровую информацию и данные по заработной плате, фактическим удержаниям, НДФЛ, вычетам, страховым взносам из базы Парус 8 учреждений (далее Парус) в конфигурацию 1С:Зарплата и кадры государственного учреждения ред. 3 (далее 1С) и начать с ней работать с любого месяца года.

122000 руб.

19.08.2020    30652    28    3    

29

Внешние источники данных Зарплата Бюджетный учет Разработчик Бухгалтер 1С:Предприятие 8 1С:Зарплата и кадры государственного учреждения 3 Государственные, бюджетные структуры Россия Бухгалтерский учет Бюджетный учет Платные (руб)

Обработка позволяет перенести кадровую информацию и данные по заработной плате, фактическим удержаниям, НДФЛ, вычетам, страховым взносам из базы Парус 7.хх учреждений (далее Парус) в конфигурацию 1С:Зарплата и кадры государственного учреждения ред. 3 (далее 1С) и начать с ней работать с любого месяца года.

85400 руб.

24.04.2017    57678    99    165    

91

Производство готовой продукции (работ, услуг) Внешние источники данных 1С:Предприятие 8 1С:Управление нашей фирмой 1.6 1С:Управление нашей фирмой 3.0 Лесное и деревообрабатывающее хозяйство Россия Управленческий учет Платные (руб)

Обработка предназначена для загрузки файлов, выгруженных из системы Базис-мебельщик, в справочник 1С "Спецификации" для последующих процессов учета и диспетчирования полуфабрикатов и изделий.

10370 руб.

24.06.2021    26858    66    55    

48

Файловый обмен (TXT, XML, DBF), FTP Внешние источники данных Бухгалтер Пользователь 1С:Предприятие 8 1С:Бухгалтерия 3.0 Здравоохранение, медицина, стоматология Фармацевтика, аптеки Россия Платные (руб)

Внешняя обработка «Загрузка документов из eФарма» создана для программы 1С:Бухгалтерия предприятия, редакция 3.0 (версии Базовая, Проф и Корп). Обработка позволяет загружать документы в базу 1С из файлов выгрузки, имеющих формат .DBF, которые создаются в программе eФарма или eФарма2, а также из любых других программ, формирующих файлы-выгрузки с таким же расширением.

9760 руб.

15.03.2024    2883    5    0    

7
Для отправки сообщения требуется регистрация/авторизация