Как настроить округление для пользовательского поля типа «Число»

Scope: crm, userfieldconfig

Кто может выполнять методы: чтобы пройти сценарий целиком, нужно самое строгое из перечисленных прав — «Разрешить изменять настройки»

Выберите инструмент для разработки с AI-агентом:

  • используйте Битрикс24 Вайбкод, чтобы создать приложение для Битрикс24 по описанию задачи без знания языков программирования. Агент напишет код и разместит приложение на сервере без ручной настройки хостинга
  • используйте MCP-сервер, чтобы разрабатывать интеграцию через REST API в своем проекте. Агент будет обращаться к официальной REST-документации

У пользовательских полей есть стандартные настройки: название, обязательность заполнения, множественное значение.

Дополнительно есть специализированные настройки, их набор зависит от типа поля:

  • значения для списка
  • точность округления для чисел
  • валюта для денежных полей

У типа «Число» — double — точность задает настройка PRECISION. Это целое число от 0 до 12: столько знаков после запятой остается в значении. Битрикс24 округляет значение в момент сохранения. Например, при PRECISION: 3 введенное 1,23456 сохранится как 1.235.

Выберите сценарий

На этой странице два независимых сценария. Они не связаны между собой: второй не использует результат первого, а начинается с поиска уже существующего поля.

В обоих сценариях примеры работают с полями сделок. Для другого объекта CRM изменится идентификатор объекта в entityId и метод получения списка полей — например, crm.lead.userfield.list для лидов.

Подготовим данные

Для выполнения примеров нужны:

  • входящий вебхук со scope crm и userfieldconfig. Вебхук выполняет запросы с правами создавшего его пользователя. Не публикуйте секретный код вебхука в клиентском коде и репозиториях — храните его в переменных окружения, как в примере на JS. В примерах на PHP и Python на месте адреса вебхука стоит плейсхолдер, подставьте туда свой способ хранения секрета
  • право «Разрешить изменять настройки» у пользователя вебхука. Это общее право на настройки CRM: оно выдается роли целиком и не задается отдельно для сделок или другого объекта. Исключение — смарт-процессы внутри автоматизированного решения: у них право проверяется на уровне самого решения. Без этого права userfieldconfig.add и userfieldconfig.update вернут ошибку доступа

Для серверных JS-примеров с B24Hook нужен Node.js 18, 20, 22 или новее, для новых проектов — 22 или новее. B24JsSDK — ES module: сохраните код в файле .mjs или добавьте "type": "module" в package.json. Для примеров с b24pysdk нужен Python 3.9 или новее, для примеров с B24PhpSDK версии 3 — PHP 8.4 или новее.

Как использовать примеры в документации

Примеры шагов продолжают друг друга. SDK инициализируется один раз здесь, дальше в примерах используется готовый экземпляр: $b24 в JS, $sb в PHP и client в Python.

import { B24Hook } from '@bitrix24/b24jssdk'

const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError

client = Client(
    BitrixWebhook(
        domain="your-domain.bitrix24.com",
        webhook_token="user_id/webhook_key",
    )
)
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';

use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Psr\Log\NullLogger;

$sb = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
    ->initFromWebhook('https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/');
// Путь вебхука — это секрет, поэтому он приходит из окружения, а не из кода.
// Клиент строится один раз на портал: он держит HTTP-клиент и состояние
// авторизации.
core := b24.NewClient(os.Getenv("B24_WEBHOOK_URL")).Core()

Учтите разный регистр полей

Методы на этой странице возвращают одни и те же данные в разном регистре. Так работает API — приводить форматы друг к другу не нужно, но их легко перепутать в коде.

  • методы userfieldconfig.* принимают и возвращают поля в camelCase: fieldName, userTypeId, editFormLabel, settings
  • методы crm.*.userfield.*, в том числе crm.deal.userfield.list, возвращают поля в UPPER_SNAKE: FIELD_NAME, USER_TYPE_ID, EDIT_FORM_LABEL, SETTINGS

Ключи внутри самих настроек в обоих случаях в верхнем регистре: PRECISION, SIZE, MIN_VALUE, MAX_VALUE, DEFAULT_VALUE. Поэтому точность созданного поля лежит в settings.PRECISION, а точность поля из списка сделок — в SETTINGS.PRECISION.

Создаем поле сразу с настройкой округления

Создадим поле сделки с типом «Число» и точностью три знака после запятой. Если в такое поле ввести значение с четырьмя или более знаками после запятой, при сохранении оно округлится до трех знаков.

