Product Bundle Configuration in 1C-Bitrix
A store wants to sell a laptop together with a mouse and bag as a single item with a 10% discount. At first glance — simply a new catalog element. In practice, this breaks inventory tracking, 1C integration, and margin calculations if implemented naively as an ordinary product.
Two approaches to bundle implementation
Approach 1: Bundle as a separate product. A catalog element is created with separate inventory tracking. Simple, but problematic: selling a bundle doesn't reduce the stock of component items. A customer can buy a mouse separately and as part of a bundle, leading to overselling.
Approach 2: Bundle as a virtual product with child items. The bundle in the catalog is an aggregate. Its "stock" is the minimum quantity among component items. When sold, component inventory is reduced accordingly. This is the correct approach.
Bitrix supports the second approach through the TYPE_SET = 4 product type in b_catalog_product. The TYPE field in b_catalog_product accepts values: 1 — simple product, 2 — product with trade offers, 4 — set/bundle.
Bundle storage structure
The "bundle → component" relationship is stored in the b_catalog_product_set table:
-
SET_ID— bundle ID (element ID of the infoblox withTYPE = 4) -
ITEM_ID— component product ID -
QUANTITY— quantity in the bundle -
SORT— display order -
IS_REQUIRED— mandatory or optional (0/1)
Creating a bundle via API:
// Create SET-type product
$elementId = $iblock->Add([
'NAME' => 'Laptop + Mouse + Bag',
'IBLOCK_ID' => CATALOG_IBLOCK_ID,
'ACTIVE' => 'Y',
]);
// Set SET type
\Bitrix\Catalog\ProductTable::update($elementId, ['TYPE' => \Bitrix\Catalog\ProductTable::TYPE_SET]);
// Add components
\Bitrix\Catalog\ProductSetTable::add([
'SET_ID' => $elementId,
'ITEM_ID' => $laptopId,
'QUANTITY' => 1,
'SORT' => 10,
'IS_REQUIRED' => 1,
]);
\Bitrix\Catalog\ProductSetTable::add([
'SET_ID' => $elementId,
'ITEM_ID' => $mouseId,
'QUANTITY' => 1,
'SORT' => 20,
'IS_REQUIRED' => 1,
]);
Bundle inventory and price calculation
Available bundle quantity is the minimum among components:
$components = \Bitrix\Catalog\ProductSetTable::getList([
'filter' => ['SET_ID' => $bundleId],
'select' => ['ITEM_ID', 'QUANTITY'],
])->fetchAll();
$available = PHP_INT_MAX;
foreach ($components as $c) {
$stock = \Bitrix\Catalog\ProductTable::getByPrimary(
$c['ITEM_ID'], ['select' => ['QUANTITY']]
)->fetch();
$available = min($available, floor((float)$stock['QUANTITY'] / $c['QUANTITY']));
}
Bundle price can be set separately (bundle discount) or calculated as the sum of component prices. In Bitrix, bundle price is stored in b_catalog_price like a regular product. For automatic N% discount on the bundle — use cart rules through b_sale_discount, not manual pricing.
Inventory deduction on sale
When a bundle is added to the cart via the standard component bitrix:sale.basket.basket, Bitrix automatically expands the bundle: lines are created in b_sale_basket for each component with the SET_PARENT_ID flag referring to the bundle row. When the order is placed, inventory is deducted from each component separately.
Problems arise with direct addition through \Bitrix\Sale\Basket::create() — without passing TYPE = TYPE_SET and child items, expansion won't occur. Always use \Bitrix\Catalog\Product\Bundle::add() instead of direct cart manipulation.
What to configure
- Creating products of type
TYPE_SETand populatingb_catalog_product_set - Calculating available quantity as the minimum among components
- Cart rule for bundle discount via
b_sale_discount - Correct bundle addition to cart via
Bundle::add() - Bundle management interface in the admin panel
- Verification of 1C synchronization: SET type should transfer correctly during exchange







