CPrice::Update() — метод, который умеет молчать. Вызываешь, получаешь true, идёшь в админку — цена прежняя. Ни исключения, ни записи в лог, ни намёка на то, что пошло не так.
Причина почти всегда в одном из трёх мест, и все три связаны с тем, что цена в Битриксе — не поле товара, а отдельная сущность со своим идентификатором. Разберём по порядку.
Проблема
При попытке обновить цену товара методом CPrice::Update() ничего не происходит — цена не изменяется, ошибок нет.
Причины
- Элемент не является товаром — не зарегистрирован в таблице
b_catalog_product - Неверный ID цены — передаётся ID товара вместо ID записи цены
- Цена не существует — нужно создать через
CPrice::Add(), а не обновлять
Вторая причина — самая частая, и она же самая коварная из-за того, как ведёт себя API. Метод получает целое число, находит по нему запись в b_catalog_price (или не находит), и отсутствие записи для него не ошибка. С точки зрения CPrice::Update() вы попросили обновить несуществующую строку — он честно ничего не обновил и вернул управление. Ошибку в такой ситуации вернул бы более строгий API; старое ядро Битрикса в этом месте предпочитает молчать.
Правильный алгоритм
<?php
use Bitrix\Main\Loader;
Loader::includeModule('catalog');
Loader::includeModule('iblock');
$productId = 123; // ID элемента инфоблока
$newPrice = 5990.00; // Новая цена
$priceTypeId = 1; // ID типа цены (базовая)
$currency = 'RUB';
// Шаг 1: Убеждаемся, что элемент является товаром
if (!\CCatalogProduct::GetByID($productId)) {
\CCatalogProduct::Add([
'ID' => $productId,
'QUANTITY' => 0,
'QUANTITY_TRACE' => 'N',
]);
}
// Шаг 2: Проверяем существование цены.
// QUANTITY_FROM/QUANTITY_TO === false — берём именно базовую строку,
// а не одну из цен для количественных диапазонов
$existingPrice = \CPrice::GetList(
[],
[
'PRODUCT_ID' => $productId,
'CATALOG_GROUP_ID' => $priceTypeId,
'QUANTITY_FROM' => false,
'QUANTITY_TO' => false,
]
)->Fetch();
// Шаг 3: Обновляем или добавляем
$priceFields = [
'PRODUCT_ID' => $productId,
'CATALOG_GROUP_ID' => $priceTypeId,
'PRICE' => $newPrice,
'CURRENCY' => $currency,
];
if ($existingPrice) {
// Обновляем существующую цену
// ВАЖНО: передаём ID записи цены, не ID товара!
$result = \CPrice::Update($existingPrice['ID'], $priceFields);
echo "Цена обновлена, ID записи: {$existingPrice['ID']}";
} else {
// Создаём новую цену
$priceId = \CPrice::Add($priceFields);
echo "Цена добавлена, ID записи: {$priceId}";
}Если в каталоге настроены цены по количеству, для одного товара и одного типа цены в b_catalog_price лежит несколько строк: базовая и по одной на каждый диапазон. GetList без явного фильтра по QUANTITY_FROM/QUANTITY_TO вернёт любую из них — какую именно, зависит от порядка в базе. Обновив «первую попавшуюся», вы поменяете цену для диапазона «от 10 штук», а витрина продолжит показывать старую. Симптом ровно тот же, что в начале статьи: «код отработал, цена не изменилась».
Класс для работы с ценами
<?php
namespace Local\Catalog;
use Bitrix\Main\Loader;
class PriceManager
{
private int $defaultPriceTypeId = 1;
private string $defaultCurrency = 'RUB';
public function __construct()
{
Loader::includeModule('catalog');
}
/**
* Установка цены (создание или обновление)
*/
public function setPrice(
int $productId,
float $price,
?int $priceTypeId = null,
?string $currency = null
): bool {
$priceTypeId = $priceTypeId ?? $this->defaultPriceTypeId;
$currency = $currency ?? $this->defaultCurrency;
// Регистрируем как товар
$this->ensureProduct($productId);
$priceFields = [
'PRODUCT_ID' => $productId,
'CATALOG_GROUP_ID' => $priceTypeId,
'PRICE' => $price,
'CURRENCY' => $currency,
];
$existingPrice = $this->getExistingPrice($productId, $priceTypeId);
if ($existingPrice) {
return (bool) \CPrice::Update($existingPrice['ID'], $priceFields);
}
return (bool) \CPrice::Add($priceFields);
}
/**
* Получение цены товара
*/
public function getPrice(int $productId, ?int $priceTypeId = null): ?float
{
$priceTypeId = $priceTypeId ?? $this->defaultPriceTypeId;
$price = $this->getExistingPrice($productId, $priceTypeId);
return $price ? (float) $price['PRICE'] : null;
}
/**
* Удаление цены
*/
public function deletePrice(int $productId, ?int $priceTypeId = null): bool
{
$priceTypeId = $priceTypeId ?? $this->defaultPriceTypeId;
$price = $this->getExistingPrice($productId, $priceTypeId);
if ($price) {
return \CPrice::Delete($price['ID']);
}
return true;
}
/**
* Массовое обновление цен
*/
public function bulkSetPrices(array $prices, ?int $priceTypeId = null): array
{
$results = [
'success' => 0,
'failed' => 0,
'errors' => [],
];
foreach ($prices as $productId => $price) {
try {
if ($this->setPrice($productId, $price, $priceTypeId)) {
$results['success']++;
} else {
$results['failed']++;
$results['errors'][] = "Не удалось обновить цену для товара {$productId}";
}
} catch (\Throwable $e) {
$results['failed']++;
$results['errors'][] = "Товар {$productId}: {$e->getMessage()}";
}
}
return $results;
}
/**
* Синхронизация цен из свойства инфоблока
*/
public function syncFromProperty(
int $iblockId,
string $propertyCode,
?int $priceTypeId = null
): array {
Loader::includeModule('iblock');
$results = ['updated' => 0, 'skipped' => 0];
$elements = \CIBlockElement::GetList(
['ID' => 'ASC'],
[
'IBLOCK_ID' => $iblockId,
'ACTIVE' => 'Y',
],
false,
false,
['ID', 'PROPERTY_' . $propertyCode]
);
while ($element = $elements->Fetch()) {
$priceValue = $element['PROPERTY_' . $propertyCode . '_VALUE'];
// Внимание: пустое значение здесь ТОЛЬКО пропускается.
// Если цену убрали в источнике, в каталоге останется старая —
// при необходимости добавьте здесь удаление цены
if (!is_numeric($priceValue) || $priceValue <= 0) {
$results['skipped']++;
continue;
}
$this->setPrice((int) $element['ID'], (float) $priceValue, $priceTypeId);
$results['updated']++;
}
return $results;
}
/**
* Регистрация элемента как товара
*/
private function ensureProduct(int $productId): void
{
if (!\CCatalogProduct::GetByID($productId)) {
\CCatalogProduct::Add([
'ID' => $productId,
'QUANTITY' => 0,
'QUANTITY_TRACE' => 'N',
'CAN_BUY_ZERO' => 'Y',
'WEIGHT' => 0,
]);
}
}
/**
* Получение существующей записи цены
*/
private function getExistingPrice(int $productId, int $priceTypeId): ?array
{
$result = \CPrice::GetList(
[],
[
'PRODUCT_ID' => $productId,
'CATALOG_GROUP_ID' => $priceTypeId,
]
);
return $result->Fetch() ?: null;
}
}Использование
<?php
use Local\Catalog\PriceManager;
$priceManager = new PriceManager();
// Установка цены одному товару
$priceManager->setPrice(123, 5990.00);
// Получение цены
$price = $priceManager->getPrice(123);
echo "Цена: {$price} руб.";
// Массовое обновление
$prices = [
100 => 1990.00,
101 => 2490.00,
102 => 3990.00,
];
$result = $priceManager->bulkSetPrices($prices);
echo "Обновлено: {$result['success']}, ошибок: {$result['failed']}";
// Синхронизация из свойства
$result = $priceManager->syncFromProperty(2, 'PRICE');
echo "Синхронизировано: {$result['updated']}";Типы цен
Типы цен настраиваются в админке: Магазин → Настройки → Типы цен.
| ID | Код | Название |
|---|---|---|
| 1 | BASE | Базовая цена |
| 2 | WHOLESALE | Оптовая |
| 3 | VIP | VIP |
// Получение ID типа цены по коду
$priceType = \CCatalogGroup::GetList(
[],
['=NAME' => 'WHOLESALE']
)->Fetch();
$wholesalePriceTypeId = $priceType['ID'];Консольный скрипт синхронизации
<?php
// /local/scripts/sync_prices.php
$_SERVER['DOCUMENT_ROOT'] = realpath(__DIR__ . '/../../');
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
use Local\Catalog\PriceManager;
$priceManager = new PriceManager();
echo "=== Синхронизация цен из свойства PRICE ===\n\n";
$result = $priceManager->syncFromProperty(
2, // ID инфоблока
'PRICE', // Код свойства
1 // ID типа цены
);
echo "Обновлено: {$result['updated']}\n";
echo "Пропущено: {$result['skipped']}\n";Про массовые операции
Класс выше удобен для точечных изменений, но на объёме он развалится, и стоит понимать почему.
Три запроса на товар. ensureProduct() — раз, поиск существующей цены — два, обновление — три. Для десяти товаров незаметно, для пятидесяти тысяч (обычный размер каталога, из-за которого и пишут такие скрипты) — это уже сто пятьдесят тысяч запросов и десятки минут работы.
Выборка целиком в память. syncFromProperty идёт по всем элементам инфоблока без ограничения. На большом каталоге скрипт упрётся либо в memory_limit, либо в max_execution_time, причём в середине процесса — то есть часть цен обновится, часть нет.
Что с этим делать:
- Обрабатывайте порциями через
nTopCountиnPageSizeс сохранением позиции, чтобы прерванный импорт можно было продолжить, а не начинать заново. - Обновляйте только изменившееся. Сравнение с текущей ценой перед записью убирает 90% запросов на типичном прогоне, где меняется несколько процентов позиций.
- Отключайте лишнее на время импорта. Обработчики событий, индексация поиска и сброс кэша на каждой позиции превращают импорт в многочасовой.
Про D7. Bitrix\Catalog\Model\Price (и PriceTable для чтения) — актуальный API, и на массовых операциях он предпочтительнее. Главное преимущество не в скорости самой по себе, а в предсказуемости: методы возвращают объект результата с ошибками вместо false без объяснений. Старое CPrice API продолжает работать и остаётся уместным в разовых скриптах.
Итоги
Отладочная последовательность, когда цена «не обновляется»:
- Есть ли запись в
b_catalog_productдля этого элемента. Нет — значит это не товар, и цены к нему не привязываются. - Существует ли строка цены нужного типа:
SELECT * FROM b_catalog_price WHERE PRODUCT_ID = ... AND CATALOG_GROUP_ID = .... Обновлять нечего — нуженAdd. - Тот ли ID вы передаёте. В
CPrice::Update()первым параметром идёт ID записи вb_catalog_price, а не ID товара. Совпадение этих чисел на тестовых данных — отдельный источник ложной уверенности. - Не попали ли вы в строку количественного диапазона вместо базовой.
- Смотрит ли витрина на тот тип цены, который вы меняете. Код отрабатывает верно, а на сайте выводится другой тип цены — типичный финал этого расследования.
И главное правило по теме: цена в Битриксе — самостоятельная сущность, а не поле товара. Почти все проблемы с ценами через API растут из попытки работать с ней как с полем.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.