Developing a Supplier Personal Cabinet for Dropshipping in 1C-Bitrix
In a dropshipping model, suppliers need to view orders containing their products, update inventory levels and prices, and confirm shipments. The standard Bitrix personal cabinet (my) is designed for customers. A supplier cabinet is a separate section with role-based access, custom components, and limited data visibility.
Access Model and Roles
In Bitrix, access control is implemented through user groups. Create a "Suppliers" group via API or administrative interface:
$groupId = CGroup::Add([
'ACTIVE' => 'Y',
'NAME' => 'Suppliers',
'STRING_ID' => 'SUPPLIERS',
]);
Linking a specific supplier to their products is done via HL-block SupplierProduct or an infoblock property. The simplest approach is using a SUPPLIER_ID property of type "Link to User" (E) on each product. This way, the supplier sees only products where SUPPLIER_ID = current user.
The supplier cabinet section is protected by a group check:
if (!$USER->IsAuthorized() || !$USER->IsInGroup($supplierGroupId)) {
LocalRedirect('/auth/?backurl=' . urlencode($_SERVER['REQUEST_URI']));
}
"My Products" Section
Displays a list of supplier's products with current inventory levels and prices. Query using CIBlockElement::GetList with a SUPPLIER_ID filter:
$userId = $USER->GetID();
$res = CIBlockElement::GetList(
['NAME' => 'ASC'],
[
'IBLOCK_ID' => CATALOG_IBLOCK_ID,
'ACTIVE' => 'Y',
'PROPERTY_SUPPLIER_ID' => $userId,
],
false,
false,
['ID', 'NAME', 'DETAIL_PAGE_URL', 'PREVIEW_PICTURE', 'PROPERTY_SUPPLIER_ID']
);
For each product, display the current inventory from b_catalog_store_product and price from b_catalog_price:
$quantityRes = CCatalogStoreProduct::GetList(
[],
['PRODUCT_ID' => $productId]
);
$totalQuantity = 0;
while ($qRow = $quantityRes->GetNext()) {
$totalQuantity += (int)$qRow['AMOUNT'];
}
Price and Inventory Update Form
Suppliers should be able to update prices and inventory without administrative access. The form sends an AJAX request:
// /local/ajax/supplier-update.php
$productId = (int)$_POST['product_id'];
$newPrice = (float)$_POST['price'];
$newQty = (int)$_POST['quantity'];
// Verify that the product belongs to the current supplier
$ownerCheck = CIBlockElement::GetProperty(
CATALOG_IBLOCK_ID,
$productId,
'sort',
'asc',
['CODE' => 'SUPPLIER_ID', 'VALUE' => $USER->GetID()]
);
if (!$ownerCheck->Fetch()) {
echo json_encode(['error' => 'Access denied']);
die();
}
// Update price
CCatalogProduct::Update($productId, []); // update without changing product fields
$priceRow = CCatalogPrice::GetList(
[], ['PRODUCT_ID' => $productId, 'CATALOG_GROUP_ID' => BASE_PRICE_GROUP_ID]
)->Fetch();
if ($priceRow) {
CCatalogPrice::Update($priceRow['ID'], ['PRICE' => $newPrice, 'CURRENCY' => 'RUB']);
} else {
CCatalogPrice::Add([
'PRODUCT_ID' => $productId,
'CATALOG_GROUP_ID' => BASE_PRICE_GROUP_ID,
'PRICE' => $newPrice,
'CURRENCY' => 'RUB',
]);
}
// Update inventory at supplier's warehouse
$storeProductRow = CCatalogStoreProduct::GetList(
[], ['PRODUCT_ID' => $productId, 'STORE_ID' => getSupplierStoreId($userId)]
)->Fetch();
if ($storeProductRow) {
CCatalogStoreProduct::Update($storeProductRow['ID'], ['AMOUNT' => $newQty]);
} else {
CCatalogStoreProduct::Add([
'PRODUCT_ID' => $productId,
'STORE_ID' => getSupplierStoreId($userId),
'AMOUNT' => $newQty,
]);
}
echo json_encode(['success' => true]);
Each supplier has their own warehouse (b_catalog_store) — this allows tracking inventory for each supplier independently.
"My Orders" Section
The supplier sees only orders containing their products. A direct query to b_sale_order is not suitable — data linking through the basket is needed:
$supplierId = $USER->GetID();
$connection = \Bitrix\Main\Application::getConnection();
$orders = $connection->query("
SELECT DISTINCT
o.ID,
o.DATE_INSERT,
o.PRICE,
o.STATUS_ID,
o.USER_ID,
u.NAME,
u.LAST_NAME,
u.EMAIL
FROM b_sale_order o
JOIN b_sale_basket b ON b.ORDER_ID = o.ID
JOIN b_iblock_element_property ep
ON ep.IBLOCK_ELEMENT_ID = b.PRODUCT_ID
AND ep.IBLOCK_PROPERTY_ID = " . SUPPLIER_PROP_ID . "
AND ep.VALUE_NUM = {$supplierId}
LEFT JOIN b_user u ON u.ID = o.USER_ID
WHERE o.DATE_INSERT >= DATE_SUB(NOW(), INTERVAL 90 DAY)
ORDER BY o.DATE_INSERT DESC
LIMIT 100
");
On the order detail page, the supplier sees only basket items related to their products — not the entire order:
$basketItems = $connection->query("
SELECT b.ID, b.NAME, b.QUANTITY, b.PRICE, b.PRODUCT_ID
FROM b_sale_basket b
JOIN b_iblock_element_property ep
ON ep.IBLOCK_ELEMENT_ID = b.PRODUCT_ID
AND ep.IBLOCK_PROPERTY_ID = " . SUPPLIER_PROP_ID . "
AND ep.VALUE_NUM = {$supplierId}
WHERE b.ORDER_ID = {$orderId}
");
Shipment Confirmation
The supplier confirms shipment of their portion of the order. The status is recorded in the HL-block SupplierShipment:
-
UF_ORDER_ID— order ID -
UF_SUPPLIER_ID— supplier ID -
UF_STATUS—pending/confirmed/shipped/delivered -
UF_TRACKING— tracking number -
UF_DATE_SHIPPED— shipment date
When all suppliers on an order set status to shipped, an agent automatically changes the Bitrix order status to "Shipped".
Bulk Price and Inventory Upload via CSV
For suppliers with large product ranges — provide a CSV file upload form:
Article;Price;Inventory
SKU-001;1500;25
SKU-002;2800;10
The PHP handler reads the CSV using SplFileObject, finds products by CML2_ARTICLE (article property), verifies supplier ownership, and updates data. File size and row count limits (up to 5,000) prevent PHP timeout.
Implementation Timeline
| Scope | Components | Timeline |
|---|---|---|
| Basic cabinet (order list + inventory update) | Components + AJAX form + role | 1–1.5 weeks |
| Full-featured cabinet (warehouse, prices, shipment, CSV) | + file upload + status agent | 2–3 weeks |
| Multilingual cabinet with sales analytics | + sales reports + i18n | 3–4 weeks |







