Setting Up Accounting for Marked Products on 1C-Bitrix
Accounting for marked products covers the entire cycle: receiving from supplier → storage in warehouse → sale → return. Each stage must be reflected in the "Honest Sign" marking system. Bitrix itself is not an accounting system for marking — but for online stores without 1C you can set up basic accounting directly on Bitrix.
Warehouse Accounting of Marked Units
Standard Bitrix stock accounting (b_catalog_store_product) works with quantities, not specific instances. For marked products you need accounting at the serial number level.
Create a serial number (marking codes) table and bind to warehouse:
CREATE TABLE b_local_marking_inventory (
ID INT AUTO_INCREMENT PRIMARY KEY,
PRODUCT_ID INT NOT NULL, -- product ID from b_iblock_element
STORE_ID INT, -- store ID from b_catalog_store
CODE VARCHAR(200) NOT NULL, -- Data Matrix code
GTIN CHAR(14),
SERIAL VARCHAR(20),
STATUS ENUM('received','reserved','sold','returned','defective') DEFAULT 'received',
ORDER_ID INT, -- when status is sold/reserved
RECEIVED_AT DATETIME,
UPDATED_AT DATETIME ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_product_status (PRODUCT_ID, STATUS),
INDEX idx_code (CODE)
);
Relationship with b_catalog_store_product table: when adding a record to b_local_marking_inventory with status received increment the balance via CCatalogStoreProduct::Update(). On sale — decrement. This maintains compatibility with Bitrix catalog components that read stock from the standard table.
Reservation on Order Checkout
When adding to cart or checking out a marked product must be reserved — transfer code to reserved status with binding to ORDER_ID. This prevents selling one instance to two buyers.
Handler on OnSaleBasketItemAdd event:
AddEventHandler("sale", "OnSaleBasketItemAdd", function(&$arFields) {
$productId = $arFields['PRODUCT_ID'];
if (isMarkedProduct($productId)) {
// Find free code for product
$code = \Local\MarkingCode\InventoryTable::getList([
'filter' => ['PRODUCT_ID' => $productId, 'STATUS' => 'received'],
'limit' => 1,
'select' => ['ID', 'CODE'],
])->fetch();
if (!$code) {
// No available instances — block adding
return false;
}
// Reserve
\Local\MarkingCode\InventoryTable::update($code['ID'], ['STATUS' => 'reserved']);
// Save code ID to cart item property
$arFields['PROPS'][] = ['NAME' => 'MARKING_CODE_ID', 'VALUE' => $code['ID']];
}
});
On order cancellation — free up reserved codes back to received status. Handler on OnSaleOrderStatusUpdate when transitioning to cancel status.
Deduction on Successful Sale
On payment receipt (event OnSaleOrderPaid or OnSalePaymentPaid) transfer all reserved codes to sold and put in queue for sending notification to GIS MT:
AddEventHandler("sale", "OnSaleOrderPaid", function($id, $arOrder) {
$markingCodes = \Local\MarkingCode\InventoryTable::getList([
'filter' => ['ORDER_ID' => $id, 'STATUS' => 'reserved'],
]);
while ($code = $markingCodes->fetch()) {
\Local\MarkingCode\InventoryTable::update($code['ID'], ['STATUS' => 'sold']);
\Local\MarkingCode\NotificationQueue::add([
'CODE' => $code['CODE'],
'ORDER_ID' => $id,
'OPERATION' => 'SALE',
]);
}
});
Reporting on Marked Products
For analytics — admin page at /local/admin/marking_report.php with selection by status, date, product. Aggregation via direct SQL queries to b_local_marking_inventory:
SELECT PRODUCT_ID,
COUNT(*) as total,
SUM(STATUS = 'received') as in_stock,
SUM(STATUS = 'sold') as sold
FROM b_local_marking_inventory
GROUP BY PRODUCT_ID;
Daily reconciliation: count of sold codes should match count of products in completed orders. Discrepancy signals an error in event handlers.







