diff --git a/formula_editor/api.py b/formula_editor/api.py index a5aef81..a95fe69 100644 --- a/formula_editor/api.py +++ b/formula_editor/api.py @@ -483,100 +483,133 @@ def on_trash(doc, method=None): except Exception as e: pass -# Генерируем функцию для работы с отчетами для клиентского скрипта -def generateReportFieldsFunction(report_fields): - """Генерирует JavaScript функцию для работы с полями отчетов""" - if not report_fields: - return "" - return f""" -async function getReportFieldValue(frm, reportFieldName) {{ - try {{ - // Получаем информацию о поле отчета - const reportFields = {json.dumps(report_fields)}; - const fieldInfo = reportFields[reportFieldName]; - - if (!fieldInfo) {{ - console.warn('Report field not found:', reportFieldName); - return 0; - }} - - // Парсим фильтры - let filters = {{}}; - try {{ - filters = JSON.parse(fieldInfo.filters_json || '{{}}'); - }} catch (e) {{ - console.error('Error parsing filters JSON:', e); - return 0; - }} - - // Обрабатываем фильтры с возможными ссылками на поля документа - const processedFilters = {{}}; - - for (const key in filters) {{ - let value = filters[key]; - - // Проверяем, является ли значение ссылкой на поле документа - if (typeof value === 'string' && value.startsWith('doc.')) {{ - const docField = value.substring(4); - if (frm.doc[docField] !== undefined) {{ - processedFilters[key] = frm.doc[docField]; - continue; - }} - }} - - // Обычные значения - processedFilters[key] = value; - }} - - // Проверяем кеш - if (!window.reportCache) window.reportCache = {{}}; - - const cacheKey = `${{fieldInfo.report}}|${{JSON.stringify(processedFilters)}}|${{fieldInfo.field_name}}`; - const now = new Date().getTime(); - - if (window.reportCache[cacheKey] && - window.reportCache[cacheKey].timestamp > now - 5 * 60 * 1000) {{ - return window.reportCache[cacheKey].value; - }} - - // Выполняем запрос к серверу - try {{ - const result = await frappe.call({{ - method: "formula_editor.api.run_report_for_formula", - args: {{ - report_name: fieldInfo.report, - filters: processedFilters, - field_name: fieldInfo.field_name - }}, - freeze: false - }}); - - if (result && result.message !== undefined) {{ - if (result.message.error) {{ - console.error('Report error:', result.message.error); - return 0; - }} - - let value = parseFloat(result.message); - if (isNaN(value)) value = 0; - - // Сохраняем в кеш - window.reportCache[cacheKey] = {{ - value: value, - timestamp: now - }}; - - return value; - }} - }} catch (err) {{ - console.error('Report field error:', err); - }} - - return 0; - }} catch (error) {{ - console.error('getReportFieldValue error:', error); - return 0; - }} -}} -""" \ No newline at end of file +# --- Client Script generation ------------------------------------------------- +# +# The generated script is what actually runs on a declaration form. It used to be +# built in the browser from the editor's rendered DOM, which meant a half-drawn +# form could serialise empty formulas over good ones. It is built here from the +# stored JSON instead, so saving a Formula Editor record always regenerates the +# script from the data of record. +# +# The emitted script carries only a CONFIG object and a call into taxes_az.formula. +# It contains no reference to this app, so formula_editor can be uninstalled from +# production machines and the declarations keep calculating. + +CLIENT_SCRIPT_PREFIX = "Formula_Editor_Script_for_" + +# Client Scripts ship to production through taxes_az's fixtures, which select on +# this module. +CLIENT_SCRIPT_MODULE = "Taxes Az" + +SCRIPT_TEMPLATE = """// Generated by Formula Editor +// Hesablamanı taxes_az.formula aparır (bax: taxes_az/public/js/formula_runtime.js). + +const CONFIG = {config}; + +frappe.ui.form.on({doctype}, taxes_az.formula.handlers(CONFIG)); +""" + + +def build_config(doc): + """Project a Formula Editor record onto the config its runtime consumes. + + This is a copy of the stored JSON, not a new format — the storage fields are + the contract and are passed through untouched. + """ + return { + "doctype": doc.target_doctype, + "formulas": json.loads(doc.formula_storage or "{}"), + "table_row_formulas": json.loads(doc.table_row_formulas_storage or "{}"), + "report_fields": normalize_report_filters(json.loads(doc.report_fields_storage or "{}")), + "universal_fields": json.loads(doc.universal_fields_storage or "{}"), + } + + +def normalize_report_filters(report_fields): + """Force report filters onto the placeholders the runtime knows how to resolve. + + `company` becomes $DefaultCompany and anything date-shaped becomes $AyIlSpecial, + exactly as the previous generator did while baking filters into the script. This + is not cosmetic: a different filter value means the report returns different + figures, and those figures go on a tax return. + + Operates on the parsed copy; the stored `filters_json` is never rewritten. + """ + for field in report_fields.values(): + if not field: + continue + + try: + filters = json.loads(field.get("filters_json") or "{}") + except ValueError: + field["filters_json"] = "{}" + continue + + company = filters.get("company") + if isinstance(company, str) and not company.startswith("$"): + filters["company"] = "$DefaultCompany" + + for key in list(filters): + lowered = key.lower() + if not ("date" in lowered or "from" in lowered or "to" in lowered): + continue + + value = filters[key] + if not isinstance(value, str): + continue + if value in ("$AyIlSpecial", "$RubIlSpecial") or value.startswith("doc."): + continue + + filters[key] = "$AyIlSpecial" + + field["filters_json"] = json.dumps(filters) + + return report_fields + + +def regenerate_client_script(doc, method=None): + """Rebuild the Client Script for a Formula Editor record. Bound to on_update.""" + if not doc.target_doctype: + return + + script = SCRIPT_TEMPLATE.format( + config=json.dumps(build_config(doc), ensure_ascii=False, indent=1), + doctype=json.dumps(doc.target_doctype, ensure_ascii=False), + ) + name = CLIENT_SCRIPT_PREFIX + doc.target_doctype + + if frappe.db.exists("Client Script", name): + client_script = frappe.get_doc("Client Script", name) + client_script.dt = doc.target_doctype + client_script.view = "Form" + client_script.enabled = 1 + client_script.script = script + client_script.save(ignore_permissions=True) + return + + frappe.get_doc( + { + "doctype": "Client Script", + "name": name, + "dt": doc.target_doctype, + "view": "Form", + "script_type": "Client", + "module": CLIENT_SCRIPT_MODULE, + "enabled": 1, + "script": script, + } + ).insert(ignore_permissions=True) + + +@frappe.whitelist() +def regenerate_all(): + """Regenerate every declaration's Client Script from its Formula Editor record.""" + regenerated = [] + for name in frappe.get_all("Formula Editor", pluck="name"): + doc = frappe.get_doc("Formula Editor", name) + regenerate_client_script(doc) + regenerated.append(doc.target_doctype) + + frappe.db.commit() + return regenerated 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 845a000..8094e3b 100644 --- a/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js +++ b/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js @@ -94,8 +94,9 @@ frappe.require([ }, before_save: function(frm) { + // Скрипт генерирует сервер в on_update (formula_editor.api), из + // сохранённого storage — здесь только переносим формулы из UI в него. save_formulas(frm); - generate_client_script(frm); }, before_remove: function(frm) { @@ -203,496 +204,6 @@ frappe.require([ return 10; // значение по умолчанию } - // Генерация клиентского скрипта - function generate_client_script(frm) { - if (!frm.doc.target_doctype) { - return; - } - - 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 || "{}"); - let table_row_formulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); - let report_fields = JSON.parse(frm.doc.report_fields_storage || "{}"); - - let main_script = `// Generated by Formula Editor\nfrappe.ui.form.on("${target_doctype}", {\n`; - let functions = []; - let table_child_scripts = {}; - let table_doctypes = {}; - let refreshHandlers = []; // Добавим это для хранения обработчиков refresh - - // Хранилище для объединения триггеров - let triggerHandlers = {}; - - // Собираем информацию о табличных триггерах - ключ: table_field, значение: массив полей основного доктайпа - const table_field_triggers = {}; - - // Проходим по всем формулам, ищем триггеры вида tablename_fieldname - Object.keys(formula_data).forEach(fieldname => { - let field_info = formula_data[fieldname]; - if (!field_info.is_table_field && field_info.triggers) { - let triggers = field_info.triggers.split(',').map(t => t.trim()); - - triggers.forEach(trigger => { - if (trigger.includes('_')) { - const parts = trigger.split('_'); - if (parts.length >= 2) { - const tableName = parts[0]; - const fieldName = parts.slice(1).join('_'); - - // Добавляем информацию о триггере - if (!table_field_triggers[trigger]) { - table_field_triggers[trigger] = []; - } - - // Сохраняем поле основного документа и признак асинхронности - table_field_triggers[trigger].push({ - main_field: fieldname, - uses_universal: field_info.formula.includes("univ("), - uses_reports: field_info.formula.includes("report(") - }); - } - } - }); - } - }); - - // Собираем все триггеры из табличных частей для последующей фильтрации - const tableTriggersSet = new Set(); - Object.keys(table_field_triggers).forEach(trigger => { - tableTriggersSet.add(trigger); - }); - - // Получаем информацию о таблицах DocType - getTableDoctypes(target_doctype, table_doctypes); - - // Сбор всех табличных частей и их полей - let child_tables = {}; - collectChildTables(formula_data, child_tables); - - // Добавляем функцию для обработки и выполнения формул - functions.push(` - // Функция для обработки и выполнения формул - async function processFormula(frm, formula) { - try { - // Проверяем наличие вызовов отчетов - if (formula.includes("report(")) { - // Извлекаем все вызовы report() из формулы - const reportRegex = /report\\(([^)]+)\\)/g; - let match; - let processedFormula = formula; - - // Массив для хранения промисов запросов к отчетам - const reportPromises = []; - const reportPlaceholders = []; - - // Находим все вызовы report() и заменяем их на плейсхолдеры - while ((match = reportRegex.exec(formula)) !== null) { - const fullMatch = match[0]; // report(fieldName) - const fieldName = match[1]; // fieldName - - // Создаем уникальный плейсхолдер - const placeholder = \`__REPORT_PLACEHOLDER_\${reportPromises.length}__\`; - - // Ищем данные отчета в хранилище - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - const reportField = Object.values(reportFields).find(f => f.name === fieldName); - - if (reportField) { - // Добавляем промис запроса к отчету - reportPromises.push( - getReportFieldValue( - frm, - reportField.report, - JSON.parse(reportField.filters_json || "{}"), - reportField.field_name, - reportField.aggregation || 'sum' - ) - ); - - // Запоминаем соответствие плейсхолдера и вызова - reportPlaceholders.push({ - placeholder: placeholder, - original: fullMatch - }); - - // Заменяем вызов отчета на плейсхолдер - processedFormula = processedFormula.replace(fullMatch, placeholder); - } - } - - // Если нашли вызовы отчетов, ждем выполнения всех запросов - if (reportPromises.length > 0) { - const reportResults = await Promise.all(reportPromises); - - // Заменяем плейсхолдеры на результаты - for (let i = 0; i < reportPlaceholders.length; i++) { - // Убедимся, что результат - числовое значение - const reportValue = reportResults[i] !== undefined ? Number(reportResults[i]) : 0; - processedFormula = processedFormula.replace( - reportPlaceholders[i].placeholder, - reportValue - ); - } - - // Теперь формула не содержит вызовов report(), обрабатываем её обычным способом - let transformResult = transform_expression(processedFormula, false); - - // Если есть внешняя трансформация, применяем её - let result; - if (transformResult && transformResult.transformed) { - result = eval(transformResult.transformed); - } else { - // Иначе просто вычисляем выражение - result = eval(processedFormula); - } - - // Возвращаем результат - return isNaN(result) ? 0 : result; - } - } - - // Стандартная обработка для формул без отчетов - let transformResult = transform_expression(formula, false); - let result = eval(transformResult.transformed); - return isNaN(result) ? 0 : result; - } catch (e) { - return 0; - } - }`); - - // Добавляем функцию для расчета универсальных полей - functions.push(generateUniversalFieldsFunction(universal_fields)); - - // Добавляем функцию для получения значений полей отчетов - if (Object.keys(report_fields).length > 0) { - functions.push(generateReportFieldsFunction(report_fields)); - } - - // НЕ добавляем обработчики для ay и il здесь отдельно - // Они будут добавлены позже вместе с другими обработчиками - - // Добавляем функцию преобразования выражений - functions.push(generateTransformExpressionFunction(universal_fields, report_fields, table_doctypes)); - // Добавляем функцию для работы с конкретными строками табличных частей - functions.push(generateTableRowAccessFunction()); - - // Создаем список всех полей, которые используют универсальные поля или отчеты - let fieldsUsingUniversalOrReports = []; - - // Проверяем, какие поля используют универсальные поля или отчеты - Object.keys(formula_data).forEach(fieldname => { - let field_info = formula_data[fieldname]; - if (field_info && field_info.formula) { - if (field_info.formula.includes("univ(")) { - fieldsUsingUniversalOrReports.push({ - field: fieldname, - uses_universal: true, - uses_reports: field_info.formula.includes("report(") - }); - } else if (field_info.formula.includes("report(")) { - fieldsUsingUniversalOrReports.push({ - field: fieldname, - uses_universal: false, - uses_reports: true - }); - } - } - }); - - // Обрабатываем все формулы для основного документа - Object.keys(formula_data).forEach(fieldname => { - let field_info = formula_data[fieldname]; - - if (!field_info || typeof field_info !== "object" || !field_info.formula) { - return; - } - - // Создаем функцию расчета для поля - let is_table_field = field_info.is_table_field; - let formula = field_info.formula.trim(); - - // Все функции расчета делаем асинхронными для единообразия, - // так как мы не можем точно определить все случаи использования report() - 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, - uses_reports: false - }; - } - - // Определяем, нужна ли асинхронная функция - let uses_universal = formula.includes("univ("); - let uses_reports = formula.includes("report("); - - table_child_scripts[table_name].calculations.push({ - field: field_name_in_table, - formula: formula, - uses_universal: uses_universal, - uses_reports: uses_reports - }); - - // Помечаем таблицу, если хотя бы одно поле использует универсальные поля или отчеты - if (uses_universal) { - table_child_scripts[table_name].uses_universal = true; - } - if (uses_reports) { - table_child_scripts[table_name].uses_reports = true; - } - - if (field_info.triggers) { - field_info.triggers.split(',').map(t => t.trim()).forEach(trigger => { - table_child_scripts[table_name].fields.add(trigger); - }); - } - } else { - // Используем единый подход для всех формул через transform_expression - functions.push(` - async function calculate_${fieldname}(frm) { - try { - ${formula.includes("univ(") ? 'await calculate_universal_fields(frm);' : ''} - const { transformed, needsAsync, reportCalls } = transform_expression(${JSON.stringify(formula)}, false); - - // Обработка вызовов отчетов, если они есть - let finalExpression = transformed; - if (reportCalls && reportCalls.length > 0) { - // Собираем все результаты отчетов - const reportResults = []; - for (const call of reportCalls) { - const result = await getReportFieldValue( - frm, - call.reportName, - call.filters, - call.fieldName, - call.aggregation - ); - reportResults.push({ placeholder: call.placeholder, value: result }); - } - - // Заменяем плейсхолдеры на полученные значения - for (const result of reportResults) { - finalExpression = finalExpression.replace(result.placeholder, result.value); - } - } - - // Теперь можно безопасно вычислить выражение - const result = eval(finalExpression); - frm.set_value("${fieldname}", isNaN(result) ? 0 : result); - } catch (e) { - } - }`); - - // Проверяем триггеры - if (field_info.triggers) { - let triggers = field_info.triggers.split(',').map(t => t.trim()); - - // Проверяем наличие предопределенного события "При открытии документа" - const hasRefreshTrigger = triggers.some(trigger => isPredefinedEvent(trigger)); - - if (hasRefreshTrigger) { - // Добавляем в список обработчиков refresh - refreshHandlers.push({ - field: fieldname, - uses_universal: formula.includes("univ("), - uses_reports: formula.includes("report(") - }); - - // Удаляем предопределенное событие из списка триггеров - triggers = triggers.filter(trigger => !isPredefinedEvent(trigger)); - } - - // Фильтруем триггеры, исключая те, что уже добавлены в table_field_triggers - const filteredTriggers = triggers.filter(trigger => !tableTriggersSet.has(trigger)); - - // Удаляем дубликаты триггеров - const uniqueTriggers = [...new Set(filteredTriggers)]; - - uniqueTriggers.forEach(trigger => { - if (trigger) { - // Добавляем обработчик в хранилище триггеров - if (!triggerHandlers[trigger]) { - triggerHandlers[trigger] = { - isAsync: true, // ✅ ВСЕГДА true, т.к. функции calculate_* всегда async - handlers: [] - }; - } else { - // Убедимся что async = true - triggerHandlers[trigger].isAsync = true; - } - - // Добавляем вызов функции расчета - triggerHandlers[trigger].handlers.push(`await calculate_${fieldname}(frm); // order: ${field_info.order || 10}`); - } - }); - } - } - }); - - // Добавляем функции для расчета формул конкретных строк табличных частей - if (Object.keys(table_row_formulas).length > 0) { - // Создаем функции для расчета формул строк таблиц - generateTableRowFormulasHandlers(table_row_formulas, functions, target_doctype, universal_fields); - } - - // Добавляем обработчик refresh, если были найдены формулы с триггером "При открытии документа" - if (refreshHandlers.length > 0 || Object.keys(table_row_formulas).length > 0) { - // Определяем, нужна ли асинхронная функция для refresh - const needsAsync = refreshHandlers.some(h => h.uses_universal || h.uses_reports) || - Object.keys(table_row_formulas).some(id => { - const formula = table_row_formulas[id]; - return formula.formula.includes("univ(") || formula.formula.includes("report("); - }); - - // Создаем обработчик refresh с вызовом всех нужных функций расчета - let refreshHandlerBody = ` "refresh": ${needsAsync ? 'async ' : ''}function(frm) {\n`; - - // Если есть формулы для строк табличных частей, инициализируем их - if (Object.keys(table_row_formulas).length > 0) { - refreshHandlerBody += ` initialize_table_row_formulas(frm);\n`; - } - - // Добавляем вызов расчета универсальных полей, если они используются - if (refreshHandlers.some(h => h.uses_universal)) { - refreshHandlerBody += ` await calculate_universal_fields(frm);\n`; - } - - // Сортируем обработчики refresh по порядку - const sortedRefreshHandlers = refreshHandlers.slice().sort((a, b) => { - const orderA = formula_data[a.field] ? (formula_data[a.field].order || 10) : 10; - const orderB = formula_data[b.field] ? (formula_data[b.field].order || 10) : 10; - return orderA - orderB; - }); - - // Добавляем вызовы для каждого поля с триггером "При открытии документа" - sortedRefreshHandlers.forEach(handler => { - if (!handler.type || handler.type !== 'table_row') { - refreshHandlerBody += ` ${needsAsync && handler.uses_universal ? 'await ' : ''}calculate_${handler.field}(frm);\n`; - } - }); - - // Добавляем явный вызов для каждой формулы строки с триггером "При открытии документа" - Object.keys(table_row_formulas).forEach(id => { - const formula = table_row_formulas[id]; - if (formula.triggers && formula.triggers.split(',').map(t => t.trim()).some(t => isPredefinedEvent(t))) { - const usesUniversal = formula.formula.includes("univ("); - const usesReports = formula.formula.includes("report("); - - if (usesUniversal || usesReports) { - refreshHandlerBody += ` await calculate_table_row_field(frm, "${formula.table_name}", ${parseInt(formula.row_index) - 1}, "${formula.field_name}", "${formula.formula}", "refresh");\n`; - } else { - refreshHandlerBody += ` calculate_table_row_field(frm, "${formula.table_name}", ${parseInt(formula.row_index) - 1}, "${formula.field_name}", "${formula.formula}", "refresh");\n`; - } - } - }); - - refreshHandlerBody += ' },\n'; - - // Добавляем обработчик refresh в начало скрипта - main_script = main_script.replace('frappe.ui.form.on("' + target_doctype + '", {\n', - 'frappe.ui.form.on("' + target_doctype + '", {\n' + refreshHandlerBody); - } - - // Обработчики для табличных частей и триггеры, связанные с ними - const tableRowFormulaHandlers = processTableRowFormulas(table_row_formulas, table_doctypes); - - // ВАЖНО: Объединяем обработчики формул строк таблиц с основными обработчиками - Object.keys(tableRowFormulaHandlers).forEach(trigger => { - const handler = tableRowFormulaHandlers[trigger]; - - // Проверяем, есть ли уже обработчик для этого триггера - if (triggerHandlers[trigger]) { - // Объединяем обработчики - // Если хотя бы один требует async, делаем весь обработчик async - if (handler.isAsync) { - triggerHandlers[trigger].isAsync = true; - } - // Добавляем новые обработчики к существующим - triggerHandlers[trigger].handlers = triggerHandlers[trigger].handlers.concat(handler.handlers); - } else { - // Если обработчика еще нет, создаем новый - triggerHandlers[trigger] = handler; - } - }); - - // Проверяем, нужны ли обработчики для ay и il для универсальных полей и основных формул - const needsAyIlHandlers = fieldsUsingUniversalOrReports.some(f => f.uses_universal) || - Object.keys(table_row_formulas).some(id => { - const formula = table_row_formulas[id]; - return formula.formula && formula.formula.includes("univ("); - }); - - if (needsAyIlHandlers) { - // Добавляем или обновляем обработчики для ay и il - ['ay', 'il'].forEach(field => { - if (!triggerHandlers[field]) { - triggerHandlers[field] = { - isAsync: true, - handlers: [] - }; - } else { - triggerHandlers[field].isAsync = true; - } - - // Добавляем вызов calculate_universal_fields в начало обработчика - const hasUniversalCall = triggerHandlers[field].handlers.some(h => - h.includes('calculate_universal_fields') - ); - - if (!hasUniversalCall) { - triggerHandlers[field].handlers.unshift('await calculate_universal_fields(frm); // order: 0'); - } - }); - } - - // Теперь создаем обработчики для каждого триггера (без дубликатов) - Object.keys(triggerHandlers).forEach(trigger => { - const handler = triggerHandlers[trigger]; - main_script += ` "${trigger}": ${handler.isAsync ? 'async ' : ''}function(frm) {\n`; - - // Для ay и il добавляем комментарий о вычислении универсальных полей - if (trigger === 'ay' || trigger === 'il') { - if (handler.handlers.some(h => h.includes('calculate_universal_fields'))) { - main_script += ` // Вычисляем универсальные поля только когда меняется ${trigger}\n`; - } - } - - // Сортируем обработчики по порядку выполнения - const sortedHandlers = handler.handlers.slice().sort((a, b) => { - // Извлекаем порядок из строки обработчика - const orderA = extractOrderFromHandler(a) || 10; - const orderB = extractOrderFromHandler(b) || 10; - return orderA - orderB; - }); - - // Добавляем отсортированные обработчики - sortedHandlers.forEach(handlerCode => { - main_script += ` ${handlerCode}\n`; - }); - - main_script += ' },\n'; - }); - - main_script += `});`; - - // Напрямую добавляем скрипты для дочерних таблиц - const child_scripts = generate_child_table_script(table_child_scripts, table_doctypes, table_field_triggers, formula_data, functions); - - // Добавляем скрипты дочерних таблиц к основному скрипту - main_script += child_scripts; - - let final_script = `${main_script}\n\n${functions.join("\n\n")}`; - update_or_create_client_script(target_doctype, final_script); - } - // UI компоненты - создание и обработка полей function generate_dynamic_fields(frm, doctype, callback) { // Проверяем доступность зависимых функций diff --git a/formula_editor/hooks.py b/formula_editor/hooks.py index 7efd586..0bbae43 100644 --- a/formula_editor/hooks.py +++ b/formula_editor/hooks.py @@ -108,6 +108,10 @@ fixtures = [ doc_events = { "Formula Editor": { + # Скрипт декларации строится из storage на сервере, а не из вёрстки + # редактора: полуотрисованная форма раньше могла записать пустые формулы + # поверх настоящих. + "on_update": "formula_editor.api.regenerate_client_script", "on_trash": "formula_editor.api.on_trash" } } diff --git a/formula_editor/public/js/formula_editor_generator.js b/formula_editor/public/js/formula_editor_generator.js index e76576e..75cc654 100644 --- a/formula_editor/public/js/formula_editor_generator.js +++ b/formula_editor/public/js/formula_editor_generator.js @@ -1,74 +1,6 @@ -function update_or_create_client_script(target_doctype, script_code) { - // Имя для нашего клиентского скрипта формул - 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) { - - 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) { - } else { - } - } - }); - } else { - createNewFormulaScript(target_doctype, script_code, formula_script_name); - } - } else { - 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) { - } else { - } - } - }); -} +// Единственное, что осталось от старого генератора кода: удаление скрипта, когда +// запись Formula Editor удаляют из UI (before_remove). Сборку скрипта делает +// сервер — formula_editor.api.regenerate_client_script, по on_update. function delete_client_script_for_doctype(doctype) { if (!doctype) { @@ -107,359 +39,5 @@ function delete_client_script_for_doctype(doctype) { }); } -function generateTransformExpressionFunction(universal_fields, report_fields, table_doctypes) { - // Получаем список всех табличных частей динамически - const tableNames = table_doctypes ? Object.keys(table_doctypes) : []; - const tablePrefixesJson = JSON.stringify(tableNames); - - return ` - function transform_expression(expression, for_child_table = false, table_name = null) { - let transformed = expression; - let needsAsync = false; - let reportCalls = []; - - // === Обработка report(...) === - const reportRegex = /report\\(([^)]+)\\)/g; - let match; - - // Находим все вызовы report() в выражении - while ((match = reportRegex.exec(expression)) !== null) { - const reportName = match[1]; // Имя поля отчета - const fullMatch = match[0]; // Полный текст match - "report(xyz)" - needsAsync = true; - - // Создаем уникальный плейсхолдер - const placeholder = \`__REPORT_PLACEHOLDER_\${reportCalls.length}__\`; - - // Ищем соответствующее поле отчета по имени - ${Object.keys(report_fields).map(fieldId => { - const field = report_fields[fieldId]; - if (!field || !field.name) return ''; - - let filtersJson = "{}"; - try { - const filtersObj = JSON.parse(field.filters_json || "{}"); - - if (filtersObj.company && typeof filtersObj.company === 'string' && - !filtersObj.company.startsWith('$')) { - filtersObj.company = "$DefaultCompany"; - } - - Object.keys(filtersObj).forEach(key => { - if (key.toLowerCase().includes('date') || - key.toLowerCase().includes('from') || - key.toLowerCase().includes('to')) { - - const value = filtersObj[key]; - - if (typeof value === 'string' && - (value === "$AyIlSpecial" || value === "$RubIlSpecial" || - value.startsWith("doc."))) { - // Сохраняем оригинальное значение - } - else if (typeof value === 'string') { - filtersObj[key] = "$AyIlSpecial"; - } - } - }); - - filtersJson = JSON.stringify(filtersObj); - } catch (e) { - filtersJson = "{}"; - } - - return ` - if (reportName === "${field.name}") { - reportCalls.push({ - placeholder: placeholder, - reportName: "${field.report}", - filters: JSON.parse(${JSON.stringify(filtersJson)}), - fieldName: "${field.field_name}", - aggregation: "${field.aggregation || 'sum'}" - }); - - transformed = transformed.replace(fullMatch, placeholder); - }`; - }).join('')} - } - - // === Обработка table(...) === - const tableRegex = /table\\(([^,)]+),\\s*([^,)]+),\\s*([^)]+)\\)/g; - let tableMatch; - while ((tableMatch = tableRegex.exec(expression)) !== null) { - const fullMatch = tableMatch[0]; - const tableName = tableMatch[1].trim().replace(/['"]/g, ''); - const rowNumber = tableMatch[2].trim(); - const fieldName = tableMatch[3].trim().replace(/['"]/g, ''); - - const replacement = \`(function() { - try { - if (!frm.doc["\${tableName}"] || !frm.doc["\${tableName}"][\${rowNumber} - 1]) { - return 0; - } - const value = frm.doc["\${tableName}"][\${rowNumber} - 1]["\${fieldName}"]; - return parseFloat(value) || 0; - } catch (e) { - return 0; - } - })()\`; - - transformed = transformed.replace(fullMatch, replacement); - } - - // === Обработка universal полей === - ${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(Boolean).join('\n')} - - // === Обработка val() и row() === - // Динамически получаем список всех табличных частей из DocType - const tablePrefixes = ${tablePrefixesJson}; - - for (const prefix of tablePrefixes) { - const sumPattern = new RegExp(\`sum\\\\(\${prefix}_([^)]+)\\\\)\`, 'g'); - transformed = transformed.replace(sumPattern, (m, f) => - \`(frm.doc["\${prefix}"] && frm.doc["\${prefix}"].reduce((s, r) => s + (parseFloat(r["\${f}"]) || 0), 0) || 0)\` - ); - - const valPattern = new RegExp(\`val\\\\(\${prefix}_([^)]+)\\\\)\`, 'g'); - transformed = transformed.replace(valPattern, (m, f) => - \`(frm.doc["\${prefix}"] && frm.doc["\${prefix}"].reduce((s, r) => s + (parseFloat(r["\${f}"]) || 0), 0) || 0)\` - ); - } - - if (for_child_table) { - transformed = transformed.replace(/row\\(([^)]+)\\)/g, (m, f) => - \`(parseFloat(row["\${f}"]) || 0)\` - ); - - transformed = transformed.replace(/val\\(([^)]+)\\)/g, (m, f) => { - if (f.startsWith(table_name + '_')) { - return \`(parseFloat(row["\${f.slice(table_name.length + 1)}"]) || 0)\`; - } - return \`(parseFloat(frm.doc["\${f}"]) || 0)\`; - }); - } else { - transformed = transformed.replace(/val\\(([^)]+)\\)/g, (m, f) => - \`(parseFloat(frm.doc["\${f}"]) || 0)\` - ); - - transformed = transformed.replace(/row\\(([^)]+)\\)/g, () => - \`0 /* ERROR: row() вне табличной части */\` - ); - } - - return { transformed, needsAsync, reportCalls }; - }`; -} - -function generateTableRowAccessFunction() { - return ` -// Функция для получения индекса строки по её имени (name/cdn) -function get_row_index(frm, tableName, rowName) { - - // Проверяем наличие таблицы - if (!frm.doc[tableName]) { - return -1; - } - - // Выводим все строки для отладки - frm.doc[tableName].forEach((row, idx) => { - }); - - // Ищем индекс строки по имени - for (let i = 0; i < frm.doc[tableName].length; i++) { - if (frm.doc[tableName][i].name === rowName) { - return i; - } - } - return -1; -} - -// Функция для доступа к полям конкретных строк таблицы -function table_row(tableName, rowIndex, fieldName) { - // Проверяем наличие необходимых аргументов - if (!tableName || rowIndex === undefined || !fieldName) { - return 0; - } - - // Преобразуем rowIndex в число (на случай, если передана строка) - const rowIdx = parseInt(rowIndex) - 1; // Индексация с 1 для пользователя, но с 0 в массиве - - try { - // Проверяем наличие таблицы и строки - if (!frm.doc[tableName] || !frm.doc[tableName][rowIdx]) { - return 0; - } - - // Получаем значение поля - const value = frm.doc[tableName][rowIdx][fieldName]; - - // Проверяем на NaN и возвращаем значение или 0 - const numValue = parseFloat(value); - if (isNaN(numValue)) { - return 0; - } - return numValue; - } catch (error) { - return 0; - } -} - -// Функция для обновления значения поля конкретной строки таблицы -function set_table_row(frm, tableName, rowIndex, fieldName, value) { - // Проверяем наличие необходимых аргументов - if (!frm || !tableName || rowIndex === undefined || !fieldName) { - return false; - } - - // Преобразуем rowIndex в число (на случай, если передана строка) - const rowIdx = parseInt(rowIndex) - 1; // Индексация с 1 для пользователя, но с 0 в массиве - - try { - // Проверяем наличие таблицы и строки - if (!frm.doc[tableName] || !frm.doc[tableName][rowIdx]) { - return false; - } - - // Получаем строку таблицы - const row = frm.doc[tableName][rowIdx]; - - // Получаем сетевое имя cdt и cdn для Frappe - const cdt = frm.fields_dict[tableName].grid.doctype; - const cdn = row.name; - - // Проверяем, является ли значение NaN, и если да, заменяем его на 0 - let finalValue = value; - if (typeof value === 'number' && isNaN(value)) { - finalValue = 0; - } - - // Обновляем значение поля - frappe.model.set_value(cdt, cdn, fieldName, finalValue); - - return true; - } catch (error) { - return false; - } -} - -// Функция для поиска строки таблицы по значению поля -function find_table_row(frm, tableName, fieldName, value) { - // Проверяем наличие необходимых аргументов - if (!frm || !tableName || !fieldName) { - return -1; - } - - try { - // Проверяем наличие таблицы - if (!frm.doc[tableName]) { - return -1; - } - - // Ищем строку с указанным значением поля - for (let i = 0; i < frm.doc[tableName].length; i++) { - if (frm.doc[tableName][i][fieldName] == value) { - // Возвращаем индекс строки (начиная с 1 для пользователя) - return i + 1; - } - } - - // Если строка не найдена - return -1; - } catch (error) { - return -1; - } -}`; -} - -function generate_child_table_script(table_child_scripts, table_doctypes, table_field_triggers, formula_data, functions) { - let child_scripts = ''; - - Object.keys(table_child_scripts).forEach(tableName => { - let childDoctype = table_child_scripts[tableName].doctype; - let usesUniversalFields = table_child_scripts[tableName].uses_universal; - let table_script = `\n\nfrappe.ui.form.on("${childDoctype}", {\n`; - - // Создаем общую функцию для расчета формул в строке таблицы - let calcFunction = ` - async function calculate_${tableName}_row(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - - try { - ${usesUniversalFields ? 'await calculate_universal_fields(frm);' : ''} - - const { transformed, needsAsync, reportCalls } = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}"); - - let finalExpression = transformed; - - if (reportCalls && reportCalls.length > 0) { - const reportResults = []; - - for (const call of reportCalls) { - const result = await getReportFieldValue( - frm, - call.reportName, - call.filters, - call.fieldName, - call.aggregation - ); - reportResults.push({ placeholder: call.placeholder, value: result }); - } - - for (const result of reportResults) { - finalExpression = finalExpression.replace(result.placeholder, result.value); - } - } - - let result = 0; - try { - result = eval(finalExpression); - } catch (e) { - } - - // Обновляем значение - row["${calc.field}"] = isNaN(result) ? 0 : result; - frm.refresh_field("${tableName}"); - } catch (error) { - } - }`; - - table_child_scripts[tableName].calculations.forEach(calc => { - calcFunction += ` - // Расчет для поля ${calc.field} - try { - const transformed = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}").transformed; - row["${calc.field}"] = eval(transformed); - } catch (e) { - row["${calc.field}"] = 0; - } - `; - }); - - calcFunction += ` - frm.refresh_field("${tableName}"); - } catch (e) { - } - }`; - - functions.push(calcFunction); - - // Добавляем обработчики для каждого триггерного поля - Array.from(table_child_scripts[tableName].fields).forEach(field => { - if (usesUniversalFields) { - table_script += ` "${field}": async function(frm, cdt, cdn) { await calculate_${tableName}_row(frm, cdt, cdn); },\n`; - } else { - table_script += ` "${field}": function(frm, cdt, cdn) { calculate_${tableName}_row(frm, cdt, cdn); },\n`; - } - }); - - table_script += `});`; - child_scripts += table_script; - }); - - return child_scripts; -} - +// Кодогенерация движка убрана: сгенерированный Client Script теперь несёт только +// CONFIG, а считает общий рантайм taxes_az.formula. Здесь остался только UI редактора. diff --git a/formula_editor/public/js/formula_editor_reports.js b/formula_editor/public/js/formula_editor_reports.js index 3153354..bf300c2 100644 --- a/formula_editor/public/js/formula_editor_reports.js +++ b/formula_editor/public/js/formula_editor_reports.js @@ -754,269 +754,5 @@ function deleteReportField(frm, fieldId) { ); } -function generateReportFieldsFunction(report_fields) { - if (!report_fields || Object.keys(report_fields).length === 0) { - return ""; - } - - return ` -// Функция для обработки азербайджанских дат -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 - }; - - // Проверяем наличие квартала (rüb) и года (il) - if (docValues && docValues.rüb && docValues.il) { - // Получаем номер квартала (преобразуем из строки вида "1 Rüb" в число 1) - const quarterStr = docValues.rüb; - const quarterMatch = quarterStr.match(/(\\d+)/); - - if (!quarterMatch) { - return null; - } - - const quarterNum = parseInt(quarterMatch[1]); - - // Получаем год из поля il или используем текущий год - let year = docValues.il || new Date().getFullYear(); - - // Определяем начальный и конечный месяц квартала - let startMonth, endMonth; - - switch(quarterNum) { - case 1: - startMonth = 1; // Январь - endMonth = 3; // Март - break; - case 2: - startMonth = 4; // Апрель - endMonth = 6; // Июнь - break; - case 3: - startMonth = 7; // Июль - endMonth = 9; // Сентябрь - break; - case 4: - startMonth = 10; // Октябрь - endMonth = 12; // Декабрь - break; - default: - return null; - } - - // Определяем первый день квартала (1-е число начального месяца) - const firstDay = new Date(year, startMonth - 1, 1); - - // Последний день квартала (последнее число конечного месяца) - const lastDay = new Date(year, endMonth, 0); - - // Форматируем даты в формате YYYY-MM-DD - const firstDayStr = year + "-" + String(startMonth).padStart(2, '0') + "-01"; - const lastDayStr = year + "-" + String(endMonth).padStart(2, '0') + "-" + String(lastDay.getDate()).padStart(2, '0'); - - // Возвращаем объект с диапазоном дат - return { - start: firstDayStr, - end: lastDayStr - }; - } - - // Проверяем, есть ли у нас значения ay и il - if (docValues && docValues.ay && monthNames[docValues.ay]) { - // Получаем номер месяца - const monthNum = monthNames[docValues.ay]; - - // Получаем год из поля il или используем текущий год - let year = docValues.il || new Date().getFullYear(); - - // Определяем первый день месяца (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'); - - // Возвращаем объект с диапазоном дат - return { - start: firstDayStr, - end: lastDayStr - }; - } - - // Если это не специальное поле или значение не распознано, возвращаем null - return null; -} - -async function getReportFieldValue(frm, reportName, filters, fieldName, aggregation) { - try { - // Обрабатываем фильтры с возможными ссылками на поля документа - const processedFilters = {}; - - for (const key in filters) { - let value = filters[key]; - - // Проверяем, является ли значение ссылкой на поле документа - if (typeof value === 'string' && value.startsWith('doc.')) { - const docField = value.substring(4); - if (frm.doc[docField] !== undefined) { - processedFilters[key] = frm.doc[docField]; - continue; - } - } - - // Обработка специальных переменных - if (typeof value === 'string' && value.startsWith('$')) { - switch (value) { - case '$DefaultCompany': - // Получаем компанию по умолчанию из настроек или документа - processedFilters[key] = frm.doc.company || frappe.defaults.get_user_default("Company"); - continue; - - case '$AyIlSpecial': - // Обрабатываем специальные даты для азербайджанских месяцев/кварталов - const dateRange = processAzerbayjanDateField(frm.doc); - if (dateRange) { - // Для полей from_date и to_date используем соответствующие значения - if (key === 'from_date') { - processedFilters[key] = dateRange.start; - } else if (key === 'to_date') { - processedFilters[key] = dateRange.end; - } else { - // Для других полей используем начальную дату - processedFilters[key] = dateRange.start; - } - } else { - // Если не удалось получить диапазон дат, используем текущую дату - processedFilters[key] = frappe.datetime.get_today(); - } - continue; - - default: - // Для других $-переменных оставляем как есть - processedFilters[key] = value; - continue; - } - } - - // Обычные значения - processedFilters[key] = value; - } - - // Проверяем кеш - if (!window.reportCache) window.reportCache = {}; - - const cacheKey = reportName + "|" + JSON.stringify(processedFilters) + "|" + fieldName + "|" + aggregation; - const now = new Date().getTime(); - - if (window.reportCache[cacheKey] && - window.reportCache[cacheKey].timestamp > now - 5 * 60 * 1000) { - return window.reportCache[cacheKey].value; - } - - // Выполняем запрос к серверу - try { - const result = await frappe.call({ - method: "formula_editor.api.run_report_for_formula", - args: { - report_name: reportName, - filters: processedFilters, - field_name: fieldName - }, - freeze: false - }); - - if (result && result.message !== undefined) { - if (result.message.error) { - console.error('Report error:', result.message.error); - return 0; - } - - let value = parseFloat(result.message); - if (isNaN(value)) value = 0; - - // Округляем до 2 знаков после запятой - value = Math.round(value * 100) / 100; - - // Сохраняем в кеш - window.reportCache[cacheKey] = { - value: value, - timestamp: now - }; - - return value; - } - } catch (err) { - console.error('Report field error:', err); - } - - return 0; - } catch (error) { - console.error('getReportFieldValue error:', error); - return 0; - } -}`; -} - -// Функция для применения агрегации к данным отчета -function applyAggregation(data, fieldName, aggregationType) { - if (!data || data.length === 0) { - return 0; - } - - const values = data.map(row => { - let value = null; - if (typeof row === 'object' && row[fieldName] !== undefined) { - value = row[fieldName]; - } - - // Преобразуем в число - const numValue = parseFloat(value); - return isNaN(numValue) ? 0 : numValue; - }).filter(v => v !== 0); // Убираем нулевые значения - - if (values.length === 0) return 0; - - switch(aggregationType) { - case 'sum': - return values.reduce((sum, val) => sum + val, 0); - case 'avg': - return values.reduce((sum, val) => sum + val, 0) / values.length; - case 'min': - return Math.min(...values); - case 'max': - return Math.max(...values); - case 'first': - return values[0] || 0; - case 'last': - return values[values.length - 1] || 0; - default: - return values.reduce((sum, val) => sum + val, 0); - } -} - -// Экспорт функций для использования в других модулях -if (typeof module !== 'undefined' && module.exports) { - module.exports = { - initReportFields, - generateReportFieldsFunction, - openReportFieldModal, - loadReportFields, - loadReportFilters - }; -} +// Кодогенерация движка убрана: сгенерированный Client Script теперь несёт только +// CONFIG, а считает общий рантайм taxes_az.formula. Здесь остался только UI редактора. diff --git a/formula_editor/public/js/formula_editor_table_rows.js b/formula_editor/public/js/formula_editor_table_rows.js index 3a4ab7f..6282576 100644 --- a/formula_editor/public/js/formula_editor_table_rows.js +++ b/formula_editor/public/js/formula_editor_table_rows.js @@ -1189,294 +1189,5 @@ function openTableRowFieldSelector(frm, targetContainer, isForTrigger = false) { $modal.modal('show'); } -function generateTableRowFormulasHandlers(table_row_formulas, functions, target_doctype, universal_fields) { - // Создаем функцию для инициализации формул строк табличных частей - let initFunction = ` - // Функция для инициализации формул строк табличных частей - function initialize_table_row_formulas(frm) { - try { - // Получаем информацию о формулах для строк табличных частей - const formulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); - - // ВАЖНО: Преобразуем объект в массив и сортируем его по порядку выполнения - const sortedFormulas = Object.entries(formulas).map(([id, formula]) => { - return { id, ...formula }; - }).sort((a, b) => { - // Сортировка по значению поля order (если порядок не задан, используем 10 по умолчанию) - const orderA = a.order !== undefined ? parseInt(a.order) : 10; - const orderB = b.order !== undefined ? parseInt(b.order) : 10; - return orderA - orderB; - }); - - // Группируем формулы по табличным частям и строкам - const formulasByTableAndRow = {}; - - sortedFormulas.forEach(formula => { - const tableName = formula.table_name; - const rowIndex = parseInt(formula.row_index); - - if (!formulasByTableAndRow[tableName]) { - formulasByTableAndRow[tableName] = {}; - } - - if (!formulasByTableAndRow[tableName][rowIndex]) { - formulasByTableAndRow[tableName][rowIndex] = []; - } - - formulasByTableAndRow[tableName][rowIndex].push(formula); - }); - - // Устанавливаем обработчики для каждой табличной части - Object.keys(formulasByTableAndRow).forEach(tableName => { - if (!frm.doc[tableName]) { - return; - } - - // Для каждой строки таблицы - frm.doc[tableName].forEach((row, rowIdx) => { - // Получаем отсортированные формулы для текущей строки (строки нумеруются с 1, а индексы с 0) - const rowFormulas = formulasByTableAndRow[tableName][rowIdx + 1] || []; - - // Выполняем формулы в порядке сортировки (по полю order) - for (const formula of rowFormulas) { - try { - // Определяем необходимость асинхронного вызова - const usesUniversal = formula.formula && formula.formula.includes("univ("); - const usesReports = formula.formula && formula.formula.includes("report("); - - // Вызываем расчет формулы согласно установленному порядку - if (usesUniversal || usesReports) { - // Для асинхронных вызовов используем await в асинхронной IIFE - (async () => { - try { - await calculate_table_row_field(frm, tableName, rowIdx, formula.field_name, formula.formula, "refresh"); - } catch (asyncError) { - } - })(); - } else { - // Для синхронных вызовов выполняем напрямую - calculate_table_row_field(frm, tableName, rowIdx, formula.field_name, formula.formula, "refresh"); - } - } catch (error) { - } - } - }); - }); - } catch (error) { - } - }`; - - functions.push(initFunction); - - // Создаем функцию для расчета значения поля строки таблицы - let calculateFunction = ` -// Функция для расчета значения поля в строке табличной части -async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, formula, triggerField) { - try { - - if (!frm.doc[tableName] || !frm.doc[tableName][rowIndex]) { - return; - } - - const row = frm.doc[tableName][rowIndex]; - const cdt = frm.fields_dict[tableName].grid.doctype; - const cdn = row.name; - - // === ЕДИНАЯ ТОЧКА ОБРАБОТКИ ВСЕХ ФУНКЦИЙ === - let finalFormula, reportCalls; - - try { - // Используем transform_expression как единственную точку преобразования - const transformResult = transform_expression(formula, true, tableName); - - if (transformResult) { - finalFormula = transformResult.transformed; - reportCalls = transformResult.reportCalls || []; - } else { - finalFormula = formula; - reportCalls = []; - } - } catch (transformError) { - console.error('Ошибка transform_expression:', transformError); - finalFormula = formula; - reportCalls = []; - } - - // === ОБРАБОТКА АСИНХРОННЫХ ВЫЗОВОВ ОТЧЕТОВ === - if (reportCalls && reportCalls.length > 0) { - try { - // Выполняем все асинхронные запросы к отчетам параллельно - const reportResults = await Promise.all( - reportCalls.map(call => - getReportFieldValue( - frm, - call.reportName, - call.filters, - call.fieldName, - call.aggregation - ) - ) - ); - - // Заменяем плейсхолдеры на реальные значения - for (let i = 0; i < reportCalls.length; i++) { - const reportValue = reportResults[i] !== undefined ? Number(reportResults[i]) : 0; - finalFormula = finalFormula.replace( - reportCalls[i].placeholder, - reportValue - ); - } - } catch (reportError) { - console.error('Ошибка получения данных отчетов:', reportError); - // В случае ошибки заменяем все плейсхолдеры на 0 - for (const call of reportCalls) { - finalFormula = finalFormula.replace(call.placeholder, 0); - } - } - } - - // === ВЫЧИСЛЕНИЕ ФИНАЛЬНОЙ ФОРМУЛЫ === - let result = 0; - try { - // Создаем функцию с доступом к контексту row и frm - const evalFunc = new Function('row', 'frm', \`return \${finalFormula};\`); - result = evalFunc(row, frm); - } catch (evalError) { - console.error('Ошибка вычисления формулы:', evalError, 'Formula:', finalFormula); - } - - if (isNaN(result)) { - result = 0; - } - - // Устанавливаем вычисленное значение - frappe.model.set_value(cdt, cdn, fieldName, result); - - // Обновляем таблицу - frm.refresh_field(tableName); - } catch (err) { - console.error('Ошибка в calculate_table_row_field:', err); - } -}`; - - functions.push(calculateFunction); - - // Функция для получения индекса строки таблицы - let getRowIndexFunction = ` -// Функция для получения индекса строки по её имени -function get_row_index(frm, tableName, rowName) { - - // Проверяем наличие таблицы - if (!frm.doc[tableName]) { - return -1; - } - - // Ищем индекс строки по имени - for (let i = 0; i < frm.doc[tableName].length; i++) { - if (frm.doc[tableName][i].name === rowName) { - return i; - } - } - return -1; -}`; - - functions.push(getRowIndexFunction); - - return functions; -} - -function processTableRowFormulas(table_row_formulas, table_doctypes) { - // Хранилище для объединенных обработчиков - const triggerHandlers = {}; - - // Проверяем, что объект table_row_formulas не пустой - if (!table_row_formulas || Object.keys(table_row_formulas).length === 0) { - return triggerHandlers; - } - - // Получаем отсортированные формулы по порядку выполнения - const sortedFormulas = Object.entries(table_row_formulas).map(([id, formula]) => { - return { id, ...formula }; - }).sort((a, b) => { - // Сортировка по значению поля order (если порядок не задан, используем 10 по умолчанию) - const orderA = a.order !== undefined ? parseInt(a.order) : 10; - const orderB = b.order !== undefined ? parseInt(b.order) : 10; - return orderA - orderB; - }); - - // Группируем формулы по триггерам - sortedFormulas.forEach(formula => { - const tableName = formula.table_name; - const rowIndex = parseInt(formula.row_index); - const usesUniversal = formula.formula && formula.formula.includes("univ("); - const usesReports = formula.formula && formula.formula.includes("report("); - - // Если есть триггеры, добавляем их - if (formula.triggers) { - const triggers = formula.triggers.split(',').map(t => t.trim()); - - triggers.forEach(trigger => { - // Пропускаем триггер "При открытии документа", он обрабатывается отдельно - if (!trigger || trigger === "При открытии документа" || - trigger === "when_document_opens" || trigger === "document_opens") { - return; - } - - // Инициализируем обработчик триггера, если его еще нет - if (!triggerHandlers[trigger]) { - triggerHandlers[trigger] = { - isAsync: usesUniversal || usesReports, - handlers: [] - }; - } else if (usesUniversal || usesReports) { - // Если текущая формула требует async, помечаем весь обработчик как async - triggerHandlers[trigger].isAsync = true; - } - - // Код для вычисления формулы, с учетом порядка выполнения - const handlerCode = ` - // Формула строки ${rowIndex} в таблице ${tableName} (порядок: ${formula.order || 10}) - if (frm.doc.${tableName} && frm.doc.${tableName}.length >= ${rowIndex}) { - try { - // Индекс строки на 1 меньше номера строки - const rowIndex = ${rowIndex - 1}; - const formula = "${formula.formula}"; - - ${usesUniversal || usesReports ? 'await ' : ''}calculate_table_row_field(frm, "${tableName}", rowIndex, "${formula.field_name}", formula, "${trigger}"); - } catch (error) { - } - }`; - - triggerHandlers[trigger].handlers.push(handlerCode); - }); - } - }); - - // Добавляем обработчики для событий add_row и remove_row для каждой таблицы - const tables = new Set(); - sortedFormulas.forEach(formula => { - tables.add(formula.table_name); - }); - - tables.forEach(tableName => { - // Add handler - triggerHandlers[`${tableName}_add`] = { - isAsync: false, - handlers: [ - `// Обновляем индексы строк после добавления новой строки`, - `setTimeout(() => initialize_table_row_formulas(frm), 100);` - ] - }; - - // Remove handler - triggerHandlers[`${tableName}_remove`] = { - isAsync: false, - handlers: [ - `// Обновляем индексы строк после удаления строки`, - `setTimeout(() => initialize_table_row_formulas(frm), 100);` - ] - }; - }); - - return triggerHandlers; -} \ No newline at end of file +// Кодогенерация движка убрана: сгенерированный Client Script теперь несёт только +// CONFIG, а считает общий рантайм taxes_az.formula. Здесь остался только UI редактора. diff --git a/formula_editor/public/js/formula_editor_universal.js b/formula_editor/public/js/formula_editor_universal.js index ab0b270..a824dad 100644 --- a/formula_editor/public/js/formula_editor_universal.js +++ b/formula_editor/public/js/formula_editor_universal.js @@ -1085,293 +1085,5 @@ function renderUniversalFields(frm) { } } -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) { - return ''; - } - - // Обработка фильтров для ссылок на поля документа - 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 или rüb - newFilter.__is_special_ay_field = (docField === 'ay' || docField === 'il'); - newFilter.__is_special_rub_field = (docField === 'rüb' || docField === 'il'); - } else { - newFilter.valueType = 'static'; - } - return newFilter; - }); - - 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') { - skipCalculation_${fieldId} = true; - return; - } - - // Проверяем, что поле il имеет допустимое значение - if (!docValues.il || isNaN(parseInt(docValues.il))) { - skipCalculation_${fieldId} = true; - return; - } - - // Обрабатываем специальные поля ay и il для получения диапазона дат - const dateRange = processAzerbayjanDateField(docValues); - - if (dateRange) { - // Добавляем два фильтра для диапазона дат - if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === ">=")) - filters_${fieldId}.push(["${filter.field}", ">=", dateRange.start]); - if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === "<")) - filters_${fieldId}.push(["${filter.field}", "<=", dateRange.end]); - } else { - skipCalculation_${fieldId} = true; - } - } else { - skipCalculation_${fieldId} = true; - } - `; - } else if (filter.__is_special_rub_field) { - return ` - // Фильтр со ссылкой на специальное поле квартала (rüb) или года (il) - if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) { - // Получаем значения квартала и года - const docValues = { - rüb: frm.doc["rüb"], - il: frm.doc["il"] - }; - - // Проверяем, что поле rüb имеет допустимое значение - if (!docValues.rüb || typeof docValues.rüb !== 'string') { - skipCalculation_${fieldId} = true; - return; - } - - // Проверяем, что поле il имеет допустимое значение - if (!docValues.il || isNaN(parseInt(docValues.il))) { - skipCalculation_${fieldId} = true; - return; - } - - // Обрабатываем специальные поля rüb и il для получения диапазона дат - const dateRange = processQuarterDateField(docValues); - - if (dateRange) { - // Добавляем два фильтра для диапазона дат - if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === ">=")) - filters_${fieldId}.push(["${filter.field}", ">=", dateRange.start]); - if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === "<")) - filters_${fieldId}.push(["${filter.field}", "<=", dateRange.end]); - } else { - skipCalculation_${fieldId} = true; - } - } else { - 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 { - skipCalculation_${fieldId} = true; - } - `; - } - } else { - // Обычные статические фильтры - return ` - // Статический фильтр - filters_${fieldId}.push(["${filter.field}", "${filter.operator}", "${filter.value}"]); - `; - } - }).join('\n ')} - - // Проверяем, нужно ли пропустить расчет - if (skipCalculation_${fieldId}) { - if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; - frm.doc.__universalFields["${fieldId}"] = 0; - return; - } - - 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}; - } catch (error) { - if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; - frm.doc.__universalFields["${fieldId}"] = 0; - }`; - }).filter(code => code !== '').join('\n')} - } - `; - - // Добавляем функцию для обработки специальных полей с кварталом - universalFieldsCode += ` - // Функция для обработки квартала (rüb) и года (il) - function processQuarterDateField(docValues) { - // Проверяем, есть ли у нас значения rüb и il - if (docValues && docValues.rüb && docValues.il) { - // Получаем номер квартала (преобразуем из строки вида "1 Rüb" в число 1) - const quarterStr = docValues.rüb; - const quarterMatch = quarterStr.match(/(\\d+)/); - - if (!quarterMatch) { - return null; - } - - const quarterNum = parseInt(quarterMatch[1]); - - // Получаем год из поля il или используем текущий год - let year = docValues.il || new Date().getFullYear(); - - // Определяем начальный и конечный месяц квартала - let startMonth, endMonth; - - switch(quarterNum) { - case 1: - startMonth = 1; // Январь - endMonth = 3; // Март - break; - case 2: - startMonth = 4; // Апрель - endMonth = 6; // Июнь - break; - case 3: - startMonth = 7; // Июль - endMonth = 9; // Сентябрь - break; - case 4: - startMonth = 10; // Октябрь - endMonth = 12; // Декабрь - break; - default: - return null; - } - - // Определяем первый день квартала (1-е число начального месяца) - const firstDay = new Date(year, startMonth - 1, 1); - - // Последний день квартала (последнее число конечного месяца) - const lastDay = new Date(year, endMonth, 0); - - // Форматируем даты в формате YYYY-MM-DD - const firstDayStr = \`\${year}-\${String(startMonth).padStart(2, '0')}-01\`; - const lastDayStr = \`\${year}-\${String(endMonth).padStart(2, '0')}-\${String(lastDay.getDate()).padStart(2, '0')}\`; - - // Возвращаем объект с диапазоном дат - return { - start: firstDayStr, - end: lastDayStr - }; - } - - // Если это не специальное поле или значение не распознано, возвращаем null - return null; - } - `; - - // Добавляем функцию для обработки специальных полей с азербайджанскими месяцами - 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 - }; - - // Проверяем наличие квартала (rüb) и года (il) - if (docValues && docValues.rüb && docValues.il) { - // Вызываем специальную функцию для обработки квартала - return processQuarterDateField(docValues); - } - - // Проверяем, есть ли у нас значения ay и il - if (docValues && docValues.ay && monthNames[docValues.ay]) { - // Получаем номер месяца - const monthNum = monthNames[docValues.ay]; - - // Получаем год из поля il или используем текущий год - let year = docValues.il || new Date().getFullYear(); - - // Определяем первый день месяца (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')}\`; - - // Возвращаем объект с диапазоном дат - return { - start: firstDayStr, - end: lastDayStr - }; - } - - // Если это не специальное поле или значение не распознано, возвращаем null - return null; - } - `; - return universalFieldsCode; -} - +// Кодогенерация движка убрана: сгенерированный Client Script теперь несёт только +// CONFIG, а считает общий рантайм taxes_az.formula. Здесь остался только UI редактора. diff --git a/formula_editor/public/js/formula_editor_utils.js b/formula_editor/public/js/formula_editor_utils.js index 87dee09..23b6863 100644 --- a/formula_editor/public/js/formula_editor_utils.js +++ b/formula_editor/public/js/formula_editor_utils.js @@ -38,24 +38,6 @@ function getTableDoctypes(target_doctype, table_doctypes) { } } -function collectChildTables(formula_data, 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); - } - } - }); -} - function processAzerbayjanDateField(docValues) { // Словарь для сопоставления названий месяцев с их номерами const monthNames = {