Development of a cost calculation form for 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
    1183
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    813
  • 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
    564
  • 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
    976

Cost Calculation Form Development on 1C-Bitrix

A cost calculation form is a calculator that allows users to independently estimate the price of a service or product before calling a manager. Used in construction, IT services, manufacturing, logistics, and insurance. The conversion effect is twofold: some users leave after seeing the price — but those who remain come prepared with realistic expectations.

Calculator Types

Simple (deterministic): each parameter has a fixed price. Total = sum of components. Example: selecting a car configuration.

Coefficient-based: there is a base price and coefficients — regional, seasonal, by complexity. Total = base_price × K1 × K2 × ... Used in construction and insurance.

Formula-based: an arbitrary formula with multiple input variables. Example: shipping cost = weight × distance × rate + fixed cost.

With range: the result is not an exact amount, but a range "from X to Y". Used in IT development where an exact estimate without a technical specification is impossible.

Storing the Calculator Configuration

It is better to store the configuration in the database (HL-block) rather than in code — so that managers can change prices without a developer.

class CalculatorConfigTable extends \Bitrix\Main\ORM\Data\DataManager
{
    public static function getTableName(): string { return 'b_hl_calculator_config'; }

    public static function getMap(): array
    {
        return [
            new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
            new StringField('SLUG'),           // Calculator identifier
            new StringField('TITLE'),
            new TextField('PARAMS_JSON'),      // Parameters and their prices
            new TextField('FORMULA_JSON'),     // Calculation formula
            new StringField('CURRENCY'),       // RUB, USD, EUR
            new FloatField('MIN_PRICE'),       // Minimum price
            new FloatField('MAX_PRICE'),       // NULL = no limit
            new BooleanField('SHOW_RANGE', ['values' => [false, true]]),
            new IntegerField('RANGE_PERCENT'), // ±% for the range
        ];
    }
}

PARAMS_JSON — parameter descriptions:

[
  {
    "id": "area",
    "label": "Area (sq.m.)",
    "type": "range",
    "min": 10,
    "max": 500,
    "default": 50,
    "step": 5,
    "unit": "sq.m."
  },
  {
    "id": "floors",
    "label": "Number of floors",
    "type": "select",
    "options": [
      {"value": 1, "label": "1 floor",  "price": 0},
      {"value": 2, "label": "2 floors", "price": 15000},
      {"value": 3, "label": "3 floors", "price": 35000}
    ]
  },
  {
    "id": "foundation",
    "label": "Foundation type",
    "type": "radio",
    "options": [
      {"value": "tape", "label": "Strip",  "price_per_m2": 2500},
      {"value": "pile", "label": "Pile",   "price_per_m2": 1800},
      {"value": "slab", "label": "Slab",   "price_per_m2": 4200}
    ]
  },
  {
    "id": "finishing",
    "label": "Finishing",
    "type": "checkbox-group",
    "options": [
      {"value": "rough",     "label": "Rough",       "price_per_m2": 3000},
      {"value": "prefinish", "label": "Pre-finish",  "price_per_m2": 5500},
      {"value": "finish",    "label": "Full finish",  "price_per_m2": 9000}
    ]
  }
]

JavaScript Calculation Engine

class PriceCalculator {
    constructor(config) {
        this.params     = config.params;
        this.formula    = config.formula;     // 'params_sum' | 'formula' | string
        this.currency   = config.currency;
        this.minPrice   = config.min_price;
        this.showRange  = config.show_range;
        this.rangePercent = config.range_percent || 15;

        this.values     = {}; // Current parameter values
        this.initDefaults();
    }

    initDefaults() {
        this.params.forEach(param => {
            if (param.default !== undefined) {
                this.values[param.id] = param.default;
            } else if (param.type === 'select' || param.type === 'radio') {
                this.values[param.id] = param.options[0]?.value;
            } else if (param.type === 'checkbox-group') {
                this.values[param.id] = [];
            }
        });
    }

    setValue(paramId, value) {
        this.values[paramId] = value;
    }

    calculate() {
        let total = 0;
        const area = parseFloat(this.values['area']) || 1;

        this.params.forEach(param => {
            const val = this.values[param.id];
            if (!val && val !== 0) return;

            switch (param.type) {
                case 'range':
                case 'number':
                    // Base price per unit (if set)
                    if (param.price_per_unit) {
                        total += parseFloat(val) * param.price_per_unit;
                    }
                    break;

                case 'select':
                case 'radio': {
                    const opt = param.options.find(o => String(o.value) === String(val));
                    if (opt) {
                        total += (opt.price || 0) + (opt.price_per_m2 || 0) * area;
                    }
                    break;
                }

                case 'checkbox-group': {
                    const selected = Array.isArray(val) ? val : [val];
                    selected.forEach(v => {
                        const opt = param.options.find(o => String(o.value) === String(v));
                        if (opt) {
                            total += (opt.price || 0) + (opt.price_per_m2 || 0) * area;
                        }
                    });
                    break;
                }
            }
        });

        if (this.minPrice && total < this.minPrice) {
            total = this.minPrice;
        }

        if (this.showRange) {
            const delta = total * (this.rangePercent / 100);
            return {
                min:   Math.floor(total - delta),
                max:   Math.ceil(total + delta),
                exact: null,
            };
        }

        return {min: null, max: null, exact: total};
    }

    formatPrice(value) {
        return new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: this.currency,
            maximumFractionDigits: 0,
        }).format(value);
    }
}

Integration with the Application Form

After the calculation — a "Get a quote" button opens a form with the calculation result pre-filled:

document.getElementById('btn-get-quote').addEventListener('click', () => {
    const result = calculator.calculate();
    const resultText = result.exact
        ? calculator.formatPrice(result.exact)
        : `from ${calculator.formatPrice(result.min)} to ${calculator.formatPrice(result.max)}`;

    // Fill hidden form fields
    document.getElementById('form-calc-result').value = resultText;
    document.getElementById('form-calc-params').value = JSON.stringify(calculator.values);

    // Scroll to the form or open a modal window
    document.getElementById('quote-form').scrollIntoView({behavior: 'smooth'});
});

The server-side handler saves the calculation parameters in the lead's comments:

$calcParams = json_decode($data['calc_params'] ?? '{}', true);
$paramsSummary = [];
foreach ($calcParams as $paramId => $value) {
    $paramsSummary[] = $paramId . ': ' . (is_array($value) ? implode(', ', $value) : $value);
}

$lead = new \CCrmLead(false);
$lead->Add([
    'TITLE'    => 'Cost calculation — ' . $name,
    'COMMENTS' => "Calculation result: {$calcResult}\n" . implode("\n", $paramsSummary),
]);

Saving Calculations

It is useful to save calculations for analytics — which parameters are chosen more often, which values lead to an application:

class CalculatorLeadTable extends \Bitrix\Main\ORM\Data\DataManager
{
    public static function getTableName(): string { return 'b_hl_calc_leads'; }
    // CALC_ID, PARAMS_JSON, RESULT_MIN, RESULT_MAX, RESULT_EXACT, SESSION_ID, CONVERTED (bool), CREATED_AT
}

Record on each "Calculate" click, mark CONVERTED = true when a lead is created. Conversion rate = CONVERTED / total records.

Development Timeline

Option Scope Timeline
Simple calculator Fixed prices, total, form 3–5 days
Coefficient-based Base price × coefficients, range 5–8 days
With configurator Price management via the admin panel 8–14 days