Разработка калькулятора комплектации на 1С-Битрикс
Калькулятор комплектации решает задачу, которую стандартный каталог 1С-Битрикс не закрывает: пользователь конфигурирует продукт из набора взаимозависимых опций, видит итоговую цену в реальном времени и добавляет в корзину уже собранную комплектацию. Актуально для автомобильных дилеров, производителей мебели, IT-интеграторов, поставщиков промышленного оборудования.
Разница между вариантами товара и комплектацией
В стандартном каталоге Битрикс торговые предложения (SKU) покрывают фиксированные комбинации атрибутов. Комплектация — динамическая сборка, где:
- опции взаимозависимы: выбор пакета «Люкс» включает опции A, B, C и делает недоступной опцию D
- итоговая цена — сумма компонентов, а не фиксированная цена SKU
- результат — набор товаров для корзины, а не один товар
Модель данных
HL-блок ConfiguratorComponents:
| Поле | Тип | Описание |
|---|---|---|
| UF_PRODUCT_ID | Int | ID базового товара в каталоге |
| UF_GROUP_CODE | String | Группа опций (engine, color, wheels) |
| UF_OPTION_CODE | String | Код опции |
| UF_OPTION_NAME | String | Название для отображения |
| UF_BASE_PRICE | Float | Цена опции |
| UF_PRICE_TYPE | Enum | fixed / percent / delta |
| UF_INCOMPATIBLE | String | Коды несовместимых опций (через запятую) |
| UF_REQUIRED_WITH | String | Обязательные при выборе этой опции |
HL-блок ConfiguratorPresets — готовые комплектации (Базовая, Стандарт, Люкс):
| Поле | Тип | Описание |
|---|---|---|
| UF_PRODUCT_ID | Int | Базовый товар |
| UF_PRESET_CODE | String | Код пресета |
| UF_PRESET_NAME | String | Название |
| UF_OPTIONS_JSON | Text | JSON с выбранными опциями |
| UF_DISCOUNT_PERCENT | Float | Скидка на пресет |
Ядро конфигуратора: управление зависимостями
Центральная проблема конфигуратора — синхронизация зависимостей. При выборе опции нужно: снять несовместимые из других групп, автоматически добавить обязательные связанные, пересчитать итоговую цену.
class ProductConfigurator {
constructor(basePrice, components) {
this.basePrice = basePrice;
this.components = components;
this.selected = {};
this.graph = this.buildIncompatibilityGraph();
}
buildIncompatibilityGraph() {
const graph = {};
this.components.forEach(c => {
if (c.uf_incompatible) {
graph[c.uf_option_code] = c.uf_incompatible.split(',').map(s => s.trim());
}
});
return graph;
}
selectOption(groupCode, optionCode) {
const blocked = this.graph[optionCode] || [];
Object.keys(this.selected).forEach(g => {
if (blocked.includes(this.selected[g])) delete this.selected[g];
});
this.selected[groupCode] = optionCode;
const c = this.components.find(
x => x.uf_group_code === groupCode && x.uf_option_code === optionCode
);
if (c && c.uf_required_with) {
c.uf_required_with.split(',').forEach(code => {
const req = this.components.find(x => x.uf_option_code === code.trim());
if (req) this.selected[req.uf_group_code] = req.uf_option_code;
});
}
return this.calculate();
}
calculate() {
let total = this.basePrice;
const breakdown = [];
Object.entries(this.selected).forEach(([group, code]) => {
const c = this.components.find(
x => x.uf_group_code === group && x.uf_option_code === code
);
if (!c) return;
let price = 0;
if (c.uf_price_type === 'fixed') price = c.uf_base_price;
if (c.uf_price_type === 'percent') price = this.basePrice * c.uf_base_price / 100;
if (c.uf_price_type === 'delta') price = c.uf_base_price;
total += price;
breakdown.push({ group, name: c.uf_option_name, price });
});
return { basePrice: this.basePrice, totalPrice: Math.round(total), breakdown };
}
}
Пресеты как точка входа
Большинство пользователей не конфигурируют с нуля. Готовые пресеты служат отправной точкой с возможностью дотюнинга. Пресет со скидкой стимулирует выбрать готовый пакет вместо ручной сборки по частям:
function applyPreset(configurator, preset) {
const options = JSON.parse(preset.uf_options_json);
configurator.selected = {};
Object.entries(options).forEach(([group, code]) => {
configurator.selected[group] = code;
});
const result = configurator.calculate();
if (preset.uf_discount_percent > 0) {
result.totalPrice = Math.round(result.totalPrice * (1 - preset.uf_discount_percent / 100));
result.presetDiscount = preset.uf_discount_percent;
}
return result;
}
Добавление комплектации в корзину Битрикс
public function addToCart(int $baseProductId, array $selectedOptions): \Bitrix\Main\Result
{
$basket = \Bitrix\Sale\Basket::loadItemsForFUser(
\Bitrix\Sale\Fuser::getId(),
\Bitrix\Main\Context::getCurrent()->getSite()
);
$item = $basket->createItem('catalog', $baseProductId);
$item->setFields([
'QUANTITY' => 1,
'PROPS' => [[
'NAME' => 'Комплектация',
'CODE' => 'CONFIGURATION',
'VALUE' => json_encode($selectedOptions),
]]
]);
foreach ($selectedOptions as $option) {
if (!empty($option['product_id'])) {
$optItem = $basket->createItem('catalog', $option['product_id']);
$optItem->setFields([
'QUANTITY' => 1,
'PRICE' => $option['price'],
'CUSTOM_PRICE' => 'Y',
]);
}
}
return $basket->save();
}
Сохранение конфигурации пользователя
Для авторизованных пользователей — HL-блок SavedConfigurations:
$hash = md5($productId . json_encode($selectedOptions));
$existing = $SavedConfig::getList([
'filter' => ['=UF_USER_ID' => $USER->GetID(), '=UF_HASH' => $hash]
])->fetch();
if (!$existing) {
$SavedConfig::add([
'UF_USER_ID' => $USER->GetID(),
'UF_PRODUCT_ID' => $productId,
'UF_OPTIONS' => json_encode($selectedOptions),
'UF_PRICE' => $totalPrice,
'UF_HASH' => $hash,
]);
}
Для гостей — sessionStorage или куки с ограниченным сроком жизни.
Сроки разработки
| Масштаб | Описание | Срок |
|---|---|---|
| Базовый | До 5 групп опций, без зависимостей, пресеты | 5–8 дней |
| Стандартный | 5–15 групп, несовместимости, сохранение конфигурации | 2–3 недели |
| Сложный | 15+ групп, позиции в корзине, история конфигураций | 4–8 недель |
| Производственный | Интеграция с 1С для актуальных цен и остатков | 2–4 месяца |
Главный технический вызов — граф зависимостей при большом числе опций. При 50+ вариантах ручное тестирование нереально: автоматизированные тесты для логики совместимости нужны с первого дня разработки.