Чтобы создать пользовательское поле, используем метод userfieldconfig.add с параметрами:

  • moduleId — идентификатор модуля в котором метод создаст поле, обязательный параметр. В примере создаем поле для сделок, модуль — crm

  • field[entityId] — идентификатор объекта, обязательный параметр. У сделок и других базовых объектов CRM идентификатор фиксированный: CRM_DEAL, CRM_LEAD, CRM_CONTACT, CRM_COMPANY. Формат CRM_{ID} с числовым идентификатором используется только для пользовательских смарт-процессов, а у системных — свои строковые идентификаторы вроде CRM_SMART_INVOICE. Полный перечень есть в статье Настройки пользовательских полей. В примере укажем CRM_DEAL

  • field[fieldName] — код поля по формуле UF_ + {идентификатор объекта} + _ + {произвольная строка в UPPERCASE}. Обязательный параметр. Код должен начинаться с UF_ и идентификатора объекта из entityId, иначе метод вернет ошибку. Допустимы символы A-Z, 0-9 и _, ограничение длины — 50 символов. В примере укажем UF_CRM_DEAL_NEW_DOUBLE_FIELD

  • field[userTypeId] — идентификатор типа поля, обязательный параметр. В примере укажем double для создания поля типа число

  • field[editFormLabel] — массив названий для отображения поля в Битрикс24 на разных языках. Необязательный параметр, при отсутствии названия в Битрикс24 будет отображаться код поля

  • field[settings] — массив дополнительных настроек поля в зависимости от его типа. В примере укажем настройку PRECISION — точность. В нее передадим целое число, равное количеству знаков после запятой. Параметр необязательный, но для типа «Число» его лучше передавать: без него точность будет 0 и значения округлятся до целых

const addResponse = await $b24.actions.v2.call.make({
    method: 'userfieldconfig.add',
    params: {
        moduleId: 'crm', // Идентификатор модуля
        field: {
            entityId: 'CRM_DEAL', // Идентификатор объекта
            fieldName: 'UF_CRM_DEAL_NEW_DOUBLE_FIELD', // Код поля
            userTypeId: 'double', // Идентификатор типа поля
            editFormLabel: {
                'ru': 'Число с округлением', // Название поля на русском
                'en': 'PRECISION double' // Название поля на английском
            },
            settings: { // Дополнительные настройки поля
                PRECISION: 3 // Количество знаков после запятой
            }
        }
    },
    requestId: 'userfieldconfig-add'
});

if (!addResponse.isSuccess) {
    throw new Error(addResponse.getErrorMessages().join('; '))
}

const createdField = addResponse.getData().result.field;
console.log(createdField.id, createdField.settings.PRECISION);
try:
    created_field = client.userfieldconfig.add(
        module_id="crm",
        field={
            "entityId": "CRM_DEAL",
            "fieldName": "UF_CRM_DEAL_NEW_DOUBLE_FIELD",
            "userTypeId": "double",
            "editFormLabel": {
                "ru": "Число с округлением",
                "en": "PRECISION double",
            },
            "settings": {
                "PRECISION": 3,
            },
        },
    ).response.result["field"]
except BitrixAPIError as error:
    print(f"Ошибка: {error}")
else:
    print(created_field["id"], created_field["settings"]["PRECISION"])
// у userfieldconfig.add нет типизированной обертки в SDK — вызываем метод через ядро
$createdField = $sb->core->call(
    'userfieldconfig.add',
    [
        'moduleId' => 'crm', // Идентификатор модуля
        'field' => [
            'entityId' => 'CRM_DEAL', // Идентификатор объекта
            'fieldName' => 'UF_CRM_DEAL_NEW_DOUBLE_FIELD', // Код поля
            'userTypeId' => 'double', // Идентификатор типа поля
            'editFormLabel' => [
                'ru' => 'Число с округлением', // Название поля на русском
                'en' => 'PRECISION double' // Название поля на английском
            ],
            'settings' => [ // Дополнительные настройки поля
                'PRECISION' => 3 // Количество знаков после запятой
            ]
        ]
    ]
)->getResponseData()->getResult()['field'];

echo $createdField['id'] . ': ' . $createdField['settings']['PRECISION'];
res, err := core.Call(ctx, "userfieldconfig.add", b24.Params{
	"moduleId": "crm",
	"field": b24.Params{
		"entityId":   "CRM_DEAL",
		"fieldName":  fieldName,
		"userTypeId": "double",
		"editFormLabel": b24.Params{
			"ru": fieldLabel,
			"en": "PRECISION double",
		},
		// Параметр необязательный, но для типа «Число» его лучше
		// передавать: без него точность будет 0 и значения округлятся
		// до целых.
		"settings": b24.Params{"PRECISION": 3},
	},
})
if err != nil {
	// Код ошибки сравнивается через errors.Is, а не строкой: опечатка в
	// литерале скомпилируется и молча уведёт в другую ветку.
	if errors.Is(err, b24.ErrAccessDenied) {
		return fmt.Errorf("нужно право «Разрешить изменять настройки» в CRM: %w", err)
	}
	return fmt.Errorf("userfieldconfig.add: %w", err)
}

// Метод заворачивает ответ в объект с ключом field и отвечает в camelCase:
// точность лежит в settings.PRECISION, а не в SETTINGS.PRECISION.
var added struct {
	Field struct {
		ID       b24.ID         `json:"id"`
		Settings map[string]any `json:"settings"`
	} `json:"field"`
}
if err := json.Unmarshal(res.Result, &added); err != nil {
	return fmt.Errorf("разбор созданного поля: %w", err)
}

