Setting up tax rates by region 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 Tax Rates by Region in 1C-Bitrix

Store operates in Belarus and Russia with different VAT rates. Or sells digital goods in EU where VAT depends on buyer's country. Standard single rate for entire catalog doesn't work here — need regional logic.

Tax groups

Bitrix implements regional rates through tax groups mechanism. Table b_sale_tax stores tax groups, b_sale_tax_rate — rates within groups tied to region.

Structure of b_sale_tax_rate: TAX_ID (group reference), COUNTRY_ID, REGION, CITY, RATE, IS_PERCENT (Y/N), IS_IN_PRICE (Y/N — analog of VAT_INCLUDED), ACTIVE.

Creating tax group with regional rates:

// Create group
$taxResult = \Bitrix\Sale\Tax::add([
    'NAME' => 'VAT',
    'ACTIVE' => 'Y',
    'LID' => 's1',
]);
$taxId = $taxResult->getId();

// Rate for Belarus
\Bitrix\Sale\TaxRate::add([
    'TAX_ID' => $taxId,
    'COUNTRY_ID' => 'BY',
    'RATE' => 20,
    'IS_PERCENT' => 'Y',
    'IS_IN_PRICE' => 'Y',
    'ACTIVE' => 'Y',
]);

// Rate for Russia
\Bitrix\Sale\TaxRate::add([
    'TAX_ID' => $taxId,
    'COUNTRY_ID' => 'RU',
    'RATE' => 20,
    'IS_PERCENT' => 'Y',
    'IS_IN_PRICE' => 'Y',
    'ACTIVE' => 'Y',
]);

Binding taxes to order properties

Determining applicable rate happens when basket is calculated. Bitrix reads buyer address from order properties (b_sale_order_props_value) — looks for property with type LOCATION or TEXT with country/region code.

Binding tax group to store — configuration in /bitrix/admin/sale_tax.php. Each group can be active for specific site (LID). When calculating basket, sale module calls \Bitrix\Sale\Tax::getList() filtered by LID and applies first matching rate by country/region from address.

Determining buyer's region

Problem with the mechanism: buyer doesn't always fill address before basket calculation. For automatic region determination by IP, the seo module is used — class \Bitrix\Seo\Ip2Location. It queries geolocation service and returns country/region.

Handler for auto-substituting country in basket:

AddEventHandler('sale', 'OnSaleBasketBeforeSaved', function($basket) {
    $order = $basket->getOrder();
    if (!$order) return;

    $ip = $_SERVER['REMOTE_ADDR'];
    $location = \Bitrix\Seo\Ip2Location::getLocationByIp($ip);

    if ($location && $location['COUNTRY_CODE']) {
        // set order property with country code
        $propertyCollection = $order->getPropertyCollection();
        $prop = $propertyCollection->getItemByOrderPropertyCode('COUNTRY');
        if ($prop) {
            $prop->setValue($location['COUNTRY_CODE']);
        }
    }
});

Taxes and prices: display

If catalog prices include VAT (VAT_INCLUDED = Y), and regional rate is lower (e.g., 0% for export), basket should recalculate prices. Bitrix does this automatically via tax mechanism in sale module, but only if IS_IN_PRICE = Y in b_sale_tax_rate and VAT_INCLUDED = Y in b_catalog_product are aligned.

Mismatch of these fields — main source of incorrect VAT with regional rates. Verification query to find unaligned products:

SELECT cp.IBLOCK_ELEMENT_ID, cp.VAT_INCLUDED, cv.RATE
FROM b_catalog_product cp
LEFT JOIN b_catalog_vat cv ON cp.VAT_ID = cv.ID
WHERE cp.VAT_ID IS NOT NULL
AND cp.VAT_INCLUDED != 'Y';

All found items must be aligned to single rule before setting up regional rates.