Setting up accounting for labeled goods in 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    565
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    980

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.