В результате получим данные созданного поля.

{
    "result": {
        "field": {
            "id": "6961",
            "entityId": "CRM_DEAL",
            "fieldName": "UF_CRM_DEAL_NEW_DOUBLE_FIELD",
            "userTypeId": "double",
            "xmlId": null,
            "sort": "100",
            "multiple": "N",
            "mandatory": "N",
            "showFilter": "N",
            "showInList": "Y",
            "editInList": "Y",
            "isSearchable": "N",
            "settings": {
                "PRECISION": 3,
                "SIZE": 20,
                "MIN_VALUE": 0,
                "MAX_VALUE": 0,
                "DEFAULT_VALUE": null
            },
            "languageId": {
                "en": "en",
                "ru": "ru"
            },
            "editFormLabel": {
                "en": "PRECISION double",
                "ru": "Число с округлением"
            },
            "listColumnLabel": {
                "en": null,
                "ru": null
            },
            "listFilterLabel": {
                "en": null,
                "ru": null
            },
            "errorMessage": {
                "en": null,
                "ru": null
            },
            "helpMessage": {
                "en": null,
                "ru": null
            }
        }
    }
}

Ответ подтверждает результат: в settings.PRECISION стоит переданная точность 3, остальные настройки типа заполнены значениями по умолчанию. Сохраните id поля — 6961 в примере. По нему поле изменяют методом userfieldconfig.update или удаляют методом userfieldconfig.delete, не запрашивая список полей заново.

На этом первый сценарий закончен. Перейдите к разделу Проверим результат — второй сценарий нужен только для полей, которые уже существуют.

Изменяем настройку у существующего поля

Сценарий не зависит от первого: поле уже создано, его точность нужно изменить. Метод userfieldconfig.update принимает id поля, поэтому сценарий состоит из двух шагов.

  1. Получим ID и текущие настройки поля методом crm.deal.userfield.list
  2. Передадим их в userfieldconfig.update, изменив только точность

Если поле только что создано первым сценарием, первый шаг не нужен: и id, и настройки уже пришли в ответе userfieldconfig.add — в field.id и field.settings. Обратите внимание, что в этом ответе они в camelCase.

1. Получаем ID поля

Чтобы получить ID поля используем метод crm.deal.userfield.list с параметрами:

  • filter[LANG] — фильтр по языку используем для вывода названий полей на нужном языке. Без этого фильтра названия не возвращаются вовсе, и найти поле по названию не получится

  • filter[USER_TYPE_ID] — фильтр по типу поля используем чтобы получить только поля с типом «Число» в результате

Метод возвращает поля в UPPER_SNAKE — учитывайте это, когда сравниваете его ответ с ответами userfieldconfig.*.

const listResponse = await $b24.actions.v2.call.make({
    method: 'crm.deal.userfield.list',
    params: {
        filter: {
            LANG: 'ru', // Фильтр по языку для вывода названия поля
            USER_TYPE_ID: 'double' // Фильтр по типу поля
        }
    },
    requestId: 'userfield-list'
});

if (!listResponse.isSuccess) {
    throw new Error(listResponse.getErrorMessages().join('; '))
}

// Выбираем нужное поле по названию — замените на название своего поля
const targetField = listResponse.getData().result
    .find(field => field.EDIT_FORM_LABEL === 'Сумма к возврату');

if (!targetField) {
    throw new Error('Поле с указанным названием не найдено')
}
fields = client.crm.deal.userfield.list(
    filter={
        "LANG": "ru",
        "USER_TYPE_ID": "double",
    }
).response.result

# Выбираем нужное поле по названию — замените на название своего поля
target_field = next(
    (field for field in fields if field["EDIT_FORM_LABEL"] == "Сумма к возврату"),
    None,
)

if target_field is None:
    raise RuntimeError("Поле с указанным названием не найдено")
$fields = $sb->getCRMScope()->dealUserfield()->list(
    order: [],
    filter: [
        'LANG' => 'ru', // Фильтр по языку для вывода названия поля
        'USER_TYPE_ID' => 'double' // Фильтр по типу поля
    ]
)->getUserfields();

// Выбираем нужное поле по названию — замените на название своего поля
$targetField = null;
foreach ($fields as $field) {
    if ($field->EDIT_FORM_LABEL === 'Сумма к возврату') {
        $targetField = $field;
        break;
    }
}

if ($targetField === null) {
    throw new \RuntimeException('Поле с указанным названием не найдено');
}
// Без фильтра LANG названия не возвращаются вовсе, и найти поле по названию
// не получится.
res, err = core.Call(ctx, "crm.deal.userfield.list", b24.Params{
	"filter": b24.Params{"LANG": "ru", "USER_TYPE_ID": "double"},
}, b24.WithIdempotent())
if err != nil {
	return fmt.Errorf("crm.deal.userfield.list: %w", err)
}

