From 282ba65f584328a6408de03584116ea7bddb2044 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Fri, 28 Mar 2025 20:15:29 +0400 Subject: [PATCH] 90% ready formula editor with small bugs --- formula_editor/api.py | 49 +- .../doctype/formula_editor/formula_editor.js | 5142 ++++++++++------- formula_editor/hooks.py | 12 +- 3 files changed, 3222 insertions(+), 1981 deletions(-) diff --git a/formula_editor/api.py b/formula_editor/api.py index 5a0a36a..5c848b0 100644 --- a/formula_editor/api.py +++ b/formula_editor/api.py @@ -3,10 +3,10 @@ import frappe 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}) + fields.append({"fieldname": field.fieldname, "label": field.label, "fieldtype": field.fieldtype}) # Проверяем таблицы внутри DocType (например, items в Sales Invoice) for child in meta.fields: if child.fieldtype == "Table": @@ -14,14 +14,15 @@ def get_doctype_fields(doctype): for sub_field in child_meta.fields: if sub_field.fieldtype in ["Int", "Float", "Currency"]: fields.append({ - "fieldname": f"{child.fieldname}{sub_field.fieldname}", - "label": f"{child.label} → {sub_field.label}" + "fieldname": f"{child.fieldname}_{sub_field.fieldname}", + "label": f"{child.label} → {sub_field.label}", + "fieldtype": sub_field.fieldtype }) return fields @frappe.whitelist() def get_all_doctype_fields(doctype): - """Получение отфильтрованных полей DocType без служебных полей""" + """Получение отфильтрованных полей DocType включая расширенные типы для фильтров""" meta = frappe.get_meta(doctype) fields = [] @@ -29,18 +30,46 @@ def get_all_doctype_fields(doctype): 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" + "Heading", "Fold", "Table MultiSelect" ] # Получаем все поля, которые не входят в исключенные типы for field in meta.fields: if field.fieldname and field.fieldtype not in excluded_types: - fields.append({ + field_data = { "fieldname": field.fieldname, "label": field.label or field.fieldname, "fieldtype": field.fieldtype - }) + } + + # Добавляем опции для полей типа Select и Link + if field.fieldtype in ["Select", "Link"] and field.options: + field_data["options"] = field.options + + fields.append(field_data) - # Мы не добавляем поля из таблиц, так как они были исключены из списка + return fields + +@frappe.whitelist() +def on_trash(doc, method=None): + """Удаляет клиентские скрипты, созданные для DocType при удалении Formula Editor""" + if not doc.target_doctype: + return - return fields \ No newline at end of file + # Ищем клиентские скрипты, созданные для целевого DocType + client_scripts = frappe.get_all( + "Client Script", + filters={ + "dt": doc.target_doctype, + "name": ["like", "Formula_Editor_Script_for_%"] + }, + fields=["name"] + ) + + # Удаляем найденные скрипты + for script in client_scripts: + try: + frappe.delete_doc("Client Script", script.name, force=True) + frappe.db.commit() + except Exception as e: + frappe.log_error(f"Не удалось удалить клиентский скрипт {script.name}: {str(e)}") \ 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 0db2667..e118b92 100644 --- a/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js +++ b/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js @@ -1,13 +1,12 @@ frappe.ui.form.on('Formula Editor', { onload_post_render: function(frm) { - // Проверяем наличие поля для хранения универсальных полей + // Инициализация хранилища для универсальных полей if (!frm.doc.universal_fields_storage) { frm.set_value("universal_fields_storage", "{}"); } 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() { @@ -26,70 +25,64 @@ frappe.ui.form.on('Formula Editor', { }, 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); - -$(frm.fields_dict.formula_fields.wrapper).find('.formula-input, .trigger-input').each(function () { - let fieldname = $(this).attr("data-fieldname"); - - if (!fieldname) return; - - 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 (typeof value !== "string") { - console.warn(`⚠️ Поле "${fieldname}" имеет некорректное значение:`, value); - return; + before_remove: function(frm) { + + if (frm.doc.target_doctype) { + delete_client_script_for_doctype(frm.doc.target_doctype); } - - console.log(`✔️ Восстанавливаем "${fieldname}" -> "${value}"`); - $(this).val(value); } }); + +// Управление формулами +function restore_formulas(frm) { + let stored_formulas = JSON.parse(frm.doc.formula_storage || "{}"); + + $(frm.fields_dict.formula_fields.wrapper).find('.formula-input, .trigger-input').each(function () { + let fieldname = $(this).attr("data-fieldname"); + if (!fieldname) return; + + 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 (typeof value !== "string") { + return; + } + + $(this).val(value); + } + }); } function save_formulas(frm) { -let formula_data = {}; + 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(); -$(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(); + formula_data[fieldname] = { + formula: formula || "", + triggers: triggers || "", + is_table_field: is_table_field + }; + }); - 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)); + frm.set_value("formula_storage", JSON.stringify(formula_data)); } +// Генерация клиентского скрипта function generate_client_script(frm) { if (!frm.doc.target_doctype) { console.error("❌ Target Doctype отсутствует!"); @@ -100,26 +93,217 @@ function generate_client_script(frm) { let formula_data = JSON.parse(frm.doc.formula_storage || "{}"); let universal_fields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - let main_script = `frappe.ui.form.on("${target_doctype}", {\n`; + let main_script = `// Generated by Formula Editor\nfrappe.ui.form.on("${target_doctype}", {\n`; let functions = []; let table_child_scripts = {}; - - // Справочник соответствия таблиц и их DocType let table_doctypes = {}; - // Сначала соберем информацию о табличных полях и их DocType + // Получаем информацию о таблицах DocType + getTableDoctypes(target_doctype, table_doctypes); + + console.log("📊 Соответствие таблиц и DocType:", table_doctypes); + + // Сбор всех табличных частей и их полей + let child_tables = {}; + collectChildTables(formula_data, child_tables); + + // Добавляем функцию для расчета универсальных полей + functions.push(generateUniversalFieldsFunction(universal_fields)); + + // Добавляем обработчики для ay и il - для расчета универсальных полей + main_script += ` "ay": async function(frm) { + // Вычисляем универсальные поля только когда меняется ay + await calculate_universal_fields(frm); + },\n`; + + main_script += ` "il": async function(frm) { + // Вычисляем универсальные поля только когда меняется il + await calculate_universal_fields(frm); + },\n`; + + // Добавляем функцию преобразования выражений + functions.push(generateTransformExpressionFunction(universal_fields)); + + // Создаем список всех полей, которые используют универсальные поля + let fieldsUsingUniversal = []; + + // Проверяем, какие поля используют универсальные поля + Object.keys(formula_data).forEach(fieldname => { + let field_info = formula_data[fieldname]; + if (field_info && field_info.formula && field_info.formula.includes("univ(")) { + fieldsUsingUniversal.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 is_table_field = field_info.is_table_field; + let formula = field_info.formula.trim(); + let uses_universal = formula.includes("univ("); + + if (is_table_field) { + // Обработка полей табличных частей... + 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`, + calculations: [], + fields: new Set(), + uses_universal: false + }; + } + + table_child_scripts[table_name].calculations.push({ + field: field_name_in_table, + formula: formula, + uses_universal: uses_universal + }); + + // Помечаем таблицу, если хотя бы одно поле использует универсальные поля + if (uses_universal) { + table_child_scripts[table_name].uses_universal = true; + } + + if (field_info.triggers) { + field_info.triggers.split(',').map(t => t.trim()).forEach(trigger => { + table_child_scripts[table_name].fields.add(trigger); + }); + } + } else { + // Для полей основного документа + // Если формула использует универсальные поля, функция должна быть асинхронной + if (uses_universal) { + functions.push(` +async function calculate_${fieldname}(frm) { +try { + // Сначала вычисляем универсальные поля + await calculate_universal_fields(frm); + + // Затем оцениваем формулу + let result = eval(transform_expression("${formula}", false)); + frm.set_value("${fieldname}", result); +} catch (e) { + console.error("❌ Ошибка в формуле для ${fieldname}: ${formula}", e); +} +}`); + } else { + functions.push(` +function calculate_${fieldname}(frm) { +try { + let result = eval(transform_expression("${formula}", false)); + frm.set_value("${fieldname}", result); +} catch (e) { + console.error("❌ Ошибка в формуле для ${fieldname}: ${formula}", e); +} +}`); + } + + // Добавляем триггеры для полей с непустыми триггерами + if (field_info.triggers) { + let triggers = field_info.triggers.split(',').map(t => t.trim()); + triggers.forEach(trigger => { + if (trigger) { + // Если формула использует универсальные поля, триггеры должны быть асинхронными + if (uses_universal) { + main_script += ` "${trigger}": async function(frm) { await calculate_${fieldname}(frm); },\n`; + } else { + 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 uses_universal = table_child_scripts[table_name].uses_universal; + + // Создаем общую функцию для расчета формул в строке таблицы + let calcFunction; + + if (uses_universal) { + calcFunction = ` +async function calculate_${table_name}_row(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + + try { + // Сначала вычисляем универсальные поля + await calculate_universal_fields(frm); + `; + } else { + 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(transform_expression("${calc.formula}", true, "${table_name}")); + `; + }); + + calcFunction += ` + 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 => { + if (uses_universal) { + table_script += ` "${field}": async function(frm, cdt, cdn) { await calculate_${table_name}_row(frm, cdt, cdn); },\n`; + } else { + 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); +} + +// Вспомогательные функции для generate_client_script +function getTableDoctypes(target_doctype, table_doctypes) { + // Пытаемся получить информацию о таблицах через API frappe.call({ method: "frappe.client.get", args: { doctype: "DocType", name: target_doctype }, - async: false, // Синхронный вызов для гарантии получения данных + 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; } }); @@ -127,7 +311,7 @@ function generate_client_script(frm) { } }); - // Если данные о таблицах не получены, пробуем альтернативный способ через meta + // Если данные не получены, используем альтернативный способ if (Object.keys(table_doctypes).length === 0) { const meta = frappe.get_meta(target_doctype); if (meta && meta.fields) { @@ -138,12 +322,9 @@ function generate_client_script(frm) { }); } } +} - console.log("📊 Соответствие таблиц и DocType:", table_doctypes); - - // Сбор всех табличных частей и их полей - let child_tables = {}; - +function collectChildTables(formula_data, child_tables) { Object.keys(formula_data).forEach(fieldname => { let field_info = formula_data[fieldname]; @@ -159,67 +340,241 @@ function generate_client_script(frm) { } } }); +} - // Добавляем функцию для загрузки универсальных полей - 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); - }); +function generateUniversalFieldsFunction(universal_fields) { + let universalFieldsCode = + ` + // Функция для расчета универсальных полей + 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 ''; } - // Сохраняем результат в временное поле документа - if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; - frm.doc.__universalFields["${fieldId}"] = univ_${fieldId}; + // Обработка фильтров для ссылок на поля документа + const processedFilters = (field.filters || []).map(filter => { + let newFilter = {...filter}; + + // Проверяем, является ли значение ссылкой на поле документа + if (typeof newFilter.value === 'string' && newFilter.value.startsWith('doc.')) { + const docField = newFilter.value.substring(4); // Убираем 'doc.' + newFilter.valueType = 'field'; + newFilter.docField = docField; + + // Проверяем, является ли это специальным полем ay или il + newFilter.__is_special_ay_field = (docField === 'ay' || docField === 'il'); + } else { + newFilter.valueType = 'static'; + } + return newFilter; + }); - 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')} + return ` + // Расчет универсального поля: ${field.name} + try { + let univ_${fieldId} = 0; + // Создаем массив фильтров + let filters_${fieldId} = []; + + // Обрабатываем фильтры с учетом возможных ссылок на поля документа + let skipCalculation_${fieldId} = false; + + ${processedFilters.map(filter => { + if (filter.valueType === 'field') { + if (filter.__is_special_ay_field) { + return ` + // Фильтр со ссылкой на специальное поле месяца (ay) или года (il) + if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) { + // Получаем значения месяца и года + const docValues = { + ay: frm.doc["ay"], + il: frm.doc["il"] + }; + + // Проверяем, что поле ay имеет допустимое значение + if (!docValues.ay || typeof docValues.ay !== 'string') { + console.warn("⚠️ Поле 'ay' пустое или имеет некорректный формат", docValues.ay); + skipCalculation_${fieldId} = true; + return; + } + + // Проверяем, что поле il имеет допустимое значение + if (!docValues.il || isNaN(parseInt(docValues.il))) { + console.warn("⚠️ Поле 'il' пустое или имеет некорректный формат", docValues.il); + skipCalculation_${fieldId} = true; + return; + } + + // Обрабатываем специальные поля ay и il для получения диапазона дат + const dateRange = processAzerbayjanDateField(docValues); + + if (dateRange) { + // Добавляем два фильтра для диапазона дат + filters_${fieldId}.push(["${filter.field}", ">=", dateRange.start]); + filters_${fieldId}.push(["${filter.field}", "<=", dateRange.end]); + console.log("🔍 Применяем специальные фильтры по датам для ${field.name}:", dateRange); + } else { + console.warn("⚠️ Не удалось обработать специальные поля месяца и года", docValues); + skipCalculation_${fieldId} = true; + } + } else { + console.log("🔍 Пропуск расчета ${field.name}: поля 'ay' или 'il' не определены"); + skipCalculation_${fieldId} = true; + } + `; + } else { + return ` + // Фильтр со ссылкой на поле документа: ${filter.docField} + if (frm.doc["${filter.docField}"] !== undefined) { + filters_${fieldId}.push(["${filter.field}", "${filter.operator}", frm.doc["${filter.docField}"]]); + } else { + console.log("⚠️ Поле '${filter.docField}' не определено в документе"); + skipCalculation_${fieldId} = true; + } + `; + } + } else { + // Обычные статические фильтры + return ` + // Статический фильтр + filters_${fieldId}.push(["${filter.field}", "${filter.operator}", "${filter.value}"]); + `; + } + }).join('\n ')} + + // Проверяем, нужно ли пропустить расчет + if (skipCalculation_${fieldId}) { + console.log("⚠️ Расчет ${field.name} пропущен из-за отсутствия или некорректности данных"); + if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; + frm.doc.__universalFields["${fieldId}"] = 0; + return; + } + + console.log("🔍 Применяемые фильтры для ${field.name}:", filters_${fieldId}); + + 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')} + } + `; + + // Добавляем функцию для обработки специальных полей месяца + universalFieldsCode += ` + // Функция для обработки специальных полей с азербайджанскими месяцами + function processAzerbayjanDateField(docValues) { + // Словарь для сопоставления названий месяцев с их номерами + const monthNames = { + "Yanvar": 1, + "Fevral": 2, + "Mart": 3, + "Aprel": 4, + "May": 5, + "İyun": 6, + "İyul": 7, + "Avqust": 8, + "Sentyabr": 9, + "Oktyabr": 10, + "Noyabr": 11, + "Dekabr": 12 + }; + + // Проверяем, есть ли у нас значения ay и il + if (docValues && docValues.ay && monthNames[docValues.ay]) { + // Получаем номер месяца + const monthNum = monthNames[docValues.ay]; + + // Получаем год из поля il или используем текущий год + let year = docValues.il || new Date().getFullYear(); + console.log(\`🔹 Обработка специальных полей: ay=\${docValues.ay} (\${monthNum}), il=\${year}\`); + + // Определяем первый день месяца (1-е число) + const firstDay = new Date(year, monthNum - 1, 1); + + // Последний день месяца (получаем последнее число месяца) + const lastDay = new Date(year, monthNum, 0); + + // Форматируем даты в формате YYYY.MM.DD + const firstDayStr = \`\${year}.\${String(monthNum).padStart(2, '0')}.01\`; + const lastDayStr = \`\${year}.\${String(monthNum).padStart(2, '0')}.\${String(lastDay.getDate()).padStart(2, '0')}\`; + + console.log(\`🔹 Диапазон дат для \${docValues.ay} \${year}: \${firstDayStr} - \${lastDayStr}\`); + + // Возвращаем объект с диапазоном дат + return { + start: firstDayStr, + end: lastDayStr + }; + } + + // Если это не специальное поле или значение не распознано, возвращаем null + return null; } - `); + `; + return universalFieldsCode; +} - // Добавляем вызов функции расчета универсальных полей при загрузке формы - main_script += ` "refresh": async function(frm) { - await calculate_universal_fields(frm); - ${Object.keys(formula_data).map(fieldname => { - return `calculate_${fieldname}(frm);`; - }).join('\n ')} +function generateRefreshFunction(formula_data) { + // Получаем список полей, для которых созданы формулы + const formulaFields = Object.keys(formula_data); + + // Фильтруем, чтобы брать только поля с непустыми формулами + const validFields = formulaFields.filter(fieldname => + formula_data[fieldname] && + formula_data[fieldname].formula && + formula_data[fieldname].formula.trim() + ); + + let refreshCode = ""; + + // Если ay не является полем с формулой, добавляем обработчики для ay и il + if (!formulaFields.includes('ay')) { + refreshCode += ` "ay": async function(frm) { + // Вычисляем универсальные поля только когда меняется ay + await calculate_universal_fields(frm); },\n`; + + refreshCode += ` "il": async function(frm) { + // Вычисляем универсальные поля только когда меняется il + await calculate_universal_fields(frm); + },\n`; + } + + return refreshCode; +} - // Добавляем обработку универсальных полей в функцию transform_expression -// Функция для преобразования выражений формул в generate_client_script -functions.push(` +function generateTransformExpressionFunction(universal_fields) { + // Получаем список табличных префиксов для включения в сгенерированный скрипт + const tablePrefixes = getTablePrefixes(); + const tablePrefixesJSON = JSON.stringify(tablePrefixes); + + return ` // Функция для преобразования выражений формул function transform_expression(expression, for_child_table = false, table_name = null) { + console.log("🔄 Исходная формула:", expression, "для табличной части:", for_child_table, "таблица:", table_name); + let transformed = expression; // Обработка универсальных полей @@ -231,80 +586,130 @@ functions.push(` 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)\`; - }); + // Список известных префиксов таблиц + const tablePrefixes = ${tablePrefixesJSON}; - // Остальной код... - } -`); - - 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}")` + // Функция суммирования по табличной части обрабатывается первой + for (const tablePrefix of tablePrefixes) { + const sumPattern = new RegExp(\`sum\\\\(\${tablePrefix}_([^)]+)\\\\)\`, 'g'); + transformed = transformed.replace(sumPattern, (match, fieldName) => { + return \`(frm.doc["\${tablePrefix}"] && frm.doc["\${tablePrefix}"].reduce((sum, row) => sum + (parseFloat(row["\${fieldName}"]) || 0), 0) || 0)\`; }); - - // Добавляем триггеры - triggers.forEach(trigger => { - table_child_scripts[table_name].fields.add(trigger); + + // Проверяем использование val() для табличных полей (должны использоваться sum()) + const valTablePattern = new RegExp(\`val\\\\(\${tablePrefix}_([^)]+)\\\\)\`, 'g'); + transformed = transformed.replace(valTablePattern, (match, fieldName) => { + console.warn(\`⚠️ Использование val() для табличного поля. Заменено на sum(): \${match}\`); + return \`(frm.doc["\${tablePrefix}"] && frm.doc["\${tablePrefix}"].reduce((sum, row) => sum + (parseFloat(row["\${fieldName}"]) || 0), 0) || 0)\`; + }); + } + + // Если мы трансформируем выражение для табличной части + if (for_child_table) { + // Заменяем val() на row[] для всех полей внутри табличной части + transformed = transformed.replace(/val\\(([^)]+)\\)/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\\(([^)]+)\\)/g, (match, field) => { + return \`row["\${field}"]\`; }); } else { - // Обычная функция расчета для поля основного документа - 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`; + // Стандартная замена для обычных полей + transformed = transformed.replace(/val\\(([^)]+)\\)/g, (match, field) => { + return \`frm.doc["\${field}"]\`; + }); + + // Предупреждение, если row() используется вне таблицы + transformed = transformed.replace(/row\\(([^)]+)\\)/g, (match, field) => { + console.warn("⚠️ Функция row() используется вне контекста табличной части"); + return \`0 /* ERROR: row() вне табличной части */\`; }); } - }); + + // Обработка процентов + transformed = transformed.replace(/(frm\\.doc\\["[^"]+"\]|\\d+|row\\["[^"]+"\])\\s*([+\\-])\\s*(\\d+(\\.\\d+)?)%/g, (match, base, operator, percent) => { + console.log("✅ Обнаружен процент в выражении:", match); + return \`\${base} \${operator} ((\${base} * \${percent}) / 100)\`; + }); + + console.log("🔄 Трансформированная формула:", transformed); + return transformed; + }`; +} - main_script += `});`; +function processFunctionField(field_info, fieldname, functions, table_child_scripts, + table_doctypes, main_script, target_doctype) { - // Добавляем обработчики для табличных частей +let formula = field_info.formula.trim(); + +// Если формула пустая, просто выходим +if (!formula) { +return; +} + +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) { +// Для поля табличной части +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`, +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 formula_expr = ${modified_formula}; +let result = eval(formula_expr); +frm.set_value("${fieldname}", result); +} catch (e) { +} +}`); + +// Добавляем триггеры для основного документа, но только если формула не пустая и есть триггеры +if (triggers.length > 0) { +triggers.forEach(trigger => { +main_script += ` "${trigger}": function(frm) { calculate_${fieldname}(frm); },\n`; +}); +} +} +} + +function generateTableScripts(table_child_scripts, functions) { + let tableScripts = ''; + 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`; @@ -316,7 +721,7 @@ functions.push(` try {`; - // Добавляем расчеты для каждого поля табличной части с правильным форматированием + // Добавляем расчеты для каждого поля табличной части table_child_scripts[table_name].calculations.forEach(calc => { calcFunction += ` // Расчет для поля ${calc.field} @@ -327,7 +732,6 @@ functions.push(` calcFunction += ` frm.refresh_field("${table_name}"); } catch (e) { - console.error("❌ Ошибка в формуле для таблицы ${table_name}:", e); } }`; @@ -339,97 +743,114 @@ functions.push(` }); table_script += `});`; - main_script += table_script; + tableScripts += 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); + + return tableScripts; } 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.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); - } + // Имя для нашего клиентского скрипта формул + const formula_script_name = `Formula_Editor_Script_for_${target_doctype}`; + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Client Script", + filters: { dt: target_doctype }, + fields: ["name", "script"] + }, + callback: function(r) { + if (r.message && r.message.length > 0) { + // Находим наш скрипт по имени или по содержимому + let formula_script = r.message.find(script => + script.name === formula_script_name || + (script.script && script.script.includes('// Generated by Formula Editor')) + ); + + if (formula_script) { + console.log(`🔄 Найден существующий Formula Editor Script: ${formula_script.name}. Обновляем...`); + + frappe.call({ + method: "frappe.client.set_value", + args: { + doctype: "Client Script", + name: formula_script.name, + fieldname: { + script: script_code, + // Обновляем имя, если оно было другим + name: formula_script_name + } + }, + callback: function(response) { + if (response.message) { + console.log(`✅ Formula Editor Script обновлен для ${target_doctype}`); + } else { + console.error("❌ Ошибка при обновлении Formula Editor Script", response); + } + } + }); + } else { + console.log("⚠️ Существующий Formula Editor Script не найден. Создаем новый..."); + createNewFormulaScript(target_doctype, script_code, formula_script_name); } - }); - } 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); - } - } - }); + } else { + console.log("⚠️ Клиентских скриптов для DocType не найдено. Создаем новый..."); + createNewFormulaScript(target_doctype, script_code, formula_script_name); + } } - } -}); + }); } +function createNewFormulaScript(target_doctype, script_code, script_name) { + // Добавляем комментарий для идентификации скрипта в будущем + const marked_script_code = `// Generated by Formula Editor\n${script_code}`; + + frappe.call({ + method: "frappe.client.insert", + args: { + doc: { + doctype: "Client Script", + name: script_name, + dt: target_doctype, + script_type: "Client", + script: marked_script_code, + enabled: 1 + } + }, + callback: function(response) { + if (response.message) { + console.log(`✅ Formula Editor Script создан для ${target_doctype} с именем ${script_name}`); + } else { + console.error("❌ Ошибка при создании Formula Editor Script", response); + } + } + }); +} + +// UI компоненты - создание и обработка полей function generate_dynamic_fields(frm, doctype, callback) { - console.log("🔄 Получаем поля для Doctype:", doctype); frappe.call({ method: "formula_editor.api.get_doctype_fields", args: { doctype: doctype }, callback: function(r) { if (!r.message || r.message.length === 0) { - 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; @@ -450,15 +871,13 @@ function generate_dynamic_fields(frm, doctype, callback) { let container = $('
'); - // Создаем модальное окно формульного редактора - createFormulaBuilderModal(); + // Создаем модальные окна + createModals(); - // Создаем модальное окно редактора полей изменения - createTriggerFieldsModal(); + // Добавляем секцию универсальных полей + addUniversalFieldsSection(wrapper); - // Создаем модальное окно универсальных полей - createUniversalFieldsModal(); - + // Создаем поля для каждой группы Object.keys(grouped_fields).forEach(group => { let section_name = group === "Основные поля" ? group : `${group} (${group.replace(/\s+/g, '_').toLowerCase()})`; let section = $('
'); @@ -468,996 +887,66 @@ function generate_dynamic_fields(frm, doctype, callback) { let is_table_field = table_fieldnames.has(field.fieldname); let dataAttr = is_table_field ? 'data-is-table-field="true"' : ''; - let field_html = $('
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '' + - '
' + - '' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '' + - '
' + - '' + - '
' + - '
' + - '
' + - '
'); + let field_html = createFormulaFieldHtml(field, dataAttr); section.append(field_html); }); container.append(section); }); - - // Добавляем секцию универсальных полей - addUniversalFieldsSection(wrapper); // Добавляем основной контейнер формул wrapper.append(container); - // Инициализируем универсальные поля + // Инициализируем и отрисовываем универсальные поля initUniversalFields(frm); - - // Отрисовываем существующие универсальные поля renderUniversalFields(frm); + // Прикрепляем обработчики событий 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 createModals() { + createFormulaBuilderModal(); + createTriggerFieldsModal(); + createUniversalFieldsModal(); } -function createFormulaBuilderModal() { -if ($("#formula_builder_modal").length) { - return; +// Создание HTML для поля формулы +function createFormulaFieldHtml(field, dataAttr) { + 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') { - // Если это таблица без подчеркивания (например, 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(' '); - } - } - - // Переносим курсор в конец контейнера - 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) { - 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'); - }); - - // Обработчик для кнопки применения формулы - $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.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 - })); - - // Добавляем класс перетаскивания - $(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 === '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); - } - }); -} - -// Обновляем функцию 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|univ)\(([^)]+)\)/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') { - // Определяем класс в зависимости от типа функции - 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(); - -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) { // Получаем части имени поля // Проверяем существование имен групп "items", "taxes" и т.д. без подчеркивания @@ -1524,171 +1013,1050 @@ function determineFieldType(fieldName, group, isInTableField) { } } +function setEndOfContentEditable(contentEditableElement) { + let range, selection; + range = document.createRange(); + range.selectNodeContents(contentEditableElement); + range.collapse(false); + selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); +} + +// Функции для работы с формулами +function getFormulaText($container) { + // Собираем все токены и текст в строку + let formula = ''; + + $container.contents().each(function() { + if (this.nodeType === Node.TEXT_NODE) { + formula += this.textContent; + } else { + const $elem = $(this); + if ($elem.hasClass('formula-token')) { + formula += $elem.data('value'); + } + } + }); + + // Удаляем лишние пробелы + return formula.replace(/\s+/g, ' ').trim(); +} + +function parseAndRenderFormula($container, formula, isTableField) { + if (!formula) return; + + // Регулярные выражения для поиска функций и операторов + const funcRegex = /(val|row|sum|univ)\(([^)]+)\)/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') { + // Определяем класс в зависимости от типа функции + 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} `)); + } +}); + +// Добавляем обработчики для токенов +addTokenHandlers($container); + +return formula; +} + +function addTokenHandlers($container) { +// Удаление токенов +$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; + }); + + $(this).off('dragend').on('dragend', function() { + window.draggedElement = null; + }); +}); +} + +// Функции для создания модальных окон +function createFormulaBuilderModal() { +if ($("#formula_builder_modal").length) { + return; +} + +const modalHtml = ` + +`; + +// Стили с поддержкой перетаскивания +const modalStyles = ` + +`; + +$('body').append(modalHtml); +$('head').append(modalStyles); +} + function createTriggerFieldsModal() { - if ($("#trigger_fields_modal").length) { +if ($("#trigger_fields_modal").length) { + return; +} + +const modalHtml = ` + +`; + +$('body').append(modalHtml); +} + +function createUniversalFieldsModal() { + if ($("#universal_fields_modal").length) { return; } const modalHtml = ` - `; $('body').append(modalHtml); - $('head').append(modalStyles); +} + +// Обработчики событий редактора формул +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 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); + +// Обработчики для кнопок операторов +$modal.find('.operator-btn').off('click').on('click', function() { + const operator = $(this).data('operator'); + addTokenToFormula(operator, 'operator', $resultContainer); +}); + +// Обработчик для кнопки применения формулы +$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', $resultContainer, isTableField, $(this).data('group')); +}); + +// Обработчики для кнопок универсальных полей +$modal.on('click', '.universal-field-btn', function() { + const fieldId = $(this).data('field-id'); + addTokenToFormula(fieldId, 'universal-field', $resultContainer, isTableField, null, frm); +}); + +// Показываем модальное окно +$modal.modal('show'); + +// Инициализируем перетаскивание +initDragAndDrop($modal, $resultContainer, isTableField, frm); + +// Настраиваем редактируемый контейнер +setupEditableContainer($resultContainer); + +// Фокусируемся на редактируемом поле +$resultContainer.focus(); +} + +function addTokenToFormula(token, type, $resultContainer, isTableField = false, group = null, frm = null) { +if (type === 'field') { + let formulaText; + + // Определяем тип поля и формулу + const fieldInfo = determineFieldType(token, group, isTableField); + + if (fieldInfo.type === 'row') { + formulaText = `row(${fieldInfo.fieldName})`; + } else if (fieldInfo.type === 'sum') { + 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(' '); + } +} + +// Переносим курсор в конец контейнера +setEndOfContentEditable($resultContainer[0]); + +// Добавляем обработчики для токенов +addTokenHandlers($resultContainer); +} + +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 $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, $resultContainer, isTableField, frm) { +// Очищаем предыдущие обработчики +$modal.find('.field-btn, .operator-btn').off('dragstart'); +$resultContainer.off('dragover dragleave drop'); + +// Настраиваем перетаскивание существующих токенов +setupTokenDragging($resultContainer); + +// Настраиваем перетаскивание полей из панели +$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 + })); +}); + +// Обработчики для целевого контейнера +$resultContainer.on('dragover', function(e) { + e.preventDefault(); + e.stopPropagation(); + $(this).addClass('drag-hover'); + + updateDropMarker(e, $(this)); +}); + +$resultContainer.on('dragleave', function(e) { + $(this).removeClass('drag-hover'); + $(this).find('.drop-marker').remove(); +}); + +$resultContainer.on('drop', function(e) { + e.preventDefault(); + e.stopPropagation(); + $(this).removeClass('drag-hover'); + $(this).find('.drop-marker').remove(); + + handleDrop(e, $(this), isTableField, frm); +}); +} + +function setupTokenDragging($container) { +$container.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; + }); +}); +} + +function updateDropMarker(e, $container) { + const x = e.originalEvent.clientX; + const y = e.originalEvent.clientY; + + let position = { element: null, position: 'after' }; + + // Проходим по всем дочерним элементам контейнера + $container.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; // Прерываем перебор + } + }); + + $container.find('.drop-marker').remove(); + + if (position.element) { + const $marker = $(''); + + if (position.position === 'before') { + $(position.element).before($marker); + } else { + $(position.element).after($marker); + } + } +} + +function handleDrop(e, $container, isTableField, frm) { + try { + const data = JSON.parse(e.originalEvent.dataTransfer.getData('text/plain')); + + const x = e.originalEvent.clientX; + const y = e.originalEvent.clientY; + + let position = findDropPosition(x, y, $container); + + // Обработка перетаскивания существующих токенов + if (data.type === 'token' && window.draggedElement) { + handleExistingTokenDrop(window.draggedElement, position, $container); + return; + } + + // Создание нового элемента + let $newElement; + + if (data.type === 'universal-field') { + $newElement = createUniversalFieldToken(data, frm); + } else if (data.type === 'field') { + $newElement = createFieldToken(data, isTableField); + } else if (data.type === 'operator') { + $newElement = createOperatorToken(data.value); + } + + // Вставляем новый элемент + if ($newElement) { + insertElementAtPosition($newElement, position, $container); + + // Инициализируем события для нового элемента + initializeTokenEvents($newElement); + } + } catch (error) { + } +} + +function findDropPosition(x, y, $container) { + let position = { element: null, position: 'after' }; + + // Проходим по всем дочерним элементам контейнера + $container.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; // Прерываем перебор + } + }); + + return position; +} + +function handleExistingTokenDrop(draggedElement, position, $container) { + const $dragged = $(draggedElement); + + if (position.element) { + if (position.position === 'before') { + $(position.element).before($dragged); + $(position.element).before(' '); + } else { + $(position.element).after($dragged); + $(position.element).after(' '); + } + } else { + $container.append($dragged); + $container.append(' '); + } + + window.draggedElement = null; +} + +function createUniversalFieldToken(data, frm) { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + const field = universalFields[data.fieldId]; + if (!field) return null; + + const formulaText = `univ(${field.name})`; + return $(`${formulaText}×`); +} + +function createFieldToken(data, isTableField) { + let formulaText; + + // Определяем тип поля и формулу + const fieldInfo = determineFieldType(data.fieldname, data.group, isTableField); + + if (fieldInfo.type === 'row') { + formulaText = `row(${fieldInfo.fieldName})`; + } else if (fieldInfo.type === 'sum') { + formulaText = `sum(${fieldInfo.fieldName})`; + } else { + formulaText = `val(${fieldInfo.fieldName})`; + } + + return $(`${formulaText}×`); +} + +function createOperatorToken(value) { + return $(`${value}×`); +} + +function insertElementAtPosition($element, position, $container) { + if (position.element) { + if (position.position === 'before') { + $(position.element).before($element); + $(position.element).before(' '); + } else { + $(position.element).after($element); + $(position.element).after(' '); + } + } else { + $container.append($element); + $container.append(' '); + } +} + +function initializeTokenEvents($token) { + $token.find('.token-remove').on('click', function(e) { + e.stopPropagation(); + $(this).parent().remove(); + }); + + $token.attr('draggable', 'true'); + + $token.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; + }); + + $token.off('dragend').on('dragend', function(e) { + $(this).removeClass('dragging'); + window.draggedElement = null; + }); +} + +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 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 openTriggerFieldsEditor(frm, targetFieldname, currentTriggers, fields, grouped_fields) { @@ -1711,47 +2079,6 @@ function openTriggerFieldsEditor(frm, targetFieldname, currentTriggers, fields, // Сохраняем данные о текущем поле $modal.data("target-field", targetFieldname); - // Функция добавления токена в результат - function addTokenToTriggers(fieldname) { - // Проверяем, не добавлено ли уже это поле - const existingTokens = $resultContainer.find(`.trigger-token[data-value="${fieldname}"]`); - if (existingTokens.length > 0) { - return; // Если поле уже добавлено, не добавляем его снова - } - - const $token = $(`${fieldname}×`); - $resultContainer.append($token).append(' '); - - // Переносим курсор в конец контейнера - setEndOfContentEditable($resultContainer[0]); - - // Добавляем обработчики для удаления токенов - attachTriggerTokenEvents(); - } - - // Обработчики событий для токенов триггеров - function attachTriggerTokenEvents() { - // Удаление токена - $resultContainer.find('.token-remove').off('click').on('click', function(e) { - e.stopPropagation(); - $(this).parent().remove(); - }); - - // Перетаскивание токенов - $resultContainer.find('.trigger-token').each(function() { - $(this).attr('draggable', 'true'); - - $(this).off('dragstart').on('dragstart', function(e) { - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'trigger-token', - value: $(this).data('value'), - html: $(this)[0].outerHTML - })); - window.draggedTriggerElement = this; - }); - }); - } - // Обработчик для кнопки применения триггеров $modal.find('.apply-triggers').off('click').on('click', function() { const triggers = getTriggerFieldsText($resultContainer); @@ -1760,17 +2087,17 @@ function openTriggerFieldsEditor(frm, targetFieldname, currentTriggers, fields, $modal.modal('hide'); }); - // Обработчики для кнопок полей - используем делегирование событий + // Обработчики для кнопок полей $modal.on('click', '.trigger-field-btn', function() { const fieldname = $(this).data('fieldname'); - addTokenToTriggers(fieldname); + addTokenToTriggers(fieldname, $resultContainer); }); // Показываем модальное окно $modal.modal('show'); // Инициализируем перетаскивание - initTriggerDragAndDrop($modal); + initTriggerDragAndDrop($modal, $resultContainer); // Настраиваем редактируемый контейнер setupTriggerEditableContainer($resultContainer); @@ -1793,7 +2120,7 @@ function populateTriggerFieldsPanel($modal, frm, grouped_fields) { const $buttonsContainer = $fieldGroup.find('.trigger-field-buttons-container'); grouped_fields[group].forEach(field => { - // Создаем кнопку поля с правильными атрибутами для drag-and-drop + // Создаем кнопку поля const $fieldBtn = $(` - - - - - - - `; - - 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('

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

'); @@ -2294,239 +2478,597 @@ function addUniversalFieldsSection(wrapper) { 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 initUniversalFields(frm) { + // Если хранилище универсальных полей не существует, создаем его + if (!frm.doc.universal_fields_storage) { + frm.set_value("universal_fields_storage", "{}"); } + + // Инициализируем обработчики событий + attachUniversalFieldsEvents(frm); + + // Для использования стилей + addUniversalFieldsStyles(); +} + +function addUniversalFieldsStyles() { + if ($('#universal-fields-styles').length) return; + + const styles = ` + `; + + $('head').append(styles); } -// Функция для прикрепления обработчиков событий универсальных полей 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(); - } + // Обработчик для клика по кнопке редактирования + $(frm.wrapper).on('click', '.edit-universal-field', function(e) { + e.stopPropagation(); + const fieldId = $(this).data('field-id'); + editUniversalField(frm, fieldId); }); - // При выборе поля, показываем контейнер фильтров - $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); + // Обработчик для клика по кнопке удаления + $(frm.wrapper).on('click', '.delete-universal-field', function(e) { + e.stopPropagation(); + const fieldId = $(this).data('field-id'); + deleteUniversalField(frm, fieldId); }); } -// Функция для открытия модального окна создания/редактирования универсального поля function openUniversalFieldModal(frm, fieldId = null) { - const $modal = $("#universal_fields_modal"); - const isEditing = fieldId !== null; - // Обновляем заголовок в зависимости от режима - $modal.find('.modal-title').text(isEditing ? 'Редактирование универсального поля' : 'Создание универсального поля'); + // Получаем значения для редактирования (если указан fieldId) + let fieldValues = {}; + if (fieldId) { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + fieldValues = universalFields[fieldId] || {}; + } - // Очищаем форму - $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); + // Создаем диалог + const d = new frappe.ui.Dialog({ + title: fieldId ? 'Редактирование универсального поля' : 'Создание универсального поля', + fields: [ + { + label: 'Название поля', + fieldname: 'name', + fieldtype: 'Data', + reqd: 1, + default: fieldValues.name || '' + }, + { + label: 'Тип документа', + fieldname: 'doctype', + fieldtype: 'Link', + options: 'DocType', + reqd: 1, + default: fieldValues.doctype || '', + filters: { + "istable": 0, + "issingle": 0 + }, + onchange: function() { + const selectedDoctype = d.get_value('doctype'); + + if (selectedDoctype) { + // Загружаем поля выбранного DocType + frappe.model.with_doctype(selectedDoctype, function() { + const meta = frappe.get_meta(selectedDoctype); + + if (!meta) { + frappe.msgprint({ + title: __('Ошибка'), + message: __(`DocType ${selectedDoctype} не найден`), + indicator: 'red' + }); + return; + } + + // Фильтруем только числовые поля + const numericFields = meta.fields.filter(field => + ['Int', 'Float', 'Currency'].includes(field.fieldtype) + ); + + if (numericFields.length === 0) { + frappe.msgprint({ + title: __('Предупреждение'), + message: __(`В DocType ${selectedDoctype} не найдено числовых полей`), + indicator: 'orange' + }); + return; + } + + // Обновляем опции для поля fieldname + const options = numericFields.map(field => ({ + label: field.label || field.fieldname, + value: field.fieldname + })); + + d.set_df_property('fieldname', 'options', options); + + // Если есть значение поля для редактирования, выбираем его + if (fieldValues.fieldname) { + d.set_value('fieldname', fieldValues.fieldname); + } + + // Обновляем опции для поля фильтра + const filterFieldOptions = meta.fields + .filter(field => + ['Int', 'Float', 'Currency', 'Data', 'Text', 'Date', 'Datetime', + 'Check', 'Select', 'Link', 'Small Text', 'Read Only'].includes(field.fieldtype) + ) + .map(field => ({ + label: field.label || field.fieldname, + value: field.fieldname + })); + + // Сохраняем информацию о полях для использования в фильтрах + d.docFields = meta.fields; + + // ВАЖНО: Сохраняем docFields также в глобальной переменной + window.docFields = meta.fields; + + // Обновляем опции для фильтров + if (d.fields_dict.filters && d.fields_dict.filters.grid) { + d.fields_dict.filters.grid.update_docfield_property('field', 'options', filterFieldOptions); + } + }); + } + } + }, + { + label: 'Поле документа', + fieldname: 'fieldname', + fieldtype: 'Select', + options: [], + reqd: 1, + default: fieldValues.fieldname || '' + }, + { + label: 'Фильтры', + fieldname: 'filters', + fieldtype: 'Table', + fields: [ + { + label: 'Поле', + fieldname: 'field', + fieldtype: 'Select', + options: [], + reqd: 1, + in_list_view: 1, + columns: 4, + change: function() { + const grid_row = this.grid_row; + const fieldname = this.get_value(); + + if (d.docFields && fieldname) { + const fieldInfo = d.docFields.find(f => f.fieldname === fieldname); + + if (fieldInfo) { + // Изменяем тип поля значения + handleFilterFieldChange(d, grid_row, fieldInfo); + } else { + } + } else { + } + } + }, + { + label: 'Оператор', + fieldname: 'operator', + fieldtype: 'Select', + options: [ + {label: '=', value: '='}, + {label: '!=', value: '!='}, + {label: '>', value: '>'}, + {label: '>=', value: '>='}, + {label: '<', value: '<'}, + {label: '<=', value: '<='}, + {label: 'Содержит', value: 'like'}, + {label: 'Не содержит', value: 'not like'}, + {label: 'В списке', value: 'in'}, + {label: 'Не в списке', value: 'not in'} + ], + default: '=', + reqd: 1, + in_list_view: 1, + columns: 3 + }, + { + label: 'Значение', + fieldname: 'value', + fieldtype: 'Data', + in_list_view: 1, + columns: 4 + } + ], + default: fieldValues.filters || [] + } + ], + primary_action_label: fieldId ? 'Обновить' : 'Создать', + primary_action: function() { + const values = d.get_values(); + + // Проверяем, что все обязательные поля заполнены + if (!values.name || !values.doctype || !values.fieldname) { + frappe.msgprint({ + title: __('Внимание'), + message: __('Заполните все обязательные поля'), + indicator: 'orange' + }); + return; + } + + // Сохраняем данные + saveUniversalField(frm, values, fieldId); + + d.hide(); } }); - // Сбрасываем состояние кнопки сохранения - $modal.find('.save-universal-field').prop('disabled', true); + // Если редактируем и есть фильтры, заполняем их + if (fieldId && fieldValues.filters && fieldValues.filters.length) { + d.set_value('filters', fieldValues.filters); + } - // Открываем модальное окно - $modal.modal('show'); + // Безопасно инициализируем обработчики для строк фильтров + try { + if (d.fields_dict.filters && d.fields_dict.filters.grid) { + const filterField = d.fields_dict.filters.grid.get_field('field'); + if (filterField && typeof filterField.refresh === 'function') { + filterField.refresh(); + } + + // Добавляем обработчик на событие добавления строки + d.fields_dict.filters.grid.on('row-added', function(row) { + if (d.docFields) { + // Копируем docFields в window для доступа из обработчиков + window.docFields = d.docFields; + } + }); + } + } catch (error) { + } + + // Показываем диалог + d.show(); +} + +function handleFilterFieldChange(dialog, grid_row, fieldInfo) { + console.log("🔄 handleFilterFieldChange вызван для поля:", fieldInfo); + + // Проверяем, что у нас есть все необходимые объекты + if (!dialog || !grid_row || !fieldInfo) { + console.error("❌ Отсутствуют необходимые параметры для handleFilterFieldChange"); + return; + } + + // Получаем текущее значение + let currentValue = undefined; + try { + if (grid_row.doc) { + currentValue = grid_row.doc.value; + } + } catch (error) { + console.warn("⚠️ Не удалось получить текущее значение:", error); + } + console.log("🔹 Текущее значение:", currentValue); + + // Определяем новый тип поля в зависимости от типа выбранного поля + let fieldtype = 'Data'; + let options = ''; + + switch (fieldInfo.fieldtype) { + case 'Select': + fieldtype = 'Select'; + options = fieldInfo.options; + console.log("🔹 Установка типа Select с опциями:", options); + break; + + case 'Link': + fieldtype = 'Link'; + options = fieldInfo.options; + console.log("🔹 Установка типа Link с опциями:", options); + break; + + case 'Check': + fieldtype = 'Check'; + console.log("🔹 Установка типа Check"); + break; + + case 'Date': + fieldtype = 'Select'; // Используем Select для показа выбора полей даты + + // Получаем target_doctype из основной формы + const target_doctype = cur_frm.doc.target_doctype; + + if (target_doctype) { + console.log("🔹 Target DocType для полей даты:", target_doctype); + + // Загружаем поля целевого DocType + try { + frappe.model.with_doctype(target_doctype, function() { + const targetMeta = frappe.get_meta(target_doctype); + if (targetMeta) { + // Фильтруем только поля типа Date и специальные поля ay/il из целевого DocType + const targetDateFields = targetMeta.fields.filter(field => + field.fieldtype === 'Date' || + field.fieldname === 'ay' || + field.fieldname === 'il' + ); + console.log("🔹 Найденные поля Date и ay/il в target_doctype:", targetDateFields); + + // Проверяем, есть ли специальные поля ay и il одновременно + const hasAyField = targetDateFields.some(field => field.fieldname === 'ay'); + const hasIlField = targetDateFields.some(field => field.fieldname === 'il'); + + if (targetDateFields.length > 0) { + // Создаем опции для полей с датой + options = targetDateFields.filter(field => field.fieldtype === 'Date').map(field => ({ + label: field.label || field.fieldname, + value: `doc.${field.fieldname}` + })); + + // Если есть оба специальных поля, добавляем опцию для выбора месяца + if (hasAyField && hasIlField) { + options.push({ + label: 'Месяц (Ay)', + value: 'doc.ay' + }); + } + + // Добавляем опцию для пользовательского ввода + options.push({ + label: 'Указать дату...', + value: 'custom' + }); + + console.log("🔹 Созданы опции для полей даты из target_doctype:", options); + + // Обновляем свойства поля значения + if (dialog.fields_dict.filters && dialog.fields_dict.filters.grid) { + dialog.fields_dict.filters.grid.update_docfield_property('value', 'fieldtype', fieldtype); + dialog.fields_dict.filters.grid.update_docfield_property('value', 'options', options); + + // Обновляем контрол + if (grid_row && typeof grid_row.refresh === 'function') { + grid_row.refresh(); + } + } + } else { + fieldtype = 'Date'; // Если нет полей с датой, используем обычный Date + console.log("⚠️ Нет полей даты в target_doctype, используем обычный Date"); + } + } else { + fieldtype = 'Date'; // Если не удалось получить метаданные, используем обычный Date + console.log("⚠️ Не удалось получить метаданные target_doctype, используем обычный Date"); + } + }); + } catch (error) { + fieldtype = 'Date'; // В случае ошибки используем обычный Date + console.error("❌ Ошибка при загрузке полей target_doctype:", error); + } + } else { + fieldtype = 'Date'; // Если target_doctype не указан, используем обычный Date + console.log("⚠️ target_doctype не указан, используем обычный Date"); + } + break; + + case 'Datetime': + fieldtype = 'Datetime'; + console.log("🔹 Установка типа Datetime"); + break; + + case 'Int': + case 'Float': + case 'Currency': + fieldtype = 'Float'; + console.log("🔹 Установка числового типа:", fieldtype); + break; + + default: + fieldtype = 'Data'; + console.log("🔹 Установка типа по умолчанию: Data"); + } + + console.log("🔄 Обновляем свойства поля значения: тип =", fieldtype, "опции =", options); + + try { + // Обновляем свойства поля значения (только для не-Date полей, для Date это делается в асинхронном колбэке) + if (fieldInfo.fieldtype !== 'Date' && dialog.fields_dict.filters && dialog.fields_dict.filters.grid) { + dialog.fields_dict.filters.grid.update_docfield_property('value', 'fieldtype', fieldtype); + dialog.fields_dict.filters.grid.update_docfield_property('value', 'options', options); + } + + // Обработчик для опции "Указать дату..." и опции "Месяц (Ay)" (для типа Date) + if (fieldInfo.fieldtype === 'Date' && fieldtype === 'Select') { + // Добавляем обработчик для изменения селекта поля даты + try { + // Безопасно устанавливаем обработчик onchange + if (dialog.fields_dict.filters.grid.fields_dict && dialog.fields_dict.filters.grid.fields_dict.value) { + const valueField = dialog.fields_dict.filters.grid.fields_dict.value; + if (valueField.df) { + valueField.df.onchange = function(e) { + try { + // Для случаев, когда this.get_value не существует или не работает + let value = ''; + + // Пробуем разные способы получить значение + if (this.get_value && typeof this.get_value === 'function') { + value = this.get_value(); + } else if (e && e.target) { + value = e.target.value; + } else if (grid_row && grid_row.doc) { + value = grid_row.doc.value; + } + + console.log("🔄 Изменение значения даты:", value); + + if (value === 'custom') { + console.log("🔄 Переключение на ручной ввод даты"); + // Переключаем на ручной ввод даты + dialog.fields_dict.filters.grid.update_docfield_property('value', 'fieldtype', 'Date'); + dialog.fields_dict.filters.grid.update_docfield_property('value', 'options', ''); + + // Обновляем контрол + if (grid_row && typeof grid_row.refresh_field === 'function') { + grid_row.refresh_field('value'); + } + } + else if (value === 'doc.ay') { + console.log("🔄 Выбрано специальное поле месяца (Ay)"); + + // Для doc.ay не меняем тип поля, только запоминаем в grid_row, что это специальный случай + if (grid_row && grid_row.doc) { + grid_row.doc.__is_special_ay_field = true; + } + } + } catch (error) { + console.error("❌ Ошибка при обработке изменения значения даты:", error); + } + }; + } + } + } catch (error) { + console.error("❌ Ошибка при настройке обработчика изменения поля даты:", error); + } + } + } catch (error) { + console.error("❌ Ошибка при обновлении свойств поля:", error); + } + + try { + // Пересоздаем контрол (только для не-Date полей, для Date это делается в асинхронном колбэке) + if (fieldInfo.fieldtype !== 'Date' && grid_row && typeof grid_row.refresh === 'function') { + grid_row.refresh(); + console.log("🔄 Контрол обновлен"); + } + + // Устанавливаем значение обратно + if (currentValue !== undefined && grid_row && grid_row.doc) { + grid_row.doc.value = currentValue; + if (typeof grid_row.refresh_field === 'function') { + grid_row.refresh_field('value'); + } + console.log("✅ Значение восстановлено:", currentValue); + } + } catch (error) { + console.error("❌ Ошибка при обновлении контрола:", error); + } + + console.log("✅ handleFilterFieldChange завершен"); } -// Функция для загрузки списка DocTypes function loadDoctypes() { return new Promise((resolve) => { - const $select = $("#universal_field_doctype"); + // Инициализируем список DocType пустым массивом + window.allDoctypes = []; frappe.call({ - method: "frappe.client.get_list", + method: "frappe.desk.search.search_link", args: { doctype: "DocType", - filters: { - istable: 0, - issingle: 0 - }, - fields: ["name"], - limit_page_length: 500, - order_by: "name asc" + txt: "", + filters: {"issingle": 0} }, callback: function(r) { - $select.empty(); - $select.append(''); - - if (r.message) { - r.message.forEach(doc => { - $select.append(``); - }); + if (r.results && r.results.length) { + // Сохраняем полный список DocType + window.allDoctypes = r.results.map(doc => doc.value || doc.name); } - + initDoctypeAutocomplete(); resolve(); } }); }); } -// Функция для загрузки полей DocType +function initDoctypeAutocomplete() { + const $input = $("#universal_field_doctype"); + const $dropdown = $(".doctype-dropdown"); + + // Обработчик ввода для поиска DocType + $input.off('input').on('input', function() { + const searchTerm = $(this).val().toLowerCase(); + + if (searchTerm.length < 1) { + $dropdown.hide(); + return; + } + + // Делаем поиск через API для каждого ввода + frappe.call({ + method: "frappe.desk.search.search_link", + args: { + doctype: "DocType", + txt: searchTerm, + filters: {"issingle": 0} + }, + callback: function(r) { + if (r.results && r.results.length) { + $dropdown.empty(); + r.results.forEach(item => { + $dropdown.append(``); + }); + $dropdown.show(); + } else { + $dropdown.empty(); + $dropdown.append(``); + $dropdown.show(); + } + } + }); + }); + + // Обработчик выбора DocType из выпадающего списка + $(document).off('click', '.doctype-item').on('click', '.doctype-item', function() { + const itemText = $(this).text(); + + // Проверяем, не является ли это сообщением об ошибке + if (itemText === 'DocType не найден' || itemText === 'Загрузка списка DocType...') { + return; + } + + const selectedDoctype = itemText; + $input.val(selectedDoctype); + $dropdown.hide(); + + // Загружаем поля выбранного DocType + loadDoctypeFields(selectedDoctype); + }); +} + function loadDoctypeFields(doctype) { const $modal = $("#universal_fields_modal"); const $fieldsSelect = $modal.find('#universal_field_fieldname'); @@ -2535,44 +3077,69 @@ function loadDoctypeFields(doctype) { // Показываем индикатор загрузки $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(``); - } - }); + // Получаем поля DocType + frappe.model.with_doctype(doctype, function() { + const meta = frappe.get_meta(doctype); + + if (!meta) { + frappe.msgprint({ + title: __('Ошибка'), + message: __(`DocType ${doctype} не найден`), + indicator: 'red' + }); + $fieldsSelect.html(''); + $modal.find('.universal-fields-container').hide(); + return; + } + + $fieldsSelect.empty(); + $fieldsSelect.append(''); + + $filterFieldsSelect.empty(); + $filterFieldsSelect.append(''); + + // Фильтруем поля только для числовых типов для основного поля + const numericTypes = ['Int', 'Float', 'Currency']; + + // Для фильтров разрешаем гораздо больше типов + const filterTypes = [ + 'Int', 'Float', 'Currency', 'Data', 'Text', 'Date', 'Datetime', + 'Check', 'Select', 'Link', 'Small Text', 'Read Only' + ]; + + // Сохраняем все поля для использования в фильтрах + window.docFields = meta.fields; + + let hasNumericFields = false; + + meta.fields.forEach(field => { + // Для основного поля - только числовые типы + if (field.fieldname && numericTypes.includes(field.fieldtype)) { + $fieldsSelect.append(``); + hasNumericFields = true; } - // Показываем контейнер полей + // Для фильтров - более широкий набор типов + if (field.fieldname && filterTypes.includes(field.fieldtype)) { + $filterFieldsSelect.append(``); + } + }); + + // Показываем контейнер полей только если найдены числовые поля + if (hasNumericFields) { $modal.find('.universal-fields-container').show(); + } else { + $modal.find('.universal-fields-container').hide(); + frappe.msgprint({ + title: __('Предупреждение'), + message: __(`В DocType ${doctype} не найдено числовых полей`), + indicator: 'orange' + }); } }); } -// Функция для добавления строки фильтра -function addFilterRow() { - const $modal = $("#universal_fields_modal"); +function addFilterRow($modal) { const $filters = $modal.find('.universal-filters'); const doctype = $modal.find('#universal_field_doctype').val(); @@ -2600,7 +3167,7 @@ function addFilterRow() { -
+
@@ -2615,38 +3182,301 @@ function addFilterRow() { $newRow.find('.filter-field').html($firstFilterSelect.html()); $filters.append($newRow); + + // Добавляем обработчик изменения поля фильтра + $newRow.find('.filter-field').on('change', function() { + const fieldname = $(this).val(); + const fieldInfo = getFieldInfo(fieldname); + updateFilterValueField($(this).closest('.filter-row'), fieldInfo); + }); } -// Функция для валидации формы -function validateUniversalFieldForm() { - const $modal = $("#universal_fields_modal"); +function getFieldInfo(fieldname) { + if (!window.docFields || !fieldname) return null; + + return window.docFields.find(field => field.fieldname === fieldname); +} + +function updateFilterValueField($filterRow, fieldInfo) { + const $valueContainer = $filterRow.find('.filter-value-container'); + const $operator = $filterRow.find('.filter-operator'); + + console.log("⚙️ updateFilterValueField вызван для поля:", fieldInfo); + + // Если информация о поле отсутствует, возвращаем обычное текстовое поле + if (!fieldInfo) { + console.log("❌ Отсутствует информация о поле, использую текстовое поле"); + $valueContainer.html(''); + return; + } + + console.log("🔍 Определение типа ввода для поля типа:", fieldInfo.fieldtype); + + // В зависимости от типа поля создаем соответствующий элемент ввода + switch (fieldInfo.fieldtype) { + case 'Date': + console.log("🔍 Обрабатываю поле типа Date"); + // Получаем target_doctype из основной формы + const target_doctype = cur_frm.doc.target_doctype; + console.log("🔹 Target DocType для полей даты:", target_doctype); + + if (target_doctype) { + // Загружаем поля целевого DocType + frappe.model.with_doctype(target_doctype, function() { + const targetMeta = frappe.get_meta(target_doctype); + if (targetMeta) { + // Фильтруем поля даты и специальные поля ay/il + const targetDateFields = targetMeta.fields.filter(field => + field.fieldtype === 'Date' || + field.fieldname === 'ay' || + field.fieldname === 'il' + ); + console.log("🔹 Найденные поля Date и ay/il в target_doctype:", targetDateFields); + + // Проверяем, есть ли специальные поля ay и il одновременно + const hasAyField = targetDateFields.some(field => field.fieldname === 'ay'); + const hasIlField = targetDateFields.some(field => field.fieldname === 'il'); + + if (targetDateFields.length > 0) { + console.log("✅ Создаю селект с полями даты из target_doctype"); + // Создаем селект с опциями из полей с датой + let selectHtml = ''; + + $valueContainer.html(selectHtml); + + // Добавляем обработчик для опции "Указать дату..." + $valueContainer.find('.filter-value').on('change', function() { + const selectedValue = $(this).val(); + console.log("🔄 Изменение значения поля даты:", selectedValue); + + if (selectedValue === 'custom') { + console.log("🔄 Переключение на ручной ввод даты"); + $valueContainer.html(''); + } + else if (selectedValue === 'doc.ay') { + console.log("🔄 Выбрано специальное поле месяца (Ay)"); + // Помечаем в data-атрибуте, что это специальное поле ay + $(this).attr('data-is-special-ay-field', 'true'); + } + }); + } else { + console.log("⚠️ Не найдено полей с типом Date в target_doctype, использую стандартный ввод даты"); + $valueContainer.html(''); + } + } else { + console.log("⚠️ Не удалось получить метаданные target_doctype, использую стандартный ввод даты"); + $valueContainer.html(''); + } + }); + } else { + console.log("⚠️ target_doctype не указан, использую стандартный ввод даты"); + $valueContainer.html(''); + } + break; + + case 'Datetime': + console.log("🔍 Обрабатываю поле типа Datetime"); + $valueContainer.html(''); + break; + + case 'Select': + console.log("🔍 Обрабатываю поле типа Select с опциями:", fieldInfo.options); + if (fieldInfo.options) { + let selectHtml = ''; + $valueContainer.html(selectHtml); + } else { + $valueContainer.html(''); + } + break; + + case 'Link': + console.log("🔍 Обрабатываю поле типа Link с целью:", fieldInfo.options); + if (fieldInfo.options) { + const targetDoctype = fieldInfo.options; + $valueContainer.html(` +
+ + +
+ `); + + // Инициализация автодополнения для Link полей + initLinkAutocomplete($valueContainer.find('.filter-value')); + } else { + $valueContainer.html(''); + } + break; + + case 'Check': + console.log("🔍 Обрабатываю поле типа Check"); + $valueContainer.html(` + + `); + break; + + case 'Int': + console.log("🔍 Обрабатываю поле типа Int"); + $valueContainer.html(''); + break; + + case 'Float': + case 'Currency': + console.log("🔍 Обрабатываю числовое поле типа:", fieldInfo.fieldtype); + $valueContainer.html(''); + break; + + default: + console.log("🔍 Обрабатываю поле с типом по умолчанию:", fieldInfo.fieldtype); + $valueContainer.html(''); + } + + // Обновляем доступные операторы в зависимости от типа поля + updateOperators($operator, fieldInfo.fieldtype); +} + +function initLinkAutocomplete($input) { + const targetDoctype = $input.data('link-target'); + if (!targetDoctype) return; + + $input.on('focus', function() { + if (!$input.data('initialized')) { + $input.data('initialized', true); + + // Загружаем значения из целевого DocType + frappe.call({ + method: "frappe.desk.search.search_link", + args: { + doctype: targetDoctype, + txt: "" + }, + callback: function(r) { + if (r.results && r.results.length) { + window.linkOptions = r.results.map(item => item.value); + } + } + }); + } + }); + + $input.on('input', function() { + const searchTerm = $(this).val().toLowerCase(); + const $dropdown = $(this).siblings('.link-dropdown'); + + if (!window.linkOptions || searchTerm.length < 1) { + $dropdown.hide(); + return; + } + + // Фильтруем значения по поисковому запросу + const filteredOptions = window.linkOptions.filter( + option => option.toLowerCase().includes(searchTerm) + ); + + // Отображаем результаты в выпадающем списке + if (filteredOptions.length > 0) { + $dropdown.empty(); + filteredOptions.forEach(option => { + $dropdown.append(``); + }); + $dropdown.show(); + } else { + $dropdown.hide(); + } + }); + + // Обработчик выбора значения из выпадающего списка + $(document).on('click', '.link-item', function() { + const selectedValue = $(this).text(); + $(this).closest('.awesomplete').find('.filter-value').val(selectedValue); + $(this).closest('.link-dropdown').hide(); + }); +} + +function updateOperators($operator, fieldtype) { + $operator.empty(); + + // Базовые операторы для всех типов + $operator.append(''); + $operator.append(''); + + // Дополнительные операторы в зависимости от типа + if (['Int', 'Float', 'Currency', 'Date', 'Datetime'].includes(fieldtype)) { + $operator.append(''); + $operator.append(''); + $operator.append(''); + $operator.append(''); + } + + if (['Data', 'Text', 'Small Text', 'Link', 'Select'].includes(fieldtype)) { + $operator.append(''); + $operator.append(''); + } + + if (['Link', 'Select'].includes(fieldtype)) { + $operator.append(''); + $operator.append(''); + } +} + +function validateUniversalFieldForm($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); +function saveUniversalField(frm, values, fieldId = null) { + const newFieldId = fieldId || 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 + key !== newFieldId && universalFields[key].name === values.name ); if (existingField) { @@ -2658,65 +3488,130 @@ function saveUniversalField(frm) { return; } - universalFields[fieldId] = universalField; + // Получаем тип поля + let fieldtype = ''; + if (values.doctype && values.fieldname) { + const meta = frappe.get_meta(values.doctype); + if (meta) { + const field = meta.fields.find(f => f.fieldname === values.fieldname); + if (field) { + fieldtype = field.fieldtype; + } + } + } + + // Проверяем фильтры на наличие специальных полей ay/il + if (values.filters && Array.isArray(values.filters)) { + values.filters.forEach(filter => { + if (typeof filter.value === 'string' && filter.value === 'doc.ay') { + filter.__is_special_ay_field = true; + console.log("🔹 Обнаружен фильтр с использованием специального поля ay:", filter); + } + }); + } + + // Создаем объект универсального поля + universalFields[newFieldId] = { + name: values.name, + doctype: values.doctype, + fieldname: values.fieldname, + fieldtype: fieldtype, + filters: values.filters || [] + }; + frm.set_value("universal_fields_storage", JSON.stringify(universalFields)); - // ... остальной код ... + frappe.show_alert({ + message: __('Универсальное поле сохранено'), + indicator: 'green' + }); + + renderUniversalFields(frm); } catch (error) { - // ... обработка ошибок ... + console.error("Ошибка при сохранении универсального поля:", error); + frappe.msgprint({ + title: __('Ошибка'), + message: __('Не удалось сохранить универсальное поле.'), + indicator: 'red' + }); } } -// Функция для заполнения формы при редактировании -function fillFormForEditing(frm, fieldId) { +function fillFormForEditing(frm, fieldId, $modal) { 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(); + loadDoctypeFields(field.doctype); + + // Настраиваем обработчик для загрузки полей doctype + const loadFieldsWatcher = setInterval(function() { + const $fieldnameSelect = $modal.find('#universal_field_fieldname'); - // Очищаем существующие фильтры - $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(); + if ($fieldnameSelect.find('option').length > 1) { + clearInterval(loadFieldsWatcher); + + $fieldnameSelect.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, index) => { + // Добавляем новую строку фильтра + if (index > 0) { + addFilterRow($modal); + } + + const $filterRow = $modal.find('.filter-row').eq(index); + + // Устанавливаем значения фильтра с небольшой задержкой для загрузки полей + setTimeout(() => { + const $fieldSelect = $filterRow.find('.filter-field'); + $fieldSelect.val(filter.field); + + // Обновляем поле значения в зависимости от типа поля + const fieldInfo = getFieldInfo(filter.field); + if (fieldInfo) { + updateFilterValueField($filterRow, fieldInfo); + + // Устанавливаем оператор и значение + $filterRow.find('.filter-operator').val(filter.operator); + $filterRow.find('.filter-value').val(filter.value); + } + }, 200); + }); + } else { + // Добавляем пустую строку фильтра, если нет сохраненных + addFilterRow($modal); + } + + // Валидируем форму + validateUniversalFieldForm($modal); } - - // Валидируем форму - validateUniversalFieldForm(); - }); + }, 100); + + // Ограничиваем время ожидания 5 секундами + setTimeout(function() { + clearInterval(loadFieldsWatcher); + }, 5000); } catch (error) { - console.error("Ошибка при заполнении формы:", error); } } -// Функция для удаления универсального поля function deleteUniversalField(frm, fieldId) { frappe.confirm( 'Вы уверены, что хотите удалить это универсальное поле?', @@ -2734,7 +3629,6 @@ function deleteUniversalField(frm, fieldId) { indicator: 'green' }); } catch (error) { - console.error("Ошибка при удалении универсального поля:", error); frappe.msgprint({ title: __('Ошибка'), message: __('Не удалось удалить универсальное поле.'), @@ -2745,18 +3639,18 @@ function deleteUniversalField(frm, fieldId) { ); } -// Функция для редактирования универсального поля function editUniversalField(frm, fieldId) { openUniversalFieldModal(frm, fieldId); } -// Исправленная функция transform_expression -function transform_expression(expression, for_child_table = false, table_name = null, universalFields = {}) { +// Функция для преобразования выражений формул +function transform_expression(expression, for_child_table = false, table_name = null) { console.log("🔄 Исходная формула:", expression, "для табличной части:", for_child_table, "таблица:", table_name); let transformed = expression; - // Обрабатываем универсальные поля + // Обработка универсальных полей + const universalFields = JSON.parse(cur_frm.doc.universal_fields_storage || "{}"); Object.keys(universalFields).forEach(fieldId => { const field = universalFields[fieldId]; if (!field || !field.name) return; @@ -2765,15 +3659,335 @@ function transform_expression(expression, for_child_table = false, table_name = 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)`; + // Получаем список табличных частей из DOM или из предопределенного списка + const tablePrefixes = getTablePrefixes(); + + // Функция суммирования по табличной части - обрабатываем первой + tablePrefixes.forEach(tablePrefix => { + const pattern = new RegExp(`sum\\(${tablePrefix}_([^)]+)\\)`, 'g'); + transformed = transformed.replace(pattern, (match, fieldName) => { + return `(frm.doc["${tablePrefix}"] && frm.doc["${tablePrefix}"].reduce((sum, row) => sum + (parseFloat(row["${fieldName}"]) || 0), 0) || 0)`; + }); + + // Проверяем использование val() для табличных полей (должны использоваться sum()) + const valTablePattern = new RegExp(`val\\(${tablePrefix}_([^)]+)\\)`, 'g'); + transformed = transformed.replace(valTablePattern, (match, fieldName) => { + console.warn(`⚠️ Использование val() для табличного поля. Заменено на sum(): ${match}`); + return `(frm.doc["${tablePrefix}"] && frm.doc["${tablePrefix}"].reduce((sum, row) => sum + (parseFloat(row["${fieldName}"]) || 0), 0) || 0)`; + }); }); - // Остальная часть функции без изменений... - // ... + // Если мы трансформируем выражение для табличной части + if (for_child_table) { + // Заменяем val() на row[] для всех полей внутри табличной части + transformed = transformed.replace(/val\(([^)]+)\)/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\(([^)]+)\)/g, (match, field) => { + return `row["${field}"]`; + }); + } else { + // Стандартная замена для обычных полей + transformed = transformed.replace(/val\(([^)]+)\)/g, (match, field) => { + return `frm.doc["${field}"]`; + }); + + // Предупреждение, если row() используется вне таблицы + transformed = transformed.replace(/row\(([^)]+)\)/g, (match, field) => { + console.warn("⚠️ Функция row() используется вне контекста табличной части"); + return `0 /* ERROR: row() вне табличной части */`; + }); + } + + // Обработка процентов + transformed = transformed.replace(/(frm\.doc\["[^"]+"\]|\d+|row\["[^"]+"\])\s*([+\-])\s*(\d+(\.\d+)?)%/g, (match, base, operator, percent) => { + console.log("✅ Обнаружен процент в выражении:", match); + return `${base} ${operator} ((${base} * ${percent}) / 100)`; + }); console.log("🔄 Трансформированная формула:", transformed); return transformed; +} + +// Вспомогательная функция для получения доступных префиксов таблиц +function getTablePrefixes() { + // Предопределенный список известных префиксов таблиц + const knownPrefixes = ["items", "taxes", "packed_items", "payments", "advances", "timesheets", "payment_schedule", "sales_team", "aksiz_test_table"]; + + // Добавляем дополнительные префиксы из текущего DOM, если доступно + try { + // Ищем таблицы в DOM + let domPrefixes = []; + $("div.form-section div.form-section-title").each(function() { + const sectionTitle = $(this).text().trim(); + if (sectionTitle && sectionTitle !== "Основные поля") { + // Преобразуем название секции в возможный префикс таблицы + // Например: "Items (items)" -> "items" + const match = sectionTitle.match(/\(([^)]+)\)/); + if (match) { + domPrefixes.push(match[1]); + } else { + // Иначе используем название секции, преобразованное в нижний регистр без пробелов + domPrefixes.push(sectionTitle.toLowerCase().replace(/\s+/g, '_')); + } + } + }); + + // Объединяем найденные префиксы с известными + return [...new Set([...knownPrefixes, ...domPrefixes])]; + } catch (e) { + console.warn("⚠️ Ошибка при получении префиксов таблиц из DOM:", e); + return knownPrefixes; + } +} + +function renderUniversalFields(frm) { + const $container = $(frm.wrapper).find('.universal-fields-list'); + $container.empty(); + + try { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + + if (Object.keys(universalFields).length === 0) { + $container.append('
Нет универсальных полей. Нажмите "Добавить поле" для создания.
'); + return; + } + + Object.keys(universalFields).forEach(fieldId => { + const field = universalFields[fieldId]; + + const $fieldButton = $(` +
+ +
+ + +
+
+ `); + + // Показываем/скрываем кнопки действий при наведении + $fieldButton.hover( + function() { $(this).find('.field-actions').show(); }, + function() { $(this).find('.field-actions').hide(); } + ); + + $container.append($fieldButton); + }); + + // Инициализируем 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) { + $container.append('
Ошибка при загрузке универсальных полей.
'); + } +} + +function delete_client_script_for_doctype(doctype) { + if (!doctype) { + return; + } + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Client Script", + filters: { dt: doctype }, + fields: ["name"] + }, + callback: function(r) { + if (r.message && r.message.length > 0) { + const script_names = r.message.map(script => script.name); + + // Удаляем каждый найденный скрипт + script_names.forEach(script_name => { + frappe.call({ + method: "frappe.client.delete", + args: { + doctype: "Client Script", + name: script_name + }, + callback: function(response) { + if (response.exc) { + } else { + } + } + }); + }); + } else { + } + } + }); +} + +// Функция для обработки изменения поля в таблице фильтров +function setupFilterRowHandlers(d) { + // Обработчик изменения поля в фильтре + d.fields_dict.filters.grid.get_visible_rows().forEach(row => { + setupFilterFieldHandler(d, row); + }); +} + +// Функция для настройки обработчика изменения поля в строке фильтра +function setupFilterFieldHandler(d, row) { + if (!row.doc || row.doc.__processed) return; + + // Отмечаем строку как обработанную + row.doc.__processed = true; + + // Находим селект поля и добавляем обработчик + $(row.wrapper).find('select[data-fieldname="field"]').on('change', function() { + const fieldname = $(this).val(); + changeFilterValueFieldType(d, row, fieldname); + }); + + // Обрабатываем текущее значение, если оно уже установлено + if (row.doc.field) { + changeFilterValueFieldType(d, row, row.doc.field); + } +} + +// Функция для изменения типа поля значения в зависимости от выбранного поля +function changeFilterValueType(grid_row, fieldInfo) { + if (!grid_row || !fieldInfo) return; + + // Получаем текущее значение + const currentValue = grid_row.doc.value; + + // Определяем новый тип поля в зависимости от типа выбранного поля + let fieldtype = 'Data'; + let options = ''; + + switch (fieldInfo.fieldtype) { + case 'Select': + fieldtype = 'Select'; + options = fieldInfo.options; + break; + + case 'Link': + fieldtype = 'Link'; + options = fieldInfo.options; + break; + + case 'Check': + fieldtype = 'Check'; + break; + + case 'Date': + fieldtype = 'Date'; + break; + + case 'Datetime': + fieldtype = 'Datetime'; + break; + + case 'Int': + case 'Float': + case 'Currency': + fieldtype = 'Float'; + break; + + default: + fieldtype = 'Data'; + } + + // Обновляем определение поля значения + grid_row.grid.meta.fields.forEach(field => { + if (field.fieldname === 'value') { + field.fieldtype = fieldtype; + field.options = options; + } + }); + + // Обновляем колонку значений для текущего DocType + grid_row.grid.grid_rows.forEach(row => { + // Получаем контрол поля значения + const value_field = row.columns_list.find(col => col.df.fieldname === 'value'); + if (value_field) { + // Пересоздаем контрол + value_field.df.fieldtype = fieldtype; + value_field.df.options = options; + row.refresh_field('value'); + } + }); + + // Устанавливаем значение обратно + if (currentValue) { + grid_row.doc.value = currentValue; + } +} + +function processAzerbayjanDateField(docValues) { + // Словарь для сопоставления названий месяцев с их номерами + const monthNames = { + "Yanvar": 1, + "Fevral": 2, + "Mart": 3, + "Aprel": 4, + "May": 5, + "İyun": 6, + "İyul": 7, + "Avqust": 8, + "Sentyabr": 9, + "Oktyabr": 10, + "Noyabr": 11, + "Dekabr": 12 + }; + + // Проверяем, есть ли у нас значения ay и il + if (docValues && docValues.ay && monthNames[docValues.ay]) { + // Получаем номер месяца + const monthNum = monthNames[docValues.ay]; + + // Получаем год из поля il или используем текущий год + let year = docValues.il || new Date().getFullYear(); + console.log(`🔹 Обработка специальных полей: ay=${docValues.ay} (${monthNum}), il=${year}`); + + // Определяем первый день месяца (1-е число) + const firstDay = new Date(year, monthNum - 1, 1); + + // Последний день месяца (получаем последнее число месяца) + const lastDay = new Date(year, monthNum, 0); + + // Форматируем даты в формате YYYY-MM-DD + const firstDayStr = `${year}.${String(monthNum).padStart(2, '0')}.01`; + const lastDayStr = `${year}.${String(monthNum).padStart(2, '0')}.${String(lastDay.getDate()).padStart(2, '0')}`; + + console.log(`🔹 Диапазон дат для ${docValues.ay} ${year}: ${firstDayStr} - ${lastDayStr}`); + + // Возвращаем объект с диапазоном дат + return { + start: firstDayStr, + end: lastDayStr + }; + } + + // Если это не специальное поле или значение не распознано, возвращаем null + return null; } \ No newline at end of file diff --git a/formula_editor/hooks.py b/formula_editor/hooks.py index f4ab3c9..2e500b6 100644 --- a/formula_editor/hooks.py +++ b/formula_editor/hooks.py @@ -140,13 +140,11 @@ app_license = "unlicense" # --------------- # Hook on document methods and events -# doc_events = { -# "*": { -# "on_update": "method", -# "on_cancel": "method", -# "on_trash": "method" -# } -# } +doc_events = { + "Formula Editor": { + "on_trash": "formula_editor.api.on_trash" + } +} # Scheduled Tasks # ---------------