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) {