// А этот метод отвечает в UPPER_SNAKE — те же данные, другой регистр.
// Ключи внутри самих настроек в обоих случаях в верхнем регистре.
var fields []struct {
	ID            b24.ID         `json:"ID"`
	FieldName     string         `json:"FIELD_NAME"`
	EditFormLabel string         `json:"EDIT_FORM_LABEL"`
	Settings      map[string]any `json:"SETTINGS"`
}
if err := json.Unmarshal(res.Result, &fields); err != nil {
	return fmt.Errorf("разбор пользовательских полей: %w", err)
}

target := -1
for i, f := range fields {
	if f.EditFormLabel == fieldLabel {
		target = i
		break
	}
}
if target < 0 {
	return fmt.Errorf("поле %q не найдено", fieldLabel)
}

В результате получим все числовые поля сделок с названиями.

{
    "result": [
        {
            "ID": "6963",
            "ENTITY_ID": "CRM_DEAL",
            "FIELD_NAME": "UF_CRM_1740471712",
            "USER_TYPE_ID": "double",
            "XML_ID": null,
            "SORT": "100",
            "MULTIPLE": "N",
            "MANDATORY": "N",
            "SHOW_FILTER": "E",
            "SHOW_IN_LIST": "Y",
            "EDIT_IN_LIST": "Y",
            "IS_SEARCHABLE": "N",
            "SETTINGS": {
                "PRECISION": 2,
                "SIZE": 20,
                "MIN_VALUE": 0,
                "MAX_VALUE": 0,
                "DEFAULT_VALUE": null
            },
            "EDIT_FORM_LABEL": "Аванс",
            "LIST_COLUMN_LABEL": "Аванс",
            "LIST_FILTER_LABEL": "Аванс",
            "ERROR_MESSAGE": null,
            "HELP_MESSAGE": null
        },
        {
            "ID": "6807",
            "ENTITY_ID": "CRM_DEAL",
            "FIELD_NAME": "UF_CRM_1723464314",
            "USER_TYPE_ID": "double",
            "XML_ID": null,
            "SORT": "150",
            "MULTIPLE": "N",
            "MANDATORY": "N",
            "SHOW_FILTER": "E",
            "SHOW_IN_LIST": "Y",
            "EDIT_IN_LIST": "Y",
            "IS_SEARCHABLE": "N",
            "SETTINGS": {
                "PRECISION": 2,
                "SIZE": 20,
                "MIN_VALUE": 0,
                "MAX_VALUE": 0,
                "DEFAULT_VALUE": null
            },
            "EDIT_FORM_LABEL": "Сумма к возврату",
            "LIST_COLUMN_LABEL": "Сумма к возврату",
            "LIST_FILTER_LABEL": "Сумма к возврату",
            "ERROR_MESSAGE": null,
            "HELP_MESSAGE": null
        }
    ],
    "total": 2
}

Из ответа нужны два значения выбранного поля:

  • ID — его передадим в id следующего вызова. В примере это 6807 у поля «Сумма к возврату»
  • SETTINGS — текущие настройки поля. Их передадим обратно, чтобы изменить только точность и не сбросить остальные настройки. Сейчас в SETTINGS.PRECISION стоит 2

Поле EDIT_FORM_LABEL — это название, по которому поле ищут в списке. Оно приходит строкой только потому, что в фильтре передан LANG.

2. Изменяем настройку округления

Для изменения настройки существующего поля используем метод userfieldconfig.update с параметрами:

  • moduleId — идентификатор модуля в котором метод изменит поле, обязательный параметр. В примере изменяем поле сделок, модуль — crm

  • id — идентификатор пользовательского поля, обязательный параметр. В примере передадим ID поля из шага 16807

  • field[settings] — массив дополнительных настроек поля в зависимости от его типа. В примере укажем настройку PRECISION — точность. В нее передадим целое число, равное количеству знаков после запятой

field[settings] заменяет набор настроек целиком, а не дописывает переданные ключи к прежним. Если передать только PRECISION, остальные настройки типа «Число» сбросятся на значения по умолчанию: SIZE20, MIN_VALUE и MAX_VALUE0, DEFAULT_VALUEnull. Поэтому в примерах мы берем SETTINGS из шага 1 и меняем в них только точность.

Понижать точность у заполненного поля опасно: лишние знаки не скрываются, а отбрасываются при следующем сохранении сделки. Вернуть их, задав прежнее значение PRECISION, уже нельзя.

Полный набор настроек типа смотрите в settings любого ответа userfieldconfig.* — там все пять ключей. Метод crm.userfield.settings.fields для типа double перечисляет только DEFAULT_VALUE и PRECISION, и на него в этом вопросе полагаться нельзя.

const updateResponse = await $b24.actions.v2.call.make({
    method: 'userfieldconfig.update',
    params: {
        moduleId: 'crm', // Идентификатор модуля
        id: Number(targetField.ID), // ID поля из шага 1
        field: {
            settings: { // Дополнительные настройки поля
                ...targetField.SETTINGS, // Переносим текущие настройки из шага 1
                PRECISION: 3 // Количество знаков после запятой
            }
        }
    },
    requestId: 'userfieldconfig-update'
});

if (!updateResponse.isSuccess) {
    throw new Error(updateResponse.getErrorMessages().join('; '))
}

