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

Массовая смена цен - наценка на группу товаров и откат

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

Решение

Отбор товаров

Отбираем товары раздела и базовый тип цены:

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% всем товарам сразу?

Скриптом по условию отбора: читаем базовые цены пачкой, умножаем на коэффициент и пишем обратно через модель цены. Штатная наценка задаётся у товара по одному, поэтому каталог целиком ей не поднять.

Как вернуть цены, если наценку посчитали неверно?

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

Можно ли обновить цены запросом прямо в базе?

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

Нужно ли менять цену родителя у товара с предложениями?

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

Где смотреть, какой получилась цена после наценки?

В расчёте итоговой цены, а не в таблице цен: он учитывает права групп, скидки и диапазоны количества. Витрина после правки показывает старое значение до сброса кэша страниц каталога.

Смежное

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