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 = $(`
-
- `);
-
- 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 = `
-
+
- `;
-
- const modalStyles = `
-
- `;
+
`;
$('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 = $(`
+
+ `);
+
+ 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 = $(`