const updatedField = updateResponse.getData().result.field;
console.log(updatedField.id, updatedField.settings.PRECISION);
updated_field = client.userfieldconfig.update(
    module_id="crm",
    bitrix_id=int(target_field["ID"]),
    field={
        "settings": {
            **target_field["SETTINGS"],  # Переносим текущие настройки из шага 1
            "PRECISION": 3,
        }
    },
).response.result["field"]

print(updated_field["id"], updated_field["settings"]["PRECISION"])
// у userfieldconfig.update нет типизированной обертки в SDK — вызываем метод через ядро
$updatedField = $sb->core->call(
    'userfieldconfig.update',
    [
        'moduleId' => 'crm', // Идентификатор модуля
        'id' => (int)$targetField->ID, // ID поля из шага 1
        'field' => [
            'settings' => array_merge(
                (array)$targetField->SETTINGS, // Переносим текущие настройки из шага 1
                ['PRECISION' => 3] // Количество знаков после запятой
            )
        ]
    ]
)->getResponseData()->getResult()['field'];

echo $updatedField['id'] . ': ' . $updatedField['settings']['PRECISION'];
// settings ЗАМЕНЯЮТСЯ целиком, а не дописываются. Если передать один
// PRECISION, остальные настройки типа «Число» сбросятся на значения по
// умолчанию — поэтому берём настройки из шага 1 и правим в них одну.
settings := fields[target].Settings
settings["PRECISION"] = 5

res, err = core.Call(ctx, "userfieldconfig.update", b24.Params{
	"moduleId": "crm",
	"id":       fields[target].ID,
	"field":    b24.Params{"settings": settings},
})
if err != nil {
	return fmt.Errorf("userfieldconfig.update: %w", err)
}

var updated struct {
	Field struct {
		ID       b24.ID         `json:"id"`
		Settings map[string]any `json:"settings"`
	} `json:"field"`
}
if err := json.Unmarshal(res.Result, &updated); err != nil {
	return fmt.Errorf("разбор изменённого поля: %w", err)
}

В результате получим данные измененного поля.

{
    "result": {
        "field": {
            "id": "6807",
            "entityId": "CRM_DEAL",
            "fieldName": "UF_CRM_1723464314",
            "userTypeId": "double",
            "xmlId": null,
            "sort": "150",
            "multiple": "N",
            "mandatory": "N",
            "showFilter": "E",
            "showInList": "Y",
            "editInList": "Y",
            "isSearchable": "N",
            "settings": {
                "PRECISION": 3,
                "SIZE": 20,
                "MIN_VALUE": 0,
                "MAX_VALUE": 0,
                "DEFAULT_VALUE": null
            },
            "languageId": {
                "ru": "ru"
            },
            "editFormLabel": {
                "ru": "Сумма к возврату"
            },
            "listColumnLabel": {
                "ru": "Сумма к возврату"
            },
            "listFilterLabel": {
                "ru": "Сумма к возврату"
            },
            "errorMessage": {
                "ru": null
            },
            "helpMessage": {
                "ru": null
            }
        }
    }
}

Метод вернул поле в camelCase, поэтому новая точность лежит в settings.PRECISION, а не в SETTINGS.PRECISION, как в ответе шага 1. Значение изменилось с 2 на 3 — сценарий выполнен.

В languageId перечислены языки, для которых у поля заданы подписи, — их набор задается при создании и изменении поля, а не порталом. Поэтому у поля из примера только ru, а у поля, созданного в первом сценарии с editFormLabel на двух языках, будут ru и en. От фильтра LANG из шага 1 набор языков не зависит.

Пример кода

Пример собирает второй сценарий целиком: находит поле сделки по названию и меняет его точность. Название поля задано константой в начале — замените ее на название своего поля.

import { B24Hook } from '@bitrix24/b24jssdk'

const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'

const FIELD_LABEL = 'Сумма к возврату' // Название поля, которое нужно изменить
const PRECISION = 3 // Количество знаков после запятой

async function updateUserField() {
    try {
        // Шаг 1: получаем пользовательские поля сделок типа double
        const listResponse = await $b24.actions.v2.call.make({
            method: 'crm.deal.userfield.list',
            params: {
                filter: {
                    LANG: 'ru', // Фильтр по языку для вывода названия поля
                    USER_TYPE_ID: 'double' // Фильтр по типу поля
                }
            },
            requestId: 'userfield-list'
        });

        if (!listResponse.isSuccess) {
            throw new Error(listResponse.getErrorMessages().join('; '))
        }

        // Ответ crm.deal.userfield.list приходит в UPPER_SNAKE
        const targetField = listResponse.getData().result
            .find(field => field.EDIT_FORM_LABEL === FIELD_LABEL);

        if (!targetField) {
            throw new Error('Поле с указанным названием не найдено')
        }

        // Шаг 2: обновляем настройки найденного поля
        const updateResponse = await $b24.actions.v2.call.make({
            method: 'userfieldconfig.update',
            params: {
                moduleId: 'crm', // Идентификатор модуля
                id: Number(targetField.ID), // ID найденного пользовательского поля
                field: {
                    settings: {
                        ...targetField.SETTINGS, // Переносим текущие настройки поля
                        PRECISION // Количество знаков после запятой
                    }
                }
            },
            requestId: 'userfieldconfig-update'
        });

        if (!updateResponse.isSuccess) {
            throw new Error(updateResponse.getErrorMessages().join('; '))
        }

        // Ответ userfieldconfig.update приходит в camelCase
        console.log('Точность поля:', updateResponse.getData().result.field.settings.PRECISION);
    } catch (error) {
        console.error(error);
    }
}

