From a8250e53e76aed31e018e00931b0bda61a689db9 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Wed, 5 Mar 2025 14:11:43 +0400 Subject: [PATCH] Added + button to triggers --- .../doctype/formula_editor/formula_editor.js | 2303 +++++++++++------ 1 file changed, 1518 insertions(+), 785 deletions(-) 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 0bb94c9..cc445d4 100644 --- a/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js +++ b/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js @@ -1,369 +1,369 @@ frappe.ui.form.on('Formula Editor', { - onload_post_render: function(frm) { - if (!frm.is_initialized) { - frm.is_initialized = true; - console.log("Initializing 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); - } - }); - } + 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); } +}, + +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); +let stored_formulas = JSON.parse(frm.doc.formula_storage || "{}"); +console.log("🔄 Восстанавливаем формулы:", stored_formulas); - $(frm.fields_dict.formula_fields.wrapper).find('.formula-input, .trigger-input').each(function () { - let fieldname = $(this).attr("data-fieldname"); +$(frm.fields_dict.formula_fields.wrapper).find('.formula-input, .trigger-input').each(function () { + let fieldname = $(this).attr("data-fieldname"); - if (!fieldname) return; + if (!fieldname) return; - let isTriggerField = fieldname.endsWith("_triggers"); - let baseFieldname = isTriggerField ? fieldname.replace("_triggers", "") : fieldname; + let isTriggerField = fieldname.endsWith("_triggers"); + let baseFieldname = isTriggerField ? fieldname.replace("_triggers", "") : fieldname; - if (stored_formulas[baseFieldname] !== undefined) { - let value = isTriggerField ? stored_formulas[baseFieldname].triggers : stored_formulas[baseFieldname].formula; + if (stored_formulas[baseFieldname] !== undefined) { + let value = isTriggerField ? stored_formulas[baseFieldname].triggers : stored_formulas[baseFieldname].formula; - if (typeof value !== "string") { - console.warn(`⚠️ Поле "${fieldname}" имеет некорректное значение:`, value); - return; - } - - console.log(`✔️ Восстанавливаем "${fieldname}" -> "${value}"`); - $(this).val(value); - } - }); -} - -function save_formulas(frm) { - let formula_data = {}; - - console.log("🔄 Начинаем сохранение формул и триггеров..."); - - $(frm.fields_dict.formula_fields.wrapper).find('.formula-input').each(function () { - let fieldname = $(this).attr("data-fieldname"); - let formula = $(this).val(); - let is_table_field = $(this).attr("data-is-table-field") === "true"; - - // Ищем триггерное поле - let triggers = $(frm.fields_dict.formula_fields.wrapper) - .find(`[data-fieldname="${fieldname}_triggers"] input`).val(); - - console.log(`📌 Обрабатываем поле: "${fieldname}"`); - console.log(` 🔹 Формула: "${formula}"`); - console.log(` 🔹 Триггеры: "${triggers}"`); - console.log(` 🔹 Табличное поле: "${is_table_field}"`); - - formula_data[fieldname] = { - formula: formula || "", - triggers: triggers || "", - is_table_field: is_table_field - }; - }); - - console.log("✅ Итоговый JSON formula_storage:", formula_data); - 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 (typeof value !== "string") { + console.warn(`⚠️ Поле "${fieldname}" имеет некорректное значение:`, value); 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; + console.log(`✔️ Восстанавливаем "${fieldname}" -> "${value}"`); + $(this).val(value); + } +}); +} - function transform_expression(expression, for_child_table = false, table_name = null) { - console.log("🔄 Исходная формула:", expression, "для табличной части:", for_child_table, "таблица:", table_name); - - 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}"]`; - }); - - // Обрабатываем row() как обычно для табличной части - transformed = transformed.replace(/row\((\w+)\)/g, (match, field) => { - return `row["${field}"]`; - }); - } 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() вне табличной части */`; - }); - } - - // Функция суммирования по табличной части применяется в обоих случаях - transformed = transformed.replace(/sum\((\w+)_(\w+)\)/g, (match, tbl, fld) => { - return `(frm.doc["${tbl}"].reduce((sum, row) => sum + (row["${fld}"] || 0), 0))`; +function save_formulas(frm) { +let formula_data = {}; + +console.log("🔄 Начинаем сохранение формул и триггеров..."); + +$(frm.fields_dict.formula_fields.wrapper).find('.formula-input').each(function () { + let fieldname = $(this).attr("data-fieldname"); + let formula = $(this).val(); + let is_table_field = $(this).attr("data-is-table-field") === "true"; + + // Ищем триггерное поле + let triggers = $(frm.fields_dict.formula_fields.wrapper) + .find(`[data-fieldname="${fieldname}_triggers"] input`).val(); + + console.log(`📌 Обрабатываем поле: "${fieldname}"`); + console.log(` 🔹 Формула: "${formula}"`); + console.log(` 🔹 Триггеры: "${triggers}"`); + console.log(` 🔹 Табличное поле: "${is_table_field}"`); + + formula_data[fieldname] = { + formula: formula || "", + triggers: triggers || "", + is_table_field: is_table_field + }; +}); + +console.log("✅ Итоговый JSON formula_storage:", formula_data); +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; + } }); - - // Обработка процентов - 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() - }; +// Если данные о таблицах не получены, пробуем альтернативный способ через 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; } + }); + } +} - // Добавляем функцию расчета для текущего поля - table_child_scripts[table_name].calculations.push({ - field: field_name_in_table, - formula: transform_expression(formula, true, table_name) +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}, так как оно не содержит формулу.`); + 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; + + function transform_expression(expression, for_child_table = false, table_name = null) { + console.log("🔄 Исходная формула:", expression, "для табличной части:", for_child_table, "таблица:", table_name); + + 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}"]`; }); - - // Добавляем триггеры - triggers.forEach(trigger => { - table_child_scripts[table_name].fields.add(trigger); + + // Обрабатываем row() как обычно для табличной части + transformed = transformed.replace(/row\((\w+)\)/g, (match, field) => { + return `row["${field}"]`; }); } 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`; + // Стандартная замена для обычных полей + 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() вне табличной части */`; }); } - }); - - 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}; -`; + // Функция суммирования по табличной части применяется в обоих случаях + transformed = transformed.replace(/sum\((\w+)_(\w+)\)/g, (match, tbl, fld) => { + return `(frm.doc["${tbl}"].reduce((sum, row) => sum + (row["${fld}"] || 0), 0))`; }); - calcFunction += ` - frm.refresh_field("${table_name}"); - } catch (e) { - console.error("❌ Ошибка в формуле для таблицы ${table_name}:", e); + // Обработка процентов + 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}; +`; + }); + + calcFunction += ` + frm.refresh_field("${table_name}"); +} catch (e) { + console.error("❌ Ошибка в формуле для таблицы ${table_name}:", e); +} }`; - functions.push(calcFunction); + 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; + // Добавляем обработчики для каждого триггерного поля + 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); +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) { - frappe.call({ - method: "frappe.client.get_list", - args: { - doctype: "Client Script", - filters: { dt: target_doctype }, - fields: ["name"] - }, - callback: function(r) { - if (r.message && r.message.length > 0) { - let existing_script_name = r.message[0].name; - console.log(`🔄 Найден существующий Client Script: ${existing_script_name}. Обновляем...`); +frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Client Script", + filters: { dt: target_doctype }, + fields: ["name"] + }, + callback: function(r) { + if (r.message && r.message.length > 0) { + let existing_script_name = r.message[0].name; + console.log(`🔄 Найден существующий Client Script: ${existing_script_name}. Обновляем...`); - frappe.call({ - method: "frappe.client.set_value", - args: { + frappe.call({ + method: "frappe.client.set_value", + args: { + doctype: "Client Script", + name: existing_script_name, + fieldname: "script", + value: script_code + }, + callback: function(response) { + if (response.message) { + console.log(`✅ Client Script обновлен для ${target_doctype}`); + } else { + console.error("❌ Ошибка при обновлении Client Script", response); + } + } + }); + } else { + console.log("⚠️ Существующий Client Script не найден. Создаем новый..."); + + frappe.call({ + method: "frappe.client.insert", + args: { + doc: { doctype: "Client Script", - name: existing_script_name, - fieldname: "script", - value: script_code - }, - callback: function(response) { - if (response.message) { - console.log(`✅ Client Script обновлен для ${target_doctype}`); - } else { - console.error("❌ Ошибка при обновлении Client Script", response); - } + name: `Client Script for ${target_doctype}`, + dt: target_doctype, + script_type: "Client", + script: script_code, + enabled:1 } - }); - } else { - console.log("⚠️ Существующий Client Script не найден. Создаем новый..."); - - frappe.call({ - method: "frappe.client.insert", - args: { - doc: { - doctype: "Client Script", - name: `Client Script for ${target_doctype}`, - dt: target_doctype, - script_type: "Client", - script: script_code, - enabled:1 - } - }, - callback: function(response) { - if (response.message) { - console.log(`✅ Client Script создан для ${target_doctype}`); - } else { - console.error("❌ Ошибка при создании Client Script", response); - } + }, + callback: function(response) { + if (response.message) { + console.log(`✅ Client Script создан для ${target_doctype}`); + } else { + console.error("❌ Ошибка при создании Client Script", response); } - }); - } + } + }); } - }); + } +}); } function generate_dynamic_fields(frm, doctype, callback) { @@ -376,21 +376,21 @@ function generate_dynamic_fields(frm, doctype, callback) { console.warn("⚠️ Нет данных из API get_doctype_fields"); return; } - + let fields = r.message; console.log("✅ Получены поля:", fields); - + if (!frm.fields_dict.formula_fields) { console.error("❌ Поле 'formula_fields' не найдено!"); return; } - + let wrapper = $(frm.fields_dict.formula_fields.wrapper); wrapper.empty(); - + let grouped_fields = { "Основные поля": [] }; let table_fieldnames = new Set(); - + fields.forEach(field => { if (!field.fieldname) return; @@ -403,26 +403,29 @@ function generate_dynamic_fields(frm, doctype, callback) { grouped_fields[table_name] = []; } grouped_fields[table_name].push({ fieldname: field.fieldname, label: field_label }); - + if (match || field.fieldname.startsWith("table_")) { table_fieldnames.add(field.fieldname); } }); - + let container = $('
'); - + // Создаем модальное окно формульного редактора createFormulaBuilderModal(); - + + // Создаем модальное окно редактора полей изменения + createTriggerFieldsModal(); + Object.keys(grouped_fields).forEach(group => { let section_name = group === "Основные поля" ? group : `${group} (${group.replace(/\s+/g, '_').toLowerCase()})`; let section = $('
'); section.append(`

${section_name}

`); - + grouped_fields[group].forEach(field => { let is_table_field = table_fieldnames.has(field.fieldname); let dataAttr = is_table_field ? 'data-is-table-field="true"' : ''; - + let field_html = $('
' + '
' + '
' + @@ -438,86 +441,976 @@ function generate_dynamic_fields(frm, doctype, callback) { '
' + '
' + '
' + - '
' + - '
' + + '
' + + '
' + '' + '
' + + '' + '
' + '
' + '
' + '
'); section.append(field_html); }); - + container.append(section); }); - + wrapper.append(container); attachFormulaBuilderEvents(frm, fields, grouped_fields); + attachTriggerEditorEvents(frm, fields, grouped_fields); frm.refresh(); if (callback) callback(); } }); } + +function attachTriggerEditorEvents(frm, fields, grouped_fields) { + // Обработчик для кнопок открытия редактора полей изменения + $(frm.wrapper).on('click', '.open-trigger-editor', function() { + const fieldname = $(this).data('fieldname'); + const currentTriggers = $(frm.wrapper).find(`.trigger-input[data-fieldname="${fieldname}_triggers"]`).val(); + + openTriggerFieldsEditor(frm, fieldname, currentTriggers, fields, grouped_fields); + return false; + }); +} function createFormulaBuilderModal() { - if ($("#formula_builder_modal").length) { +if ($("#formula_builder_modal").length) { + return; +} + +const modalHtml = ` + +`; + +// Обновленные стили с поддержкой перетаскивания +const modalStyles = ` + +`; + +$('body').append(modalHtml); +$('head').append(modalStyles); +} + +function attachFormulaBuilderEvents(frm, fields, grouped_fields) { +// Обработчик для кнопок открытия визуального редактора +$(frm.wrapper).on('click', '.open-formula-builder', function() { + const fieldname = $(this).data('fieldname'); + const isTableField = $(this).data('is-table') === true; + const currentFormula = $(frm.wrapper).find(`.formula-input[data-fieldname="${fieldname}"]`).val(); + + openFormulaBuilder(frm, fieldname, isTableField, currentFormula, fields, grouped_fields); + return false; +}); +} + +function openFormulaBuilder(frm, targetFieldname, isTableField, currentFormula, fields, grouped_fields) { +const $modal = $("#formula_builder_modal"); +const $resultContainer = $modal.find(".formula-result"); + +// Очистка предыдущих обработчиков и данных +$modal.off('click', '.field-btn'); +$resultContainer.empty(); +$modal.find(".field-groups-container").empty(); + +// Заполняем текущую формулу, если она есть +if (currentFormula) { + parseAndRenderFormula($resultContainer, currentFormula, isTableField); +} + +// Заполняем панель с полями +populateFieldsPanel($modal, frm, grouped_fields, isTableField); + +// Сохраняем данные о текущем поле +$modal.data("target-field", targetFieldname); +$modal.data("is-table-field", isTableField); + +// Функция добавления токена в формулу +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') { + formulaText = `sum(${fieldInfo.fieldName})`; + } else { + formulaText = `val(${fieldInfo.fieldName})`; + } + + const $token = $(`${formulaText}×`); + $resultContainer.append($token).append(' '); +} else if (type === 'operator') { + const $token = $(`${token}×`); + + $resultContainer.append($token).append(' '); +} + +// Переносим курсор в конец контейнера +setEndOfContentEditable($resultContainer[0]); + +// Добавляем обработчики для удаления токенов +attachTokenEvents(); +} + +// Обработчики событий для токенов формулы +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) { + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'token', + value: $(this).data('value'), + html: $(this)[0].outerHTML + })); + window.draggedElement = this; + }); + }); +} + +// Обработчики для кнопок операторов +$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; + + // Создаем кнопку поля с правильными атрибутами для drag-and-drop + const $fieldBtn = $(` + + `); + + $buttonsContainer.append($fieldBtn); + }); + + $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; + + // Добавляем данные для переноса + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'token', + value: $(this).data('value'), + html: $(this)[0].outerHTML + })); + + // Добавляем класс перетаскивания + $(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')); + + 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; // Прерываем перебор + } + }); + + // Обработка перетаскивания существующих токенов + if (data.type === 'token' && window.draggedElement) { + const $dragged = $(window.draggedElement); + + 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(' '); + } + + window.draggedElement = null; + return; + } + + // Создание нового элемента + let $newElement; + +// В обработчике drop +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); + } +});} + +// Обновляем функцию attachFormulaBuilderEvents, чтобы сделать поля ввода редактируемыми вручную +function attachFormulaBuilderEvents(frm, fields, grouped_fields) { +// Сначала сделаем поля ввода формул редактируемыми +$(frm.wrapper).find('.formula-input').prop('readonly', false); + +// Обработчик для кнопок открытия визуального редактора +$(frm.wrapper).on('click', '.open-formula-builder', function() { + const fieldname = $(this).data('fieldname'); + const isTableField = $(this).data('is-table') === true; + const currentFormula = $(frm.wrapper).find(`.formula-input[data-fieldname="${fieldname}"]`).val(); + + openFormulaBuilder(frm, fieldname, isTableField, currentFormula, fields, grouped_fields); + return false; +}); +} + +function parseAndRenderFormula($container, formula, isTableField) { +if (!formula) return; + +// Регулярные выражения для поиска функций и операторов +const funcRegex = /(val|row|sum)\(([^)]+)\)/g; +const operatorsRegex = /([+\-*/()%])/g; + +// Сохраняем текущую позицию +let lastIndex = 0; +let match; +let formulaParts = []; + +// Поиск всех функций +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) + }); +} + +// Теперь разбираем операторы в частях, которые не являются функциями +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: 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'); + } +}); + +// Очищаем лишние пробелы +formula = formula.replace(/\s+/g, ' ').trim(); + +return formula; +} + +function setEndOfContentEditable(contentEditableElement) { +let range, selection; + +// Создаем диапазон выделения +range = document.createRange(); +range.selectNodeContents(contentEditableElement); +range.collapse(false); // Перемещаем курсор в конец + +// Применяем выделение +selection = window.getSelection(); +selection.removeAllRanges(); +selection.addRange(range); +} + +function setupEditableContainer($container) { +// Обработчик нажатия клавиш в контейнере формулы +$container.off('keydown').on('keydown', function(e) { + // Если нажата клавиша Delete или Backspace + if (e.keyCode === 8 || e.keyCode === 46) { + const selection = window.getSelection(); + + // Проверяем, выделен ли токен + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + + if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) { + // Текстовый узел - обычное удаление + return true; + } else { + // Возможно, выделен токен + const $selectedNode = $(range.commonAncestorContainer); + + if ($selectedNode.hasClass('formula-token') || $selectedNode.parents('.formula-token').length) { + // Удаляем токен + const $token = $selectedNode.hasClass('formula-token') ? + $selectedNode : $selectedNode.parents('.formula-token'); + $token.remove(); + e.preventDefault(); + return false; + } + } + } + } + + return true; +}); + +// Обработчик вставки текста +$container.off('paste').on('paste', function(e) { + e.preventDefault(); + + // Получаем вставляемый текст + let text = ''; + if (e.clipboardData || e.originalEvent.clipboardData) { + text = (e.clipboardData || e.originalEvent.clipboardData).getData('text/plain'); + } else if (window.clipboardData) { + text = window.clipboardData.getData('Text'); + } + + // Вставляем текст как обычный текст + if (text) { + document.execCommand('insertText', false, text); + } +}); +} + +// Функция для определения типа поля (одинаковая для всех частей кода) +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('_') + }; + } else { + // В основном документе используем sum() + return { + type: 'sum', + fieldName: fieldName + }; + } +} else { + // Для обычных полей всегда val() + return { + type: 'val', + fieldName: fieldName + }; +} +} + + +function createTriggerFieldsModal() { + if ($("#trigger_fields_modal").length) { return; } const modalHtml = ` -