Added new tool called 'formula_editor' which allows to create formulas for any doctype without tables (wip)
This commit is contained in:
parent
151548f947
commit
244470694e
|
|
@ -0,0 +1,24 @@
|
|||
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`)
|
||||
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}",
|
||||
"label": f"{child.label} → {sub_field.label}"
|
||||
})
|
||||
|
||||
return fields
|
||||
|
|
@ -19,14 +19,14 @@ def create_custom_fields():
|
|||
insert_after='swift_number'
|
||||
)
|
||||
],
|
||||
"Employee Transfer": [
|
||||
dict(
|
||||
fieldname='reason',
|
||||
label='Reason',
|
||||
fieldtype='Data',
|
||||
insert_after='amended_from'
|
||||
)
|
||||
],
|
||||
# "Employee Transfer": [
|
||||
# dict(
|
||||
# fieldname='reason',
|
||||
# label='Reason',
|
||||
# fieldtype='Data',
|
||||
# insert_after='amended_from'
|
||||
# )
|
||||
# ],
|
||||
"Company": [
|
||||
dict(
|
||||
fieldname='ceo',
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ override_doctype_class = {
|
|||
"Report": "jey_erp.custom_report.CustomReport"
|
||||
}
|
||||
|
||||
doc_events = {
|
||||
"Employee Onboarding": {
|
||||
"on_update": "jey_erp.custom_employee_onboarding.on_update"
|
||||
}
|
||||
}
|
||||
# doc_events = {
|
||||
# "Employee Onboarding": {
|
||||
# "on_update": "jey_erp.custom_employee_onboarding.on_update"
|
||||
# }
|
||||
# }
|
||||
|
||||
after_migrate = "jey_erp.hooks.after_migrate_combined"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,293 @@
|
|||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
|
||||
$(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;
|
||||
}
|
||||
|
||||
console.log(`✔️ Восстанавливаем "${fieldname}" -> "${value}"`);
|
||||
$(this).val(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save_formulas(frm) {
|
||||
let formula_data = {};
|
||||
|
||||
console.log("🔄 Начинаем сохранение формул и триггеров...");
|
||||
|
||||
$(frm.fields_dict.formula_fields.wrapper).find('.formula-input').each(function () {
|
||||
let fieldname = $(this).attr("data-fieldname");
|
||||
let formula = $(this).val();
|
||||
|
||||
// Ищем триггерное поле
|
||||
let triggers = $(frm.fields_dict.formula_fields.wrapper)
|
||||
.find(`[data-fieldname="${fieldname}_triggers"] input`).val();
|
||||
|
||||
console.log(`📌 Обрабатываем поле: "${fieldname}"`);
|
||||
console.log(` 🔹 Формула: "${formula}"`);
|
||||
console.log(` 🔹 Триггеры: "${triggers}"`);
|
||||
|
||||
formula_data[fieldname] = {
|
||||
formula: formula || "",
|
||||
triggers: triggers || ""
|
||||
};
|
||||
});
|
||||
|
||||
console.log("✅ Итоговый JSON formula_storage:", formula_data);
|
||||
frm.set_value("formula_storage", JSON.stringify(formula_data));
|
||||
}
|
||||
|
||||
function generate_client_script(frm) {
|
||||
if (!frm.doc.target_doctype) {
|
||||
console.error("❌ Target Doctype отсутствует!");
|
||||
return;
|
||||
}
|
||||
|
||||
let target_doctype = frm.doc.target_doctype;
|
||||
let formula_data = JSON.parse(frm.doc.formula_storage || "{}");
|
||||
|
||||
let main_script = `frappe.ui.form.on("${target_doctype}", {\n`;
|
||||
let functions = [];
|
||||
let table_triggers = {};
|
||||
|
||||
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()) : [];
|
||||
|
||||
function transform_expression(expression) {
|
||||
console.log("🔄 Исходная формула:", expression);
|
||||
let transformed = expression
|
||||
.replace(/sum\((\w+)_(\w+)\)/g, (match, tbl, fld) => {
|
||||
return `(frm.doc["${tbl}"].reduce((sum, row) => sum + (row["${fld}"] || 0), 0))`;
|
||||
})
|
||||
.replace(/val\((\w+)\)/g, (match, field) => {
|
||||
return `frm.doc["${field}"]`;
|
||||
})
|
||||
.replace(/(frm\.doc\["\w+"\]|\d+)\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);
|
||||
|
||||
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 += `});\n`;
|
||||
let final_script = `${main_script}\n${functions.join("\n")}`;
|
||||
console.log("✅ **Сгенерированный Client Script:**\n", final_script);
|
||||
|
||||
update_or_create_client_script(target_doctype, final_script);
|
||||
}
|
||||
|
||||
function update_or_create_client_script(target_doctype, script_code) {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_list",
|
||||
args: {
|
||||
doctype: "Client Script",
|
||||
filters: { dt: target_doctype },
|
||||
fields: ["name"]
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.length > 0) {
|
||||
let existing_script_name = r.message[0].name;
|
||||
console.log(`🔄 Найден существующий Client Script: ${existing_script_name}. Обновляем...`);
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.client.set_value",
|
||||
args: {
|
||||
doctype: "Client Script",
|
||||
name: existing_script_name,
|
||||
fieldname: "script",
|
||||
value: script_code
|
||||
},
|
||||
callback: function(response) {
|
||||
if (response.message) {
|
||||
console.log(`✅ Client Script обновлен для ${target_doctype}`);
|
||||
} else {
|
||||
console.error("❌ Ошибка при обновлении Client Script", response);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("⚠️ Существующий Client Script не найден. Создаем новый...");
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.client.insert",
|
||||
args: {
|
||||
doc: {
|
||||
doctype: "Client Script",
|
||||
name: `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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function generate_dynamic_fields(frm, doctype, callback) {
|
||||
console.log("🔄 Получаем поля для Doctype:", doctype);
|
||||
frappe.call({
|
||||
method: "jey_erp.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;
|
||||
|
||||
let match = field.label.match(/^(.*?)\s*→\s*(.*?)$/);
|
||||
let table_name = match ? match[1] : "Основные поля";
|
||||
let field_label = match ? match[2] : field.label;
|
||||
|
||||
if (!grouped_fields[table_name]) {
|
||||
grouped_fields[table_name] = [];
|
||||
}
|
||||
grouped_fields[table_name].push({ fieldname: field.fieldname, label: field_label });
|
||||
|
||||
if (match) {
|
||||
table_fieldnames.add(field.fieldname); // Запоминаем, какие поля относятся к таблицам
|
||||
}
|
||||
});
|
||||
|
||||
let container = $('<div class="formula-container" style="display:flex;flex-direction:column;align-items:center;justify-content:center;max-width:900px;margin:0 auto;"></div>');
|
||||
|
||||
Object.keys(grouped_fields).forEach(group => {
|
||||
let section = $('<div class="formula-section" style="width:100%;margin-top:20px;padding:10px;border:1px solid #ddd;border-radius:5px;"></div>');
|
||||
section.append(`<h4 style="margin-bottom:10px;">${group}</h4>`);
|
||||
|
||||
grouped_fields[group].forEach(field => {
|
||||
let is_table_field = table_fieldnames.has(field.fieldname);
|
||||
let dataAttr = is_table_field ? 'data-is-table-field="true"' : '';
|
||||
|
||||
let field_html = $('<div class="formula-row" style="display:flex;justify-content:space-between;gap:20px;align-items:center;width:100%">' +
|
||||
'<div class="frappe-control input-max-width" data-fieldtype="Data" data-fieldname="' + field.fieldname + '" ' + dataAttr + ' style="flex:1;">' +
|
||||
'<div class="form-group horizontal">' +
|
||||
'<div class="clearfix"><label class="control-label">' + field.label + ' (' + field.fieldname + ')</label></div>' +
|
||||
'<div class="control-input-wrapper">' +
|
||||
'<div class="control-input">' +
|
||||
'<input type="text" class="input-with-feedback form-control formula-input" data-fieldtype="Data" data-fieldname="' + field.fieldname + '" ' + dataAttr + ' autocomplete="off" placeholder="Введите формулу">' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="frappe-control input-max-width" data-fieldtype="Data" data-fieldname="' + field.fieldname + '_triggers" ' + dataAttr + ' style="flex:1;">' +
|
||||
'<div class="form-group horizontal">' +
|
||||
'<div class="clearfix"><label class="control-label">Обновлять при изменении</label></div>' +
|
||||
'<div class="control-input-wrapper">' +
|
||||
'<div class="control-input">' +
|
||||
'<input type="text" class="input-with-feedback form-control trigger-input" data-fieldtype="Data" data-fieldname="' + field.fieldname + '_triggers" ' + dataAttr + ' autocomplete="off" placeholder="Введите поля через запятую">' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>');
|
||||
section.append(field_html);
|
||||
});
|
||||
|
||||
container.append(section);
|
||||
});
|
||||
|
||||
wrapper.append(container);
|
||||
frm.refresh();
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-02-20 14:32:09.445777",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"target_doctype",
|
||||
"formula_storage",
|
||||
"formula_fields",
|
||||
"test",
|
||||
"test_copy"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "target_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "target_doctype",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "formula_storage",
|
||||
"fieldtype": "Text",
|
||||
"label": "formula_storage"
|
||||
},
|
||||
{
|
||||
"fieldname": "formula_fields",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "formula_fields"
|
||||
},
|
||||
{
|
||||
"fieldname": "test",
|
||||
"fieldtype": "Int",
|
||||
"label": "test"
|
||||
},
|
||||
{
|
||||
"fieldname": "test_copy",
|
||||
"fieldtype": "Int",
|
||||
"label": "test Copy"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-20 15:52:59.048027",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Jey Erp",
|
||||
"name": "Formula Editor",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Copyright (c) 2025, JeyERP and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class FormulaEditor(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright (c) 2025, JeyERP and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record depdendencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestFormulaEditor(UnitTestCase):
|
||||
"""
|
||||
Unit tests for FormulaEditor.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestFormulaEditor(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for FormulaEditor.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
Loading…
Reference in New Issue