mcpField
B
bitrix-catalog

bitrix-catalog

github.com/bxmaximumDevelopment47292 installsUpdated 2w ago
Repository

About bitrix-catalog

A Bitrix development skill for trade catalogs, products, prices, SKU offers, and stock. It explains which ORM reads and model-based writes to use, including safeguards around recalculation and store access.

Key features

  1. 01Product and price ORM reads
  2. 02Model-based product and price writes
  3. 03Catalog iblock and SKU setup
  4. 04Store stock and access filtering
  5. 05Price types and VAT guidance

Use cases

  1. 01Updating product quantities or VAT
  2. 02Adding prices with catalog validation
  3. 03Reading stock under store access restrictions
  4. 04Handling products with SKU offers

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/bxmaximum/bitrix-framework-skills --skill bitrix-catalog

After installing, type/bitrix-catalogin Claude Code to confirm it loaded.

Details

From the project README

Trade catalog (catalog)

Baseline: main 23.0+ · Verified: catalog 26.400.0, main 26.800.0

Product ID = iblock element ID. Needs iblock + catalog loaded. Cart, orders, payments → bitrix-sale; elements and properties → bitrix-iblocks.

API choice

NeedUse
Read product/price/store rowsProductTable, PriceTable, StoreProductTable, CatalogIblockTable (ORM)
Write product fields (qty, type, VAT)Model\Product::add/update — recalculates AVAILABLE, parent SKU type/availability, sets, subscriptions
Write pricesModel\Price::add/update/delete — computes PRICE_SCALE, validates currency, recounts parent SKU prices
Register catalog iblockCCatalog::Add/Update (no D7 writer)
Legacy, compat onlyCCatalogProduct, CPrice, CCatalogSku helpers

Never write ProductTable/PriceTable directly: PriceTable::add() without PRICE_SCALE fails validation, and direct writes skip availability and SKU recalculation.

Catalog iblocks and SKU

\CCatalog::Add(['IBLOCK_ID' => $productIblockId]);                       // simple catalog
\CCatalog::Add([
    'IBLOCK_ID' => $offersIblockId,
    'PRODUCT_IBLOCK_ID' => $productIblockId,
    'SKU_PROPERTY_ID' => $skuPropertyId, // E property on the offers iblock, USER_TYPE = PropertyTable::USER_TYPE_SKU, LINK_IBLOCK_ID = products
]);
$info = \CCatalogSku::GetInfoByIBlock($iblockId); // CATALOG_TYPE, PRODUCT_IBLOCK_ID, IBLOCK_ID, SKU_PROPERTY_ID…

CATALOG_TYPE: CCatalogSku::TYPE_CATALOG (D, plain), TYPE_PRODUCT (P, parents only), TYPE_OFFERS (O, offers iblock), TYPE_FULL (X, products + SKU parents). Offers of products: \CCatalogSku::getOffersList($productIds, $iblockId).

Product types ProductTable::TYPE_*: PRODUCT 1, SET 2, SKU 3 (parent with offers), OFFER 4, FREE_OFFER 5 (offer without parent), EMPTY_SKU 6, SERVICE 7 (no stock tracking). Buy the offer (TYPE_OFFER), never the SKU parent.

use Bitrix\Catalog\Model\Product;
use Bitrix\Catalog\ProductTable;

$result = Product::update($elementId, [ // Product::add(['ID' => $elementId, 'TYPE' => ProductTable::TYPE_PRODUCT, …]) for a new row
    'QUANTITY' => 10,
    'QUANTITY_TRACE' => ProductTable::STATUS_YES,
    'CAN_BUY_ZERO' => ProductTable::STATUS_NO,
]);
if (!$result->isSuccess()) { /* $result->getErrorMessages() */ }

Prices

Price types: GroupTable (b_catalog_group); base type GroupTable::getBasePriceTypeId() (cached, ?int). Who may see/buy by type: GroupAccessTable.

use Bitrix\Catalog\GroupTable;
use Bitrix\Catalog\Model\Price;
use Bitrix\Catalog\PriceTable;

$result = Price::add([
    'PRODUCT_ID' => $elementId,
    'CATALOG_GROUP_ID' => GroupTable::getBasePriceTypeId(),
    'PRICE' => 1990.00,
    'CURRENCY' => 'RUB',   // must exist in the currency module
]);
if (!$result->isSuccess()) { /* $result->getErrorMessages() */ }

$prices = PriceTable::getList([
    'select' => ['ID', 'CATALOG_GROUP_ID', 'PRICE', 'CURRENCY', 'QUANTITY_FROM', 'QUANTITY_TO'],
    'filter' => ['=PRODUCT_ID' => $elementId],
])->fetchAll();

VAT lives on the product (VAT_ID, VAT_INCLUDED); rates in VatTable. Final buyer prices and discounts are computed by sale (bitrix-sale), not by reading PriceTable.

Stock and stores

  • Product level: QUANTITY, QUANTITY_RESERVED, QUANTITY_TRACE, CAN_BUY_ZERO, AVAILABLE on ProductTable.
  • Stores: StoreTable + StoreProductTable (STORE_ID, PRODUCT_ID, AMOUNT, QUANTITY_RESERVED). Warehouse operations (receipt, move, write-off) go through store documents (StoreDocumentTable, conduct with CCatalogDocs::conductDocument($docId, $userId), false + $APPLICATION->GetException() on error), not direct AMOUNT edits or SQL.
  • Store rights (inventory management with crm): mirror what the user may see:
use Bitrix\Catalog\Access\AccessController;
use Bitrix\Catalog\Access\ActionDictionary;
use Bitrix\Catalog\StoreProductTable;

$access = AccessController::getCurrent()
    ->getEntityFilter(ActionDictionary::ACTION_STORE_VIEW, StoreProductTable::class) ?? [];
$rows = StoreProductTable::getList([
    'select' => ['STORE_ID', 'AMOUNT', 'QUANTITY_RESERVED'],
    'filter' => [['=PRODUCT_ID' => $elementId], $access], // nested: an OR in user filter cannot bypass access
])->fetchAll();
  • Since catalog 26.400 REST catalog.storeproduct.list/get and catalog.document.element.list apply this filter. Document rows of forbidden stores return STORE_FROM/STORE_TO/AMOUNT/PURCHASING_PRICE = null, and these fields cannot be filtered or sorted. Integrations must not rely on them. Stock rows are not filtered for admins and in legacy mode: AccessController::getCurrent()->isLegacyAccessMode() (Since catalog 26.400; before: !ModuleManager::isModuleInstalled('crm')). Older versions return all stores over REST: filter in the integration.

Checklist

  • iblock + catalog loaded; catalog iblocks registered via CCatalog::Add, SKU with PRODUCT_IBLOCK_ID + SKU_PROPERTY_ID.
  • Product and price writes via Model\Product / Model\Price, every Result checked.
  • Base price type via GroupTable::getBasePriceTypeId(), no hardcoded IDs.
  • Offers (not SKU parents) go to the basket.
  • Stock read with the store access filter where users are restricted; no raw SQL.
  • Batch reads (collect IDs, one query); no CCatalogProduct::GetByID in loops.

Comments

Markdown supported