From 9fd63933884c8b85599ac99ab000b844851da093 Mon Sep 17 00:00:00 2001
From: Ali <010109ali@gmail.com>
Date: Mon, 7 Jul 2025 17:33:36 +0400
Subject: [PATCH] Fixed bug with reports not working in formulas
---
formula_editor/api.py | 707 +++-----
.../doctype/formula_editor/formula_editor.js | 1201 +++++++------
.../public/js/formula_editor_generator.js | 4 +-
.../public/js/formula_editor_reports.js | 1512 ++++++-----------
.../public/js/formula_editor_table_rows.js | 133 +-
5 files changed, 1570 insertions(+), 1987 deletions(-)
diff --git a/formula_editor/api.py b/formula_editor/api.py
index 52ec0a7..f3667b0 100644
--- a/formula_editor/api.py
+++ b/formula_editor/api.py
@@ -4,6 +4,7 @@ import copy
from frappe.model.document import Document
from frappe.utils import getdate, nowdate, flt
import re
+import importlib
@frappe.whitelist()
def get_doctype_fields(doctype):
@@ -26,7 +27,6 @@ def get_doctype_fields(doctype):
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}",
@@ -65,44 +65,262 @@ def get_all_doctype_fields(doctype):
return fields
+def get_report_module(report_name):
+ """Универсальное получение модуля отчета"""
+ try:
+ # Получаем документ отчета
+ report_doc = frappe.get_doc("Report", report_name)
+
+ if report_doc.report_type != "Script Report":
+ return None
+
+ # Пробуем найти модуль отчета через различные пути
+ module_name = report_name.replace(" ", "_").replace("-", "_").lower()
+
+ # Возможные пути к модулям отчетов
+ possible_paths = [
+ # ERPNext модули
+ f"erpnext.accounts.report.{module_name}.{module_name}",
+ f"erpnext.selling.report.{module_name}.{module_name}",
+ f"erpnext.buying.report.{module_name}.{module_name}",
+ f"erpnext.stock.report.{module_name}.{module_name}",
+ f"erpnext.manufacturing.report.{module_name}.{module_name}",
+ f"erpnext.projects.report.{module_name}.{module_name}",
+ f"erpnext.hr.report.{module_name}.{module_name}",
+ f"erpnext.crm.report.{module_name}.{module_name}",
+ f"erpnext.assets.report.{module_name}.{module_name}",
+ f"erpnext.setup.report.{module_name}.{module_name}",
+ f"erpnext.utilities.report.{module_name}.{module_name}",
+
+ # Frappe модули
+ f"frappe.desk.query_report.{module_name}",
+ f"frappe.core.report.{module_name}.{module_name}",
+
+ # Пользовательские приложения - получаем список всех установленных приложений
+ ]
+
+ # Добавляем пути для всех установленных приложений
+ apps = frappe.get_installed_apps()
+ for app in apps:
+ if app not in ['frappe', 'erpnext']: # уже добавлены выше
+ possible_paths.extend([
+ f"{app}.{app}.report.{module_name}.{module_name}",
+ f"{app}.report.{module_name}.{module_name}",
+ f"{app}.reports.{module_name}.{module_name}",
+ ])
+
+ # Пробуем импортировать модуль
+ for path in possible_paths:
+ try:
+ module = importlib.import_module(path)
+ return module
+ except ImportError:
+ continue
+
+ return None
+
+ except Exception as e:
+ frappe.log_error(f"Error getting report module for {report_name}: {str(e)}")
+ return None
+
@frappe.whitelist()
-def on_trash(doc, method=None):
- """Удаляет клиентские скрипты, созданные для DocType при удалении Formula Editor"""
- if not doc.target_doctype:
- return
+def get_report_filters_dynamic(report_name):
+ """Универсальное получение фильтров отчета"""
+ if not report_name:
+ return []
- # Ищем клиентские скрипты, созданные для целевого DocType
- client_scripts = frappe.get_all(
- "Client Script",
- filters={
- "dt": doc.target_doctype,
- "name": ["like", "Formula_Editor_Script_for_%"]
- },
- fields=["name"]
- )
+ try:
+ # Способ 1: Пробуем получить через модуль отчета
+ module = get_report_module(report_name)
+ if module and hasattr(module, 'get_filters'):
+ try:
+ filters_meta = module.get_filters() or []
+ if filters_meta:
+ return format_filters(filters_meta)
+ except Exception as e:
+ frappe.log_error(f"Error calling get_filters for {report_name}: {str(e)}")
+
+ # Способ 2: Получаем из метаданных отчета
+ report_doc = frappe.get_doc("Report", report_name)
+ if report_doc.report_type != "Script Report":
+ frappe.throw("Выбранный отчет должен быть типа 'Script Report'")
+
+ filters_meta = report_doc.get('filters') or []
+ if filters_meta:
+ return format_filters(filters_meta)
+
+ # Способ 3: Выполняем отчет с пустыми фильтрами и анализируем ошибки
+ # (этот способ сложен в реализации, поэтому возвращаем базовые фильтры)
+ return get_default_report_filters()
- # Удаляем найденные скрипты
- for script in client_scripts:
+ except Exception as e:
+ frappe.log_error(f"Error in get_report_filters_dynamic: {str(e)}")
+ return get_default_report_filters()
+
+@frappe.whitelist()
+def get_report_fields(report_name, filters=None):
+ """Универсальное получение полей отчета"""
+ if not report_name:
+ frappe.throw("Не указано имя отчета")
+
+ try:
+ # Обрабатываем фильтры
+ if filters and isinstance(filters, str):
+ try:
+ filters = json.loads(filters)
+ except json.JSONDecodeError:
+ frappe.throw(f"Ошибка при разборе JSON фильтров: {filters}")
+
+ if not filters:
+ filters = get_basic_filters()
+
+ # Способ 1: Пробуем получить через модуль отчета
+ module = get_report_module(report_name)
+ if module and hasattr(module, 'get_columns'):
+ try:
+ columns = module.get_columns(filters) or []
+ if columns:
+ return format_columns(columns)
+ except Exception as e:
+ frappe.log_error(f"Error calling get_columns for {report_name}: {str(e)}")
+
+ # Способ 2: Выполняем отчет для получения колонок
+ from frappe.desk.query_report import run
+
+ # Добавляем базовые фильтры если их нет
+ if not filters.get('company'):
+ filters['company'] = frappe.defaults.get_user_default("Company") or frappe.db.get_single_value("Global Defaults", "default_company")
+
try:
- frappe.delete_doc("Client Script", script.name, force=True)
- frappe.db.commit()
+ result = run(report_name, filters=filters)
except Exception as e:
- pass
+ # Если отчет не выполняется с фильтрами, пробуем с базовыми
+ try:
+ basic_filters = get_basic_filters()
+ result = run(report_name, filters=basic_filters)
+ except Exception as e2:
+ frappe.throw(f"Не удалось выполнить отчет: {str(e2)}")
+
+ if not result:
+ frappe.throw("Отчет не вернул результатов")
+
+ columns = result.get('columns', [])
+ if not columns:
+ frappe.throw("В результатах отчета отсутствуют колонки")
+
+ return format_columns(columns)
+
+ except Exception as e:
+ frappe.throw(f"Ошибка при получении полей отчета: {str(e)}")
+
+def format_filters(filters_meta):
+ """Форматирует фильтры в унифицированный формат"""
+ formatted_filters = []
+
+ for filter_meta in filters_meta:
+ # Обработка фильтров в формате списка [fieldname, label, fieldtype, options, default, reqd]
+ if isinstance(filter_meta, list) and len(filter_meta) > 0:
+ filter_data = {
+ 'fieldname': filter_meta[0],
+ 'label': filter_meta[1] if len(filter_meta) > 1 else filter_meta[0],
+ 'fieldtype': filter_meta[2] if len(filter_meta) > 2 else 'Data',
+ 'options': filter_meta[3] if len(filter_meta) > 3 else None,
+ 'default': filter_meta[4] if len(filter_meta) > 4 else None,
+ 'reqd': filter_meta[5] if len(filter_meta) > 5 else 0
+ }
+ formatted_filters.append(filter_data)
+
+ # Обработка фильтров в формате словаря
+ elif isinstance(filter_meta, dict) and filter_meta.get('fieldname'):
+ filter_data = {
+ 'fieldname': filter_meta.get('fieldname'),
+ 'label': filter_meta.get('label') or filter_meta.get('fieldname'),
+ 'fieldtype': filter_meta.get('fieldtype') or 'Data',
+ 'options': filter_meta.get('options'),
+ 'default': filter_meta.get('default'),
+ 'reqd': filter_meta.get('reqd') or 0,
+ 'description': filter_meta.get('description')
+ }
+ formatted_filters.append(filter_data)
+
+ return formatted_filters
+
+def format_columns(columns):
+ """Форматирует колонки в унифицированный формат"""
+ fields = []
+
+ for i, col in enumerate(columns):
+ field_data = {}
+
+ if isinstance(col, dict):
+ field_data = {
+ "fieldname": col.get('fieldname') or f"column_{i}",
+ "label": col.get('label') or f"Column {i+1}",
+ "fieldtype": col.get('fieldtype') or "Data"
+ }
+ elif isinstance(col, str):
+ field_data = {
+ "fieldname": f"column_{i}",
+ "label": col,
+ "fieldtype": "Data"
+ }
+ else:
+ field_data = {
+ "fieldname": f"column_{i}",
+ "label": f"Column {i+1}",
+ "fieldtype": "Data"
+ }
+
+ fields.append(field_data)
+
+ return fields
+
+def get_basic_filters():
+ """Возвращает базовые фильтры для выполнения отчетов"""
+ return {
+ 'company': frappe.defaults.get_user_default("Company") or frappe.db.get_single_value("Global Defaults", "default_company"),
+ 'from_date': frappe.utils.get_first_day(frappe.utils.nowdate()),
+ 'to_date': frappe.utils.nowdate()
+ }
+
+def get_default_report_filters():
+ """Возвращает базовые фильтры по умолчанию для интерфейса"""
+ return [
+ {
+ 'fieldname': 'company',
+ 'label': 'Company',
+ 'fieldtype': 'Link',
+ 'options': 'Company',
+ 'default': frappe.defaults.get_user_default("Company"),
+ 'reqd': 1
+ },
+ {
+ 'fieldname': 'from_date',
+ 'label': 'From Date',
+ 'fieldtype': 'Date',
+ 'default': frappe.utils.get_first_day(frappe.utils.nowdate()),
+ 'reqd': 1
+ },
+ {
+ 'fieldname': 'to_date',
+ 'label': 'To Date',
+ 'fieldtype': 'Date',
+ 'default': frappe.utils.nowdate(),
+ 'reqd': 1
+ }
+ ]
@frappe.whitelist()
def run_report_for_formula(report_name, filters, field_name=None):
- """Выполняет кастомный отчет с указанными фильтрами и возвращает результат для формулы"""
+ """Универсальное выполнение отчета для формулы"""
try:
# Парсим фильтры, если они переданы как строка
if isinstance(filters, str):
filters = json.loads(filters)
- # Обрабатываем специальные значения в фильтрах
- processed_filters = process_filters_for_custom_reports(filters)
-
# Выполняем отчет
from frappe.desk.query_report import run
- report_result = run(report_name, filters=processed_filters)
+ report_result = run(report_name, filters=filters)
# Проверяем результат на ошибки
if isinstance(report_result, dict) and report_result.get('message'):
@@ -125,241 +343,8 @@ def run_report_for_formula(report_name, filters, field_name=None):
frappe.log_error(f"Error in run_report_for_formula: {str(e)}")
return {"error": str(e)}
-@frappe.whitelist()
-def get_report_fields(report_name, filters=None):
- """Получает поля из кастомного отчета для использования в Formula Editor"""
- if not report_name:
- frappe.throw("Не указано имя отчета")
-
- try:
- # Если переданы фильтры, обрабатываем их
- if filters and isinstance(filters, str):
- try:
- filters = json.loads(filters)
- except json.JSONDecodeError:
- frappe.throw(f"Ошибка при разборе JSON фильтров: {filters}")
-
- # Получаем метаданные отчета
- report_doc = frappe.get_doc("Report", report_name)
-
- # Проверяем, что это Script Report
- if report_doc.report_type != "Script Report":
- frappe.throw("Выбранный отчет должен быть типа 'Script Report'")
-
- # Подготавливаем базовые фильтры
- base_filters = prepare_base_filters(filters)
-
- # Выполняем отчет для получения колонок
- from frappe.desk.query_report import run
- try:
- result = run(report_name, filters=base_filters)
- except Exception as e:
- frappe.throw(f"Ошибка при выполнении отчета: {str(e)}")
-
- if not result:
- frappe.throw("Отчет не вернул результатов")
-
- if not result.get('columns'):
- frappe.throw("В результатах отчета отсутствуют колонки")
-
- # Получаем колонки отчета
- columns = result.get('columns', [])
- fields = []
-
- # Преобразуем колонки в удобный формат
- for i, col in enumerate(columns):
- field_data = {}
-
- if isinstance(col, dict):
- field_data = {
- "fieldname": col.get('fieldname') or f"column_{i}",
- "label": col.get('label') or f"Column {i+1}",
- "fieldtype": col.get('fieldtype') or "Data"
- }
- else:
- # Для случая, когда колонка просто строка
- field_data = {
- "fieldname": f"column_{i}",
- "label": col,
- "fieldtype": "Data"
- }
-
- # Добавляем только числовые поля и поля, которые могут быть преобразованы в числа
- if is_numeric_field(field_data):
- fields.append(field_data)
-
- # Если нет явно числовых полей, анализируем данные
- if not fields:
- data = result.get('result', [])
- fields = find_numeric_fields_in_data(data, columns)
-
- if not fields:
- frappe.throw(f"В отчете не найдено числовых полей. Пожалуйста, убедитесь, что в отчете есть поля с типом Currency, Float или Int.")
-
- return fields
-
- except Exception as e:
- frappe.throw(f"Ошибка при получении полей отчета: {str(e)}")
-
-@frappe.whitelist()
-def get_report_filters_dynamic(report_name):
- """Получить доступные фильтры для указанного кастомного отчета"""
- if not report_name:
- return []
-
- try:
- # Получаем документ отчета
- report_doc = frappe.get_doc("Report", report_name)
-
- # Проверяем, что отчет - script report
- if report_doc.report_type != "Script Report":
- frappe.throw("Выбранный отчет должен быть типа 'Script Report'")
-
- # Получаем фильтры из метаданных отчета
- filters_meta = report_doc.get('filters') or []
-
- # Преобразуем фильтры в формат, удобный для создания полей
- formatted_filters = []
-
- for filter_meta in filters_meta:
- # Обработка фильтров в формате списка
- if isinstance(filter_meta, list) and len(filter_meta) > 0:
- filter_data = {
- 'fieldname': filter_meta[0],
- 'label': filter_meta[1] if len(filter_meta) > 1 else filter_meta[0],
- 'fieldtype': filter_meta[2] if len(filter_meta) > 2 else 'Data',
- 'options': filter_meta[3] if len(filter_meta) > 3 else None,
- 'default': filter_meta[4] if len(filter_meta) > 4 else None,
- 'reqd': filter_meta[5] if len(filter_meta) > 5 else 0
- }
- formatted_filters.append(filter_data)
-
- # Обработка фильтров в формате словаря
- elif isinstance(filter_meta, dict) and filter_meta.get('fieldname'):
- filter_data = {
- 'fieldname': filter_meta.get('fieldname'),
- 'label': filter_meta.get('label') or filter_meta.get('fieldname'),
- 'fieldtype': filter_meta.get('fieldtype') or 'Data',
- 'options': filter_meta.get('options'),
- 'default': filter_meta.get('default'),
- 'reqd': filter_meta.get('reqd') or 0,
- 'description': filter_meta.get('description')
- }
- formatted_filters.append(filter_data)
-
- # Если фильтров нет в метаданных, добавляем базовые
- if not formatted_filters:
- formatted_filters = get_default_filters()
-
- # Обеспечиваем наличие базовых фильтров
- ensure_basic_filters(formatted_filters)
-
- return formatted_filters
-
- except Exception as e:
- frappe.log_error(f"Error in get_report_filters_dynamic: {str(e)}")
- return get_default_filters()
-
-def process_filters_for_custom_reports(filters):
- """Обрабатывает фильтры специально для кастомных отчетов"""
- processed_filters = {}
-
- for key, value in filters.items():
- # Обработка специального значения компании
- if value == "$DefaultCompany":
- processed_filters[key] = (
- frappe.defaults.get_user_default("Company") or
- frappe.db.get_single_value("Global Defaults", "default_company")
- )
- # Специальные значения дат и ссылки на поля документа обрабатываются на клиенте
- elif value in ["$AyIlSpecial", "$RubIlSpecial"]:
- # Эти значения должны быть уже обработаны на клиенте
- continue
- elif isinstance(value, str) and value.startswith("doc."):
- # Ссылки на поля документа также обрабатываются на клиенте
- continue
- else:
- # Обычные значения
- processed_filters[key] = value
-
- return processed_filters
-
-def prepare_base_filters(filters):
- """Подготавливает базовые фильтры с значениями по умолчанию"""
- if not filters:
- filters = {}
-
- # Автоматическая подстановка компании
- if not filters.get("company"):
- filters["company"] = (
- frappe.defaults.get_user_default("Company") or
- frappe.db.get_single_value("Global Defaults", "default_company")
- )
-
- # Автоматическая подстановка дат
- if not filters.get("from_date"):
- filters["from_date"] = frappe.utils.get_first_day(frappe.utils.nowdate())
-
- if not filters.get("to_date"):
- filters["to_date"] = frappe.utils.nowdate()
-
- # Преобразование строковых дат в объекты дат
- for date_field in ["from_date", "to_date"]:
- if isinstance(filters.get(date_field), str):
- try:
- filters[date_field] = getdate(filters[date_field])
- except:
- pass
-
- return filters
-
-def is_numeric_field(field_data):
- """Проверяет, является ли поле числовым"""
- # Явно числовые типы
- if field_data.get('fieldtype') in ['Int', 'Float', 'Currency', 'Percent']:
- return True
-
- # Поля с числовыми названиями
- label = field_data.get('label', '').lower()
- numeric_keywords = [
- 'amount', 'total', 'sum', 'qty', 'quantity', 'price', 'rate',
- 'cost', 'value', 'balance', 'count', 'number', 'percent',
- 'debit', 'credit', 'profit', 'loss', 'margin'
- ]
-
- return any(keyword in label for keyword in numeric_keywords)
-
-def find_numeric_fields_in_data(data, columns):
- """Ищет числовые поля в данных отчета"""
- fields = []
-
- if data and isinstance(data, list) and len(data) > 0:
- # Если первая строка является словарем
- if isinstance(data[0], dict):
- for key, value in data[0].items():
- if isinstance(value, (int, float)):
- fields.append({
- "fieldname": key,
- "label": key.replace('_', ' ').title(),
- "fieldtype": "Currency" if any(term in key.lower() for term in ['amount', 'total', 'balance']) else "Float"
- })
- # Если первая строка является списком
- elif isinstance(data[0], list) and len(data[0]) == len(columns):
- for i, value in enumerate(data[0]):
- if isinstance(value, (int, float)) and i < len(columns):
- col_name = f"column_{i}"
- col_label = columns[i] if isinstance(columns[i], str) else (columns[i].get('label') or f"Column {i+1}")
-
- fields.append({
- "fieldname": col_name,
- "label": col_label,
- "fieldtype": "Currency" if any(term in col_label.lower() for term in ['amount', 'total', 'balance']) else "Float"
- })
-
- return fields
-
def extract_field_from_report(data, columns, field_name):
- """Извлекает значение конкретного поля из результатов отчета (упрощенная версия для кастомных отчетов)"""
+ """Универсальное извлечение значения поля из результатов отчета"""
if not data:
return 0
@@ -374,7 +359,7 @@ def extract_field_from_report(data, columns, field_name):
column_idx = i
break
- # Если колонка не найдена, ищем ее по любым подстрокам в имени
+ # Если колонка не найдена, ищем по частичному совпадению
if column_idx is None:
for i, col in enumerate(columns):
col_name = ""
@@ -383,17 +368,14 @@ def extract_field_from_report(data, columns, field_name):
elif isinstance(col, str):
col_name = col
- # Проверка на частичное совпадение
if field_name.lower() in col_name.lower():
column_idx = i
break
- # Если колонка найдена, ищем итоговую строку
if column_idx is not None:
- # Определяем возможные маркеры итоговых строк
+ # Ищем итоговые строки сначала
total_markers = ['total', 'итого', 'grand total', 'общий итог', 'итого по', 'total row']
- # Ищем итоговые строки
for row in data:
is_total_row = False
@@ -446,23 +428,17 @@ def extract_field_from_report(data, columns, field_name):
count = 0
for row in data:
+ value = None
if isinstance(row, dict) and field_name in row:
value = row[field_name]
- elif not isinstance(row, dict) and column_idx < len(row):
- value = row[column_idx]
- else:
+ elif isinstance(row, dict):
# Поиск поля с игнорированием регистра
- if isinstance(row, dict):
- found = False
- for key in row:
- if key.lower() == field_name.lower():
- value = row[key]
- found = True
- break
- if not found:
- continue
- else:
- continue
+ for key in row:
+ if key.lower() == field_name.lower():
+ value = row[key]
+ break
+ elif isinstance(row, list) and column_idx < len(row):
+ value = row[column_idx]
# Преобразуем в число
try:
@@ -475,79 +451,31 @@ def extract_field_from_report(data, columns, field_name):
return total
- # Если колонка не найдена, возвращаем 0
return 0
-def get_default_filters():
- """Возвращает базовые фильтры по умолчанию"""
- return [
- {
- 'fieldname': 'company',
- 'label': 'Company',
- 'fieldtype': 'Link',
- 'options': 'Company',
- 'default': frappe.defaults.get_user_default("Company"),
- 'reqd': 1,
- 'hidden': 1
+@frappe.whitelist()
+def on_trash(doc, method=None):
+ """Удаляет клиентские скрипты, созданные для DocType при удалении Formula Editor"""
+ if not doc.target_doctype:
+ return
+
+ # Ищем клиентские скрипты, созданные для целевого DocType
+ client_scripts = frappe.get_all(
+ "Client Script",
+ filters={
+ "dt": doc.target_doctype,
+ "name": ["like", "Formula_Editor_Script_for_%"]
},
- {
- 'fieldname': 'from_date',
- 'label': 'From Date',
- 'fieldtype': 'Date',
- 'default': frappe.utils.get_first_day(frappe.utils.nowdate()),
- 'reqd': 1
- },
- {
- 'fieldname': 'to_date',
- 'label': 'To Date',
- 'fieldtype': 'Date',
- 'default': frappe.utils.nowdate(),
- 'reqd': 1
- }
- ]
-
-def ensure_basic_filters(formatted_filters):
- """Обеспечивает наличие базовых фильтров"""
- existing_fieldnames = [f['fieldname'] for f in formatted_filters]
+ fields=["name"]
+ )
- # Добавляем компанию, если её нет
- if 'company' not in existing_fieldnames:
- formatted_filters.append({
- 'fieldname': 'company',
- 'label': 'Company',
- 'fieldtype': 'Link',
- 'options': 'Company',
- 'default': frappe.defaults.get_user_default("Company"),
- 'reqd': 1,
- 'hidden': 1 # Скрываем, так как обрабатывается автоматически
- })
- else:
- # Если компания есть, делаем её скрытой
- for f in formatted_filters:
- if f['fieldname'] == 'company':
- f['hidden'] = 1
- f['reqd'] = 0
- if not f.get('default'):
- f['default'] = frappe.defaults.get_user_default("Company")
-
- # Добавляем даты, если их нет
- if 'from_date' not in existing_fieldnames:
- formatted_filters.append({
- 'fieldname': 'from_date',
- 'label': 'From Date',
- 'fieldtype': 'Date',
- 'default': frappe.utils.get_first_day(frappe.utils.nowdate()),
- 'reqd': 1
- })
-
- if 'to_date' not in existing_fieldnames:
- formatted_filters.append({
- 'fieldname': 'to_date',
- 'label': 'To Date',
- 'fieldtype': 'Date',
- 'default': frappe.utils.nowdate(),
- 'reqd': 1
- })
+ # Удаляем найденные скрипты
+ for script in client_scripts:
+ try:
+ frappe.delete_doc("Client Script", script.name, force=True)
+ frappe.db.commit()
+ except Exception as e:
+ pass
# Генерируем функцию для работы с отчетами для клиентского скрипта
def generateReportFieldsFunction(report_fields):
@@ -556,9 +484,6 @@ def generateReportFieldsFunction(report_fields):
return ""
return f"""
-window.notifiedAboutAyIl = false;
-window.notifiedAboutRubIl = false;
-
async function getReportFieldValue(frm, reportFieldName) {{
try {{
// Получаем информацию о поле отчета
@@ -566,6 +491,7 @@ async function getReportFieldValue(frm, reportFieldName) {{
const fieldInfo = reportFields[reportFieldName];
if (!fieldInfo) {{
+ console.warn('Report field not found:', reportFieldName);
return 0;
}}
@@ -574,92 +500,16 @@ async function getReportFieldValue(frm, reportFieldName) {{
try {{
filters = JSON.parse(fieldInfo.filters_json || '{{}}');
}} catch (e) {{
+ console.error('Error parsing filters JSON:', e);
return 0;
}}
- // Проверка необходимости полей для специальных значений
- if (Object.values(filters).some(v => v === "$AyIlSpecial")) {{
- if (!frm.doc["ay"] || !frm.doc["il"]) {{
- if (!window.notifiedAboutAyIl) {{
- frappe.msgprint({{
- title: "Not enough data",
- message: "Please fill in the ay and il fields",
- indicator: "orange"
- }});
- window.notifiedAboutAyIl = true;
- }}
- return 0;
- }}
- }}
-
- if (Object.values(filters).some(v => v === "$RubIlSpecial")) {{
- if (!frm.doc["rüb"] || !frm.doc["il"]) {{
- if (!window.notifiedAboutRubIl) {{
- frappe.msgprint({{
- title: "Not enough data",
- message: "Please fill in the rüb and il fields",
- indicator: "orange"
- }});
- window.notifiedAboutRubIl = true;
- }}
- return 0;
- }}
- }}
-
- // Обрабатываем фильтры
+ // Обрабатываем фильтры с возможными ссылками на поля документа
const processedFilters = {{}};
for (const key in filters) {{
let value = filters[key];
- // Специальная обработка для $DefaultCompany
- if (value === "$DefaultCompany") {{
- processedFilters[key] = frm.doc.company || frappe.defaults.get_user_default("Company") || "";
- continue;
- }}
-
- // Специальная обработка для $AyIlSpecial
- if (value === "$AyIlSpecial") {{
- if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) {{
- const dateRange = processAzerbayjanDateField({{
- ay: frm.doc["ay"],
- il: frm.doc["il"]
- }});
-
- if (dateRange) {{
- if (key.includes('from_date')) {{
- processedFilters[key] = dateRange.start;
- }} else if (key.includes('to_date')) {{
- processedFilters[key] = dateRange.end;
- }} else {{
- processedFilters[key] = dateRange.start;
- }}
- continue;
- }}
- }}
- }}
-
- // Специальная обработка для $RubIlSpecial
- if (value === "$RubIlSpecial") {{
- if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) {{
- const dateRange = processQuarterDateField({{
- rüb: frm.doc["rüb"],
- il: frm.doc["il"]
- }});
-
- if (dateRange) {{
- if (key.includes('from_date')) {{
- processedFilters[key] = dateRange.start;
- }} else if (key.includes('to_date')) {{
- processedFilters[key] = dateRange.end;
- }} else {{
- processedFilters[key] = dateRange.start;
- }}
- continue;
- }}
- }}
- }}
-
// Проверяем, является ли значение ссылкой на поле документа
if (typeof value === 'string' && value.startsWith('doc.')) {{
const docField = value.substring(4);
@@ -696,8 +546,9 @@ async function getReportFieldValue(frm, reportFieldName) {{
freeze: false
}});
- if (result && result.message) {{
+ if (result && result.message !== undefined) {{
if (result.message.error) {{
+ console.error('Report error:', result.message.error);
return 0;
}}
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 48cd83b..721164d 100644
--- a/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js
+++ b/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js
@@ -1,3 +1,5 @@
+window.formula_editor_dependencies_loaded = false;
+
frappe.require([
"/assets/formula_editor/js/formula_editor_utils.js",
"/assets/formula_editor/js/formula_editor_ui.js",
@@ -10,193 +12,240 @@ frappe.require([
"/assets/formula_editor/js/formula_editor_generator.js",
"/assets/formula_editor/js/formula_editor_modals.js"
]).then(() => {
-// Основной контроллер формы Formula Editor
-frappe.ui.form.on('Formula Editor', {
- onload_post_render: function(frm) {
+ window.formula_editor_dependencies_loaded = true;
+
+ // Функция для инициализации формы Formula Editor
+ function initialize_formula_editor(frm, source) {
+ // Проверяем, загружены ли зависимости
+ if (!window.formula_editor_dependencies_loaded) {
+ setTimeout(() => initialize_formula_editor(frm, source + '_retry'), 300);
+ return;
+ }
+
+ // Предотвращаем повторную инициализацию
+ if (frm.is_formula_editor_initialized) {
+ return;
+ }
+
// Инициализация хранилища для универсальных полей
if (!frm.doc.universal_fields_storage) {
frm.set_value("universal_fields_storage", "{}");
}
-
+
// Инициализация хранилища для полей отчетов
if (!frm.doc.report_fields_storage) {
frm.set_value("report_fields_storage", "{}");
}
-
- addTokenStyles();
+
+ if (typeof addTokenStyles === 'function') {
+ addTokenStyles();
+ }
// Инициализация хранилища для формул строк табличных частей
if (!frm.doc.table_row_formulas_storage) {
frm.set_value("table_row_formulas_storage", "{}");
}
-
- if (!frm.is_initialized) {
- frm.is_initialized = true;
-
- 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) {
- save_formulas(frm);
- generate_client_script(frm);
- },
-
- before_remove: function(frm) {
+
+ // Автоматическая инициализация при загрузке формы, если доктайп заполнен
if (frm.doc.target_doctype) {
- delete_client_script_for_doctype(frm.doc.target_doctype);
- }
- }
-});
-
-// Восстановление формул из хранилища
-function restore_formulas(frm) {
- let stored_formulas = JSON.parse(frm.doc.formula_storage || "{}");
-
- $(frm.fields_dict.formula_fields.wrapper).find('.formula-input, .trigger-input').each(function () {
- let fieldname = $(this).attr("data-fieldname");
- if (!fieldname) return;
-
- let isTriggerField = fieldname.endsWith("_triggers");
- let baseFieldname = isTriggerField ? fieldname.replace("_triggers", "") : fieldname;
-
- if (stored_formulas[baseFieldname] !== undefined) {
- let value = isTriggerField ? stored_formulas[baseFieldname].triggers : stored_formulas[baseFieldname].formula;
-
- if (typeof value !== "string") {
- return;
- }
-
- $(this).val(value);
- }
- });
-
- // Убедимся, что предопределенные события обрабатываются при первой загрузке формы
- if (Object.keys(stored_formulas).length > 0) {
- let hasRefreshTriggers = false;
-
- // Проверяем, есть ли формулы с триггером "При открытии документа"
- Object.keys(stored_formulas).forEach(fieldname => {
- const formula = stored_formulas[fieldname];
- if (formula && formula.triggers && formula.triggers.includes("При открытии документа")) {
- hasRefreshTriggers = true;
- }
- });
-
- // Если нашли такие формулы, вызываем обработчик refresh для их выполнения
- if (hasRefreshTriggers && typeof frm.trigger === 'function') {
- setTimeout(() => frm.trigger('refresh'), 100);
- }
- }
-}
-
-// Сохранение формул в хранилище
-function save_formulas(frm) {
- let formula_data = {};
-
- $(frm.fields_dict.formula_fields.wrapper).find('.formula-input').each(function () {
- let fieldname = $(this).attr("data-fieldname");
- let formula = $(this).val();
- let is_table_field = $(this).attr("data-is-table-field") === "true";
-
- // Получаем триггерное поле
- let triggers = $(frm.fields_dict.formula_fields.wrapper)
- .find(`[data-fieldname="${fieldname}_triggers"] input`).val();
-
- // Проверяем, что формула или триггеры не пустые
- if (formula.trim() !== "" || (triggers && triggers.trim() !== "")) {
- formula_data[fieldname] = {
- formula: formula || "",
- triggers: triggers || "",
- is_table_field: is_table_field
- };
- }
- });
-
- frm.set_value("formula_storage", JSON.stringify(formula_data));
-}
-
-// Генерация клиентского скрипта
-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(")
- });
- }
+ generate_dynamic_fields(frm, frm.doc.target_doctype, function() {
+ if (frm.doc.formula_storage) {
+ restore_formulas(frm);
}
});
}
+
+ // Устанавливаем флаг инициализации
+ frm.is_formula_editor_initialized = true;
+ }
+
+ // Основной контроллер формы Formula Editor
+ frappe.ui.form.on('Formula Editor', {
+ onload: function(frm) {
+ // Добавляем небольшую задержку для загрузки зависимостей
+ setTimeout(() => {
+ initialize_formula_editor(frm, 'onload');
+ }, 100);
+ },
+
+ onload_post_render: function(frm) {
+ initialize_formula_editor(frm, 'onload_post_render');
+ },
+
+ refresh: function(frm) {
+ // Fallback инициализация, если другие события не сработали
+ if (!frm.is_formula_editor_initialized && frm.doc.target_doctype) {
+ setTimeout(() => {
+ initialize_formula_editor(frm, 'refresh');
+ }, 200);
+ }
+ },
+
+ target_doctype: function(frm) {
+ // Сбрасываем флаг инициализации при смене доктайпа
+ frm.is_formula_editor_initialized = false;
+
+ if (frm.doc.target_doctype) {
+ generate_dynamic_fields(frm, frm.doc.target_doctype, function() {
+ restore_formulas(frm);
+ frm.is_formula_editor_initialized = true;
+ });
+ }
+ },
+
+ before_save: function(frm) {
+ save_formulas(frm);
+ generate_client_script(frm);
+ },
+
+ before_remove: function(frm) {
+ if (frm.doc.target_doctype) {
+ delete_client_script_for_doctype(frm.doc.target_doctype);
+ }
+ }
});
- // Собираем все триггеры из табличных частей для последующей фильтрации
- const tableTriggersSet = new Set();
- Object.keys(table_field_triggers).forEach(trigger => {
- tableTriggersSet.add(trigger);
- });
+ // Восстановление формул из хранилища
+ function restore_formulas(frm) {
+ let stored_formulas = JSON.parse(frm.doc.formula_storage || "{}");
- // Получаем информацию о таблицах DocType
- getTableDoctypes(target_doctype, table_doctypes);
-
- // Сбор всех табличных частей и их полей
- let child_tables = {};
- collectChildTables(formula_data, child_tables);
+ $(frm.fields_dict.formula_fields.wrapper).find('.formula-input, .trigger-input').each(function() {
+ let fieldname = $(this).attr("data-fieldname");
+ if (!fieldname) {
+ return;
+ }
- // Добавляем функцию для обработки и выполнения формул
- functions.push(`
+ let isTriggerField = fieldname.endsWith("_triggers");
+ let baseFieldname = isTriggerField ? fieldname.replace("_triggers", "") : fieldname;
+
+ if (stored_formulas[baseFieldname] !== undefined) {
+ let value = isTriggerField ? stored_formulas[baseFieldname].triggers : stored_formulas[baseFieldname].formula;
+
+ if (typeof value !== "string") {
+ return;
+ }
+ $(this).val(value);
+ }
+ });
+
+ // Убедимся, что предопределенные события обрабатываются при первой загрузке формы
+ if (Object.keys(stored_formulas).length > 0) {
+ let hasRefreshTriggers = false;
+
+ // Проверяем, есть ли формулы с триггером "При открытии документа"
+ Object.keys(stored_formulas).forEach(fieldname => {
+ const formula = stored_formulas[fieldname];
+ if (formula && formula.triggers && formula.triggers.includes("При открытии документа")) {
+ hasRefreshTriggers = true;
+ }
+ });
+
+ // Если нашли такие формулы, вызываем обработчик refresh для их выполнения
+ if (hasRefreshTriggers && typeof frm.trigger === 'function') {
+ setTimeout(() => frm.trigger('refresh'), 100);
+ }
+ }
+ }
+
+ // Сохранение формул в хранилище
+ function save_formulas(frm) {
+ let formula_data = {};
+ let fieldsProcessed = 0;
+ let formulasFound = 0;
+
+ $(frm.fields_dict.formula_fields.wrapper).find('.formula-input').each(function() {
+ fieldsProcessed++;
+
+ let fieldname = $(this).attr("data-fieldname");
+ let formula = $(this).val();
+ let is_table_field = $(this).attr("data-is-table-field") === "true";
+
+ // Получаем триггерное поле
+ let triggers = $(frm.fields_dict.formula_fields.wrapper)
+ .find(`[data-fieldname="${fieldname}_triggers"] input`).val();
+
+ // Проверяем, что формула или триггеры не пустые
+ if (formula.trim() !== "" || (triggers && triggers.trim() !== "")) {
+ formulasFound++;
+ formula_data[fieldname] = {
+ formula: formula || "",
+ triggers: triggers || "",
+ is_table_field: is_table_field
+ };
+ }
+ });
+ frm.set_value("formula_storage", JSON.stringify(formula_data));
+ }
+
+ // Генерация клиентского скрипта
+ 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 {
@@ -285,112 +334,112 @@ function generate_client_script(frm) {
return 0;
}
}`);
-
- // Добавляем функцию для расчета универсальных полей
- functions.push(generateUniversalFieldsFunction(universal_fields));
-
- // Добавляем функцию для получения значений полей отчетов
- if (Object.keys(report_fields).length > 0) {
- functions.push(generateReportFieldsFunction(report_fields));
- }
- // Добавляем обработчики для ay и il - для расчета универсальных полей
- triggerHandlers["ay"] = {
- isAsync: true,
- handlers: ["// Вычисляем универсальные поля только когда меняется ay", "await calculate_universal_fields(frm);"]
- };
-
- triggerHandlers["il"] = {
- isAsync: true,
- handlers: ["// Вычисляем универсальные поля только когда меняется il", "await calculate_universal_fields(frm);"]
- };
+ // Добавляем функцию для расчета универсальных полей
+ functions.push(generateUniversalFieldsFunction(universal_fields));
- // Добавляем функцию преобразования выражений
- functions.push(generateTransformExpressionFunction(universal_fields, report_fields));
-
- // Добавляем функцию для работы с конкретными строками табличных частей
- 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;
+ // Добавляем функцию для получения значений полей отчетов
+ if (Object.keys(report_fields).length > 0) {
+ functions.push(generateReportFieldsFunction(report_fields));
}
- // Создаем функцию расчета для поля
- 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('_');
+ // Добавляем обработчики для ay и il - для расчета универсальных полей
+ triggerHandlers["ay"] = {
+ isAsync: true,
+ handlers: ["// Вычисляем универсальные поля только когда меняется ay", "await calculate_universal_fields(frm);"]
+ };
- 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
- };
+ triggerHandlers["il"] = {
+ isAsync: true,
+ handlers: ["// Вычисляем универсальные поля только когда меняется il", "await calculate_universal_fields(frm);"]
+ };
+
+ // Добавляем функцию преобразования выражений
+ functions.push(generateTransformExpressionFunction(universal_fields, report_fields));
+
+ // Добавляем функцию для работы с конкретными строками табличных частей
+ 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 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;
- }
+ // Создаем функцию расчета для поля
+ let is_table_field = field_info.is_table_field;
+ let formula = field_info.formula.trim();
- if (field_info.triggers) {
- field_info.triggers.split(',').map(t => t.trim()).forEach(trigger => {
- table_child_scripts[table_name].fields.add(trigger);
+ // Все функции расчета делаем асинхронными для единообразия,
+ // так как мы не можем точно определить все случаи использования 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
});
- }
- } else {
- // Используем единый подход для всех формул через transform_expression
- functions.push(`
+
+ // Помечаем таблицу, если хотя бы одно поле использует универсальные поля или отчеты
+ 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);' : ''}
@@ -425,271 +474,399 @@ function generate_client_script(frm) {
}
}`);
- // Проверяем триггеры
- 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: formula.includes("univ(") || formula.includes("report("),
- handlers: []
- };
- } else if ((formula.includes("univ(") || formula.includes("report("))) {
- // Если хотя бы один обработчик требует async, весь обработчик будет async
- triggerHandlers[trigger].isAsync = true;
- }
-
- // Добавляем вызов функции расчета
- triggerHandlers[trigger].handlers.push(`await calculate_${fieldname}(frm);`);
+ // Проверяем триггеры
+ 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));
}
- });
- }
- }
- });
-
- // Добавляем функции для расчета формул конкретных строк табличных частей
- 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`;
- }
-
- // Добавляем вызовы для каждого поля с триггером "При открытии документа"
- refreshHandlers.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`;
+
+ // Фильтруем триггеры, исключая те, что уже добавлены в 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: formula.includes("univ(") || formula.includes("report("),
+ handlers: []
+ };
+ } else if ((formula.includes("univ(") || formula.includes("report("))) {
+ // Если хотя бы один обработчик требует async, весь обработчик будет async
+ triggerHandlers[trigger].isAsync = true;
+ }
+
+ // Добавляем вызов функции расчета
+ triggerHandlers[trigger].handlers.push(`await calculate_${fieldname}(frm);`);
+ }
+ });
}
}
});
-
- refreshHandlerBody += ' },\n';
-
- // Добавляем обработчик refresh в начало скрипта
- main_script = main_script.replace('frappe.ui.form.on("' + target_doctype + '", {\n',
- 'frappe.ui.form.on("' + target_doctype + '", {\n' + refreshHandlerBody);
- }
-
- // Теперь создаем обработчики для каждого триггера
- Object.keys(triggerHandlers).forEach(trigger => {
- const handler = triggerHandlers[trigger];
-
- main_script += ` "${trigger}": ${handler.isAsync ? 'async ' : ''}function(frm) {
+
+ // Добавляем функции для расчета формул конкретных строк табличных частей
+ 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`;
+ }
+
+ // Добавляем вызовы для каждого поля с триггером "При открытии документа"
+ refreshHandlers.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);
+ }
+
+ // Теперь создаем обработчики для каждого триггера
+ Object.keys(triggerHandlers).forEach(trigger => {
+ const handler = triggerHandlers[trigger];
+ main_script += ` "${trigger}": ${handler.isAsync ? 'async ' : ''}function(frm) {
${handler.handlers.join('\n ')}
},\n`;
- });
-
- // Обработчики для табличных частей и триггеры, связанные с ними
- const tableRowFormulaHandlers = processTableRowFormulas(table_row_formulas, table_doctypes);
- Object.keys(tableRowFormulaHandlers).forEach(trigger => {
- const handler = tableRowFormulaHandlers[trigger];
-
- main_script += ` "${trigger}": ${handler.isAsync ? 'async ' : ''}function(frm) {
+ });
+
+ // Обработчики для табличных частей и триггеры, связанные с ними
+ const tableRowFormulaHandlers = processTableRowFormulas(table_row_formulas, table_doctypes);
+ Object.keys(tableRowFormulaHandlers).forEach(trigger => {
+ const handler = tableRowFormulaHandlers[trigger];
+ main_script += ` "${trigger}": ${handler.isAsync ? 'async ' : ''}function(frm) {
${handler.isAsync ? 'await calculate_universal_fields(frm);' : '// Универсальные поля не используются'}
${handler.handlers.join('\n ')}
},\n`;
- });
+ });
- main_script += `});`;
+ main_script += `});`;
- // Напрямую добавляем скрипты для дочерних таблиц
- const child_scripts = generate_child_table_script(table_child_scripts, table_doctypes, table_field_triggers, formula_data, functions);
-
- // Добавляем скрипты дочерних таблиц к основному скрипту
- main_script += child_scripts;
+ // Напрямую добавляем скрипты для дочерних таблиц
+ const child_scripts = generate_child_table_script(table_child_scripts, table_doctypes, table_field_triggers, formula_data, functions);
- let final_script = `${main_script}\n\n${functions.join("\n\n")}`;
+ // Добавляем скрипты дочерних таблиц к основному скрипту
+ main_script += child_scripts;
- update_or_create_client_script(target_doctype, final_script);
-}
+ 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) {
- // Получаем числовые поля для формул
- frappe.call({
- method: "formula_editor.api.get_doctype_fields",
- args: { doctype: doctype },
- callback: function(r) {
- if (!r.message || r.message.length === 0) {
- return;
- }
-
- let formula_fields = r.message;
-
- // Получаем все подходящие поля для триггеров
- frappe.call({
- method: "formula_editor.api.get_all_doctype_fields",
- args: { doctype: doctype },
- callback: function(trigger_response) {
- let trigger_fields = trigger_response.message || [];
-
- if (!frm.fields_dict.formula_fields) {
- return;
+ // UI компоненты - создание и обработка полей
+ function generate_dynamic_fields(frm, doctype, callback) {
+ // Проверяем доступность зависимых функций
+ const requiredFunctions = [
+ 'addTokenStyles', 'createModals', 'addUniversalFieldsSection',
+ 'addReportFieldsSection', 'addTableRowFormulasSection', 'initUniversalFields',
+ 'renderUniversalFields', 'initReportFields', 'renderReportFields',
+ 'initTableRowFormulas', 'createFormulaFieldHtml', 'attachFormulaBuilderEvents',
+ 'attachTriggerEditorEvents'
+ ];
+
+ const missingFunctions = requiredFunctions.filter(func => typeof window[func] !== 'function');
+
+ if (missingFunctions.length > 0) {
+ setTimeout(() => generate_dynamic_fields(frm, doctype, callback), 500);
+ return;
+ }
+
+ // Проверяем готовность формы
+ if (!frm.fields_dict || !frm.fields_dict.formula_fields) {
+ setTimeout(() => generate_dynamic_fields(frm, doctype, callback), 300);
+ return;
+ }
+
+ // Получаем числовые поля для формул
+ frappe.call({
+ method: "formula_editor.api.get_doctype_fields",
+ args: { doctype: doctype },
+ callback: function(r) {
+ if (!r.message || r.message.length === 0) {
+ return;
+ }
+
+ let formula_fields = r.message;
+
+ // Получаем все подходящие поля для триггеров
+ frappe.call({
+ method: "formula_editor.api.get_all_doctype_fields",
+ args: { doctype: doctype },
+ callback: function(trigger_response) {
+ let trigger_fields = trigger_response.message || [];
+ if (!frm.fields_dict.formula_fields) {
+ return;
+ }
+
+ let wrapper = $(frm.fields_dict.formula_fields.wrapper);
+ wrapper.empty();
+
+ // Группировка полей для формул
+ let grouped_fields = { "Основные поля": [] };
+ let table_fieldnames = new Set();
+
+ formula_fields.forEach(field => {
+ if (!field.fieldname) {
+ return;
+ }
+
+ let label = field.label ? field.label : "*Нет наименования*";
+ let match = label.match(/^(.*?)\s*→\s*(.*?)$/);
+ let table_name = match ? match[1] : field.fieldname.startsWith("table_") ? field.fieldname : "Основные поля";
+ let field_label = match ? match[2] : label;
+
+ if (!grouped_fields[table_name]) {
+ grouped_fields[table_name] = [];
+ }
+ grouped_fields[table_name].push({ fieldname: field.fieldname, label: field_label, fieldtype: field.fieldtype });
+
+ if (match || field.fieldname.startsWith("table_")) {
+ table_fieldnames.add(field.fieldname);
+ }
+ });
+
+ // Группировка полей для триггеров
+ let grouped_trigger_fields = { "Основные поля": [] };
+
+ trigger_fields.forEach(field => {
+ if (!field.fieldname) {
+ return;
+ }
+
+ let label = field.label ? field.label : "*Нет наименования*";
+ let match = label.match(/^(.*?)\s*→\s*(.*?)$/);
+ let table_name = match ? match[1] : field.fieldname.startsWith("table_") ? field.fieldname : "Основные поля";
+ let field_label = match ? match[2] : label;
+
+ if (!grouped_trigger_fields[table_name]) {
+ grouped_trigger_fields[table_name] = [];
+ }
+ grouped_trigger_fields[table_name].push({
+ fieldname: field.fieldname,
+ label: field_label,
+ fieldtype: field.fieldtype,
+ options: field.options
+ });
+ });
+
+ // Сохраняем списки полей в frm для использования в триггерах
+ frm.trigger_fields = trigger_fields;
+ frm.grouped_trigger_fields = grouped_trigger_fields;
+
+ let container = $('
');
+
+ // Создаем модальные окна
+ createModals();
+
+ // Добавляем секцию универсальных полей
+ addUniversalFieldsSection(wrapper);
+
+ // Добавляем секцию полей отчетов
+ addReportFieldsSection(wrapper);
+
+ // Добавляем секцию формул для строк табличных частей
+ addTableRowFormulasSection(wrapper);
+
+ // Создаем поля для каждой группы
+ Object.keys(grouped_fields).forEach(group => {
+ let section_name = group === "Основные поля" ? group : `${group} (${group.replace(/\s+/g, '_').toLowerCase()})`;
+ let section = $('');
+ section.append(`${section_name}
`);
+
+ 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 = createFormulaFieldHtml(field, dataAttr);
+ section.append(field_html);
+ });
+
+ container.append(section);
+ });
+
+ // Добавляем основной контейнер формул
+ wrapper.append(container);
+
+ // Инициализируем и отрисовываем универсальные поля
+ initUniversalFields(frm);
+ renderUniversalFields(frm);
+
+ // Инициализируем и отрисовываем поля отчетов
+ initReportFields(frm);
+ renderReportFields(frm);
+
+ // Инициализируем формулы для строк табличных частей
+ initTableRowFormulas(frm);
+
+ // Прикрепляем обработчики событий
+ attachFormulaBuilderEvents(frm, formula_fields, grouped_fields);
+ attachTriggerEditorEvents(frm, trigger_fields, grouped_trigger_fields);
+
+ frm.refresh();
+ if (callback) {
+ callback();
+ }
+ },
+ error: function(err) {
}
-
- let wrapper = $(frm.fields_dict.formula_fields.wrapper);
- wrapper.empty();
-
- // Группировка полей для формул
- let grouped_fields = { "Основные поля": [] };
- let table_fieldnames = new Set();
-
- formula_fields.forEach(field => {
- if (!field.fieldname) return;
-
- let label = field.label ? field.label : "*Нет наименования*";
- let match = label.match(/^(.*?)\s*→\s*(.*?)$/);
- let table_name = match ? match[1] : field.fieldname.startsWith("table_") ? field.fieldname : "Основные поля";
- let field_label = match ? match[2] : label;
-
- if (!grouped_fields[table_name]) {
- grouped_fields[table_name] = [];
- }
- grouped_fields[table_name].push({ fieldname: field.fieldname, label: field_label, fieldtype: field.fieldtype });
-
- if (match || field.fieldname.startsWith("table_")) {
- table_fieldnames.add(field.fieldname);
- }
- });
-
- // Группировка полей для триггеров
- let grouped_trigger_fields = { "Основные поля": [] };
-
- trigger_fields.forEach(field => {
- if (!field.fieldname) return;
-
- let label = field.label ? field.label : "*Нет наименования*";
- let match = label.match(/^(.*?)\s*→\s*(.*?)$/);
- let table_name = match ? match[1] : field.fieldname.startsWith("table_") ? field.fieldname : "Основные поля";
- let field_label = match ? match[2] : label;
-
- if (!grouped_trigger_fields[table_name]) {
- grouped_trigger_fields[table_name] = [];
- }
- grouped_trigger_fields[table_name].push({
- fieldname: field.fieldname,
- label: field_label,
- fieldtype: field.fieldtype,
- options: field.options
- });
- });
-
- // Сохраняем списки полей в frm для использования в триггерах
- frm.trigger_fields = trigger_fields;
- frm.grouped_trigger_fields = grouped_trigger_fields;
-
- let container = $('');
-
- // Создаем модальные окна
- createModals();
-
- // Добавляем секцию универсальных полей
- addUniversalFieldsSection(wrapper);
-
- // Добавляем секцию полей отчетов
- addReportFieldsSection(wrapper);
-
- // Добавляем секцию формул для строк табличных частей
- addTableRowFormulasSection(wrapper);
-
- // Создаем поля для каждой группы
- Object.keys(grouped_fields).forEach(group => {
- let section_name = group === "Основные поля" ? group : `${group} (${group.replace(/\s+/g, '_').toLowerCase()})`;
- let section = $('');
- section.append(`${section_name}
`);
-
- 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 = createFormulaFieldHtml(field, dataAttr);
- section.append(field_html);
- });
-
- container.append(section);
- });
-
- // Добавляем основной контейнер формул
- wrapper.append(container);
-
- // Инициализируем и отрисовываем универсальные поля
- initUniversalFields(frm);
- renderUniversalFields(frm);
-
- // Инициализируем и отрисовываем поля отчетов
- initReportFields(frm);
- renderReportFields(frm);
-
- // Инициализируем формулы для строк табличных частей
- initTableRowFormulas(frm);
-
- // Прикрепляем обработчики событий
- attachFormulaBuilderEvents(frm, formula_fields, grouped_fields);
- attachTriggerEditorEvents(frm, trigger_fields, grouped_trigger_fields);
-
- frm.refresh();
- if (callback) callback();
+ });
+ },
+ error: function(err) {
+ }
+ });
+ }
+
+ // Принудительная инициализация для уже открытых форм
+ setTimeout(() => {
+ // Ищем все открытые формы Formula Editor
+ if (window.cur_frm && window.cur_frm.doctype === 'Formula Editor') {
+ initialize_formula_editor(window.cur_frm, 'forced_init');
+ }
+
+ // Также проверяем frappe.ui.form.forms - хранилище всех открытых форм
+ if (frappe.ui.form && frappe.ui.form.forms) {
+ Object.keys(frappe.ui.form.forms).forEach(form_key => {
+ const form_obj = frappe.ui.form.forms[form_key];
+ if (form_obj && form_obj.doctype === 'Formula Editor' && form_obj.doc) {
+ initialize_formula_editor(form_obj, 'forced_init_from_forms');
}
});
}
- });
-}});
+
+ // Дополнительная проверка через route
+ if (frappe.get_route && frappe.get_route()[0] === 'Form' && frappe.get_route()[1] === 'Formula Editor') {
+ // Попробуем найти форму через более глубокий поиск
+ setTimeout(() => {
+ const forms = document.querySelectorAll('[data-doctype="Formula Editor"]');
+ if (forms.length > 0) {
+ // Попробуем получить форму из DOM
+ if (window.cur_frm && window.cur_frm.doctype === 'Formula Editor') {
+ initialize_formula_editor(window.cur_frm, 'delayed_forced_init');
+ }
+ }
+ }, 200);
+ }
+ }, 100);
+
+ // Дополнительный хук на изменение роута для перехвата открытия Formula Editor
+ if (frappe.router && typeof frappe.router.on === 'function') {
+ frappe.router.on('change', () => {
+ const route = frappe.get_route();
+ if (route && route[0] === 'Form' && route[1] === 'Formula Editor') {
+ setTimeout(() => {
+ if (window.cur_frm && window.cur_frm.doctype === 'Formula Editor' && !window.cur_frm.is_formula_editor_initialized) {
+ initialize_formula_editor(window.cur_frm, 'route_change');
+ }
+ }, 300);
+ }
+ });
+ }
+
+ // Еще один fallback через MutationObserver для отслеживания изменений DOM
+ if (typeof MutationObserver !== 'undefined') {
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ if (mutation.type === 'childList') {
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType === 1 && node.querySelector && node.querySelector('[data-doctype="Formula Editor"]')) {
+ setTimeout(() => {
+ if (window.cur_frm && window.cur_frm.doctype === 'Formula Editor' && !window.cur_frm.is_formula_editor_initialized) {
+ initialize_formula_editor(window.cur_frm, 'dom_observer');
+ }
+ }, 100);
+ }
+ });
+ }
+ });
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true
+ });
+
+ // Остановим наблюдение через 10 секунд, чтобы не засорять память
+ setTimeout(() => {
+ observer.disconnect();
+ }, 10000);
+ }
+
+ // Периодическая проверка неинициализированных форм (последний резерв)
+ let checkAttempts = 0;
+ const maxCheckAttempts = 20; // 10 секунд проверок
+ const formCheckInterval = setInterval(() => {
+ checkAttempts++;
+
+ if (checkAttempts > maxCheckAttempts) {
+ clearInterval(formCheckInterval);
+ return;
+ }
+
+ if (window.cur_frm &&
+ window.cur_frm.doctype === 'Formula Editor' &&
+ !window.cur_frm.is_formula_editor_initialized &&
+ window.cur_frm.doc &&
+ window.cur_frm.doc.target_doctype) {
+
+ clearInterval(formCheckInterval);
+ initialize_formula_editor(window.cur_frm, 'periodic_check');
+ }
+ }, 500);
+
+}).catch((error) => {
+});
\ No newline at end of file
diff --git a/formula_editor/public/js/formula_editor_generator.js b/formula_editor/public/js/formula_editor_generator.js
index 41144e7..caf21c8 100644
--- a/formula_editor/public/js/formula_editor_generator.js
+++ b/formula_editor/public/js/formula_editor_generator.js
@@ -125,7 +125,7 @@ function generateTransformExpressionFunction(universal_fields, report_fields) {
needsAsync = true;
// Создаем уникальный плейсхолдер
- const placeholder = \`__REPORT_\${reportCalls.length}__\`;
+ const placeholder = \`__REPORT_PLACEHOLDER_\${reportCalls.length}__\`;
// Ищем соответствующее поле отчета по имени
${Object.keys(report_fields).map(fieldId => {
@@ -179,7 +179,7 @@ function generateTransformExpressionFunction(universal_fields, report_fields) {
}).join('')}
}
- // === ИСПРАВЛЕНО: Обработка table(...) ===
+ // === Обработка table(...) ===
const tableRegex = /table\\(([^,)]+),\\s*([^,)]+),\\s*([^)]+)\\)/g;
let tableMatch;
while ((tableMatch = tableRegex.exec(expression)) !== null) {
diff --git a/formula_editor/public/js/formula_editor_reports.js b/formula_editor/public/js/formula_editor_reports.js
index 61b3309..8a3255d 100644
--- a/formula_editor/public/js/formula_editor_reports.js
+++ b/formula_editor/public/js/formula_editor_reports.js
@@ -66,13 +66,9 @@ function createReportFieldModal() {
$('body').append(modalHtml);
}
-function openReportFieldModal(frm, fieldId = null, fieldCopy = null) {
- // Создаем/обновляем модальное окно
- createReportFieldModal();
-
+function openReportFieldModal(frm, fieldId = null, fieldValues = null) {
// Получаем значения для редактирования
- let fieldValues = fieldCopy || {};
- if (!fieldCopy && fieldId) {
+ if (!fieldValues && fieldId) {
const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
fieldValues = reportFields[fieldId] || {};
}
@@ -86,7 +82,7 @@ function openReportFieldModal(frm, fieldId = null, fieldCopy = null) {
fieldname: 'name',
fieldtype: 'Data',
reqd: 1,
- default: fieldValues.name || ''
+ default: fieldValues?.name || ''
},
{
label: 'Отчет',
@@ -94,7 +90,7 @@ function openReportFieldModal(frm, fieldId = null, fieldCopy = null) {
fieldtype: 'Link',
options: 'Report',
reqd: 1,
- default: fieldValues.report || '',
+ default: fieldValues?.report || '',
filters: {
"report_type": "Script Report"
},
@@ -103,65 +99,13 @@ function openReportFieldModal(frm, fieldId = null, fieldCopy = null) {
if (selectedReport) {
// Загружаем фильтры отчета
- frappe.call({
- method: "formula_editor.api.get_report_filters_dynamic",
- args: {
- report_name: selectedReport
- },
- callback: function(r) {
- if (r.message) {
- // Сохраняем фильтры для использования
- d.report_filters = r.message;
-
- // Обновляем поле фильтров
- if (d.fields_dict.filters_json) {
- let current_filters = {};
- try {
- current_filters = JSON.parse(d.get_value('filters_json') || '{}');
- } catch (e) {
- current_filters = {};
- }
-
- // Показываем секцию фильтров (используя set_df_property)
- d.set_df_property('filters_section', 'hidden', 0);
- d.set_df_property('filters_helper', 'hidden', 0);
-
- // Генерируем HTML для фильтров
- const filtersHtml = generateFiltersHtml(r.message, current_filters);
- d.fields_dict.filters_container.$wrapper.html(filtersHtml);
-
- // Инициализируем обработчики для фильтров
- initFilterControls(d, current_filters);
-
- // Восстанавливаем специальные значения дат, если они есть
- if (d.has_special_dates) {
- restoreSpecialDateValues(d, current_filters);
- }
- }
-
- // Загружаем поля отчета
- loadReportFields(d, selectedReport);
- } else {
- // Скрываем секцию фильтров
- d.set_df_property('filters_section', 'hidden', 1);
- d.set_df_property('filters_helper', 'hidden', 1);
- d.fields_dict.filters_container.$wrapper.html('');
-
- // Очищаем поле выбора полей
- d.fields_dict.field_name.df.options = [];
- d.fields_dict.field_name.refresh();
- }
- }
- });
+ loadReportFilters(d, selectedReport, fieldValues?.filters_json);
+ // Загружаем поля отчета
+ loadReportFields(d, selectedReport);
} else {
- // Скрываем секцию фильтров
- d.set_df_property('filters_section', 'hidden', 1);
- d.set_df_property('filters_helper', 'hidden', 1);
- d.fields_dict.filters_container.$wrapper.html('');
-
- // Очищаем поле выбора полей
- d.fields_dict.field_name.df.options = [];
- d.fields_dict.field_name.refresh();
+ // Очищаем секции
+ clearFiltersSection(d);
+ clearFieldsSection(d);
}
}
},
@@ -188,14 +132,7 @@ function openReportFieldModal(frm, fieldId = null, fieldCopy = null) {
label: 'Фильтры (JSON)',
hidden: 1,
options: 'JSON',
- default: fieldValues.filters_json || '{}'
- },
- {
- fieldname: 'has_special_dates',
- fieldtype: 'Check',
- label: 'Использует специальные даты',
- hidden: 1,
- default: fieldValues.__has_special_dates || 0
+ default: fieldValues?.filters_json || '{}'
},
{
label: 'Поле отчета',
@@ -208,65 +145,27 @@ function openReportFieldModal(frm, fieldId = null, fieldCopy = null) {
fieldtype: 'Select',
options: [],
reqd: 1,
- default: fieldValues.field_name || ''
+ default: fieldValues?.field_name || ''
},
{
label: 'Агрегация',
fieldname: 'aggregation',
fieldtype: 'Select',
options: [
- {label: 'Сумма', value: 'sum'}
+ {label: 'Сумма', value: 'sum'},
+ {label: 'Среднее', value: 'avg'},
+ {label: 'Минимум', value: 'min'},
+ {label: 'Максимум', value: 'max'},
+ {label: 'Первое значение', value: 'first'},
+ {label: 'Последнее значение', value: 'last'}
],
- default: fieldValues.aggregation || 'sum'
+ default: fieldValues?.aggregation || 'sum'
}
],
primary_action_label: fieldId ? 'Обновить' : 'Создать',
primary_action: function() {
const values = d.get_values();
- // Получаем JSON фильтров
- let filters_json = d.get_value('filters_json');
-
- // Восстанавливаем специальные значения перед сохранением
- if (d.get_value('has_special_dates')) {
- try {
- let filters = JSON.parse(filters_json);
-
- // Восстанавливаем оригинальные значения дат
- Object.keys(filters).forEach(key => {
- if (key.startsWith('__original_')) {
- const originalKey = key.replace('__original_', '');
- filters[originalKey] = filters[key]; // Восстанавливаем оригинальное значение
- delete filters[key]; // Удаляем временный ключ
- }
- });
-
- // Устанавливаем значения выбранные в выпадающих списках для дат
- if (d.filter_controls) {
- Object.keys(d.filter_controls).forEach(fieldname => {
- const ctrl = d.filter_controls[fieldname];
- if (ctrl && ctrl.df &&
- (fieldname.toLowerCase().includes('date') ||
- fieldname.toLowerCase().includes('from') ||
- fieldname.toLowerCase().includes('to'))) {
-
- const $select = $(`#filter_${fieldname} select.filter-value`);
- if ($select.length) {
- const value = $select.val();
- // Если выбрано специальное значение или ссылка на поле документа
- if (value === "$AyIlSpecial" || value === "$RubIlSpecial" || (value && value.startsWith('doc.'))) {
- filters[fieldname] = value;
- }
- }
- }
- });
- }
-
- filters_json = JSON.stringify(filters);
- } catch (e) {
- }
- }
-
// Проверяем, что все обязательные поля заполнены
if (!values.name || !values.report || !values.field_name) {
frappe.msgprint({
@@ -277,8 +176,8 @@ function openReportFieldModal(frm, fieldId = null, fieldCopy = null) {
return;
}
- // Обновляем значение фильтров
- values.filters_json = filters_json;
+ // Собираем фильтры из контролов
+ values.filters_json = collectFiltersFromControls(d);
// Сохраняем данные
saveReportField(frm, values, fieldId);
@@ -287,72 +186,112 @@ function openReportFieldModal(frm, fieldId = null, fieldCopy = null) {
}
});
- // Сохраняем информацию о специальных датах
- d.has_special_dates = fieldValues.__has_special_dates;
-
// Показываем диалог
d.show();
// Если редактируем существующее поле, загружаем данные
- if (fieldId && fieldValues.report) {
+ if (fieldId && fieldValues?.report) {
setTimeout(() => {
d.fields_dict.report.df.onchange.call(d.fields_dict.report);
}, 200);
}
}
-function restoreSpecialDateValues(d, current_filters) {
- try {
- if (!d.has_special_dates) return;
-
- // Ищем оригинальные значения дат, сохраненные с префиксом __original_
- Object.keys(current_filters).forEach(key => {
- if (key.startsWith('__original_')) {
- const originalKey = key.replace('__original_', '');
- const originalValue = current_filters[key];
+function loadReportFilters(d, reportName, existingFilters = null) {
+ frappe.call({
+ method: "formula_editor.api.get_report_filters_dynamic",
+ args: {
+ report_name: reportName
+ },
+ callback: function(r) {
+ if (r.message && r.message.length > 0) {
+ // Показываем секцию фильтров
+ d.set_df_property('filters_section', 'hidden', 0);
+ d.set_df_property('filters_helper', 'hidden', 0);
- // Если есть контрол для этого поля
- if (d.filter_controls && d.filter_controls[originalKey]) {
- const $select = $(`#filter_${originalKey} select.filter-value`);
- if ($select.length) {
- // Проверяем, есть ли опция с таким значением
- if ($select.find(`option[value="${originalValue}"]`).length) {
- $select.val(originalValue);
- } else if (originalValue === "$AyIlSpecial") {
- // Если это специальное значение месяца, ищем соответствующую опцию
- $select.find('option').each(function() {
- if ($(this).text().toLowerCase().includes('месяц')) {
- $select.val($(this).val());
- return false; // Останавливаем цикл
- }
- });
- } else if (originalValue === "$RubIlSpecial") {
- // Если это специальное значение квартала, ищем соответствующую опцию
- $select.find('option').each(function() {
- if ($(this).text().toLowerCase().includes('квартал')) {
- $select.val($(this).val());
- return false; // Останавливаем цикл
- }
- });
- }
- }
- }
+ // Генерируем HTML для фильтров
+ const filtersHtml = generateUniversalFiltersHtml(r.message, existingFilters);
+ d.fields_dict.filters_container.$wrapper.html(filtersHtml);
+
+ // Инициализируем контролы фильтров
+ initUniversalFilterControls(d, r.message, existingFilters);
+ } else {
+ clearFiltersSection(d);
}
- });
- } catch (e) {
- }
+ },
+ error: function() {
+ clearFiltersSection(d);
+ }
+ });
}
-function generateFiltersHtml(filters, currentValues) {
- let html = '';
+function loadReportFields(d, reportName) {
+ // Получаем текущие фильтры
+ const filtersJson = d.get_value('filters_json') || '{}';
+ let filters = {};
+
+ try {
+ filters = JSON.parse(filtersJson);
+ } catch (e) {
+ filters = {};
+ }
+
+ frappe.call({
+ method: "formula_editor.api.get_report_fields",
+ args: {
+ report_name: reportName,
+ filters: filters
+ },
+ callback: function(r) {
+ if (r.message && r.message.length) {
+ const fields = r.message.map(field => ({
+ label: field.label || field.fieldname,
+ value: field.fieldname
+ }));
+
+ // Обновляем опции селекта
+ d.set_df_property('field_name', 'options', fields);
+ d.fields_dict.field_name.refresh();
+
+ // Если есть сохраненное значение, выбираем его
+ const savedValue = d.get_value('field_name');
+ if (savedValue) {
+ d.set_value('field_name', savedValue);
+ }
+ } else {
+ clearFieldsSection(d);
+ frappe.msgprint({
+ title: __('Информация'),
+ message: __('В отчете не найдено подходящих полей'),
+ indicator: 'orange'
+ });
+ }
+ },
+ error: function() {
+ clearFieldsSection(d);
+ }
+ });
+}
+
+function generateUniversalFiltersHtml(filters, existingValues = null) {
+ let existing = {};
+ if (existingValues) {
+ try {
+ existing = JSON.parse(existingValues);
+ } catch (e) {
+ existing = {};
+ }
+ }
+
+ let html = '
';
filters.forEach(filter => {
const fieldId = `filter_${filter.fieldname}`;
- const currentValue = currentValues[filter.fieldname] || filter.default;
+ const currentValue = existing[filter.fieldname] || filter.default || '';
html += `
-
-
${filter.label || filter.fieldname}
+
+
${filter.label || filter.fieldname}
`;
});
@@ -361,332 +300,163 @@ function generateFiltersHtml(filters, currentValues) {
return html;
}
-function initFilterControls(d, currentValues) {
- if (!d.report_filters) {
- frappe.throw("Отсутствует информация о фильтрах отчета");
- return;
+function initUniversalFilterControls(d, filters, existingValues = null) {
+ let existing = {};
+ if (existingValues) {
+ try {
+ existing = JSON.parse(existingValues);
+ } catch (e) {
+ existing = {};
+ }
}
// Инициализируем объект для хранения контролов
if (!d.filter_controls) d.filter_controls = {};
- d.report_filters.forEach(filter => {
- // Скрываем поле company всегда и предустанавливаем $DefaultCompany
- if (filter.fieldname === 'company') {
- // Для скрытых полей просто сохраняем специальное значение
- d.filter_controls[filter.fieldname] = {
- get_value: function() {
- return "$DefaultCompany";
- },
- set_value: function() {}, // пустая функция
- df: {
- fieldname: filter.fieldname,
- hidden: true,
- __is_hidden_field: true
- }
- };
-
- // Обновляем фильтры
- updateFiltersJson(d);
- return;
- }
-
+ filters.forEach(filter => {
const fieldId = `filter_${filter.fieldname}`;
- // Используем jQuery для поиска элемента
const $container = $(document.getElementById(fieldId));
if (!$container.length) {
- frappe.show_alert({
- message: `Не найден контейнер для фильтра ${filter.fieldname}`,
- indicator: 'orange'
- });
return;
}
- const currentValue = currentValues[filter.fieldname] !== undefined ?
- currentValues[filter.fieldname] :
- filter.default;
-
- // Проверяем, есть ли оригинальное значение для даты
- const originalKey = `__original_${filter.fieldname}`;
- const hasOriginalValue = currentValues[originalKey] !== undefined;
- const originalValue = hasOriginalValue ? currentValues[originalKey] : null;
+ const currentValue = existing[filter.fieldname] || filter.default;
// Создаем контрол в зависимости от типа поля
let control;
- switch(filter.fieldtype) {
- case 'Link':
- control = new frappe.ui.form.ControlLink({
- parent: $container,
- df: {
- fieldname: filter.fieldname,
- label: filter.label || filter.fieldname,
- fieldtype: 'Link',
- options: filter.options,
- reqd: filter.reqd || 0,
- default: currentValue
- },
- render_input: true
- });
- break;
-
- case 'Select':
- control = new frappe.ui.form.ControlSelect({
- parent: $container,
- df: {
- fieldname: filter.fieldname,
- label: filter.label || filter.fieldname,
- fieldtype: 'Select',
- options: filter.options,
- reqd: filter.reqd || 0,
- default: currentValue
- },
- render_input: true
- });
- break;
-
+ try {
+ switch(filter.fieldtype) {
+ case 'Link':
+ control = new frappe.ui.form.ControlLink({
+ parent: $container,
+ df: {
+ fieldname: filter.fieldname,
+ label: filter.label || filter.fieldname,
+ fieldtype: 'Link',
+ options: filter.options,
+ reqd: filter.reqd || 0,
+ default: currentValue
+ },
+ render_input: true
+ });
+ break;
+
+ case 'Select':
+ control = new frappe.ui.form.ControlSelect({
+ parent: $container,
+ df: {
+ fieldname: filter.fieldname,
+ label: filter.label || filter.fieldname,
+ fieldtype: 'Select',
+ options: filter.options,
+ reqd: filter.reqd || 0,
+ default: currentValue
+ },
+ render_input: true
+ });
+ break;
+
case 'Date':
- // Получаем target_doctype из основной формы
- const target_doctype = cur_frm.doc.target_doctype;
-
- if (target_doctype) {
- // Загружаем поля целевого DocType
- frappe.model.with_doctype(target_doctype, function() {
- const targetMeta = frappe.get_meta(target_doctype);
- if (targetMeta) {
- // Фильтруем только поля типа Date и специальные поля ay/il/rüb
- const targetDateFields = targetMeta.fields.filter(field =>
- field.fieldtype === 'Date' ||
- field.fieldname === 'ay' ||
- field.fieldname === 'il' ||
- field.fieldname === 'rüb'
- );
-
- // Проверяем, есть ли специальные поля ay, il и rüb одновременно
- const hasAyField = targetDateFields.some(field => field.fieldname === 'ay');
- const hasIlField = targetDateFields.some(field => field.fieldname === 'il');
- const hasRubField = targetDateFields.some(field => field.fieldname === 'rüb');
-
- if (targetDateFields.length > 0) {
- // Создаем селект с опциями из полей с датой
- let selectHtml = '
';
-
- $container.html(selectHtml);
-
- // Добавляем обработчик для опции "Указать дату..."
- $container.find('.filter-value').on('change', function() {
- const selectedValue = $(this).val();
-
- if (selectedValue === 'custom') {
- $container.html('
');
-
- // Добавляем обработчик для нового поля даты
- $container.find('.filter-value').on('change', function() {
- updateFiltersJson(d);
- });
- }
- else if (selectedValue === "$AyIlSpecial") {
- // Помечаем в data-атрибуте, что это специальное поле ay
- $(this).attr('data-is-special-ay-field', 'true');
- d.set_value('has_special_dates', 1);
- updateFiltersJson(d);
- }
- else if (selectedValue === "$RubIlSpecial") {
- // Помечаем в data-атрибуте, что это специальное поле rüb
- $(this).attr('data-is-special-rub-field', 'true');
- d.set_value('has_special_dates', 1);
- updateFiltersJson(d);
- }
- else {
- updateFiltersJson(d);
- }
- });
-
- // Создаем контрол на основе Select
- control = {
- get_value: function() {
- return $container.find('.filter-value').val() || "";
- },
- set_value: function(v) {
- const $select = $container.find('.filter-value');
-
- // Если в селекте есть опция с таким значением, выбираем ее
- if ($select.find(`option[value="${v}"]`).length > 0) {
- $select.val(v);
- }
- // Иначе, если это обычная дата, переключаемся на пользовательский ввод
- else if (v && v !== "$AyIlSpecial" && !v.startsWith("doc.")) {
- $select.val('custom').trigger('change');
- setTimeout(() => {
- $container.find('.filter-value').val(v);
- }, 100);
- }
- },
- df: {
- fieldname: filter.fieldname,
- fieldtype: 'Select'
- }
- };
-
- // Сохраняем контрол
- d.filter_controls[filter.fieldname] = control;
-
- // Если имеется оригинальное значение (например, $AyIlSpecial)
- if (hasOriginalValue) {
- // Сразу устанавливаем значение специального маркера
- $container.find('.filter-value').val(originalValue).trigger('change');
- d.set_value('has_special_dates', 1);
- } else {
- // Иначе используем обычное значение
- if (typeof currentValue === 'string' && currentValue.startsWith('doc.')) {
- $container.find('.filter-value').val(currentValue).trigger('change');
- }
- }
-
- // Добавляем событие изменения для обновления JSON
- $container.find('.filter-value').on('change', function() {
- updateFiltersJson(d);
- });
- } else {
- createDefaultDateControl();
- }
- } else {
- createDefaultDateControl();
- }
+ control = new frappe.ui.form.ControlDate({
+ parent: $container,
+ df: {
+ fieldname: filter.fieldname,
+ label: filter.label || filter.fieldname,
+ fieldtype: 'Date',
+ reqd: filter.reqd || 0,
+ default: currentValue || frappe.datetime.get_today()
+ },
+ render_input: true
});
- } else {
- createDefaultDateControl();
- }
-
- // Функция для создания стандартного контрола даты
- function createDefaultDateControl() {
- try {
- control = new frappe.ui.form.ControlDate({
- parent: $container,
- df: {
- fieldname: filter.fieldname,
- label: filter.label || filter.fieldname,
- fieldtype: 'Date',
- reqd: filter.reqd || 0,
- default: currentValue
- },
- render_input: true
- });
-
- // Сохраняем контрол
- d.filter_controls[filter.fieldname] = control;
- } catch (e) {
- $container.html('
');
-
- if (currentValue && !hasOriginalValue) {
- $container.find('.filter-value').val(currentValue);
- }
-
- control = {
- get_value: function() {
- return $container.find('.filter-value').val();
- },
- set_value: function(v) {
- $container.find('.filter-value').val(v);
- },
- df: {
- fieldname: filter.fieldname,
- fieldtype: 'Date'
- }
- };
-
- // Сохраняем контрол
- d.filter_controls[filter.fieldname] = control;
- }
+ break;
- // Обработчик изменения для обновления JSON
- $container.find('input').on('change', function() {
- updateFiltersJson(d);
+ case 'Datetime':
+ control = new frappe.ui.form.ControlDatetime({
+ parent: $container,
+ df: {
+ fieldname: filter.fieldname,
+ label: filter.label || filter.fieldname,
+ fieldtype: 'Datetime',
+ reqd: filter.reqd || 0,
+ default: currentValue
+ },
+ render_input: true
});
- }
- break;
-
- case 'Datetime':
- control = new frappe.ui.form.ControlDatetime({
- parent: $container,
- df: {
- fieldname: filter.fieldname,
- label: filter.label || filter.fieldname,
- fieldtype: 'Datetime',
- reqd: filter.reqd || 0,
- default: currentValue
- },
- render_input: true
- });
- break;
-
- case 'Check':
- control = new frappe.ui.form.ControlCheck({
- parent: $container,
- df: {
- fieldname: filter.fieldname,
- label: filter.label || filter.fieldname,
- fieldtype: 'Check',
- reqd: filter.reqd || 0,
- default: currentValue
- },
- render_input: true
- });
- break;
-
- default:
- control = new frappe.ui.form.ControlData({
- parent: $container,
- df: {
- fieldname: filter.fieldname,
- label: filter.label || filter.fieldname,
- fieldtype: 'Data',
- reqd: filter.reqd || 0,
- default: currentValue
- },
- render_input: true
- });
- }
-
- // Обработчик изменения значения фильтра (для не-дата полей)
- if (control && filter.fieldtype !== 'Date') {
- control.df.change = function() {
- updateFiltersJson(d);
- };
+ break;
+
+ case 'Check':
+ control = new frappe.ui.form.ControlCheck({
+ parent: $container,
+ df: {
+ fieldname: filter.fieldname,
+ label: filter.label || filter.fieldname,
+ fieldtype: 'Check',
+ reqd: filter.reqd || 0,
+ default: currentValue
+ },
+ render_input: true
+ });
+ break;
+
+ case 'Int':
+ control = new frappe.ui.form.ControlInt({
+ parent: $container,
+ df: {
+ fieldname: filter.fieldname,
+ label: filter.label || filter.fieldname,
+ fieldtype: 'Int',
+ reqd: filter.reqd || 0,
+ default: currentValue
+ },
+ render_input: true
+ });
+ break;
+
+ case 'Float':
+ case 'Currency':
+ control = new frappe.ui.form.ControlFloat({
+ parent: $container,
+ df: {
+ fieldname: filter.fieldname,
+ label: filter.label || filter.fieldname,
+ fieldtype: filter.fieldtype,
+ reqd: filter.reqd || 0,
+ default: currentValue
+ },
+ render_input: true
+ });
+ break;
+
+ default:
+ control = new frappe.ui.form.ControlData({
+ parent: $container,
+ df: {
+ fieldname: filter.fieldname,
+ label: filter.label || filter.fieldname,
+ fieldtype: 'Data',
+ reqd: filter.reqd || 0,
+ default: currentValue
+ },
+ render_input: true
+ });
+ }
- // Сохраняем контрол
- d.filter_controls[filter.fieldname] = control;
+ // Добавляем обработчик изменения
+ if (control) {
+ control.df.change = function() {
+ updateFiltersJson(d);
+ };
+
+ // Сохраняем контрол
+ d.filter_controls[filter.fieldname] = control;
+ }
+
+ } catch (e) {
+ console.warn(`Failed to create control for ${filter.fieldname}:`, e);
+ // Fallback: создаем простой input
+ createFallbackControl($container, filter, currentValue, d);
}
});
@@ -694,205 +464,101 @@ function initFilterControls(d, currentValues) {
updateFiltersJson(d);
}
-function loadReportFields(d, reportName) {
- // Сначала проверяем, все ли обязательные фильтры заполнены
- const filtersJson = d.get_value('filters_json') || '{}';
- let filters = {};
-
- try {
- filters = JSON.parse(filtersJson);
- } catch (e) {
- frappe.throw(`Ошибка при разборе JSON фильтров: ${e.message}`);
- return;
+function createFallbackControl($container, filter, currentValue, d) {
+ let inputType = 'text';
+ switch(filter.fieldtype) {
+ case 'Date':
+ inputType = 'date';
+ break;
+ case 'Int':
+ case 'Float':
+ case 'Currency':
+ inputType = 'number';
+ break;
+ case 'Check':
+ inputType = 'checkbox';
+ break;
}
- // Показываем какие фильтры мы пытаемся использовать
- frappe.show_alert({
- message: `Загружаем поля отчета '${reportName}' с фильтрами: ${JSON.stringify(filters)}`,
- indicator: 'blue'
+ const checked = filter.fieldtype === 'Check' && currentValue ? 'checked' : '';
+ const value = filter.fieldtype === 'Check' ? '' : `value="${currentValue || ''}"`;
+
+ $container.html(`
`);
+
+ const control = {
+ get_value: function() {
+ if (filter.fieldtype === 'Check') {
+ return $container.find('.filter-value').is(':checked') ? 1 : 0;
+ }
+ return $container.find('.filter-value').val();
+ },
+ set_value: function(v) {
+ if (filter.fieldtype === 'Check') {
+ $container.find('.filter-value').prop('checked', v);
+ } else {
+ $container.find('.filter-value').val(v);
+ }
+ },
+ df: {
+ fieldname: filter.fieldname,
+ fieldtype: filter.fieldtype
+ }
+ };
+
+ d.filter_controls[filter.fieldname] = control;
+
+ // Добавляем обработчик изменения
+ $container.find('.filter-value').on('change', function() {
+ updateFiltersJson(d);
});
-
- // Загружаем информацию о фильтрах отчета, если ее еще нет
- if (!d.report_filters) {
- frappe.call({
- method: "formula_editor.api.get_report_filters_dynamic",
- args: {
- report_name: reportName
- },
- callback: function(r) {
- if (r.message) {
- d.report_filters = r.message;
-
- // Проверяем обязательные фильтры
- checkRequiredFilters();
- } else {
- // Если нет фильтров, пробуем загрузить поля
- frappe.show_alert({
- message: `Для отчета '${reportName}' не найдены фильтры. Пробуем загрузить поля напрямую.`,
- indicator: 'orange'
- });
- fetchReportFields();
- }
- }
- });
- } else {
- // Если информация о фильтрах уже есть, проверяем обязательные фильтры
- checkRequiredFilters();
- }
-
- // Функция для проверки обязательных фильтров
- function checkRequiredFilters() {
- // Получаем список обязательных фильтров
- const requiredFilters = d.report_filters.filter(f => f.reqd);
-
- // Показываем список обязательных фильтров
- if (requiredFilters.length > 0) {
- const requiredFilterNames = requiredFilters.map(f => f.label || f.fieldname).join(", ");
- frappe.show_alert({
- message: `Для отчета требуются обязательные фильтры: ${requiredFilterNames}`,
- indicator: 'blue'
- });
- }
-
- // Проверяем, какие обязательные фильтры отсутствуют
- const missingRequiredFilters = requiredFilters.filter(f => {
- // Пропускаем скрытые поля, которые заполняются автоматически
- if (f.hidden) return false;
-
- const isMissing = !(f.fieldname in filters) ||
- filters[f.fieldname] === undefined ||
- filters[f.fieldname] === null ||
- filters[f.fieldname] === "";
- return isMissing;
- });
+}
- // Если есть отсутствующие обязательные фильтры даты, пробуем заполнить их значениями по умолчанию
- const missingDateFilters = missingRequiredFilters.filter(f =>
- f.fieldtype === 'Date' && (
- f.fieldname.toLowerCase().includes('date') ||
- f.fieldname.toLowerCase().includes('from') ||
- f.fieldname.toLowerCase().includes('to')
- )
- );
-
- if (missingDateFilters.length > 0) {
- // Заполняем значения по умолчанию для дат
- const today = frappe.datetime.get_today();
- const lastMonth = frappe.datetime.add_months(today, -1);
-
- missingDateFilters.forEach(filter => {
- // Определяем, какой тип фильтра даты
- const isFromFilter = filter.fieldname.toLowerCase().includes('from');
- const isToFilter = filter.fieldname.toLowerCase().includes('to');
-
- // Устанавливаем значения по умолчанию
- if (isFromFilter) {
- filters[filter.fieldname] = lastMonth;
- } else if (isToFilter || filter.fieldname.toLowerCase().includes('date')) {
- filters[filter.fieldname] = today;
- }
-
- // Обновляем контрол фильтра, если он существует
- if (d.filter_controls && d.filter_controls[filter.fieldname]) {
- try {
- d.filter_controls[filter.fieldname].set_value(filters[filter.fieldname]);
- } catch(e) {
- frappe.show_alert({
- message: `Ошибка при обновлении контрола фильтра ${filter.fieldname}: ${e.message}`,
- indicator: 'red'
- });
- }
- }
- });
-
- // Обновляем JSON фильтров
- d.set_value('filters_json', JSON.stringify(filters, null, 2));
-
- // Показываем уведомление о заполнении фильтров
- frappe.show_alert({
- message: `Добавлены значения по умолчанию для обязательных фильтров даты: ${missingDateFilters.map(f => f.fieldname).join(", ")}`,
- indicator: 'blue'
- });
- }
-
- // Проверяем оставшиеся обязательные фильтры после автозаполнения дат
- const remainingMissingFilters = missingRequiredFilters.filter(f => {
- // Проверяем, не был ли фильтр заполнен автоматически
- const isMissing = !(f.fieldname in filters) ||
- filters[f.fieldname] === undefined ||
- filters[f.fieldname] === null ||
- filters[f.fieldname] === "";
- return isMissing;
- });
-
- if (remainingMissingFilters.length > 0) {
- // Если остались обязательные фильтры, показываем предупреждение
- const missingFieldNames = remainingMissingFilters.map(f => f.label || f.fieldname).join(", ");
-
- frappe.throw(`Для отчета "${reportName}" необходимо заполнить следующие обязательные фильтры: ${missingFieldNames}`);
-
- // Очищаем опции селекта полей
- d.set_df_property('field_name', 'options', []);
- d.fields_dict.field_name.refresh();
- } else {
- // Если все обязательные фильтры заполнены, загружаем поля отчета
- fetchReportFields();
- }
- }
+function collectFiltersFromControls(d) {
+ const values = {};
- // Функция для загрузки полей отчета
- function fetchReportFields() {
- frappe.show_alert({
- message: `Запрашиваем поля отчета с фильтрами: ${JSON.stringify(filters)}`,
- indicator: 'blue'
- });
-
- frappe.call({
- method: "formula_editor.api.get_report_fields",
- args: {
- report_name: reportName,
- filters: filters
- },
- callback: function(r) {
- if (r.exc) {
- // Если произошла ошибка на сервере
- frappe.throw(`Ошибка при загрузке полей отчета: ${r.exc}`);
- return;
- }
-
- if (r.message && r.message.length) {
- const fields = r.message.map(field => ({
- label: field.label || field.fieldname,
- value: field.fieldname
- }));
-
- frappe.show_alert({
- message: `Найдено ${fields.length} полей отчета`,
- indicator: 'green'
- });
-
- // Обновляем опции селекта
- d.set_df_property('field_name', 'options', fields);
- d.fields_dict.field_name.refresh();
-
- // Если есть сохраненное значение, выбираем его
- if (d.get_value('field_name')) {
- d.set_value('field_name', d.get_value('field_name'));
+ if (d.filter_controls) {
+ Object.keys(d.filter_controls).forEach(fieldname => {
+ const ctrl = d.filter_controls[fieldname];
+ if (ctrl) {
+ try {
+ const value = ctrl.get_value();
+ if (value !== undefined && value !== null && value !== '') {
+ values[fieldname] = value;
}
- } else {
- // Если полей нет, очищаем селект
- d.set_df_property('field_name', 'options', []);
- d.fields_dict.field_name.refresh();
-
- frappe.throw(`В отчете '${reportName}' не найдено подходящих числовых полей. Пожалуйста, выберите другой отчет.`);
+ } catch (e) {
+ console.warn(`Error getting value for filter ${fieldname}:`, e);
}
}
});
}
+
+ return JSON.stringify(values);
+}
+
+function updateFiltersJson(d) {
+ try {
+ const filtersJson = collectFiltersFromControls(d);
+ d.set_value('filters_json', filtersJson);
+ } catch (e) {
+ console.error('Error updating filters JSON:', e);
+ }
+}
+
+function clearFiltersSection(d) {
+ d.set_df_property('filters_section', 'hidden', 1);
+ d.set_df_property('filters_helper', 'hidden', 1);
+ if (d.fields_dict.filters_container) {
+ d.fields_dict.filters_container.$wrapper.html('');
+ }
+ d.filter_controls = {};
+}
+
+function clearFieldsSection(d) {
+ d.set_df_property('field_name', 'options', []);
+ d.fields_dict.field_name.refresh();
}
function saveReportField(frm, values, fieldId = null) {
- // Используем имя поля в качестве идентификатора или указанный ID
const newFieldId = fieldId || frappe.utils.get_random(10);
try {
@@ -912,24 +578,6 @@ function saveReportField(frm, values, fieldId = null) {
return;
}
- // Обработка JSON фильтров
- let filters = {};
- try {
- // Парсим JSON фильтров
- filters = JSON.parse(values.filters_json || "{}");
-
- // Удаляем служебные поля
- Object.keys(filters).forEach(key => {
- if (key.startsWith('__')) {
- delete filters[key];
- }
- });
-
- // Обновляем JSON фильтров
- values.filters_json = JSON.stringify(filters);
- } catch (e) {
- }
-
// Сохраняем информацию о поле отчета
reportFields[newFieldId] = {
name: values.name,
@@ -975,7 +623,8 @@ function renderReportFields(frm) {
@@ -995,26 +644,33 @@ function renderReportFields(frm) {
});
// Инициализируем drag-n-drop для полей отчетов
- $container.find('.report-field-btn').each(function() {
- $(this).attr('draggable', 'true');
-
- $(this).off('dragstart').on('dragstart', function(e) {
- const fieldId = $(this).data('field-id');
- const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
- const field = reportFields[fieldId];
-
- e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
- type: 'report-field',
- fieldId: fieldId,
- name: field.name
- }));
- });
- });
+ initReportFieldsDragDrop(frm);
+
} catch (error) {
$container.append('
Ошибка при загрузке полей отчетов.
');
}
}
+function initReportFieldsDragDrop(frm) {
+ const $container = $(frm.wrapper).find('.report-fields-list');
+
+ $container.find('.report-field-btn').each(function() {
+ $(this).attr('draggable', 'true');
+
+ $(this).off('dragstart').on('dragstart', function(e) {
+ const fieldId = $(this).data('field-id');
+ const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
+ const field = reportFields[fieldId];
+
+ e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
+ type: 'report-field',
+ fieldId: fieldId,
+ name: field.name
+ }));
+ });
+ });
+}
+
function editReportField(frm, fieldId) {
try {
const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
@@ -1025,54 +681,8 @@ function editReportField(frm, fieldId) {
return;
}
- // Важно: создаем копию поля, чтобы не изменять оригинал в хранилище
- const fieldCopy = {...field};
-
- // Обрабатываем фильтры перед открытием диалога
- if (fieldCopy.filters_json) {
- try {
- let filters = JSON.parse(fieldCopy.filters_json);
- const processedFilters = {};
-
- // Проходим по всем фильтрам и обрабатываем специальные значения
- Object.keys(filters).forEach(key => {
- let value = filters[key];
-
- // Обработка дат для месяца (ay)
- if (key.toLowerCase().includes('date') && value === "$AyIlSpecial") {
- // Заменяем специальное значение на обычную дату для временного использования
- // Потом мы восстановим специальное значение при сохранении
- processedFilters["__original_" + key] = "$AyIlSpecial"; // Сохраняем оригинальное значение
- processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата
- }
- // Обработка дат для квартала (rüb)
- else if (key.toLowerCase().includes('date') && value === "$RubIlSpecial") {
- // Аналогично сохраняем оригинальное значение для квартала
- processedFilters["__original_" + key] = "$RubIlSpecial"; // Сохраняем оригинальное значение
- processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата
- }
- // Обработка ссылок на поля документа
- else if (typeof value === 'string' && value.startsWith("doc.") &&
- (key.toLowerCase().includes('date') || key.toLowerCase().includes('from') || key.toLowerCase().includes('to'))) {
- // Обрабатываем также ссылки на поля документа
- processedFilters["__original_" + key] = value; // Сохраняем оригинальное значение
- processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата
- }
- // Обычные значения без обработки
- else {
- processedFilters[key] = value;
- }
- });
-
- // Обновляем JSON фильтров в копии
- fieldCopy.filters_json = JSON.stringify(processedFilters);
- fieldCopy.__has_special_dates = true; // Маркер для последующего восстановления
- } catch (e) {
- }
- }
-
- // Открываем модальное окно с обработанной копией поля
- openReportFieldModal(frm, fieldId, fieldCopy);
+ // Открываем модальное окно с данными поля
+ openReportFieldModal(frm, fieldId, field);
} catch (error) {
frappe.msgprint({
title: __('Ошибка'),
@@ -1110,195 +720,181 @@ function deleteReportField(frm, fieldId) {
}
function generateReportFieldsFunction(report_fields) {
-
+ if (!report_fields || Object.keys(report_fields).length === 0) {
+ return "";
+ }
- return `
- window.notifiedAboutAyIl = false;
- window.notifiedAboutRubIl = false;
-async function getReportFieldValue(frm, reportName, filters, fieldName, aggregation = 'sum') {
+ 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 {
-
- // Проверка необходимости полей сразу для всех фильтров
- if (Object.values(filters).some(v => v === "$AyIlSpecial")) {
- if (!frm.doc["ay"] || !frm.doc["il"]) {
- if (!notifiedAboutAyIl) {
- frappe.msgprint({
- title: "Not enough data",
- message: "Please fill in the
ay and
il fields",
- indicator: "orange"
- });
- notifiedAboutAyIl = true;
- }
- return 0;
- }
- }
-
- // Проверка: если используется $RubIlSpecial, но rüb или il не заполнены — возвращаем 0
- if (Object.values(filters).some(v => v === "$RubIlSpecial")) {
- if (!frm.doc["rüb"] || !frm.doc["il"]) {
- if (!notifiedAboutRubIl) {
- frappe.msgprint({
- title: "Not enough data",
- message: "Please fill in the
rüb and
il fields",
- indicator: "orange"
- });
- notifiedAboutRubIl = true;
- }
- return 0;
- }
- }
-
// Обрабатываем фильтры с возможными ссылками на поля документа
const processedFilters = {};
- // Используем for...in loop для перебора свойств объекта filters
for (const key in filters) {
let value = filters[key];
- // Специальная обработка для $DefaultCompany
- if (value === "$DefaultCompany") {
- processedFilters[key] = frm.doc.company || frappe.defaults.get_user_default("Company") || "";
- continue;
- }
-
- // Специальная обработка для $AyIlSpecial
- if (value === "$AyIlSpecial") {
- // Проверяем, есть ли у документа поля ay и il
- if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) {
- // Обрабатываем специальные поля ay и il для получения диапазона дат
- const dateRange = processAzerbayjanDateField({
- ay: frm.doc["ay"],
- il: frm.doc["il"]
- });
-
- // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия
- if (dateRange) {
- if (key.includes('from_date')) {
- // Для начальной даты используем только >=
- processedFilters[key] = dateRange.start;
- } else if (key.includes('to_date')) {
- // Для конечной даты используем только <=
- processedFilters[key] = dateRange.end;
- } else {
- // Для других полей даты используем начало периода
- processedFilters[key] = dateRange.start;
- }
- continue;
- }
- }
- }
-
- // Специальная обработка для $RubIlSpecial
- if (value === "$RubIlSpecial") {
- // Проверяем, есть ли у документа поля rüb и il
- if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) {
- // Обрабатываем специальные поля rüb и il для получения диапазона дат
- const dateRange = processQuarterDateField({
- rüb: frm.doc["rüb"],
- il: frm.doc["il"]
- });
-
- // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия
- if (dateRange) {
- if (key.includes('from_date')) {
- // Для начальной даты используем только >=
- processedFilters[key] = dateRange.start;
- } else if (key.includes('to_date')) {
- // Для конечной даты используем только <=
- processedFilters[key] = dateRange.end;
- } else {
- // Для других полей даты используем начало периода
- processedFilters[key] = dateRange.start;
- }
- continue;
- }
- }
- }
-
// Проверяем, является ли значение ссылкой на поле документа
if (typeof value === 'string' && value.startsWith('doc.')) {
- const docField = value.substring(4); // Убираем 'doc.'
-
- // Проверяем, является ли это специальным полем ay или il
- if (docField === 'ay' || docField === 'il') {
- // Получаем значения ay и il из документа
- if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) {
- // Обрабатываем специальные поля ay и il для получения диапазона дат
- const dateRange = processAzerbayjanDateField({
- ay: frm.doc["ay"],
- il: frm.doc["il"]
- });
-
- // Если удалось получить диапазон дат, создаем правильные фильтры
- if (dateRange) {
- if (key.includes('from_date')) {
- // Для начальной даты используем только >=
- processedFilters[key] = dateRange.start;
- } else if (key.includes('to_date')) {
- // Для конечной даты используем только <=
- processedFilters[key] = dateRange.end;
- } else {
- // Для других полей даты используем начало периода
- processedFilters[key] = dateRange.start;
- }
- continue;
- }
- }
- }
- // Проверяем, является ли это специальным полем rüb
- else if (docField === 'rüb') {
- // Получаем значения rüb и il из документа
- if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) {
- // Обрабатываем специальные поля rüb и il для получения диапазона дат
- const dateRange = processQuarterDateField({
- rüb: frm.doc["rüb"],
- il: frm.doc["il"]
- });
-
- // Если удалось получить диапазон дат, создаем правильные фильтры
- if (dateRange) {
- if (key.includes('from_date')) {
- // Для начальной даты используем только >=
- processedFilters[key] = dateRange.start;
- } else if (key.includes('to_date')) {
- // Для конечной даты используем только <=
- processedFilters[key] = dateRange.end;
- } else {
- // Для других полей даты используем начало периода
- processedFilters[key] = dateRange.start;
- }
- continue;
- }
- }
- }
- // Для других полей документа
- else if (frm.doc[docField] !== undefined) {
- // Берем значение из документа
+ const docField = value.substring(4);
+ if (frm.doc[docField] !== undefined) {
processedFilters[key] = frm.doc[docField];
continue;
- } else {
- // Если поле не найдено, пропускаем фильтр
- 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}\`;
-
- // Если есть в кеше и кеш не устарел (5 минут)
+ 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",
@@ -1307,16 +903,15 @@ async function getReportFieldValue(frm, reportName, filters, fieldName, aggregat
filters: processedFilters,
field_name: fieldName
},
- freeze: false // Не блокируем интерфейс
+ freeze: false
});
- // Проверяем результат
- if (result && result.message) {
+ 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;
@@ -1325,66 +920,65 @@ async function getReportFieldValue(frm, reportName, filters, fieldName, aggregat
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 updateFiltersJson(d) {
- try {
- // Собираем все значения фильтров
- const values = {};
- Object.keys(d.filter_controls || {}).forEach(fieldname => {
- const ctrl = d.filter_controls[fieldname];
- if (ctrl) {
- try {
- const value = ctrl.get_value();
- if (value !== undefined && value !== null && value !== '') {
- // Сохраняем все значения как есть
- values[fieldname] = value;
-
- // Проверка на специальные значения дат
- if ((value === "$AyIlSpecial" || value === "$RubIlSpecial" ||
- (typeof value === 'string' && value.startsWith("doc."))) &&
- (fieldname.toLowerCase().includes('date') ||
- fieldname.toLowerCase().includes('from') ||
- fieldname.toLowerCase().includes('to'))) {
-
- // Сохраняем оригинальное значение отдельно
- values[`__original_${fieldname}`] = value;
-
- // Временно заменяем значение на валидную дату для проверок
- values[fieldname] = frappe.datetime.get_today();
-
- // Отмечаем, что у нас есть специальные даты
- d.set_value('has_special_dates', 1);
- }
- }
- } catch (e) {
- frappe.show_alert({
- message: `Ошибка при получении значения фильтра ${fieldname}: ${e.message}`,
- indicator: 'red'
- });
- }
- }
- });
-
- // Обновляем JSON фильтров
- if (Object.keys(values).length > 0) {
- d.set_value('filters_json', JSON.stringify(values, null, 2));
- }
- } catch (e) {
- frappe.show_alert({
- message: `Ошибка при обновлении фильтров: ${e.message}`,
- indicator: 'red'
- });
+// Функция для применения агрегации к данным отчета
+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
+ };
}
\ No newline at end of file
diff --git a/formula_editor/public/js/formula_editor_table_rows.js b/formula_editor/public/js/formula_editor_table_rows.js
index 9c821c9..f98373e 100644
--- a/formula_editor/public/js/formula_editor_table_rows.js
+++ b/formula_editor/public/js/formula_editor_table_rows.js
@@ -1169,7 +1169,7 @@ function generateTableRowFormulasHandlers(table_row_formulas, functions, target_
functions.push(initFunction);
- // ИСПРАВЛЕННАЯ: Создаем функцию для расчета значения поля строки таблицы
+ // Создаем функцию для расчета значения поля строки таблицы
let calculateFunction = `
// Функция для расчета значения поля в строке табличной части
async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, formula, triggerField) {
@@ -1183,98 +1183,60 @@ async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, fo
const cdt = frm.fields_dict[tableName].grid.doctype;
const cdn = row.name;
- // === Обработка report() ===
- let processedFormula = formula;
- if (formula.includes("report(")) {
- // Регулярное выражение для поиска вызовов report()
- const reportRegex = /report\\\\(([^)]+)\\\\)/g;
- let match;
- 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}__\`;
-
- // Ищем данные отчета в хранилище или используем предопределенный отчет
- let reportName, reportFilters, reportField, reportAggregation;
-
- if (fieldName === "testt") {
- reportName = "General Ledger";
- reportFilters = {"company":"$DefaultCompany","from_date":"$AyIlSpecial","to_date":"$AyIlSpecial"};
- reportField = "debit";
- reportAggregation = "sum";
- } else {
- // Ищем в хранилище полей отчетов
- const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
- const reportObj = Object.values(reportFields).find(f => f.name === fieldName);
-
- if (reportObj) {
- reportName = reportObj.report;
- reportFilters = JSON.parse(reportObj.filters_json || "{}");
- reportField = reportObj.field_name;
- reportAggregation = reportObj.aggregation || 'sum';
- } else {
- processedFormula = processedFormula.replace(fullMatch, "0");
- continue;
- }
- }
-
- // Добавляем промис запроса к отчету
- reportPromises.push(
- getReportFieldValue(
- frm,
- reportName,
- reportFilters,
- reportField,
- reportAggregation
- )
- );
-
- // Запоминаем соответствие плейсхолдера и вызова
- 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
- );
- }
- }
- }
-
- // === ИСПРАВЛЕНИЕ: Используем transform_expression для обработки всех функций включая table() ===
- let finalFormula = processedFormula;
+ // === ЕДИНАЯ ТОЧКА ОБРАБОТКИ ВСЕХ ФУНКЦИЙ ===
+ let finalFormula, reportCalls;
try {
- // Сначала применяем transform_expression для обработки table() и других вызовов
- const transformResult = transform_expression(processedFormula, true, tableName);
- if (transformResult && transformResult.transformed) {
+ // Используем 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 = [];
}
- // Вычисление выражения с доступом к объекту row
+ // === ОБРАБОТКА АСИНХРОННЫХ ВЫЗОВОВ ОТЧЕТОВ ===
+ 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
@@ -1298,7 +1260,6 @@ async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, fo
}
}`;
-
functions.push(calculateFunction);
// Функция для получения индекса строки таблицы