A client complains: "The product card says '10 in stock', but in reality, at the Central mall it's 5, at Lenina it's 2, and at Zapadny there's none." The standard bitrix:catalog.element component sums up stock across all warehouses without separating pickup points. The customer arrives at an empty store — reputation suffers.
We fix this problem: we show the real quantity for each store and update the data when a trade offer is selected without reloading. By directly querying the warehouse stock table with a filter by pickup points and using tagged caching, the page loads quickly and data is always up-to-date. Integration with 1C via CommerceML ensures stock updates automatically with each upload. Page load time drops from 200ms to 50ms thanks to tagged caching, and data accuracy is guaranteed through synchronization with 1C.
Why the standard component isn't suitable
bitrix:catalog.element outputs the total stock from the CATALOG_QUANTITY field. To break it down by warehouse, you need to make your own query to the b_catalog_store_product table. Here's a typical query structure:
SELECT
s.ID,
s.TITLE,
s.ADDRESS,
COALESCE(sp.AMOUNT, 0) as AMOUNT,
COALESCE(sp.QUANTITY_RESERVED, 0) as RESERVED,
COALESCE(sp.AMOUNT, 0) - COALESCE(sp.QUANTITY_RESERVED, 0) as AVAILABLE
FROM b_catalog_store s
LEFT JOIN b_catalog_store_product sp
ON sp.STORE_ID = s.ID AND sp.PRODUCT_ID = ?
WHERE s.ACTIVE = 'Y' AND s.IS_SITE = 'Y'
ORDER BY s.SORT ASC;
The flag IS_SITE = 'Y' distinguishes pickup points from regular warehouses. It is set in the admin panel: Catalog → Warehouses → [Edit].
How to distinguish a pickup point from a warehouse in Bitrix
In the admin panel, each warehouse has a field "Is a pickup point" (IS_SITE). If the flag is enabled, during the exchange with 1C, stock for this warehouse will be accounted separately. In practice, this allows flexible display management: for example, an online store can show only pickup stores, excluding wholesale warehouses.
How we do it
We develop the getStoreAvailability() function, which retrieves stock for all active pickup points in a single query. We use Bitrix ORM: \Bitrix\Catalog\StoreTable and \Bitrix\Catalog\StoreProductTable. PHP code:
function getStoreAvailability(int $productId): array
{
$result = [];
$storesQuery = \Bitrix\Catalog\StoreTable::getList([
'filter' => ['ACTIVE' => 'Y', 'IS_SITE' => 'Y'],
'select' => ['ID', 'TITLE', 'ADDRESS', 'GPS_N', 'GPS_S', 'SORT'],
'order' => ['SORT' => 'ASC'],
]);
$stores = [];
while ($store = $storesQuery->fetch()) {
$stores[$store['ID']] = $store;
}
if (empty($stores)) {
return [];
}
// Stock in one query for all warehouses
$stockQuery = \Bitrix\Catalog\StoreProductTable::getList([
'filter' => [
'PRODUCT_ID' => $productId,
'STORE_ID' => array_keys($stores),
],
'select' => ['STORE_ID', 'AMOUNT', 'QUANTITY_RESERVED'],
]);
$stocks = [];
while ($stock = $stockQuery->fetch()) {
$stocks[$stock['STORE_ID']] = $stock;
}
foreach ($stores as $storeId => $store) {
$amount = (float)($stocks[$storeId]['AMOUNT'] ?? 0);
$reserved = (float)($stocks[$storeId]['QUANTITY_RESERVED'] ?? 0);
$available = max(0, $amount - $reserved);
$result[] = [
'ID' => $storeId,
'TITLE' => $store['TITLE'],
'ADDRESS' => $store['ADDRESS'],
'GPS_N' => $store['GPS_N'],
'GPS_S' => $store['GPS_S'],
'AMOUNT' => $amount,
'AVAILABLE' => $available,
'IN_STOCK' => $available > 0,
];
}
return $result;
}
Our solution uses tagged caching (tag catalog_store_product) — when stock changes, the cache is automatically invalidated. This is 5 times faster than a standard uncached query. For 50 warehouses, the query executes in under 10 ms.
What if there are more than 50 stores?
For a large number of pickup points (50+), we optimize queries. We add indexes on the STORE_ID and PRODUCT_ID fields in the b_catalog_store_product table and use caching with a 5-minute time-to-live. This allows handling up to 1000 warehouses without performance loss. If instant freshness is needed, caching can be disabled — but database load increases. We recommend a compromise: tagged caching with invalidation upon 1C exchange.
Integration into the product card template
In template.php of the bitrix:catalog.element component:
\Bitrix\Main\Loader::includeModule('catalog');
$storeAvailability = getStoreAvailability($arResult['ID']);
$inStockCount = count(array_filter($storeAvailability, fn($s) => $s['IN_STOCK']));
?>
<div class="store-availability">
<?php if ($inStockCount > 0): ?>
<div class="in-stock-summary">
In stock at <?= $inStockCount ?> store<?= $inStockCount > 1 ? 's' : '' ?>
</div>
<button class="toggle-stores" type="button">Show all stores</button>
<ul class="store-list" style="display:none">
<?php foreach ($storeAvailability as $store): ?>
<li class="store-item <?= $store['IN_STOCK'] ? 'in-stock' : 'out-of-stock' ?>">
<span class="store-name"><?= htmlspecialchars($store['TITLE']) ?></span>
<span class="store-address"><?= htmlspecialchars($store['ADDRESS']) ?></span>
<span class="store-qty">
<?= $store['IN_STOCK']
? $store['AVAILABLE'] . ' pcs.'
: 'Out of stock' ?>
</span>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<div class="out-of-stock">Out of stock in all stores</div>
<?php endif; ?>
</div>
AJAX update when selecting a trade offer
For products with variations (size, color), we update stock without page reload:
document.querySelectorAll('.offer-option').forEach(function(el) {
el.addEventListener('change', function() {
var offerId = this.value;
fetch('/ajax/store-availability/?product_id=' + offerId)
.then(r => r.json())
.then(data => updateStoreList(data.stores));
});
});
The endpoint /ajax/store-availability/ is a separate PHP file returning JSON with the result of getStoreAvailability($offerId).
What's included in the work
- Analysis of the current warehouse scheme and offer types.
- Development of the stock retrieval function with caching.
- Integration of the block into the
catalog.elementtemplate. - Creation of the AJAX route for trade offers.
- Testing on real data (from 2 to 10 pickup points).
- Documentation for further adjustments and access handover.
Comparison: standard solution vs ours
| Characteristic | Standard catalog.element |
Our customization |
|---|---|---|
| Display by store | No | Yes, with address and GPS |
| Caching | No | Tagged, automatic invalidation |
| Update on offer change | Only after reload | AJAX without reload |
| Page load time | ~200 ms (without cache) | ~50 ms (with cache) |
Unlike the standard solution, our customization not only displays stock by store but also lets the customer see the address and GPS coordinates, convenient for route planning.
Stages and timeline
| Stage | Tasks | Time |
|---|---|---|
| Analysis | Check warehouse structure, offer types, current template | 1-2 hours |
| Development | getStoreAvailability function, caching, AJAX endpoint | 3-4 hours |
| Integration | Block markup in catalog.element template, testing | 2-3 hours |
| Testing | Check on real data, scenarios with stock | 1-2 hours |
| Documentation | Adjustment description, access handover | 0.5-1 hour |
Total: 8 to 12 hours depending on catalog complexity and number of warehouses. Contact us — we will estimate your project within one business day.
Certified 1C-Bitrix specialists with over 5 years of experience guarantee error-free integration and full support. Get in touch for a cost estimate. Order the setup, and your online store will display real stock for each pickup point.







