How to Create an Edit Deal Form

Scope: crm

Who 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:

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:

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)