updateUserField();
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError


FIELD_LABEL = "Сумма к возврату"  # Название поля, которое нужно изменить
PRECISION = 3  # Количество знаков после запятой


def update_user_field(client, field_label: str, precision: int) -> None:
    try:
        # Шаг 1: получаем пользовательские поля сделок типа double
        fields = client.crm.deal.userfield.list(
            filter={
                "LANG": "ru",
                "USER_TYPE_ID": "double",
            }
        ).response.result

        # Ответ crm.deal.userfield.list приходит в UPPER_SNAKE
        target_field = next(
            (field for field in fields if field["EDIT_FORM_LABEL"] == field_label),
            None,
        )

        if target_field is None:
            raise RuntimeError("Поле с указанным названием не найдено")

        # Шаг 2: обновляем настройки найденного поля
        updated_field = client.userfieldconfig.update(
            module_id="crm",
            bitrix_id=int(target_field["ID"]),
            field={
                "settings": {
                    **target_field["SETTINGS"],  # Переносим текущие настройки поля
                    "PRECISION": precision,
                }
            },
        ).response.result["field"]
    except (BitrixAPIError, RuntimeError) as error:
        print(f"Ошибка: {error}")
    else:
        # Ответ userfieldconfig.update приходит в camelCase
        print("Точность поля:", updated_field["settings"]["PRECISION"])


client = Client(
    BitrixWebhook(
        domain="your-domain.bitrix24.com",
        webhook_token="user_id/webhook_key",
    )
)

update_user_field(client, FIELD_LABEL, PRECISION)
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';

use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Bitrix24\SDK\Services\ServiceBuilder;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Psr\Log\NullLogger;

$sb = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
    ->initFromWebhook('https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/');

const FIELD_LABEL = 'Сумма к возврату'; // Название поля, которое нужно изменить
const PRECISION = 3; // Количество знаков после запятой

function updateUserField(ServiceBuilder $sb, string $fieldLabel, int $precision): void {
    try {
        // Шаг 1: получаем пользовательские поля сделок типа double
        $fields = $sb->getCRMScope()->dealUserfield()->list(
            order: [],
            filter: [
                'LANG' => 'ru', // Фильтр по языку для вывода названия поля
                'USER_TYPE_ID' => 'double' // Фильтр по типу поля
            ]
        )->getUserfields();

        // Ответ crm.deal.userfield.list приходит в UPPER_SNAKE
        $targetField = null;
        foreach ($fields as $field) {
            if ($field->EDIT_FORM_LABEL === $fieldLabel) {
                $targetField = $field;
                break;
            }
        }

        if ($targetField === null) {
            throw new \RuntimeException('Поле с указанным названием не найдено');
        }

        // Шаг 2: обновляем настройки найденного поля
        // у userfieldconfig.update нет типизированной обертки в SDK — вызываем метод через ядро
        $updatedField = $sb->core->call(
            'userfieldconfig.update',
            [
                'moduleId' => 'crm', // Идентификатор модуля
                'id' => (int)$targetField->ID, // ID найденного пользовательского поля
                'field' => [
                    'settings' => array_merge(
                        (array)$targetField->SETTINGS, // Переносим текущие настройки поля
                        ['PRECISION' => $precision] // Количество знаков после запятой
                    )
                ]
            ]
        )->getResponseData()->getResult()['field'];

        // Ответ userfieldconfig.update приходит в camelCase
        echo 'Точность поля: ' . $updatedField['settings']['PRECISION'];
    } catch (\Throwable $e) {
        echo 'Ошибка: ' . $e->getMessage();
    }
}

updateUserField($sb, FIELD_LABEL, PRECISION);
// Подготовка в пустом каталоге — go get без go mod init не сработает:
//
//	go mod init example && go get github.com/bitrix24/b24gosdk
//
// Запуск:
//
//	export B24_WEBHOOK_URL='https://ваш-портал.bitrix24.ru/rest/1/токен/' && go run .
//
// Пример самодостаточный: он создаёт поле сделки с точностью 2, находит его по
// названию, меняет точность на 3 и удаляет поле за собой. Второй сценарий
// страницы требует уже существующего поля — пример готовит его первым
// сценарием, поэтому запускается на любом портале, ничего править не нужно.
package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"os"

	b24 "github.com/bitrix24/b24gosdk"
)

const (
	fieldName  = "UF_CRM_DEAL_NEW_DOUBLE_FIELD"
	fieldLabel = "Число с округлением"
)

