How to Create an Edit Deal Form
Scope:
crmWho can execute the method: users with administrative access to the CRM section
If you are developing integrations for Bitrix24 using AI tools (Codex, Claude Code, Cursor), connect the MCP server so that the assistant can utilize the official REST documentation.
Example of automatically generating an edit deal form with all fields created in Bitrix24 on the application page.
Some field types are not implemented in this example; for fields with unsupported types, the message field not support will be displayed.
Generated form code:
JS
PHP
Python
import express from 'express'
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
const app = express()
app.use(express.urlencoded({ extended: true }))
function input(p) {
const count = p.MULTIPLE && p.TYPE !== 'file' ? 2 : 0
const values = Array.isArray(p.VALUE) ? p.VALUE : []
let html = ''
for (let i = 0; i <= count; i++) {
const value = count > 0 ? (values[i] ?? '') : (p.VALUE ?? '')
html += '<input class="form-control' + (p.TYPE === 'file' ? '-file' : '') + '"'
if (p.NAME) html += ` name="${p.NAME}${p.MULTIPLE ? '[]' : ''}"`
if (p.TYPE) html += ` type="${p.TYPE}"`
if (p.REQUIRED) html += ' required'
if (p.DISABLE) html += ' disabled'
if (p.CHECKED) html += ' checked'
if (p.MULTIPLE) html += ' multiple'
if (value) html += ` value="${value}"`
html += '>'
}
return html
}
function select(p, list) {
if (!list || !Object.keys(list).length) return ''
let html = '<select class="form-control"'
if (p.NAME) html += ` name="${p.NAME}${p.MULTIPLE ? '[]' : ''}"`
if (p.REQUIRED) html += ' required'
if (p.DISABLE) html += ' disabled'
if (p.MULTIPLE) html += ' multiple'
html += '>'
const value = Array.isArray(p.VALUE) ? p.VALUE.map(String) : [String(p.VALUE ?? '')]
for (const [key, title] of Object.entries(list)) {
html += `<option value="${key}"${value.includes(String(key)) ? ' selected' : ''}>${title}</option>`
}
return html + '</select>'
}
app.get('/', async (req, res) => {
const id = parseInt(String(req.query.ID ?? '0'), 10) || 0
const arResult = {}
arResult.FIELDS = (await $b24.actions.v2.call.make({
method: 'crm.deal.fields', params: {}, requestId: 'deal-fields',
})).getData().result
if (arResult.FIELDS.SOURCE_ID) {
arResult.FIELD_VALUES_SOURCE_ID = (await $b24.actions.v2.call.make({
method: 'crm.status.list', params: { filter: { ENTITY_ID: arResult.FIELDS.SOURCE_ID.statusType } }, requestId: 'src',
})).getData().result
}
if (arResult.FIELDS.STAGE_ID) {
arResult.FIELD_VALUES_STAGE_ID = (await $b24.actions.v2.call.make({
method: 'crm.status.list', params: { filter: { ENTITY_ID: arResult.FIELDS.STAGE_ID.statusType } }, requestId: 'stage',
})).getData().result
}
arResult.FIELD_VALUES_CURRENCY = (await $b24.actions.v2.call.make({
method: 'crm.currency.list', params: {}, requestId: 'cur',
})).getData().result
if (id > 0) {
arResult.ITEM = (await $b24.actions.v2.call.make({
method: 'crm.deal.get', params: { id }, requestId: 'item',
})).getData().result
// Deal pipelines (directions) — for the CATEGORY_ID field
arResult.VALUE_CATEGORY_ID = (await $b24.actions.v2.call.make({
method: 'crm.dealcategory.list', params: { filter: { IS_LOCKED: 'N' } }, requestId: 'categories',
})).getData().result
// Deal quotes — for the read-only QUOTE_ID field
arResult.VALUE_QUOTE_ID = (await $b24.actions.v2.call.make({
method: 'crm.quote.list', params: { filter: { DEAL_ID: id }, select: ['ID', 'TITLE'] }, requestId: 'quotes',
})).getData().result
if (arResult.ITEM.LEAD_ID && arResult.ITEM.LEAD_ID !== '0') {
arResult.VALUE_LEAD_ID = (await $b24.actions.v2.call.make({
method: 'crm.lead.get', params: { id: arResult.ITEM.LEAD_ID }, requestId: 'lead',
})).getData().result
}
}
let sResult = ''
let sResultCustom = ''
// CONTACT_ID/CONTACT_IDS are deprecated — use crm.deal.contact.items.get
delete arResult.FIELDS.CONTACT_ID
delete arResult.FIELDS.CONTACT_IDS
for (const [key, field] of Object.entries(arResult.FIELDS)) {
let value = arResult.ITEM?.[key] ?? ''
let listKey = arResult['FIELD_VALUES_' + key] ?? []
let list = {}
let ret = ''
const params = { NAME: `form[${key}]`, REQUIRED: field.isRequired, DISABLE: field.isReadOnly, MULTIPLE: field.isMultiple, VALUE: value }
switch (field.type) {
case 'crm_status':
if (!listKey.length) {
listKey = (await $b24.actions.v2.call.make({
method: 'crm.status.list', params: { filter: { ENTITY_ID: field.statusType } }, requestId: `st-${key}`,
})).getData().result
}
list = Object.fromEntries(listKey.map((s) => [s.STATUS_ID, s.NAME]))
ret = select(params, list)
break
case 'crm_currency':
list = Object.fromEntries(arResult.FIELD_VALUES_CURRENCY.map((c) => [c.CURRENCY, c.FULL_NAME]))
ret = select(params, list)
break
case 'enumeration':
if (field.items) list = Object.fromEntries(field.items.map((it) => [it.ID, it.VALUE]))
ret = select(params, list)
break
case 'crm_multifield':
if (Array.isArray(value) && value.length) value = value[0].VALUE
ret = input({ ...params, MULTIPLE: false, VALUE: value, TYPE: 'text' })
break
case 'crm_category': {
const cats = Object.fromEntries((arResult.VALUE_CATEGORY_ID ?? []).map((c) => [c.ID, c.NAME]))
ret = select(params, cats)
break
}
case 'crm_quote':
// only for QUOTE_ID — read-only, we show the quote names
if ((arResult.VALUE_QUOTE_ID ?? []).length) {
ret += (arResult.VALUE_QUOTE_ID).map((q) => q.TITLE).join(', ')
}
break
case 'crm_lead':
ret = input({ ...params, TYPE: 'text' })
if (arResult.VALUE_LEAD_ID && String(value) === String(arResult.VALUE_LEAD_ID.ID)) {
ret += `(${arResult.VALUE_LEAD_ID.TITLE})`
}
break
case 'crm_company': {
ret = input({ ...params, TYPE: 'text' })
if (value) {
const companies = (await $b24.actions.v2.call.make({
method: 'crm.company.list', params: { filter: { ID: value }, select: ['ID', 'TITLE'] }, requestId: `co-${key}`,
})).getData().result
if (companies.length) ret += '(' + companies.map((c) => c.TITLE).join(', ') + ')'
}
break
}
case 'crm_contact': {
ret = input({ ...params, TYPE: 'text' })
if (value) {
const contacts = (await $b24.actions.v2.call.make({
method: 'crm.contact.list', params: { filter: { ID: value }, select: ['ID', 'NAME', 'LAST_NAME'] }, requestId: `ct-${key}`,
})).getData().result
if (contacts.length) ret += '(' + contacts.map((c) => [c.NAME, c.LAST_NAME].join(' ')).join(', ') + ')'
}
break
}
case 'date':
if (value) value = String(value).slice(0, 10)
ret = input({ ...params, VALUE: value, TYPE: 'date' })
break
case 'datetime':
if (value) value = String(value).slice(0, 19)
ret = input({ ...params, VALUE: value, TYPE: 'datetime-local' })
break
case 'char':
ret = input({ ...params, VALUE: 'Y', CHECKED: value === 'Y', TYPE: 'checkbox' })
break
case 'boolean':
ret = input({ ...params, VALUE: '1', CHECKED: value === 'Y', TYPE: 'checkbox' })
break
case 'double':
case 'integer':
ret = input({ ...params, TYPE: 'number' })
break
case 'user': {
ret = input({ ...params, TYPE: 'number' })
if (value) {
const users = (await $b24.actions.v2.call.make({
method: 'user.get', params: { filter: { ID: value } }, requestId: `user-${key}`,
})).getData().result
if (users.length) {
ret += '(' + users.map((u) => [u.NAME, u.LAST_NAME].join(' ')).join(', ') + ')'
}
}
break
}
case 'money': {
const [money, currency] = String(value).split('|')
ret = input({ ...params, VALUE: money, TYPE: 'number' })
list = Object.fromEntries(arResult.FIELD_VALUES_CURRENCY.map((c) => [c.CURRENCY, c.FULL_NAME]))
ret += select({ NAME: `${key}_CURRENCY`, REQUIRED: field.isRequired, DISABLE: field.isReadOnly, MULTIPLE: field.isMultiple, VALUE: currency }, list)
break
}
case 'resourcebooking':
ret = 'field not support'
break
default:
ret = input({ ...params, TYPE: 'text' })
break
}
const label = field.formLabel || field.title
const block = `<div class="col-4 mt-3">${label}: </div><div class="col-6 mt-3">${ret}</div>`
if (key.startsWith('UF_')) sResultCustom += block
else sResult += block
}
res.send(`
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
<div class="container">
<form id="auto_form" enctype="multipart/form-data" method="post">
${arResult.ITEM?.ID ? `<input type="hidden" name="form[ID]" value="${arResult.ITEM.ID}">` : ''}
<h2>Standard fields</h2>
<div class="row">${sResult}</div>
<h2>Custom fields</h2>
<div class="row">${sResultCustom}</div>
<div class="row"><div class="col-sm-10 mt-5">
<input type="submit" class="btn btn-primary" value="Submit">
</div></div>
</form>
</div>
<script>
document.getElementById('auto_form').addEventListener('submit', async (el) => {
el.preventDefault()
const response = await fetch('/form', { method: 'POST', body: new FormData(el.currentTarget) })
const json = await response.json()
if (json.message) alert(json.message)
})
<\/script>
`)
})
app.listen(3000)
<?php
// 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.com/rest/USER_ID/TOKEN/');
$crm = $sb->getCRMScope();
$ID = intVal($_REQUEST['ID']);
class CPrintForm
{
/**
* @return string html select
* @var $arParams array params input keys: 'NAME', 'ID', 'TYPE', 'REQUIRED', 'CHECKED', 'DISABLE', 'MULTIPLE', 'VALUE'
*/
public static function input($arParams)
{
$count = 0;
$i = 0;
$sResult = '';
if ($arParams['MULTIPLE'] && $arParams['TYPE'] != 'file')
{
$count = 2;
}
$value = $arParams['VALUE'];
while ($i <= $count)
{
if ($count > 0)
{
$value = $arParams['VALUE'][$i];
}
$sResult .= '<input class="form-control' . (($arParams['TYPE'] == 'file') ? '-file' : '') . '"';
if (!empty($arParams['ID']))
{
$sResult .= ' id="' . $arParams['ID'] . '"';
}
if (!empty($arParams['NAME']))
{
$sResult .= ' name="' . $arParams['NAME'] . '' . (($arParams['MULTIPLE']) ? '[]' : '') . '"';
}
if (!empty($arParams['TYPE']))
{
$sResult .= ' type="' . $arParams['TYPE'] . '"';
}
if (!empty($arParams['REQUIRED']))
{
$sResult .= ' required';
}
if (!empty($arParams['DISABLE']))
{
$sResult .= ' disabled';
}
if (!empty($arParams['CHECKED']))
{
$sResult .= ' checked';
}
if (!empty($arParams['MULTIPLE']))
{//sometimes work, not for standard type="text"
$sResult .= ' multiple';
}
if (!empty($arParams['VALUE']))
{
$sResult .= ' value="' . $value . '"';
}
$sResult .= '>';
$i++;
}
return $sResult;
}
/**
* @return string html select
* @var $arList array of select options where key is value option and value is title
* @var $arParams array settings of select params keys: 'NAME', 'ID', 'REQUIRED', 'DISABLE','MULTIPLE', 'VALUE'
*/
public static function select($arParams, $arList)
{
$sResult = '';
if (!empty($arList) && is_array($arList))
{
$sResult .= '<select class="form-control"' .
(($arParams['NAME']) ? ' name="' .
$arParams['NAME'] .
'' .
(($arParams['MULTIPLE']) ? '[]' : '') .
'"' : '') .
(($arParams['ID']) ? ' id="' . $arParams['ID'] . '"' : '') .
(($arParams['REQUIRED']) ? ' required' : '') .
(($arParams['DISABLE']) ? ' disabled' : '') .
(($arParams['MULTIPLE']) ? ' multiple' : '') .
'>';
$value = [];
if (is_array($arParams['VALUE']))
{
$value = $arParams['VALUE'];
}
else
{
$value[] = ($arParams['VALUE']) ? $arParams['VALUE'] : '';
}
foreach ($arList as $key => $title)
{
$sResult .= '<option value="' .
$key .
'" ' .
((in_array($key, $value)) ? ' selected' : '') .
'>' .
$title .
'</option>';
}
$sResult .= '</select>';
}
return $sResult;
}
}
// Field descriptions and dictionary values (sequential calls).
// Dictionaries are immediately converted into "code => label" maps for select.
$arResult = [];
$arResult['FIELDS'] = $crm->deal()->fields()->getFieldsDescription();
$arResult['FIELD_VALUES_SOURCE_ID'] = [];
if (isset($arResult['FIELDS']['SOURCE_ID']))
{
foreach ($crm->status()->list([], ['ENTITY_ID' => $arResult['FIELDS']['SOURCE_ID']['statusType']], [])->getStatuses() as $status)
{
$arResult['FIELD_VALUES_SOURCE_ID'][$status->STATUS_ID] = $status->NAME;
}
}
$arResult['FIELD_VALUES_STAGE_ID'] = [];
if (isset($arResult['FIELDS']['STAGE_ID']))
{
foreach ($crm->status()->list([], ['ENTITY_ID' => $arResult['FIELDS']['STAGE_ID']['statusType']], [])->getStatuses() as $status)
{
$arResult['FIELD_VALUES_STAGE_ID'][$status->STATUS_ID] = $status->NAME;
}
}
$arResult['FIELD_VALUES_CURRENCY'] = [];
foreach ($crm->currency()->list([])->getCurrencies() as $currency)
{
// CURRENCY in B24PhpSDK — Money\Currency object, the string code is retrieved via getCode()
$arResult['FIELD_VALUES_CURRENCY'][$currency->CURRENCY->getCode()] = $currency->FULL_NAME;
}
if ($ID > 0)
{//get item and standard enum field values if is update form
// The element is read as an array — the set of fields is dynamic (including UF_*).
$arResult['ITEM'] = iterator_to_array($crm->deal()->get($ID)->deal()->getIterator());
// Deal pipelines (directions) — for the CATEGORY_ID field
$arResult['VALUE_CATEGORY_ID'] = [];
foreach ($crm->dealCategory()->list([], ['IS_LOCKED' => 'N'], [], 0)->getDealCategories() as $category)
{
$arResult['VALUE_CATEGORY_ID'][] = ['ID' => $category->ID, 'NAME' => $category->NAME];
}
// Deal quotes — for the read-only QUOTE_ID field
$arResult['VALUE_QUOTE_ID'] = [];
foreach ($crm->quote()->list([], ['DEAL_ID' => $ID], ['ID', 'TITLE'], 0)->getQuotes() as $quote)
{
$arResult['VALUE_QUOTE_ID'][] = ['TITLE' => $quote->TITLE];
}
if (!empty($arResult['ITEM']['LEAD_ID']))
{
$lead = $crm->lead()->get((int)$arResult['ITEM']['LEAD_ID'])->lead();
$arResult['VALUE_LEAD_ID'] = ['ID' => $lead->ID, 'TITLE' => $lead->TITLE];
}
}
$sResult = '';
$sResultCustom = '';
if (is_array($arResult['FIELDS'])):
if (isset($arResult['FIELDS']['CONTACT_ID']))//deprecated use crm.deal.contact.items.get
{
unset($arResult['FIELDS']['CONTACT_ID']);
}
if (isset($arResult['FIELDS']['CONTACT_IDS']))//use crm.deal.contact.items.get
{
unset($arResult['FIELDS']['CONTACT_IDS']);
}
foreach ($arResult['FIELDS'] as $key => $arField)
{
$value = '';
$return = '';
if (!empty($arResult['ITEM'][$key]))
{
$value = $arResult['ITEM'][$key];
}
$arList = (isset($arResult['FIELD_VALUES_' . $key])) ? $arResult['FIELD_VALUES_' . $key] : [];
switch ($arField['type'])
{
case 'crm_status':
if (empty($arList))
{
foreach ($crm->status()->list([], ['ENTITY_ID' => $arField['statusType']], [])->getStatuses() as $status)
{
$arList[$status->STATUS_ID] = $status->NAME;
}
}
$return = CPrintForm::select(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value
],
$arList
);
break;
case 'crm_currency':
$return = CPrintForm::select(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value
],
$arResult['FIELD_VALUES_CURRENCY']
);
break;
case 'enumeration':
if (!empty($arField['items']))
{
$arList = array_column($arField['items'], 'VALUE', 'ID');
}
$return = CPrintForm::select(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value
],
$arList
);
break;
case 'crm_multifield'://its simple example: need multifield, check data type and more...
if (!empty($value) && is_array($value))
{
$value = reset($value)['VALUE'];
}
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => false,
'VALUE' => $value,
'TYPE' => 'text',
]
);
break;
case 'crm_category':
$arList = [];
if (!empty($arResult['VALUE_' . $key]))
{
foreach ($arResult['VALUE_' . $key] as $category)
{
$arList[$category['ID']] = $category['NAME'];
}
}
$return = CPrintForm::select(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value
],
$arList
);
break;
case 'crm_quote'://only for QUOTE_ID read only type
if (!empty($arResult['VALUE_QUOTE_ID']))
{
$return .= implode(', ', array_column($arResult['VALUE_QUOTE_ID'], 'TITLE'));
}
break;
case 'crm_lead':
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'text',
]
);
if (!empty($arResult['VALUE_LEAD_ID']) && $value == $arResult['VALUE_LEAD_ID']['ID'])
{
$return .= '(' . $arResult['VALUE_LEAD_ID']['TITLE'] . ')';
}
break;
case 'crm_company':
// The relation provides an identifier — we will fetch the name via crm.company.list
$arCompany = [];
if (!empty($value))
{
foreach ($crm->company()->list([], ['ID' => $value], ['ID', 'TITLE'], 0)->getCompanies() as $company)
{
$arCompany[] = $company->TITLE;
}
}
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'text',
]
);
if (!empty($arCompany))
{
$return .= '(' . implode(', ', $arCompany) . ')';
}
break;
case 'crm_contact':
// The relation provides an identifier — we will fetch the names via crm.contact.list
$arContact = [];
if (!empty($value))
{
foreach ($crm->contact()->list([], ['ID' => $value], ['ID', 'NAME', 'LAST_NAME'], 0)->getContacts() as $contact)
{
$arContact[] = implode(' ', [$contact->NAME, $contact->LAST_NAME]);
}
}
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'text',
]
);
if (!empty($arContact))
{
$return .= '(' . implode(', ', $arContact) . ')';
}
break;
case 'file':
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'file',
]
);
if ($arField['isMultiple'])
{
if (is_array($value))
{
foreach ($value as $k => $val)
{
if (!empty($val['downloadUrl']))
{
$return .= '<br/><a href="' . $val['downloadUrl'] . '">old file ' . $k . '</a>';
}
}
}
}
else
{
if (!empty($value['downloadUrl']))
{
$return .= '<br/><a href="' . $value['downloadUrl'] . '">old file</a>';
}
}
break;
case 'date':
if (!empty($value))
{
$value = date('Y-m-d', strtotime($value));
}
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'date',
]
);
break;
case 'datetime':
if (!empty($value))
{
$value = date('Y-m-d\TH:i:s', strtotime($value));
}
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'datetime-local',
]
);
break;
case 'char':
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => 'Y',
'CHECKED' => ($value == 'Y') ? true : false,
'TYPE' => 'checkbox',
]
);
break;
case 'boolean':
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => '1',
'CHECKED' => ($value == 'Y') ? true : false,
'TYPE' => 'checkbox',
]
);
break;
case 'double':
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'number'
]
);
break;
case 'user':
$arUserNames = [];
if (!empty($value))
{
foreach ($sb->getUserScope()->user()->get([], ['ID' => $value], true)->getUsers() as $user)
{
$arUserNames[] = implode(' ', [$user->NAME, $user->LAST_NAME]);
}
}
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'number'
]
);
if (!empty($arUserNames))
{
$return .= '(' . implode(', ', $arUserNames) . ')';
}
break;
case 'url':
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'text',
]
);
break;
case 'integer':
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'number',
]
);
break;
case 'money':
list($money, $currency) = explode('|', $value);
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $money,
'TYPE' => 'number',
]
);
$return .= CPrintForm::select(
[
'NAME' => $key . '_CURRENCY',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $currency
],
$arResult['FIELD_VALUES_CURRENCY']
);
break;
case 'address':
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'text',
]
);
break;
case 'resourcebooking':
//some code booking
$return = 'field not support';
break;
default:
$return = CPrintForm::input(
[
'NAME' => 'form[' . $key . ']',
'REQUIRED' => $arField['isRequired'],
'DISABLE' => $arField['isReadOnly'],
'MULTIPLE' => $arField['isMultiple'],
'VALUE' => $value,
'TYPE' => 'text',
]
);
break;
}
if (strpos($key, 'UF_') === 0)
{
$sResultCustom .= '<div class="col-4 mt-3">' . (($arField['formLabel']) ? $arField['formLabel'] : $arField['title']) . ': ' . '</div>';
$sResultCustom .= '<div class="col-6 mt-3">' . $return . '</div>';
}
else
{
$sResult .= '<div class="col-4 mt-3">' . (($arField['formLabel']) ? $arField['formLabel'] : $arField['title']) . ': ' . '</div>';
$sResult .= '<div class="col-6 mt-3">' . $return . '</div>';
}
}
?>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
$(document).ready(function ()
{
$('#auto_form').on('submit', function (el) {//event submit form
el.preventDefault();//the default action of the event will not be triggered
var formData = new FormData(this);
var xhr = new XMLHttpRequest();
xhr.open("POST", 'auto_form.php');
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
if (this.status >= 200 && this.status < 400) {
var resp = this.responseText;
try {
var json = JSON.parse(resp);
if (typeof json.message !== 'undefined') {
alert(json.message);
}
} catch (e) {
return false;
}
} else {
alert('error');
}
}
};
xhr.send(formData);
});
});
</script>
<div class="container">
<form id="auto_form" action="" enctype="multipart/form-data" method="post">
<?php if (!empty($arResult['ITEM']['ID']))://for update entity ?>
<input type="hidden" name="form[ID]" value="<?= $arResult['ITEM']['ID'] ?>">
<?php endif; ?>
<h2>Standard fields</h2>
<div class="row">
<?= $sResult ?>
</div>
<h2>Custom fields</h2>
<div class="row">
<?= $sResultCustom ?>
</div>
<div class="row">
<div class="col-sm-10 mt-5">
<input type="submit" class="btn btn-primary" value="Submit">
</div>
</div>
</form>
</div>
<?php endif; ?>
# pip install b24pysdk flask
from html import escape
from flask import Flask, request, jsonify
from b24pysdk import BitrixWebhook, Client
app = Flask(__name__)
token = BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="USER_ID/TOKEN", # user_id/token only, without https://
)
client = Client(token)
def input_field(params):
"""Builds <input> (or a set for a multiple field)."""
count = 2 if params.get("MULTIPLE") and params.get("TYPE") != "file" else 0
values = params.get("VALUE") if isinstance(params.get("VALUE"), list) else []
html = ""
for i in range(count + 1):
value = (values[i] if i < len(values) else "") if count > 0 else params.get("VALUE")
html += '<input class="form-control' + ("-file" if params.get("TYPE") == "file" else "") + '"'
if params.get("NAME"):
html += f' name="{params["NAME"]}{"[]" if params.get("MULTIPLE") else ""}"'
if params.get("TYPE"):
html += f' type="{params["TYPE"]}"'
if params.get("REQUIRED"):
html += " required"
if params.get("DISABLE"):
html += " disabled"
if params.get("CHECKED"):
html += " checked"
if params.get("MULTIPLE"):
html += " multiple"
if value:
html += f' value="{value}"'
html += ">"
return html
def select_field(params, options):
"""Builds <select> based on a \"value => label\" dictionary."""
if not options:
return ""
html = '<select class="form-control"'
if params.get("NAME"):
html += f' name="{params["NAME"]}{"[]" if params.get("MULTIPLE") else ""}"'
if params.get("REQUIRED"):
html += " required"
if params.get("DISABLE"):
html += " disabled"
if params.get("MULTIPLE"):
html += " multiple"
html += ">"
value = params.get("VALUE")
value = [str(v) for v in value] if isinstance(value, list) else [str(value or "")]
for key, title in options.items():
selected = " selected" if str(key) in value else ""
html += f'<option value="{key}"{selected}>{title}</option>'
return html + "</select>"
PAGE = """
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
<div class="container">
<form id="auto_form" enctype="multipart/form-data" method="post">
%(hidden_id)s
<h2>Standard fields</h2>
<div class="row">%(standard)s</div>
<h2>Custom fields</h2>
<div class="row">%(custom)s</div>
<div class="row"><div class="col-sm-10 mt-5">
<input type="submit" class="btn btn-primary" value="Submit">
</div></div>
</form>
</div>
<script>
document.getElementById('auto_form').addEventListener('submit', async (el) => {
el.preventDefault();
const response = await fetch('/form', { method: 'POST', body: new FormData(el.currentTarget) });
const json = await response.json();
if (json.message) alert(json.message);
});
</script>
"""
@app.route("/")
def form_page():
item_id = int(request.args.get("ID", 0) or 0)
ar_result = {}
ar_result["FIELDS"] = client.crm.deal.fields().result
if "SOURCE_ID" in ar_result["FIELDS"]:
ar_result["FIELD_VALUES_SOURCE_ID"] = client.crm.status.list(
filter={"ENTITY_ID": ar_result["FIELDS"]["SOURCE_ID"]["statusType"]}).result
if "STAGE_ID" in ar_result["FIELDS"]:
ar_result["FIELD_VALUES_STAGE_ID"] = client.crm.status.list(
filter={"ENTITY_ID": ar_result["FIELDS"]["STAGE_ID"]["statusType"]}).result
ar_result["FIELD_VALUES_CURRENCY"] = client.crm.currency.list().result
if item_id > 0:
ar_result["ITEM"] = client.crm.deal.get(bitrix_id=item_id).result
# Deal pipelines (directions) — for the CATEGORY_ID field.
# The crm.dealcategory.list method does not have an SDK wrapper yet — call it directly.
category_filter = {"IS_LOCKED": "N"}
ar_result["VALUE_CATEGORY_ID"] = token.call_method(
"crm.dealcategory.list", {"filter": category_filter})["result"]
# Deal quotes — for the read-only QUOTE_ID field
ar_result["VALUE_QUOTE_ID"] = client.crm.quote.list(
filter={"DEAL_ID": item_id}, select=["ID", "TITLE"]).result
if ar_result["ITEM"].get("LEAD_ID") and ar_result["ITEM"]["LEAD_ID"] != "0":
ar_result["VALUE_LEAD_ID"] = client.crm.lead.get(
bitrix_id=int(ar_result["ITEM"]["LEAD_ID"])).result
item = ar_result.get("ITEM", {})
# CONTACT_ID/CONTACT_IDS are deprecated — use crm.deal.contact.items.get
ar_result["FIELDS"].pop("CONTACT_ID", None)
ar_result["FIELDS"].pop("CONTACT_IDS", None)
s_result = ""
s_result_custom = ""
for key, field in ar_result["FIELDS"].items():
value = item.get(key) or ""
params = {"NAME": f"form[{key}]", "REQUIRED": field.get("isRequired"),
"DISABLE": field.get("isReadOnly"), "MULTIPLE": field.get("isMultiple"), "VALUE": value}
ret = ""
field_type = field.get("type")
if field_type == "crm_status":
rows = ar_result.get("FIELD_VALUES_" + key) or client.crm.status.list(
filter={"ENTITY_ID": field["statusType"]}).result
ret = select_field(params, {r["STATUS_ID"]: r["NAME"] for r in rows})
elif field_type == "crm_currency":
options = {c["CURRENCY"]: c["FULL_NAME"] for c in ar_result["FIELD_VALUES_CURRENCY"]}
ret = select_field(params, options)
elif field_type == "enumeration":
options = {it["ID"]: it["VALUE"] for it in field.get("items", [])}
ret = select_field(params, options)
elif field_type == "crm_multifield":
if isinstance(value, list) and value:
value = value[0]["VALUE"]
ret = input_field({**params, "MULTIPLE": False, "VALUE": value, "TYPE": "text"})
elif field_type == "crm_category":
cats = {c["ID"]: c["NAME"] for c in ar_result.get("VALUE_CATEGORY_ID", [])}
ret = select_field(params, cats)
elif field_type == "crm_quote":
# only for QUOTE_ID — read-only, we show the quote names
quotes = ar_result.get("VALUE_QUOTE_ID", [])
if quotes:
ret += ", ".join(q["TITLE"] for q in quotes)
elif field_type == "crm_lead":
ret = input_field({**params, "TYPE": "text"})
lead = ar_result.get("VALUE_LEAD_ID")
if lead and str(value) == str(lead["ID"]):
ret += f"({lead['TITLE']})"
elif field_type == "crm_company":
ret = input_field({**params, "TYPE": "text"})
if value:
companies = client.crm.company.list(
filter={"ID": value}, select=["ID", "TITLE"]).result
if companies:
ret += "(" + ", ".join(c["TITLE"] for c in companies) + ")"
elif field_type == "crm_contact":
ret = input_field({**params, "TYPE": "text"})
if value:
contacts = client.crm.contact.list(
filter={"ID": value}, select=["ID", "NAME", "LAST_NAME"]).result
if contacts:
ret += "(" + ", ".join(f"{c['NAME']} {c['LAST_NAME']}" for c in contacts) + ")"
elif field_type == "date":
if value:
value = str(value)[:10]
ret = input_field({**params, "VALUE": value, "TYPE": "date"})
elif field_type == "datetime":
if value:
value = str(value)[:19]
ret = input_field({**params, "VALUE": value, "TYPE": "datetime-local"})
elif field_type == "char":
ret = input_field({**params, "VALUE": "Y", "CHECKED": value == "Y", "TYPE": "checkbox"})
elif field_type == "boolean":
ret = input_field({**params, "VALUE": "1", "CHECKED": value == "Y", "TYPE": "checkbox"})
elif field_type in ("double", "integer"):
ret = input_field({**params, "TYPE": "number"})
elif field_type == "user":
ret = input_field({**params, "TYPE": "number"})
if value:
users = client.user.get(filter={"ID": value}).result
if users:
ret += "(" + ", ".join(f"{u['NAME']} {u['LAST_NAME']}" for u in users) + ")"
elif field_type == "money":
money, _, currency = str(value).partition("|")
ret = input_field({**params, "VALUE": money, "TYPE": "number"})
options = {c["CURRENCY"]: c["FULL_NAME"] for c in ar_result["FIELD_VALUES_CURRENCY"]}
ret += select_field({"NAME": f"{key}_CURRENCY", "REQUIRED": field.get("isRequired"),
"DISABLE": field.get("isReadOnly"), "MULTIPLE": field.get("isMultiple"),
"VALUE": currency}, options)
elif field_type == "resourcebooking":
ret = "field not support"
else:
ret = input_field({**params, "TYPE": "text"})
label = field.get("formLabel") or field.get("title")
block = f'<div class="col-4 mt-3">{label}: </div><div class="col-6 mt-3">{ret}</div>'
if key.startswith("UF_"):
s_result_custom += block
else:
s_result += block
# The hidden ID field is added only for the edit form
item_id_value = item.get("ID")
hidden_id = ""
if item_id_value:
hidden_id = f'<input type="hidden" name="form[ID]" value="{escape(str(item_id_value))}">'
return PAGE % {"hidden_id": hidden_id, "standard": s_result, "custom": s_result_custom}
if __name__ == "__main__":
app.run(port=5000)
Handler code that saves the form:
JS
PHP
Python
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
// Handler: saves the form — updates an existing lead or creates a new one.
// req.body.form — an object with form fields (Express with express.urlencoded()).
export async function handler(req, res) {
const arForm = { ...req.body.form }
// Convert values to the REST format according to the field type
const arFields = (await $b24.actions.v2.call.make({
method: 'crm.deal.fields', params: {}, requestId: 'deal-fields-save',
})).getData().result
for (const [key, prop] of Object.entries(arFields)) {
if (!(key in arForm)) {
// Fields that are not in the form are cleared (except for read-only and files)
if (!prop.isReadOnly && prop.type !== 'file') {
if (prop.type === 'enumeration' && prop.isMultiple) arForm[key] = [false]
else if (prop.isMultiple) arForm[key] = []
else arForm[key] = ''
}
continue
}
if (prop.type === 'crm_multifield') {
arForm[key] = [{ VALUE: arForm[key] }]
} else if (prop.type === 'money') {
arForm[key] = [arForm[key], arForm[`${key}_CURRENCY`]].join('|')
delete arForm[`${key}_CURRENCY`]
}
}
const id = parseInt(String(arForm.ID ?? '0'), 10) || 0
delete arForm.ID
if (id > 0) {
const result = await $b24.actions.v2.call.make({
method: 'crm.deal.update', params: { id, fields: arForm }, requestId: 'deal-update',
})
res.json({ message: result.getData().result ? 'Deal update' : 'Deal not updated' })
} else {
const result = await $b24.actions.v2.call.make({
method: 'crm.deal.add', params: { fields: arForm }, requestId: 'deal-add',
})
const newId = result.getData().result
res.json({ message: newId ? `Deal add ID:${newId}` : 'Deal not added' })
}
}
<?php
// 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.com/rest/USER_ID/TOKEN/');
$crm = $sb->getCRMScope();
$arForm = [];
foreach($_POST['form'] as $key => $item)
{
if(is_array($item))
{
$arForm[$key] = [];
foreach($item as $k => $val)
{
$arForm[$key][$k] = htmlspecialchars($val);
}
}
else
{
$arForm[$key] = htmlspecialchars($item);
}
}
//make array multiple files for add to custom field
if(!empty($_FILES['form']['tmp_name']) && is_array($_FILES['form']['tmp_name']))
{
foreach($_FILES['form']['tmp_name'] as $key => $files)
{
if(is_array($files))
{
foreach($files as $k => $file)
{
$arForm[$key][$k] = [
"fileData" => [
$_FILES['form']['name'][$key][$k],
base64_encode(file_get_contents($file))
]
];
}
}
else
{
$arForm[$key] = [
"fileData" => [
$_FILES['form']['name'][$key],
base64_encode(file_get_contents($files))
]
];
}
}
}
$arFields = $crm->deal()->fields()->getFieldsDescription();
if(!empty($arFields))
{
foreach($arFields as $key => $prop)
{
if(!isset($arForm[$key]))
{
if(!$prop['isReadOnly'] && $prop['type'] != 'file')
{
if($prop['type'] == 'enumeration' && $prop['isMultiple'])
{
//if type multiple enumeration to clean selected value need send: [false]
$arForm[$key] = [false];
}
elseif($prop['isMultiple'])
{
$arForm[$key] = [];
}
else
{
$arForm[$key] = '';
}
}
continue;
}
//here may be any check field example by type
if($prop['type'] == 'crm_multifield')
{
if(isset($arForm[$key]))
{
$arForm[$key] = [['VALUE' => $arForm[$key]]];
}
}
elseif($prop['type'] == 'money')
{
$arForm[$key] = implode('|', [$arForm[$key], $arForm[$key . '_CURRENCY']]);
unset($arForm[$key . '_CURRENCY']);
}
}
}
$arForm['ID'] = intVal($arForm['ID']);
if($arForm['ID'] > 0)
{
$id = $arForm['ID'];
unset($arForm['ID']);
$result = $crm->deal()->update($id, $arForm)->isSuccess();
$arMess = [
'success' => 'Deal update',
'error' => 'Deal not updated',
];
echo json_encode(['message' => $result ? $arMess['success'] : $arMess['error']]);
}
else
{
unset($arForm['ID']);
$resultId = $crm->deal()->add($arForm)->getId();
$arMess = [
'success' => 'Deal add',
'error' => 'Deal not added',
];
echo json_encode(['message' => $resultId ? $arMess['success'] . ' ID:' . $resultId : $arMess['error']]);
}
# pip install b24pysdk flask
from flask import Flask, request, jsonify
from b24pysdk import BitrixWebhook, Client
app = Flask(__name__)
client = Client(BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="USER_ID/TOKEN", # user_id/token only, without https://
))
@app.route("/form", methods=["POST"])
def save_form():
ar_form = {}
for full_key in request.form:
if full_key.startswith("form[") and full_key.endswith("]"):
ar_form[full_key[5:-1]] = request.form[full_key]
ar_fields = client.crm.deal.fields().result
for key, prop in ar_fields.items():
if key not in ar_form:
if not prop.get("isReadOnly") and prop.get("type") != "file":
if prop.get("type") == "enumeration" and prop.get("isMultiple"):
ar_form[key] = [False]
elif prop.get("isMultiple"):
ar_form[key] = []
else:
ar_form[key] = ""
continue
if prop.get("type") == "crm_multifield":
ar_form[key] = [{"VALUE": ar_form[key]}]
elif prop.get("type") == "money":
ar_form[key] = "|".join([ar_form[key], ar_form.get(f"{key}_CURRENCY", "")])
ar_form.pop(f"{key}_CURRENCY", None)
item_id = int(ar_form.get("ID") or 0)
ar_form.pop("ID", None)
if item_id > 0:
result = client.crm.deal.update(bitrix_id=item_id, fields=ar_form).result
message = "Deal update" if result else "Deal not updated"
else:
new_id = client.crm.deal.add(fields=ar_form).result
message = f"Deal add ID:{new_id}" if new_id else "Deal not added"
return jsonify(message=message)
if __name__ == "__main__":
app.run(port=5000)