diff --git a/formula_editor/api.py b/formula_editor/api.py index 5558f56..5a0a36a 100644 --- a/formula_editor/api.py +++ b/formula_editor/api.py @@ -1,24 +1,46 @@ import frappe - @frappe.whitelist() def get_doctype_fields(doctype): meta = frappe.get_meta(doctype) fields = [] - # Получаем все int-поля for field in meta.fields: if field.fieldtype in ["Int", "Float", "Currency"]: fields.append({"fieldname": field.fieldname, "label": field.label}) - - # Проверяем таблицы внутри DocType (например, `items` в `Sales Invoice`) + # Проверяем таблицы внутри DocType (например, items в Sales Invoice) for child in meta.fields: if child.fieldtype == "Table": child_meta = frappe.get_meta(child.options) for sub_field in child_meta.fields: if sub_field.fieldtype in ["Int", "Float", "Currency"]: fields.append({ - "fieldname": f"{child.fieldname}_{sub_field.fieldname}", + "fieldname": f"{child.fieldname}{sub_field.fieldname}", "label": f"{child.label} → {sub_field.label}" }) - return fields + +@frappe.whitelist() +def get_all_doctype_fields(doctype): + """Получение отфильтрованных полей DocType без служебных полей""" + meta = frappe.get_meta(doctype) + fields = [] + + # Типы полей, которые нужно исключить + excluded_types = [ + "Tab Break", "Column Break", "Button", "Section Break", "Column Break", + "HTML", "Table", "Attach", "Attach Image", "Code", "Text Editor", "Image", + "Read Only", "Heading", "Fold", "Table MultiSelect" + ] + + # Получаем все поля, которые не входят в исключенные типы + for field in meta.fields: + if field.fieldname and field.fieldtype not in excluded_types: + fields.append({ + "fieldname": field.fieldname, + "label": field.label or field.fieldname, + "fieldtype": field.fieldtype + }) + + # Мы не добавляем поля из таблиц, так как они были исключены из списка + + return fields \ No newline at end of file diff --git a/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js b/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js index cc445d4..0db2667 100644 --- a/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js +++ b/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js @@ -1,34 +1,39 @@ frappe.ui.form.on('Formula Editor', { -onload_post_render: function(frm) { - if (!frm.is_initialized) { - frm.is_initialized = true; - console.log("Initializing Formula Editor"); - - if (frm.doc.target_doctype) { - generate_dynamic_fields(frm, frm.doc.target_doctype, function() { - if (frm.doc.formula_storage) { - restore_formulas(frm); - } - }); + onload_post_render: function(frm) { + // Проверяем наличие поля для хранения универсальных полей + if (!frm.doc.universal_fields_storage) { + frm.set_value("universal_fields_storage", "{}"); } - } -}, - -target_doctype: function(frm) { - generate_dynamic_fields(frm, frm.doc.target_doctype, function() { - restore_formulas(frm); - }); -}, - -before_save: function(frm) { - console.log("📝 `before_save` вызван. Проверяем сохраненные формулы..."); - save_formulas(frm); + + if (!frm.is_initialized) { + frm.is_initialized = true; + console.log("Initializing Formula Editor"); - console.log("🔄 `formula_storage` перед сохранением документа:", frm.doc.formula_storage); - generate_client_script(frm); -} + if (frm.doc.target_doctype) { + generate_dynamic_fields(frm, frm.doc.target_doctype, function() { + if (frm.doc.formula_storage) { + restore_formulas(frm); + } + }); + } + } + }, + + target_doctype: function(frm) { + generate_dynamic_fields(frm, frm.doc.target_doctype, function() { + restore_formulas(frm); + }); + }, + + before_save: function(frm) { + console.log("📝 `before_save` вызван. Проверяем сохраненные формулы..."); + save_formulas(frm); + + console.log("🔄 `formula_storage` перед сохранением документа:", frm.doc.formula_storage); + generate_client_script(frm); + } }); - + function restore_formulas(frm) { let stored_formulas = JSON.parse(frm.doc.formula_storage || "{}"); console.log("🔄 Восстанавливаем формулы:", stored_formulas); @@ -86,227 +91,261 @@ frm.set_value("formula_storage", JSON.stringify(formula_data)); } function generate_client_script(frm) { -if (!frm.doc.target_doctype) { - console.error("❌ Target Doctype отсутствует!"); - return; -} - -let target_doctype = frm.doc.target_doctype; -let formula_data = JSON.parse(frm.doc.formula_storage || "{}"); - -let main_script = `frappe.ui.form.on("${target_doctype}", {\n`; -let functions = []; -let table_child_scripts = {}; - -// Справочник соответствия таблиц и их DocType -let table_doctypes = {}; - -// Сначала соберем информацию о табличных полях и их DocType -frappe.call({ - method: "frappe.client.get", - args: { - doctype: "DocType", - name: target_doctype - }, - async: false, // Синхронный вызов для гарантии получения данных - callback: function(r) { - if (r.message && r.message.child_tables) { - r.message.child_tables.forEach(table => { - if (table.options) { - // Сохраняем соответствие имени поля таблицы в родительском DocType и DocType самой таблицы - table_doctypes[table.fieldname] = table.options; - } - }); - } - } -}); - -// Если данные о таблицах не получены, пробуем альтернативный способ через meta -if (Object.keys(table_doctypes).length === 0) { - const meta = frappe.get_meta(target_doctype); - if (meta && meta.fields) { - meta.fields.forEach(field => { - if (field.fieldtype === 'Table' && field.options) { - table_doctypes[field.fieldname] = field.options; - } - }); - } -} - -console.log("📊 Соответствие таблиц и DocType:", table_doctypes); - -// Сбор всех табличных частей и их полей -let child_tables = {}; - -Object.keys(formula_data).forEach(fieldname => { - let field_info = formula_data[fieldname]; - - if (field_info && field_info.is_table_field) { - // Извлекаем имя табличной части из fieldname (формат: tablename_fieldname) - let parts = fieldname.split('_'); - if (parts.length >= 2) { - let table_name = parts[0]; - if (!child_tables[table_name]) { - child_tables[table_name] = []; - } - child_tables[table_name].push(fieldname); - } - } -}); - -Object.keys(formula_data).forEach(fieldname => { - let field_info = formula_data[fieldname]; - - if (!field_info || typeof field_info !== "object" || !field_info.formula) { - console.warn(`⚠️ Пропущено поле ${fieldname}, так как оно не содержит формулу.`); + if (!frm.doc.target_doctype) { + console.error("❌ Target Doctype отсутствует!"); return; } - let formula = field_info.formula.trim(); - let triggers = field_info.triggers ? field_info.triggers.split(',').map(t => t.trim()) : []; - let is_table_field = field_info.is_table_field; + let target_doctype = frm.doc.target_doctype; + let formula_data = JSON.parse(frm.doc.formula_storage || "{}"); + let universal_fields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - function transform_expression(expression, for_child_table = false, table_name = null) { - console.log("🔄 Исходная формула:", expression, "для табличной части:", for_child_table, "таблица:", table_name); + let main_script = `frappe.ui.form.on("${target_doctype}", {\n`; + let functions = []; + let table_child_scripts = {}; + + // Справочник соответствия таблиц и их DocType + let table_doctypes = {}; + + // Сначала соберем информацию о табличных полях и их DocType + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "DocType", + name: target_doctype + }, + async: false, // Синхронный вызов для гарантии получения данных + callback: function(r) { + if (r.message && r.message.child_tables) { + r.message.child_tables.forEach(table => { + if (table.options) { + // Сохраняем соответствие имени поля таблицы в родительском DocType и DocType самой таблицы + table_doctypes[table.fieldname] = table.options; + } + }); + } + } + }); + + // Если данные о таблицах не получены, пробуем альтернативный способ через meta + if (Object.keys(table_doctypes).length === 0) { + const meta = frappe.get_meta(target_doctype); + if (meta && meta.fields) { + meta.fields.forEach(field => { + if (field.fieldtype === 'Table' && field.options) { + table_doctypes[field.fieldname] = field.options; + } + }); + } + } + + console.log("📊 Соответствие таблиц и DocType:", table_doctypes); + + // Сбор всех табличных частей и их полей + let child_tables = {}; + + Object.keys(formula_data).forEach(fieldname => { + let field_info = formula_data[fieldname]; + if (field_info && field_info.is_table_field) { + // Извлекаем имя табличной части из fieldname (формат: tablename_fieldname) + let parts = fieldname.split('_'); + if (parts.length >= 2) { + let table_name = parts[0]; + if (!child_tables[table_name]) { + child_tables[table_name] = []; + } + child_tables[table_name].push(fieldname); + } + } + }); + + // Добавляем функцию для загрузки универсальных полей + functions.push(` + // Функция для расчета универсальных полей + async function calculate_universal_fields(frm) { + ${Object.keys(universal_fields).map(fieldId => { + const field = universal_fields[fieldId]; + if (!field || !field.name || !field.doctype || !field.fieldname) { + console.warn(`⚠️ Некорректное универсальное поле с ID ${fieldId}:`, field); + return ''; + } + return ` + // Расчет универсального поля: ${field.name} + try { + let univ_${fieldId} = 0; + const filters_${fieldId} = ${JSON.stringify((field.filters || []).reduce((obj, filter) => { + if (filter && filter.field && filter.operator) { + obj[filter.field] = [filter.operator, filter.value || '']; + } + return obj; + }, {}))}; + + const result_${fieldId} = await frappe.db.get_list("${field.doctype}", { + fields: ["${field.fieldname}"], + filters: filters_${fieldId}, + limit: 1000 + }); + + if (result_${fieldId} && result_${fieldId}.length > 0) { + result_${fieldId}.forEach(r => { + univ_${fieldId} += (parseFloat(r.${field.fieldname}) || 0); + }); + } + + // Сохраняем результат в временное поле документа + if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; + frm.doc.__universalFields["${fieldId}"] = univ_${fieldId}; + + console.log("✅ Универсальное поле ${field.name} рассчитано:", univ_${fieldId}); + } catch (error) { + console.error("❌ Ошибка при расчете универсального поля ${field.name}:", error); + if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; + frm.doc.__universalFields["${fieldId}"] = 0; + }`; + }).filter(code => code !== '').join('\n')} + } + `); + + // Добавляем вызов функции расчета универсальных полей при загрузке формы + main_script += ` "refresh": async function(frm) { + await calculate_universal_fields(frm); + ${Object.keys(formula_data).map(fieldname => { + return `calculate_${fieldname}(frm);`; + }).join('\n ')} + },\n`; + + // Добавляем обработку универсальных полей в функцию transform_expression +// Функция для преобразования выражений формул в generate_client_script +functions.push(` + // Функция для преобразования выражений формул + function transform_expression(expression, for_child_table = false, table_name = null) { let transformed = expression; - // Если мы трансформируем выражение для табличной части - if (for_child_table) { - // Заменяем val() на row() для всех полей внутри табличной части - transformed = transformed.replace(/val\((\w+)\)/g, (match, field) => { - // Проверяем, если имя поля содержит префикс таблицы - if (field.startsWith(table_name + '_')) { - // Извлекаем имя поля без префикса - const actualFieldName = field.substring(table_name.length + 1); - return `row["${actualFieldName}"]`; - } - // Если поле не из текущей таблицы, используем frm.doc - return `frm.doc["${field}"]`; + // Обработка универсальных полей + ${Object.keys(universal_fields).map(fieldId => { + const field = universal_fields[fieldId]; + if (!field || !field.name) { + return ''; + } + return `transformed = transformed.replace(/univ\\(${field.name}\\)/g, "(frm.doc.__universalFields && frm.doc.__universalFields['${fieldId}'] || 0)");`; + }).filter(code => code !== '').join('\n ')} + + // Функция суммирования по табличной части - обрабатываем ее первой + transformed = transformed.replace(/sum\\((\\w+)[_]?(\\w+)\\)/g, (match, tbl, fld) => { + return \`(frm.doc["\${tbl}"] && frm.doc["\${tbl}"].reduce((sum, row) => sum + (parseFloat(row["\${fld}"]) || 0), 0) || 0)\`; + }); + + // Остальной код... + } +`); + + Object.keys(formula_data).forEach(fieldname => { + let field_info = formula_data[fieldname]; + + if (!field_info || typeof field_info !== "object" || !field_info.formula) { + console.warn(`⚠️ Пропущено поле ${fieldname}, так как оно не содержит формулу.`); + return; + } + + let formula = field_info.formula.trim(); + let triggers = field_info.triggers ? field_info.triggers.split(',').map(t => t.trim()) : []; + let is_table_field = field_info.is_table_field; + + let modified_formula = `transform_expression("${formula}", ${is_table_field})`; + + if (is_table_field) { + // Определяем имя табличной части из fieldname + let parts = fieldname.split('_'); + let table_name = parts[0]; + let field_name_in_table = parts.slice(1).join('_'); + + // Создаем или дополняем обработчики для конкретной табличной части + if (!table_child_scripts[table_name]) { + table_child_scripts[table_name] = { + doctype: table_doctypes[table_name] || `${target_doctype} Item`, // По умолчанию используем Parent DocType + Item + calculations: [], + fields: new Set() + }; + } + + // Добавляем функцию расчета для текущего поля + table_child_scripts[table_name].calculations.push({ + field: field_name_in_table, + formula: `transform_expression("${formula}", true, "${table_name}")` }); - - // Обрабатываем row() как обычно для табличной части - transformed = transformed.replace(/row\((\w+)\)/g, (match, field) => { - return `row["${field}"]`; + + // Добавляем триггеры + triggers.forEach(trigger => { + table_child_scripts[table_name].fields.add(trigger); }); } else { - // Стандартная замена для обычных полей - transformed = transformed.replace(/val\((\w+)\)/g, (match, field) => { - return `frm.doc["${field}"]`; - }); - - // Предупреждение, если row() используется вне таблицы - transformed = transformed.replace(/row\((\w+)\)/g, (match, field) => { - console.warn("⚠️ Функция row() используется вне контекста табличной части"); - return `0 /* ERROR: row() вне табличной части */`; + // Обычная функция расчета для поля основного документа + functions.push(` + function calculate_${fieldname}(frm) { + try { + console.log("📊 Вычисление формулы для ${fieldname}: ${formula}"); + let formula_expr = ${modified_formula}; + console.log("📊 Преобразованное выражение: " + formula_expr); + let result = eval(formula_expr); + console.log("📊 Результат: " + result); + frm.set_value("${fieldname}", result); + } catch (e) { + console.error("❌ Ошибка в формуле для ${fieldname}: ${formula}", e); + } + }`); + + // Добавляем триггеры для основного документа + triggers.forEach(trigger => { + main_script += ` "${trigger}": function(frm) { calculate_${fieldname}(frm); },\n`; }); } + }); + + main_script += `});`; + + // Добавляем обработчики для табличных частей + Object.keys(table_child_scripts).forEach(table_name => { + let childDoctype = table_child_scripts[table_name].doctype; + let table_script = `\n\nfrappe.ui.form.on("${childDoctype}", {\n`; - // Функция суммирования по табличной части применяется в обоих случаях - transformed = transformed.replace(/sum\((\w+)_(\w+)\)/g, (match, tbl, fld) => { - return `(frm.doc["${tbl}"].reduce((sum, row) => sum + (row["${fld}"] || 0), 0))`; + // Создаем общую функцию для расчета формул в строке таблицы + let calcFunction = ` + function calculate_${table_name}_row(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + + try {`; + + // Добавляем расчеты для каждого поля табличной части с правильным форматированием + table_child_scripts[table_name].calculations.forEach(calc => { + calcFunction += ` + // Расчет для поля ${calc.field} + row["${calc.field}"] = eval(${calc.formula}); + `; }); - // Обработка процентов - transformed = transformed.replace(/(frm\.doc\["\w+"\]|\d+|row\["\w+"\])\s*([+\-])\s*(\d+(\.\d+)?)%/g, (match, base, operator, percent) => { - console.log("✅ Обнаружен процент в выражении:", match); - return `${base} ${operator} ((${base} * ${percent}) / 100)`; - }); - - console.log("🔄 Трансформированная формула:", transformed); - return transformed; - } - - let modified_formula = transform_expression(formula, is_table_field); - - if (is_table_field) { - // Определяем имя табличной части из fieldname - let parts = fieldname.split('_'); - let table_name = parts[0]; - let field_name_in_table = parts.slice(1).join('_'); - - // Создаем или дополняем обработчики для конкретной табличной части - if (!table_child_scripts[table_name]) { - table_child_scripts[table_name] = { - doctype: table_doctypes[table_name] || `${target_doctype} Item`, // По умолчанию используем Parent DocType + Item - calculations: [], - fields: new Set() - }; - } - - // Добавляем функцию расчета для текущего поля - table_child_scripts[table_name].calculations.push({ - field: field_name_in_table, - formula: transform_expression(formula, true, table_name) - }); - - // Добавляем триггеры - triggers.forEach(trigger => { - table_child_scripts[table_name].fields.add(trigger); - }); - } else { - // Обычная функция расчета для поля основного документа - functions.push(` -function calculate_${fieldname}(frm) { -try { - let result = ${modified_formula}; - frm.set_value("${fieldname}", result); -} catch (e) { - console.error("❌ Ошибка в формуле для ${fieldname}: ${formula}", e); -} -}`); - - // Добавляем триггеры для основного документа - triggers.forEach(trigger => { - main_script += ` "${trigger}": function(frm) { calculate_${fieldname}(frm); },\n`; - }); - } -}); - -main_script += `});`; - -// Добавляем обработчики для табличных частей -Object.keys(table_child_scripts).forEach(table_name => { - let childDoctype = table_child_scripts[table_name].doctype; - let table_script = `\n\nfrappe.ui.form.on("${childDoctype}", {\n`; - - // Создаем общую функцию для расчета формул в строке таблицы - let calcFunction = ` -function calculate_${table_name}_row(frm, cdt, cdn) { -let row = locals[cdt][cdn]; - -try {`; - - // Добавляем расчеты для каждого поля табличной части с правильным форматированием - table_child_scripts[table_name].calculations.forEach(calc => { calcFunction += ` - // Расчет для поля ${calc.field} - row["${calc.field}"] = ${calc.formula}; -`; + frm.refresh_field("${table_name}"); + } catch (e) { + console.error("❌ Ошибка в формуле для таблицы ${table_name}:", e); + } + }`; + + functions.push(calcFunction); + + // Добавляем обработчики для каждого триггерного поля + Array.from(table_child_scripts[table_name].fields).forEach(field => { + table_script += ` "${field}": function(frm, cdt, cdn) { calculate_${table_name}_row(frm, cdt, cdn); },\n`; + }); + + table_script += `});`; + main_script += table_script; }); - - calcFunction += ` - frm.refresh_field("${table_name}"); -} catch (e) { - console.error("❌ Ошибка в формуле для таблицы ${table_name}:", e); -} -}`; - functions.push(calcFunction); + let final_script = `${main_script}\n\n${functions.join("\n\n")}`; + console.log("✅ **Сгенерированный Client Script:**\n", final_script); - // Добавляем обработчики для каждого триггерного поля - Array.from(table_child_scripts[table_name].fields).forEach(field => { - table_script += ` "${field}": function(frm, cdt, cdn) { calculate_${table_name}_row(frm, cdt, cdn); },\n`; - }); - - table_script += `});`; - main_script += table_script; -}); - -let final_script = `${main_script}\n\n${functions.join("\n\n")}`; -console.log("✅ **Сгенерированный Client Script:**\n", final_script); - -update_or_create_client_script(target_doctype, final_script); + update_or_create_client_script(target_doctype, final_script); } function update_or_create_client_script(target_doctype, script_code) { @@ -416,6 +455,9 @@ function generate_dynamic_fields(frm, doctype, callback) { // Создаем модальное окно редактора полей изменения createTriggerFieldsModal(); + + // Создаем модальное окно универсальных полей + createUniversalFieldsModal(); Object.keys(grouped_fields).forEach(group => { let section_name = group === "Основные поля" ? group : `${group} (${group.replace(/\s+/g, '_').toLowerCase()})`; @@ -456,7 +498,18 @@ function generate_dynamic_fields(frm, doctype, callback) { container.append(section); }); + // Добавляем секцию универсальных полей + addUniversalFieldsSection(wrapper); + + // Добавляем основной контейнер формул wrapper.append(container); + + // Инициализируем универсальные поля + initUniversalFields(frm); + + // Отрисовываем существующие универсальные поля + renderUniversalFields(frm); + attachFormulaBuilderEvents(frm, fields, grouped_fields); attachTriggerEditorEvents(frm, fields, grouped_fields); frm.refresh(); @@ -586,6 +639,11 @@ const modalStyles = ` background-color: #f9f9f9; } + .universal-field { + background-color: #e3efff !important; + border: 1px solid #abc9f5 !important; + } + .fields-title { font-weight: bold; margin-bottom: 5px; @@ -715,286 +773,301 @@ $(frm.wrapper).on('click', '.open-formula-builder', function() { } function openFormulaBuilder(frm, targetFieldname, isTableField, currentFormula, fields, grouped_fields) { -const $modal = $("#formula_builder_modal"); -const $resultContainer = $modal.find(".formula-result"); + const $modal = $("#formula_builder_modal"); + const $resultContainer = $modal.find(".formula-result"); -// Очистка предыдущих обработчиков и данных -$modal.off('click', '.field-btn'); -$resultContainer.empty(); -$modal.find(".field-groups-container").empty(); + // Очистка предыдущих обработчиков и данных + $modal.off('click', '.field-btn'); + $resultContainer.empty(); + $modal.find(".field-groups-container").empty(); -// Заполняем текущую формулу, если она есть -if (currentFormula) { - parseAndRenderFormula($resultContainer, currentFormula, isTableField); -} + // Заполняем текущую формулу, если она есть + if (currentFormula) { + parseAndRenderFormula($resultContainer, currentFormula, isTableField); + } -// Заполняем панель с полями -populateFieldsPanel($modal, frm, grouped_fields, isTableField); + // Заполняем панель с полями + populateFieldsPanel($modal, frm, grouped_fields, isTableField); -// Сохраняем данные о текущем поле -$modal.data("target-field", targetFieldname); -$modal.data("is-table-field", isTableField); + // Сохраняем данные о текущем поле + $modal.data("target-field", targetFieldname); + $modal.data("is-table-field", isTableField); -// Функция добавления токена в формулу -function addTokenToFormula(token, type) { -if (type === 'field') { - let formulaText; + function addTokenToFormula(token, type) { + if (type === 'field') { + let formulaText; + + // Получаем группу из кнопки поля + const $fieldBtn = $modal.find(`.field-btn[data-fieldname="${token}"]`); + const group = $fieldBtn.data('group'); + + // Определяем тип поля и формулу + const fieldInfo = determineFieldType(token, group, isTableField); + + if (fieldInfo.type === 'row') { + formulaText = `row(${fieldInfo.fieldName})`; + } else if (fieldInfo.type === 'sum') { + // Если это таблица без подчеркивания (например, itemsqty) + let tableName = fieldInfo.tableName || token.match(/^([a-z_]+)/)[1]; + let fieldWithoutPrefix = token.substring(tableName.length); + if (fieldWithoutPrefix.startsWith('_')) { + fieldWithoutPrefix = fieldWithoutPrefix.substring(1); + } + + formulaText = `sum(${tableName}_${fieldWithoutPrefix})`; + } else { + formulaText = `val(${fieldInfo.fieldName})`; + } + + const $token = $(`${formulaText}×`); + $resultContainer.append($token).append(' '); + } else if (type === 'operator') { + const $token = $(`${token}×`); + $resultContainer.append($token).append(' '); + } else if (type === 'universal-field') { + // Обработка для универсальных полей + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + const field = universalFields[token]; + if (field) { + const formulaText = `univ(${field.name})`; + const $token = $(`${formulaText}×`); + $resultContainer.append($token).append(' '); + } + } - // Получаем группу из кнопки поля - const $fieldBtn = $modal.find(`.field-btn[data-fieldname="${token}"]`); - const group = $fieldBtn.data('group'); + // Переносим курсор в конец контейнера + setEndOfContentEditable($resultContainer[0]); - // Определяем тип поля и формулу - const fieldInfo = determineFieldType(token, group, isTableField); - - if (fieldInfo.type === 'row') { - formulaText = `row(${fieldInfo.fieldName})`; - } else if (fieldInfo.type === 'sum') { - formulaText = `sum(${fieldInfo.fieldName})`; - } else { - formulaText = `val(${fieldInfo.fieldName})`; + // Добавляем обработчики для удаления токенов + attachTokenEvents(); } - const $token = $(`${formulaText}×`); - $resultContainer.append($token).append(' '); -} else if (type === 'operator') { - const $token = $(`${token}×`); + // Обновленная функция прикрепления обработчиков событий + function attachTokenEvents() { + // Удаление токена + $resultContainer.find('.token-remove').off('click').on('click', function(e) { + e.stopPropagation(); + $(this).parent().remove(); + }); + + // Перетаскивание токенов + $resultContainer.find('.formula-token').each(function() { + $(this).attr('draggable', 'true'); + + $(this).off('dragstart').on('dragstart', function(e) { + console.log("Начало перетаскивания токена:", $(this).data('value')); + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'token', + value: $(this).data('value'), + html: $(this)[0].outerHTML + })); + window.draggedElement = this; + }); + + $(this).off('dragend').on('dragend', function() { + window.draggedElement = null; + }); + }); + } + + // Обработчики для кнопок операторов + $modal.find('.operator-btn').off('click').on('click', function() { + const operator = $(this).data('operator'); + addTokenToFormula(operator, 'operator'); + }); - $resultContainer.append($token).append(' '); -} + // Обработчик для кнопки применения формулы + $modal.find('.apply-formula').off('click').on('click', function() { + const formula = getFormulaText($resultContainer); + const $targetInput = $(frm.wrapper).find(`.formula-input[data-fieldname="${targetFieldname}"]`); + $targetInput.val(formula); + $modal.modal('hide'); + }); -// Переносим курсор в конец контейнера -setEndOfContentEditable($resultContainer[0]); - -// Добавляем обработчики для удаления токенов -attachTokenEvents(); -} - -// Обработчики событий для токенов формулы -function attachTokenEvents() { - // Удаление токена - $resultContainer.find('.token-remove').off('click').on('click', function(e) { - e.stopPropagation(); - $(this).parent().remove(); + // Обработчики для кнопок полей - используем делегирование событий + $modal.on('click', '.field-btn', function() { + const fieldname = $(this).data('fieldname'); + addTokenToFormula(fieldname, 'field'); }); - // Перетаскивание токенов + // Обработчики для кнопок универсальных полей + $modal.on('click', '.universal-field-btn', function() { + const fieldId = $(this).data('field-id'); + addTokenToFormula(fieldId, 'universal-field'); + }); + + // Показываем модальное окно + $modal.modal('show'); + + // Инициализируем перетаскивание + initDragAndDrop($modal); + + // Настраиваем редактируемый контейнер + setupEditableContainer($resultContainer); + + // Фокусируемся на редактируемом поле + $resultContainer.focus(); +} + +function populateFieldsPanel($modal, frm, grouped_fields, isTableField) { + const $container = $modal.find(".field-groups-container"); + + Object.keys(grouped_fields).forEach(group => { + const $fieldGroup = $(` +
+
${group}
+
+
+ `); + + const $buttonsContainer = $fieldGroup.find('.field-buttons-container'); + + grouped_fields[group].forEach(field => { + // Определяем, является ли это поле таблицы или нет + const isField = group === "Основные поля"; + const isThisTableField = !isField; + + // Создаем кнопку поля с правильными атрибутами для drag-and-drop + const $fieldBtn = $(` + + `); + + $buttonsContainer.append($fieldBtn); + }); + + $container.append($fieldGroup); + }); + + // Добавляем секцию универсальных полей + if (frm.doc.universal_fields_storage) { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + + if (Object.keys(universalFields).length > 0) { + const $universalFieldGroup = $(` +
+
Универсальные поля
+
+
+ `); + + const $buttonsContainer = $universalFieldGroup.find('.field-buttons-container'); + + Object.keys(universalFields).forEach(fieldId => { + const field = universalFields[fieldId]; + + const $fieldBtn = $(` + + `); + + $buttonsContainer.append($fieldBtn); + }); + + $container.append($universalFieldGroup); + + // Настраиваем перетаскивание универсальных полей + $modal.find('.universal-field-btn').on('dragstart', function(e) { + const fieldId = $(this).data('field-id'); + const fieldName = $(this).data('name'); + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'universal-field', + fieldId: fieldId, + name: fieldName + })); + }); + } + } +} + +function initDragAndDrop($modal) { + const $resultContainer = $modal.find(".formula-result"); + const isTableField = $modal.data('is-table-field'); + + // Очищаем предыдущие обработчики + $modal.find('.field-btn').off('dragstart'); + $modal.find('.operator-btn').off('dragstart'); + $resultContainer.off('dragover dragleave drop'); + + // Настраиваем перетаскивание существующих токенов $resultContainer.find('.formula-token').each(function() { $(this).attr('draggable', 'true'); $(this).off('dragstart').on('dragstart', function(e) { + // Сохраняем ссылку на текущий элемент + window.draggedElement = this; + + // Добавляем данные для переноса e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ type: 'token', value: $(this).data('value'), html: $(this)[0].outerHTML })); - window.draggedElement = this; + + // Добавляем класс перетаскивания + $(this).addClass('dragging'); + + return true; + }); + + // При завершении перетаскивания + $(this).off('dragend').on('dragend', function(e) { + $(this).removeClass('dragging'); + window.draggedElement = null; }); }); -} -// Обработчики для кнопок операторов -$modal.find('.operator-btn').off('click').on('click', function() { - const operator = $(this).data('operator'); - addTokenToFormula(operator, 'operator'); -}); - -// Обработчик для кнопки применения формулы -$modal.find('.apply-formula').off('click').on('click', function() { - const formula = getFormulaText($resultContainer); - const $targetInput = $(frm.wrapper).find(`.formula-input[data-fieldname="${targetFieldname}"]`); - $targetInput.val(formula); - $modal.modal('hide'); -}); - -// Обработчики для кнопок полей - используем делегирование событий -$modal.on('click', '.field-btn', function() { - const fieldname = $(this).data('fieldname'); - addTokenToFormula(fieldname, 'field'); -}); - -// Показываем модальное окно -$modal.modal('show'); - -// Инициализируем перетаскивание -initDragAndDrop($modal); - -// Настраиваем редактируемый контейнер -setupEditableContainer($resultContainer); - -// Фокусируемся на редактируемом поле -$resultContainer.focus(); -} - -function populateFieldsPanel($modal, frm, grouped_fields, isTableField) { -const $container = $modal.find(".field-groups-container"); - -Object.keys(grouped_fields).forEach(group => { - const $fieldGroup = $(` -
-
${group}
-
-
- `); - - const $buttonsContainer = $fieldGroup.find('.field-buttons-container'); - - grouped_fields[group].forEach(field => { - // Определяем, является ли это поле таблицы или нет - const isField = group === "Основные поля"; - const isThisTableField = !isField; + // Настраиваем перетаскивание полей из панели + $modal.find('.field-btn').on('dragstart', function(e) { + const fieldname = $(this).data('fieldname'); + const label = $(this).data('label'); + const group = $(this).data('group'); - // Создаем кнопку поля с правильными атрибутами для drag-and-drop - const $fieldBtn = $(` - - `); + // Проверяем принадлежность к группе + const isTableFieldGroup = group !== "Основные поля"; + const fieldData = { + type: 'field', + fieldname: fieldname, + label: label, + group: group, + isTableField: isTableFieldGroup + }; - $buttonsContainer.append($fieldBtn); + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(fieldData)); }); - - $container.append($fieldGroup); -}); -} -function initDragAndDrop($modal) { -const $resultContainer = $modal.find(".formula-result"); -const isTableField = $modal.data('is-table-field'); - -// Очищаем предыдущие обработчики -$modal.find('.field-btn').off('dragstart'); -$modal.find('.operator-btn').off('dragstart'); -$resultContainer.off('dragover dragleave drop'); - -// Настраиваем перетаскивание существующих токенов -$resultContainer.find('.formula-token').each(function() { - $(this).attr('draggable', 'true'); - - $(this).off('dragstart').on('dragstart', function(e) { - // Сохраняем ссылку на текущий элемент - window.draggedElement = this; + // Настраиваем перетаскивание операторов + $modal.find('.operator-btn').on('dragstart', function(e) { + const operator = $(this).data('operator'); - // Добавляем данные для переноса e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'token', - value: $(this).data('value'), - html: $(this)[0].outerHTML + type: 'operator', + value: operator })); - - // Добавляем класс перетаскивания - $(this).addClass('dragging'); - - return true; }); - - // При завершении перетаскивания - $(this).off('dragend').on('dragend', function(e) { - $(this).removeClass('dragging'); - window.draggedElement = null; - }); -}); -// Настраиваем перетаскивание полей из панели -$modal.find('.field-btn').on('dragstart', function(e) { - const fieldname = $(this).data('fieldname'); - const label = $(this).data('label'); - const group = $(this).data('group'); - - // Проверяем принадлежность к группе - const isTableFieldGroup = group !== "Основные поля"; - const fieldData = { - type: 'field', - fieldname: fieldname, - label: label, - group: group, - isTableField: isTableFieldGroup - }; - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(fieldData)); -}); - -// Настраиваем перетаскивание операторов -$modal.find('.operator-btn').on('dragstart', function(e) { - const operator = $(this).data('operator'); - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'operator', - value: operator - })); -}); - -// Обработка dragover -$resultContainer.on('dragover', function(e) { - e.preventDefault(); - e.stopPropagation(); - $(this).addClass('drag-hover'); - - const x = e.originalEvent.clientX; - const y = e.originalEvent.clientY; - - let position = { element: null, position: 'after' }; - - // Проходим по всем дочерним элементам контейнера - $resultContainer.contents().each(function() { - if (this.nodeType === Node.TEXT_NODE) return; - - const rect = this.getBoundingClientRect(); - - // Если курсор находится над элементом - if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { - position.element = this; - - // Определяем, до или после элемента - if (x < rect.left + rect.width / 2) { - position.position = 'before'; - } else { - position.position = 'after'; - } - - return false; // Прерываем перебор - } - }); - - $resultContainer.find('.drop-marker').remove(); - - if (position.element) { - const $marker = $(''); - - if (position.position === 'before') { - $(position.element).before($marker); - } else { - $(position.element).after($marker); - } - } -}); - -// Обработка dragleave -$resultContainer.on('dragleave', function(e) { - $(this).removeClass('drag-hover'); - $(this).find('.drop-marker').remove(); -}); - -// Обработка drop -$resultContainer.on('drop', function(e) { - e.preventDefault(); - e.stopPropagation(); - $(this).removeClass('drag-hover'); - $(this).find('.drop-marker').remove(); - - try { - const data = JSON.parse(e.originalEvent.dataTransfer.getData('text/plain')); + // Обработка dragover + $resultContainer.on('dragover', function(e) { + e.preventDefault(); + e.stopPropagation(); + $(this).addClass('drag-hover'); const x = e.originalEvent.clientX; const y = e.originalEvent.clientY; let position = { element: null, position: 'after' }; - // Находим позицию для вставки + // Проходим по всем дочерним элементам контейнера $resultContainer.contents().each(function() { if (this.nodeType === Node.TEXT_NODE) return; @@ -1015,95 +1088,155 @@ $resultContainer.on('drop', function(e) { } }); - // Обработка перетаскивания существующих токенов - if (data.type === 'token' && window.draggedElement) { - const $dragged = $(window.draggedElement); + $resultContainer.find('.drop-marker').remove(); + + if (position.element) { + const $marker = $(''); - if (position.element) { - if (position.position === 'before') { - $(position.element).before($dragged); - $(position.element).before(' '); - } else { - $(position.element).after($dragged); - $(position.element).after(' '); - } + if (position.position === 'before') { + $(position.element).before($marker); } else { - $resultContainer.append($dragged); - $resultContainer.append(' '); + $(position.element).after($marker); } - - window.draggedElement = null; - return; } - - // Создание нового элемента - let $newElement; - -// В обработчике drop -if (data.type === 'field') { -let formulaText; -const isModalTableField = isTableField; + }); -// Определяем тип поля и формулу с помощью общей функции -const fieldInfo = determineFieldType(data.fieldname, data.group, isModalTableField); + // Обработка dragleave + $resultContainer.on('dragleave', function(e) { + $(this).removeClass('drag-hover'); + $(this).find('.drop-marker').remove(); + }); -if (fieldInfo.type === 'row') { - formulaText = `row(${fieldInfo.fieldName})`; -} else if (fieldInfo.type === 'sum') { - formulaText = `sum(${fieldInfo.fieldName})`; -} else { - formulaText = `val(${fieldInfo.fieldName})`; -} - -$newElement = $(`${formulaText}×`); -} + // Обработка drop + $resultContainer.on('drop', function(e) { + e.preventDefault(); + e.stopPropagation(); + $(this).removeClass('drag-hover'); + $(this).find('.drop-marker').remove(); - // Вставляем новый элемент - if ($newElement) { - if (position.element) { - if (position.position === 'before') { - $(position.element).before($newElement); - $(position.element).before(' '); - } else { - $(position.element).after($newElement); - $(position.element).after(' '); + try { + const data = JSON.parse(e.originalEvent.dataTransfer.getData('text/plain')); + + const x = e.originalEvent.clientX; + const y = e.originalEvent.clientY; + + let position = { element: null, position: 'after' }; + + // Находим позицию для вставки + $resultContainer.contents().each(function() { + if (this.nodeType === Node.TEXT_NODE) return; + + const rect = this.getBoundingClientRect(); + + // Если курсор находится над элементом + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { + position.element = this; + + // Определяем, до или после элемента + if (x < rect.left + rect.width / 2) { + position.position = 'before'; + } else { + position.position = 'after'; + } + + return false; // Прерываем перебор } - } else { - $resultContainer.append($newElement); - $resultContainer.append(' '); - } - - // Инициализируем события для нового элемента - $newElement.find('.token-remove').on('click', function(e) { - e.stopPropagation(); - $(this).parent().remove(); }); - // Добавляем обработчики для перетаскивания - $newElement.attr('draggable', 'true'); - - $newElement.off('dragstart').on('dragstart', function(e) { - window.draggedElement = this; + // Обработка перетаскивания существующих токенов + if (data.type === 'token' && window.draggedElement) { + const $dragged = $(window.draggedElement); - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'token', - value: $(this).data('value'), - html: $(this)[0].outerHTML - })); + if (position.element) { + if (position.position === 'before') { + $(position.element).before($dragged); + $(position.element).before(' '); + } else { + $(position.element).after($dragged); + $(position.element).after(' '); + } + } else { + $resultContainer.append($dragged); + $resultContainer.append(' '); + } - $(this).addClass('dragging'); - return true; - }); - - $newElement.off('dragend').on('dragend', function(e) { - $(this).removeClass('dragging'); window.draggedElement = null; - }); + return; + } + + // Создание нового элемента + let $newElement; + + // Добавляем обработку универсальных полей в drop + if (data.type === 'universal-field') { + // Создаем токен для универсального поля + const formulaText = `univ(${data.name})`; + $newElement = $(`${formulaText}×`); + } else if (data.type === 'field') { + let formulaText; + const isModalTableField = isTableField; + + // Определяем тип поля и формулу с помощью общей функции + const fieldInfo = determineFieldType(data.fieldname, data.group, isModalTableField); + + if (fieldInfo.type === 'row') { + formulaText = `row(${fieldInfo.fieldName})`; + } else if (fieldInfo.type === 'sum') { + formulaText = `sum(${fieldInfo.fieldName})`; + } else { + formulaText = `val(${fieldInfo.fieldName})`; + } + + $newElement = $(`${formulaText}×`); + } + + // Вставляем новый элемент + if ($newElement) { + if (position.element) { + if (position.position === 'before') { + $(position.element).before($newElement); + $(position.element).before(' '); + } else { + $(position.element).after($newElement); + $(position.element).after(' '); + } + } else { + $resultContainer.append($newElement); + $resultContainer.append(' '); + } + + // Инициализируем события для нового элемента + $newElement.find('.token-remove').on('click', function(e) { + e.stopPropagation(); + $(this).parent().remove(); + }); + + // Добавляем обработчики для перетаскивания + $newElement.attr('draggable', 'true'); + + $newElement.off('dragstart').on('dragstart', function(e) { + window.draggedElement = this; + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'token', + value: $(this).data('value'), + html: $(this)[0].outerHTML + })); + + $(this).addClass('dragging'); + return true; + }); + + $newElement.off('dragend').on('dragend', function(e) { + $(this).removeClass('dragging'); + window.draggedElement = null; + }); + } + } catch (error) { + console.error("Ошибка при обработке drop-события:", error); } - } catch (error) { - console.error("Ошибка при обработке drop-события:", error); - } -});} + }); +} // Обновляем функцию attachFormulaBuilderEvents, чтобы сделать поля ввода редактируемыми вручную function attachFormulaBuilderEvents(frm, fields, grouped_fields) { @@ -1122,136 +1255,135 @@ $(frm.wrapper).on('click', '.open-formula-builder', function() { } function parseAndRenderFormula($container, formula, isTableField) { -if (!formula) return; + if (!formula) return; -// Регулярные выражения для поиска функций и операторов -const funcRegex = /(val|row|sum)\(([^)]+)\)/g; -const operatorsRegex = /([+\-*/()%])/g; + // Регулярные выражения для поиска функций и операторов + const funcRegex = /(val|row|sum|univ)\(([^)]+)\)/g; + const operatorsRegex = /([+\-*/()%])/g; -// Сохраняем текущую позицию -let lastIndex = 0; -let match; -let formulaParts = []; + // Сохраняем текущую позицию + let lastIndex = 0; + let match; + let formulaParts = []; -// Поиск всех функций -while ((match = funcRegex.exec(formula)) !== null) { - // Добавляем текст до функции - if (match.index > lastIndex) { + // Поиск всех функций + while ((match = funcRegex.exec(formula)) !== null) { + // Добавляем текст до функции + if (match.index > lastIndex) { + formulaParts.push({ + type: 'text', + value: formula.substring(lastIndex, match.index) + }); + } + + // Добавляем функцию + formulaParts.push({ + type: 'function', + value: match[0], + func: match[1], + param: match[2] + }); + + lastIndex = match.index + match[0].length; + } + + // Добавляем оставшийся текст после последней функции + if (lastIndex < formula.length) { formulaParts.push({ type: 'text', - value: formula.substring(lastIndex, match.index) + value: formula.substring(lastIndex) }); } - - // Добавляем функцию - formulaParts.push({ - type: 'function', - value: match[0], - func: match[1], - param: match[2] - }); - - lastIndex = match.index + match[0].length; -} -// Добавляем оставшийся текст после последней функции -if (lastIndex < formula.length) { - formulaParts.push({ - type: 'text', - value: formula.substring(lastIndex) - }); -} - -// Теперь разбираем операторы в частях, которые не являются функциями -let finalParts = []; -formulaParts.forEach(part => { - if (part.type === 'function') { - finalParts.push(part); - } else if (part.type === 'text') { - let text = part.value; - lastIndex = 0; - - // Сбрасываем lastIndex для operatorsRegex - operatorsRegex.lastIndex = 0; - - // Разбираем операторы - while ((match = operatorsRegex.exec(text)) !== null) { - // Добавляем текст до оператора - if (match.index > lastIndex) { - const textBefore = text.substring(lastIndex, match.index).trim(); - if (textBefore) { + // Теперь разбираем операторы в частях, которые не являются функциями + let finalParts = []; + formulaParts.forEach(part => { + if (part.type === 'function') { + finalParts.push(part); + } else if (part.type === 'text') { + let text = part.value; + lastIndex = 0; + + // Сбрасываем lastIndex для operatorsRegex + operatorsRegex.lastIndex = 0; + + // Разбираем операторы + while ((match = operatorsRegex.exec(text)) !== null) { + // Добавляем текст до оператора + if (match.index > lastIndex) { + const textBefore = text.substring(lastIndex, match.index).trim(); + if (textBefore) { + finalParts.push({ + type: 'text', + value: textBefore + }); + } + } + + // Добавляем оператор + finalParts.push({ + type: 'operator', + value: match[1] + }); + + lastIndex = match.index + match[1].length; + } + + // Добавляем оставшийся текст после последнего оператора + if (lastIndex < text.length) { + const textAfter = text.substring(lastIndex).trim(); + if (textAfter) { finalParts.push({ type: 'text', - value: textBefore + value: textAfter }); } } - - // Добавляем оператор - finalParts.push({ - type: 'operator', - value: match[1] - }); - - lastIndex = match.index + match[1].length; } - - // Добавляем оставшийся текст после последнего оператора - if (lastIndex < text.length) { - const textAfter = text.substring(lastIndex).trim(); - if (textAfter) { - finalParts.push({ - type: 'text', - value: textAfter - }); - } - } - } -}); - -// Рендерим части формулы -finalParts.forEach(part => { - if (part.type === 'function') { - const $token = $(`${part.value}×`); - $container.append($token).append(' '); - } else if (part.type === 'operator') { - const $token = $(`${part.value}×`); - $container.append($token).append(' '); - } else if (part.type === 'text') { - $container.append(document.createTextNode(`${part.value} `)); - } -}); - -// Добавляем обработчики для удаления и перетаскивания токенов -$container.find('.token-remove').off('click').on('click', function(e) { - e.stopPropagation(); - $(this).parent().remove(); -}); - -$container.find('.formula-token').each(function() { - $(this).attr('draggable', 'true'); - - $(this).off('dragstart').on('dragstart', function(e) { - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'token', - value: $(this).data('value'), - html: $(this)[0].outerHTML - })); - window.draggedElement = this; }); -}); -} -function getFormulaText($container) { -// Получаем текст формулы, объединяя содержимое всех узлов -let formula = ''; -$container.contents().each(function() { - if (this.nodeType === Node.TEXT_NODE) { - formula += this.nodeValue; - } else if ($(this).hasClass('formula-token')) { - formula += $(this).data('value'); - } -}); + // Рендерим части формулы + finalParts.forEach(part => { + if (part.type === 'function') { + // Определяем класс в зависимости от типа функции + let tokenClass = 'field'; + if (part.func === 'univ') { + tokenClass = 'field universal-field'; + } + + const $token = $(`${part.value}×`); + $container.append($token).append(' '); + } else if (part.type === 'operator') { + const $token = $(`${part.value}×`); + $container.append($token).append(' '); + } else if (part.type === 'text') { + $container.append(document.createTextNode(`${part.value} `)); + } + }); + + // Добавляем обработчики для удаления и перетаскивания токенов + $container.find('.token-remove').off('click').on('click', function(e) { + e.stopPropagation(); + $(this).parent().remove(); + }); + + $container.find('.formula-token').each(function() { + $(this).attr('draggable', 'true'); + + $(this).off('dragstart').on('dragstart', function(e) { + console.log("Начало перетаскивания токена:", $(this).data('value')); + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'token', + value: $(this).data('value'), + html: $(this)[0].outerHTML + })); + window.draggedElement = this; + }); + + $(this).off('dragend').on('dragend', function() { + window.draggedElement = null; + }); + }); // Очищаем лишние пробелы formula = formula.replace(/\s+/g, ' ').trim(); @@ -1327,44 +1459,70 @@ $container.off('paste').on('paste', function(e) { // Функция для определения типа поля (одинаковая для всех частей кода) function determineFieldType(fieldName, group, isInTableField) { -// Получаем части имени поля -const parts = fieldName.split('_'); -const potentialTableName = parts[0]; - -// Проверяем формат имени поля -const tablePrefixPattern = /^[a-zA-Z0-9_]+_[a-zA-Z0-9_]+$/; - -// Проверка, является ли поле табличным -const isFieldFromChildTable = - tablePrefixPattern.test(fieldName) && - (group && group !== "Основные поля") && - (group.toLowerCase().includes(potentialTableName.toLowerCase())); - -// Определяем тип формулы -if (isFieldFromChildTable) { - // Для полей таблицы - if (isInTableField) { - // В табличной части используем row() - return { - type: 'row', - fieldName: parts.slice(1).join('_') - }; + // Получаем части имени поля + // Проверяем существование имен групп "items", "taxes" и т.д. без подчеркивания + const tablePrefixes = ["items", "taxes", "packed_items", "payments", "advances", "timesheets", "payment_schedule", "sales_team"]; + + // Проверяем, начинается ли fieldName с известного префикса таблицы + let isTableField = false; + let tableName = ""; + + for (const prefix of tablePrefixes) { + if (fieldName.startsWith(prefix)) { + // Это поле табличной части без подчеркивания (например, "itemsqty") + isTableField = true; + tableName = prefix; + break; + } + } + + // Если не нашли по префиксу, проверяем по первой части с подчеркиванием + if (!isTableField) { + const parts = fieldName.split('_'); + if (parts.length > 1) { + const potentialTableName = parts[0]; + if (tablePrefixes.includes(potentialTableName)) { + isTableField = true; + tableName = potentialTableName; + } + } + } + + // Определяем тип формулы + if (isTableField) { + // Для полей таблицы + if (isInTableField) { + // В табличной части используем row() + // Извлекаем имя поля без префикса таблицы + let fieldNameWithoutTable = fieldName; + if (fieldName.startsWith(tableName)) { + fieldNameWithoutTable = fieldName.substring(tableName.length); + // Если есть подчеркивание, удаляем его + if (fieldNameWithoutTable.startsWith('_')) { + fieldNameWithoutTable = fieldNameWithoutTable.substring(1); + } + } + + return { + type: 'row', + fieldName: fieldNameWithoutTable + }; + } else { + // В основном документе используем sum() + return { + type: 'sum', + fieldName: fieldName, + tableName: tableName + }; + } } else { - // В основном документе используем sum() + // Для обычных полей всегда val() return { - type: 'sum', + type: 'val', fieldName: fieldName }; } -} else { - // Для обычных полей всегда val() - return { - type: 'val', - fieldName: fieldName - }; } -} - function createTriggerFieldsModal() { if ($("#trigger_fields_modal").length) { @@ -1977,4 +2135,645 @@ function setupTriggerEditableContainer($container) { }); } }); +} + +// Функция создания модального окна универсальных полей +function createUniversalFieldsModal() { + if ($("#universal_fields_modal").length) { + return; + } + + const modalHtml = ` + + `; + + const modalStyles = ` + + `; + + $('body').append(modalHtml); + $('head').append(modalStyles); +} + +// Функция для инициализации управления универсальными полями +function initUniversalFields(frm) { + // Если хранилище универсальных полей не существует, создаем его + if (!frm.doc.universal_fields_storage) { + frm.set_value("universal_fields_storage", "{}"); + } + + // Создаем модальное окно для универсальных полей + createUniversalFieldsModal(); + + // Инициализируем обработчики событий + attachUniversalFieldsEvents(frm); +} + +// Функция для добавления секции универсальных полей в интерфейс +function addUniversalFieldsSection(wrapper) { + const universalSection = $('
'); + universalSection.append('

Универсальные поля

'); + universalSection.append('
'); + wrapper.append(universalSection); +} + +// Функция для отрисовки универсальных полей +function renderUniversalFields(frm) { + const $container = $(frm.wrapper).find('.universal-fields-list'); + $container.empty(); + + try { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + + Object.keys(universalFields).forEach(fieldId => { + const field = universalFields[fieldId]; + + const $fieldButton = $(` + + `); + + // Добавляем контекстное меню для редактирования/удаления + $fieldButton.on('contextmenu', function(e) { + e.preventDefault(); + + // Удаляем предыдущее контекстное меню, если оно есть + $('.universal-field-context-menu').remove(); + + // Создаем новое контекстное меню + const contextMenu = $(` +
+
Редактировать
+
Удалить
+
+ `); + + $('body').append(contextMenu); + + // Закрываем меню при клике вне его + $(document).on('click', function() { + $('.universal-field-context-menu').remove(); + $(document).off('click'); + }); + + // Обработчики для пунктов меню + contextMenu.on('click', '.edit-universal-field', function() { + editUniversalField(frm, $(this).data('field-id')); + }); + + contextMenu.on('click', '.delete-universal-field', function() { + deleteUniversalField(frm, $(this).data('field-id')); + }); + + return false; + }); + + $container.append($fieldButton); + }); + + // Если нет полей, показываем сообщение + if (Object.keys(universalFields).length === 0) { + $container.append('
Нет универсальных полей. Нажмите "Добавить поле" для создания.
'); + } + + // Инициализируем drag-n-drop для универсальных полей + $container.find('.universal-field-btn').each(function() { + $(this).attr('draggable', 'true'); + + $(this).off('dragstart').on('dragstart', function(e) { + const fieldId = $(this).data('field-id'); + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + const field = universalFields[fieldId]; + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'universal-field', + fieldId: fieldId, + name: field.name + })); + }); + }); + } catch (error) { + console.error("Ошибка при отрисовке универсальных полей:", error); + $container.append('
Ошибка при загрузке универсальных полей.
'); + } +} + +// Функция для прикрепления обработчиков событий универсальных полей +function attachUniversalFieldsEvents(frm) { + // Обработчик нажатия на кнопку "Добавить поле" + $(frm.wrapper).on('click', '.add-universal-field', function() { + openUniversalFieldModal(frm); + }); + + // Обработчики для модального окна + const $modal = $("#universal_fields_modal"); + + // При выборе типа документа, загружаем его поля + $modal.on('change', '#universal_field_doctype', function() { + const doctype = $(this).val(); + if (doctype) { + loadDoctypeFields(doctype); + } else { + $modal.find('.universal-fields-container').hide(); + $modal.find('.universal-filter-container').hide(); + } + }); + + // При выборе поля, показываем контейнер фильтров + $modal.on('change', '#universal_field_fieldname', function() { + if ($(this).val()) { + $modal.find('.universal-filter-container').show(); + } else { + $modal.find('.universal-filter-container').hide(); + } + }); + + // Валидация для кнопки сохранения + $modal.on('input', '#universal_field_name, #universal_field_doctype, #universal_field_fieldname', function() { + validateUniversalFieldForm(); + }); + + // Обработчик добавления фильтра + $modal.on('click', '.add-filter', function() { + addFilterRow(); + }); + + // Обработчик удаления фильтра + $modal.on('click', '.remove-filter', function() { + $(this).closest('.filter-row').remove(); + }); + + // Сохранение универсального поля + $modal.on('click', '.save-universal-field', function() { + saveUniversalField(frm); + }); +} + +// Функция для открытия модального окна создания/редактирования универсального поля +function openUniversalFieldModal(frm, fieldId = null) { + const $modal = $("#universal_fields_modal"); + const isEditing = fieldId !== null; + + // Обновляем заголовок в зависимости от режима + $modal.find('.modal-title').text(isEditing ? 'Редактирование универсального поля' : 'Создание универсального поля'); + + // Очищаем форму + $modal.find('#universal_field_name').val(''); + $modal.find('#universal_field_doctype').val(''); + $modal.find('#universal_field_fieldname').val('').empty().append(''); + $modal.find('.universal-fields-container').hide(); + $modal.find('.universal-filter-container').hide(); + $modal.find('.universal-filters').empty().append(` +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ `); + + // Сохраняем ID поля, если редактируем + $modal.data('editing-field-id', fieldId); + + // Загружаем список DocTypes + loadDoctypes().then(() => { + // Если редактируем существующее поле, заполняем форму + if (isEditing) { + fillFormForEditing(frm, fieldId); + } + }); + + // Сбрасываем состояние кнопки сохранения + $modal.find('.save-universal-field').prop('disabled', true); + + // Открываем модальное окно + $modal.modal('show'); +} + +// Функция для загрузки списка DocTypes +function loadDoctypes() { + return new Promise((resolve) => { + const $select = $("#universal_field_doctype"); + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "DocType", + filters: { + istable: 0, + issingle: 0 + }, + fields: ["name"], + limit_page_length: 500, + order_by: "name asc" + }, + callback: function(r) { + $select.empty(); + $select.append(''); + + if (r.message) { + r.message.forEach(doc => { + $select.append(``); + }); + } + + resolve(); + } + }); + }); +} + +// Функция для загрузки полей DocType +function loadDoctypeFields(doctype) { + const $modal = $("#universal_fields_modal"); + const $fieldsSelect = $modal.find('#universal_field_fieldname'); + const $filterFieldsSelect = $modal.find('.filter-field'); + + // Показываем индикатор загрузки + $fieldsSelect.html(''); + + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "DocType", + name: doctype + }, + callback: function(r) { + $fieldsSelect.empty(); + $fieldsSelect.append(''); + + $filterFieldsSelect.empty(); + $filterFieldsSelect.append(''); + + if (r.message && r.message.fields) { + // Фильтруем поля только для числовых и текстовых типов + const allowedTypes = ['Int', 'Float', 'Currency', 'Data', 'Text', 'Date', 'Datetime', 'Check']; + + r.message.fields.forEach(field => { + if (field.fieldname && allowedTypes.includes(field.fieldtype)) { + $fieldsSelect.append(``); + } + + // Для фильтров добавляем больше типов полей + if (field.fieldname) { + $filterFieldsSelect.append(``); + } + }); + } + + // Показываем контейнер полей + $modal.find('.universal-fields-container').show(); + } + }); +} + +// Функция для добавления строки фильтра +function addFilterRow() { + const $modal = $("#universal_fields_modal"); + const $filters = $modal.find('.universal-filters'); + const doctype = $modal.find('#universal_field_doctype').val(); + + if (!doctype) return; + + const $newRow = $(` +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ `); + + // Копируем опции полей из первой строки фильтра + const $firstFilterSelect = $modal.find('.filter-field').first(); + $newRow.find('.filter-field').html($firstFilterSelect.html()); + + $filters.append($newRow); +} + +// Функция для валидации формы +function validateUniversalFieldForm() { + const $modal = $("#universal_fields_modal"); + const name = $modal.find('#universal_field_name').val().trim(); + const doctype = $modal.find('#universal_field_doctype').val(); + const fieldname = $modal.find('#universal_field_fieldname').val(); + + const isValid = name && doctype && fieldname; + $modal.find('.save-universal-field').prop('disabled', !isValid); +} + +// Функция для сохранения универсального поля +function saveUniversalField(frm) { + const $modal = $("#universal_fields_modal"); + const fieldId = $modal.data('editing-field-id') || frappe.utils.get_random(10); + + const name = $modal.find('#universal_field_name').val().trim(); + const doctype = $modal.find('#universal_field_doctype').val(); + const fieldname = $modal.find('#universal_field_fieldname').val(); + const fieldtype = $modal.find(`#universal_field_fieldname option[value="${fieldname}"]`).data('fieldtype'); + + // ... остальной код сбора данных ... + + // Сохраняем в storage + try { + let universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + + // Проверяем, не существует ли уже поле с таким именем (кроме текущего редактируемого) + const existingField = Object.keys(universalFields).find(key => + key !== fieldId && universalFields[key].name === name + ); + + if (existingField) { + frappe.msgprint({ + title: __('Внимание'), + message: __('Поле с таким именем уже существует.'), + indicator: 'orange' + }); + return; + } + + universalFields[fieldId] = universalField; + frm.set_value("universal_fields_storage", JSON.stringify(universalFields)); + + // ... остальной код ... + } catch (error) { + // ... обработка ошибок ... + } +} + +// Функция для заполнения формы при редактировании +function fillFormForEditing(frm, fieldId) { + try { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + const field = universalFields[fieldId]; + + if (!field) { + console.error("Поле не найдено:", fieldId); + return; + } + + const $modal = $("#universal_fields_modal"); + + // Заполняем базовые поля + $modal.find('#universal_field_name').val(field.name); + $modal.find('#universal_field_doctype').val(field.doctype); + + // Загружаем поля выбранного DocType и затем выбираем нужное поле + loadDoctypeFields(field.doctype).then(() => { + $modal.find('#universal_field_fieldname').val(field.fieldname); + $modal.find('.universal-fields-container').show(); + $modal.find('.universal-filter-container').show(); + + // Очищаем существующие фильтры + $modal.find('.universal-filters').empty(); + + // Добавляем сохраненные фильтры + if (field.filters && field.filters.length > 0) { + field.filters.forEach(filter => { + addFilterRow(); + const $lastRow = $modal.find('.filter-row').last(); + $lastRow.find('.filter-field').val(filter.field); + $lastRow.find('.filter-operator').val(filter.operator); + $lastRow.find('.filter-value').val(filter.value); + }); + } else { + // Добавляем пустую строку фильтра, если нет сохраненных + addFilterRow(); + } + + // Валидируем форму + validateUniversalFieldForm(); + }); + + } catch (error) { + console.error("Ошибка при заполнении формы:", error); + } +} + +// Функция для удаления универсального поля +function deleteUniversalField(frm, fieldId) { + frappe.confirm( + 'Вы уверены, что хотите удалить это универсальное поле?', + function() { + try { + let universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + delete universalFields[fieldId]; + frm.set_value("universal_fields_storage", JSON.stringify(universalFields)); + + // Перерисовываем список полей + renderUniversalFields(frm); + + frappe.show_alert({ + message: __('Универсальное поле удалено'), + indicator: 'green' + }); + } catch (error) { + console.error("Ошибка при удалении универсального поля:", error); + frappe.msgprint({ + title: __('Ошибка'), + message: __('Не удалось удалить универсальное поле.'), + indicator: 'red' + }); + } + } + ); +} + +// Функция для редактирования универсального поля +function editUniversalField(frm, fieldId) { + openUniversalFieldModal(frm, fieldId); +} + +// Исправленная функция transform_expression +function transform_expression(expression, for_child_table = false, table_name = null, universalFields = {}) { + console.log("🔄 Исходная формула:", expression, "для табличной части:", for_child_table, "таблица:", table_name); + + let transformed = expression; + + // Обрабатываем универсальные поля + Object.keys(universalFields).forEach(fieldId => { + const field = universalFields[fieldId]; + if (!field || !field.name) return; + + const pattern = new RegExp(`univ\\(${field.name}\\)`, 'g'); + transformed = transformed.replace(pattern, `(frm.doc.__universalFields && frm.doc.__universalFields["${fieldId}"] || 0)`); + }); + + // Функция суммирования по табличной части - обрабатываем ее первой + transformed = transformed.replace(/sum\((\w+)[_]?(\w+)\)/g, (match, tbl, fld) => { + // Проверяем, содержит ли таблица подчеркивание или нет + return `(frm.doc["${tbl}"] && frm.doc["${tbl}"].reduce((sum, row) => sum + (parseFloat(row["${fld}"]) || 0), 0) || 0)`; + }); + + // Остальная часть функции без изменений... + // ... + + console.log("🔄 Трансформированная формула:", transformed); + return transformed; } \ No newline at end of file diff --git a/formula_editor/formula_editor/doctype/formula_editor/formula_editor.json b/formula_editor/formula_editor/doctype/formula_editor/formula_editor.json index 5e3f3e2..0dfd423 100644 --- a/formula_editor/formula_editor/doctype/formula_editor/formula_editor.json +++ b/formula_editor/formula_editor/doctype/formula_editor/formula_editor.json @@ -7,6 +7,7 @@ "field_order": [ "target_doctype", "formula_storage", + "universal_fields_storage", "formula_fields", "test" ], @@ -30,11 +31,16 @@ { "fieldname": "test", "fieldtype": "HTML" + }, + { + "fieldname": "universal_fields_storage", + "fieldtype": "Text", + "label": "universal_fields_storage" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-02-27 19:12:22.113845", + "modified": "2025-03-06 13:11:19.311286", "modified_by": "Administrator", "module": "Formula Editor", "name": "Formula Editor",