func main() {
	if err := run(context.Background()); err != nil {
		log.Fatal(err)
	}
}

func run(ctx context.Context) error {
	// Путь вебхука — это секрет, поэтому он приходит из окружения, а не из кода.
	// Клиент строится один раз на портал: он держит HTTP-клиент и состояние
	// авторизации.
	core := b24.NewClient(os.Getenv("B24_WEBHOOK_URL")).Core()
	// --- сценарий 1: создаём поле сразу с точностью
	res, err := core.Call(ctx, "userfieldconfig.add", b24.Params{
		"moduleId": "crm",
		"field": b24.Params{
			"entityId":   "CRM_DEAL",
			"fieldName":  fieldName,
			"userTypeId": "double",
			"editFormLabel": b24.Params{
				"ru": fieldLabel,
				"en": "PRECISION double",
			},
			// Параметр необязательный, но для типа «Число» его лучше
			// передавать: без него точность будет 0 и значения округлятся
			// до целых.
			"settings": b24.Params{"PRECISION": 3},
		},
	})
	if err != nil {
		// Код ошибки сравнивается через errors.Is, а не строкой: опечатка в
		// литерале скомпилируется и молча уведёт в другую ветку.
		if errors.Is(err, b24.ErrAccessDenied) {
			return fmt.Errorf("нужно право «Разрешить изменять настройки» в CRM: %w", err)
		}
		return fmt.Errorf("userfieldconfig.add: %w", err)
	}

	// Метод заворачивает ответ в объект с ключом field и отвечает в camelCase:
	// точность лежит в settings.PRECISION, а не в SETTINGS.PRECISION.
	var added struct {
		Field struct {
			ID       b24.ID         `json:"id"`
			Settings map[string]any `json:"settings"`
		} `json:"field"`
	}
	if err := json.Unmarshal(res.Result, &added); err != nil {
		return fmt.Errorf("разбор созданного поля: %w", err)
	}
	defer del(ctx, core, "userfieldconfig.delete", b24.Params{
		"moduleId": "crm", "id": added.Field.ID,
	})
	fmt.Printf("поле %d создано, PRECISION=%v\n", added.Field.ID, added.Field.Settings["PRECISION"])

	// --- сценарий 2, шаг 1: находим поле по названию
	// Без фильтра LANG названия не возвращаются вовсе, и найти поле по названию
	// не получится.
	res, err = core.Call(ctx, "crm.deal.userfield.list", b24.Params{
		"filter": b24.Params{"LANG": "ru", "USER_TYPE_ID": "double"},
	}, b24.WithIdempotent())
	if err != nil {
		return fmt.Errorf("crm.deal.userfield.list: %w", err)
	}

	// А этот метод отвечает в UPPER_SNAKE — те же данные, другой регистр.
	// Ключи внутри самих настроек в обоих случаях в верхнем регистре.
	var fields []struct {
		ID            b24.ID         `json:"ID"`
		FieldName     string         `json:"FIELD_NAME"`
		EditFormLabel string         `json:"EDIT_FORM_LABEL"`
		Settings      map[string]any `json:"SETTINGS"`
	}
	if err := json.Unmarshal(res.Result, &fields); err != nil {
		return fmt.Errorf("разбор пользовательских полей: %w", err)
	}

	target := -1
	for i, f := range fields {
		if f.EditFormLabel == fieldLabel {
			target = i
			break
		}
	}
	if target < 0 {
		return fmt.Errorf("поле %q не найдено", fieldLabel)
	}
	fmt.Printf("нашли поле %d (%s), сейчас PRECISION=%v\n",
		fields[target].ID, fields[target].FieldName, fields[target].Settings["PRECISION"])

	// --- сценарий 2, шаг 2: меняем точность
	// settings ЗАМЕНЯЮТСЯ целиком, а не дописываются. Если передать один
	// PRECISION, остальные настройки типа «Число» сбросятся на значения по
	// умолчанию — поэтому берём настройки из шага 1 и правим в них одну.
	settings := fields[target].Settings
	settings["PRECISION"] = 5

	res, err = core.Call(ctx, "userfieldconfig.update", b24.Params{
		"moduleId": "crm",
		"id":       fields[target].ID,
		"field":    b24.Params{"settings": settings},
	})
	if err != nil {
		return fmt.Errorf("userfieldconfig.update: %w", err)
	}

	var updated struct {
		Field struct {
			ID       b24.ID         `json:"id"`
			Settings map[string]any `json:"settings"`
		} `json:"field"`
	}
	if err := json.Unmarshal(res.Result, &updated); err != nil {
		return fmt.Errorf("разбор изменённого поля: %w", err)
	}
	fmt.Printf("поле %d: PRECISION=%v, остальные настройки сохранены: %v\n",
		updated.Field.ID, updated.Field.Settings["PRECISION"], updated.Field.Settings)
	return nil
}

