Перейти к содержимому

Прайс поставщика по расписанию - сопоставление, наценка, защита

Обновляем цены и остатки из прайса поставщика по расписанию: проверка файла, сопоставление, наценка и журнал.

Механика

Прайс поставщика - это чужой файл с чужой структурой и чужой дисциплиной. Задача не в том, чтобы его загрузить, а в том, чтобы привести его к своему каталогу и не испортить витрину, когда файл придёт битым.

Артикул поставщика почти никогда не совпадает с артикулом того же товара у вас. Один и тот же товар у разных поставщиков называется по-разному, поэтому связь хранят отдельной таблицей соответствия, а не надеждой на совпадение строк.

Импорт трогает только цены и остатки товаров, но не содержимое карточек. Названия, описания и картинки в карточке принадлежат магазину, и перезаписывать их данными поставщика - верный способ потерять собственные тексты за одну ночь.

Закупочная цена поставщика - это ещё не цена магазина на витрине. Между ними стоит правило наценки: по категории, по бренду, по диапазону закупочной цены, и это правило меняется чаще, чем сам код импорта.

Отсутствие позиции в файле не означает её удаления. Поставщик присылает то, что есть на складе сегодня; правильная реакция - нулевой остаток, а не деактивация или удаление товара из каталога.

Битый файл поставщика опаснее для каталога магазина, чем полностью отсутствующий файл выгрузки. Прайс, оборвавшийся на середине выгрузки, обнулит остатки половины каталога, поэтому файл проверяют до применения и сравнивают с предыдущим по объёму.

Импорт идёт порциями и в тихие часы, когда покупателей на сайте мало. Ночное окно и порции по несколько сотен позиций держат нагрузку в разумных пределах и позволяют продолжить работу с того места, где задание прервалось.

Журнал импорта обязателен с первого дня работы, а не с первого спора. Сколько строк пришло, сколько сопоставилось, сколько цен изменилось - без этих чисел любой спор с поставщиком превращается в догадки.

Шаги

  1. Договориться о формате, адресе, кодировке и времени обновления файла.
  2. Завести таблицу соответствия артикулов поставщика товарам магазина.
  3. Скачивать файл заданием по расписанию во временный каталог сайта.
  4. Проверять файл до применения: объём, колонки, сравнение с прошлым.
  5. Обновлять цены с наценкой и остатки порциями, не трогая карточки.
  6. Писать журнал и уведомлять о позициях, которые не сопоставились.

Код

Скачиваем файл заданием:

$http = new \Bitrix\Main\Web\HttpClient(['socketTimeout' => 10, 'streamTimeout' => 60]);
$tmp = $_SERVER['DOCUMENT_ROOT'] . '/upload/tmp/price-' . date('Ymd') . '.csv';
if (!$http->download($url, $tmp)) {
// недоступный файл - это не повод обнулять каталог: просто выходим с записью
return __METHOD__ . '();';
}

Недоступный поставщик - обычное дело, а не авария. Задание в таком случае просто завершает работу с записью в журнале и повторяет попытку в следующий запуск, не трогая текущие цены.

Проверяем файл до применения:

$lines = count(file($tmp, FILE_SKIP_EMPTY_LINES));
$previous = (int)Option::get('vendor.price', 'last_lines', 0);
if ($previous > 0 && $lines < $previous * 0.5) {
throw new \RuntimeException("прайс вдвое короче обычного: {$lines} против {$previous}");
}
Option::set('vendor.price', 'last_lines', $lines);
// резкое сокращение файла означает сбой у поставщика, а не распродажу склада

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

Сопоставляем артикулы:

$row = SupplierSkuTable::getRow([
'filter' => ['=SUPPLIER_SKU' => trim($csv['ARTICLE']), '=SUPPLIER_ID' => $supplierId],
'select' => ['PRODUCT_ID'],
]);
if (!$row) {
$unmatched[] = $csv['ARTICLE']; // копим и показываем в отчёте
continue;
}

Несопоставленные позиции - не ошибка, а рабочий материал. Их показывают менеджеру списком: часть окажется новыми товарами, часть - опечатками в файле поставщика.

Считаем цену по правилу наценки:

$markup = $markupBySection[$sectionId] ?? $defaultMarkup; // наценка по разделу
$price = round((float)$csv['PRICE'] * (1 + $markup / 100), 2);
// закупочную цену тоже сохраняют: по ней считают маржу и проверяют наценку
$existing = \Bitrix\Catalog\Model\Price::getList([
'filter' => ['=PRODUCT_ID' => $row['PRODUCT_ID'], '=CATALOG_GROUP_ID' => $baseType],
'select' => ['ID'], 'limit' => 1,
])->fetch();
// без поиска пары «товар и тип цены» вместо правки получаются дубли цен
$existing
? \Bitrix\Catalog\Model\Price::update($existing['ID'], ['PRICE' => $price])
: \Bitrix\Catalog\Model\Price::add(['PRODUCT_ID' => $row['PRODUCT_ID'],
'CATALOG_GROUP_ID' => $baseType, 'PRICE' => $price, 'CURRENCY' => 'RUB']);

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

Обновляем остаток:

\Bitrix\Catalog\Model\Product::update($row['PRODUCT_ID'], [
'QUANTITY' => (int)$csv['QUANTITY'],
'AVAILABLE' => (int)$csv['QUANTITY'] > 0 ? 'Y' : 'N',
]);
// позиции, которых нет в файле, получают нулевой остаток, а не удаление

Нулевой остаток честнее деактивации товара. Карточка остаётся в каталоге и в поиске, покупатель видит отсутствие товара и может подписаться на поступление, а адреса страниц не рассыпаются при каждой ночной загрузке.

Идём порциями и продолжаем с прерванного места:

$offset = (int)Option::get('vendor.price', 'offset', 0);
$handle = fopen($tmp, 'r');
fseek($handle, $offset);
$processed = 0;
while (($csv = fgetcsv($handle, 0, ';')) !== false && $processed < 500) {
// обработка строки
$processed++;
}
Option::set('vendor.price', 'offset', ftell($handle)); // где остановились

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

Пишем журнал и уведомляем о несопоставленном:

\Bitrix\Main\Mail\Event::send(['EVENT_NAME' => 'VENDOR_PRICE_REPORT', 'LID' => SITE_ID,
'C_FIELDS' => ['LINES' => $lines, 'UPDATED' => $updated,
'UNMATCHED' => implode(', ', array_slice($unmatched, 0, 50))]]);
// письмо приходит менеджеру каждое утро: тишина тоже результат

Отчёт читает человек, а не разработчик. Менеджер по закупкам видит, что вчера обновились не все позиции, и идёт к поставщику - без этого письма расхождение всплывает через месяц, на жалобе покупателя.

Ограничения

Формат файла меняется без предупреждения. Поставщик добавит колонку, поменяет разделитель или переименует заголовок, и импорт начнёт молча пропускать строки; проверка обязательных колонок закрывает это на входе.

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

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

Своя цена магазина важнее любой цены, пришедшей из прайса стороннего поставщика товара. Позиции с ручной ценой помечают и не трогают импортом, иначе акции и договорные цены обнуляются при первой же ночной загрузке.

Несколько поставщиков на один товар - отдельная задача. Нужно правило выбора: минимальная цена, приоритет поставщика или наличие на складе, и это правило описывают до написания кода.

Импорт прайса поставщика не заменяет собой полноценного обмена с учётной системой магазина. Если магазин работает с ней, цены и остатки приходят оттуда, а прайс поставщика используют для закупок, а не для витрины.

Типичные проблемы

После ночного импорта половина каталога стала недоступна.

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

Названия товаров заменились на строки поставщика.

Импорт обновляет карточку целиком, а не только цену и остаток. Названия, описания и картинки товара принадлежат магазину, а не поставщику этого товара.

Часть позиций прайса не попала в каталог.

Артикулы поставщика не сопоставлены с товарами магазина в отдельной таблице соответствия артикулов. Несопоставленные строки собирают в отчёт, а не пропускают молча.

Акционные цены сбросились до обычных.

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

В названиях появились нечитаемые символы.

Файл пришёл в другой кодировке, а разбор шёл как есть. Перекодировку файла делают до разбора строк, а не после появления мусора.

Задание не доходит до конца на большом прайсе.

Импорт идёт одним проходом и упирается во время выполнения. Обработку ведут порциями с запоминанием позиции в файле.

Частые вопросы

Как связать артикулы поставщика со своими товарами?

Отдельной таблицей соответствия: артикул поставщика, идентификатор товара, поставщик. Совпадение строк напрямую работает только до первого поставщика с собственной системой артикулов.

Что делать с позициями, которых нет в файле?

Ставить нулевой остаток, а не удалять и не деактивировать. Поставщик присылает наличие на сегодня, и отсутствие позиции означает «нет на складе», а не «товар исчез».

Где хранить правило наценки?

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

Как не потерять свои акционные цены?

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

Нужен ли импорт прайса, если есть обмен с 1С?

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

Смежное

Первоисточник