Розробка калькулятора комплектації на 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+ варіантах ручне тестування нереально: автоматизовані тести для логіки сумісності потрібні з першого дня розробки.