// del убирает созданное. Ошибку уборки печатаем, но не возвращаем: она не
// должна подменить собой настоящую ошибку сценария.
func del(ctx context.Context, core *b24.Core, method string, params b24.Params) {
	if _, err := core.Call(ctx, method, params); err != nil {
		fmt.Fprintf(os.Stderr, "уборка, %s: %v\n", method, err)
	}
}

Проверим результат

Сценарий выполнен верно, если в ответе метода:

  • userfieldconfig.add есть field.id, а в field.settings.PRECISION стоит переданная точность
  • userfieldconfig.update в field.settings.PRECISION стоит новая точность, а field.id совпадает с идентификатором из шага 1

Актуальные настройки поля в любой момент возвращает метод userfieldconfig.get с параметрами moduleId: crm и id поля. Он отдает данные в том же camelCase, что add и update.

Проверить округление на данных можно так: откройте карточку сделки — поле отображается с названием из editFormLabel. Введите значение с большим числом знаков после запятой, чем задано в PRECISION, и сохраните сделку. Через REST то же самое делают методы crm.deal.update и crm.deal.get: запишите в поле 1,23456 и прочитайте его — при PRECISION: 3 вернется 1.235.

Ошибки и диагностика

Если метод вернул ошибку, проверьте данные запроса.

Ошибка

Причина и действие

Вы не можете создавать пользовательские поля

У пользователя нет права «Разрешить изменять настройки» в CRM, либо в field[entityId] передан объект, к настройкам которого нет доступа, либо тип поля из field[userTypeId] запрещено создавать через REST. Проверьте, от имени какого пользователя создан вебхук

Некорректный код поля

field[fieldName] не начинается с UF_ и идентификатора объекта из field[entityId]. Для CRM_DEAL код должен начинаться с UF_CRM_DEAL_. Префикс сверяется с учетом регистра, поэтому uf_crm_deal_... не подойдет. Эта же ошибка приходит, если field[fieldName] вовсе не передан. Если префикс верный, но в коде есть строчные буквы, кириллица или символы вне A-Z, 0-9 и _, придет Имя поля содержит недопустимые символы..., а если код длиннее 50 символов — Имя поля слишком длинное...

Поле #FIELD_NAME# для объекта #ENTITY_ID# уже существует.

Поле с таким field[fieldName] у объекта уже есть, вместо #FIELD_NAME# и #ENTITY_ID# подставляются переданные значения. Создавать поле заново не нужно — перейдите ко второму сценарию и измените точность у существующего поля

Вы не можете изменить настройки пользьовательского поля

Недостаточно прав на изменение поля. Эта же ошибка приходит, если поле с переданным id удалено или принадлежит другому модулю, чем указан в moduleId. Опечатка в слове «пользьовательского» — на стороне Битрикс24, ищите ошибку по этой строке как есть

The current method required more scopes. (crm)

У вебхука или приложения нет scope модуля из moduleId. Для CRM нужны оба scope: userfieldconfig и crm

Access denied.

Ошибка crm.deal.userfield.list: у пользователя нет права на чтение сделок

Метод может отработать без ошибки, но результат окажется не тем, что ожидали.

  • в ответе crm.deal.userfield.list нет названий полей — в filter не передан LANG. Без него подписи не возвращаются вовсе, и поиск поля по названию не сработает
  • поле в ответе не нашлось — проверьте USER_TYPE_ID: у типа «Число» это double, а не integer и не money
  • точность изменилась, а другие настройки поля сбросились — в field[settings] был передан только PRECISION. Повторите шаг 2, передав настройки из шага 1 целиком
  • значение в поле не округлилось — округление применяется при сохранении значения, а не при изменении настройки. Уже сохраненные значения не пересчитываются

Шаги второго сценария повторяются независимо: шаг 1 ничего не меняет, его можно выполнять сколько угодно раз. Если ошибку вернул шаг 2, настройка не изменилась — исправьте параметры и повторите только его.

Что важно учитывать

  • PRECISION принимает целое число от 0 до 12. Значения вне диапазона Битрикс24 не отклоняет, а приводит к границе: отрицательное станет 0, больше 12 — 12
  • значение поля принимается и с точкой, и с запятой, пробелы удаляются
  • MIN_VALUE и MAX_VALUE, равные 0, означают, что ограничения нет
  • userfieldconfig.update не меняет entityId, fieldName, userTypeId и multiple — эти параметры игнорируются. Чтобы изменить их, удалите поле методом userfieldconfig.delete и создайте заново
  • языковые подписи update перезаписывает так же, как настройки: если передать field[editFormLabel] хотя бы с одним языком, подписи на остальных языках удалятся. В примерах этой страницы подписи не передаются, поэтому они сохраняются
  • методы userfieldconfig.* работают не только с CRM. Для полей другого модуля меняются moduleId и entityId, а точность у типа double задается той же настройкой PRECISION
  • если нужен один регистр полей на весь сценарий, вместо crm.deal.userfield.list подойдет userfieldconfig.list: он возвращает camelCase, как add и update. У него другие правила вызова: обязателен moduleId, список полей приходит в result.fields, а не в корне result, и названия полей вернутся только если передать в select ключ language — это аналог фильтра LANG из шага 1

Продолжите изучение