Массовая смена цен - наценка на группу товаров и откат
Поднимаем цены на группу товаров разом: наценка процентом, округление по правилам типа цены и возможность вернуть прежние значения. Задача приходит с пересмотром прайса, когда в админке цену меняют по одному товару.
Решение
Отбор товаров
Отбираем товары раздела и базовый тип цены:
use Bitrix\Main\Loader;use Bitrix\Catalog\GroupTable;
Loader::requireModule('catalog');$basePriceTypeId = (int)GroupTable::getBasePriceType()['ID']; // базовый тип один$targetTypeId = $basePriceTypeId; // наценку можно класть и в отдельный тип цены
$ids = [];$res = CIBlockElement::GetList(['ID' => 'ASC'], ['IBLOCK_ID' => 5, 'SECTION_ID' => 42, 'INCLUDE_SUBSECTIONS' => 'Y'], false, ['nTopCount' => 500], ['ID']);while ($row = $res->Fetch()) { $ids[] = (int)$row['ID']; }Отбор описываем условием, а не собранным руками списком идентификаторов. Условие переживает повторный запуск и новые товары раздела, а порция в несколько сотен позиций держит память в разумных границах.
Копия цен перед правкой
Снимаем текущие цены одной выборкой:
$backup = [];$rs = \Bitrix\Catalog\PriceTable::getList([ 'select' => ['ID', 'PRODUCT_ID', 'CATALOG_GROUP_ID', 'PRICE', 'CURRENCY'], 'filter' => ['=CATALOG_GROUP_ID' => [$basePriceTypeId, $targetTypeId], '=PRODUCT_ID' => $ids],]);while ($price = $rs->fetch()) { $backup[$price['PRODUCT_ID'] . ':' . $price['CATALOG_GROUP_ID']] = $price; // ключ - пара}file_put_contents($dump, json_encode($backup)); // это и есть план откатаОдна выборка по массиву идентификаторов заменяет чтение цены в цикле по товарам. Копию снимаем до первой записи: после правки прежних значений в базе уже нет, и восстанавливать их будет не из чего.
Наценка и округление
Считаем новую цену и округляем её:
$base = $backup[$productId . ':' . $basePriceTypeId] ?? null;if (!$base) { $skipped[] = $productId; continue; } // базовой цены нет$raw = (float)$base['PRICE'] * (1 + $markup / 100); // наценка процентом$new = \Bitrix\Catalog\Product\Price::roundPrice( $targetTypeId, $raw, $base['CURRENCY']); // правила типа цены// либо техническое округление вниз, когда правил у типа цены не заведено:// $new = \Bitrix\Catalog\Product\Price::roundValue(// $raw, 1, \Bitrix\Catalog\RoundingTable::ROUND_DOWN);Правила округления принадлежат типу цены, а не скрипту, и текущие показывает
getRoundRules. Округление своей формулой расходится с тем, как ту же цену
покажет и посчитает платформа.
Запись без дублей
Пишем цену по паре товар и тип цены:
$current = $backup[$productId . ':' . $targetTypeId] ?? null; // цена целевого типа$fields = ['PRICE' => $new, 'CURRENCY' => $base['CURRENCY']];$result = $current ? \Bitrix\Catalog\Model\Price::update((int)$current['ID'], $fields) : \Bitrix\Catalog\Model\Price::add($fields + ['PRODUCT_ID' => $productId, 'CATALOG_GROUP_ID' => $targetTypeId]);if (!$result->isSuccess()) { $log[] = $productId . ': ' . implode('; ', $result->getErrorMessages()); }Найденная запись обновляется, отсутствующая создаётся. Без поиска по паре на товар ложится вторая строка того же типа цены: карточка показывает одну, а в корзину попадает другая. Ветка создания срабатывает, когда наценку кладут в отдельный тип цены и у товара его ещё нет.
Торговые предложения
У товара с вариантами правим цены предложений:
$offers = \CCatalogSKU::getOffersList($ids, 0, [], ['ID']); // сгруппированы по родителям\Bitrix\Catalog\Product\Sku::enableDeferredCalculation();foreach ($offers as $parentId => $list) { foreach ($list as $offer) { /* та же правка цены по $offer['ID'] */ }}\Bitrix\Catalog\Product\Sku::calculate();\Bitrix\Catalog\Product\Sku::disableDeferredCalculation();Цена, записанная родительской карточке, в расчёт при выборе варианта не попадёт. Отложенный пересчёт снимает повторный пересчёт родителя на каждом предложении, а тяжёлый пересчёт большого каталога уносят в задание по расписанию.
Проверка и откат
Читаем итог тем же вызовом, что и витрина:
$optimal = \CCatalogProduct::GetOptimalPrice($productId, 1, [2], 'N', [], SITE_ID);echo $optimal['RESULT_PRICE']['DISCOUNT_PRICE']; // цена, которую увидит гостьBXClearCache(true, '/catalog/'); // кэш страниц каталогаВызов учитывает права групп, скидки каталога и диапазоны количества, поэтому проверять глазами карточку недостаточно. До сброса кэша витрина показывает прежние цены, хотя в админке лежат уже новые.
Возвращаем прежние цены из копии:
foreach (json_decode(file_get_contents($dump), true) as $price) { \Bitrix\Catalog\Model\Price::update((int)$price['ID'], ['PRICE' => $price['PRICE'], 'CURRENCY' => $price['CURRENCY']]);}Откат идёт тем же методом и по тем же идентификаторам строк цены. Строки, созданные во время правки, в копии не значатся, и убирают их отдельно по журналу неудачных и новых записей.
Прямым запросом к таблице цен смену не делают даже ради скорости. Запись мимо модели каталога обходит проверки, события и пересчёт доступности, а при работающем обмене ближайший сеанс вернёт значения учётной системы. Постоянную наценку в таком случае держат отдельным типом цены или правилом корзины.
Типичные проблемы
Во вкладке «Цены» нет раздела наценок.
Механизм наценок доступен не во всех редакциях магазина. В младшей редакции ту же задачу решают скриптом или правилом корзины.
Наценка ставится каждому товару по одному.
Штатная наценка задаётся у товара, а не у каталога целиком. Массовое применение делают своим скриптом по условию отбора.
После правки у товара две цены одного типа.
Запись шла без поиска пары товар и тип цены. Карточка показывает одну строку, а в корзину попадает другая.
Наутро цены вернулись к прежним значениям.
Обмен с учётной системой переписал их своими данными. Наценку поверх обмена держат отдельным типом цены или правилом корзины.
У товара с вариантами цена не изменилась.
Цена записана родительской карточке, а покупает человек предложение. Цены таких товаров правят по идентификатору предложения.
Частые вопросы
Как поднять цену на 10% всем товарам сразу?
Скриптом по условию отбора: читаем базовые цены пачкой, умножаем на коэффициент и пишем обратно через модель цены. Штатная наценка задаётся у товара по одному, поэтому каталог целиком ей не поднять.
Как вернуть цены, если наценку посчитали неверно?
Только из копии, снятой перед правкой: обратной операции у массовой смены цен нет. Копию выгружают в файл одной выборкой, а заливают обратно тем же методом обновления.
Можно ли обновить цены запросом прямо в базе?
Так делать не стоит: прямая запись обходит проверки, события и пересчёт доступности. Цены пишут через модель каталога, а при работающем обмене правку всё равно перезапишет ближайший сеанс.
Нужно ли менять цену родителя у товара с предложениями?
Нет, покупатель берёт конкретное предложение, и цена родителя в расчёт не идёт. Правку ведут по списку предложений, полученному одним вызовом на всю пачку товаров.
Где смотреть, какой получилась цена после наценки?
В расчёте итоговой цены, а не в таблице цен: он учитывает права групп, скидки и диапазоны количества. Витрина после правки показывает старое значение до сброса кэша страниц каталога.
Смежное
- Цены, скидки и купоны - оглавление подтемы
- Каталог и продажи - устройство магазина целиком
- Типы цен: розница и опт, права групп, цена по количеству - в какой тип цены пишем
- Прайс поставщика по расписанию: сопоставление, наценка, защита - та же наценка, но регулярным приёмом файла
- Массовая правка товаров: обновление свойств скриптом - тот же обход, но правятся свойства, а не цены
- Путь цены: от типа цены до суммы в корзине - что произойдёт с новой ценой дальше
- Скидки из кода: создание, обязательные поля, перенос - когда вместо смены цен нужна скидка
- Разовый скрипт на боевом: запуск, защита, журнал, откат - как запускать такую правку
- Скидки и ручные цены при обмене: что перезаписывает обмен - переживёт ли наценка ночной сеанс
- Цена на витрине не та: разбор причин - если после правки видна не та цена
- Создание торговых предложений из кода: привязка, цены, остатки - цены у вариантов товара