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